Skip to content

Commit afbe198

Browse files
authored
feat: report uncovered lines (#152)
1 parent 652d831 commit afbe198

File tree

10 files changed

+329
-45
lines changed

10 files changed

+329
-45
lines changed

pkg/testcoverage/check.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ func Analyze(cfg Config, current, base []coverage.Stats) AnalyzeResult {
8686
PackagesBelowThreshold: checkCoverageStatsBelowThreshold(
8787
makePackageStats(current), thr.Package, overrideRules,
8888
),
89-
TotalStats: coverage.CalcTotalStats(current),
90-
HasBaseBreakdown: len(base) > 0,
91-
Diff: calculateStatsDiff(current, base),
89+
FilesWithUncoveredLines: coverage.StatsFilterWithUncoveredLines(current),
90+
TotalStats: coverage.CalcTotalStats(current),
91+
HasBaseBreakdown: len(base) > 0,
92+
Diff: calculateStatsDiff(current, base),
9293
}
9394
}
9495

pkg/testcoverage/check_test.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestCheck(t *testing.T) {
3030
return
3131
}
3232

33-
prefix := "github.com/vladopajic/go-test-coverage/v2"
33+
const prefix = "github.com/vladopajic/go-test-coverage/v2"
3434

3535
t.Run("no profile", func(t *testing.T) {
3636
t.Parallel()
@@ -40,6 +40,7 @@ func TestCheck(t *testing.T) {
4040
assert.False(t, pass)
4141
assertGithubActionErrorsCount(t, buf.String(), 0)
4242
assertHumanReport(t, buf.String(), 0, 0)
43+
assertNoUncoveredLinesInfo(t, buf.String())
4344
})
4445

4546
t.Run("invalid profile", func(t *testing.T) {
@@ -51,6 +52,7 @@ func TestCheck(t *testing.T) {
5152
assert.False(t, pass)
5253
assertGithubActionErrorsCount(t, buf.String(), 0)
5354
assertHumanReport(t, buf.String(), 0, 0)
55+
assertNoUncoveredLinesInfo(t, buf.String())
5456
})
5557

5658
t.Run("valid profile - pass", func(t *testing.T) {
@@ -62,6 +64,7 @@ func TestCheck(t *testing.T) {
6264
assert.True(t, pass)
6365
assertGithubActionErrorsCount(t, buf.String(), 0)
6466
assertHumanReport(t, buf.String(), 1, 0)
67+
assertNoUncoveredLinesInfo(t, buf.String())
6568
})
6669

6770
t.Run("valid profile with exclude - pass", func(t *testing.T) {
@@ -79,6 +82,7 @@ func TestCheck(t *testing.T) {
7982
assert.True(t, pass)
8083
assertGithubActionErrorsCount(t, buf.String(), 0)
8184
assertHumanReport(t, buf.String(), 1, 0)
85+
assertNoUncoveredLinesInfo(t, buf.String())
8286
})
8387

8488
t.Run("valid profile - fail", func(t *testing.T) {
@@ -90,10 +94,15 @@ func TestCheck(t *testing.T) {
9094
assert.False(t, pass)
9195
assertGithubActionErrorsCount(t, buf.String(), 0)
9296
assertHumanReport(t, buf.String(), 0, 1)
93-
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
97+
assertHasUncoveredLinesInfo(t, buf.String(), []string{
98+
"pkg/testcoverage/badgestorer/cdn.go",
99+
"pkg/testcoverage/badgestorer/github.go",
100+
"pkg/testcoverage/check.go",
101+
"pkg/testcoverage/coverage/cover.go",
102+
})
94103
})
95104

96-
t.Run("valid profile - fail with prefix", func(t *testing.T) {
105+
t.Run("valid profile - pass with prefix", func(t *testing.T) {
97106
t.Parallel()
98107

99108
buf := &bytes.Buffer{}
@@ -103,7 +112,8 @@ func TestCheck(t *testing.T) {
103112
assert.True(t, pass)
104113
assertGithubActionErrorsCount(t, buf.String(), 0)
105114
assertHumanReport(t, buf.String(), 1, 0)
106-
assert.Equal(t, 0, strings.Count(buf.String(), prefix))
115+
assertNoFileNames(t, buf.String(), prefix)
116+
assertNoUncoveredLinesInfo(t, buf.String())
107117
})
108118

109119
t.Run("valid profile - pass after override", func(t *testing.T) {
@@ -119,7 +129,8 @@ func TestCheck(t *testing.T) {
119129
assert.True(t, pass)
120130
assertGithubActionErrorsCount(t, buf.String(), 0)
121131
assertHumanReport(t, buf.String(), 2, 0)
122-
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
132+
assertNoFileNames(t, buf.String(), prefix)
133+
assertNoUncoveredLinesInfo(t, buf.String())
123134
})
124135

125136
t.Run("valid profile - fail after override", func(t *testing.T) {
@@ -135,7 +146,12 @@ func TestCheck(t *testing.T) {
135146
assert.False(t, pass)
136147
assertGithubActionErrorsCount(t, buf.String(), 0)
137148
assertHumanReport(t, buf.String(), 0, 2)
138-
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
149+
assertHasUncoveredLinesInfo(t, buf.String(), []string{
150+
"pkg/testcoverage/badgestorer/cdn.go",
151+
"pkg/testcoverage/badgestorer/github.go",
152+
"pkg/testcoverage/check.go",
153+
"pkg/testcoverage/coverage/cover.go",
154+
})
139155
})
140156

141157
t.Run("valid profile - pass after file override", func(t *testing.T) {
@@ -151,7 +167,8 @@ func TestCheck(t *testing.T) {
151167
assert.True(t, pass)
152168
assertGithubActionErrorsCount(t, buf.String(), 0)
153169
assertHumanReport(t, buf.String(), 1, 0)
154-
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
170+
assertNoFileNames(t, buf.String(), prefix)
171+
assertNoUncoveredLinesInfo(t, buf.String())
155172
})
156173

157174
t.Run("valid profile - fail after file override", func(t *testing.T) {
@@ -168,6 +185,12 @@ func TestCheck(t *testing.T) {
168185
assertGithubActionErrorsCount(t, buf.String(), 0)
169186
assertHumanReport(t, buf.String(), 0, 1)
170187
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
188+
assertHasUncoveredLinesInfo(t, buf.String(), []string{
189+
"pkg/testcoverage/badgestorer/cdn.go",
190+
"pkg/testcoverage/badgestorer/github.go",
191+
"pkg/testcoverage/check.go",
192+
"pkg/testcoverage/coverage/cover.go",
193+
})
171194
})
172195

173196
t.Run("valid profile - fail couldn't save badge", func(t *testing.T) {
@@ -260,6 +283,7 @@ func TestCheckNoParallel(t *testing.T) {
260283
assertGithubActionErrorsCount(t, buf.String(), 0)
261284
assertHumanReport(t, buf.String(), 1, 0)
262285
assertGithubOutputValues(t, testFile)
286+
assertNoUncoveredLinesInfo(t, buf.String())
263287
})
264288

265289
t.Run("ok fail; with github output file", func(t *testing.T) {
@@ -273,6 +297,7 @@ func TestCheckNoParallel(t *testing.T) {
273297
assertGithubActionErrorsCount(t, buf.String(), 1)
274298
assertHumanReport(t, buf.String(), 0, 1)
275299
assertGithubOutputValues(t, testFile)
300+
assertHasUncoveredLinesInfo(t, buf.String(), []string{})
276301
})
277302
}
278303

pkg/testcoverage/coverage/cover.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"go/token"
99
"os"
1010
"path/filepath"
11+
"sort"
1112
"strings"
1213

1314
"golang.org/x/tools/cover"
@@ -258,27 +259,35 @@ func sumCoverage(profile *cover.Profile, funcs, blocks, annotations []extent) St
258259
s := Stats{}
259260

260261
for _, f := range funcs {
261-
c, t := coverage(profile, f, blocks, annotations)
262+
c, t, ul := coverage(profile, f, blocks, annotations)
262263
s.Total += t
263264
s.Covered += c
265+
s.UncoveredLines = append(s.UncoveredLines, ul...)
264266
}
265267

268+
s.UncoveredLines = dedup(s.UncoveredLines)
269+
266270
return s
267271
}
268272

269273
// coverage returns the fraction of the statements in the
270274
// function that were covered, as a numerator and denominator.
271275
//
272-
//nolint:cyclop,gocognit // relax
273-
func coverage(profile *cover.Profile, f extent, blocks, annotations []extent) (int64, int64) {
276+
//nolint:cyclop,gocognit,maintidx // relax
277+
func coverage(
278+
profile *cover.Profile,
279+
f extent,
280+
blocks, annotations []extent,
281+
) (int64, int64, []int) {
274282
if hasExtentWithStartLine(annotations, f.StartLine) {
275283
// case when entire function is ignored
276-
return 0, 0
284+
return 0, 0, nil
277285
}
278286

279287
var (
280288
covered, total int64
281289
skip extent
290+
uncoveredLines []int
282291
)
283292

284293
// the blocks are sorted, so we can stop counting as soon as
@@ -312,8 +321,29 @@ func coverage(profile *cover.Profile, f extent, blocks, annotations []extent) (i
312321

313322
if b.Count > 0 {
314323
covered += int64(b.NumStmt)
324+
} else {
325+
for i := range (b.EndLine - b.StartLine) + 1 {
326+
uncoveredLines = append(uncoveredLines, b.StartLine+i)
327+
}
328+
}
329+
}
330+
331+
return covered, total, uncoveredLines
332+
}
333+
334+
func dedup(ss []int) []int {
335+
if len(ss) <= 1 {
336+
return ss
337+
}
338+
339+
sort.Ints(ss)
340+
result := []int{ss[0]}
341+
342+
for i := 1; i < len(ss); i++ {
343+
if ss[i] != ss[i-1] {
344+
result = append(result, ss[i])
315345
}
316346
}
317347

318-
return covered, total
348+
return result
319349
}

pkg/testcoverage/coverage/cover_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,10 @@ func Test_sumCoverage(t *testing.T) {
206206
}}
207207

208208
s := SumCoverage(profile, funcs, nil, nil)
209-
assert.Equal(t, Stats{Total: 10, Covered: 0}, s)
209+
expected := Stats{Total: 10, Covered: 0, UncoveredLines: []int{
210+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20,
211+
}}
212+
assert.Equal(t, expected, s)
210213

211214
// Coverage should be empty when every function is excluded
212215
s = SumCoverage(profile, funcs, nil, funcs)
@@ -216,7 +219,10 @@ func Test_sumCoverage(t *testing.T) {
216219
annotations := []Extent{{StartLine: 4, EndLine: 4}}
217220
blocks := []Extent{{StartLine: 4, EndLine: 10}}
218221
s = SumCoverage(profile, funcs, blocks, annotations)
219-
assert.Equal(t, Stats{Total: 7, Covered: 0}, s)
222+
expected = Stats{Total: 7, Covered: 0, UncoveredLines: []int{
223+
1, 2, 3, 12, 13, 14, 15, 16, 17, 18, 19, 20,
224+
}}
225+
assert.Equal(t, expected, s)
220226
}
221227

222228
func pluckStartLine(extents []Extent) []int {

pkg/testcoverage/coverage/types.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import (
1111
)
1212

1313
type Stats struct {
14-
Name string
15-
Total int64
16-
Covered int64
17-
Threshold int
14+
Name string
15+
Total int64
16+
Covered int64
17+
Threshold int
18+
UncoveredLines []int
1819
}
1920

20-
func (s Stats) UncoveredLines() int {
21+
func (s Stats) UncoveredLinesCount() int {
2122
return int(s.Total - s.Covered)
2223
}
2324

@@ -118,6 +119,40 @@ func CalcTotalStats(stats []Stats) Stats {
118119
return total
119120
}
120121

122+
func StatsPluckName(stats []Stats) []string {
123+
result := make([]string, len(stats))
124+
125+
for i, s := range stats {
126+
result[i] = s.Name
127+
}
128+
129+
return result
130+
}
131+
132+
func StatsFilterWithUncoveredLines(stats []Stats) []Stats {
133+
return filter(stats, func(s Stats) bool {
134+
return len(s.UncoveredLines) > 0
135+
})
136+
}
137+
138+
func StatsFilterWithCoveredLines(stats []Stats) []Stats {
139+
return filter(stats, func(s Stats) bool {
140+
return len(s.UncoveredLines) == 0
141+
})
142+
}
143+
144+
func filter[T any](slice []T, predicate func(T) bool) []T {
145+
var result []T
146+
147+
for _, value := range slice {
148+
if predicate(value) {
149+
result = append(result, value)
150+
}
151+
}
152+
153+
return result
154+
}
155+
121156
func SerializeStats(stats []Stats) []byte {
122157
b := bytes.Buffer{}
123158
sep, nl := []byte(";"), []byte("\n")

pkg/testcoverage/export_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ var (
1515
GenerateAndSaveBadge = generateAndSaveBadge
1616
SetOutputValue = setOutputValue
1717
LoadBaseCoverageBreakdown = loadBaseCoverageBreakdown
18+
CompressUncoveredLines = compressUncoveredLines
19+
ReportUncoveredLines = reportUncoveredLines
1820
)
1921

2022
type (

0 commit comments

Comments
 (0)