Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +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 | 🏆 | 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 | 🚫 | | |
Expand Down
2 changes: 2 additions & 0 deletions fws/fws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -13,6 +14,7 @@ import (

func Frameworks() []fwlib.Framework {
return []fwlib.Framework{
go_mutesting.NewGoMutesting(),
infection.NewInfection(),
mull.NewMull(),
mutest_rs.NewMutestRS(),
Expand Down
178 changes: 178 additions & 0 deletions fws/go_mutesting/go_mutesting.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
break // NOTE: we never care about the second hunk for go-mutesting
}

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
}
2 changes: 1 addition & 1 deletion internal/marvinfo/marvinfo.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package marvinfo

func Version() string {
return "1.2.0"
return "1.2.1"
}
1 change: 1 addition & 0 deletions internal/mutations/mutations.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func (m Mutations) MergeConflicting() {
if len(conflicts) <= 1 {
continue
}
conflicts.Sort()

merged := make(Conflicts, 0, len(conflicts))
current := conflicts[0]
Expand Down
Loading