package report import ( "testing" "time" "llm-api-benchmark-tool/pkg/stats" "github.com/go-echarts/go-echarts/v2/opts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCalculateHistogram(t *testing.T) { // Helper function to create durations from milliseconds dms := func(ms int) time.Duration { return time.Duration(ms) * time.Millisecond } tests := []struct { name string data []time.Duration numBins int wantCats []string wantDataVals []int // Just check the values isErrorCase bool // Indicates if we expect the "No Data" case }{ { name: "Empty data", data: []time.Duration{}, numBins: 5, wantCats: []string{"No Data"}, wantDataVals: []int{0}, isErrorCase: true, }, { name: "Zero bins", data: []time.Duration{dms(100), dms(200)}, numBins: 0, wantCats: []string{"No Data"}, wantDataVals: []int{0}, isErrorCase: true, }, { name: "Single data point", data: []time.Duration{dms(150)}, numBins: 5, wantCats: []string{"150.00 ms"}, // Special case format wantDataVals: []int{1}, }, { name: "All data points same", data: []time.Duration{dms(100), dms(100), dms(100)}, numBins: 3, wantCats: []string{"100.00 ms"}, wantDataVals: []int{3}, }, { name: "Simple case, 2 bins", data: []time.Duration{dms(50), dms(60), dms(110), dms(120), dms(130)}, // Range: 50 to 130 (80ms) // Bin width: 80 / 2 = 40 // Bins: [50, 90), [90, 130] numBins: 2, wantCats: []string{"[50.00, 90.00) ms", "[90.00, 130.00] ms"}, wantDataVals: []int{2, 3}, }, { name: "Even distribution, 5 bins", data: []time.Duration{dms(10), dms(25), dms(35), dms(45), dms(55), dms(65), dms(75), dms(85), dms(95), dms(100)}, // Range: 10 to 100 (90ms) -> max should be 100? Let's make max 110 for simpler bins // Let's adjust data slightly for easier math: min 10, max 110 // data: {10, 25, 35, 45, 55, 65, 75, 85, 95, 110} // Range: 10 to 110 (100ms) // Bin width: 100 / 5 = 20 // Bins: [10,30), [30,50), [50,70), [70,90), [90,110] numBins: 5, wantCats: []string{"[10.00, 28.00) ms", "[28.00, 46.00) ms", "[46.00, 64.00) ms", "[64.00, 82.00) ms", "[82.00, 100.00] ms"}, wantDataVals: []int{2, 2, 1, 2, 3}, }, { name: "Data clustered at ends", data: []time.Duration{dms(10), dms(15), dms(20), dms(90), dms(95), dms(100)}, // Range: 10 to 100 (90). Width (3 bins): 30. // Bins: [10, 40), [40, 70), [70, 100] numBins: 3, wantCats: []string{"[10.00, 40.00) ms", "[40.00, 70.00) ms", "[70.00, 100.00] ms"}, wantDataVals: []int{3, 0, 3}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gcats, gdata := calculateHistogram(tt.data, tt.numBins) require.Equal(t, len(tt.wantCats), len(gcats), "Number of categories mismatch") assert.Equal(t, tt.wantCats, gcats, "Category labels mismatch") require.Equal(t, len(tt.wantDataVals), len(gdata), "Number of data points mismatch") // Extract values from opts.BarData gotDataVals := make([]int, len(gdata)) for i, item := range gdata { // Check if item.Value is an int or can be converted if val, ok := item.Value.(int); ok { gotDataVals[i] = val } else if valF, ok := item.Value.(float64); ok { gotDataVals[i] = int(valF) // Or handle float differently if needed } else { t.Fatalf("Unexpected data type in BarData: %T", item.Value) } } assert.Equal(t, tt.wantDataVals, gotDataVals, "Data values mismatch") }) } } // Helper to create durations func dms(ms int) time.Duration { return time.Duration(ms) * time.Millisecond } // TestCreateLatencyHistogram checks if the latency histogram chart is created correctly. func TestCreateLatencyHistogram(t *testing.T) { mockStats := stats.FinalStats{ LatencyData: []time.Duration{dms(50), dms(100), dms(110), dms(150), dms(200), dms(210), dms(220)}, } chart := createLatencyHistogram(mockStats) require.NotNil(t, chart, "Chart should not be nil") require.NotNil(t, chart.BaseConfiguration.Title, "Chart title configuration should not be nil") // Check title assert.Equal(t, "Latency Distribution", chart.BaseConfiguration.Title.Title, "Chart title mismatch") // Check series data (basic check - assuming one series with correct name) require.Len(t, chart.MultiSeries, 1, "Expected one series") assert.Equal(t, "Latency", chart.MultiSeries[0].Name, "Series name mismatch") // Optionally, check the number of data points generated by calculateHistogram via the chart // This depends on the numBins used internally by createLatencyHistogram (currently 10) // For the mock data {50, 100, 110, 150, 200, 210, 220}, range=170, 10 bins -> width=17 // Bins: [50, 67), [67, 84), [84, 101), [101, 118), [118, 135), [135, 152), [152, 169), [169, 186), [186, 203), [203, 220] // Counts: 1, 0, 1, 1, 0, 1, 0, 0, 1, 2 -> Total 7 points, 10 bars seriesData, ok := chart.MultiSeries[0].Data.([]opts.BarData) require.True(t, ok, "Series data should be of type []opts.BarData") assert.Len(t, seriesData, 10, "Expected 10 data points (bins) in the series") } // TestCreateTTFTHistogram checks if the TTFT histogram chart is created correctly. func TestCreateTTFTHistogram(t *testing.T) { mockStats := stats.FinalStats{ TTFTData: []time.Duration{dms(20), dms(30), dms(35), dms(40), dms(60)}, } chart := createTTFTHistogram(mockStats) require.NotNil(t, chart, "Chart should not be nil") require.NotNil(t, chart.BaseConfiguration.Title, "Chart title configuration should not be nil") // Check title assert.Equal(t, "TTFT Distribution", chart.BaseConfiguration.Title.Title, "Chart title mismatch") // Check series data require.Len(t, chart.MultiSeries, 1, "Expected one series") assert.Equal(t, "TTFT", chart.MultiSeries[0].Name, "Series name mismatch") // Optionally, check the number of data points // Data: {20, 30, 35, 40, 60}, Range=40, 10 bins -> width=4 // Bins: [20,24), [24,28), [28,32), [32,36), [36,40), [40,44), [44,48), [48,52), [52,56), [56,60] // Counts: 1, 0, 1, 1, 0, 1, 0, 0, 0, 1 -> Total 5 points, 10 bars seriesData, ok := chart.MultiSeries[0].Data.([]opts.BarData) require.True(t, ok, "Series data should be of type []opts.BarData") assert.Len(t, seriesData, 10, "Expected 10 data points (bins) in the series") }