Skip to content

Commit

Permalink
Add teamcity reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
prymitive committed Nov 6, 2023
1 parent a2a4d0b commit 71fce28
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 3 deletions.
15 changes: 13 additions & 2 deletions cmd/pint/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
baseBranchFlag = "base-branch"
devFlag = "dev"
failOnFlag = "fail-on"
teamCityFlag = "teamcity"
)

var ciCmd = &cli.Command{
Expand Down Expand Up @@ -54,6 +55,12 @@ var ciCmd = &cli.Command{
Value: "bug",
Usage: "Exit with non-zero code if there are problems with given severity (or higher) detected",
},
&cli.BoolFlag{
Name: teamCityFlag,
Aliases: []string{"t"},
Value: false,
Usage: "Report problems using TeamCity Service Messages",
},
},
}

Expand Down Expand Up @@ -120,8 +127,12 @@ func actionCI(c *cli.Context) error {
summary.Report(verifyOwners(entries, meta.cfg.Owners.CompileAllowed())...)
}

reps := []reporter.Reporter{
reporter.NewConsoleReporter(os.Stderr, checks.Information),
reps := []reporter.Reporter{}

if c.Bool(teamCityFlag) {
reps = append(reps, reporter.NewTeamCityReporter(os.Stderr))
} else {
reps = append(reps, reporter.NewConsoleReporter(os.Stderr, checks.Information))
}

if meta.cfg.Repository != nil && meta.cfg.Repository.BitBucket != nil {
Expand Down
14 changes: 13 additions & 1 deletion cmd/pint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ var lintCmd = &cli.Command{
Value: "bug",
Usage: "Exit with non-zero code if there are problems with given severity (or higher) detected",
},
&cli.BoolFlag{
Name: teamCityFlag,
Aliases: []string{"t"},
Value: false,
Usage: "Report problems using TeamCity Service Messages",
},
},
}

Expand Down Expand Up @@ -87,7 +93,13 @@ func actionLint(c *cli.Context) error {
return fmt.Errorf("invalid --%s value: %w", failOnFlag, err)
}

r := reporter.NewConsoleReporter(os.Stderr, minSeverity)
var r reporter.Reporter
if c.Bool(teamCityFlag) {
r = reporter.NewTeamCityReporter(os.Stderr)
} else {
r = reporter.NewConsoleReporter(os.Stderr, minSeverity)
}

err = r.Submit(summary)
if err != nil {
return err
Expand Down
31 changes: 31 additions & 0 deletions cmd/pint/tests/0158_lint_teamcity.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
env NO_COLOR=1
pint.error --no-color lint --min-severity=info --teamcity rules
! stdout .
cmp stderr stderr.txt

-- stderr.txt --
level=INFO msg="Finding all rules to check" paths=["rules"]
##teamcity[testSuiteStarted name='promql/syntax']
##teamcity[testSuiteStarted name='Fatal']
##teamcity[testStarted name='rules/0001.yml:7']
##teamcity[testFailed name='rules/0001.yml:7' message='' details='syntax error: unexpected identifier "with"']
##teamcity[testFinished name='rules/0001.yml:7']
##teamcity[testSuiteFinished name='Fatal']
##teamcity[testSuiteFinished name='promql/syntax']
##teamcity[testSuiteStarted name='alerts/comparison']
##teamcity[testSuiteStarted name='Warning']
##teamcity[testStarted name='rules/0001.yml:5']
##teamcity[testStdErr name='rules/0001.yml:5' out='alert query doesn|'t have any condition, it will always fire if the metric exists']
##teamcity[testFinished name='rules/0001.yml:5']
##teamcity[testSuiteFinished name='Warning']
##teamcity[testSuiteFinished name='alerts/comparison']
level=INFO msg="Problems found" Fatal=1 Warning=1
level=ERROR msg="Fatal error" err="found 1 problem(s) with severity Bug or higher"
-- rules/0001.yml --
groups:
- name: test
rules:
- alert: Example
expr: up
- alert: Example
expr: sum(xxx) with()
64 changes: 64 additions & 0 deletions cmd/pint/tests/0159_ci_teamcity.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
mkdir testrepo
cd testrepo
exec git init --initial-branch=main .

cp ../src/.pint.hcl .
env GIT_AUTHOR_NAME=pint
env GIT_AUTHOR_EMAIL=pint@example.com
env GIT_COMMITTER_NAME=pint
env GIT_COMMITTER_EMAIL=pint@example.com
exec git add .
exec git commit -am 'import rules and config'

exec git checkout -b v1
cp ../src/a.yml a.yml
exec git add a.yml
exec git commit -am 'v1'

exec git checkout -b v2
cp ../src/b.yml b.yml
exec git add b.yml
exec git commit -am 'v2'

exec git checkout -b v3
exec git rm a.yml
exec git commit -am 'v3'

pint.error --no-color ci -t
! stdout .
cmp stderr ../stderr.txt

-- stderr.txt --
level=INFO msg="Loading configuration file" path=.pint.hcl
level=INFO msg="Finding all rules to check on current git branch using git blame" base=main
level=INFO msg="Problems found" Fatal=1 Warning=1
##teamcity[testSuiteStarted name='alerts/comparison']
##teamcity[testSuiteStarted name='Warning']
##teamcity[testStarted name='b.yml:4']
##teamcity[testStdErr name='b.yml:4' out='alert query doesn|'t have any condition, it will always fire if the metric exists']
##teamcity[testFinished name='b.yml:4']
##teamcity[testSuiteFinished name='Warning']
##teamcity[testSuiteFinished name='alerts/comparison']
##teamcity[testSuiteStarted name='promql/syntax']
##teamcity[testSuiteStarted name='Fatal']
##teamcity[testStarted name='b.yml:2']
##teamcity[testFailed name='b.yml:2' message='' details='syntax error: unexpected identifier "bi"']
##teamcity[testFinished name='b.yml:2']
##teamcity[testSuiteFinished name='Fatal']
##teamcity[testSuiteFinished name='promql/syntax']
level=ERROR msg="Fatal error" err="problems found"
-- src/a.yml --
- record: rule1
expr: sum(foo) bi()
-- src/b.yml --
- record: rule1
expr: sum(foo) bi()
- alert: rule2
expr: sum(foo)
-- src/.pint.hcl --
ci {
baseBranch = "main"
}
parser {
relaxed = [".*"]
}
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Added

- Added [alerts/external_labels](checks/alerts/external_labels.md) check.
- Added support for reporting problems to TeamCity using [Service Messages](https://www.jetbrains.com/help/teamcity/service-messages.html).
To enable run it run `pint --teamcity lint` or `pint --teamcity ci`.

### Changed

Expand Down
87 changes: 87 additions & 0 deletions internal/reporter/teamcity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package reporter

import (
"fmt"
"io"
"strconv"
"strings"

"github.com/cloudflare/pint/internal/checks"
)

func NewTeamCityReporter(output io.Writer) TeamCityReporter {
return TeamCityReporter{
output: output,
escaper: strings.NewReplacer(
"'", "|'",
"\n", "|n",
"\r", "|r",
"\\uNNNN", "|0xNNNN",
"|", "||",
"[", "|[",
"]", "|]",
),
}
}

type TeamCityReporter struct {
output io.Writer
escaper *strings.Replacer
}

func (tc TeamCityReporter) name(report Report) string {
return fmt.Sprintf("%s:%d", report.ReportedPath, report.Problem.Lines[0])
}

func (tc TeamCityReporter) escape(s string) string {
return tc.escaper.Replace(s)
}

func (tc TeamCityReporter) Submit(summary Summary) error {
var buf strings.Builder
for _, report := range summary.reports {
buf.WriteString("##teamcity[testSuiteStarted name='")
buf.WriteString(report.Problem.Reporter)
buf.WriteString("']\n")

buf.WriteString("##teamcity[testSuiteStarted name='")
buf.WriteString(report.Problem.Severity.String())
buf.WriteString("']\n")

buf.WriteString("##teamcity[testStarted name='")
buf.WriteString(tc.name(report))
buf.WriteString("']\n")

if report.Problem.Severity >= checks.Bug {
buf.WriteString("##teamcity[testFailed name='")
buf.WriteString(tc.name(report))
buf.WriteString("' message='' details='")
buf.WriteString(tc.escape(report.Problem.Text))
buf.WriteString("']\n")
} else {
buf.WriteString("##teamcity[testStdErr name='")
buf.WriteString(tc.name(report))
buf.WriteString("' out='")
buf.WriteString(tc.escape(report.Problem.Text))
buf.WriteString("']\n")
}

buf.WriteString("##teamcity[testFinished name='")
buf.WriteString(report.ReportedPath)
buf.WriteRune(':')
buf.WriteString(strconv.Itoa(report.Problem.Lines[0]))
buf.WriteString("']\n")

buf.WriteString("##teamcity[testSuiteFinished name='")
buf.WriteString(report.Problem.Severity.String())
buf.WriteString("']\n")

buf.WriteString("##teamcity[testSuiteFinished name='")
buf.WriteString(report.Problem.Reporter)
buf.WriteString("']\n")

fmt.Fprint(tc.output, buf.String())
buf.Reset()
}
return nil
}
135 changes: 135 additions & 0 deletions internal/reporter/teamcity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package reporter_test

import (
"bytes"
"log/slog"
"testing"

"github.com/neilotoole/slogt"
"github.com/stretchr/testify/require"

"github.com/cloudflare/pint/internal/checks"
"github.com/cloudflare/pint/internal/parser"
"github.com/cloudflare/pint/internal/reporter"
)

func TestTeamCityReporter(t *testing.T) {
type testCaseT struct {
description string
summary reporter.Summary
output string
err string
}

p := parser.NewParser()
mockRules, _ := p.Parse([]byte(`
- record: target is down
expr: up == 0
`))

testCases := []testCaseT{
{
description: "no reports",
summary: reporter.Summary{},
output: "",
},
{
description: "info report",
summary: reporter.NewSummary([]reporter.Report{
{
ReportedPath: "foo.txt",
SourcePath: "foo.txt",
ModifiedLines: []int{2, 4, 5},
Rule: mockRules[0],
Problem: checks.Problem{
Fragment: "up",
Lines: []int{5, 6},
Reporter: "mock",
Text: "mock text",
Severity: checks.Information,
},
},
}),
output: `##teamcity[testSuiteStarted name='mock']
##teamcity[testSuiteStarted name='Information']
##teamcity[testStarted name='foo.txt:5']
##teamcity[testStdErr name='foo.txt:5' out='mock text']
##teamcity[testFinished name='foo.txt:5']
##teamcity[testSuiteFinished name='Information']
##teamcity[testSuiteFinished name='mock']
`,
},
{
description: "bug report",
summary: reporter.NewSummary([]reporter.Report{
{
ReportedPath: "foo.txt",
SourcePath: "foo.txt",
ModifiedLines: []int{2, 4, 5},
Rule: mockRules[0],
Problem: checks.Problem{
Fragment: "up",
Lines: []int{5, 6},
Reporter: "mock",
Text: "mock text",
Severity: checks.Bug,
},
},
}),
output: `##teamcity[testSuiteStarted name='mock']
##teamcity[testSuiteStarted name='Bug']
##teamcity[testStarted name='foo.txt:5']
##teamcity[testFailed name='foo.txt:5' message='' details='mock text']
##teamcity[testFinished name='foo.txt:5']
##teamcity[testSuiteFinished name='Bug']
##teamcity[testSuiteFinished name='mock']
`,
},
{
description: "escaping characters",
summary: reporter.NewSummary([]reporter.Report{
{
ReportedPath: "foo.txt",
SourcePath: "foo.txt",
ModifiedLines: []int{2, 4, 5},
Rule: mockRules[0],
Problem: checks.Problem{
Fragment: "up",
Lines: []int{5, 6},
Reporter: "mock",
Text: `mock text
with [new lines] and pipe| chars that are 'quoted'
`,
Severity: checks.Bug,
},
},
}),
output: `##teamcity[testSuiteStarted name='mock']
##teamcity[testSuiteStarted name='Bug']
##teamcity[testStarted name='foo.txt:5']
##teamcity[testFailed name='foo.txt:5' message='' details='mock text|nwith |[new lines|] and pipe|| chars that are |'quoted|'|n']
##teamcity[testFinished name='foo.txt:5']
##teamcity[testSuiteFinished name='Bug']
##teamcity[testSuiteFinished name='mock']
`,
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
slog.SetDefault(slogt.New(t))

out := bytes.NewBuffer(nil)

reporter := reporter.NewTeamCityReporter(out)
err := reporter.Submit(tc.summary)

if tc.err != "" {
require.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
require.Equal(t, tc.output, out.String())
}
})
}
}

0 comments on commit 71fce28

Please sign in to comment.