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/source_test.go

901lines · modecode

1package source_test
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strconv"
9 "strings"
10 "testing"
11
12 "github.com/gkampitakis/go-snaps/snaps"
13 "github.com/stretchr/testify/require"
14 "go.yaml.in/yaml/v3"
15
16 "github.com/cloudflare/pint/internal/parser"
17 "github.com/cloudflare/pint/internal/parser/source"
18
19 promParser "github.com/prometheus/prometheus/promql/parser"
20)
21
22func TestMain(t *testing.M) {
23 v := t.Run()
24 if _, err := snaps.Clean(t, snaps.CleanOpts{Sort: true}); err != nil {
25 fmt.Printf("snaps.Clean() returned an error: %s", err)
26 os.Exit(100)
27 }
28 os.Exit(v)
29}
30
31var testCases = []string{
32 "1",
33 "1 / 5",
34 "(2 ^ 5) == bool 5",
35 "(2 ^ 5 + 11) % 5 <= bool 2",
36 "(2 ^ 5 + 11) % 5 >= bool 20",
37 "(2 ^ 5 + 11) % 5 <= bool 3",
38 "(2 ^ 5 + 11) % 5 < bool 1",
39 "20 - 15 < bool 1",
40 "2 * 5",
41 "(foo or bar) * 5",
42 "(foo or vector(2)) * 5",
43 "(foo or vector(5)) * (vector(2) or bar)",
44 `1 > bool 0`,
45 `20 > bool 10`,
46 `"test"`,
47 "foo",
48 "(foo > 1) > bool 1",
49 "foo > bool 5",
50 "foo > bool 5 == 1",
51 "foo > bool bar",
52 "(foo > bool bar) == 0",
53 "foo > bool on(instance) bar",
54 "(foo > bool on(instance) bar) == 1",
55 "foo > bool on(instance) group_left(version) bar",
56 "bar > bool on(instance) group_right(version) foo",
57 "foo and bar > bool 0",
58 "foo offset 5m",
59 `foo{job="bar"}`,
60 `foo{job=""}`,
61 `foo{job="bar"} or bar{job="foo"}`,
62 `foo{a="bar"} or bar{b="foo"}`,
63 "foo[5m]",
64 "prometheus_build_info[2m:1m]",
65 "deriv(rate(distance_covered_meters_total[1m])[5m:1m])",
66 "foo - 1",
67 "foo / 5",
68 "-foo",
69 `sum(foo{job="myjob"})`,
70 `sum(count(foo{job="myjob"}) by(instance))`,
71 `sum(foo{job="myjob"}) > 20`,
72 `sum(foo{job="myjob"}) without(job)`,
73 `sum(foo) by(job)`,
74 `sum(foo{job="myjob"}) by(job)`,
75 `abs(foo{job="myjob"} offset 5m)`,
76 `abs(foo{job="myjob"} or bar{cluster="dev"})`,
77 `sum(foo{job="myjob"} or bar{cluster="dev"}) without(instance)`,
78 `sum(foo{job="myjob"}) without(instance)`,
79 `min(foo{job="myjob"}) / max(foo{job="myjob"})`,
80 `max(foo{job="myjob"}) / min(foo{job="myjob"})`,
81 `avg(foo{job="myjob"}) by(job)`,
82 `group(foo) by(job)`,
83 `stddev(rate(foo[5m]))`,
84 `stdvar(rate(foo[5m]))`,
85 `stddev_over_time(foo[5m])`,
86 `stdvar_over_time(foo[5m])`,
87 `quantile(0.9, rate(foo[5m]))`,
88 `count_values("version", build_version)`,
89 `count_values("version", build_version) without(job)`,
90 `count_values("version", build_version{job="foo"}) without(job)`,
91 `count_values("version", build_version) by(job)`,
92 `topk(10, foo{job="myjob"}) > 10`,
93 `topk(10, foo or bar)`,
94 `rate(foo[10m])`,
95 `sum(rate(foo[10m])) without(instance)`,
96 `foo{job="foo"} / bar`,
97 `foo{job="foo"} * on(instance) bar`,
98 `foo{job="foo"} * on(instance) group_left(bar) bar`,
99 `foo{job="foo"} * on(instance) group_left(cluster) bar{cluster="bar", ignored="true"}`,
100 `foo{job="foo", ignored="true"} * on(instance) group_right(job) bar{cluster="bar"}`,
101 `count(foo / bar)`,
102 `count(up{job="a"} / on () up{job="b"})`,
103 `count(up{job="a"} / on (env) up{job="b"})`,
104 `foo{job="foo", instance="1"} and bar`,
105 `foo{job="foo", instance="1"} and on(cluster) bar`,
106 `topk(10, foo)`,
107 `topk(10, foo) without(cluster)`,
108 `topk(10, foo) by(cluster)`,
109 `bottomk(10, sum(rate(foo[5m])) without(job))`,
110 `foo or bar`,
111 `foo or bar or baz`,
112 `(foo or bar) or baz`,
113 `foo unless bar`,
114 `foo unless bar > 5`,
115 `foo unless bar unless baz`,
116 `count(sum(up{job="foo", cluster="dev"}) by(job, cluster) == 0) without(job, cluster)`,
117 "year()",
118 "year(foo)",
119 `label_join(up{job="api-server",src1="a",src2="b",src3="c"}, "foo", ",", "src1", "src2", "src3")`,
120 `
121(
122 sum(foo:sum > 0) without(notify)
123 * on(job) group_left(notify)
124 job:notify
125)
126and on(job)
127sum(foo:count) by(job) > 20`,
128 `container_file_descriptors / on (instance, app_name) container_ulimits_soft{ulimit="max_open_files"}`,
129 `container_file_descriptors / on (instance, app_name) group_left() container_ulimits_soft{ulimit="max_open_files"}`,
130 `absent(foo{job="bar"})`,
131 `absent(foo{job="bar", cluster!="dev", instance=~".+", env="prod"})`,
132 `absent(sum(foo) by(job, instance))`,
133 `absent(foo{job="prometheus", xxx="1"}) AND on(job) prometheus_build_info`,
134 `1 + sum(foo) by(notjob)`,
135 `count(node_exporter_build_info) by (instance, version) != ignoring(package,version) group_left(foo) count(deb_package_version) by (instance, version, package)`,
136 `absent(foo) or absent(bar)`,
137 `absent(vector(1))`,
138 `absent_over_time(foo[5m]) or absent(bar)`,
139 `bar * on() group_right(cluster, env) absent(foo{job="xxx"})`,
140 `bar * on() group_right() absent(foo{job="xxx"})`,
141 "vector(1)",
142 "vector(scalar(foo))",
143 "vector(0.0 >= bool 0.5) == 1",
144 `sum_over_time(foo{job="myjob"}[5m])`,
145 `days_in_month()`,
146 `days_in_month(foo{job="foo"})`,
147 `label_replace(up{job="api-server",service="a:c"}, "foo", "$1", "service", "(.*):.*")`,
148 `label_replace(sum by (pod) (pod_status) > 0, "cluster", "$1", "pod", "(.*)")`,
149 `(time() - my_metric) > 5*3600`,
150 `up{instance="a", job="prometheus"} * ignoring(job) up{instance="a", job="pint"}`,
151 `
152avg without(router, colo_id, instance) (router_anycast_prefix_enabled{cidr_use_case!~".*offpeak.*"})
153< 0.5 > 0
154or sum without(router, colo_id, instance) (router_anycast_prefix_enabled{cidr_use_case=~".*tier1.*"})
155< on() count(colo_router_tier:disabled_pops:max{tier="1",router=~"edge.*"}) * 0.4 > 0
156or avg without(router, colo_id, instance) (router_anycast_prefix_enabled{cidr_use_case=~".*regional.*"})
157< 0.1 > 0
158`,
159 `label_replace(sum(foo) without(instance), "instance", "none", "", "")`,
160 `
161sum by (region, target, colo_name) (
162 sum_over_time(probe_success{job="abc"}[5m])
163 or
164 vector(1)
165) == 0`,
166 `vector(1) or foo`,
167 `vector(0) > 0`,
168 `vector(0) > vector(1)`,
169 `sum(foo or vector(0)) > 0`,
170 `(sum(foo or vector(1)) > 0) == 2`,
171 `(sum(foo or vector(1)) > 0) != 2`,
172 `(sum(foo or vector(2)) > 0) != 2`,
173 `(sum(sometimes{foo!="bar"} or vector(0)))
174or
175((bob > 10) or sum(foo) or vector(1))`,
176 `
177(
178 sum(sometimes{foo!="bar"})
179 or
180 vector(1)
181) and (
182 ((bob > 10) or sum(bar))
183 or
184 notfound > 0
185)`,
186 "foo offset 5m > 5",
187 `
188(rate(metric2[5m]) or vector(0)) +
189(rate(metric1[5m]) or vector(1)) +
190(rate(metric3{log_name="samplerd"}[5m]) or vector(2)) > 0
191`,
192 `label_replace(vector(1), "nexthop_tag", "$1", "nexthop", "(.+)")`,
193 `(sum(foo{job="myjob"}))`,
194 `(-foo{job="myjob"})`,
195 "\n((( group(vector(0)) ))) > 0",
196 "1 > bool 5",
197 `prometheus_ready{job="prometheus"} unless vector(0)`,
198 `prometheus_ready{job="prometheus"} unless on() vector(0)`,
199 `prometheus_ready{job="prometheus"} unless on(job) vector(0)`,
200 `
201max by (instance, cluster) (cf_node_role{kubernetes_role="master",role="kubernetes"})
202unless
203 sum by (instance, cluster) (time() - node_systemd_timer_last_trigger_seconds{name=~"etcd-defrag-.*.timer"})
204 * on (instance) group_left (cluster)
205 cf_node_role{kubernetes_role="master",role="kubernetes"}
206`,
207 `foo{a="1"} * on() bar{b="2"}`,
208 `foo{a="1"} * on(instance) group_left(c,d) bar{b="2"}`,
209 `foo{a="1"} * on(instance) group_right(c,d) bar{b="2"}`,
210 `foo{a="1"} * on(instance) sum(bar{b="2"})`,
211 `foo{a="1"} * on(instance) group_left(c,d) sum(bar{b="2"})`,
212 `sum(foo{a="1"}) * on(instance) group_right(c,d) bar{b="2"}`,
213 `foo{a="1"} * on(instance) group_left(c,d) sum(bar{b="2"}) without(instance)`,
214 `sum(foo{a="1"}) without(instance) * on(instance) group_right(c,d) bar{b="2"}`,
215 `
216 max without (source_instance) (
217 increase(kernel_device_io_errors_total{device!~"loop.+"}[120m]) > 3 unless on(instance, device) (
218 increase(kernel_device_io_soft_errors_total{device!~"loop.+"}[125m])*2 > increase(kernel_device_io_errors_total[120m])
219 )
220 and on(device, instance) absent(node_disk_info)
221 ) * on(instance) group_left(group) label_replace(salt_highstate_runner_configured_minions, "instance", "$1", "minion", "(.+)")
222`,
223 `sum(foo{a="1"}) by(job) * on() bar{b="2"}`,
224 `sum(sum(foo) without(job)) by(job)`,
225 `
226prometheus:scrape_series_added:since_gc:sum
227* on(prometheus) group_left()
228label_replace(
229 max(max_over_time(go_memstats_alloc_bytes{job="prometheus"}[2h])) by(instance)
230 /
231 max(max_over_time(prometheus_tsdb_head_series[2h])) by(instance),
232 "prometheus", "$1",
233 "instance", "(.+)"
234)
235`,
236 `(day_of_week() == 6 and hour() < 1) or vector(1)`,
237 `
238sum by (foo, bar) (
239 rate(errors_total[5m])
240 * on (instance) group_left (bob, alice)
241 server_errors_total
242)`,
243 `1 - (foo or vector(0)) < 0.999`,
244 `
245(
246 vector(1) and month() == 2
247) or vector(0)
248`,
249 `count by (region) (stddev by (colo_name, region) (error_total))`,
250 `
251(
252 avg(
253 rate(foo_rejections[6h])
254 or
255 vector(0)
256 ) by (colo_name)
257 /
258 (
259 avg(
260 rate(foo_total[6h])
261 or
262 vector(1)
263 ) by (colo_name)
264 )
265) > 5
266*
267(
268 avg(
269 rate(foo_rejections[6h] offset 1d)
270 or
271 vector(0)
272 ) by (colo_name)
273 /
274 avg(
275 rate(foo_total[6h] offset 1d)
276 or
277 vector(1)
278 ) by (colo_name)
279) and on (colo_name) (colo_job:foo_total:rate2m or vector(0)) > 80
280 and on (colo_name) (colo_job:foo_total:rate2m offset 1d or vector(0)) > 80
281`,
282 `sum(selector) / sum(selector offset 30m) > 5`,
283 `
284count by (dc) (
285 max(0 < (token_expiration - time()) < (6*60*60)) by (instance)
286 * on (instance) group_right label_replace(
287 configured_minions, "instance", "$1", "minion", "(.+)")
288 ) > 5`,
289 `topk(10, prometheus_build_info*prometheus_ready)`,
290 `bottomk(10, prometheus_build_info*prometheus_ready)`,
291 `histogram_fraction(0, 0.1, metric)`,
292 `foo * foo `,
293 `foo + on(__name__, job) foo `,
294 `foo + on(__name__, job) group_left foo `,
295 `foo + on(__name__, job) group_right foo `,
296 `group by (env, cluster) (
297 up{env="prod", job="foo"} and on (instance) (services_enabled == 999)
298 )`,
299 `group by (env, cluster) (
300 up{env="prod", job="foo"} * on (instance) (services_enabled == 999)
301 )`,
302 `foo / on(instance) sum(bar)`,
303 `foo / on(instance) group_left(cluster) sum(bar)`,
304 `sum(bar) / on(instance) group_right(cluster) foo`,
305 `sum(bar) * on(cluster) sum(foo)`,
306 `
307group by (colo_name, instance, tier, animal, brand, sliver, pop_name) (
308 up{node_status="v", job="node_exporter"}
309 and on (instance) (metal_services_enabled == 999)
310 * on (colo_name) group_left(tier, animal, brand, pop_name) colo_metadata{colo_status="v"}
311 * on (instance) group_left (sliver) sliver_metadata{node_status="v"}
312)`,
313 `
314up{node_status="v", job="node_exporter"}
315* on (colo_name) group_left(tier) colo_metadata{colo_status="v"}
316* on (instance) group_left (sliver) sliver_metadata{node_status="v"}
317`, // all group_left() labels are joined to the left
318 `
319up{node_status="v", job="node_exporter"}
320and on (colo_name) colo_metadata{colo_status="v"}
321* on (instance) group_left (sliver) sliver_metadata{node_status="v"}
322`, // all group_left() labels are NOT joined to the left
323 `
324colo_metadata{colo_status="v"} * on (colo_name) group_right(tier, animal, brand, pop_name)
325sliver_metadata{node_status="v"} * on (instance) group_right (sliver)
326(metal_services_enabled == 999) * on (instance)
327up{node_status="v", job="node_exporter"}
328`, // only instance label will be present
329 `
330colo_metadata{colo_status="v"} * on (colo_name) group_right(tier, animal, brand, pop_name)
331sliver_metadata{node_status="v"} * on (instance) group_right (sliver)
332(metal_services_enabled == 999) * on (instance) group_right()
333up{node_status="v", job="node_exporter"}
334`, // no labels are joined to the right
335 `
336sliver_metadata{node_status="v"} * on (instance) group_right (sliver)
337(metal_services_enabled == 999) * on (colo_name) group_left(tier, animal, brand, pop_name)
338colo_metadata{colo_status="v"}
339`, // labels from both group_left and group_right are joined
340 `
341colo_metadata * on (colo_name) group_right(tier, animal, brand, pop_name)
342sliver_metadata * on (instance) group_right (sliver)
343metal_services_enabled
344`, // only sliver and tier are joined to the right
345 `
346colo_metadata * on (colo_name) group_right(tier, animal, brand, pop_name)
347(
348 sliver_metadata * on (instance) group_right (sliver)
349 metal_services_enabled
350)
351`, // all labels are joined to the right
352 `
353up{node_status="v", job="node_exporter"}
354* on(instance) group_left(node_status) sliver_metadata
355`, // group_left on a label already guaranteed on the left
356 `services_enabled{job=""}`,
357 `
358group by (cluster, namespace, workload, workload_type, pod) (
359 label_join(
360 label_join(
361 group by (cluster, namespace, job_name, pod) (
362 label_join(
363 kube_pod_owner{job="kube-state-metrics", owner_kind="Job"}
364 , "job_name", "", "owner_name")
365 )
366 * on (cluster, namespace, job_name) group_left(owner_kind, owner_name)
367 group by (cluster, namespace, job_name, owner_kind, owner_name) (
368 kube_job_owner{job="kube-state-metrics", owner_kind!="Pod", owner_kind!=""}
369 )
370 , "workload", "", "owner_name")
371 , "workload_type", "", "owner_kind")
372)
373`,
374 `foo{job="xxx"} + on(job) group_right(instance) bar{}`,
375 `foo{job="xxx"} + ignoring(job) group_right(instance) bar{job="zzz"}`,
376 `foo or ignoring(job) bar`,
377 `foo or on(job) bar`,
378 `
379(
380 sum(rate(panics_total{module_name=~".+"}[5m])) by (colo_name, module_name)
381 /
382 ignoring(module_name) group_left
383 sum(colo:requests:rate5m) by (colo_name)
384) > 0.01
385`,
386 `foo atan2 bar`,
387 `foo offset -5m`,
388 `foo @ 1609459200`,
389 `foo @ start()`,
390 `foo @ end()`,
391 `foo[5m] @ 1609459200`,
392 `rate(foo[5m] @ start())`,
393 `histogram_quantile(0.9, rate(foo[5m]))`,
394 `predict_linear(foo[5m], 3600)`,
395 `clamp(foo, 0, 100)`,
396 `irate(foo[5m])`,
397 `delta(foo[5m])`,
398 `idelta(foo[5m])`,
399 `increase(foo[5m])`,
400 `changes(foo[5m])`,
401 `resets(foo[5m])`,
402 `timestamp(foo)`,
403 `sort(foo)`,
404 `sort_desc(foo)`,
405 `pi()`,
406 `sgn(foo)`,
407 `clamp_min(foo, 0)`,
408 `clamp_max(foo, 100)`,
409 `round(foo, 0.1)`,
410 `exp(foo)`,
411 `ln(foo)`,
412 `log2(foo)`,
413 `log10(foo)`,
414 `sqrt(foo)`,
415 `ceil(foo)`,
416 `floor(foo)`,
417 `avg_over_time(foo[5m])`,
418 `min_over_time(foo[5m])`,
419 `max_over_time(foo[5m])`,
420 `count_over_time(foo[5m])`,
421 `last_over_time(foo[5m])`,
422 `present_over_time(foo[5m])`,
423 `quantile_over_time(0.9, foo[5m])`,
424 `absent_over_time(foo[5m])`,
425 `deg(foo)`,
426 `rad(foo)`,
427 `acos(foo)`,
428 `asin(foo)`,
429 `atan(foo)`,
430 `cos(foo)`,
431 `sin(foo)`,
432 `tan(foo)`,
433 `acosh(foo)`,
434 `asinh(foo)`,
435 `atanh(foo)`,
436 `cosh(foo)`,
437 `sinh(foo)`,
438 `tanh(foo)`,
439 `histogram_count(foo)`,
440 `histogram_sum(foo)`,
441 `histogram_avg(foo)`,
442 `histogram_fraction(0, 0.1, foo)`,
443 `histogram_stddev(foo)`,
444 `histogram_stdvar(foo)`,
445 `min(foo) by(job)`,
446 `max(foo) without(job)`,
447 `sum(foo * on(job) group_left(cluster) bar) by(cluster)`,
448 `sum(foo * on(job) group_left(cluster) bar) without(instance)`,
449 `foo atan2 on(job) bar`,
450 `foo atan2 on(job) group_left(cluster) bar`,
451 `foo != bool bar`,
452 `foo == on(job) bar`,
453 `foo > on(job) group_left() bar`,
454 `foo < on(job) group_right() bar`,
455 `foo >= on(job) group_left(cluster) bar`,
456 `foo <= on(job) group_right(cluster) bar`,
457 `foo * ignoring(job) group_left() bar`,
458 `foo / ignoring(job) group_right() bar`,
459 `foo % bar`,
460 `foo ^ bar`,
461 `sum(foo * on(job) group_left(cluster) bar) by(job) * on(job) group_left(cluster) baz`,
462 `sum by (colo) (metric_a) / scalar(sum(metric_b))`, // {colo} / <number> to avoid on()
463 `clamp(foo, scalar(bar{job=~"test"}), 10)`,
464 `histogram_quantile(scalar(threshold), rate(foo[5m]))`,
465 `round(foo, scalar(precision_metric))`,
466 `predict_linear(foo[5m], scalar(horizon))`,
467 `foo + scalar(bar)`,
468 `
469(
470 clamp_min(
471 sum(foo{status_class="5xx"})
472 -
473 (sum(bar{status_class="5xx"}) or vector(0)),
474 0
475 )
476 /
477 sum(foo)
478) > 0.05`,
479 `abs(sum(foo{job="bar"}))`,
480 `ceil(sum(foo{job="bar"}))`,
481 `hour(sum(foo{job="bar"}))`,
482 `ln(sum(foo{job="bar"}))`,
483 `histogram_quantile(0.9, sum(foo{job="bar"}))`,
484 `timestamp(sum(foo{job="bar"}))`,
485}
486
487func TestLabelsSource(t *testing.T) {
488 type Snapshot struct {
489 Expr string
490 Output []*source.Source
491 }
492
493 _, file, _, ok := runtime.Caller(0)
494 require.True(t, ok, "can't get caller function")
495 file = strings.TrimSuffix(filepath.Base(file), ".go")
496
497 done := map[string]struct{}{}
498 for i, expr := range testCases {
499 t.Run(strconv.Itoa(i+1), func(t *testing.T) {
500 if _, ok := done[expr]; ok {
501 t.Fatalf("Duplicated query: %s", expr)
502 }
503 done[expr] = struct{}{}
504
505 t.Log(expr)
506 n, err := parser.DecodeExpr(expr)
507 if err != nil {
508 t.Error(err)
509 t.FailNow()
510 }
511 output := source.LabelsSource(expr, n.Expr)
512
513 for _, src := range output {
514 src.WalkSources(func(s *source.Source, _ *source.Join, _ *source.Unless) {
515 require.Positive(t, s.Position.End, "empty position %+v", s)
516 if s.DeadInfo != nil {
517 require.Positive(t, s.DeadInfo.Fragment.End, "empty dead position %+v", s)
518 }
519 })
520 }
521
522 snap := Snapshot{
523 Expr: expr,
524 Output: output,
525 }
526 d, err := yaml.Marshal(snap)
527 require.NoError(t, err, "failed to YAML encode snapshots")
528 snaps.WithConfig(snaps.Dir("."), snaps.Filename(file)).MatchSnapshot(t, string(d))
529 })
530 }
531}
532
533func TestLabelsSourceCallCoverage(t *testing.T) {
534 for name, def := range promParser.Functions {
535 t.Run(name, func(t *testing.T) {
536 if def.Experimental {
537 t.SkipNow()
538 }
539
540 var b strings.Builder
541 b.WriteString(name)
542 b.WriteRune('(')
543 for i, at := range def.ArgTypes {
544 if i > 0 {
545 b.WriteString(", ")
546 }
547 switch at {
548 case promParser.ValueTypeNone:
549 case promParser.ValueTypeScalar:
550 b.WriteRune('1')
551 case promParser.ValueTypeVector:
552 b.WriteString("http_requests_total")
553 case promParser.ValueTypeMatrix:
554 b.WriteString("http_requests_total[2m]")
555 case promParser.ValueTypeString:
556 b.WriteString(`"foo"`)
557 }
558 }
559 b.WriteRune(')')
560
561 n, err := parser.DecodeExpr(b.String())
562 if err != nil {
563 t.Error(err)
564 t.FailNow()
565 }
566 output := source.LabelsSource(b.String(), n.Expr)
567 require.Len(t, output, 1)
568 require.NotEmpty(t, output[0].Operations)
569 call, ok := source.MostOuterOperation[*promParser.Call](output[0])
570 require.True(t, ok, "no call found in operations for: %q ~> %+v", b.String(), output)
571 require.NotNil(t, call, "no call detected in: %q ~> %+v", b.String(), output)
572 require.Equal(t, name, output[0].Operation())
573 require.Equal(t, def.ReturnType, output[0].Returns, "incorrect return type on Source{}")
574 })
575 }
576}
577
578// Verifies that experimental functions can be parsed and that
579// LabelsSource produces output.
580func TestLabelsSourceCallCoverageExperimental(t *testing.T) {
581 // info() second arg must be a bare label selector, not a named metric.
582 overrides := map[string]string{
583 "info": `info(http_requests_total, {job="foo"})`,
584 }
585
586 for name, def := range promParser.Functions {
587 t.Run(name, func(t *testing.T) {
588 if !def.Experimental {
589 t.SkipNow()
590 }
591
592 var b strings.Builder
593 if override, ok := overrides[name]; ok {
594 b.WriteString(override)
595 } else {
596 b.WriteString(name)
597 b.WriteRune('(')
598 for i, at := range def.ArgTypes {
599 if i > 0 {
600 b.WriteString(", ")
601 }
602 switch at {
603 case promParser.ValueTypeNone:
604 case promParser.ValueTypeScalar:
605 b.WriteRune('1')
606 case promParser.ValueTypeVector:
607 b.WriteString("http_requests_total")
608 case promParser.ValueTypeMatrix:
609 b.WriteString("http_requests_total[2m]")
610 case promParser.ValueTypeString:
611 b.WriteString(`"foo"`)
612 }
613 }
614 b.WriteRune(')')
615 }
616
617 n, err := parser.DecodeExpr(b.String())
618 require.NoError(t, err, "unexpected parse error for: %s", b.String())
619
620 output := source.LabelsSource(b.String(), n.Expr)
621 require.NotEmpty(t, output, "LabelsSource returned no sources for: %s", b.String())
622 })
623 }
624}
625
626func TestLabelsSourceCallCoverageFail(t *testing.T) {
627 n := &parser.PromQLNode{
628 Expr: &promParser.Call{
629 Func: &promParser.Function{
630 Name: "fake_call",
631 },
632 },
633 }
634 output := source.LabelsSource("fake_call()", n.Expr)
635 require.Len(t, output, 1)
636 call, ok := source.MostOuterOperation[*promParser.Call](output[0])
637 require.False(t, ok, "no call should have been detected in fake function, got: %v", ok)
638 require.Nil(t, call, "no call should have been detected in fake function, got: %+v", call)
639}
640
641func TestVectorOperation(t *testing.T) {
642 n := &parser.PromQLNode{
643 Expr: &promParser.NumberLiteral{
644 Val: 1,
645 },
646 }
647 output := source.LabelsSource("1", n.Expr)
648 require.Len(t, output, 1)
649 require.Empty(t, output[0].Operation())
650}
651
652func TestDeadLabelKindUnknownString(t *testing.T) {
653 var dl source.DeadLabelKind = 100
654 require.Equal(t, "unknown", dl.String())
655}
656
657// Verifies that RangeSelectorMode.MarshalYAML returns "default" for the zero value.
658func TestRangeSelectorModeDefaultMarshalYAML(t *testing.T) {
659 var rsm source.RangeSelectorMode
660 val, err := rsm.MarshalYAML()
661 require.NoError(t, err)
662 require.Equal(t, "default", val)
663}
664
665// Verifies that LabelsSource correctly processes expressions that require
666// experimental parser features.
667func TestLabelsSourceWithFeatures(t *testing.T) {
668 type Snapshot struct {
669 Expr string
670 Output []*source.Source
671 }
672
673 _, file, _, ok := runtime.Caller(0)
674 require.True(t, ok, "can't get caller function")
675 file = strings.TrimSuffix(filepath.Base(file), ".go")
676
677 type testCaseT struct {
678 description string
679 expr string
680 }
681
682 testCases := []testCaseT{
683 // Experimental function: mad_over_time.
684 {
685 description: "mad_over_time",
686 expr: `mad_over_time(foo[5m])`,
687 },
688 // Experimental aggregator: limitk.
689 {
690 description: "limitk",
691 expr: `limitk(5, foo)`,
692 },
693 // Experimental function: sort_by_label.
694 {
695 description: "sort_by_label",
696 expr: `sort_by_label(foo, "job")`,
697 },
698 // Duration expression in matrix selector.
699 {
700 description: "duration expression in matrix selector",
701 expr: `foo[11s+10s]`,
702 },
703 // Duration expression inside rate().
704 {
705 description: "duration expression inside rate",
706 expr: `rate(foo[5m+1m])`,
707 },
708 // Anchored modifier on vector selector.
709 {
710 description: "anchored vector selector",
711 expr: `foo anchored`,
712 },
713 // Smoothed modifier on matrix selector.
714 {
715 description: "smoothed matrix selector inside rate",
716 expr: `rate(foo[5m] smoothed)`,
717 },
718 // Fill modifier on binop.
719 {
720 description: "binop fill modifier",
721 expr: `foo + on(job) fill(0) bar`,
722 },
723 // fill_left modifier on binop.
724 {
725 description: "binop fill_left modifier",
726 expr: `foo + on(job) fill_left(0) bar`,
727 },
728 // Experimental function with fill modifier.
729 {
730 description: "experimental function with fill modifier",
731 expr: `mad_over_time(foo[5m]) + on(job) fill(0) bar`,
732 },
733 // Experimental function with smoothed modifier.
734 {
735 description: "experimental function with smoothed modifier",
736 expr: `mad_over_time(foo[5m] smoothed)`,
737 },
738 // Duration expression with smoothed modifier.
739 {
740 description: "duration expression with smoothed modifier",
741 expr: `rate(foo[5m+1m] smoothed)`,
742 },
743 // fill_left modifier on binop.
744 {
745 description: "binop fill_left modifier only",
746 expr: `foo + on(job) fill_left(0) bar`,
747 },
748 // fill_right modifier on binop.
749 {
750 description: "binop fill_right modifier only",
751 expr: `foo + on(job) fill_right(0) bar`,
752 },
753 // Duration expression in subquery range.
754 {
755 description: "duration expression in subquery range",
756 expr: `max_over_time(rate(foo[5m])[1h+10m:5m])`,
757 },
758 // Duration expression in subquery step.
759 {
760 description: "duration expression in subquery step",
761 expr: `max_over_time(foo[1h:5m+1m])`,
762 },
763 // Duration expression in both subquery range and step.
764 {
765 description: "duration expression in subquery range and step",
766 expr: `max_over_time(rate(foo[5m])[1h+10m:5m+1m])`,
767 },
768 // Duration expression in vector selector offset.
769 {
770 description: "duration expression in vector offset",
771 expr: `foo offset 5m+1m`,
772 },
773 // Duration expression in vector selector offset using parenthesized
774 // duration to trigger OriginalOffsetExpr on VectorSelector.
775 {
776 description: "duration expression in vector selector offset expr",
777 expr: `foo offset (5m+1m)`,
778 },
779 // Duration expression in subquery offset using parenthesized duration
780 // to trigger OriginalOffsetExpr on SubqueryExpr.
781 {
782 description: "duration expression in subquery offset expr",
783 expr: `max_over_time(foo[1h:5m] offset (5m+1m))`,
784 },
785 // Fill modifier with group_right (CardOneToMany).
786 {
787 description: "fill with group_right",
788 expr: `foo + on(job) group_right() fill(0) bar`,
789 },
790 // Fill modifier with group_left (CardManyToOne).
791 {
792 description: "fill with group_left",
793 expr: `foo + on(job) group_left() fill(0) bar`,
794 },
795 // All four features combined in a single query.
796 {
797 description: "all four features combined",
798 expr: `mad_over_time(foo[5m+1m] smoothed) + on(job) fill(0) bar`,
799 },
800 // Experimental aggregator: limitk.
801 {
802 description: "limitk aggregator",
803 expr: `limitk(5, foo) by(job)`,
804 },
805 // Experimental aggregator: limit_ratio.
806 {
807 description: "limit_ratio aggregator",
808 expr: `limit_ratio(0.5, foo) by(job)`,
809 },
810 // Experimental aggregator: limitk without grouping.
811 {
812 description: "limitk without grouping",
813 expr: `limitk(5, foo)`,
814 },
815 // Experimental aggregator: limit_ratio with without().
816 {
817 description: "limit_ratio with without",
818 expr: `limit_ratio(0.5, foo) without(job)`,
819 },
820 // Experimental function: start() and end() used to compute query range.
821 {
822 description: "start and end functions",
823 expr: `foo / (end() - start())`,
824 },
825 // Experimental function: range() used as matrix selector duration.
826 {
827 description: "range function in rate",
828 expr: `rate(foo_total[range()])`,
829 },
830 // Experimental function: step() used as matrix selector duration.
831 {
832 description: "step function in rate",
833 expr: `rate(foo_total[step() * 4])`,
834 },
835 // @ start() on a vector selector requires start feature.
836 {
837 description: "vector selector with @ start()",
838 expr: `foo @ start()`,
839 },
840 // @ end() on a vector selector requires end feature.
841 {
842 description: "vector selector with @ end()",
843 expr: `foo @ end()`,
844 },
845 // @ start() on a matrix selector via rate().
846 {
847 description: "rate with @ start()",
848 expr: `rate(foo[5m] @ start())`,
849 },
850 // range() used inside a duration expression requires both features.
851 {
852 description: "range() inside duration expression",
853 expr: `foo[5m+range()]`,
854 },
855 // Smoothed vector selector with @ modifier in binary operation.
856 {
857 description: "smoothed with @ modifier in binop",
858 expr: `rate(foo[5m] smoothed @ 1609459200) + bar`,
859 },
860 }
861
862 for _, tc := range testCases {
863 t.Run(tc.description, func(t *testing.T) {
864 n, err := parser.DecodeExpr(tc.expr)
865 require.NoError(t, err, "unexpected parse error for: %s", tc.expr)
866
867 output := source.LabelsSource(tc.expr, n.Expr)
868 require.NotEmpty(t, output, "LabelsSource returned no sources for: %s", tc.expr)
869
870 snap := Snapshot{
871 Expr: tc.expr,
872 Output: output,
873 }
874 d, err := yaml.Marshal(snap)
875 require.NoError(t, err, "failed to YAML encode snapshots")
876 snaps.WithConfig(snaps.Dir("."), snaps.Filename(file)).MatchSnapshot(t, string(d))
877 })
878 }
879}
880
881func BenchmarkLabelsSource(b *testing.B) {
882 type testCase struct {
883 node promParser.Node
884 expr string
885 }
886 queries := make([]testCase, 0, len(testCases))
887 for _, expr := range testCases {
888 n, err := parser.DecodeExpr(expr)
889 require.NoError(b, err)
890 queries = append(queries, testCase{
891 expr: expr,
892 node: n.Expr,
893 })
894 }
895
896 for b.Loop() {
897 for _, tc := range queries {
898 source.LabelsSource(tc.expr, tc.node)
899 }
900 }
901}