From 1639ae9baea574af3d291c7e7903a3abb47b610e Mon Sep 17 00:00:00 2001 From: vlado Date: Fri, 4 Jul 2025 12:32:45 +0200 Subject: [PATCH 01/10] initial effort --- pkg/testcoverage/check.go | 2 ++ pkg/testcoverage/config.go | 3 ++- pkg/testcoverage/config_test.go | 4 +++- pkg/testcoverage/helpers_test.go | 4 ++++ pkg/testcoverage/report.go | 28 +++++++++++++++++----------- pkg/testcoverage/types.go | 25 +++++++++++++++++++++++-- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/pkg/testcoverage/check.go b/pkg/testcoverage/check.go index 834cf103..85623b1c 100644 --- a/pkg/testcoverage/check.go +++ b/pkg/testcoverage/check.go @@ -105,6 +105,7 @@ func Analyze(cfg Config, current, base []coverage.Stats) AnalyzeResult { return AnalyzeResult{ Threshold: thr, + DiffThreshold: cfg.Diff.Threshold, HasFileOverrides: hasFileOverrides, HasPackageOverrides: hasPackageOverrides, FilesBelowThreshold: checkCoverageStatsBelowThreshold(current, thr.File, overrideRules), @@ -115,6 +116,7 @@ func Analyze(cfg Config, current, base []coverage.Stats) AnalyzeResult { TotalStats: coverage.StatsCalcTotal(current), HasBaseBreakdown: len(base) > 0, Diff: calculateStatsDiff(current, base), + DiffPercentage: TotalPercentageDiff(current, base), } } diff --git a/pkg/testcoverage/config.go b/pkg/testcoverage/config.go index fc245e0b..78ef7959 100644 --- a/pkg/testcoverage/config.go +++ b/pkg/testcoverage/config.go @@ -53,7 +53,8 @@ type Exclude struct { } type Diff struct { - BaseBreakdownFileName string `yaml:"base-breakdown-file-name"` + BaseBreakdownFileName string `yaml:"base-breakdown-file-name"` + Threshold *float64 `yaml:"threshold"` } type Badge struct { diff --git a/pkg/testcoverage/config_test.go b/pkg/testcoverage/config_test.go index d4f1bceb..dc215ec3 100644 --- a/pkg/testcoverage/config_test.go +++ b/pkg/testcoverage/config_test.go @@ -240,6 +240,7 @@ func nonZeroConfig() Config { BreakdownFileName: "breakdown.testcoverage", Diff: Diff{ BaseBreakdownFileName: "breakdown.testcoverage", + Threshold: ptr(-1.1), }, GithubActionOutput: true, } @@ -261,7 +262,8 @@ exclude: - path2 breakdown-file-name: 'breakdown.testcoverage' diff: - base-breakdown-file-name: 'breakdown.testcoverage' + base-breakdown-file-name: 'breakdown.testcoverage' + threshold: -1.1 github-action-output: true` } diff --git a/pkg/testcoverage/helpers_test.go b/pkg/testcoverage/helpers_test.go index 206682a1..5ac92060 100644 --- a/pkg/testcoverage/helpers_test.go +++ b/pkg/testcoverage/helpers_test.go @@ -14,6 +14,10 @@ import ( "github.com/vladopajic/go-test-coverage/v2/pkg/testcoverage/coverage" ) +func ptr[T any](t T) *T { + return &t +} + func mergeStats(a, b []coverage.Stats) []coverage.Stats { r := make([]coverage.Stats, 0, len(a)+len(b)) r = append(r, a...) diff --git a/pkg/testcoverage/report.go b/pkg/testcoverage/report.go index 4568e251..c1dfda9e 100644 --- a/pkg/testcoverage/report.go +++ b/pkg/testcoverage/report.go @@ -27,14 +27,6 @@ func reportCoverage(w io.Writer, result AnalyzeResult) { tabber := tabwriter.NewWriter(w, 1, 8, 2, '\t', 0) //nolint:mnd // relax defer tabber.Flush() - statusStr := func(passing bool) string { - if passing { - return "PASS" - } - - return "FAIL" - } - thr := result.Threshold if thr.File > 0 || result.HasFileOverrides { // File threshold report @@ -103,13 +95,19 @@ func reportDiff(w io.Writer, result AnalyzeResult) { tabber := tabwriter.NewWriter(w, 1, 8, 2, '\t', 0) //nolint:mnd // relax defer tabber.Flush() + if result.DiffThreshold != nil { + status := statusStr(result.MeetsDiffThreshold()) + fmt.Fprintf(tabber, "\nCoverage difference threshold (%.2f%%) satisfied:\t %s", *result.DiffThreshold, status) + fmt.Fprintf(tabber, "\nCoverage difference: %.2f%%\n", result.DiffPercentage) + } + if len(result.Diff) == 0 { - fmt.Fprintf(tabber, "\nCurrent tests coverage has not changed.\n") + fmt.Fprintf(tabber, "\nNo coverage changes in any files compared to the base.\n") return } - td := TotalLinesDiff(result.Diff) - fmt.Fprintf(tabber, "\nCurrent tests coverage has changed with %d lines missing coverage.", td) + td := TotalLinesMissingCoverage(result.Diff) + fmt.Fprintf(tabber, "\nTest coverage has changed in the current files, with %d lines missing coverage.", td) fmt.Fprintf(tabber, "\n file:\tuncovered:\tcurrent coverage:\tbase coverage:") for _, d := range result.Diff { @@ -242,3 +240,11 @@ func compressUncoveredLines(w io.Writer, ull []int) { printRange(last, ull[len(ull)-1]) } } + +func statusStr(passing bool) string { + if passing { + return "PASS" + } + + return "FAIL" +} diff --git a/pkg/testcoverage/types.go b/pkg/testcoverage/types.go index 5e2aa480..8f4a66e2 100644 --- a/pkg/testcoverage/types.go +++ b/pkg/testcoverage/types.go @@ -10,12 +10,14 @@ import ( type AnalyzeResult struct { Threshold Threshold + DiffThreshold *float64 FilesBelowThreshold []coverage.Stats PackagesBelowThreshold []coverage.Stats FilesWithUncoveredLines []coverage.Stats TotalStats coverage.Stats HasBaseBreakdown bool Diff []FileCoverageDiff + DiffPercentage float64 HasFileOverrides bool HasPackageOverrides bool } @@ -23,7 +25,16 @@ type AnalyzeResult struct { func (r *AnalyzeResult) Pass() bool { return r.MeetsTotalCoverage() && len(r.FilesBelowThreshold) == 0 && - len(r.PackagesBelowThreshold) == 0 + len(r.PackagesBelowThreshold) == 0 && + r.MeetsDiffThreshold() +} + +func (r *AnalyzeResult) MeetsDiffThreshold() bool { + if r.DiffThreshold == nil || !r.HasBaseBreakdown { + return true + } + + return *r.DiffThreshold <= r.DiffPercentage } func (r *AnalyzeResult) MeetsTotalCoverage() bool { @@ -109,7 +120,7 @@ func calculateStatsDiff(current, base []coverage.Stats) []FileCoverageDiff { return res } -func TotalLinesDiff(diff []FileCoverageDiff) int { +func TotalLinesMissingCoverage(diff []FileCoverageDiff) int { r := 0 for _, d := range diff { r += d.Current.UncoveredLinesCount() @@ -117,3 +128,13 @@ func TotalLinesDiff(diff []FileCoverageDiff) int { return r } + +func TotalPercentageDiff(current, base []coverage.Stats) float64 { + curretStats := coverage.StatsCalcTotal(current) + baseStats := coverage.StatsCalcTotal(base) + + cp := curretStats.CoveredPercentageF() + bp := baseStats.CoveredPercentageF() + + return cp - bp +} From f2813513ed83afa427a2eb4a6d9a9f1a379005e1 Mon Sep 17 00:00:00 2001 From: vlado Date: Fri, 4 Jul 2025 12:36:52 +0200 Subject: [PATCH 02/10] fix test --- pkg/testcoverage/report_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/testcoverage/report_test.go b/pkg/testcoverage/report_test.go index e24cabb8..c56919dc 100644 --- a/pkg/testcoverage/report_test.go +++ b/pkg/testcoverage/report_test.go @@ -94,7 +94,7 @@ func Test_ReportForHuman(t *testing.T) { result := Analyze(cfg, stats, stats) ReportForHuman(buf, result) - assert.Contains(t, buf.String(), "Current tests coverage has not changed") + assert.Contains(t, buf.String(), "No coverage changes in any files compared to the base") }) t.Run("diff - has change", func(t *testing.T) { @@ -114,7 +114,7 @@ func Test_ReportForHuman(t *testing.T) { ReportForHuman(buf, result) assert.Contains(t, buf.String(), - "Current tests coverage has changed with 2 lines missing coverage", + "Test coverage has changed in the current files, with 2 lines missing coverage", ) }) } From 36c168aca856f89f0dbd133a0580dfc4efc18522 Mon Sep 17 00:00:00 2001 From: vlado Date: Fri, 4 Jul 2025 12:41:03 +0200 Subject: [PATCH 03/10] lint fix --- pkg/testcoverage/report.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/testcoverage/report.go b/pkg/testcoverage/report.go index c1dfda9e..12eacb3b 100644 --- a/pkg/testcoverage/report.go +++ b/pkg/testcoverage/report.go @@ -87,6 +87,7 @@ func reportUncoveredLines(w io.Writer, result AnalyzeResult) { fmt.Fprintf(tabber, "\n") } +//nolint:lll // relax func reportDiff(w io.Writer, result AnalyzeResult) { if !result.HasBaseBreakdown { return From bbd1b29109669b8d433a03bce1b2527180ceb1a9 Mon Sep 17 00:00:00 2001 From: vlado Date: Fri, 4 Jul 2025 13:50:00 +0200 Subject: [PATCH 04/10] add empty test funcs --- pkg/testcoverage/check_test.go | 25 +++++++++++++++++++++++++ pkg/testcoverage/helpers_test.go | 15 +++++++++++++++ pkg/testcoverage/report_test.go | 28 +++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/pkg/testcoverage/check_test.go b/pkg/testcoverage/check_test.go index 1d39758c..db53825f 100644 --- a/pkg/testcoverage/check_test.go +++ b/pkg/testcoverage/check_test.go @@ -265,6 +265,16 @@ func TestCheck(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "failed to load base coverage breakdown") }) + + t.Run("valid profile - diff pass", func(t *testing.T) { + t.Parallel() + // add test + }) + + t.Run("valid profile - diff fail", func(t *testing.T) { + t.Parallel() + // add test + }) } //nolint:paralleltest // must not be parallel because it uses env @@ -450,6 +460,21 @@ func Test_Analyze(t *testing.T) { assert.False(t, result.Pass()) assertPrefix(t, result, prefix, true) }) + + t.Run("diff stats", func(t *testing.T) { + t.Parallel() + // add test + }) + + t.Run("diff below threshold", func(t *testing.T) { + t.Parallel() + // add test + }) + + t.Run("diff above threshold", func(t *testing.T) { + t.Parallel() + // add test + }) } func TestLoadBaseCoverageBreakdown(t *testing.T) { diff --git a/pkg/testcoverage/helpers_test.go b/pkg/testcoverage/helpers_test.go index 5ac92060..07564c07 100644 --- a/pkg/testcoverage/helpers_test.go +++ b/pkg/testcoverage/helpers_test.go @@ -3,6 +3,7 @@ package testcoverage_test import ( crand "crypto/rand" "encoding/hex" + "fmt" "math/rand" "os" "strings" @@ -193,6 +194,20 @@ func assertNoUncoveredLinesInfo(t *testing.T, content string) { assert.Empty(t, uncoveredReport) } +func assertDiffNoChange(t *testing.T, content string) { + t.Helper() + + assert.Contains(t, content, "No coverage changes in any files compared to the base") +} + +func assertDiffChange(t *testing.T, content string, lines int) { + t.Helper() + + //nolint:lll //relax + str := fmt.Sprintf("Test coverage has changed in the current files, with %d lines missing coverage", lines) + assert.Contains(t, content, str) +} + func assertGithubActionErrorsCount(t *testing.T, content string, count int) { t.Helper() diff --git a/pkg/testcoverage/report_test.go b/pkg/testcoverage/report_test.go index c56919dc..6ee4290f 100644 --- a/pkg/testcoverage/report_test.go +++ b/pkg/testcoverage/report_test.go @@ -25,6 +25,7 @@ func Test_ReportForHuman(t *testing.T) { buf := &bytes.Buffer{} ReportForHuman(buf, AnalyzeResult{Threshold: thr, TotalStats: coverage.Stats{}}) + assertHumanReport(t, buf.String(), 3, 0) assertNoUncoveredLinesInfo(t, buf.String()) }) @@ -34,6 +35,7 @@ func Test_ReportForHuman(t *testing.T) { buf := &bytes.Buffer{} ReportForHuman(buf, AnalyzeResult{Threshold: thr, TotalStats: coverage.Stats{Total: 1}}) + assertHumanReport(t, buf.String(), 2, 1) assertNoUncoveredLinesInfo(t, buf.String()) }) @@ -48,6 +50,7 @@ func Test_ReportForHuman(t *testing.T) { allStats := mergeStats(statsWithError, statsNoError) result := Analyze(cfg, allStats, nil) ReportForHuman(buf, result) + headReport, uncoveredReport := splitReport(t, buf.String()) assertHumanReport(t, headReport, 0, 1) assertContainStats(t, headReport, statsWithError) @@ -70,6 +73,7 @@ func Test_ReportForHuman(t *testing.T) { allStats := mergeStats(statsWithError, statsNoError) result := Analyze(cfg, allStats, nil) ReportForHuman(buf, result) + headReport, uncoveredReport := splitReport(t, buf.String()) assertHumanReport(t, headReport, 0, 1) assertContainStats(t, headReport, MakePackageStats(statsWithError)) @@ -83,6 +87,12 @@ func Test_ReportForHuman(t *testing.T) { coverage.StatsPluckName(coverage.StatsFilterWithCoveredLines(allStats)), ) }) +} + +func Test_ReportForHumanDiff(t *testing.T) { + t.Parallel() + + const prefix = "organization.org" t.Run("diff - no change", func(t *testing.T) { t.Parallel() @@ -94,7 +104,7 @@ func Test_ReportForHuman(t *testing.T) { result := Analyze(cfg, stats, stats) ReportForHuman(buf, result) - assert.Contains(t, buf.String(), "No coverage changes in any files compared to the base") + assertDiffNoChange(t, buf.String()) }) t.Run("diff - has change", func(t *testing.T) { @@ -113,16 +123,24 @@ func Test_ReportForHuman(t *testing.T) { result := Analyze(cfg, stats, base) ReportForHuman(buf, result) - assert.Contains(t, buf.String(), - "Test coverage has changed in the current files, with 2 lines missing coverage", - ) + assertDiffChange(t, buf.String(), 2) + }) + + t.Run("diff - threshold failed", func(t *testing.T) { + t.Parallel() + // add test + }) + + t.Run("diff - threshold pass", func(t *testing.T) { + t.Parallel() + // add test }) } func Test_ReportForGithubAction(t *testing.T) { t.Parallel() - prefix := "organization.org/pkg/" + const prefix = "organization.org/pkg/" t.Run("total coverage - pass", func(t *testing.T) { t.Parallel() From 1ed0dbaad9a1924b952b9f943bddc7d743fef71c Mon Sep 17 00:00:00 2001 From: vlado Date: Fri, 4 Jul 2025 16:04:25 +0200 Subject: [PATCH 05/10] update docs --- .testcoverage.example.yml | 30 +++++++++++++++++++++++++----- README.md | 28 ++++++++++++++++++++++++---- pkg/testcoverage/config.go | 2 +- pkg/testcoverage/config_test.go | 4 ++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/.testcoverage.example.yml b/.testcoverage.example.yml index da9937d3..32f82a35 100644 --- a/.testcoverage.example.yml +++ b/.testcoverage.example.yml @@ -41,11 +41,31 @@ exclude: - \.pb\.go$ # excludes all protobuf generated files - ^pkg/bar # exclude package `pkg/bar` -# File name of go-test-coverage breakdown file, which can be used to -# analyze coverage difference. +# If specified, saves the current test coverage breakdown to this file. +# +# Typically, this breakdown is generated only for main (base) branches and +# stored as an artifact. Later, this file can be used in feature branches +# to compare test coverage against the base branch. breakdown-file-name: '' diff: - # File name of go-test-coverage breakdown file which will be used to - # report coverage difference. - base-breakdown-file-name: '' \ No newline at end of file + # Path to the test coverage breakdown file from the base branch. + # + # This file is usually generated and stored in the main (base) branch, + # controled via `breakdown-file-name` property. + # When set in a feature branch, it allows the tool to compute and report + # the coverage difference between the current (feature) branch and the base. + base-breakdown-file-name: '' + + # Allowed threshold for the test coverage difference (in percentage) + # between the feature branch and the base branch. + # + # By default, this is disabled (set to nil). Valid values range from + # -100.0 to +100.0. + # + # Example: + # If set to 0.5, an error will be reported if the feature branch has + # less than 0.5% more coverage than the base. + # + # If set to -0.5, the check allows up to 0.5% less coverage than the base. + threshold: nil \ No newline at end of file diff --git a/README.md b/README.md index 0ba3b3ae..7ec4d917 100644 --- a/README.md +++ b/README.md @@ -125,14 +125,34 @@ exclude: - \.pb\.go$ # excludes all protobuf generated files - ^pkg/bar # exclude package `pkg/bar` -# File name of go-test-coverage breakdown file, which can be used to -# analyze coverage difference. +# If specified, saves the current test coverage breakdown to this file. +# +# Typically, this breakdown is generated only for main (base) branches and +# stored as an artifact. Later, this file can be used in feature branches +# to compare test coverage against the base branch. breakdown-file-name: '' diff: - # File name of go-test-coverage breakdown file which will be used to - # report coverage difference. + # Path to the test coverage breakdown file from the base branch. + # + # This file is usually generated and stored in the main (base) branch, + # controled via `breakdown-file-name` property. + # When set in a feature branch, it allows the tool to compute and report + # the coverage difference between the current (feature) branch and the base. base-breakdown-file-name: '' + + # Allowed threshold for the test coverage difference (in percentage) + # between the feature branch and the base branch. + # + # By default, this is disabled (set to nil). Valid values range from + # -100.0 to +100.0. + # + # Example: + # If set to 0.5, an error will be reported if the feature branch has + # less than 0.5% more coverage than the base. + # + # If set to -0.5, the check allows up to 0.5% less coverage than the base. + threshold: nil ``` ### Exclude Code from Coverage diff --git a/pkg/testcoverage/config.go b/pkg/testcoverage/config.go index 78ef7959..929b9c1e 100644 --- a/pkg/testcoverage/config.go +++ b/pkg/testcoverage/config.go @@ -54,7 +54,7 @@ type Exclude struct { type Diff struct { BaseBreakdownFileName string `yaml:"base-breakdown-file-name"` - Threshold *float64 `yaml:"threshold"` + Threshold *float64 `yaml:"threshold,omitempty"` } type Badge struct { diff --git a/pkg/testcoverage/config_test.go b/pkg/testcoverage/config_test.go index dc215ec3..3bf8f28e 100644 --- a/pkg/testcoverage/config_test.go +++ b/pkg/testcoverage/config_test.go @@ -240,7 +240,7 @@ func nonZeroConfig() Config { BreakdownFileName: "breakdown.testcoverage", Diff: Diff{ BaseBreakdownFileName: "breakdown.testcoverage", - Threshold: ptr(-1.1), + Threshold: ptr(-1.01), }, GithubActionOutput: true, } @@ -263,7 +263,7 @@ exclude: breakdown-file-name: 'breakdown.testcoverage' diff: base-breakdown-file-name: 'breakdown.testcoverage' - threshold: -1.1 + threshold: -1.01 github-action-output: true` } From 93245219ee97084d346dd43c00fae3ee89f3006a Mon Sep 17 00:00:00 2001 From: vlado Date: Wed, 9 Jul 2025 16:24:20 +0200 Subject: [PATCH 06/10] more test --- pkg/testcoverage/check_test.go | 109 ++++++++++++++++++++++++++++--- pkg/testcoverage/export_test.go | 1 + pkg/testcoverage/helpers_test.go | 27 ++++++++ 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/pkg/testcoverage/check_test.go b/pkg/testcoverage/check_test.go index db53825f..30c48e17 100644 --- a/pkg/testcoverage/check_test.go +++ b/pkg/testcoverage/check_test.go @@ -265,16 +265,109 @@ func TestCheck(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "failed to load base coverage breakdown") }) +} - t.Run("valid profile - diff pass", func(t *testing.T) { - t.Parallel() - // add test - }) +func TestCheckDiff(t *testing.T) { + t.Parallel() - t.Run("valid profile - diff fail", func(t *testing.T) { - t.Parallel() - // add test - }) + if testing.Short() { + return + } + + brakedownFile := t.TempDir() + "/breakdown.testcoverage" + brakedownCurrentFile := t.TempDir() + "/breakdown-current.testcoverage" + brakedownFileEdited := "breakdown-edit.testcoverage" + + // run check to generate brakedown file + cfg := Config{ + Profile: profileOK, + BreakdownFileName: brakedownFile, + SourceDir: sourceDir, + } + buf := &bytes.Buffer{} + pass, err := Check(buf, cfg) + assert.True(t, pass) + assert.NoError(t, err) + + // should pass since brakedown is the same + cfg = Config{ + Profile: profileOK, + SourceDir: sourceDir, + Diff: Diff{ + BaseBreakdownFileName: brakedownFile, + Threshold: ptr(0.0), + }, + } + buf = &bytes.Buffer{} + pass, err = Check(buf, cfg) + assert.True(t, pass) + assert.NoError(t, err) + assertDiffNoChange(t, buf.String()) + assertDiffPercentage(t, buf.String(), 0.0) + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, true) + + // should pass since diff is negative + cfg = Config{ + Profile: profileOK, + SourceDir: sourceDir, + Diff: Diff{ + BaseBreakdownFileName: brakedownFile, + Threshold: ptr(-0.001), + }, + } + buf = &bytes.Buffer{} + pass, err = Check(buf, cfg) + assert.True(t, pass) + assert.NoError(t, err) + assertDiffNoChange(t, buf.String()) + assertDiffPercentage(t, buf.String(), 0.0) + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, true) + + // should NOT pass since brakedown is the same, and diff is positive + cfg = Config{ + Profile: profileOK, + SourceDir: sourceDir, + Diff: Diff{ + BaseBreakdownFileName: brakedownFile, + Threshold: ptr(0.1), + }, + } + buf = &bytes.Buffer{} + pass, err = Check(buf, cfg) + assert.False(t, pass) + assert.NoError(t, err) + assertDiffNoChange(t, buf.String()) + assertDiffPercentage(t, buf.String(), 0.0) + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, false) + + // change brakedown file to have positive difference + base := readStats(t, brakedownFile) + base[0].Covered = 0 + base[1].Covered = 0 + + tmpFile, err := os.CreateTemp(t.TempDir(), brakedownFileEdited) + assert.NoError(t, err) + _, err = tmpFile.Write(coverage.StatsSerialize(base)) + assert.NoError(t, err) + + // check should now pass since difference has increased + cfg = Config{ + Profile: profileOK, + SourceDir: sourceDir, + BreakdownFileName: brakedownCurrentFile, + Diff: Diff{ + BaseBreakdownFileName: tmpFile.Name(), + Threshold: ptr(1.0), + }, + } + buf = &bytes.Buffer{} + pass, err = Check(buf, cfg) + assert.True(t, pass) + assert.NoError(t, err) + + diff := TotalPercentageDiff(readStats(t, brakedownCurrentFile), base) + assertDiffPercentage(t, buf.String(), diff) + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, true) } //nolint:paralleltest // must not be parallel because it uses env diff --git a/pkg/testcoverage/export_test.go b/pkg/testcoverage/export_test.go index 4765f280..a85a0b8d 100644 --- a/pkg/testcoverage/export_test.go +++ b/pkg/testcoverage/export_test.go @@ -17,6 +17,7 @@ var ( LoadBaseCoverageBreakdown = loadBaseCoverageBreakdown CompressUncoveredLines = compressUncoveredLines ReportUncoveredLines = reportUncoveredLines + StatusStr = statusStr ) type ( diff --git a/pkg/testcoverage/helpers_test.go b/pkg/testcoverage/helpers_test.go index 07564c07..ed091609 100644 --- a/pkg/testcoverage/helpers_test.go +++ b/pkg/testcoverage/helpers_test.go @@ -208,6 +208,21 @@ func assertDiffChange(t *testing.T, content string, lines int) { assert.Contains(t, content, str) } +func assertDiffThreshold(t *testing.T, content string, thr float64, isSatisfied bool) { + t.Helper() + + //nolint:lll //relax + str := fmt.Sprintf("Coverage difference threshold (%.2f%%) satisfied:\t %s", thr, StatusStr(isSatisfied)) + assert.Contains(t, content, str) +} + +func assertDiffPercentage(t *testing.T, content string, p float64) { + t.Helper() + + str := fmt.Sprintf("Coverage difference: %.2f%%", p) + assert.Contains(t, content, str) +} + func assertGithubActionErrorsCount(t *testing.T, content string, count int) { t.Helper() @@ -264,3 +279,15 @@ func assertGithubOutputValues(t *testing.T, file string) { assertNonEmptyValue(t, content, GaOutputBadgeText) assertNonEmptyValue(t, content, GaOutputReport) } + +func readStats(t *testing.T, file string) []coverage.Stats { + t.Helper() + + contentBytes, err := os.ReadFile(file) + assert.NoError(t, err) + assert.NotEmpty(t, contentBytes) + stats, err := coverage.StatsDeserialize(contentBytes) + assert.NoError(t, err) + + return stats +} From ebad849e9dda77a2a70d1591b08b3f4e7a31b392 Mon Sep 17 00:00:00 2001 From: vlado Date: Wed, 9 Jul 2025 22:30:56 +0200 Subject: [PATCH 07/10] more tests --- pkg/testcoverage/coverage/types.go | 14 +++++++++--- pkg/testcoverage/helpers_test.go | 4 ++++ pkg/testcoverage/report_test.go | 36 ++++++++++++++++++++++++++---- pkg/testcoverage/types.go | 4 ++-- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/pkg/testcoverage/coverage/types.go b/pkg/testcoverage/coverage/types.go index 185e1d9e..80aaef7b 100644 --- a/pkg/testcoverage/coverage/types.go +++ b/pkg/testcoverage/coverage/types.go @@ -27,7 +27,11 @@ func (s Stats) CoveredPercentage() int { } func (s Stats) CoveredPercentageF() float64 { - return coveredPercentageF(s.Total, s.Covered) + return coveredPercentageF(s.Total, s.Covered, true) +} + +func (s Stats) CoveredPercentageFNR() float64 { + return coveredPercentageF(s.Total, s.Covered, false) } //nolint:mnd // relax @@ -53,11 +57,11 @@ func StatsSearchMap(stats []Stats) map[string]Stats { } func CoveredPercentage(total, covered int64) int { - return int(coveredPercentageF(total, covered)) + return int(coveredPercentageF(total, covered, true)) } //nolint:mnd // relax -func coveredPercentageF(total, covered int64) float64 { +func coveredPercentageF(total, covered int64, round bool) float64 { if total == 0 { return 0 } @@ -68,6 +72,10 @@ func coveredPercentageF(total, covered int64) float64 { p := float64(covered*100) / float64(total) + if !round { + return p + } + // round to %.1f return float64(int(math.Round(p*10))) / 10 } diff --git a/pkg/testcoverage/helpers_test.go b/pkg/testcoverage/helpers_test.go index ed091609..c4c18b90 100644 --- a/pkg/testcoverage/helpers_test.go +++ b/pkg/testcoverage/helpers_test.go @@ -27,6 +27,10 @@ func mergeStats(a, b []coverage.Stats) []coverage.Stats { return r } +func copyStats(s []coverage.Stats) []coverage.Stats { + return mergeStats(make([]coverage.Stats, 0), s) +} + func randStats(localPrefix string, minc, maxc int) []coverage.Stats { const count = 100 diff --git a/pkg/testcoverage/report_test.go b/pkg/testcoverage/report_test.go index 6ee4290f..a0c3fefd 100644 --- a/pkg/testcoverage/report_test.go +++ b/pkg/testcoverage/report_test.go @@ -111,7 +111,7 @@ func Test_ReportForHumanDiff(t *testing.T) { t.Parallel() stats := randStats(prefix, 10, 100) - base := mergeStats(make([]coverage.Stats, 0), stats) + base := copyStats(stats) stats = append(stats, coverage.Stats{Name: "foo", Total: 9, Covered: 8}) stats = append(stats, coverage.Stats{Name: "foo-new", Total: 9, Covered: 8}) @@ -124,16 +124,44 @@ func Test_ReportForHumanDiff(t *testing.T) { ReportForHuman(buf, result) assertDiffChange(t, buf.String(), 2) + assert.Contains(t, buf.String(), "foo\t\t 1\t\t88.9% (8/9)\t\t100% (10/10)") + assert.Contains(t, buf.String(), "foo-new\t 1\t\t88.9% (8/9)\t") }) - t.Run("diff - threshold failed", func(t *testing.T) { + t.Run("diff - threshold failed", func(t *testing.T) { //nolit:dupl // relax t.Parallel() - // add test + + base := []coverage.Stats{{Name: "foo", Total: 10, Covered: 1}} + stats := []coverage.Stats{{Name: "foo", Total: 10, Covered: 8}} + + buf := &bytes.Buffer{} + cfg := Config{ + Diff: Diff{Threshold: ptr(999.0)}, + } + result := Analyze(cfg, stats, base) + ReportForHuman(buf, result) + + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, false) + assertDiffPercentage(t, buf.String(), 70) + assertDiffChange(t, buf.String(), 2) }) t.Run("diff - threshold pass", func(t *testing.T) { t.Parallel() - // add test + + base := []coverage.Stats{{Name: "foo", Total: 10, Covered: 1}} + stats := []coverage.Stats{{Name: "foo", Total: 10, Covered: 8}} + + buf := &bytes.Buffer{} + cfg := Config{ + Diff: Diff{Threshold: ptr(70.0)}, + } + result := Analyze(cfg, stats, base) + ReportForHuman(buf, result) + + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, true) + assertDiffPercentage(t, buf.String(), 70) + assertDiffChange(t, buf.String(), 2) }) } diff --git a/pkg/testcoverage/types.go b/pkg/testcoverage/types.go index 8f4a66e2..72471750 100644 --- a/pkg/testcoverage/types.go +++ b/pkg/testcoverage/types.go @@ -133,8 +133,8 @@ func TotalPercentageDiff(current, base []coverage.Stats) float64 { curretStats := coverage.StatsCalcTotal(current) baseStats := coverage.StatsCalcTotal(base) - cp := curretStats.CoveredPercentageF() - bp := baseStats.CoveredPercentageF() + cp := curretStats.CoveredPercentageFNR() + bp := baseStats.CoveredPercentageFNR() return cp - bp } From 07aa88bb65b2f26271b21cc4d0360658dbcb4fb2 Mon Sep 17 00:00:00 2001 From: vlado Date: Wed, 9 Jul 2025 23:00:18 +0200 Subject: [PATCH 08/10] more effort --- pkg/testcoverage/check_test.go | 51 +++++++++++++++++++++++++++++++-- pkg/testcoverage/report_test.go | 18 ++++++++++++ pkg/testcoverage/types.go | 6 +++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/pkg/testcoverage/check_test.go b/pkg/testcoverage/check_test.go index 30c48e17..a3691c39 100644 --- a/pkg/testcoverage/check_test.go +++ b/pkg/testcoverage/check_test.go @@ -556,17 +556,62 @@ func Test_Analyze(t *testing.T) { t.Run("diff stats", func(t *testing.T) { t.Parallel() - // add test + + stats := randStats(prefix, 10, 100) + + cfg := Config{} + result := Analyze(cfg, stats, stats) + assert.Empty(t, result.Diff) + assert.True(t, result.Pass()) + assert.Equal(t, 0.0, result.DiffPercentage) }) t.Run("diff below threshold", func(t *testing.T) { t.Parallel() - // add test + + base := []coverage.Stats{{Name: "foo", Total: 10, Covered: 1}} + stats := []coverage.Stats{{Name: "foo", Total: 10, Covered: 8}} + + cfg := Config{ + Diff: Diff{Threshold: ptr(999.0)}, + } + result := Analyze(cfg, stats, base) + assert.NotEmpty(t, result.Diff) + assert.False(t, result.Pass()) + assert.False(t, result.MeetsDiffThreshold()) + assert.Equal(t, 70.0, result.DiffPercentage) }) t.Run("diff above threshold", func(t *testing.T) { t.Parallel() - // add test + + base := []coverage.Stats{{Name: "foo", Total: 10, Covered: 1}} + stats := []coverage.Stats{{Name: "foo", Total: 10, Covered: 8}} + + cfg := Config{ + Diff: Diff{Threshold: ptr(1.0)}, + } + result := Analyze(cfg, stats, base) + assert.NotEmpty(t, result.Diff) + assert.True(t, result.Pass()) + assert.True(t, result.MeetsDiffThreshold()) + assert.Equal(t, 70.0, result.DiffPercentage) + }) + + t.Run("diff above threshold (small diff)", func(t *testing.T) { + t.Parallel() + + base := []coverage.Stats{{Name: "foo", Total: 10000, Covered: 9999}} + stats := []coverage.Stats{{Name: "foo", Total: 10000, Covered: 10000}} + + cfg := Config{ + Diff: Diff{Threshold: ptr(0.0)}, + } + result := Analyze(cfg, stats, base) + assert.NotEmpty(t, result.Diff) + assert.True(t, result.Pass()) + assert.True(t, result.MeetsDiffThreshold()) + assert.Equal(t, 0.01, result.DiffPercentage) }) } diff --git a/pkg/testcoverage/report_test.go b/pkg/testcoverage/report_test.go index a0c3fefd..64d17fde 100644 --- a/pkg/testcoverage/report_test.go +++ b/pkg/testcoverage/report_test.go @@ -163,6 +163,24 @@ func Test_ReportForHumanDiff(t *testing.T) { assertDiffPercentage(t, buf.String(), 70) assertDiffChange(t, buf.String(), 2) }) + + t.Run("diff - negative threshold pass", func(t *testing.T) { + t.Parallel() + + base := []coverage.Stats{{Name: "foo", Total: 100, Covered: 100}} + stats := []coverage.Stats{{Name: "foo", Total: 100, Covered: 90}} + + buf := &bytes.Buffer{} + cfg := Config{ + Diff: Diff{Threshold: ptr(-11.0)}, + } + result := Analyze(cfg, stats, base) + ReportForHuman(buf, result) + + assertDiffThreshold(t, buf.String(), *cfg.Diff.Threshold, true) + assertDiffPercentage(t, buf.String(), -10) + assertDiffChange(t, buf.String(), 10) + }) } func Test_ReportForGithubAction(t *testing.T) { diff --git a/pkg/testcoverage/types.go b/pkg/testcoverage/types.go index 72471750..7fafeb8a 100644 --- a/pkg/testcoverage/types.go +++ b/pkg/testcoverage/types.go @@ -2,6 +2,7 @@ package testcoverage import ( "maps" + "math" "slices" "strings" @@ -136,5 +137,8 @@ func TotalPercentageDiff(current, base []coverage.Stats) float64 { cp := curretStats.CoveredPercentageFNR() bp := baseStats.CoveredPercentageFNR() - return cp - bp + p := cp - bp + + // round to %.2f + return float64(int(math.Round(p*100))) / 100 } From 33315c3df589d1f028abd6e045fd9db6b54b1737 Mon Sep 17 00:00:00 2001 From: vlado Date: Wed, 9 Jul 2025 23:01:58 +0200 Subject: [PATCH 09/10] fix test --- pkg/testcoverage/check_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/testcoverage/check_test.go b/pkg/testcoverage/check_test.go index a3691c39..0a8f3f6a 100644 --- a/pkg/testcoverage/check_test.go +++ b/pkg/testcoverage/check_test.go @@ -608,7 +608,6 @@ func Test_Analyze(t *testing.T) { Diff: Diff{Threshold: ptr(0.0)}, } result := Analyze(cfg, stats, base) - assert.NotEmpty(t, result.Diff) assert.True(t, result.Pass()) assert.True(t, result.MeetsDiffThreshold()) assert.Equal(t, 0.01, result.DiffPercentage) From 1239770cee4ff7ac7df79de090c698050390b8a7 Mon Sep 17 00:00:00 2001 From: vlado Date: Wed, 9 Jul 2025 23:08:28 +0200 Subject: [PATCH 10/10] lint fix --- pkg/testcoverage/check_test.go | 8 ++++---- pkg/testcoverage/report_test.go | 3 ++- pkg/testcoverage/types.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/testcoverage/check_test.go b/pkg/testcoverage/check_test.go index 0a8f3f6a..e610441e 100644 --- a/pkg/testcoverage/check_test.go +++ b/pkg/testcoverage/check_test.go @@ -563,7 +563,7 @@ func Test_Analyze(t *testing.T) { result := Analyze(cfg, stats, stats) assert.Empty(t, result.Diff) assert.True(t, result.Pass()) - assert.Equal(t, 0.0, result.DiffPercentage) + assert.Equal(t, 0.0, result.DiffPercentage) //nolint:testifylint //relax }) t.Run("diff below threshold", func(t *testing.T) { @@ -579,7 +579,7 @@ func Test_Analyze(t *testing.T) { assert.NotEmpty(t, result.Diff) assert.False(t, result.Pass()) assert.False(t, result.MeetsDiffThreshold()) - assert.Equal(t, 70.0, result.DiffPercentage) + assert.Equal(t, 70.0, result.DiffPercentage) //nolint:testifylint //relax }) t.Run("diff above threshold", func(t *testing.T) { @@ -595,7 +595,7 @@ func Test_Analyze(t *testing.T) { assert.NotEmpty(t, result.Diff) assert.True(t, result.Pass()) assert.True(t, result.MeetsDiffThreshold()) - assert.Equal(t, 70.0, result.DiffPercentage) + assert.Equal(t, 70.0, result.DiffPercentage) //nolint:testifylint //relax }) t.Run("diff above threshold (small diff)", func(t *testing.T) { @@ -610,7 +610,7 @@ func Test_Analyze(t *testing.T) { result := Analyze(cfg, stats, base) assert.True(t, result.Pass()) assert.True(t, result.MeetsDiffThreshold()) - assert.Equal(t, 0.01, result.DiffPercentage) + assert.Equal(t, 0.01, result.DiffPercentage) //nolint:testifylint //relax }) } diff --git a/pkg/testcoverage/report_test.go b/pkg/testcoverage/report_test.go index 64d17fde..06921a54 100644 --- a/pkg/testcoverage/report_test.go +++ b/pkg/testcoverage/report_test.go @@ -89,6 +89,7 @@ func Test_ReportForHuman(t *testing.T) { }) } +//nolint:dupl // relax func Test_ReportForHumanDiff(t *testing.T) { t.Parallel() @@ -128,7 +129,7 @@ func Test_ReportForHumanDiff(t *testing.T) { assert.Contains(t, buf.String(), "foo-new\t 1\t\t88.9% (8/9)\t") }) - t.Run("diff - threshold failed", func(t *testing.T) { //nolit:dupl // relax + t.Run("diff - threshold failed", func(t *testing.T) { t.Parallel() base := []coverage.Stats{{Name: "foo", Total: 10, Covered: 1}} diff --git a/pkg/testcoverage/types.go b/pkg/testcoverage/types.go index 7fafeb8a..2ff9b770 100644 --- a/pkg/testcoverage/types.go +++ b/pkg/testcoverage/types.go @@ -140,5 +140,5 @@ func TotalPercentageDiff(current, base []coverage.Stats) float64 { p := cp - bp // round to %.2f - return float64(int(math.Round(p*100))) / 100 + return float64(int(math.Round(p*100))) / 100 //nolint:mnd //relax }