From 9750d9192d6271cbbf5f4bd3d30b4fad03bfd5dd Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Thu, 14 May 2026 12:24:48 +0100 Subject: [PATCH 1/3] ci: add mutation testing workflow running go-mutesting on itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs on push to master only (not PRs — too slow). Scoped to the mutator and filter packages whose tests are plain unit tests; cmd/ is excluded because its tests re-invoke the binary, and internal/annotation is excluded due to existing build errors. exec-timeout is 30s per mutant, job timeout is 45 minutes. continue-on-error keeps the result informational until a baseline score is established. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/mutation.yml | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/mutation.yml diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..eb5760c --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,38 @@ +name: Mutation Testing + +on: + push: + branches: [master] + +permissions: + contents: read + +jobs: + mutation: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go.mod + + - name: Build go-mutesting + run: go build -o /usr/local/bin/go-mutesting ./cmd/go-mutesting + + - name: Run mutation testing + # Scoped to packages with isolated unit tests. cmd/ is excluded because + # its tests are integration tests that re-invoke the binary. internal/annotation + # is excluded until its build errors are resolved. + run: | + go-mutesting \ + --exec-timeout 30 \ + github.com/avito-tech/go-mutesting/mutator/arithmetic \ + github.com/avito-tech/go-mutesting/mutator/branch \ + github.com/avito-tech/go-mutesting/mutator/expression \ + github.com/avito-tech/go-mutesting/mutator/loop \ + github.com/avito-tech/go-mutesting/mutator/numbers \ + github.com/avito-tech/go-mutesting/mutator/statement \ + github.com/avito-tech/go-mutesting/internal/filter + # Informational only: do not fail the build on mutation score. + continue-on-error: true From 54d902300571278fe5697640e515e5b55664d22c Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Thu, 14 May 2026 12:39:29 +0100 Subject: [PATCH 2/3] test: kill 8 surviving mutants in SkipMakeArgsFilter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 4 targeted test cases to internal/filter that together cover the specific AST paths previously left untested: a 1-arg make (kills the len>1 → >=1/true variants), unary negation of a complex sub-expression (kills else-branch removal), a nested CallExpr with two int args (kills loop-break and statement-remove on the arg-recursion loop), and unary on float literals (kills the token.INT → true comparison mutation). Co-Authored-By: Claude Sonnet 4.6 --- internal/filter/skip_mutation_test.go | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/filter/skip_mutation_test.go b/internal/filter/skip_mutation_test.go index 4bd2a17..f396a47 100644 --- a/internal/filter/skip_mutation_test.go +++ b/internal/filter/skip_mutation_test.go @@ -87,6 +87,39 @@ func TestSkipMutationForInitSlicesAndMaps(t *testing.T) { expectedLiterals: []string{"3", "2", "4"}, expectedOperators: []string{"+", "*"}, }, + { + // kills mutants that relax len(callExpr.Args) > 1 to >= 1, > 0, or true: + // with those mutants the 1-arg make(MySlice) enters the isIdent branch + // and panics on Args[1] access. + name: "do not skip mutation for single-arg make with type alias", + code: `package main; type MySlice []int; var a = make(MySlice)`, + expectedLiterals: []string{}, + expectedOperators: []string{}, + }, + { + // kills mutants that remove or noop the else branch of the UnaryExpr case: + // without recursion into -(2+3), the "2", "3", and "+" inside are not collected. + name: "skip mutation for unary negation of complex inner expression", + code: `package main; var a = make([]int, 0, -(2+3))`, + expectedLiterals: []string{"0", "2", "3"}, + expectedOperators: []string{"+"}, + }, + { + // kills mutants that break or remove the collectForIgnoredNodes call inside the + // CallExpr case loop: with those mutants only the first arg (or nothing) is collected. + name: "skip mutation for nested call with multiple int args", + code: `package main; var a = make([]int, someFunc(2, 3))`, + expectedLiterals: []string{"2", "3"}, + expectedOperators: []string{}, + }, + { + // kills the mutant that replaces xLit.Kind == token.INT with true: that mutant + // treats float unary operands like int ones and adds their operator positions. + name: "do not skip mutation for unary on float literals", + code: `package main; var a = make([]float64, -1.5, +2.3)`, + expectedLiterals: []string{}, + expectedOperators: []string{}, + }, } for _, tt := range tests { From 39722a4be16a2bc0312579bbdb31d73289153530 Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Thu, 14 May 2026 12:47:50 +0100 Subject: [PATCH 3/3] fix: clean up self-mutation-testing branch before merge - Remove continue-on-error from mutation workflow (the tool exits 0 regardless of score, making the flag redundant and misleading) - Document all excluded packages and the exit-0 behaviour in a comment - Gitignore mutation run artifacts (binary, report.json/html, *.go.new) - Remove mutant-specific comments from test table entries; keep only the parser-validity note on the 1-arg make case Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/mutation.yml | 13 ++++++++----- .gitignore | 6 ++++++ internal/filter/skip_mutation_test.go | 11 ++--------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index eb5760c..9d901e2 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -21,9 +21,14 @@ jobs: run: go build -o /usr/local/bin/go-mutesting ./cmd/go-mutesting - name: Run mutation testing - # Scoped to packages with isolated unit tests. cmd/ is excluded because - # its tests are integration tests that re-invoke the binary. internal/annotation - # is excluded until its build errors are resolved. + # Scoped to packages with isolated unit tests. + # Excluded: + # cmd/ - integration tests re-invoke the binary (would recurse) + # internal/annotation - build errors prevent compilation + # internal/importing - relies on GOPATH/src layout; fails in module mode + # internal/parser - uses deprecated go/loader; excluded until migration + # The tool always exits 0 regardless of mutation score, so this step is + # informational. Check report.json for the full breakdown. run: | go-mutesting \ --exec-timeout 30 \ @@ -34,5 +39,3 @@ jobs: github.com/avito-tech/go-mutesting/mutator/numbers \ github.com/avito-tech/go-mutesting/mutator/statement \ github.com/avito-tech/go-mutesting/internal/filter - # Informational only: do not fail the build on mutation score. - continue-on-error: true diff --git a/.gitignore b/.gitignore index daf913b..f4d1ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ _testmain.go *.exe *.test *.prof + +# go-mutesting build output and run artifacts +/go-mutesting +report.json +report.html +*.go.new diff --git a/internal/filter/skip_mutation_test.go b/internal/filter/skip_mutation_test.go index f396a47..e6676ee 100644 --- a/internal/filter/skip_mutation_test.go +++ b/internal/filter/skip_mutation_test.go @@ -88,33 +88,26 @@ func TestSkipMutationForInitSlicesAndMaps(t *testing.T) { expectedOperators: []string{"+", "*"}, }, { - // kills mutants that relax len(callExpr.Args) > 1 to >= 1, > 0, or true: - // with those mutants the 1-arg make(MySlice) enters the isIdent branch - // and panics on Args[1] access. + // make(MySlice) with one arg is syntactically valid for the parser even though + // the type-checker would reject it; it exercises the len(Args) > 1 guard. name: "do not skip mutation for single-arg make with type alias", code: `package main; type MySlice []int; var a = make(MySlice)`, expectedLiterals: []string{}, expectedOperators: []string{}, }, { - // kills mutants that remove or noop the else branch of the UnaryExpr case: - // without recursion into -(2+3), the "2", "3", and "+" inside are not collected. name: "skip mutation for unary negation of complex inner expression", code: `package main; var a = make([]int, 0, -(2+3))`, expectedLiterals: []string{"0", "2", "3"}, expectedOperators: []string{"+"}, }, { - // kills mutants that break or remove the collectForIgnoredNodes call inside the - // CallExpr case loop: with those mutants only the first arg (or nothing) is collected. name: "skip mutation for nested call with multiple int args", code: `package main; var a = make([]int, someFunc(2, 3))`, expectedLiterals: []string{"2", "3"}, expectedOperators: []string{}, }, { - // kills the mutant that replaces xLit.Kind == token.INT with true: that mutant - // treats float unary operands like int ones and adds their operator positions. name: "do not skip mutation for unary on float literals", code: `package main; var a = make([]float64, -1.5, +2.3)`, expectedLiterals: []string{},