cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.18.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser_test.go

889lines · modepreview

package parser_test

import (
	"fmt"
	"strconv"
	"testing"

	"github.com/cloudflare/pint/internal/parser"

	"github.com/google/go-cmp/cmp"
	promparser "github.com/prometheus/prometheus/promql/parser"
)

func TestParse(t *testing.T) {
	type testCaseT struct {
		content     []byte
		output      []parser.Rule
		shouldError bool
	}

	testCases := []testCaseT{
		{
			content:     nil,
			output:      nil,
			shouldError: false,
		},
		{
			content:     []byte{},
			output:      nil,
			shouldError: false,
		},
		{
			content:     []byte(string("! !00 \xf6")),
			output:      nil,
			shouldError: true,
		},
		{
			content: []byte(string("- 0: 0\n  00000000: 000000\n  000000:00000000000: 00000000\n  00000000000:000000: 0000000000000000000000000000000000\n  000000: 0000000\n  expr: |")),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("incomplete rule, no alert or record key"), Line: 6}},
			},
		},
		{
			content: []byte("- record: |\n    multiline\n"),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("missing expr key"), Line: 1}},
			},
		},
		{
			content: []byte("- expr: foo\n"),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("incomplete rule, no alert or record key"), Line: 1}},
			},
		},
		{
			content: []byte("- alert: foo\n"),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("missing expr key"), Line: 1}},
			},
		},
		{
			content: []byte("- alert: foo\n  record: foo\n"),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("got both record and alert keys in a single rule"), Line: 1}},
			},
		},
		{
			content: []byte("- record: foo\n  labels:\n    foo: bar\n"),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("missing expr key"), Line: 1}},
			},
		},
		{
			content:     []byte("- record: - foo\n"),
			shouldError: true,
		},
		{
			content:     []byte("- record: foo  expr: sum(\n"),
			shouldError: true,
		},
		{
			content:     []byte("- record\n\texpr: foo\n"),
			shouldError: true,
		},
		{
			content: []byte(`
- record: foo
  expr: bar
  expr: bar
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated expr key"), Line: 4}},
			},
		},
		{
			content: []byte(`
- record: foo
  expr: bar
  record: bar
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated record key"), Line: 4}},
			},
		},
		{
			content: []byte(`
- alert: foo
  alert: bar
  expr: bar
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated alert key"), Line: 3}},
			},
		},
		{
			content: []byte(`
- alert: foo
  for: 5m
  expr: bar
  for: 1m
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated for key"), Line: 5}},
			},
		},
		{
			content: []byte(`
- alert: foo
  labels: {}
  expr: bar
  labels: {}
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated labels key"), Line: 5}},
			},
		},
		{
			content: []byte(`
- record: foo
  labels: {}
  expr: bar
  labels: {}
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated labels key"), Line: 5}},
			},
		},
		{
			content: []byte(`
- alert: foo
  annotations: {}
  expr: bar
  annotations: {}
`),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("duplicated annotations key"), Line: 5}},
			},
		},
		{
			content: []byte("- record: foo\n  expr: foo\n  extra: true\n"),
			output: []parser.Rule{
				{Error: parser.ParseError{Err: fmt.Errorf("invalid key(s) found: extra"), Line: 3}},
			},
		},
		{
			content: []byte("- record: foo\n  expr: foo offset 10m\n"),
			output: []parser.Rule{
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "record",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "foo",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "foo offset 10m",
							},
							Query: &parser.PromQLNode{
								Expr: "foo offset 10m",
							},
						},
					},
				},
			},
		},
		{
			content: []byte("- record: foo\n  expr: foo offset -10m\n"),
			output: []parser.Rule{
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "record",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "foo",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "foo offset -10m",
							},
							Query: &parser.PromQLNode{
								Expr: "foo offset -10m",
							},
						},
					},
				},
			},
		},
		{
			content: []byte(`
# head comment
- record: foo # record comment
  expr: foo offset 10m # expr comment
  #  pre-labels comment
  labels:
    # pre-foo comment
    foo: bar
    # post-foo comment
    bob: alice
# foot comment
`),
			output: []parser.Rule{
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{3}},
								Value:    "record",
								Comments: []string{"# head comment"},
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{3}},
								Value:    "foo",
								Comments: []string{"# record comment"},
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{4}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{4}},
								Value:    "foo offset 10m",
								Comments: []string{"# expr comment"},
							},
							Query: &parser.PromQLNode{
								Expr: "foo offset 10m",
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{6}},
								Value:    "labels",
								Comments: []string{"#  pre-labels comment"},
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{8}},
										Value:    "foo",
										Comments: []string{"# pre-foo comment"},
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{8}},
										Value:    "bar",
									},
								},
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{10}},
										Value:    "bob",
										Comments: []string{"# post-foo comment"},
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{10}},
										Value:    "alice",
									},
								},
							},
						},
					},
				},
			},
		},
		{
			content: []byte("- record: foo\n  expr: foo[5m] offset 10m\n"),
			output: []parser.Rule{
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "record",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "foo",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "foo[5m] offset 10m",
							},
							Query: &parser.PromQLNode{
								Expr: "foo[5m] offset 10m",
								Children: []*parser.PromQLNode{
									{Expr: "foo offset 10m"},
								},
							},
						},
					},
				},
			},
		},
		{
			content: []byte(`
- record: name
  expr: sum(foo)
  labels:
    foo: bar
    bob: alice
`),
			output: []parser.Rule{
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "record",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "name",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{3}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{3}},
								Value:    "sum(foo)",
							},
							Query: &parser.PromQLNode{
								Expr: "sum(foo)",
								Children: []*parser.PromQLNode{
									{Expr: "foo"},
								},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{4}},
								Value:    "labels",
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{5}},
										Value:    "foo",
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{5}},
										Value:    "bar",
									},
								},
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{6}},
										Value:    "bob",
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{6}},
										Value:    "alice",
									},
								},
							},
						},
					},
				},
			},
			shouldError: false,
		},
		{
			content: []byte(`
groups:
- name: custom_rules
  rules:
    - record: name
      expr: sum(foo)
      labels:
        foo: bar
        bob: alice
`),
			output: []parser.Rule{
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{5}},
								Value:    "record",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{5}},
								Value:    "name",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{6}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{6}},
								Value:    "sum(foo)",
							},
							Query: &parser.PromQLNode{
								Expr: "sum(foo)",
								Children: []*parser.PromQLNode{
									{Expr: "foo"},
								},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{7}},
								Value:    "labels",
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{8}},
										Value:    "foo",
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{8}},
										Value:    "bar",
									},
								},
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{9}},
										Value:    "bob",
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{9}},
										Value:    "alice",
									},
								},
							},
						},
					},
				},
			},
			shouldError: false,
		},
		{
			content: []byte(`- alert: Down
  expr: |
    up == 0
  for: |+
    11m
  labels:
    severity: critical
  annotations:
    uri: https://docs.example.com/down.html

- record: >
    foo
  expr: |-
    bar
    /
    baz > 1
  labels: {}
`),
			output: []parser.Rule{
				{
					AlertingRule: &parser.AlertingRule{
						Alert: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "alert",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "Down",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{3}},
								Value:    "up == 0\n",
							},
							Query: &parser.PromQLNode{
								Expr: "up == 0\n",
								Children: []*parser.PromQLNode{
									{Expr: "up"},
									{Expr: "0"},
								},
							},
						},
						For: &parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{4}},
								Value:    "for",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{5}},
								Value:    "11m\n",
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{6}},
								Value:    "labels",
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{7}},
										Value:    "severity",
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{7}},
										Value:    "critical",
									},
								},
							},
						},
						Annotations: &parser.YamlMap{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{8}},
								Value:    "annotations",
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{9}},
										Value:    "uri",
									},
									Value: &parser.YamlNode{
										Position: parser.FilePosition{Lines: []int{9}},
										Value:    "https://docs.example.com/down.html",
									},
								},
							},
						},
					},
				},
				{
					RecordingRule: &parser.RecordingRule{
						Record: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{11}},
								Value:    "record",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{12}},
								Value:    "foo\n",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{13}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{14, 15, 16}},
								Value:    "bar\n/\nbaz > 1",
							},
							Query: &parser.PromQLNode{
								Expr: "bar\n/\nbaz > 1",
								Children: []*parser.PromQLNode{
									{
										Expr: "bar / baz", Children: []*parser.PromQLNode{
											{Expr: "bar"},
											{Expr: "baz"},
										},
									},
									{Expr: "1"},
								},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{17}},
								Value:    "labels",
							},
						},
					},
				},
			},
			shouldError: false,
		},
		{
			content: []byte(`- alert: Foo
  expr:
    (
      xxx
      -
      yyy
    ) * bar > 0
    and on(instance, device) baz
  for: 30m
`),
			output: []parser.Rule{
				{
					AlertingRule: &parser.AlertingRule{
						Alert: parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "alert",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{1}},
								Value:    "Foo",
							},
						},
						Expr: parser.PromQLExpr{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{2}},
								Value:    "expr",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{3, 4, 5, 6, 7, 8}},
								Value:    "( xxx - yyy ) * bar > 0 and on(instance, device) baz",
							},
							Query: &parser.PromQLNode{
								Expr: "( xxx - yyy ) * bar > 0 and on(instance, device) baz",
								Children: []*parser.PromQLNode{
									{
										Expr: "(xxx - yyy) * bar > 0",
										Children: []*parser.PromQLNode{
											{
												Expr: "(xxx - yyy) * bar",
												Children: []*parser.PromQLNode{
													{
														Expr: "(xxx - yyy)",
														Children: []*parser.PromQLNode{
															{
																Expr: "xxx - yyy",
																Children: []*parser.PromQLNode{
																	{Expr: "xxx"},
																	{Expr: "yyy"},
																},
															},
														},
													},
													{
														Expr: "bar",
													},
												},
											},
											{
												Expr: "0",
											},
										},
									},
									{Expr: "baz"},
								},
							},
						},
						For: &parser.YamlKeyValue{
							Key: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{9}},
								Value:    "for",
							},
							Value: &parser.YamlNode{
								Position: parser.FilePosition{Lines: []int{9}},
								Value:    "30m",
							},
						},
					},
				},
			},
		},
		{
			content: []byte(`---
kind: ConfigMap
apiVersion: v1
metadata:
  name: example-app-alerts
  labels:
  app: example-app
data:
  alerts: |
    groups:
      - name: example-app-alerts
        rules:
          - alert: Example_Is_Down
            expr: kube_deployment_status_replicas_available{namespace="example-app"} < 1
            for: 5m
            labels:
              priority: "2"
              environment: production
            annotations:
              summary: "No replicas for Example have been running for 5 minutes"

          - alert: Example_High_Restart_Rate
            expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )
`),
			output: nil,
		},
		/*
					FIXME https://github.com/cloudflare/pint/issues/20
					{
						content: []byte(`groups:
			  - name: "haproxy.api_server.rules"
			    rules:
			      - alert: HaproxyServerHealthcheckFailure
			        expr: increase(haproxy_server_check_failures_total[15m]) > 100
			        for: 5m
			        labels:
			          severity: 24x7
			        annotations:
			          summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})"
			          description: "Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}"
			`),
						output: []parser.Rule{
							{
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlKeyValue{
										Key: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{4}},
											Value:    "alert",
										},
										Value: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{4}},
											Value:    "HaproxyServerHealthcheckFailure",
										},
									},
									Expr: parser.PromQLExpr{
										Key: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{5}},
											Value:    "expr",
										},
										Value: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{5}},
											Value:    "increase(haproxy_server_check_failures_total[15m]) > 100",
										},
										Query: &parser.PromQLNode{
											Expr: "increase(haproxy_server_check_failures_total[15m]) > 100",
											Children: []*parser.PromQLNode{
												{
													Expr: "increase(haproxy_server_check_failures_total[15m])",
													Children: []*parser.PromQLNode{
														{
															Expr: "haproxy_server_check_failures_total[15m]",
															Children: []*parser.PromQLNode{
																{
																	Expr: "haproxy_server_check_failures_total",
																},
															},
														},
													},
												},
												{Expr: "100"},
											},
										},
									},
									For: &parser.YamlKeyValue{
										Key: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{6}},
											Value:    "for",
										},
										Value: &parser.YamlNode{Position: parser.FilePosition{Lines: []int{6}},
											Value: "5m",
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{7}},
											Value:    "labels",
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Position: parser.FilePosition{Lines: []int{8}},
													Value:    "severity",
												},
												Value: &parser.YamlNode{
													Position: parser.FilePosition{Lines: []int{8}},
													Value:    "24x7",
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Position: parser.FilePosition{Lines: []int{9}},
											Value:    "annotations",
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Position: parser.FilePosition{Lines: []int{10}},
													Value:    "summary",
												},
												Value: &parser.YamlNode{
													Position: parser.FilePosition{Lines: []int{10}},
													Value:    "HAProxy server healthcheck failure (instance {{ $labels.instance }})",
												},
											},
											{
												Key: &parser.YamlNode{
													Position: parser.FilePosition{Lines: []int{11}},
													Value:    "description",
												},
												Value: &parser.YamlNode{
													Position: parser.FilePosition{Lines: []int{11}},
													Value:    `Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}`,
												},
											},
										},
									},
								},
							},
						},
					},
		*/
	}

	alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
	ignorePrometheusExpr := cmp.FilterValues(func(x, y interface{}) bool {
		_, xe := x.(promparser.Expr)
		_, ye := y.(promparser.Expr)
		return xe || ye
	}, alwaysEqual)

	cmpErrorText := cmp.Comparer(func(x, y interface{}) bool {
		xe := x.(error)
		ye := y.(error)
		return xe.Error() == ye.Error()
	})
	sameErrorText := cmp.FilterValues(func(x, y interface{}) bool {
		_, xe := x.(error)
		_, ye := y.(error)
		return xe && ye
	}, cmpErrorText)

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i+1), func(t *testing.T) {
			p := parser.NewParser()
			output, err := p.Parse(tc.content)

			hadError := err != nil
			if hadError != tc.shouldError {
				t.Errorf("Parse() returned err=%v, expected=%v", err, tc.shouldError)
				return
			}

			if diff := cmp.Diff(tc.output, output, ignorePrometheusExpr, sameErrorText); diff != "" {
				t.Errorf("Parse() returned wrong output (-want +got):\n%s", diff)
				return
			}
		})
	}
}