Skip to content

Commit b48e731

Browse files
committed
feat: add team city output format
fixes #3597
1 parent 69e5481 commit b48e731

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

pkg/commands/run.go

+2
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer,
494494
p = printers.NewJunitXML(w)
495495
case config.OutFormatGithubActions:
496496
p = printers.NewGithub(w)
497+
case config.OutFormatTeamCity:
498+
p = printers.NewTeamCity(&e.reportData, w, nil)
497499
default:
498500
return nil, fmt.Errorf("unknown output format %s", format)
499501
}

pkg/config/output.go

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const (
1010
OutFormatHTML = "html"
1111
OutFormatJunitXML = "junit-xml"
1212
OutFormatGithubActions = "github-actions"
13+
OutFormatTeamCity = "team-city"
1314
)
1415

1516
var OutFormats = []string{
@@ -22,6 +23,7 @@ var OutFormats = []string{
2223
OutFormatHTML,
2324
OutFormatJunitXML,
2425
OutFormatGithubActions,
26+
OutFormatTeamCity,
2527
}
2628

2729
type Output struct {

pkg/printers/teamcity.go

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package printers
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"sort"
9+
"strings"
10+
"time"
11+
12+
"github.com/golangci/golangci-lint/pkg/report"
13+
"github.com/golangci/golangci-lint/pkg/result"
14+
)
15+
16+
const (
17+
timestampFormat = "2006-01-02T15:04:05.000"
18+
testStarted = "##teamcity[testStarted timestamp='%s' name='%s']\n"
19+
testStdErr = "##teamcity[testStdErr timestamp='%s' name='%s' out='%s']\n"
20+
testFailed = "##teamcity[testFailed timestamp='%s' name='%s']\n"
21+
testIgnored = "##teamcity[testIgnored timestamp='%s' name='%s']\n"
22+
testFinished = "##teamcity[testFinished timestamp='%s' name='%s']\n"
23+
)
24+
25+
type teamcityLinter struct {
26+
data *report.LinterData
27+
issues []string
28+
}
29+
30+
func (l *teamcityLinter) getName() string {
31+
return fmt.Sprintf("linter: %s", l.data.Name)
32+
}
33+
34+
func (l *teamcityLinter) failed() bool {
35+
return len(l.issues) > 0
36+
}
37+
38+
type teamcity struct {
39+
linters map[string]*teamcityLinter
40+
w io.Writer
41+
err error
42+
now now
43+
}
44+
45+
type now func() string
46+
47+
// NewTeamCity output format outputs issues according to TeamCity service message format
48+
func NewTeamCity(rd *report.Data, w io.Writer, nower now) Printer {
49+
t := &teamcity{
50+
linters: map[string]*teamcityLinter{},
51+
w: w,
52+
now: nower,
53+
}
54+
if t.now == nil {
55+
t.now = func() string {
56+
return time.Now().Format(timestampFormat)
57+
}
58+
}
59+
for i, l := range rd.Linters {
60+
t.linters[l.Name] = &teamcityLinter{
61+
data: &rd.Linters[i],
62+
}
63+
}
64+
return t
65+
}
66+
67+
func (p *teamcity) getSortedLinterNames() []string {
68+
names := make([]string, 0, len(p.linters))
69+
for name := range p.linters {
70+
names = append(names, name)
71+
}
72+
sort.Strings(names)
73+
return names
74+
}
75+
76+
// escape transforms strings for TeamCity service messages
77+
// https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values
78+
func (p *teamcity) escape(s string) string {
79+
var buf bytes.Buffer
80+
for {
81+
nextSpecial := strings.IndexAny(s, "'\n\r|[]")
82+
switch nextSpecial {
83+
case -1:
84+
if buf.Len() == 0 {
85+
return s
86+
}
87+
return buf.String() + s
88+
case 0:
89+
default:
90+
buf.WriteString(s[:nextSpecial])
91+
}
92+
switch s[nextSpecial] {
93+
case '\'':
94+
buf.WriteString("|'")
95+
case '\n':
96+
buf.WriteString("|n")
97+
case '\r':
98+
buf.WriteString("|r")
99+
case '|':
100+
buf.WriteString("||")
101+
case '[':
102+
buf.WriteString("|[")
103+
case ']':
104+
buf.WriteString("|]")
105+
}
106+
s = s[nextSpecial+1:]
107+
}
108+
}
109+
110+
func (p *teamcity) print(format string, args ...any) {
111+
if p.err != nil {
112+
return
113+
}
114+
args = append([]any{p.now()}, args...)
115+
_, p.err = fmt.Fprintf(p.w, format, args...)
116+
}
117+
118+
func (p *teamcity) Print(_ context.Context, issues []result.Issue) error {
119+
for i := range issues {
120+
issue := &issues[i]
121+
122+
var col string
123+
if issue.Pos.Column != 0 {
124+
col = fmt.Sprintf(":%d", issue.Pos.Column)
125+
}
126+
127+
formatted := fmt.Sprintf("%s:%v%s - %s", issue.FilePath(), issue.Line(), col, issue.Text)
128+
p.linters[issue.FromLinter].issues = append(p.linters[issue.FromLinter].issues, formatted)
129+
}
130+
131+
for _, linterName := range p.getSortedLinterNames() {
132+
linter := p.linters[linterName]
133+
134+
name := p.escape(linter.getName())
135+
p.print(testStarted, name)
136+
if !linter.data.Enabled && !linter.data.EnabledByDefault {
137+
p.print(testIgnored, name)
138+
continue
139+
}
140+
141+
if linter.failed() {
142+
for _, issue := range linter.issues {
143+
p.print(testStdErr, name, p.escape(issue))
144+
}
145+
p.print(testFailed, name)
146+
} else {
147+
p.print(testFinished, name)
148+
}
149+
}
150+
return p.err
151+
}

pkg/printers/teamcity_test.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//nolint:dupl
2+
package printers
3+
4+
import (
5+
"bytes"
6+
"context"
7+
"go/token"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/golangci/golangci-lint/pkg/report"
14+
"github.com/golangci/golangci-lint/pkg/result"
15+
)
16+
17+
func TestTeamCity_Print(t *testing.T) {
18+
issues := []result.Issue{
19+
{
20+
FromLinter: "linter-a",
21+
Severity: "error",
22+
Text: "some issue",
23+
Pos: token.Position{
24+
Filename: "path/to/filea.go",
25+
Offset: 2,
26+
Line: 10,
27+
Column: 4,
28+
},
29+
},
30+
{
31+
FromLinter: "linter-a",
32+
Severity: "error",
33+
Text: "some issue 2",
34+
Pos: token.Position{
35+
Filename: "path/to/filea.go",
36+
Offset: 2,
37+
Line: 10,
38+
},
39+
},
40+
{
41+
FromLinter: "linter-b",
42+
Severity: "error",
43+
Text: "another issue",
44+
SourceLines: []string{
45+
"func foo() {",
46+
"\tfmt.Println(\"bar\")",
47+
"}",
48+
},
49+
Pos: token.Position{
50+
Filename: "path/to/fileb.go",
51+
Offset: 5,
52+
Line: 300,
53+
Column: 9,
54+
},
55+
},
56+
}
57+
58+
buf := new(bytes.Buffer)
59+
rd := &report.Data{
60+
Linters: []report.LinterData{
61+
{Name: "linter-a", Enabled: true},
62+
{Name: "linter-b", Enabled: false},
63+
},
64+
}
65+
nower := func() string {
66+
return "2023-02-17T15:42:23.630"
67+
}
68+
printer := NewTeamCity(rd, buf, nower)
69+
70+
err := printer.Print(context.Background(), issues)
71+
require.NoError(t, err)
72+
73+
expected := `##teamcity[testStarted timestamp='2023-02-17T15:42:23.630' name='linter: linter-a']
74+
##teamcity[testStdErr timestamp='2023-02-17T15:42:23.630' name='linter: linter-a' out='path/to/filea.go:10:4 - some issue']
75+
##teamcity[testStdErr timestamp='2023-02-17T15:42:23.630' name='linter: linter-a' out='path/to/filea.go:10 - some issue 2']
76+
##teamcity[testFailed timestamp='2023-02-17T15:42:23.630' name='linter: linter-a']
77+
##teamcity[testStarted timestamp='2023-02-17T15:42:23.630' name='linter: linter-b']
78+
##teamcity[testIgnored timestamp='2023-02-17T15:42:23.630' name='linter: linter-b']
79+
`
80+
81+
assert.Equal(t, expected, buf.String())
82+
}

0 commit comments

Comments
 (0)