From d9ea86d257d6a46d7f221f27c47b33f8c6e6396d Mon Sep 17 00:00:00 2001 From: SecretSheppy <62794249+SecretSheppy@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:19:35 +0100 Subject: [PATCH 1/4] feat: add experimental go-mutesting support --- README.md | 1 + fws/fws.go | 2 + fws/go_mutesting/go_mutesting.go | 178 +++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 fws/go_mutesting/go_mutesting.go diff --git a/README.md b/README.md index 4fc0b7c..f446828 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ A list of mutation testing frameworks that either are currently supported or wil | [Mull](https://mull-project.com/) | C/C++ | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema | | [Dextool Mutate](https://joakim-brannstrom.github.io/dextool/plugin/mutate/) | C/C++ | 🚫 | | | | [stryker-net](https://github.com/stryker-mutator/stryker-net) | C# | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema | +| [go-mutesting](https://github.com/zimmski/go-mutesting) | Go | ⚠️ | | `loop/range_break` mutants from [avito-tech fork](https://github.com/avito-tech/go-mutesting) never seem to be in the correct place. | | [hcoles/pitest](https://github.com/hcoles/pitest) | Java | ✅️ | 1.0.0 | See [Pitest configuration](fws/pitest/README.md) | | [Major](https://mutation-testing.org/) | Java | 🚫 | | | | [stryker-js](https://github.com/stryker-mutator/stryker-js) | JavaScript | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema | diff --git a/fws/fws.go b/fws/fws.go index 0eccc55..da45322 100644 --- a/fws/fws.go +++ b/fws/fws.go @@ -2,6 +2,7 @@ package fws import ( "github.com/SecretSheppy/marv/fwlib" + "github.com/SecretSheppy/marv/fws/go_mutesting" "github.com/SecretSheppy/marv/fws/infection" "github.com/SecretSheppy/marv/fws/mull" "github.com/SecretSheppy/marv/fws/mutest_rs" @@ -13,6 +14,7 @@ import ( func Frameworks() []fwlib.Framework { return []fwlib.Framework{ + go_mutesting.NewGoMutesting(), infection.NewInfection(), mull.NewMull(), mutest_rs.NewMutestRS(), diff --git a/fws/go_mutesting/go_mutesting.go b/fws/go_mutesting/go_mutesting.go new file mode 100644 index 0000000..7dec2a6 --- /dev/null +++ b/fws/go_mutesting/go_mutesting.go @@ -0,0 +1,178 @@ +package go_mutesting + +import ( + "encoding/json" + "os" + "strings" + + "github.com/SecretSheppy/marv/fwlib" + "github.com/SecretSheppy/marv/internal/languages" + "github.com/SecretSheppy/marv/internal/mutations" + "github.com/aymanbagabas/go-udiff" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" +) + +var meta = fwlib.Meta{ + Name: "go-mutesting", + Language: languages.Go, + URL: "https://github.com/zimmski/go-mutesting", // NOTE: for actively maintained fork see https://github.com/avito-tech/go-mutesting +} + +type YamlConfig struct { + JsonReport string `yaml:"json-report"` +} + +type YamlWrapper struct { + Cfg *YamlConfig `yaml:"go-mutesting"` +} + +func (y *YamlWrapper) Init() interface{} { + return &YamlWrapper{Cfg: &YamlConfig{}} +} + +func (y *YamlWrapper) Load(yml []byte) (bool, error) { + if err := yaml.Unmarshal(yml, y); err != nil { + return false, err + } + if y.Cfg == nil { + return false, nil + } + return y.Cfg.JsonReport != "", nil +} + +type Report struct { + Escaped []Mutation `json:"escaped"` + Timeouted []Mutation `json:"timeouted"` + Killed []Mutation `json:"killed"` + Errored []Mutation `json:"errored"` +} + +type Mutation struct { + Mutator Mutator `json:"mutator"` +} + +type Mutator struct { + MutatorName string `json:"mutatorName"` + OriginalSourceCode string `json:"originalSourceCode"` + MutatedSourceCode string `json:"mutatedSourceCode"` + OriginalFilePath string `json:"originalFilePath"` + OriginalStartLine int `json:"originalStartLine"` +} + +type GoMutesting struct { + yml *YamlWrapper + report Report + ms mutations.Mutations + files map[string][]string +} + +func NewGoMutesting() *GoMutesting { + return &GoMutesting{yml: &YamlWrapper{}} +} + +func (g *GoMutesting) Meta() *fwlib.Meta { + return &meta +} + +func (g *GoMutesting) Yaml() fwlib.FWConfig { + return g.yml +} + +func (g *GoMutesting) LoadResults() error { + log.Info().Msgf("%s - loading results", g.Meta().Name) + + data, err := os.ReadFile(g.yml.Cfg.JsonReport) + if err != nil { + return err + } + return json.Unmarshal(data, &g.report) +} + +func (g *GoMutesting) TransformResults() error { + g.ms = make(mutations.Mutations) + g.files = make(map[string][]string) + if err := g.transformResults(g.report.Escaped, mutations.Survived); err != nil { + return err + } + if err := g.transformResults(g.report.Timeouted, mutations.Timeout); err != nil { + return err + } + if err := g.transformResults(g.report.Killed, mutations.Killed); err != nil { + return err + } + return g.transformResults(g.report.Errored, mutations.Crashed) +} + +func (g *GoMutesting) transformResults(ms []Mutation, status mutations.Status) error { + for _, mutation := range ms { + mutator := mutation.Mutator + lines := g.addOrGetFile(mutator) + + edits := udiff.Strings(mutator.OriginalSourceCode, mutator.MutatedSourceCode) + d, err := udiff.ToUnifiedDiff("old", "new", mutator.OriginalSourceCode, edits, 0) + if err != nil { + return err + } + + var ( + removedLineCount, hunkStartLine int + replacement strings.Builder + ) + for _, h := range d.Hunks { + hunkStartLine = h.FromLine + + for _, l := range h.Lines { + switch l.Kind { + case udiff.Delete: + removedLineCount++ + case udiff.Insert, udiff.Equal: + replacement.WriteString(l.Content) + } + } + } + + startLine := mutator.OriginalStartLine - 1 + if startLine <= 0 || mutator.MutatorName == "loop/range_break" { + startLine = hunkStartLine - 1 + } + endLine := startLine + removedLineCount - 1 + + m := &mutations.Mutation{ + Operation: mutator.MutatorName, + Start: &mutations.Range{ + Line: startLine, + Char: 0, + }, + End: &mutations.Range{ + Line: endLine, + Char: len(lines[endLine]), + }, + Status: status, + Replacement: replacement.String(), + } + + g.ms.Append(mutator.OriginalFilePath, m) + } + return nil +} + +func (g *GoMutesting) addOrGetFile(mutator Mutator) []string { + path := mutator.OriginalFilePath + if g.files[path] == nil { + lines := make([]string, 0) + for line := range strings.Lines(mutator.OriginalSourceCode) { + lines = append(lines, strings.ReplaceAll(line, "\n", "")) + } + g.files[path] = lines + } + return g.files[path] +} + +func (g *GoMutesting) Mutations() mutations.Mutations { + return g.ms +} + +func (g *GoMutesting) ReadLines(file string) ([]string, error) { + return g.files[file], nil +} From f4c101a7434e647082410d5295b44317cd6bb247 Mon Sep 17 00:00:00 2001 From: SecretSheppy <62794249+SecretSheppy@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:45:52 +0100 Subject: [PATCH 2/4] fix: resolve issues with go-mutesting support --- fws/go_mutesting/go_mutesting.go | 2 +- internal/mutations/mutations.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/fws/go_mutesting/go_mutesting.go b/fws/go_mutesting/go_mutesting.go index 7dec2a6..b5cdd27 100644 --- a/fws/go_mutesting/go_mutesting.go +++ b/fws/go_mutesting/go_mutesting.go @@ -121,7 +121,6 @@ func (g *GoMutesting) transformResults(ms []Mutation, status mutations.Status) e ) for _, h := range d.Hunks { hunkStartLine = h.FromLine - for _, l := range h.Lines { switch l.Kind { case udiff.Delete: @@ -130,6 +129,7 @@ func (g *GoMutesting) transformResults(ms []Mutation, status mutations.Status) e replacement.WriteString(l.Content) } } + break // NOTE: we never care about the second hunk for go-mutesting } startLine := mutator.OriginalStartLine - 1 diff --git a/internal/mutations/mutations.go b/internal/mutations/mutations.go index b667a2d..88a06aa 100644 --- a/internal/mutations/mutations.go +++ b/internal/mutations/mutations.go @@ -205,6 +205,7 @@ func (m Mutations) MergeConflicting() { if len(conflicts) <= 1 { continue } + conflicts.Sort() merged := make(Conflicts, 0, len(conflicts)) current := conflicts[0] From 4465759f2549ea4cdb335e30cacc2cb42cd8807c Mon Sep 17 00:00:00 2001 From: SecretSheppy <62794249+SecretSheppy@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:49:41 +0100 Subject: [PATCH 3/4] docs: add go-mutesting support and add mutaml to list --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f446828..58a5766 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,11 @@ A list of mutation testing frameworks that either are currently supported or wil | [Mull](https://mull-project.com/) | C/C++ | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema | | [Dextool Mutate](https://joakim-brannstrom.github.io/dextool/plugin/mutate/) | C/C++ | 🚫 | | | | [stryker-net](https://github.com/stryker-mutator/stryker-net) | C# | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema | -| [go-mutesting](https://github.com/zimmski/go-mutesting) | Go | ⚠️ | | `loop/range_break` mutants from [avito-tech fork](https://github.com/avito-tech/go-mutesting) never seem to be in the correct place. | +| [go-mutesting](https://github.com/zimmski/go-mutesting) | Go | 🏆 | 1.2.1 | | | [hcoles/pitest](https://github.com/hcoles/pitest) | Java | ✅️ | 1.0.0 | See [Pitest configuration](fws/pitest/README.md) | | [Major](https://mutation-testing.org/) | Java | 🚫 | | | | [stryker-js](https://github.com/stryker-mutator/stryker-js) | JavaScript | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema | +| [mutaml](https://github.com/jmid/mutaml) | OCaml | 🚫 | | | | [infection](https://github.com/infection/infection) | PHP | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema, additionally see [infection readme](fws/infection/README.md) | | [Cosmic Ray](https://github.com/sixty-north/cosmic-ray) | Python | 🚫 | | | | [MutPy](https://github.com/mutpy/mutpy) | Python | 🚫 | | | From dee4c5dd184d9155b40f9a728fbc2825b3dee592 Mon Sep 17 00:00:00 2001 From: SecretSheppy <62794249+SecretSheppy@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:50:05 +0100 Subject: [PATCH 4/4] chore: bump version number --- internal/marvinfo/marvinfo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/marvinfo/marvinfo.go b/internal/marvinfo/marvinfo.go index d4e835a..0157191 100644 --- a/internal/marvinfo/marvinfo.go +++ b/internal/marvinfo/marvinfo.go @@ -1,5 +1,5 @@ package marvinfo func Version() string { - return "1.2.0" + return "1.2.1" }