| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package funcapi |
| 4 | |
| 5 | import ( |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/stretchr/testify/assert" |
| 9 | "github.com/stretchr/testify/require" |
| 10 | ) |
| 11 | |
| 12 | // testColumn is a test column type that embeds ColumnMeta. |
| 13 | type testColumn struct { |
| 14 | ColumnMeta |
| 15 | Extra string // Simulates collector-specific field |
| 16 | } |
| 17 | |
| 18 | func testColumnSet(cols ...testColumn) ColumnSet[testColumn] { |
| 19 | return Columns(cols, func(c testColumn) ColumnMeta { return c.ColumnMeta }) |
| 20 | } |
| 21 | |
| 22 | func TestColumnSet_Len(t *testing.T) { |
| 23 | cs := testColumnSet( |
| 24 | testColumn{ColumnMeta: ColumnMeta{Name: "a"}}, |
| 25 | testColumn{ColumnMeta: ColumnMeta{Name: "b"}}, |
| 26 | ) |
| 27 | assert.Equal(t, 2, cs.Len()) |
| 28 | } |
| 29 | |
| 30 | func TestColumnSet_Names(t *testing.T) { |
| 31 | cs := testColumnSet( |
| 32 | testColumn{ColumnMeta: ColumnMeta{Name: "col1"}}, |
| 33 | testColumn{ColumnMeta: ColumnMeta{Name: "col2"}}, |
| 34 | testColumn{ColumnMeta: ColumnMeta{Name: "col3"}}, |
| 35 | ) |
| 36 | assert.Equal(t, []string{"col1", "col2", "col3"}, cs.Names()) |
| 37 | } |
| 38 | |
| 39 | func TestColumnSet_ContainsColumn(t *testing.T) { |
| 40 | cs := testColumnSet( |
| 41 | testColumn{ColumnMeta: ColumnMeta{Name: "exists"}}, |
| 42 | ) |
| 43 | assert.True(t, cs.ContainsColumn("exists")) |
| 44 | assert.False(t, cs.ContainsColumn("not_exists")) |
| 45 | } |
| 46 | |
| 47 | func TestColumnSet_BuildColumns(t *testing.T) { |
| 48 | cs := testColumnSet( |
| 49 | testColumn{ColumnMeta: ColumnMeta{ |
| 50 | Name: "query", |
| 51 | Tooltip: "Query", |
| 52 | Type: FieldTypeString, |
| 53 | Visible: true, |
| 54 | Sortable: true, |
| 55 | Filter: FieldFilterMultiselect, |
| 56 | }}, |
| 57 | testColumn{ColumnMeta: ColumnMeta{ |
| 58 | Name: "duration", |
| 59 | Tooltip: "Duration", |
| 60 | Type: FieldTypeDuration, |
| 61 | Units: "ms", |
| 62 | Visible: true, |
| 63 | Sortable: true, |
| 64 | Transform: FieldTransformDuration, |
| 65 | DecimalPoints: 2, |
| 66 | }}, |
| 67 | ) |
| 68 | |
| 69 | result := cs.BuildColumns() |
| 70 | |
| 71 | require.Len(t, result, 2) |
| 72 | require.Contains(t, result, "query") |
| 73 | require.Contains(t, result, "duration") |
| 74 | |
| 75 | // Check query column |
| 76 | queryCol := result["query"].(map[string]any) |
| 77 | assert.Equal(t, 0, queryCol["index"]) |
| 78 | assert.Equal(t, "Query", queryCol["name"]) |
| 79 | assert.Equal(t, "string", queryCol["type"]) |
| 80 | assert.Equal(t, true, queryCol["visible"]) |
| 81 | assert.Equal(t, true, queryCol["sortable"]) |
| 82 | |
| 83 | // Check duration column - should auto-switch to bar visualization |
| 84 | durationCol := result["duration"].(map[string]any) |
| 85 | assert.Equal(t, 1, durationCol["index"]) |
| 86 | assert.Equal(t, "Duration", durationCol["name"]) |
| 87 | assert.Equal(t, "duration", durationCol["type"]) |
| 88 | assert.Equal(t, "bar", durationCol["visualization"]) // Auto-switched from value to bar |
| 89 | assert.Equal(t, "ms", durationCol["units"]) |
| 90 | } |
| 91 | |
| 92 | func TestColumnSet_BuildColumns_ExplicitVisualization(t *testing.T) { |
| 93 | // When visualization is explicitly set, it should NOT be overridden |
| 94 | cs := testColumnSet( |
| 95 | testColumn{ColumnMeta: ColumnMeta{ |
| 96 | Name: "duration", |
| 97 | Tooltip: "Duration", |
| 98 | Type: FieldTypeDuration, |
| 99 | Visualization: FieldVisualPill, // Explicitly set to pill |
| 100 | }}, |
| 101 | ) |
| 102 | |
| 103 | result := cs.BuildColumns() |
| 104 | durationCol := result["duration"].(map[string]any) |
| 105 | assert.Equal(t, "pill", durationCol["visualization"]) // Should remain pill, not bar |
| 106 | } |
| 107 | |
| 108 | func TestColumnSet_BuildCharts(t *testing.T) { |
| 109 | cs := testColumnSet( |
| 110 | testColumn{ColumnMeta: ColumnMeta{ |
| 111 | Name: "query", |
| 112 | Tooltip: "Query", |
| 113 | Chart: nil, // Not a chart metric |
| 114 | }}, |
| 115 | testColumn{ColumnMeta: ColumnMeta{ |
| 116 | Name: "exec_time", |
| 117 | Tooltip: "Execution Time", |
| 118 | Chart: &ChartOptions{Group: "Time", Title: "Query Time"}, |
| 119 | }}, |
| 120 | testColumn{ColumnMeta: ColumnMeta{ |
| 121 | Name: "cpu_time", |
| 122 | Tooltip: "CPU Time", |
| 123 | Chart: &ChartOptions{Group: "Time", Title: "Query Time"}, |
| 124 | }}, |
| 125 | testColumn{ColumnMeta: ColumnMeta{ |
| 126 | Name: "rows", |
| 127 | Tooltip: "Rows", |
| 128 | Chart: &ChartOptions{Group: "Rows", Title: "Row Count"}, |
| 129 | }}, |
| 130 | ) |
| 131 | |
| 132 | result := cs.BuildCharts() |
| 133 | |
| 134 | require.Len(t, result, 2) |
| 135 | |
| 136 | // Time chart should have 2 columns |
| 137 | timeChart := result["Time"] |
| 138 | assert.Equal(t, "Query Time", timeChart.Name) |
| 139 | assert.Equal(t, "stacked-bar", timeChart.Type) |
| 140 | assert.Equal(t, []string{"exec_time", "cpu_time"}, timeChart.Columns) |
| 141 | |
| 142 | // Rows chart should have 1 column |
| 143 | rowsChart := result["Rows"] |
| 144 | assert.Equal(t, "Row Count", rowsChart.Name) |
| 145 | assert.Equal(t, []string{"rows"}, rowsChart.Columns) |
| 146 | } |
| 147 | |
| 148 | func TestColumnSet_BuildCharts_FallbackTitle(t *testing.T) { |
| 149 | cs := testColumnSet( |
| 150 | testColumn{ColumnMeta: ColumnMeta{ |
| 151 | Name: "metric", |
| 152 | Chart: &ChartOptions{Group: "MyGroup", Title: ""}, // Empty title |
| 153 | }}, |
| 154 | ) |
| 155 | |
| 156 | result := cs.BuildCharts() |
| 157 | assert.Equal(t, "MyGroup", result["MyGroup"].Name) // Falls back to Group |
| 158 | } |
| 159 | |
| 160 | func TestColumnSet_BuildGroupBy(t *testing.T) { |
| 161 | cs := testColumnSet( |
| 162 | testColumn{ColumnMeta: ColumnMeta{ |
| 163 | Name: "query", |
| 164 | Tooltip: "Query", |
| 165 | GroupBy: &GroupByOptions{}, |
| 166 | }}, |
| 167 | testColumn{ColumnMeta: ColumnMeta{ |
| 168 | Name: "database", |
| 169 | Tooltip: "Database", |
| 170 | GroupBy: &GroupByOptions{}, |
| 171 | }}, |
| 172 | testColumn{ColumnMeta: ColumnMeta{ |
| 173 | Name: "duration", |
| 174 | Tooltip: "Duration", |
| 175 | GroupBy: nil, // Not available for grouping |
| 176 | }}, |
| 177 | ) |
| 178 | |
| 179 | result := cs.BuildGroupBy() |
| 180 | |
| 181 | require.Len(t, result, 2) |
| 182 | assert.Equal(t, "Group by Query", result["query"].Name) |
| 183 | assert.Equal(t, []string{"query"}, result["query"].Columns) |
| 184 | assert.Equal(t, "Group by Database", result["database"].Name) |
| 185 | } |
| 186 | |
| 187 | func TestColumnSet_FindDefaultGroupBy(t *testing.T) { |
| 188 | tests := []struct { |
| 189 | name string |
| 190 | cols []testColumn |
| 191 | expected string |
| 192 | }{ |
| 193 | { |
| 194 | name: "returns default groupby if exists", |
| 195 | cols: []testColumn{ |
| 196 | {ColumnMeta: ColumnMeta{Name: "a", GroupBy: &GroupByOptions{}}}, |
| 197 | {ColumnMeta: ColumnMeta{Name: "b", GroupBy: &GroupByOptions{IsDefault: true}}}, |
| 198 | {ColumnMeta: ColumnMeta{Name: "c", GroupBy: &GroupByOptions{}}}, |
| 199 | }, |
| 200 | expected: "b", |
| 201 | }, |
| 202 | { |
| 203 | name: "falls back to first groupby if no default", |
| 204 | cols: []testColumn{ |
| 205 | {ColumnMeta: ColumnMeta{Name: "a", GroupBy: nil}}, |
| 206 | {ColumnMeta: ColumnMeta{Name: "b", GroupBy: &GroupByOptions{}}}, |
| 207 | {ColumnMeta: ColumnMeta{Name: "c", GroupBy: &GroupByOptions{}}}, |
| 208 | }, |
| 209 | expected: "b", |
| 210 | }, |
| 211 | { |
| 212 | name: "returns empty if no groupby columns", |
| 213 | cols: []testColumn{ |
| 214 | {ColumnMeta: ColumnMeta{Name: "a"}}, |
| 215 | {ColumnMeta: ColumnMeta{Name: "b"}}, |
| 216 | }, |
| 217 | expected: "", |
| 218 | }, |
| 219 | } |
| 220 | |
| 221 | for _, tt := range tests { |
| 222 | t.Run(tt.name, func(t *testing.T) { |
| 223 | cs := testColumnSet(tt.cols...) |
| 224 | assert.Equal(t, tt.expected, cs.FindDefaultGroupBy()) |
| 225 | }) |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | func TestColumnSet_FindDefaultChartGroups(t *testing.T) { |
| 230 | tests := []struct { |
| 231 | name string |
| 232 | cols []testColumn |
| 233 | expected []string |
| 234 | }{ |
| 235 | { |
| 236 | name: "returns default chart groups", |
| 237 | cols: []testColumn{ |
| 238 | {ColumnMeta: ColumnMeta{Name: "a", Chart: &ChartOptions{Group: "A", IsDefault: true}}}, |
| 239 | {ColumnMeta: ColumnMeta{Name: "b", Chart: &ChartOptions{Group: "B", IsDefault: false}}}, |
| 240 | {ColumnMeta: ColumnMeta{Name: "c", Chart: &ChartOptions{Group: "C", IsDefault: true}}}, |
| 241 | }, |
| 242 | expected: []string{"A", "C"}, |
| 243 | }, |
| 244 | { |
| 245 | name: "falls back to all groups if no defaults", |
| 246 | cols: []testColumn{ |
| 247 | {ColumnMeta: ColumnMeta{Name: "a", Chart: &ChartOptions{Group: "A", IsDefault: false}}}, |
| 248 | {ColumnMeta: ColumnMeta{Name: "b", Chart: &ChartOptions{Group: "B", IsDefault: false}}}, |
| 249 | }, |
| 250 | expected: []string{"A", "B"}, |
| 251 | }, |
| 252 | { |
| 253 | name: "deduplicates groups", |
| 254 | cols: []testColumn{ |
| 255 | {ColumnMeta: ColumnMeta{Name: "a", Chart: &ChartOptions{Group: "Same", IsDefault: true}}}, |
| 256 | {ColumnMeta: ColumnMeta{Name: "b", Chart: &ChartOptions{Group: "Same", IsDefault: true}}}, |
| 257 | }, |
| 258 | expected: []string{"Same"}, |
| 259 | }, |
| 260 | { |
| 261 | name: "returns nil for no chart columns", |
| 262 | cols: []testColumn{ |
| 263 | {ColumnMeta: ColumnMeta{Name: "a", Chart: nil}}, |
| 264 | }, |
| 265 | expected: nil, |
| 266 | }, |
| 267 | } |
| 268 | |
| 269 | for _, tt := range tests { |
| 270 | t.Run(tt.name, func(t *testing.T) { |
| 271 | cs := testColumnSet(tt.cols...) |
| 272 | assert.Equal(t, tt.expected, cs.FindDefaultChartGroups()) |
| 273 | }) |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | func TestColumnSet_BuildDefaultCharts(t *testing.T) { |
| 278 | cs := testColumnSet( |
| 279 | testColumn{ColumnMeta: ColumnMeta{Name: "query", GroupBy: &GroupByOptions{IsDefault: true}}}, |
| 280 | testColumn{ColumnMeta: ColumnMeta{Name: "time", Chart: &ChartOptions{Group: "Time", IsDefault: true}}}, |
| 281 | testColumn{ColumnMeta: ColumnMeta{Name: "rows", Chart: &ChartOptions{Group: "Rows", IsDefault: true}}}, |
| 282 | ) |
| 283 | |
| 284 | result := cs.BuildDefaultCharts() |
| 285 | |
| 286 | expected := DefaultCharts{ |
| 287 | {Chart: "Time", GroupBy: "query"}, |
| 288 | {Chart: "Rows", GroupBy: "query"}, |
| 289 | } |
| 290 | assert.Equal(t, expected, result) |
| 291 | |
| 292 | // Test Build() method for JSON output |
| 293 | assert.Equal(t, [][]string{{"Time", "query"}, {"Rows", "query"}}, result.Build()) |
| 294 | } |
| 295 | |
| 296 | func TestColumnSet_BuildDefaultCharts_NoGroupBy(t *testing.T) { |
| 297 | cs := testColumnSet( |
| 298 | testColumn{ColumnMeta: ColumnMeta{Name: "metric", Chart: &ChartOptions{Group: "G"}}}, |
| 299 | ) |
| 300 | |
| 301 | result := cs.BuildDefaultCharts() |
| 302 | assert.Nil(t, result) |
| 303 | } |
| 304 | |
| 305 | func TestColumnSet_Empty(t *testing.T) { |
| 306 | cs := testColumnSet() |
| 307 | |
| 308 | assert.Equal(t, 0, cs.Len()) |
| 309 | assert.Empty(t, cs.Names()) |
| 310 | assert.Empty(t, cs.BuildColumns()) |
| 311 | assert.Empty(t, cs.BuildCharts()) |
| 312 | assert.Empty(t, cs.BuildGroupBy()) |
| 313 | assert.Nil(t, cs.BuildDefaultCharts()) |
| 314 | assert.Equal(t, "", cs.FindDefaultGroupBy()) |
| 315 | assert.Nil(t, cs.FindDefaultChartGroups()) |
| 316 | assert.False(t, cs.ContainsColumn("any")) |
| 317 | } |