Skip to content

feat: report uncovered lines #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions pkg/testcoverage/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ func Analyze(cfg Config, current, base []coverage.Stats) AnalyzeResult {
PackagesBelowThreshold: checkCoverageStatsBelowThreshold(
makePackageStats(current), thr.Package, overrideRules,
),
TotalStats: coverage.CalcTotalStats(current),
HasBaseBreakdown: len(base) > 0,
Diff: calculateStatsDiff(current, base),
FilesWithUncoveredLines: coverage.StatsFilterWithUncoveredLines(current),
TotalStats: coverage.CalcTotalStats(current),
HasBaseBreakdown: len(base) > 0,
Diff: calculateStatsDiff(current, base),
}
}

Expand Down
39 changes: 32 additions & 7 deletions pkg/testcoverage/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestCheck(t *testing.T) {
return
}

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

t.Run("no profile", func(t *testing.T) {
t.Parallel()
Expand All @@ -40,6 +40,7 @@ func TestCheck(t *testing.T) {
assert.False(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 0, 0)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("invalid profile", func(t *testing.T) {
Expand All @@ -51,6 +52,7 @@ func TestCheck(t *testing.T) {
assert.False(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 0, 0)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("valid profile - pass", func(t *testing.T) {
Expand All @@ -62,6 +64,7 @@ func TestCheck(t *testing.T) {
assert.True(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 1, 0)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("valid profile with exclude - pass", func(t *testing.T) {
Expand All @@ -79,6 +82,7 @@ func TestCheck(t *testing.T) {
assert.True(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 1, 0)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("valid profile - fail", func(t *testing.T) {
Expand All @@ -90,10 +94,15 @@ func TestCheck(t *testing.T) {
assert.False(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 0, 1)
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
assertHasUncoveredLinesInfo(t, buf.String(), []string{
"pkg/testcoverage/badgestorer/cdn.go",
"pkg/testcoverage/badgestorer/github.go",
"pkg/testcoverage/check.go",
"pkg/testcoverage/coverage/cover.go",
})
})

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

buf := &bytes.Buffer{}
Expand All @@ -103,7 +112,8 @@ func TestCheck(t *testing.T) {
assert.True(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 1, 0)
assert.Equal(t, 0, strings.Count(buf.String(), prefix))
assertNoFileNames(t, buf.String(), prefix)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("valid profile - pass after override", func(t *testing.T) {
Expand All @@ -119,7 +129,8 @@ func TestCheck(t *testing.T) {
assert.True(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 2, 0)
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
assertNoFileNames(t, buf.String(), prefix)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("valid profile - fail after override", func(t *testing.T) {
Expand All @@ -135,7 +146,12 @@ func TestCheck(t *testing.T) {
assert.False(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 0, 2)
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
assertHasUncoveredLinesInfo(t, buf.String(), []string{
"pkg/testcoverage/badgestorer/cdn.go",
"pkg/testcoverage/badgestorer/github.go",
"pkg/testcoverage/check.go",
"pkg/testcoverage/coverage/cover.go",
})
})

t.Run("valid profile - pass after file override", func(t *testing.T) {
Expand All @@ -151,7 +167,8 @@ func TestCheck(t *testing.T) {
assert.True(t, pass)
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 1, 0)
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
assertNoFileNames(t, buf.String(), prefix)
assertNoUncoveredLinesInfo(t, buf.String())
})

t.Run("valid profile - fail after file override", func(t *testing.T) {
Expand All @@ -168,6 +185,12 @@ func TestCheck(t *testing.T) {
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 0, 1)
assert.GreaterOrEqual(t, strings.Count(buf.String(), prefix), 0)
assertHasUncoveredLinesInfo(t, buf.String(), []string{
"pkg/testcoverage/badgestorer/cdn.go",
"pkg/testcoverage/badgestorer/github.go",
"pkg/testcoverage/check.go",
"pkg/testcoverage/coverage/cover.go",
})
})

t.Run("valid profile - fail couldn't save badge", func(t *testing.T) {
Expand Down Expand Up @@ -260,6 +283,7 @@ func TestCheckNoParallel(t *testing.T) {
assertGithubActionErrorsCount(t, buf.String(), 0)
assertHumanReport(t, buf.String(), 1, 0)
assertGithubOutputValues(t, testFile)
assertNoUncoveredLinesInfo(t, buf.String())
})

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

Expand Down
40 changes: 35 additions & 5 deletions pkg/testcoverage/coverage/cover.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go/token"
"os"
"path/filepath"
"sort"
"strings"

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

for _, f := range funcs {
c, t := coverage(profile, f, blocks, annotations)
c, t, ul := coverage(profile, f, blocks, annotations)
s.Total += t
s.Covered += c
s.UncoveredLines = append(s.UncoveredLines, ul...)
}

s.UncoveredLines = dedup(s.UncoveredLines)

return s
}

// coverage returns the fraction of the statements in the
// function that were covered, as a numerator and denominator.
//
//nolint:cyclop,gocognit // relax
func coverage(profile *cover.Profile, f extent, blocks, annotations []extent) (int64, int64) {
//nolint:cyclop,gocognit,maintidx // relax
func coverage(
profile *cover.Profile,
f extent,
blocks, annotations []extent,
) (int64, int64, []int) {
if hasExtentWithStartLine(annotations, f.StartLine) {
// case when entire function is ignored
return 0, 0
return 0, 0, nil
}

var (
covered, total int64
skip extent
uncoveredLines []int
)

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

if b.Count > 0 {
covered += int64(b.NumStmt)
} else {
for i := range (b.EndLine - b.StartLine) + 1 {
uncoveredLines = append(uncoveredLines, b.StartLine+i)
}
}
}

return covered, total, uncoveredLines
}

func dedup(ss []int) []int {
if len(ss) <= 1 {
return ss
}

sort.Ints(ss)
result := []int{ss[0]}

for i := 1; i < len(ss); i++ {
if ss[i] != ss[i-1] {
result = append(result, ss[i])
}
}

return covered, total
return result
}
10 changes: 8 additions & 2 deletions pkg/testcoverage/coverage/cover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ func Test_sumCoverage(t *testing.T) {
}}

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

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

func pluckStartLine(extents []Extent) []int {
Expand Down
45 changes: 40 additions & 5 deletions pkg/testcoverage/coverage/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
)

type Stats struct {
Name string
Total int64
Covered int64
Threshold int
Name string
Total int64
Covered int64
Threshold int
UncoveredLines []int
}

func (s Stats) UncoveredLines() int {
func (s Stats) UncoveredLinesCount() int {
return int(s.Total - s.Covered)
}

Expand Down Expand Up @@ -118,6 +119,40 @@ func CalcTotalStats(stats []Stats) Stats {
return total
}

func StatsPluckName(stats []Stats) []string {
result := make([]string, len(stats))

for i, s := range stats {
result[i] = s.Name
}

return result
}

func StatsFilterWithUncoveredLines(stats []Stats) []Stats {
return filter(stats, func(s Stats) bool {
return len(s.UncoveredLines) > 0
})
}

func StatsFilterWithCoveredLines(stats []Stats) []Stats {
return filter(stats, func(s Stats) bool {
return len(s.UncoveredLines) == 0
})
}

func filter[T any](slice []T, predicate func(T) bool) []T {
var result []T

for _, value := range slice {
if predicate(value) {
result = append(result, value)
}
}

return result
}

func SerializeStats(stats []Stats) []byte {
b := bytes.Buffer{}
sep, nl := []byte(";"), []byte("\n")
Expand Down
2 changes: 2 additions & 0 deletions pkg/testcoverage/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var (
GenerateAndSaveBadge = generateAndSaveBadge
SetOutputValue = setOutputValue
LoadBaseCoverageBreakdown = loadBaseCoverageBreakdown
CompressUncoveredLines = compressUncoveredLines
ReportUncoveredLines = reportUncoveredLines
)

type (
Expand Down
Loading
Loading