package tempo import ( "context" "encoding/json" "errors" "io" "reflect" "strings" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/tempo/kinds/dataquery" "github.com/grafana/tempo/pkg/tempopb" "google.golang.org/grpc/metadata" ) func TestProcessStream_ValidInput_ReturnsNoError(t *testing.T) { logger := backend.NewLoggerWith("logger", "tsdb.tempo.test") service := &Service{ logger: logger, } searchClient := &mockStreamer{} streamSender := &mockSender{} err := service.processStream(context.Background(), searchClient, streamSender) if err != nil { t.Errorf("Expected no error, but got %s", err) } } func TestProcessStream_InvalidInput_ReturnsError(t *testing.T) { logger := backend.NewLoggerWith("logger", "tsdb.tempo.test") service := &Service{ logger: logger, } searchClient := &mockStreamer{err: errors.New("invalid input")} streamSender := &mockSender{} err := service.processStream(context.Background(), searchClient, streamSender) if err != nil { if !strings.Contains(err.Error(), "invalid input") { t.Errorf("Expected error message to contain 'invalid input', but got %s", err) } } } func TestProcessStream_ValidInput_ReturnsExpectedOutput(t *testing.T) { logger := backend.NewLoggerWith("logger", "tsdb.tempo.test") service := &Service{ logger: logger, } searchClient := &mockStreamer{ tracingMetadata: []*tempopb.TraceSearchMetadata{ {TraceID: "abcdefg", StartTimeUnixNano: 1234}, {TraceID: "hijklmn", StartTimeUnixNano: 5678}, }, metrics: &tempopb.SearchMetrics{ CompletedJobs: 2, TotalJobs: 5, InspectedBytes: 123456789, TotalBlockBytes: 987654321, InspectedTraces: 123, }, expectedResponses: []ExtendedResponse{ { SearchResponse: &tempopb.SearchResponse{ Traces: []*tempopb.TraceSearchMetadata{ {TraceID: "abcdefg", StartTimeUnixNano: 1234}, }, Metrics: &tempopb.SearchMetrics{ CompletedJobs: 2, TotalJobs: 5, InspectedBytes: 123456789, TotalBlockBytes: 987654321, InspectedTraces: 123, }, }, State: dataquery.SearchStreamingStateStreaming, }, { SearchResponse: &tempopb.SearchResponse{ Traces: []*tempopb.TraceSearchMetadata{ {TraceID: "abcdefg", StartTimeUnixNano: 1234}, {TraceID: "hijklmn", StartTimeUnixNano: 5678}, }, Metrics: &tempopb.SearchMetrics{ CompletedJobs: 2, TotalJobs: 5, InspectedBytes: 123456789, TotalBlockBytes: 987654321, InspectedTraces: 123, }, }, State: dataquery.SearchStreamingStateStreaming, }, { SearchResponse: &tempopb.SearchResponse{ Traces: []*tempopb.TraceSearchMetadata{ {TraceID: "abcdefg", StartTimeUnixNano: 1234}, {TraceID: "hijklmn", StartTimeUnixNano: 5678}, }, Metrics: &tempopb.SearchMetrics{ CompletedJobs: 2, TotalJobs: 5, InspectedBytes: 123456789, TotalBlockBytes: 987654321, InspectedTraces: 123, }, }, State: dataquery.SearchStreamingStateDone, }, }, } streamSender := &mockSender{} err := service.processStream(context.Background(), searchClient, streamSender) if err != nil { t.Errorf("Expected no error, but got %s", err) return } if len(streamSender.responses) != 3 { t.Errorf("Expected 3 responses, but got %d", len(streamSender.responses)) return } for i, frame := range streamSender.responses { expectedMetrics := searchClient.expectedResponses[i].Metrics expectedTraces := searchClient.expectedResponses[i].Traces expectedState := string(searchClient.expectedResponses[i].State) if len(frame.Fields) != 4 { t.Errorf("Expected 4 fields in data frame, but was '%d'", len(frame.Fields)) return } var traceList []*tempopb.TraceSearchMetadata if err := json.Unmarshal(frame.Fields[0].At(0).(json.RawMessage), &traceList); err != nil { t.Errorf("Error unmarshaling trace list: %s", err) } else { if !reflect.DeepEqual(traceList, expectedTraces) { t.Errorf("Expected response traces to be '%+v', but was '%+v'", expectedTraces, traceList) return } } var metrics *tempopb.SearchMetrics if err := json.Unmarshal(frame.Fields[1].At(0).(json.RawMessage), &metrics); err != nil { t.Errorf("Error unmarshaling metrics: %s", err) } else { if !reflect.DeepEqual(metrics, expectedMetrics) { t.Errorf("Expected response metrics to be '%+v', but was '%+v'", expectedMetrics, metrics) return } } state := frame.Fields[2].At(0).(string) if state != expectedState { t.Errorf("Expected response state to be '%+v', but was '%+v'", expectedState, state) return } frameErr := frame.Fields[3].At(0).(string) if frameErr != "" { t.Errorf("Didn't expect error but got '%+v'", frameErr) return } } } type mockSender struct { backend.StreamSender responses []*data.Frame } func (s *mockSender) SendFrame(frame *data.Frame, include data.FrameInclude) error { s.responses = append(s.responses, frame) return nil } type mockStreamer struct { tracingMetadata []*tempopb.TraceSearchMetadata copyOfTracingMetadata []*tempopb.TraceSearchMetadata metrics *tempopb.SearchMetrics expectedResponses []ExtendedResponse err error } func (m *mockStreamer) Recv() (*tempopb.SearchResponse, error) { if m.err != nil { return nil, m.err } if m.copyOfTracingMetadata == nil { m.copyOfTracingMetadata = make([]*tempopb.TraceSearchMetadata, len(m.tracingMetadata)) copy(m.copyOfTracingMetadata, m.tracingMetadata) } if len(m.copyOfTracingMetadata) == 0 { return &tempopb.SearchResponse{ Metrics: m.metrics, Traces: m.tracingMetadata, }, io.EOF } traceMetadata := m.copyOfTracingMetadata[0] m.copyOfTracingMetadata = m.copyOfTracingMetadata[1:] return &tempopb.SearchResponse{ Metrics: m.metrics, Traces: []*tempopb.TraceSearchMetadata{traceMetadata}, }, nil } func (m *mockStreamer) Header() (metadata.MD, error) { panic("implement me") } func (m *mockStreamer) Trailer() metadata.MD { panic("implement me") } func (m *mockStreamer) CloseSend() error { panic("implement me") } func (m *mockStreamer) Context() context.Context { panic("implement me") } func (m *mockStreamer) SendMsg(a any) error { panic("implement me") } func (m *mockStreamer) RecvMsg(a any) error { panic("implement me") }