Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bfff83f
feat: mutation displays operation if no description provided
SecretSheppy Apr 23, 2026
2eacbff
chore: add cpp language
SecretSheppy Apr 23, 2026
acbc65a
fix: resolved issue with conflict calculation
SecretSheppy Apr 23, 2026
9161c82
feat: add LessThan method to Range
SecretSheppy Apr 23, 2026
af41dc3
feat: add early version of mtelib and mull fw
SecretSheppy Apr 23, 2026
fc65bf2
refactor: add GetDescription() method to Mutation
SecretSheppy Apr 23, 2026
2f96171
feat: add description generation
SecretSheppy Apr 23, 2026
c14d640
refactor: add more precise message for deletion mutations
SecretSheppy Apr 23, 2026
0b6fe92
feat: add broken mutant extractor
SecretSheppy Apr 23, 2026
fba30ea
test: add testing for mutations package
SecretSheppy Apr 23, 2026
0f96216
fix: removed automatically failing incomplete test
SecretSheppy Apr 23, 2026
e1bfbe5
feat: add conflict merging methods
SecretSheppy Apr 23, 2026
4eb6ae7
feat: add Pending and Ignored statuses
SecretSheppy Apr 23, 2026
186b23f
refactor: wrapped filters to be half width creating a table like look
SecretSheppy Apr 23, 2026
5752292
refactor: made mtelib internal
SecretSheppy Apr 23, 2026
028f1ab
refactor: attached lineSpan and columnSpan to MutantResult
SecretSheppy Apr 23, 2026
edc9bd3
refactor: dropped themeing package
SecretSheppy Apr 23, 2026
b5536dc
refactor: swap language icons and add licenses
SecretSheppy Apr 23, 2026
9627d6f
fix: add merge conflicts that conflict with each other
SecretSheppy Apr 23, 2026
7b18f70
refactor: moved progressbar initialization into fwlib
SecretSheppy Apr 23, 2026
35859b8
feat: add progressbar to mull framework
SecretSheppy Apr 23, 2026
3045601
refactor: swapped script import order
SecretSheppy Apr 24, 2026
22360ca
refactor: now uses fwlib.NewProgressbar
SecretSheppy Apr 24, 2026
33edb45
feat: add stryker-net, stryker-js and stryker4s frameworks
SecretSheppy Apr 24, 2026
b9697a1
feat: add infection php framework
SecretSheppy Apr 24, 2026
22173e4
feat: remove conflicts that have no mutations
SecretSheppy Apr 24, 2026
79464e1
feat: switches over operation to attempt fixing broken mutations
SecretSheppy Apr 24, 2026
e27f7f7
docs: update supported frameworks list and gallery
SecretSheppy Apr 24, 2026
ab656e2
refactor: made list primary name of listCmd
SecretSheppy Apr 24, 2026
02fdf60
docs: improved installation instructions
SecretSheppy Apr 24, 2026
50f4a4a
docs: update pitest docs with mvn run command
SecretSheppy Apr 24, 2026
e7c779a
test: add mutation sorting by range method
SecretSheppy Apr 25, 2026
ebc68cb
fix: corrected errors with broken mutation fixing
SecretSheppy Apr 25, 2026
390917c
fix: getFuncName can no longer return empty string
SecretSheppy Apr 25, 2026
3f2bd7b
docs: add source for mutation descriptions
SecretSheppy Apr 25, 2026
ea24a6e
docs: add definition of a broken mutation
SecretSheppy Apr 25, 2026
5bb5167
chore: bump version number
SecretSheppy Apr 25, 2026
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
173 changes: 81 additions & 92 deletions README.md

Large diffs are not rendered by default.

Binary file added docs/marv_infection_php_mutant.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/marv_pitest_guava.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/marv_pitest_guava_mutant.png
Binary file not shown.
Binary file modified docs/marv_results_overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions fwlib/fwlib.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package fwlib

import (
"fmt"
"os"

"github.com/SecretSheppy/marv/internal/languages"
"github.com/SecretSheppy/marv/internal/mutations"
"github.com/schollz/progressbar/v3"
)

type Meta struct {
Expand Down Expand Up @@ -46,3 +50,16 @@ type Framework interface {
// ReadLines returns the lines of the specified file
ReadLines(file string) ([]string, error)
}

func NewProgressbar(length int, desc string) *progressbar.ProgressBar {
return progressbar.NewOptions(
length,
progressbar.OptionSetWriter(os.Stdout),
progressbar.OptionSetDescription(desc),
progressbar.OptionSetRenderBlankState(true))
}

func FinishProgressbar(bar *progressbar.ProgressBar) {
bar.Finish()
fmt.Println()
}
10 changes: 10 additions & 0 deletions fws/fws.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ package fws

import (
"github.com/SecretSheppy/marv/fwlib"
"github.com/SecretSheppy/marv/fws/infection"
"github.com/SecretSheppy/marv/fws/mull"
"github.com/SecretSheppy/marv/fws/mutest_rs"
"github.com/SecretSheppy/marv/fws/pitest"
"github.com/SecretSheppy/marv/fws/stryker4s"
"github.com/SecretSheppy/marv/fws/stryker_js"
"github.com/SecretSheppy/marv/fws/stryker_net"
)

func Frameworks() []fwlib.Framework {
return []fwlib.Framework{
infection.NewInfection(),
mull.NewMull(),
mutest_rs.NewMutestRS(),
pitest.NewPitest(),
stryker4s.NewStryker4s(),
stryker_js.NewStrykerJS(),
stryker_net.NewStrykerNet(),
}
}

Expand Down
49 changes: 49 additions & 0 deletions fws/infection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Important Information About Infection Support in Marv

> [!NOTE]
> See [infection example MTE report](https://dashboard.stryker-mutator.io/reports/github.com/infection/infection/master#mutant) for the mutant dataset used to in examples.

The Infection framework often exports what Marv views as "broken" mutations. This is because the value of the
mutations end column exceeds the length of the end line, for example:

```json
{
"id": "adf6cacd35a4867054da054ee6a6b4a0",
"mutatorName": "ConcatOperandRemoval",
"replacement": "'Cannot access to the command application if the command has not been ',",
"description": "Removes an operand from a string concatenation.\n\n```php\n$x = 'foo' . 'bar';\n```\n\nWill be mutated to:\n\n```php\n$x = 'foo';\n```\n\nAnd:\n\n```php\n$x = 'bar';\n",
"location": {
"start": {
"line": 61,
"column": 13
},
"end": {
"line": 62,
"column": 84
}
},
"status": "Survived"
}
```

**Caption:** Mutant taken from [`Command/BaseCommand.php`](https://dashboard.stryker-mutator.io/reports/github.com/infection/infection/master#mutant/Command/BaseCommand.php) that Marv would view as "broken".

If Marv tries to render a "broken" mutation, like the one shown above, it will panic and fail to render the file. The
reason this occurs in Marv and not in [Stryker Mutation Testing Elements (MTE)](https://github.com/stryker-mutator/mutation-testing-elements/tree/master)
is because of the different ways the two system parse files. MTE never splits the source file into lines, and instead
makes indexes of where newline characters occur, and uses those to read and replace bits of the source. This means that
when a "broken" mutation is shown by MTE, it will not fail to render the file. However, it will render the mutation
incorrectly, as shown below.

![MTE Formatting Error Example](docs/mte_incorrect_formatting.png)

As previously stated, if Marv tried to render the above mutation as is, it would panic and fail to show the file. This
is because Marv does split the source file into a slice of lines, and then performs the replacement substitution on
the individual lines.

To correct for this issue with Infection, the [Marv Infection Framework](infection.go) instance will iterate over all
mutations, and for any mutations where `mutation.End.Char > len(line)`, it then sets `mutation.End.Char = len(line)`.
In practise, this seems to solve the formatting issues for most cases. The Marv formatted version of the same mutation
is shown below.

![Marv Formatting Correction Example](docs/marv_formatting_correction.png)
Binary file added fws/infection/docs/marv_formatting_correction.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fws/infection/docs/mte_incorrect_formatting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 96 additions & 0 deletions fws/infection/infection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package infection

import (
"github.com/SecretSheppy/marv/fwlib"
"github.com/SecretSheppy/marv/internal/languages"
"github.com/SecretSheppy/marv/internal/mtelib"
"github.com/SecretSheppy/marv/internal/mutations"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)

var meta = fwlib.Meta{
Name: "infection",
Language: languages.Php,
URL: "https://https://infection.github.io/",
}

type YamlConfig struct {
MTEJson string `yaml:"mte-json"`
}

type YamlWrapper struct {
Cfg *YamlConfig `yaml:"infection"`
}

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.MTEJson != "", nil
}

type Infection struct {
yml *YamlWrapper
mte *mtelib.MTE
}

func NewInfection() *Infection {
return &Infection{yml: &YamlWrapper{}}
}

func (i *Infection) Meta() *fwlib.Meta {
return &meta
}

func (i *Infection) Yaml() fwlib.FWConfig {
return i.yml
}

func (i *Infection) LoadResults() error {
log.Info().Msgf("%s - loading results", i.Meta().Name)
var err error
i.mte, err = mtelib.NewMTE(i.yml.Cfg.MTEJson)
return err
}

func (i *Infection) TransformResults() error {
log.Info().Msgf("%s - transforming results", i.Meta().Name)

bar := fwlib.NewProgressbar(i.mte.RawMutationsCount(), "transforming")
i.mte.Transform(bar)
fwlib.FinishProgressbar(bar)

i.correctLineLengthOverhangs()
return nil
}

// see infection/README.md as to why this method is necessary and for visual examples of it in practise.
func (i *Infection) correctLineLengthOverhangs() {
for file, conflicts := range i.mte.Mutations() {
lines := i.mte.ReadLines(file)
for _, conflict := range conflicts {
for _, mutation := range conflict.Mutations {
endLineLength := len(lines[mutation.End.Line])
if mutation.End.Char > endLineLength {
mutation.End.Char = endLineLength
}
}
}
}
}

func (i *Infection) Mutations() mutations.Mutations {
return i.mte.Mutations()
}

func (i *Infection) ReadLines(file string) ([]string, error) {
return i.mte.ReadLines(file), nil
}
177 changes: 177 additions & 0 deletions fws/mull/mull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package mull

import (
"fmt"
"regexp"
"slices"

"github.com/SecretSheppy/marv/fwlib"
"github.com/SecretSheppy/marv/internal/languages"
"github.com/SecretSheppy/marv/internal/mtelib"
"github.com/SecretSheppy/marv/internal/mutations"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)

var (
meta = fwlib.Meta{
Name: "Mull",
Language: languages.Cpp,
URL: "https://mull-project.com/",
}
function = regexp.MustCompile(`\s*([A-Za-z_0-9]*)\s*\(`)

// cpp keywords according to https://en.cppreference.com/cpp/keyword
cppKeyWords = []string{
"alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto",
"bitand", "bitor", "bool", "break", "case", "catch", "char", "char8_t", "char16_t", "char32_t", "class",
"compl", "concept", "const", "consteval", "constexpr", "constinit", "const_cast", "continue", "contract_assert",
"co_await", "co_return", "co_yield", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else",
"enum", "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "inline", "int",
"long", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq",
"private", "protected", "public", "reflexpr", "register", "reinterpret_cast", "requires", "return", "short",
"signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template",
"this", "thread_local", "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using",
"virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq",
}
)

type YamlConfig struct {
MTEJson string `yaml:"mte-json"`
}

type YamlWrapper struct {
Cfg *YamlConfig `yaml:"mull"`
}

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.MTEJson != "", nil
}

type Mull struct {
yml *YamlWrapper
mte *mtelib.MTE
}

func NewMull() *Mull {
return &Mull{yml: &YamlWrapper{}}
}

func (m *Mull) Meta() *fwlib.Meta {
return &meta
}

func (m *Mull) Yaml() fwlib.FWConfig {
return m.yml
}

func (m *Mull) LoadResults() error {
log.Info().Msgf("%s - loading results", m.Meta().Name)
var err error
m.mte, err = mtelib.NewMTE(m.yml.Cfg.MTEJson)
return err
}

func (m *Mull) TransformResults() error {
log.Info().Msgf("%s - transforming results", m.Meta().Name)

bar := fwlib.NewProgressbar(m.mte.RawMutationsCount(), "transforming")
m.mte.Transform(bar)
fwlib.FinishProgressbar(bar)

fixed := 0
for file, conflicts := range m.mte.Mutations() {
lines := m.mte.ReadLines(file)
for _, conflict := range conflicts {
for _, mutation := range conflict.Mutations {
if mutation.IsBroken() {
fixed += attemptBrokenMutationFix(mutation)
conflict.ResizeToInclude(mutation)
}
m.generateDescription(lines, mutation)
}
}
}
log.Info().Msgf("%s - fixed %d broken mutations", m.Meta().Name, fixed)

return nil
}

// generates a description based off of the descriptions give on the supported mutation operators page of the mull
// documentation: https://mull.readthedocs.io/en/latest/SupportedMutations.html.
func (m *Mull) generateDescription(lines []string, mutation *mutations.Mutation) {
switch mutation.Operation {
case "cxx_remove_void_call":
mutation.Description = fmt.Sprintf("Removed call to void function `%s`", getFuncName(lines, mutation))
case "cxx_replace_scalar_call":
mutation.Description = fmt.Sprintf("Replaced call to function `%s` with `42`", getFuncName(lines, mutation))
case "negate_mutator":
mutation.Description = "Negated conditionals"
case "scalar_value_mutator":
mutation.Description = "Replaced zeros with `42` and non-zeros with `0`"
default:
line := lines[mutation.Start.Line]
endChar := len(line) - 1
if mutation.Start.Line == mutation.End.Line {
endChar = mutation.End.Char
}
original := line[mutation.Start.Char:endChar]
if mutation.Replacement != "" {
mutation.Description = fmt.Sprintf("Replaced `%s` with `%s`", original, mutation.Replacement)
} else {
mutation.Description = fmt.Sprintf("Removed `%s`", original)
}
}
Comment thread
SecretSheppy marked this conversation as resolved.
}

func attemptBrokenMutationFix(mutation *mutations.Mutation) int {
switch mutation.Operation {
case "cxx_assign_const", "cxx_init_const", "cxx_remove_void_call", "cxx_replace_scalar_void", "negate_mutator":
// Operators marv does not currently fix.
return 0
case "cxx_bitwise_not_to_noop", "cxx_minus_to_noop", "cxx_post_dec_to_post_inc", "cxx_pre_dec_to_pre_inc", "cxx_remove_negation", "cxx_gt_to_ge", "cxx_gt_to_le", "cxx_lt_to_ge", "cxx_lt_to_le":
// Operators that replace a source string length of 1 (~, !, -)
mutation.End.Line = mutation.Start.Line
mutation.End.Char = mutation.Start.Char + 1
case "cxx_ge_to_gt", "cxx_ge_to_lt", "cxx_le_to_gt", "cxx_le_to_lt", "cxx_post_inc_to_post_dec", "cxx_pre_inc_to_pre_dec":
// Operators that replace a source string length of 2 (==, <=, /=, ...)
mutation.End.Line = mutation.Start.Line
mutation.End.Char = mutation.Start.Char + 2
default:
// Operators that replace a source string of length equal to its replacement string
mutation.End.Line = mutation.Start.Line
mutation.End.Char = mutation.Start.Char + len(mutation.Replacement)
}
return 1
}

func getFuncName(lines []string, mutation *mutations.Mutation) string {
match := function.FindAllStringSubmatch(lines[mutation.Start.Line], -1)
funcStr := "??"
for _, str := range match {
// NOTE: takes first non keyword in the replacements string that matches the regex as the function name.
if !slices.Contains(cppKeyWords, str[1]) && str[1] != "" {
funcStr = str[1]
break
}
}
return funcStr
}

func (m *Mull) Mutations() mutations.Mutations {
return m.mte.Mutations()
}

func (m *Mull) ReadLines(file string) ([]string, error) {
return m.mte.ReadLines(file), nil
}
Loading
Loading