diff --git a/cmd/internal/flags/flags_test.go b/cmd/internal/flags/flags_test.go index 97ebee8a..78e7feca 100644 --- a/cmd/internal/flags/flags_test.go +++ b/cmd/internal/flags/flags_test.go @@ -121,6 +121,7 @@ func TestSet(t *testing.T) { cmd := &cobra.Command{} + // #nosec G601 - We are in tests, we don't care err := Set(cmd, &tc.flag) if (tc.expectError && err == nil) || (!tc.expectError && err != nil) { t.Fatal("error not expected") @@ -132,6 +133,7 @@ func TestSet(t *testing.T) { } tc.flag.Name += "_persistent" + // #nosec G601 - We are in tests, we don't care err = SetPersistent(cmd, &tc.flag) if (tc.expectError && err == nil) || (!tc.expectError && err != nil) { t.Fatal("error not expected") diff --git a/cmd/unleash.go b/cmd/unleash.go index 4c195c39..c438c63e 100644 --- a/cmd/unleash.go +++ b/cmd/unleash.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/pflag" "github.com/go-gremlins/gremlins/internal/coverage" + "github.com/go-gremlins/gremlins/internal/diff" "github.com/go-gremlins/gremlins/internal/engine" "github.com/go-gremlins/gremlins/internal/engine/workdir" "github.com/go-gremlins/gremlins/internal/log" @@ -46,6 +47,7 @@ type unleashCmd struct { const ( commandName = "unleash" + paramDiff = "diff" paramBuildTags = "tags" paramCoverPackages = "coverpkg" paramDryRun = "dry-run" @@ -154,6 +156,11 @@ func cleanUp(wd string) { } func run(ctx context.Context, mod gomodule.GoModule, workDir string) (report.Results, error) { + fDiff, err := diff.New() + if err != nil { + return report.Results{}, err + } + c := coverage.New(workDir, mod) cProfile, err := c.Run() @@ -166,7 +173,12 @@ func run(ctx context.Context, mod gomodule.GoModule, workDir string) (report.Res jDealer := engine.NewExecutorDealer(mod, wdDealer, cProfile.Elapsed) - mut := engine.New(mod, cProfile, jDealer) + codeData := engine.CodeData{ + Cov: cProfile.Profile, + Diff: fDiff, + } + + mut := engine.New(mod, codeData, jDealer) results := mut.Run(ctx) return results, nil @@ -188,6 +200,7 @@ func setFlagsOnCmd(cmd *cobra.Command) error { {Name: paramDryRun, CfgKey: configuration.UnleashDryRunKey, Shorthand: "d", DefaultV: false, Usage: "find mutations but do not executes tests"}, {Name: paramBuildTags, CfgKey: configuration.UnleashTagsKey, Shorthand: "t", DefaultV: "", Usage: "a comma-separated list of build tags"}, {Name: paramCoverPackages, CfgKey: configuration.UnleashCoverPkgKey, DefaultV: "", Usage: "a comma-separated list of package patterns"}, + {Name: paramDiff, CfgKey: configuration.UnleashDiffRef, Shorthand: "D", DefaultV: "", Usage: "diff branch or commit"}, {Name: paramOutput, CfgKey: configuration.UnleashOutputKey, Shorthand: "o", DefaultV: "", Usage: "set the output file for machine readable results"}, {Name: paramIntegrationMode, CfgKey: configuration.UnleashIntegrationMode, Shorthand: "i", DefaultV: false, Usage: "makes Gremlins run the complete test suite for each mutation"}, {Name: paramThresholdEfficacy, CfgKey: configuration.UnleashThresholdEfficacyKey, DefaultV: float64(0), Usage: "threshold for code-efficacy percent"}, diff --git a/cmd/unleash_test.go b/cmd/unleash_test.go index 5bf7bade..0e7b6376 100644 --- a/cmd/unleash_test.go +++ b/cmd/unleash_test.go @@ -65,6 +65,12 @@ func TestUnleash(t *testing.T) { flagType: "string", defValue: "", }, + { + name: "diff", + shorthand: "D", + flagType: "string", + defValue: "", + }, { name: "dry-run", shorthand: "d", diff --git a/docs/docs/index.md b/docs/docs/index.md index b60614b2..4af87e26 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -33,6 +33,7 @@ catch their damage? - Discovers mutant candidates and tests them - Only tests mutants covered by tests +- Can test mutants only in PR changes - Supports five mutant types - Yaml-based configuration - Can run as quality gate on CI diff --git a/docs/docs/usage/ci/github-action.md b/docs/docs/usage/ci/github-action.md index e2f8854a..a5478404 100644 --- a/docs/docs/usage/ci/github-action.md +++ b/docs/docs/usage/ci/github-action.md @@ -20,7 +20,7 @@ jobs: gremlins: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 - - uses: actions/gremlins-action@v1 + - uses: go-gremlins/gremlins-action@v1 with: version: latest args: --tags="tag1,tag2" diff --git a/docs/docs/usage/commands/unleash/index.md b/docs/docs/usage/commands/unleash/index.md index db43c9c5..f0cd1438 100644 --- a/docs/docs/usage/commands/unleash/index.md +++ b/docs/docs/usage/commands/unleash/index.md @@ -60,6 +60,35 @@ The default is for each test to analyze only the package being tested. gremlins unleash --coverpkg "./internal/...,./pkg/..." ``` +### Diff + +:material-flag: `--diff` · :material-sign-direction: Default: empty + +Run tests only for mutants inside code changes between current state and git reference (branch or commit). +The default is each mutant covered by tests. + +#### Branch merge base + +```shell +gremlins unleash --diff "origin/main" +``` + +#### Commit + +```shell +gremlins unleash --diff "b62af323" +``` + +#### PR + +```shell +gremlins unleash --diff "origin/$GITHUB_BASE_REF" +``` + +Use `actions/checkout@v4` with `fetch-depth: 0` to fetch all history. + +#### Using + ### Dry run :material-flag:`--dry-run`/`-d` · :material-sign-direction: Default: false diff --git a/go.mod b/go.mod index e73e27a1..a7d76d92 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/MakeNowJust/heredoc v1.0.0 + github.com/bluekeyes/go-gitdiff v0.7.1 github.com/fatih/color v1.14.1 github.com/google/go-cmp v0.5.9 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b diff --git a/go.sum b/go.sum index d91fa3f8..2239fda5 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ= +github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 54051bca..b92c4258 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -41,6 +41,7 @@ const ( UnleashTestCPUKey = "unleash.test-cpu" UnleashTimeoutCoefficientKey = "unleash.timeout-coefficient" UnleashIntegrationMode = "unleash.integration" + UnleashDiffRef = "unleash.diff" UnleashThresholdEfficacyKey = "unleash.threshold.efficacy" UnleashThresholdMCoverageKey = "unleash.threshold.mutant-coverage" ) diff --git a/internal/diff/diff.go b/internal/diff/diff.go new file mode 100644 index 00000000..827118f4 --- /dev/null +++ b/internal/diff/diff.go @@ -0,0 +1,63 @@ +package diff + +import ( + "go/token" + + "github.com/bluekeyes/go-gitdiff/gitdiff" +) + +type FileName string + +type Change struct { + StartLine int + EndLine int +} + +type Diff map[FileName][]Change + +func newDiff(files []*gitdiff.File) Diff { + result := map[FileName][]Change{} + + for _, file := range files { + name, changes := newChanges(file) + + result[name] = changes + } + + return result +} + +func newChanges(file *gitdiff.File) (FileName, []Change) { + var changes []Change + + for _, fragment := range file.TextFragments { + if fragment.LinesAdded == 0 { + continue + } + + startLine := int(fragment.NewPosition + fragment.LeadingContext) + + changes = append(changes, Change{ + StartLine: startLine, + EndLine: startLine + int(fragment.LinesAdded-1), + }) + } + + return FileName(file.NewName), changes +} + +func (d Diff) IsChanged(pos token.Position) bool { + if len(d) == 0 { + return true + } + + fileDiff := d[FileName(pos.Filename)] + + for _, change := range fileDiff { + if pos.Line >= change.StartLine && pos.Line <= change.EndLine { + return true + } + } + + return false +} diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go new file mode 100644 index 00000000..c986a669 --- /dev/null +++ b/internal/diff/diff_test.go @@ -0,0 +1,162 @@ +package diff + +import ( + "go/token" + "reflect" + "testing" + + "github.com/bluekeyes/go-gitdiff/gitdiff" +) + +func TestDiff_IsChanged(t *testing.T) { + tests := []struct { + name string + d Diff + pos token.Position + want bool + }{ + { + name: "must be changed on nil Diff", + d: nil, + pos: token.Position{}, + want: true, + }, + { + name: "must be changed on empty Diff", + d: map[FileName][]Change{}, + pos: token.Position{}, + want: true, + }, + { + name: "must be changed if in range", + d: map[FileName][]Change{ + "test": {{StartLine: 21, EndLine: 21}}, + }, + pos: token.Position{Filename: "test", Line: 21}, + want: true, + }, + { + name: "must be unchanged if outside range", + d: map[FileName][]Change{ + "test": {{StartLine: 21, EndLine: 21}}, + }, + pos: token.Position{Filename: "test", Line: 22}, + want: false, + }, + { + name: "must be unchanged if no such file", + d: map[FileName][]Change{ + "test": {{StartLine: 21, EndLine: 21}}, + }, + pos: token.Position{Filename: "test1", Line: 21}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.d.IsChanged(tt.pos) + if got != tt.want { + t.Errorf("IsChanged() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newDiff(t *testing.T) { + fragments := []*gitdiff.TextFragment{fragment(21, 1)} + + files := []*gitdiff.File{ + { + NewName: "test1", + TextFragments: fragments, + }, + { + NewName: "test2", + TextFragments: fragments, + }, + } + + expected := Diff{ + "test1": {{StartLine: 25, EndLine: 25}}, + "test2": {{StartLine: 25, EndLine: 25}}, + } + + result := newDiff(files) + if !reflect.DeepEqual(result, expected) { + t.Log("want", expected) + t.Log("got", result) + t.Fatalf("unexpected newDiff result") + } +} + +func Test_newChanges(t *testing.T) { + fragments := []*gitdiff.TextFragment{ + fragment(0, 1), + fragment(10, 0), + fragment(21, 2), + fragment(44, 4), + fragment(231, 201), + } + file := &gitdiff.File{ + NewName: "test", + TextFragments: fragments, + } + + expect := []Change{ + {StartLine: 4, EndLine: 4}, + {StartLine: 25, EndLine: 26}, + {StartLine: 48, EndLine: 51}, + {StartLine: 235, EndLine: 435}, + } + + name, changes := newChanges(file) + + if name != "test" { + t.Fatalf("name %s unexpected", name) + } + if !reflect.DeepEqual(changes, expect) { + t.Log("want", expect) + t.Log("got", changes) + t.Fatalf("unexpected newChanges result") + } +} + +func fragment(startLine int, adds int, del ...int) *gitdiff.TextFragment { + const contexts = 4 + + dels := adds + if len(del) > 0 { + dels = del[0] + } + + var lines []gitdiff.Line + + lines = append(lines, opLines(gitdiff.OpContext, contexts)...) + lines = append(lines, opLines(gitdiff.OpDelete, dels)...) + lines = append(lines, opLines(gitdiff.OpAdd, adds)...) + lines = append(lines, opLines(gitdiff.OpContext, contexts)...) + + line := int64(startLine) + added := int64(adds) + deleted := int64(dels) + + return &gitdiff.TextFragment{ + OldLines: line - 1, + NewPosition: line, + LinesAdded: added, + LinesDeleted: deleted, + LeadingContext: contexts, + TrailingContext: contexts, + Lines: lines, + } +} + +func opLines(op gitdiff.LineOp, count int) []gitdiff.Line { + result := make([]gitdiff.Line, count) + + for i := 0; i < count; i++ { + result[i] = gitdiff.Line{Op: op, Line: "test"} + } + + return result +} diff --git a/internal/diff/parse.go b/internal/diff/parse.go new file mode 100644 index 00000000..7f0772b2 --- /dev/null +++ b/internal/diff/parse.go @@ -0,0 +1,43 @@ +package diff + +import ( + "bytes" + "fmt" + "os/exec" + + "github.com/bluekeyes/go-gitdiff/gitdiff" + + "github.com/go-gremlins/gremlins/internal/configuration" + "github.com/go-gremlins/gremlins/internal/log" +) + +func New() (Diff, error) { + return NewWithCmd(exec.Command) +} + +type execCmd interface { + CombinedOutput() ([]byte, error) +} + +func NewWithCmd[T execCmd](cmdContext func(name string, args ...string) T) (Diff, error) { + diffRef := configuration.Get[string](configuration.UnleashDiffRef) + if diffRef == "" { + return nil, nil + } + + log.Infoln("Gathering files diff...") + + cmd := cmdContext("git", "diff", "--merge-base", diffRef) + + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("an error occured while calling git diff: %w\n\n%s", err, out) + } + + files, _, err := gitdiff.Parse(bytes.NewReader(out)) + if err != nil { + return nil, fmt.Errorf("an error occured while parsing diff: %w", err) + } + + return newDiff(files), nil +} diff --git a/internal/diff/parse_test.go b/internal/diff/parse_test.go new file mode 100644 index 00000000..90a5ce51 --- /dev/null +++ b/internal/diff/parse_test.go @@ -0,0 +1,129 @@ +package diff + +import ( + "errors" + "reflect" + "testing" + + "github.com/spf13/viper" + + "github.com/go-gremlins/gremlins/internal/configuration" +) + +func TestNewWithCmd(t *testing.T) { + t.Run("must return nil on empty flag", func(t *testing.T) { + m := &mock{} + + d, err := NewWithCmd(m.call) + + if d != nil && err != nil { + t.Fatal("incorrect result") + } + }) + + t.Run("must return error", func(t *testing.T) { + viper.Set(configuration.UnleashDiffRef, "test") + + m := &mock{ + outputErr: errors.New("test"), + } + + _, err := NewWithCmd(m.call) + if err == nil { + t.Error("must return error") + } + + if m.calls != 1 { + t.Fatal("cmd not called") + } + + expectedArgs := []string{"diff", "--merge-base", "test"} + + if m.callName != "git" || !reflect.DeepEqual(m.callArgs, expectedArgs) { + t.Log("name", m.callName) + t.Log("args", m.callArgs) + t.Error("cmd not called properly") + } + }) + + t.Run("must return diff error", func(t *testing.T) { + viper.Set(configuration.UnleashDiffRef, "test") + + m := &mock{ + output: []byte(testErrDiff), + } + + _, err := NewWithCmd(m.call) + if err == nil { + t.Error("must return error") + } + }) + + t.Run("must return changes", func(t *testing.T) { + viper.Set(configuration.UnleashDiffRef, "test") + + m := &mock{ + output: []byte(testDiff), + } + + expected := Diff{ + "test/test": {{StartLine: 44, EndLine: 44}}, + } + + result, err := NewWithCmd(m.call) + + if err != nil || !reflect.DeepEqual(result, expected) { + t.Log("err", err) + t.Log("result", result) + t.Error("unexpected result") + } + }) +} + +type mock struct { + calls int + callName string + callArgs []string + output []byte + outputErr error +} + +func (m *mock) call(name string, args ...string) execCmd { + m.calls++ + m.callName = name + m.callArgs = args + + return m +} + +func (m *mock) CombinedOutput() ([]byte, error) { + return m.output, m.outputErr +} + +const ( + testDiff = ` +diff --git a/test/test b/test/test +index 54051bc..b92c425 100644 +--- a/test/test ++++ b/test/test +@@ -41,6 +41,7 @@ const ( + test = "test" + test = "test" + test = "test" ++ test = "test" + test = "test" + test = "test" + ) +` + testErrDiff = ` +diff --git a/test/test b/test/test +index 54051bc..b92c425 100644 +--- a/test/test ++++ b/test/test +@@ -41,7 +41,7 @@ const ( + test = "test" ++ test = "test" + test = "test" + ) +` +) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 1374e163..3856e8de 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -30,6 +30,7 @@ import ( "time" "github.com/go-gremlins/gremlins/internal/coverage" + "github.com/go-gremlins/gremlins/internal/diff" "github.com/go-gremlins/gremlins/internal/engine/workerpool" "github.com/go-gremlins/gremlins/internal/mutator" "github.com/go-gremlins/gremlins/internal/report" @@ -45,25 +46,31 @@ import ( type Engine struct { fs fs.FS jDealer ExecutorDealer - covProfile coverage.Profile + codeData CodeData mutantStream chan mutator.Mutator module gomodule.GoModule } +// CodeData is used to check if the mutant should be executed. +type CodeData struct { + Cov coverage.Profile + Diff diff.Diff +} + // Option for the Engine initialization. type Option func(m Engine) Engine -// New instantiates a Engine. +// New instantiates an Engine. // -// It gets a fs.FS on which to perform the analysis, a coverage.Profile to -// check if the mutants are covered and a sets of Option. -func New(mod gomodule.GoModule, r coverage.Result, jDealer ExecutorDealer, opts ...Option) Engine { +// It gets a fs.FS on which to perform the analysis, a CodeData to +// check if the mutants are executable and a sets of Option. +func New(mod gomodule.GoModule, codeData CodeData, jDealer ExecutorDealer, opts ...Option) Engine { dirFS := os.DirFS(filepath.Join(mod.Root, mod.CallingDir)) mut := Engine{ - module: mod, - jDealer: jDealer, - covProfile: r.Profile, - fs: dirFS, + module: mod, + jDealer: jDealer, + codeData: codeData, + fs: dirFS, } for _, opt := range opts { mut = opt(mut) @@ -173,10 +180,15 @@ func normalisePkgPath(pkg string) string { func (mu *Engine) mutationStatus(pos token.Position) mutator.Status { var status mutator.Status - if mu.covProfile.IsCovered(pos) { + + if mu.codeData.Cov.IsCovered(pos) { status = mutator.Runnable } + if !mu.codeData.Diff.IsChanged(pos) { + status = mutator.Skipped + } + return status } diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 2959c2ee..57657dbf 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -36,6 +36,8 @@ const ( expectedModule = "example.com" ) +var testCodeData = engine.CodeData{Cov: coveredPosition(defaultFixture).Profile} + func coveredPosition(fixture string) coverage.Result { fn := filenameFromFixture(fixture) p := coverage.Profile{fn: {{StartLine: 6, EndLine: 7, StartCol: 8, EndCol: 9}}} @@ -500,7 +502,7 @@ func TestMutations(t *testing.T) { mapFS, mod, c := loadFixture(tc.fixture, ".") defer c() - mut := engine.New(mod, tc.covResult, newJobDealerStub(t), engine.WithDirFs(mapFS)) + mut := engine.New(mod, engine.CodeData{Cov: tc.covResult.Profile}, newJobDealerStub(t), engine.WithDirFs(mapFS)) res := mut.Run(context.Background()) got := res.Mutants @@ -544,7 +546,7 @@ func TestMutantSkipDisabled(t *testing.T) { }) defer viperReset() - mut := engine.New(mod, coveredPosition(defaultFixture), newJobDealerStub(t), engine.WithDirFs(mapFS)) + mut := engine.New(mod, testCodeData, newJobDealerStub(t), engine.WithDirFs(mapFS)) res := mut.Run(context.Background()) got := res.Mutants @@ -573,7 +575,7 @@ func TestSkipTestAndNonGoFiles(t *testing.T) { } viperSet(map[string]any{configuration.UnleashDryRunKey: true}) defer viperReset() - mut := engine.New(mod, coverage.Result{}, newJobDealerStub(t), engine.WithDirFs(sys)) + mut := engine.New(mod, engine.CodeData{}, newJobDealerStub(t), engine.WithDirFs(sys)) res := mut.Run(context.Background()) if got := res.Mutants; len(got) != 0 { @@ -585,8 +587,7 @@ func TestStopsOnCancel(t *testing.T) { mapFS, mod, c := loadFixture(defaultFixture, ".") defer c() - mut := engine.New(mod, coveredPosition(defaultFixture), newJobDealerStub(t), - engine.WithDirFs(mapFS)) + mut := engine.New(mod, testCodeData, newJobDealerStub(t), engine.WithDirFs(mapFS)) ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -629,7 +630,7 @@ func TestPackageDiscovery(t *testing.T) { defer c() jds := newJobDealerStub(t) - mut := engine.New(mod, coveredPosition(defaultFixture), jds, engine.WithDirFs(mapFS)) + mut := engine.New(mod, testCodeData, jds, engine.WithDirFs(mapFS)) _ = mut.Run(context.Background()) diff --git a/internal/engine/executor.go b/internal/engine/executor.go index 28a91127..99fc2b13 100644 --- a/internal/engine/executor.go +++ b/internal/engine/executor.go @@ -170,7 +170,7 @@ func (m *mutantExecutor) Start(w *workerpool.Worker) { workingDir := filepath.Join(rootDir, m.module.CallingDir) m.mutant.SetWorkdir(workingDir) - if m.mutant.Status() == mutator.NotCovered || m.dryRun { + if m.mutant.Status() == mutator.NotCovered || m.mutant.Status() == mutator.Skipped || m.dryRun { m.outCh <- m.mutant report.Mutant(m.mutant) diff --git a/internal/mutator/mutator.go b/internal/mutator/mutator.go index de1d9477..b39ce9fa 100644 --- a/internal/mutator/mutator.go +++ b/internal/mutator/mutator.go @@ -34,6 +34,7 @@ type Status int const ( NotCovered Status = iota Runnable + Skipped Lived Killed NotViable @@ -46,6 +47,8 @@ func (ms Status) String() string { return "NOT COVERED" case Runnable: return "RUNNABLE" + case Skipped: + return "SKIPPED" case Lived: return "LIVED" case Killed: diff --git a/internal/report/report.go b/internal/report/report.go index 5319d34e..3a709a10 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -58,6 +58,7 @@ type reportStatus struct { lived int timedOut int notCovered int + skipped int notViable int runnable int @@ -110,6 +111,8 @@ func reportMutationStatus(m mutator.Mutator, rep *reportStatus) { rep.lived++ case mutator.NotCovered: rep.notCovered++ + case mutator.Skipped: + rep.skipped++ case mutator.TimedOut: rep.timedOut++ case mutator.NotViable: @@ -211,11 +214,12 @@ func (r *reportStatus) fullRunReport() { lived := fgRed(r.lived) timedOut := fgGreen(r.timedOut) notViable := fgHiBlack(r.notViable) + skipped := fgHiBlack(r.skipped) notCovered := fgHiYellow(r.notCovered) log.Infoln("") log.Infof("Mutation testing completed in %s\n", r.elapsed.String()) log.Infof("Killed: %s, Lived: %s, Not covered: %s\n", killed, lived, notCovered) - log.Infof("Timed out: %s, Not viable: %s\n", timedOut, notViable) + log.Infof("Timed out: %s, Not viable: %s, Skipped: %s\n", timedOut, notViable, skipped) log.Infof("Test efficacy: %.2f%%\n", r.tEfficacy) log.Infof("Mutator coverage: %.2f%%\n", r.mCovered) } @@ -275,7 +279,7 @@ func Mutant(m mutator.Mutator) { status = fgHiYellow(m.Status()) case mutator.TimedOut: status = fgGreen(m.Status()) - case mutator.NotViable: + case mutator.NotViable, mutator.Skipped: status = fgHiBlack(m.Status()) } log.Infof("%s%s %s at %s\n", padding(m.Status()), status, m.Type(), m.Position()) diff --git a/internal/report/report_test.go b/internal/report/report_test.go index 405b8fe6..e2bcb230 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -44,6 +44,11 @@ import ( var fakePosition = newPosition("aFolder/aFile.go", 3, 12) func TestReport(t *testing.T) { + const ( + testingLine = "Mutation testing completed in 2 minutes 22 seconds\n" + coverageLine = "Mutator coverage: 0.00%\n" + ) + nrTestCases := []struct { name string mutants []mutator.Mutator @@ -57,12 +62,13 @@ func TestReport(t *testing.T) { stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsNegation, position: fakePosition}, stubMutant{status: mutator.NotViable, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, stubMutant{status: mutator.TimedOut, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, + stubMutant{status: mutator.Skipped, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, }, want: "\n" + // Limit the time reporting to the first two units (millis are excluded) - "Mutation testing completed in 2 minutes 22 seconds\n" + + testingLine + "Killed: 1, Lived: 1, Not covered: 1\n" + - "Timed out: 1, Not viable: 1\n" + + "Timed out: 1, Not viable: 1, Skipped: 1\n" + "Test efficacy: 50.00%\n" + "Mutator coverage: 66.67%\n", }, @@ -73,11 +79,11 @@ func TestReport(t *testing.T) { }, want: "\n" + // Limit the time reporting to the first two units (millis are excluded) - "Mutation testing completed in 2 minutes 22 seconds\n" + + testingLine + "Killed: 0, Lived: 0, Not covered: 1\n" + - "Timed out: 0, Not viable: 0\n" + + "Timed out: 0, Not viable: 0, Skipped: 0\n" + "Test efficacy: 0.00%\n" + - "Mutator coverage: 0.00%\n", + coverageLine, }, { name: "reports findings with timeouts", @@ -87,11 +93,11 @@ func TestReport(t *testing.T) { }, want: "\n" + // Limit the time reporting to the first two units (millis are excluded) - "Mutation testing completed in 2 minutes 22 seconds\n" + + testingLine + "Killed: 0, Lived: 0, Not covered: 0\n" + - "Timed out: 2, Not viable: 0\n" + + "Timed out: 2, Not viable: 0, Skipped: 0\n" + "Test efficacy: 0.00%\n" + - "Mutator coverage: 0.00%\n", + coverageLine, }, { name: "reports nothing if no result", @@ -148,7 +154,7 @@ func TestReport(t *testing.T) { // Limit the time reporting to the first two units (millis are excluded) "Dry run completed in 2 minutes 22 seconds\n" + "Runnable: 0, Not covered: 0\n" + - "Mutator coverage: 0.00%\n", + coverageLine, }, } for _, tc := range drTestCases {