cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.84.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/diags/problems_test.go

371lines · modecode

1package diags
2
3import (
4 "path/filepath"
5 "runtime"
6 "strings"
7 "testing"
8
9 "github.com/gkampitakis/go-snaps/snaps"
10 promParser "github.com/prometheus/prometheus/promql/parser"
11 "github.com/stretchr/testify/require"
12
13 "github.com/cloudflare/pint/internal/output"
14)
15
16func TestInjectDiagnostics(t *testing.T) {
17 type testCaseT struct {
18 name string
19 input string
20 diags []Diagnostic
21 }
22
23 testCases := []testCaseT{
24 {
25 name: "single diagnostic on one line",
26 input: "expr: foo(bar) by()",
27 diags: []Diagnostic{
28 {FirstColumn: 1, LastColumn: 13, Message: "this is bad"},
29 },
30 },
31 {
32 name: "caret in the middle of a line",
33 input: "expr: foo(bar) on()",
34 diags: []Diagnostic{
35 {FirstColumn: 10, LastColumn: 11, Message: "oops"},
36 },
37 },
38 {
39 name: "two diagnostics on different columns",
40 input: `
41expr: sum(foo{job="bar"})
42 / on(a,b)
43 sum(foo)
44`,
45 diags: []Diagnostic{
46 {FirstColumn: 23, LastColumn: 29, Message: "abc"},
47 {FirstColumn: 26, LastColumn: 28, Message: "efg"},
48 },
49 },
50 {
51 name: "YAML literal block scalar",
52 input: `
53expr: |
54 sum(bar{job="foo"})
55 / on(c,d)
56 sum(bar)
57`,
58 diags: []Diagnostic{
59 {FirstColumn: 23, LastColumn: 24, Message: "123"},
60 {FirstColumn: 31, LastColumn: 33, Message: "456"},
61 },
62 },
63 {
64 name: "two diagnostics on same columns",
65 input: `
66expr:
67 sum(bar{job="foo"})
68 / on(c,d)
69 sum(bar)
70`,
71 diags: []Diagnostic{
72 {FirstColumn: 23, LastColumn: 29, Message: "abc"},
73 {FirstColumn: 23, LastColumn: 29, Message: "efg"},
74 },
75 },
76 {
77 name: "YAML folded block scalar with surrounding lines",
78 input: `
79### BEGIN ###
80expr: >-
81 sum(bar{job="foo"})
82 / on(c,d)
83 sum(bar)
84### END ###
85`,
86 diags: []Diagnostic{
87 {FirstColumn: 23, LastColumn: 29, Message: "abc"},
88 {FirstColumn: 23, LastColumn: 29, Message: "efg"},
89 },
90 },
91 {
92 name: "single column caret",
93 input: "expr: cnt(bar) by()",
94 diags: []Diagnostic{
95 {FirstColumn: 14, LastColumn: 14, Message: "this is bad"},
96 },
97 },
98 {
99 name: "multi-line expression with caret on last line",
100 input: `
101expr: |
102 foo{
103 job="bar"
104 }
105`,
106 diags: []Diagnostic{
107 {FirstColumn: 1, LastColumn: 16, Message: "this is bad"},
108 },
109 },
110 {
111 name: "issue and context diagnostics on same column",
112 input: "expr: foo(bar) by()",
113 diags: []Diagnostic{
114 {FirstColumn: 1, LastColumn: 13, Message: "this is bad", Kind: Issue},
115 {FirstColumn: 1, LastColumn: 13, Message: "this is context", Kind: Context},
116 },
117 },
118 {
119 name: "rightmost caret is printed first",
120 input: "expr: foo(bar) by()",
121 diags: []Diagnostic{
122 {FirstColumn: 1, LastColumn: 13, Message: "this is bad", Kind: Issue},
123 {FirstColumn: 10, LastColumn: 13, Message: "this is context", Kind: Context},
124 },
125 },
126 {
127 name: "multiple position ranges on same line compute min and max columns",
128 input: `sum by (instance) (rate(http_requests_total{job="api",status=~"5.."}[5m])) / sum by (instance) (rate(up{job="api"}[5m])) > 0.01`,
129 diags: []Diagnostic{
130 {
131 Message: "check this",
132 Pos: PositionRanges{
133 {Line: 1, FirstColumn: 1, LastColumn: 74},
134 {Line: 1, FirstColumn: 78, LastColumn: 120},
135 },
136 FirstColumn: 99,
137 LastColumn: 100,
138 },
139 },
140 },
141 {
142 name: "short expression range on long line skips AST trimming",
143 input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: sum(foo) by(bar)",
144 diags: []Diagnostic{
145 {
146 Message: "bad",
147 Pos: PositionRanges{{Line: 1, FirstColumn: 96, LastColumn: 99}},
148 FirstColumn: 96,
149 LastColumn: 99,
150 },
151 },
152 },
153 {
154 name: "AST trimming with multi-line positions skips other lines",
155 input: `sum by (instance) (rate(http_requests_total{job="api",status=~"5.."}[5m])) / sum by (instance) (rate(up{job="api"}[5m])) > 0.01`,
156 diags: []Diagnostic{
157 {
158 Message: "dead code",
159 Pos: PositionRanges{{Line: 1, FirstColumn: 1, LastColumn: 120}},
160 FirstColumn: 102,
161 LastColumn: 103,
162 },
163 {
164 Message: "extra",
165 Pos: PositionRanges{
166 {Line: 1, FirstColumn: 102, LastColumn: 103},
167 {Line: 2, FirstColumn: 1, LastColumn: 5},
168 },
169 FirstColumn: 102,
170 LastColumn: 103,
171 },
172 },
173 },
174 {
175 name: "empty message diagnostic produces no message line",
176 input: "expr: foo(bar) by()",
177 diags: []Diagnostic{
178 {FirstColumn: 1, LastColumn: 13, Message: ""},
179 },
180 },
181 {
182 name: "non-consecutive lines produce gap marker",
183 input: "line1: foo\nline2: bar\nline3: baz\n",
184 diags: []Diagnostic{
185 {
186 Message: "err1",
187 Pos: PositionRanges{{Line: 1, FirstColumn: 8, LastColumn: 10}},
188 FirstColumn: 8,
189 LastColumn: 10,
190 },
191 {
192 Message: "err3",
193 Pos: PositionRanges{{Line: 3, FirstColumn: 8, LastColumn: 10}},
194 FirstColumn: 8,
195 LastColumn: 10,
196 },
197 },
198 },
199 {
200 name: "non-overlapping subexpression is replaced with ellipsis",
201 input: `
202expr: sum by (instance) (rate(http_requests_total{job="api",status=~"5.."}[5m])) / sum by (instance) (rate(up{job="api"}[5m])) > 0.01`,
203 diags: []Diagnostic{
204 {FirstColumn: 102, LastColumn: 103, Message: "dead code"},
205 },
206 },
207 {
208 name: "large vector selector inside sum is replaced",
209 input: `
210expr: sum(oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) by(x)`,
211 diags: []Diagnostic{
212 {FirstColumn: 97, LastColumn: 101, Message: "by(x) issue"},
213 },
214 },
215 {
216 name: "invalid PromQL skips trimming and keeps full line",
217 input: `
218expr: sum(rate(http_requests_total{job="api",status=~"5.."}[5m]) / sum(rate(up{job="api"}[5m])) > 0.01`,
219 diags: []Diagnostic{
220 {FirstColumn: 91, LastColumn: 96, Message: "syntax error"},
221 },
222 },
223 {
224 name: "invalid PromQL with far right caret places message on left",
225 input: `
226expr: sum(rate(http_requests_total{job="api",status=~"5.."}[5m])) / sum(rate(up{job="api"}[5m]))) > 0.01`,
227 diags: []Diagnostic{
228 {FirstColumn: 105, LastColumn: 108, Message: "syntax error"},
229 },
230 },
231 {
232 name: "replacement after diagnostic does not shift caret",
233 input: `
234expr: sum(foo) + sum(rate(very_long_metric_name_aaaa{job="api",status=~"5..",instance=~".*"}[5m])) > 0`,
235 diags: []Diagnostic{
236 {FirstColumn: 1, LastColumn: 8, Message: "bad sum"},
237 },
238 },
239 {
240 name: "far right caret places short message on left",
241 input: `
242expr: sum(foo) without(colo_id, instance, node_type, region, node_status, job, colo_name)`,
243 diags: []Diagnostic{
244 {FirstColumn: 74, LastColumn: 82, Message: "bad label"},
245 },
246 },
247 {
248 name: "far right caret wraps long message on left",
249 input: `
250expr: sum(foo) without(colo_id, instance, node_type, region, node_status, job, colo_name)`,
251 diags: []Diagnostic{
252 {FirstColumn: 74, LastColumn: 82, Message: "Using `without(colo_id, instance, node_type, region, node_status, job, colo_name)` removes all these labels from the results."},
253 },
254 },
255 {
256 name: "multi-line expression trims individual long lines",
257 input: `
258expr: |
259 sum(rate(very_long_metric_name_that_pushes_past_the_width_limit_aaaa{job="api",status=~"5.."}[5m])) by(instance)
260 + sum(rate(another_very_long_metric_name_that_pushes_past_the_width_limit{job="api"}[5m]))`,
261 diags: []Diagnostic{
262 {FirstColumn: 1, LastColumn: 3, Message: "bad rate"},
263 },
264 },
265 {
266 name: "diagnostic covers entire expression so no nodes are replaced",
267 input: `
268expr: sum(rate(very_long_metric_name_that_pushes_past_the_width_limit_aaaa{job="api",status=~"5.."}[5m])) by(instance)`,
269 diags: []Diagnostic{
270 {FirstColumn: 1, LastColumn: 109, Message: "bad query"},
271 },
272 },
273 {
274 name: "right side message wraps at line width limit",
275 input: `
276expr: sum(foo) without(colo_id, instance, node_type, region, node_status, job, colo_name)`,
277 diags: []Diagnostic{
278 {FirstColumn: 10, LastColumn: 16, Message: "Query is using aggregation with `without(colo_id, instance, node_type, region, node_status, job, colo_name)`, all labels included inside `without(...)` will be removed from the results. `job` label is required and should be preserved when aggregating all rules."},
279 },
280 },
281 {
282 name: "indented multi-line gap uses aligned ellipsis marker",
283 input: " line1\n line2\n line3\n line4\n line5\n line6\n line7\n line8\n line9\n line10",
284 diags: []Diagnostic{
285 {
286 Message: "problem here",
287 FirstColumn: 3,
288 LastColumn: 7,
289 Pos: PositionRanges{
290 {Line: 1, FirstColumn: 1, LastColumn: 7},
291 {Line: 2, FirstColumn: 1, LastColumn: 7},
292 {Line: 3, FirstColumn: 1, LastColumn: 7},
293 {Line: 4, FirstColumn: 1, LastColumn: 7},
294 {Line: 5, FirstColumn: 1, LastColumn: 7},
295 {Line: 6, FirstColumn: 1, LastColumn: 7},
296 {Line: 7, FirstColumn: 1, LastColumn: 7},
297 {Line: 8, FirstColumn: 1, LastColumn: 7},
298 {Line: 9, FirstColumn: 1, LastColumn: 7},
299 {Line: 10, FirstColumn: 1, LastColumn: 7},
300 },
301 },
302 },
303 },
304 {
305 name: "multi-line expression trims long inner line",
306 input: `
307expr: >-
308 sum by (exporter, colo_name) (rate(otelcol_exporter_send_failed_log_records{node_status="v", exporter=~"(failover|otlp)/.*"}[5m]))
309 /
310 (
311 sum by (exporter, colo_name) (rate(otelcol_exporter_send_failed_log_records{exporter=~"(failover|otlp)/.*"}[5m])) + sum by (exporter, colo_name) (rate(otelcol_exporter_sent_log_records{exporter=~"(failover|otlp)/.*"}[5m]))
312 )
313 > 0.1
314 and
315 sum by (exporter, colo_name) (rate(otelcol_exporter_send_failed_log_records{node_status="v", exporter=~"(failover|otlp)/.*"}[5m])) > 10`,
316 diags: []Diagnostic{
317 {FirstColumn: 323, LastColumn: 352, Message: "smelly regexp selector"},
318 },
319 },
320 }
321
322 _, file, _, ok := runtime.Caller(0)
323 require.True(t, ok, "can't get caller function")
324 file = strings.TrimSuffix(filepath.Base(file), ".go")
325 for _, tc := range testCases {
326 t.Run(tc.name, func(t *testing.T) {
327 diags := make([]Diagnostic, 0, len(tc.diags))
328 allHavePos := true
329 for _, d := range tc.diags {
330 if len(d.Pos) == 0 {
331 allHavePos = false
332 break
333 }
334 }
335
336 if allHavePos {
337 diags = append(diags, tc.diags...)
338 } else {
339 key, val := parseYaml(tc.input)
340 require.NotNil(t, key)
341 require.NotNil(t, val)
342 pos := NewPositionRange(strings.Split(tc.input, "\n"), val, key.Column+2)
343 require.NotEmpty(t, pos)
344
345 var expr promParser.Node
346 if node, err := promParser.NewParser(promParser.Options{}).ParseExpr(val.Value); err == nil {
347 expr = node
348 }
349
350 for _, diag := range tc.diags {
351 diags = append(diags, Diagnostic{
352 Message: diag.Message,
353 Kind: diag.Kind,
354 Pos: pos,
355 Expr: expr,
356 FirstColumn: diag.FirstColumn,
357 LastColumn: diag.LastColumn,
358 })
359 }
360 }
361
362 out := InjectDiagnostics(tc.input, diags, output.None)
363 snaps.WithConfig(snaps.Dir("."), snaps.Filename(file)).MatchSnapshot(
364 t,
365 tc.input,
366 "",
367 out,
368 )
369 })
370 }
371}
372