cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e622a47c6fc350caabb3fe75c25cc3bfbd6feab6

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/source/position_test.go

496lines · modecode

1package source_test
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "github.com/gkampitakis/go-snaps/snaps"
9 "github.com/stretchr/testify/require"
10
11 "github.com/cloudflare/pint/internal/parser"
12 "github.com/cloudflare/pint/internal/parser/source"
13
14 "github.com/prometheus/prometheus/model/labels"
15 "github.com/prometheus/prometheus/promql/parser/posrange"
16)
17
18// highlightRange returns expr with a caret line under each line, marking the
19// bytes selected by pos with '^'. Carets stay aligned with the expr above them
20// (no shifting); tabs and newlines are copied through so indentation lines up.
21func highlightRange(expr string, pos posrange.PositionRange) string {
22 start := min(int(pos.Start), len(expr))
23 end := min(int(pos.End), len(expr))
24 if start > end {
25 start = end
26 }
27
28 marker := []byte(expr)
29 for i := range marker {
30 switch {
31 case marker[i] == '\n' || marker[i] == '\t':
32 case i >= start && i < end:
33 marker[i] = '^'
34 default:
35 marker[i] = ' '
36 }
37 }
38
39 exprLines := strings.Split(expr, "\n")
40 markerLines := strings.Split(string(marker), "\n")
41 var out []string
42 for i := range exprLines {
43 out = append(out, exprLines[i])
44 if strings.ContainsRune(markerLines[i], '^') {
45 out = append(out, markerLines[i])
46 }
47 }
48 return strings.Join(out, "\n")
49}
50
51func matchPositionSnapshot(t *testing.T, content string) {
52 t.Helper()
53 snaps.WithConfig(snaps.Dir("."), snaps.Filename("position_test")).MatchSnapshot(t, content)
54}
55
56func renderOutside(outside []posrange.PositionRange) string {
57 if len(outside) == 0 {
58 return "none"
59 }
60 parts := make([]string, 0, len(outside))
61 for _, o := range outside {
62 parts = append(parts, fmt.Sprintf("%d-%d", o.Start, o.End))
63 }
64 return strings.Join(parts, ", ")
65}
66
67func TestLabelsSourceFindArgumentPositionCoverage(t *testing.T) {
68 type testCase struct {
69 description string
70 expr string
71 label string
72 }
73
74 testCases := []testCase{
75 {
76 description: "skip label name embedded in another identifier",
77 expr: `sum(foo) by(job, notjob)`,
78 label: "job",
79 },
80 {
81 description: "skip whitespace before closing delimiter",
82 expr: `sum(foo) by(job )`,
83 label: "job",
84 },
85 {
86 description: "skip whitespace before label name",
87 expr: `sum(foo) by( job)`,
88 label: "job",
89 },
90 {
91 description: "skip multiple spaces around label name",
92 expr: `sum(foo) by( job )`,
93 label: "job",
94 },
95 {
96 description: "skip tabs around label name",
97 expr: "sum(foo) by(\tjob\t)",
98 label: "job",
99 },
100 {
101 description: "skip newlines around label name",
102 expr: "sum(foo) by(\njob\n)",
103 label: "job",
104 },
105 {
106 description: "skip mixed whitespace in multi-argument list",
107 expr: "sum(foo) by(\n job,\n instance\n)",
108 label: "job",
109 },
110 {
111 description: "skip label name that is a prefix of an earlier argument",
112 expr: `sum(foo) by(jobx, job)`,
113 label: "job",
114 },
115 {
116 description: "skip invalid suffix and keep searching for earlier valid match",
117 expr: `(foo * on(job) group_left(cluster) bar) and on(job) baz{job="x"}`,
118 label: "job",
119 },
120 {
121 description: "find middle label in multiline grouping with tabs and spaces",
122 expr: "sum(foo) by(\n\tjob,\n\t instance,\n region\n)",
123 label: "instance",
124 },
125 {
126 description: "find last label in multiline grouping with tabs and spaces",
127 expr: "sum(foo) by(\n\tjob,\n\t instance,\n region\n)",
128 label: "region",
129 },
130 {
131 description: "find first label in multiline grouping with tabs and spaces",
132 expr: "sum(foo) by(\n\tjob,\n\t instance,\n region\n)",
133 label: "job",
134 },
135 {
136 description: "label named like the aggregation it groups",
137 expr: `sum(sum) by(sum)`,
138 label: "sum",
139 },
140 {
141 description: "label named like the metric inside without grouping",
142 expr: `sum(rate) without(rate)`,
143 label: "rate",
144 },
145 {
146 description: "find label in deeply nested aggregation grouping",
147 expr: `sum(sum(sum(foo) by(job)) by(job)) by(job)`,
148 label: "job",
149 },
150 }
151
152 for _, tc := range testCases {
153 t.Run(tc.description, func(t *testing.T) {
154 n, err := parser.DecodeExpr(tc.expr)
155 require.NoError(t, err)
156
157 output := source.LabelsSource(tc.expr, n.Expr)
158 require.NotEmpty(t, output)
159
160 var fragment posrange.PositionRange
161 for _, src := range output {
162 src.WalkSources(func(s *source.Source, _ *source.Join, _ *source.Unless) {
163 label, ok := s.Labels[tc.label]
164 if !ok {
165 return
166 }
167 if fragment == (posrange.PositionRange{}) {
168 fragment = label.Fragment
169 return
170 }
171 if label.Fragment.End-label.Fragment.Start < fragment.End-fragment.Start {
172 fragment = label.Fragment
173 }
174 })
175 }
176
177 require.NotEqual(t, posrange.PositionRange{}, fragment)
178 matchPositionSnapshot(t, fmt.Sprintf(
179 "label: %s\nexpr:\n%s",
180 tc.label,
181 highlightRange(tc.expr, fragment),
182 ))
183 })
184 }
185}
186
187func TestGetQueryFragment(t *testing.T) {
188 type testCase struct {
189 description string
190 expr string
191 expected string
192 pos posrange.PositionRange
193 }
194
195 testCases := []testCase{
196 {
197 description: "extracts the leading token",
198 expr: "sum(foo)",
199 pos: posrange.PositionRange{Start: 0, End: 3},
200 expected: "sum",
201 },
202 {
203 description: "extracts a token in the middle",
204 expr: "sum(foo)",
205 pos: posrange.PositionRange{Start: 4, End: 7},
206 expected: "foo",
207 },
208 {
209 description: "returns empty string for an empty range",
210 expr: "sum(foo)",
211 pos: posrange.PositionRange{Start: 3, End: 3},
212 expected: "",
213 },
214 }
215
216 for _, tc := range testCases {
217 t.Run(tc.description, func(t *testing.T) {
218 require.Equal(t, tc.expected, source.GetQueryFragment(tc.expr, tc.pos))
219 })
220 }
221}
222
223func TestFindFuncNamePosition(t *testing.T) {
224 type testCase struct {
225 description string
226 expr string
227 fn string
228 within posrange.PositionRange
229 }
230
231 testCases := []testCase{
232 {
233 description: "returns within when fn is absent",
234 expr: "sum(foo)",
235 within: posrange.PositionRange{Start: 0, End: 8},
236 fn: "rate",
237 },
238 {
239 description: "matches the function name before the paren",
240 expr: "sum(foo)",
241 within: posrange.PositionRange{Start: 0, End: 8},
242 fn: "sum",
243 },
244 {
245 description: "matches with whitespace before the paren",
246 expr: "sum (foo)",
247 within: posrange.PositionRange{Start: 0, End: 9},
248 fn: "sum",
249 },
250 {
251 description: "matches case-insensitively",
252 expr: "SUM(foo)",
253 within: posrange.PositionRange{Start: 0, End: 8},
254 fn: "sum",
255 },
256 {
257 description: "skips an occurrence inside a word and matches the call",
258 expr: "sumx + sum(foo)",
259 within: posrange.PositionRange{Start: 0, End: 15},
260 fn: "sum",
261 },
262 {
263 description: "respects the within offset",
264 expr: "xx sum(foo)",
265 within: posrange.PositionRange{Start: 3, End: 11},
266 fn: "sum",
267 },
268 {
269 description: "returns the first occurrence when the name repeats",
270 expr: "sum(sum) by(sum)",
271 within: posrange.PositionRange{Start: 0, End: 16},
272 fn: "sum",
273 },
274 {
275 description: "matches the call and not a longer identifier with the same prefix",
276 expr: "sum(summary)",
277 within: posrange.PositionRange{Start: 0, End: 12},
278 fn: "sum",
279 },
280 {
281 description: "skips the name inside a leading word and matches the later call",
282 expr: "summary + sum(x)",
283 within: posrange.PositionRange{Start: 0, End: 16},
284 fn: "sum",
285 },
286 {
287 description: "matches across newline and tab before the paren",
288 expr: "SuM\n\t(x)",
289 within: posrange.PositionRange{Start: 0, End: 8},
290 fn: "sum",
291 },
292 {
293 description: "returns within when the name only appears inside a word",
294 expr: "bytes",
295 within: posrange.PositionRange{Start: 0, End: 5},
296 fn: "by",
297 },
298 {
299 description: "returns within when the name is followed by a non-paren token",
300 expr: "by job",
301 within: posrange.PositionRange{Start: 0, End: 6},
302 fn: "by",
303 },
304 {
305 description: "matches an aggregation wrapping a nested call",
306 expr: "sum(rate(foo[5m]))",
307 within: posrange.PositionRange{Start: 0, End: 18},
308 fn: "sum",
309 },
310 }
311
312 for _, tc := range testCases {
313 t.Run(tc.description, func(t *testing.T) {
314 got := source.FindFuncNamePosition(tc.expr, tc.within, tc.fn)
315 matchPositionSnapshot(t, fmt.Sprintf(
316 "fn: %s\nexpr:\n%s",
317 tc.fn,
318 highlightRange(tc.expr, got),
319 ))
320 })
321 }
322}
323
324func TestFindFuncPosition(t *testing.T) {
325 type testCase struct {
326 description string
327 expr string
328 fn string
329 outside []posrange.PositionRange
330 within posrange.PositionRange
331 }
332
333 testCases := []testCase{
334 {
335 description: "returns within when fn is absent",
336 expr: "x sum(foo)",
337 within: posrange.PositionRange{Start: 0, End: 10},
338 fn: "rate",
339 outside: nil,
340 },
341 {
342 description: "matches the whole call when no outside ranges are given",
343 expr: "x sum(foo)",
344 within: posrange.PositionRange{Start: 0, End: 10},
345 fn: "sum",
346 outside: nil,
347 },
348 {
349 description: "returns within when every match is contained in an outside range",
350 expr: "x sum(foo)",
351 within: posrange.PositionRange{Start: 0, End: 10},
352 fn: "sum",
353 outside: []posrange.PositionRange{{Start: 0, End: 100}},
354 },
355 {
356 description: "matches when the outside range does not contain the match",
357 expr: "x sum(foo)",
358 within: posrange.PositionRange{Start: 0, End: 10},
359 fn: "sum",
360 outside: []posrange.PositionRange{{Start: 50, End: 60}},
361 },
362 {
363 description: "skips fn not followed by a paren and matches a later call",
364 expr: "sumx + sum(foo)",
365 within: posrange.PositionRange{Start: 0, End: 15},
366 fn: "sum",
367 outside: nil,
368 },
369 {
370 description: "returns within when the call has no closing paren",
371 expr: "sum(foo",
372 within: posrange.PositionRange{Start: 0, End: 7},
373 fn: "sum",
374 outside: nil,
375 },
376 {
377 description: "matches the first call when the name repeats",
378 expr: "sum(sum) by(sum)",
379 within: posrange.PositionRange{Start: 0, End: 16},
380 fn: "sum",
381 outside: nil,
382 },
383 {
384 description: "matches a grouping keyword call",
385 expr: "sum(sum) by(sum)",
386 within: posrange.PositionRange{Start: 0, End: 16},
387 fn: "by",
388 outside: nil,
389 },
390 {
391 description: "matches a multiline grouping with mixed whitespace",
392 expr: "sum(foo)\nby(\n\tjob\n)",
393 within: posrange.PositionRange{Start: 0, End: 19},
394 fn: "by",
395 outside: nil,
396 },
397 {
398 description: "ends a nested call at the first closing paren",
399 expr: "scalar(vector(1))",
400 within: posrange.PositionRange{Start: 0, End: 17},
401 fn: "scalar",
402 outside: nil,
403 },
404 {
405 description: "finds the keyword between operands when the operands are excluded",
406 expr: "aa * on(b) cc",
407 within: posrange.PositionRange{Start: 0, End: 13},
408 fn: "on",
409 outside: []posrange.PositionRange{{Start: 0, End: 2}, {Start: 11, End: 13}},
410 },
411 {
412 description: "returns within when the only match is contained in an outside range",
413 expr: "x on(b)",
414 within: posrange.PositionRange{Start: 0, End: 7},
415 fn: "on",
416 outside: []posrange.PositionRange{{Start: 0, End: 100}},
417 },
418 }
419
420 for _, tc := range testCases {
421 t.Run(tc.description, func(t *testing.T) {
422 got := source.FindFuncPosition(tc.expr, tc.within, tc.fn, tc.outside)
423 matchPositionSnapshot(t, fmt.Sprintf(
424 "fn: %s\noutside: %s\nexpr:\n%s",
425 tc.fn,
426 renderOutside(tc.outside),
427 highlightRange(tc.expr, got),
428 ))
429 })
430 }
431}
432
433func TestFindMatcherPos(t *testing.T) {
434 type testCase struct {
435 matcher *labels.Matcher
436 description string
437 expr string
438 within posrange.PositionRange
439 }
440
441 testCases := []testCase{
442 {
443 description: "matches name op and quoted value",
444 expr: `foo{job=~"bar"}`,
445 within: posrange.PositionRange{Start: 0, End: 15},
446 matcher: labels.MustNewMatcher(labels.MatchRegexp, "job", "bar"),
447 },
448 {
449 description: "returns within when the matcher is absent",
450 expr: `foo{job=~"bar"}`,
451 within: posrange.PositionRange{Start: 0, End: 15},
452 matcher: labels.MustNewMatcher(labels.MatchRegexp, "job", "baz"),
453 },
454 {
455 description: "matches an equality matcher",
456 expr: `foo{job="bar"}`,
457 within: posrange.PositionRange{Start: 0, End: 14},
458 matcher: labels.MustNewMatcher(labels.MatchEqual, "job", "bar"),
459 },
460 {
461 description: "matches a negative equality matcher",
462 expr: `foo{job!="bar"}`,
463 within: posrange.PositionRange{Start: 0, End: 15},
464 matcher: labels.MustNewMatcher(labels.MatchNotEqual, "job", "bar"),
465 },
466 {
467 description: "matches a negative regexp matcher",
468 expr: `foo{job!~"bar"}`,
469 within: posrange.PositionRange{Start: 0, End: 15},
470 matcher: labels.MustNewMatcher(labels.MatchNotRegexp, "job", "bar"),
471 },
472 {
473 description: "matches the requested matcher among several",
474 expr: `foo{a="1",job=~"bar"}`,
475 within: posrange.PositionRange{Start: 0, End: 21},
476 matcher: labels.MustNewMatcher(labels.MatchRegexp, "job", "bar"),
477 },
478 {
479 description: "returns within when the value contains an escaped quote",
480 expr: `foo{job="a\"b"}`,
481 within: posrange.PositionRange{Start: 0, End: 15},
482 matcher: labels.MustNewMatcher(labels.MatchEqual, "job", `a"b`),
483 },
484 }
485
486 for _, tc := range testCases {
487 t.Run(tc.description, func(t *testing.T) {
488 got := source.FindMatcherPos(tc.expr, tc.within, tc.matcher)
489 matchPositionSnapshot(t, fmt.Sprintf(
490 "matcher: %s\nexpr:\n%s",
491 tc.matcher.String(),
492 highlightRange(tc.expr, got),
493 ))
494 })
495 }
496}
497