diff --git a/cmd/pint/tests/0111_snooze.txt b/cmd/pint/tests/0111_snooze.txt index 17af2040..7d540945 100644 --- a/cmd/pint/tests/0111_snooze.txt +++ b/cmd/pint/tests/0111_snooze.txt @@ -8,7 +8,7 @@ level=INFO msg="Finding all rules to check" paths=["rules"] level=DEBUG msg="File parsed" path=rules/0001.yml rules=1 level=DEBUG msg="Generated all Prometheus servers" count=0 level=DEBUG msg="Found recording rule" path=rules/0001.yml record=sum-job lines=2-3 -level=DEBUG msg="Check snoozed by comment" check=promql/aggregate(job:true) comment="snooze 2099-11-28T10:24:18Z promql/aggregate" until="2099-11-28T10:24:18Z" snooze=promql/aggregate +level=DEBUG msg="Check snoozed by comment" check=promql/aggregate(job:true) match=promql/aggregate until="2099-11-28T10:24:18Z" level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp"] path=rules/0001.yml rule=sum-job -- rules/0001.yml -- # pint snooze 2099-11-28T10:24:18Z promql/aggregate diff --git a/cmd/pint/tests/0116_file_snooze.txt b/cmd/pint/tests/0116_file_snooze.txt index b643b322..307f3cb2 100644 --- a/cmd/pint/tests/0116_file_snooze.txt +++ b/cmd/pint/tests/0116_file_snooze.txt @@ -5,8 +5,8 @@ cmp stderr stderr.txt -- stderr.txt -- level=INFO msg="Loading configuration file" path=.pint.hcl level=INFO msg="Finding all rules to check" paths=["rules"] -level=DEBUG msg="Check snoozed by comment" check=promql/aggregate(job:true) comment="file/snooze 2099-11-28T10:24:18Z promql/aggregate(job:true)" until="2099-11-28T10:24:18Z" snooze=promql/aggregate(job:true) -level=DEBUG msg="Check snoozed by comment" check=alerts/for comment="file/snooze 2099-11-28T10:24:18Z alerts/for" until="2099-11-28T10:24:18Z" snooze=alerts/for +level=DEBUG msg="Check snoozed by comment" check=promql/aggregate(job:true) until="2099-11-28T10:24:18Z" +level=DEBUG msg="Check snoozed by comment" check=alerts/for until="2099-11-28T10:24:18Z" level=DEBUG msg="File parsed" path=rules/0001.yml rules=2 level=DEBUG msg="Generated all Prometheus servers" count=0 level=DEBUG msg="Found recording rule" path=rules/0001.yml record=sum-job lines=4-5 diff --git a/internal/checks/base_test.go b/internal/checks/base_test.go index e6a8be72..49bac0bc 100644 --- a/internal/checks/base_test.go +++ b/internal/checks/base_test.go @@ -173,7 +173,7 @@ func runTests(t *testing.T, testCases []checkTest) { func parseContent(content string) (entries []discovery.Entry, err error) { p := parser.NewParser() - rules, err := p.Parse([]byte(content)) + rules, _, err := p.Parse([]byte(content)) if err != nil { return nil, err } diff --git a/internal/checks/promql_series.go b/internal/checks/promql_series.go index 98a2c00b..a40bc689 100644 --- a/internal/checks/promql_series.go +++ b/internal/checks/promql_series.go @@ -10,6 +10,7 @@ import ( "golang.org/x/exp/slices" + "github.com/cloudflare/pint/internal/comments" "github.com/cloudflare/pint/internal/discovery" "github.com/cloudflare/pint/internal/output" "github.com/cloudflare/pint/internal/parser" @@ -615,18 +616,23 @@ func (c SeriesCheck) instantSeriesCount(ctx context.Context, query string) (int, func (c SeriesCheck) getMinAge(rule parser.Rule, selector promParser.VectorSelector) (minAge time.Duration, problems []Problem) { minAge = time.Hour * 2 - bareSelector := stripLabels(selector) - for _, s := range [][]string{ - {"rule/set", c.Reporter(), "min-age"}, - {"rule/set", fmt.Sprintf("%s(%s)", c.Reporter(), bareSelector.String()), "min-age"}, - {"rule/set", fmt.Sprintf("%s(%s)", c.Reporter(), selector.String()), "min-age"}, - } { - if cmt, ok := rule.GetComment(s...); ok { - dur, err := model.ParseDuration(cmt.Value) + prefixes := []string{ + fmt.Sprintf("%s min-age ", c.Reporter()), + fmt.Sprintf("%s(%s) min-age ", c.Reporter(), bareSelector.String()), + fmt.Sprintf("%s(%s) min-age ", c.Reporter(), selector.String()), + } + for _, cmt := range comments.Filter(rule.Comments, comments.RuleSetType) { + ruleSet := cmt.Value.(comments.RuleSet) + for _, prefix := range prefixes { + if !strings.HasPrefix(ruleSet.Value, prefix) { + continue + } + val := strings.TrimPrefix(ruleSet.Value, prefix) + dur, err := model.ParseDuration(val) if err != nil { problems = append(problems, Problem{ - Fragment: cmt.String(), + Fragment: fmt.Sprintf("%s %s", comments.RuleSetComment, ruleSet.Value), Lines: rule.LineRange(), Reporter: c.Reporter(), Text: fmt.Sprintf("Failed to parse pint comment as duration: %s", err), @@ -644,13 +650,17 @@ func (c SeriesCheck) getMinAge(rule parser.Rule, selector promParser.VectorSelec func (c SeriesCheck) isLabelValueIgnored(rule parser.Rule, selector promParser.VectorSelector, labelName string) bool { bareSelector := stripLabels(selector) - for _, s := range []string{ - fmt.Sprintf("rule/set %s ignore/label-value %s", c.Reporter(), labelName), - fmt.Sprintf("rule/set %s(%s) ignore/label-value %s", c.Reporter(), bareSelector.String(), labelName), - fmt.Sprintf("rule/set %s(%s) ignore/label-value %s", c.Reporter(), selector.String(), labelName), - } { - if rule.HasComment(s) { - return true + values := []string{ + fmt.Sprintf("%s ignore/label-value %s", c.Reporter(), labelName), + fmt.Sprintf("%s(%s) ignore/label-value %s", c.Reporter(), bareSelector.String(), labelName), + fmt.Sprintf("%s(%s) ignore/label-value %s", c.Reporter(), selector.String(), labelName), + } + for _, cmt := range comments.Filter(rule.Comments, comments.RuleSetType) { + ruleSet := cmt.Value.(comments.RuleSet) + for _, val := range values { + if ruleSet.Value == val { + return true + } } } return false @@ -704,9 +714,10 @@ func stripLabels(selector promParser.VectorSelector) promParser.VectorSelector { } func isDisabled(rule parser.Rule, selector promParser.VectorSelector) bool { - for _, c := range rule.GetComments("disable") { - if strings.HasPrefix(c.Value, SeriesCheckName+"(") && strings.HasSuffix(c.Value, ")") { - cs := strings.TrimSuffix(strings.TrimPrefix(c.Value, SeriesCheckName+"("), ")") + for _, cmt := range comments.Filter(rule.Comments, comments.DisableType) { + disable := cmt.Value.(comments.Disable) + if strings.HasPrefix(disable.Match, SeriesCheckName+"(") && strings.HasSuffix(disable.Match, ")") { + cs := strings.TrimSuffix(strings.TrimPrefix(disable.Match, SeriesCheckName+"("), ")") // try full string or name match first if cs == selector.String() || cs == selector.Name { return true diff --git a/internal/checks/template_test.go b/internal/checks/template_test.go index 0cd4e41d..62450817 100644 --- a/internal/checks/template_test.go +++ b/internal/checks/template_test.go @@ -106,7 +106,7 @@ func TestTemplatedRegexpExpand(t *testing.T) { func newMustRule(content string) parser.Rule { p := parser.NewParser() - rules, err := p.Parse([]byte(content)) + rules, _, err := p.Parse([]byte(content)) if err != nil { panic(err) } diff --git a/internal/comments/comments.go b/internal/comments/comments.go new file mode 100644 index 00000000..e3c2ad48 --- /dev/null +++ b/internal/comments/comments.go @@ -0,0 +1,283 @@ +package comments + +import ( + "bufio" + "fmt" + "strings" + "time" + "unicode" +) + +type Type int + +const ( + UnknownType Type = iota + InvalidComment + IgnoreFileType // ignore/file + IgnoreLineType // ignore/line + IgnoreBeginType // ignore/begin + IgnoreEndType // ignore/end + IgnoreNextLineType // ignore/next-line + FileOwnerType // file/owner + RuleOwnerType // rule/owner + FileDisableType // file/disable + DisableType // disable + FileSnoozeType // file/snooze + SnoozeType // snooze + RuleSetType // rule/set +) + +var ( + Prefix = "pint" + + IgnoreFileComment = "ignore/file" + IgnoreLineComment = "ignore/line" + IgnoreBeginComment = "ignore/begin" + IgnoreEndComment = "ignore/end" + IgnoreNextLineComment = "ignore/next-line" + FileOwnerComment = "file/owner" + RuleOwnerComment = "rule/owner" + FileDisableComment = "file/disable" + DisableComment = "disable" + FileSnoozeComment = "file/snooze" + SnoozeComment = "snooze" + RuleSetComment = "rule/set" + + EmptyComment Comment +) + +type Comment struct { + Type Type + Value any +} + +func parseType(s string) Type { + switch s { + case IgnoreFileComment: + return IgnoreFileType + case IgnoreLineComment: + return IgnoreLineType + case IgnoreBeginComment: + return IgnoreBeginType + case IgnoreEndComment: + return IgnoreEndType + case IgnoreNextLineComment: + return IgnoreNextLineType + case FileOwnerComment: + return FileOwnerType + case RuleOwnerComment: + return RuleOwnerType + case FileDisableComment: + return FileDisableType + case DisableComment: + return DisableType + case FileSnoozeComment: + return FileSnoozeType + case SnoozeComment: + return SnoozeType + case RuleSetComment: + return RuleSetType + default: + return UnknownType + } +} + +type Owner struct { + Name string +} + +type Disable struct { + Match string +} + +type Snooze struct { + Match string + Until time.Time +} + +type RuleSet struct { + Value string +} + +func parseSnooze(s string) (snz Snooze, err error) { + parts := strings.SplitN(s, " ", 2) + if len(parts) != 2 { + return Snooze{}, fmt.Errorf("invalid snooze comment, expected '$TIME $MATCH' got %q", s) + } + + snz = Snooze{Match: parts[1]} + snz.Until, err = time.Parse(time.RFC3339, parts[0]) + if err != nil { + snz.Until, err = time.Parse("2006-01-02", parts[0]) + } + if err != nil { + return snz, fmt.Errorf("invalid snooze timestamp: %w", err) + } + return snz, nil +} + +func parseValue(typ Type, s string) (any, error) { + switch typ { + case IgnoreFileType, IgnoreLineType, IgnoreBeginType, IgnoreEndType, IgnoreNextLineType: + if s != "" { + return nil, fmt.Errorf("unexpected comment suffix: %q", s) + } + case FileOwnerType: + if s == "" { + return nil, fmt.Errorf("missing %s value", FileOwnerComment) + } + return Owner{Name: s}, nil + case RuleOwnerType: + if s == "" { + return nil, fmt.Errorf("missing %s value", RuleOwnerComment) + } + return Owner{Name: s}, nil + case FileDisableType: + if s == "" { + return nil, fmt.Errorf("missing %s value", FileDisableComment) + } + return Disable{Match: s}, nil + case DisableType: + if s == "" { + return nil, fmt.Errorf("missing %s value", DisableComment) + } + return Disable{Match: s}, nil + case FileSnoozeType: + if s == "" { + return nil, fmt.Errorf("missing %s value", FileSnoozeComment) + } + return parseSnooze(s) + case SnoozeType: + if s == "" { + return nil, fmt.Errorf("missing %s value", SnoozeComment) + } + return parseSnooze(s) + case RuleSetType: + if s == "" { + return nil, fmt.Errorf("missing %s value", RuleSetComment) + } + return RuleSet{Value: s}, nil + case UnknownType, InvalidComment: + // pass + } + return nil, nil +} + +const ( + needsPrefix int = iota + readsPrefix + needsType + readsType + needsValue + readsValue +) + +func parseComment(s string) (c Comment, err error) { + var buf strings.Builder + + state := needsPrefix + for _, r := range s + "\n" { + READRUNE: + switch state { + case needsPrefix: + if unicode.IsSpace(r) { + goto NEXT + } + state = readsPrefix + goto READRUNE + case readsPrefix: + if unicode.IsLetter(r) { + _, _ = buf.WriteRune(r) + goto NEXT + } + if unicode.IsSpace(r) { + // Invalid comment prefix, ignore this comment. + if buf.String() != Prefix { + return EmptyComment, nil + } + buf.Reset() + state = needsType + goto NEXT + } + // Invalid character in the prefix, ignore this comment. + return EmptyComment, nil + case needsType: + if unicode.IsSpace(r) { + goto NEXT + } + state = readsType + goto READRUNE + case readsType: + if unicode.IsLetter(r) || r == '/' || r == '-' { + _, _ = buf.WriteRune(r) + goto NEXT + } + if unicode.IsSpace(r) || r == '\n' { + c.Type = parseType(buf.String()) + buf.Reset() + state = needsValue + goto NEXT + } + // Invalid character in the type, ignore this comment. + return EmptyComment, nil + case needsValue: + if unicode.IsSpace(r) { + goto NEXT + } + state = readsValue + goto READRUNE + case readsValue: + if r == '\n' { + goto NEXT + } + _, _ = buf.WriteRune(r) + } + NEXT: + } + + c.Value, err = parseValue(c.Type, strings.TrimSpace(buf.String())) + return c, err +} + +func Parse(text string) (comments []Comment) { + sc := bufio.NewScanner(strings.NewReader(text)) + for sc.Scan() { + elems := strings.SplitN(sc.Text(), "#", 2) + if len(elems) != 2 { + continue + } + c, err := parseComment(elems[1]) + switch { + case err != nil: + comments = append(comments, Comment{ + Type: InvalidComment, + Value: err, + }) + case c == EmptyComment: + // pass + default: + comments = append(comments, c) + } + } + return comments +} + +func Filter(src []Comment, typ Type) []Comment { + dst := make([]Comment, 0, len(src)) + for _, c := range src { + if c.Type != typ { + continue + } + dst = append(dst, c) + } + return dst +} + +func IsRuleComment(typ Type) bool { + // nolint:exhaustive + switch typ { + case RuleOwnerType, DisableType, SnoozeType, RuleSetType: + return true + } + return false +} diff --git a/internal/comments/comments_test.go b/internal/comments/comments_test.go new file mode 100644 index 00000000..f0853754 --- /dev/null +++ b/internal/comments/comments_test.go @@ -0,0 +1,350 @@ +package comments_test + +import ( + "fmt" + "testing" + "time" + + "github.com/cloudflare/pint/internal/comments" + + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + type testCaseT struct { + input string + output []comments.Comment + } + + parseUntil := func(s string) time.Time { + until, err := time.Parse(time.RFC3339, s) + require.NoError(t, err) + return until + } + + errUntil := func(s string) error { + _, err := time.Parse("2006-01-02", s) + require.Error(t, err) + return err + } + + testCases := []testCaseT{ + { + input: "code\n", + }, + { + input: "code # bob\n", + }, + { + input: "code # bob\ncode # alice\n", + }, + { + input: "# pint bamboozle me this", + }, + { + input: "# pint/xxx bamboozle me this", + }, + { + input: "# pint bambo[]ozle me this", + }, + { + input: "# pint ignore/file \t this file", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`unexpected comment suffix: "this file"`), + }, + }, + }, + { + input: "# pint ignore/file", + output: []comments.Comment{ + {Type: comments.IgnoreFileType}, + }, + }, + { + input: "# pint ignore/line this line", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`unexpected comment suffix: "this line"`), + }, + }, + }, + { + input: "# pint ignore/line", + output: []comments.Comment{ + {Type: comments.IgnoreLineType}, + }, + }, + { + input: "# pint ignore/begin here", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`unexpected comment suffix: "here"`), + }, + }, + }, + { + input: "# pint ignore/begin", + output: []comments.Comment{ + {Type: comments.IgnoreBeginType}, + }, + }, + { + input: "# pint ignore/end here", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`unexpected comment suffix: "here"`), + }, + }, + }, + { + input: "# pint ignore/end", + output: []comments.Comment{ + {Type: comments.IgnoreEndType}, + }, + }, + { + input: "# pint ignore/next-line\there", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`unexpected comment suffix: "here"`), + }, + }, + }, + { + input: "# pint ignore/next-line", + output: []comments.Comment{ + {Type: comments.IgnoreNextLineType}, + }, + }, + { + input: "# pint file/owner", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing file/owner value"), + }, + }, + }, + { + input: "# pint file/owner bob and alice", + output: []comments.Comment{ + { + Type: comments.FileOwnerType, + Value: comments.Owner{Name: "bob and alice"}, + }, + }, + }, + { + input: "# pint rule/owner", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing rule/owner value"), + }, + }, + }, + { + input: "# pint rule/owner bob and alice", + output: []comments.Comment{ + { + Type: comments.RuleOwnerType, + Value: comments.Owner{Name: "bob and alice"}, + }, + }, + }, + { + input: "# pint file/disable", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing file/disable value"), + }, + }, + }, + { + input: `# pint file/disable promql/series(http_errors_total{label="this has spaces"})`, + output: []comments.Comment{ + { + Type: comments.FileDisableType, + Value: comments.Disable{Match: `promql/series(http_errors_total{label="this has spaces"})`}, + }, + }, + }, + { + input: "# pint disable", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing disable value"), + }, + }, + }, + { + input: `# pint disable promql/series(http_errors_total{label="this has spaces"})`, + output: []comments.Comment{ + { + Type: comments.DisableType, + Value: comments.Disable{Match: `promql/series(http_errors_total{label="this has spaces"})`}, + }, + }, + }, + { + input: "# pint file/snooze", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing file/snooze value"), + }, + }, + }, + { + input: "# pint file/snooze 2023-12-31", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`invalid snooze comment, expected '$TIME $MATCH' got "2023-12-31"`), + }, + }, + }, + { + input: "# pint file/snooze abc", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`invalid snooze comment, expected '$TIME $MATCH' got "abc"`), + }, + }, + }, + { + input: `# pint file/snooze 2023-1231 promql/series(http_errors_total{label="this has spaces"})`, + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("invalid snooze timestamp: %w", errUntil("2023-1231")), + }, + }, + }, + { + input: `# pint file/snooze 2023-12-31 promql/series(http_errors_total{label="this has spaces"})`, + output: []comments.Comment{ + { + Type: comments.FileSnoozeType, + Value: comments.Snooze{ + Until: parseUntil("2023-12-31T00:00:00Z"), + Match: `promql/series(http_errors_total{label="this has spaces"})`, + }, + }, + }, + }, + { + input: "# pint snooze", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing snooze value"), + }, + }, + }, + { + input: "# pint snooze 2023-12-31", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`invalid snooze comment, expected '$TIME $MATCH' got "2023-12-31"`), + }, + }, + }, + { + input: "# pint snooze abc", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf(`invalid snooze comment, expected '$TIME $MATCH' got "abc"`), + }, + }, + }, + { + input: `# pint snooze 2023-1231 promql/series(http_errors_total{label="this has spaces"})`, + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("invalid snooze timestamp: %w", errUntil("2023-1231")), + }, + }, + }, + { + input: `# pint snooze 2023-12-31 promql/series(http_errors_total{label="this has spaces"})`, + output: []comments.Comment{ + { + Type: comments.SnoozeType, + Value: comments.Snooze{ + Until: parseUntil("2023-12-31T00:00:00Z"), + Match: `promql/series(http_errors_total{label="this has spaces"})`, + }, + }, + }, + }, + { + input: "# pint rule/set", + output: []comments.Comment{ + { + Type: comments.InvalidComment, + Value: fmt.Errorf("missing rule/set value"), + }, + }, + }, + { + input: "# pint rule/set bob and alice", + output: []comments.Comment{ + { + Type: comments.RuleSetType, + Value: comments.RuleSet{Value: "bob and alice"}, + }, + }, + }, + { + input: "code # pint disable xxx \ncode # alice\n", + output: []comments.Comment{ + { + Type: comments.DisableType, + Value: comments.Disable{Match: "xxx"}, + }, + }, + }, + { + input: "code # pint disable xxx yyy \n # pint\tfile/owner bob", + output: []comments.Comment{ + { + Type: comments.DisableType, + Value: comments.Disable{Match: "xxx yyy"}, + }, + { + Type: comments.FileOwnerType, + Value: comments.Owner{Name: "bob"}, + }, + }, + }, + { + input: "# pint rule/set promql/series(found) min-age foo", + output: []comments.Comment{ + { + Type: comments.RuleSetType, + Value: comments.RuleSet{Value: "promql/series(found) min-age foo"}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + output := comments.Parse(tc.input) + require.Equal(t, tc.output, output) + }) + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ce826f42..ccedaf71 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -133,7 +133,7 @@ func TestSetDisabledChecks(t *testing.T) { func newRule(t *testing.T, content string) parser.Rule { p := parser.NewParser() - rules, err := p.Parse([]byte(content)) + rules, _, err := p.Parse([]byte(content)) if err != nil { t.Error(err) t.FailNow() diff --git a/internal/config/rule.go b/internal/config/rule.go index 3caf93ae..8d470c30 100644 --- a/internal/config/rule.go +++ b/internal/config/rule.go @@ -7,9 +7,8 @@ import ( "regexp" "time" - "golang.org/x/exp/slices" - "github.com/cloudflare/pint/internal/checks" + "github.com/cloudflare/pint/internal/comments" "github.com/cloudflare/pint/internal/parser" "github.com/cloudflare/pint/internal/promapi" ) @@ -271,51 +270,47 @@ func (rule Rule) resolveChecks(ctx context.Context, path string, r parser.Rule, } func isEnabled(enabledChecks, disabledChecks []string, rule parser.Rule, name string, check checks.RuleChecker, promTags []string) bool { - instance := check.String() - - comments := []string{ - fmt.Sprintf("disable %s", name), - fmt.Sprintf("disable %s", instance), + matches := []string{ + name, + check.String(), } for _, tag := range promTags { - comments = append(comments, fmt.Sprintf("disable %s(+%s)", name, tag)) + matches = append(matches, fmt.Sprintf("%s(+%s)", name, tag)) } - for _, comment := range comments { - if rule.HasComment(comment) { - slog.Debug( - "Check disabled by comment", - slog.String("check", instance), - slog.String("comment", comment), - ) - return false + for _, comment := range comments.Filter(rule.Comments, comments.DisableType) { + disable := comment.Value.(comments.Disable) + for _, match := range matches { + if match == disable.Match { + slog.Debug( + "Check disabled by comment", + slog.String("check", check.String()), + slog.String("match", match), + ) + return false + } } } - - disabled := []string{name, instance} - for _, tag := range promTags { - disabled = append(disabled, fmt.Sprintf("%s(+%s)", name, tag)) - } - for _, comment := range rule.GetComments("snooze") { - s := parser.ParseSnooze(comment.Value) - if s == nil { + for _, comment := range comments.Filter(rule.Comments, comments.SnoozeType) { + snooze := comment.Value.(comments.Snooze) + if !snooze.Until.After(time.Now()) { continue } - if !slices.Contains(disabled, s.Text) { - continue + for _, match := range matches { + if match == snooze.Match { + slog.Debug( + "Check snoozed by comment", + slog.String("check", check.String()), + slog.String("match", match), + slog.Time("until", snooze.Until), + ) + return false + } } - slog.Debug( - "Check snoozed by comment", - slog.String("check", instance), - slog.String("comment", comment.String()), - slog.Time("until", s.Until), - slog.String("snooze", s.Text), - ) - return false } for _, c := range disabledChecks { - if c == name || c == instance { + if c == name || c == check.String() { return false } for _, tag := range promTags { diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go index 1661e320..b1c85026 100644 --- a/internal/discovery/discovery.go +++ b/internal/discovery/discovery.go @@ -7,10 +7,12 @@ import ( "log/slog" "regexp" "strings" + "time" "github.com/prometheus/prometheus/model/rulefmt" "golang.org/x/exp/slices" + "github.com/cloudflare/pint/internal/comments" "github.com/cloudflare/pint/internal/output" "github.com/cloudflare/pint/internal/parser" ) @@ -105,29 +107,34 @@ func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (ent } body := string(content.Body) - fileOwner, _ := parser.GetLastComment(body, FileOwnerComment) + var fileOwner string var disabledChecks []string - for _, comment := range parser.GetComments(body, FileDisabledCheckComment) { - if !slices.Contains(disabledChecks, comment.Value) { - disabledChecks = append(disabledChecks, comment.Value) - } - } - for _, comment := range parser.GetComments(body, FileSnoozeCheckComment) { - s := parser.ParseSnooze(comment.Value) - if s == nil { - continue - } - if !slices.Contains(disabledChecks, s.Text) { - disabledChecks = append(disabledChecks, s.Text) + for _, comment := range comments.Parse(body) { + // nolint:exhaustive + switch comment.Type { + case comments.FileOwnerType: + owner := comment.Value.(comments.Owner) + fileOwner = owner.Name + case comments.FileDisableType: + disable := comment.Value.(comments.Disable) + if !slices.Contains(disabledChecks, disable.Match) { + disabledChecks = append(disabledChecks, disable.Match) + } + case comments.FileSnoozeType: + snooze := comment.Value.(comments.Snooze) + if !snooze.Until.After(time.Now()) { + continue + } + if !slices.Contains(disabledChecks, snooze.Match) { + disabledChecks = append(disabledChecks, snooze.Match) + } + slog.Debug( + "Check snoozed by comment", + slog.String("check", snooze.Match), + slog.Time("until", snooze.Until), + ) } - slog.Debug( - "Check snoozed by comment", - slog.String("check", s.Text), - slog.String("comment", comment.String()), - slog.Time("until", s.Until), - slog.String("snooze", s.Text), - ) } if content.Ignored { @@ -135,7 +142,7 @@ func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (ent ReportedPath: reportedPath, SourcePath: sourcePath, PathError: ErrFileIsIgnored, - Owner: fileOwner.Value, + Owner: fileOwner, ModifiedLines: contentLines, }) return entries, nil @@ -156,7 +163,7 @@ func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (ent ReportedPath: reportedPath, SourcePath: sourcePath, PathError: err, - Owner: fileOwner.Value, + Owner: fileOwner, ModifiedLines: contentLines, }) } @@ -166,7 +173,7 @@ func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (ent } } - rules, err := p.Parse(content.Body) + rules, _, err := p.Parse(content.Body) if err != nil { slog.Error( "Failed to parse file content", @@ -178,23 +185,23 @@ func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (ent ReportedPath: reportedPath, SourcePath: sourcePath, PathError: err, - Owner: fileOwner.Value, + Owner: fileOwner, ModifiedLines: contentLines, }) return entries, nil } for _, rule := range rules { - owner, ok := rule.GetComment(RuleOwnerComment) - if !ok { - owner = fileOwner + owner := fileOwner + for _, comment := range comments.Filter(rule.Comments, comments.RuleOwnerType) { + owner = comment.Value.(comments.Owner).Name } entries = append(entries, Entry{ ReportedPath: reportedPath, SourcePath: sourcePath, Rule: rule, ModifiedLines: rule.Lines(), - Owner: owner.Value, + Owner: owner, DisabledChecks: disabledChecks, }) } diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go index 52866828..0f3d0bf8 100644 --- a/internal/discovery/discovery_test.go +++ b/internal/discovery/discovery_test.go @@ -24,7 +24,7 @@ func (r failingReader) Read(_ []byte) (int, error) { func TestReadRules(t *testing.T) { mustParse := func(offset int, s string) parser.Rule { p := parser.NewParser() - r, err := p.Parse([]byte(strings.Repeat("\n", offset) + s)) + r, _, err := p.Parse([]byte(strings.Repeat("\n", offset) + s)) if err != nil { panic(fmt.Sprintf("failed to parse rule:\n---\n%s\n---\nerror: %s", s, err)) } diff --git a/internal/discovery/git_branch_test.go b/internal/discovery/git_branch_test.go index fd64d23a..297f8303 100644 --- a/internal/discovery/git_branch_test.go +++ b/internal/discovery/git_branch_test.go @@ -39,7 +39,7 @@ func TestGitBranchFinder(t *testing.T) { mustParse := func(offset int, s string) parser.Rule { p := parser.NewParser() - r, err := p.Parse([]byte(strings.Repeat("\n", offset) + s)) + r, _, err := p.Parse([]byte(strings.Repeat("\n", offset) + s)) if err != nil { panic(fmt.Sprintf("failed to parse rule:\n---\n%s\n---\nerror: %s", s, err)) } diff --git a/internal/discovery/glob_test.go b/internal/discovery/glob_test.go index 29251f8e..3287ede8 100644 --- a/internal/discovery/glob_test.go +++ b/internal/discovery/glob_test.go @@ -29,7 +29,7 @@ func TestGlobPathFinder(t *testing.T) { p := parser.NewParser() testRuleBody := "# pint file/owner bob\n\n- record: foo\n expr: sum(foo)\n" - testRules, err := p.Parse([]byte(testRuleBody)) + testRules, _, err := p.Parse([]byte(testRuleBody)) require.NoError(t, err) parseErr := func(input string) error { diff --git a/internal/parser/fuzz_test.go b/internal/parser/fuzz_test.go index 971d0261..bbeec1fc 100644 --- a/internal/parser/fuzz_test.go +++ b/internal/parser/fuzz_test.go @@ -279,6 +279,6 @@ labels: p := parser.NewParser() f.Fuzz(func(t *testing.T, s string) { t.Logf("Parsing: [%s]\n", s) - _, _ = p.Parse([]byte(s)) + _, _, _ = p.Parse([]byte(s)) }) } diff --git a/internal/parser/models.go b/internal/parser/models.go index 9440e197..94a0c18b 100644 --- a/internal/parser/models.go +++ b/internal/parser/models.go @@ -1,7 +1,6 @@ package parser import ( - "bufio" "fmt" "strings" @@ -9,6 +8,8 @@ import ( "gopkg.in/yaml.v3" promparser "github.com/prometheus/prometheus/promql/parser" + + "github.com/cloudflare/pint/internal/comments" ) func appendLine(lines []int, newLines ...int) []int { @@ -268,10 +269,11 @@ type ParseError struct { type Rule struct { AlertingRule *AlertingRule RecordingRule *RecordingRule - Comments []string + Comments []comments.Comment Error ParseError } +// FIXME func (r Rule) ToYAML() string { if r.Error.Err != nil { return fmt.Sprintf("line=%d fragment=%s err=%s", r.Error.Line, r.Error.Fragment, r.Error.Err) @@ -283,8 +285,9 @@ func (r Rule) ToYAML() string { var b strings.Builder + // FIXME for _, c := range r.Comments { - b.WriteString(c) + b.WriteString(fmt.Sprintf("# %d %s", c.Type, c.Value)) b.WriteRune('\n') } @@ -436,6 +439,7 @@ func (r Rule) LineRange() []int { return lines } +/* func (r Rule) HasComment(comment string) bool { for _, c := range r.Comments { if hasComment(c, comment) { @@ -466,6 +470,7 @@ func (r Rule) GetComments(key string) (cs []Comment) { } return cs } +*/ type RuleType string diff --git a/internal/parser/parser.go b/internal/parser/parser.go index e591a692..3df84c03 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -1,10 +1,13 @@ package parser import ( + "errors" "fmt" "strings" "gopkg.in/yaml.v3" + + "github.com/cloudflare/pint/internal/comments" ) const ( @@ -17,15 +20,17 @@ const ( annotationsKey = "annotations" ) +var ErrRuleCommentOnFile = errors.New("this comment is only valid when attached to a rule") + func NewParser() Parser { return Parser{} } type Parser struct{} -func (p Parser) Parse(content []byte) (rules []Rule, err error) { +func (p Parser) Parse(content []byte) (rules []Rule, fileComments []comments.Comment, err error) { if len(content) == 0 { - return nil, nil + return nil, nil, nil } defer func() { @@ -37,10 +42,24 @@ func (p Parser) Parse(content []byte) (rules []Rule, err error) { var node yaml.Node err = yaml.Unmarshal(content, &node) if err != nil { - return nil, err + return nil, nil, err } - return parseNode(content, &node, 0) + for _, s := range []string{node.HeadComment, node.LineComment, node.FootComment} { + for _, comment := range comments.Parse(s) { + if comments.IsRuleComment(comment.Type) { + fileComments = append(fileComments, comments.Comment{ + Type: comments.InvalidComment, + Value: ErrRuleCommentOnFile, + }) + continue + } + fileComments = append(fileComments, comment) + } + } + + rules, err = parseNode(content, &node, 0) + return rules, fileComments, err } func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule, err error) { @@ -117,16 +136,21 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool, var key *yaml.Node unknownKeys := []*yaml.Node{} - var comments []string + var ruleComments []comments.Comment for i, part := range unpackNodes(node) { if i == 0 && node.HeadComment != "" && part.HeadComment == "" { part.HeadComment = node.HeadComment } + if i == 0 && node.LineComment != "" && part.LineComment == "" { + part.LineComment = node.LineComment + } if i == len(node.Content)-1 && node.FootComment != "" && part.HeadComment == "" { part.FootComment = node.FootComment } - comments = append(comments, mergeComments(part)...) + for _, s := range mergeComments(part) { + ruleComments = append(ruleComments, comments.Parse(s)...) + } if i%2 == 0 { key = part @@ -239,7 +263,7 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool, Expr: *exprPart, Labels: labelsPart, }, - Comments: comments, + Comments: ruleComments, } return rule, false, err } @@ -254,7 +278,7 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool, Labels: labelsPart, Annotations: annotationsPart, }, - Comments: comments, + Comments: ruleComments, } return rule, false, err } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 94cf0033..5724a1bf 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -5,6 +5,7 @@ import ( "strconv" "testing" + "github.com/cloudflare/pint/internal/comments" "github.com/cloudflare/pint/internal/parser" "github.com/google/go-cmp/cmp" @@ -15,6 +16,7 @@ func TestParse(t *testing.T) { type testCaseT struct { content []byte output []parser.Rule + comments []comments.Comment shouldError bool } @@ -239,26 +241,48 @@ func TestParse(t *testing.T) { }, { content: []byte(` -# head comment -- record: foo # record comment - expr: foo offset 10m # expr comment - # pre-labels comment +# pint disable head comment +- record: foo # pint disable record comment + expr: foo offset 10m # pint disable expr comment + # pint disable pre-labels comment labels: - # pre-foo comment + # pint disable pre-foo comment foo: bar - # post-foo comment + # pint disable post-foo comment bob: alice -# foot comment + # pint disable foot comment `), output: []parser.Rule{ { - Comments: []string{ - "# head comment", - "# record comment", - "# expr comment", - "# pre-labels comment", - "# pre-foo comment", - "# post-foo comment", + Comments: []comments.Comment{ + { + Type: comments.DisableType, + Value: comments.Disable{Match: "head comment"}, + }, + { + Type: comments.DisableType, + Value: comments.Disable{Match: "record comment"}, + }, + { + Type: comments.DisableType, + Value: comments.Disable{Match: "expr comment"}, + }, + { + Type: comments.DisableType, + Value: comments.Disable{Match: "pre-labels comment"}, + }, + { + Type: comments.DisableType, + Value: comments.Disable{Match: "foot comment"}, + }, + { + Type: comments.DisableType, + Value: comments.Disable{Match: "pre-foo comment"}, + }, + { + Type: comments.DisableType, + Value: comments.Disable{Match: "post-foo comment"}, + }, }, RecordingRule: &parser.RecordingRule{ Record: parser.YamlKeyValue{ @@ -1109,7 +1133,7 @@ data: - record: name2 expr: expr2 labels: *labelsAnchor - # foot comment + # pint disable foot comment `), output: []parser.Rule{ { @@ -1166,7 +1190,12 @@ data: }, }, { - Comments: []string{"# foot comment"}, + Comments: []comments.Comment{ + { + Type: comments.DisableType, + Value: comments.Disable{Match: "foot comment"}, + }, + }, RecordingRule: &parser.RecordingRule{ Record: parser.YamlKeyValue{ Key: &parser.YamlNode{ @@ -1257,6 +1286,102 @@ data: {Error: parser.ParseError{Err: fmt.Errorf("missing expr key"), Line: 1}}, }, }, + { + content: []byte(string(` +# pint file/owner bob +# pint ignore/begin +# pint ignore/end +# pint disable up + +- record: foo + expr: up + +# pint file/owner alice + +- record: foo + expr: up + +# pint ignore/next-line +`)), + comments: []comments.Comment{ + { + Type: comments.FileOwnerType, + Value: comments.Owner{Name: "bob"}, + }, + { + Type: comments.IgnoreBeginType, + }, + { + Type: comments.IgnoreEndType, + }, + { + Type: comments.InvalidComment, + Value: parser.ErrRuleCommentOnFile, + }, + { + Type: comments.IgnoreNextLineType, + }, + }, + output: []parser.Rule{ + { + RecordingRule: &parser.RecordingRule{ + Record: parser.YamlKeyValue{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{7}}, + Value: "record", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{7}}, + Value: "foo", + }, + }, + Expr: parser.PromQLExpr{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{8}}, + Value: "expr", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{8}}, + Value: "up", + }, + Query: &parser.PromQLNode{Expr: "up"}, + }, + }, + }, + { + RecordingRule: &parser.RecordingRule{ + Record: parser.YamlKeyValue{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{12}}, + Value: "record", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{12}}, + Value: "foo", + }, + }, + Expr: parser.PromQLExpr{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{13}}, + Value: "expr", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{13}}, + Value: "up", + }, + Query: &parser.PromQLNode{Expr: "up"}, + }, + }, + Comments: []comments.Comment{ + { + Type: comments.FileOwnerType, + Value: comments.Owner{Name: "alice"}, + }, + }, + }, + }, + shouldError: false, + }, } alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) @@ -1280,7 +1405,7 @@ data: for i, tc := range testCases { t.Run(strconv.Itoa(i+1), func(t *testing.T) { p := parser.NewParser() - output, err := p.Parse(tc.content) + output, fileComments, err := p.Parse(tc.content) hadError := err != nil if hadError != tc.shouldError { @@ -1292,6 +1417,11 @@ data: t.Errorf("Parse() returned wrong output (-want +got):\n%s", diff) return } + + if diff := cmp.Diff(tc.comments, fileComments, sameErrorText); diff != "" { + t.Errorf("Parse() returned wrong comments (-want +got):\n%s", diff) + return + } }) } } diff --git a/internal/parser/read.go b/internal/parser/read.go index 321df267..b73133f9 100644 --- a/internal/parser/read.go +++ b/internal/parser/read.go @@ -5,6 +5,8 @@ import ( "errors" "io" "strings" + + "github.com/cloudflare/pint/internal/comments" ) type skipMode int @@ -48,7 +50,7 @@ func hasComment(line, comment string) bool { if len(parts) < 2 { continue } - if parts[0] == "pint" && parts[1] == comment { + if parts[0] == comments.Prefix && parts[1] == comment { return true } } @@ -57,15 +59,15 @@ func hasComment(line, comment string) bool { func parseSkipComment(line string) (skipMode, bool) { switch { - case hasComment(line, "ignore/file"): + case hasComment(line, comments.IgnoreFileComment): return skipFile, true - case hasComment(line, "ignore/line"): + case hasComment(line, comments.IgnoreLineComment): return skipCurrentLine, true - case hasComment(line, "ignore/next-line"): + case hasComment(line, comments.IgnoreNextLineComment): return skipNextLine, true - case hasComment(line, "ignore/begin"): + case hasComment(line, comments.IgnoreBeginComment): return skipBegin, true - case hasComment(line, "ignore/end"): + case hasComment(line, comments.IgnoreEndComment): return skipEnd, true default: return skipNone, false @@ -150,52 +152,3 @@ func ReadContent(r io.Reader) (out Content, err error) { return out, nil } - -type Comment struct { - Key string - Value string -} - -func (c Comment) String() string { - return c.Key + " " + c.Value -} - -func GetComments(text string, comment ...string) (comments []Comment) { - sc := bufio.NewScanner(strings.NewReader(text)) - for sc.Scan() { - elems := strings.Split(sc.Text(), "#") - lastComment := elems[len(elems)-1] - parts := strings.Split(removeRedundantSpaces(lastComment), " ") - if len(parts) < 2 { - continue - } - keys := make([]string, 0, len(parts)) - values := make([]string, 0, len(parts)) - if parts[0] == "pint" && len(parts) >= len(comment)+1 { - for i, c := range comment { - if parts[i+1] != c { - goto NEXT - } - keys = append(keys, parts[i+1]) - } - for i := len(comment) + 1; i < len(parts); i++ { - values = append(values, parts[i]) - } - comments = append(comments, Comment{ - Key: strings.Join(keys, " "), - Value: strings.Join(values, " "), - }) - } - NEXT: - } - - return comments -} - -func GetLastComment(text string, comment ...string) (Comment, bool) { - comments := GetComments(text, comment...) - if len(comments) == 0 { - return Comment{}, false - } - return comments[len(comments)-1], true -} diff --git a/internal/parser/read_test.go b/internal/parser/read_test.go index 9457e18a..7e08a987 100644 --- a/internal/parser/read_test.go +++ b/internal/parser/read_test.go @@ -2,7 +2,6 @@ package parser_test import ( "bytes" - "fmt" "strconv" "strings" "testing" @@ -160,123 +159,3 @@ func TestReadContent(t *testing.T) { }) } } - -func TestGetLastComment(t *testing.T) { - type testCaseT struct { - input string - comment []string - output parser.Comment - ok bool - } - - testCases := []testCaseT{ - { - input: "", - comment: []string{"rule/owner"}, - }, - { - input: "\n", - comment: []string{"rule/owner"}, - }, - { - input: "\n \n", - comment: []string{"rule/owner"}, - }, - { - input: "foo bar", - comment: []string{"rule/owner"}, - }, - { - input: "foo bar\n", - comment: []string{"rule/owner"}, - }, - { - input: "line1\nline2", - comment: []string{"rule/owner"}, - }, - { - input: "line1\nline2\n", - comment: []string{"rule/owner"}, - }, - { - input: "line1\n\nline2\n\n", - comment: []string{"rule/owner"}, - }, - { - input: "# pint rule/owner", - comment: []string{"rule/owner"}, - ok: true, - output: parser.Comment{Key: "rule/owner"}, - }, - { - input: "# pint rule/owner foo", - comment: []string{"rule/owner"}, - ok: true, - output: parser.Comment{Key: "rule/owner", Value: "foo"}, - }, - { - input: "# pint rule/owner foo bar bob/alice", - comment: []string{"rule/owner"}, - ok: true, - output: parser.Comment{Key: "rule/owner", Value: "foo bar bob/alice"}, - }, - { - input: "line1\n # pint rule/owner foo bar bob/alice\n line2\n\n", - comment: []string{"rule/owner"}, - ok: true, - output: parser.Comment{Key: "rule/owner", Value: "foo bar bob/alice"}, - }, - { - input: "line1\n #### pint rule/owner foo bar bob/alice\n line2\n\n", - comment: []string{"rule/owner"}, - ok: true, - output: parser.Comment{Key: "rule/owner", Value: "foo bar bob/alice"}, - }, - { - input: "# pint set promql/series min-age 1w", - comment: []string{"set promql/series min-age"}, - }, - { - input: "# pint set promql/series min-age 1w", - comment: []string{"set", "promql/series", "min-age"}, - ok: true, - output: parser.Comment{Key: "set promql/series min-age", Value: "1w"}, - }, - { - input: "# pint set promql/series min-age 1d\n# pint set promql/series min-age 1w\n", - comment: []string{"set", "promql/series", "min-age"}, - ok: true, - output: parser.Comment{Key: "set promql/series min-age", Value: "1w"}, - }, - { - input: "# pint set promql/series min-age 2d\n# pint set promql/series min-age 1w\n# pint set promql/series min-age 1s\n", - comment: []string{"set", "promql/series", "min-age"}, - ok: true, - output: parser.Comment{Key: "set promql/series min-age", Value: "1s"}, - }, - { - input: "# pint set promql/series min-age 1w ", - comment: []string{"set", "promql/series", "min-age"}, - ok: true, - output: parser.Comment{Key: "set promql/series min-age", Value: "1w"}, - }, - { - input: "# pint set", - comment: []string{"set", "promql/series", "min-age"}, - }, - { - input: "# pint rule/set promql/series ignore/label-value error", - comment: []string{"rule/set", "promql/series", "ignore/label-value"}, - ok: true, - output: parser.Comment{Key: "rule/set promql/series ignore/label-value", Value: "error"}, - }, - } - - for i, tc := range testCases { - t.Run(fmt.Sprintf("%d/%s", i, tc.input), func(t *testing.T) { - output, ok := parser.GetLastComment(tc.input, tc.comment...) - require.Equal(t, tc.ok, ok) - require.Equal(t, tc.output, output) - }) - } -} diff --git a/internal/reporter/bitbucket_test.go b/internal/reporter/bitbucket_test.go index 4a28eb45..abdf4420 100644 --- a/internal/reporter/bitbucket_test.go +++ b/internal/reporter/bitbucket_test.go @@ -40,7 +40,7 @@ func TestBitBucketReporter(t *testing.T) { } p := parser.NewParser() - mockRules, _ := p.Parse([]byte(` + mockRules, _, _ := p.Parse([]byte(` - record: target is down expr: up == 0 - record: sum errors diff --git a/internal/reporter/github_test.go b/internal/reporter/github_test.go index 63de2a12..46e6f982 100644 --- a/internal/reporter/github_test.go +++ b/internal/reporter/github_test.go @@ -33,7 +33,7 @@ func TestGithubReporter(t *testing.T) { } p := parser.NewParser() - mockRules, _ := p.Parse([]byte(` + mockRules, _, _ := p.Parse([]byte(` - record: target is down expr: up == 0 - record: sum errors diff --git a/internal/reporter/teamcity_test.go b/internal/reporter/teamcity_test.go index be851ff8..debe571f 100644 --- a/internal/reporter/teamcity_test.go +++ b/internal/reporter/teamcity_test.go @@ -22,7 +22,7 @@ func TestTeamCityReporter(t *testing.T) { } p := parser.NewParser() - mockRules, _ := p.Parse([]byte(` + mockRules, _, _ := p.Parse([]byte(` - record: target is down expr: up == 0 `))