cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.83.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/diags/problems_test.go

325lines · modecode

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