diff --git a/README.md b/README.md
index 734cd58..4fc0b7c 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-
+
-Mutations Analysis, Review and Visualisation
+Marv: Mutations Analysis, Review and Visualisation
Marv is a visualization and review tool for mutation testing. It provides a standardized results format and
visualization across all [supported frameworks](#supported-frameworks).
@@ -13,15 +13,15 @@ or even languages in one go.
## Table of Contents
* [Supported Frameworks](#supported-frameworks)
- * [Pitest Configuration](#pitest-configuration)
- * [Decompilers](#decompilers)
-* [Install & Build](#install--build)
+* [Installation](#installation)
+ * [Build from source](#build-from-source)
* [Libraries](#libraries)
* [Usage](#usage)
* [Gallery](#gallery)
* [Export Format](#export-format)
* [Mutations Format](#mutations-format)
* [Reviews Format](#reviews-format)
+* [Other](#other)
## Supported Frameworks
@@ -33,106 +33,88 @@ A list of mutation testing frameworks that either are currently supported or wil
* 🚧 In development
* 🚫 Not currently supported
-| Framework | language | Support | Marv Version | Required Libraries | Notes |
-|------------------------------------------------------------------------------|------------|:-------:|:------------:|---------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|
-| [Mull](https://mull-project.com/) | C/C++ | 🚧 | | | |
-| [Dextool Mutate](https://joakim-brannstrom.github.io/dextool/plugin/mutate/) | C/C++ | 🚫 | | | |
-| [stryker-net](https://github.com/stryker-mutator/stryker-net) | C# | 🚫 | | | |
-| [hcoles/pitest](https://github.com/hcoles/pitest) | Java | ✅️ | 1.0.0 | [vineflower-server](https://github.com/SecretSheppy/vineflower-server) (recommended)
or one of [alternatives](#decompilers) | See [Pitest configuration](#pitest) |
-| [Major](https://mutation-testing.org/) | Java | 🚫 | | | |
-| [stryker-js](https://github.com/stryker-mutator/stryker-js) | JavaScript | 🚫 | | | |
-| [infection](https://github.com/infection/infection) | PHP | 🚫 | | | |
-| [Cosmic Ray](https://github.com/sixty-north/cosmic-ray) | Python | 🚫 | | | |
-| [MutPy](https://github.com/mutpy/mutpy) | Python | 🚫 | | | |
-| [mutant](https://github.com/mbj/mutant) | Ruby | 🚫 | | | |
-| [mutest-rs](https://github.com/zalanlevai/mutest-rs) | Rust | 🏆 | 1.0.0 | Native | |
-
-### Pitest Configuration
-
-Pitest must be run with the `-Dfeatures="+EXPORT"` flag which exports the mutated class files. This is required because
-Marv will decompile these class files to construct each mutants replacement string.
-
-> [!NOTE]
-> The replacement strings (inserted lines) that Marv produces are correct, however they are occasionally flanked by
-> incorrectly formatted deleted lines due to formatting differences between the source code and decompiled class code.
-
-A new Marv Pitest configuration can be created by running the `marv init -f Pitest` command.
-
-#### Decompilers
-
-Marv has a range of decompiler options that can be used with to construct the Pitest mutant replacement strings. They
-are listed below.
-
-> [!CAUTION]
-> The `garlic` decompiler is currently unstable and using it could cause some mutants to be skipped due to a
-> segmentation fault that occurs when running `garlic` on some class files.
-
-* [vineflower-server](https://github.com/SecretSheppy/vineflower-server) (recommended)
-* [vineflower](https://github.com/Vineflower/vineflower)
-* [garlic](https://github.com/neocanable/garlic)
-
-For installation location see [Installation - Libraries](#libraries)
-
-## Install & Build
-
-Marv can be quickly and easily installed with the `go` tool:
-
+| Framework | language | Support | Marv Version | Notes |
+|------------------------------------------------------------------------------|------------|:-------:|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [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 |
+| [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 |
+| [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 | 🚫 | | |
+| [mutant](https://github.com/mbj/mutant) | Ruby | 🚫 | | |
+| [mutest-rs](https://github.com/zalanlevai/mutest-rs) | Rust | 🏆 | 1.0.0 | |
+| [strkyer4s](https://github.com/stryker-mutator/stryker4s) | Scala | 🏆 | 1.2.0 | Supported through [MTE](https://github.com/stryker-mutator/mutation-testing-elements) schema |
+
+## Installation
+
+Marv can be installed with the `go` tool. To use the installed executable, add the `GOPATH` environment variable
+to your system path. For more information run `go help install`.
```
go install github.com/SecretSheppy/marv/cmd/marv@latest
```
-### Manual
-
-Clone the repository and run the command below that relates to the host operating system. To run the `marv` executable
-from anywhere on the system, add the compiled executable to the system `PATH` variable.
+### Build from source
-* **Linux/MacOS:** `go build cmd/marv/marv.go -o marv`
-* **Windows:** `go build cmd/marv/marv.go -o marv.exe`
+Builds exactly as a normal go project would. See the [go.dev](https://go.dev/doc/tutorial/compile-install) tutorial
+for more information. The target file to build is [`cmd/marv/main.go`](cmd/marv/main.go).
+```cli
+go build cmd/marv/main.go -o [output]
+```
### Libraries
-Libraries can either be stored directly in the Marv install directory in the `lib` folder (this will need to be created,
-as it does not exist by default) or in an external folder provided to Marv via the `MARV_LIB_PATH` environment variable.
+If using a framework that requires external libraries, they will need to be set with the `MARV_LIB_PATH` environment
+variable. The alternative to this is to put the library into the local directory where the Marv tool is being run.
## Usage
-The output from `marv --help` is featured below and details how marv can be used. Marv defaults to the port `:8080`.
-
-```terminaloutput
-Mutations Analysis, Review and Visualisation (Marv) is a tool that allows for efficient analysis and
-review of mutations through visualisations - it can be used 'as is' or can be integrated into a
-third party application to streamline review processes
-
-Usage:
- marv [flags]
- marv [command]
-
-Available Commands:
- export exports framework output into standardised JSON
- frameworks lists all installed frameworks
- help Help about any command
- init initialises a new default marv.yml file
-
-Flags:
- -c, --config string .marv.yml file path
- -h, --help help for marv
- -m, --merge merges all frameworks output into one large json
- -o, --output string specifies the output path
- -p, --port string port to listen on
- -v, --version version for marv
+A simple guide of how to run Marv on a project for the first time. If at any point you need more information about
+one of the Marv commands, try using the help command.
+```cli
+marv help [command]
```
-## Gallery
+1. The first step is to ensure that Marv is correctly installed. If the Marv executable is correctly installed, running
+the Marv version command will output a version number. If an error is printed, then it likely means you need to add
+the Marv executable install location to your system path.
+```cli
+marv --version
+```
+
+2. Run the list command to see a `list` of all the frameworks that your installed version of Marv supports.
+```cli
+marv list
+```
+
+3. Then navigate to your project location and run the Marv `init` command with the list of frameworks you are using
+(Marv framework names are case-sensitive, so make sure to copy them correctly from the output of the `list` command).
+This will create a `.marv.yml` file in the directory that Marv was run in. The file will contain the default Marv
+configuration as well as a blank configuration for each framework that was listed.
+```cli
+marv init -f [framework] -f [framework] ...
+```
+
+4. Now fill in the configurations for each framework. Where frameworks require paths, using paths relative to a repository
+will allow you to safely commit the `.marv.yml` file for others to use. When finished with the configurations, simply
+run `marv`.
+```cli
+marv
+```
+
+5. If you have correctly configured the frameworks then that is it! Provided you keep the `.marv.yml` configuration
+file then all you have to do in future is simply run `marv`.
-Screenshots of the Marv user interface showing results from:
+## Gallery
-* [mutest-rs](https://github.com/zalanlevai/mutest-rs) run on [alacritty](https://github.com/alacritty/alacritty)
-* [hcoles/pitest](https://github.com/hcoles/pitest) run on [guava](https://github.com/google/guava)
+Screenshots of the Marv user interface showing results from various frameworks.
-| | |
-|---------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
-| **Marv Results Overview:** Showing results from `mutest-rs` and `Pitest`
 | **Marv Pitest Results:** Showing `Pitest` mutants inline with a file from guava
 |
-| **Marv mutest-rs Results:** Showing `mutest-rs` mutants inline with a file from alacritty
 | **Marv Pitest Mutant:** Showing an isolated `Pitest` mutant inline with a file from guava
 |
+| | |
+|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Marv Results Overview:** Showing results from [stryker-net](https://github.com/stryker-mutator/stryker-net) run on itself
 | **Marv Pitest Results:** Showing [hcoles/pitest](https://github.com/hcoles/pitest) mutants inline with a file from [guava](https://github.com/google/guava)
 |
+| **Marv mutest-rs Results:** Showing [mutest-rs](https://github.com/zalanlevai/mutest-rs) mutants inline with a file from [alacritty](https://github.com/alacritty/alacritty)
 | **Marv Infection PHP Mutant:** Showing an isolated [Infection](https://github.com/infection/infection) mutant inline with a file from its own source
 |
## Export Format
@@ -141,8 +123,8 @@ By using the `-m` or `--merge` flags, the results from all frameworks are merged
### Mutations Format
-The mutations format follows the internal structures defined in [`internal/mutations`](internal/mutations). The basic
-structure is `file path` > `conflict region` > `mutation`. Marv uses `conflict regions` or internally called
+The mutations format follows the internal structures defined in [`internal/mutations`](internal/mutations/mutations.go).
+The basic structure is `file path` > `conflict region` > `mutation`. Marv uses `conflict regions` or internally called
`mutations.Conflict` to wrap all mutations that would conflict with each other if just rendered inline due to overlaps.
Any `ID` field is a UUID created by Marv. Where frameworks create mutant identifiers, they are stored against the mutant
@@ -180,6 +162,9 @@ as `FrameworkMutantID`.
### Reviews Format
+Reviews are exported against the corresponding mutations Marv Mutation ID and their Framework Mutation
+ID (if applicable). The review structure is defined in [`internal/review`](internal/review/review.go).
+
```json
[
{
@@ -189,4 +174,8 @@ as `FrameworkMutantID`.
"Review": "An example review"
}
]
-```
\ No newline at end of file
+```
+
+## Other
+
+[Icon Licenses (icons.md)](icons.md)
\ No newline at end of file
diff --git a/docs/marv_infection_php_mutant.png b/docs/marv_infection_php_mutant.png
new file mode 100644
index 0000000..46b1f66
Binary files /dev/null and b/docs/marv_infection_php_mutant.png differ
diff --git a/docs/marv_pitest_guava.png b/docs/marv_pitest_guava.png
index c564a65..b593c0e 100644
Binary files a/docs/marv_pitest_guava.png and b/docs/marv_pitest_guava.png differ
diff --git a/docs/marv_pitest_guava_mutant.png b/docs/marv_pitest_guava_mutant.png
deleted file mode 100644
index 84b26d8..0000000
Binary files a/docs/marv_pitest_guava_mutant.png and /dev/null differ
diff --git a/docs/marv_results_overview.png b/docs/marv_results_overview.png
index 5478831..06c4b76 100644
Binary files a/docs/marv_results_overview.png and b/docs/marv_results_overview.png differ
diff --git a/fwlib/fwlib.go b/fwlib/fwlib.go
index 8aa93f6..def951b 100644
--- a/fwlib/fwlib.go
+++ b/fwlib/fwlib.go
@@ -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 {
@@ -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()
+}
diff --git a/fws/fws.go b/fws/fws.go
index 1952179..0eccc55 100644
--- a/fws/fws.go
+++ b/fws/fws.go
@@ -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(),
}
}
diff --git a/fws/infection/README.md b/fws/infection/README.md
new file mode 100644
index 0000000..bcaaa37
--- /dev/null
+++ b/fws/infection/README.md
@@ -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.
+
+
+
+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.
+
+
\ No newline at end of file
diff --git a/fws/infection/docs/marv_formatting_correction.png b/fws/infection/docs/marv_formatting_correction.png
new file mode 100644
index 0000000..5515c09
Binary files /dev/null and b/fws/infection/docs/marv_formatting_correction.png differ
diff --git a/fws/infection/docs/mte_incorrect_formatting.png b/fws/infection/docs/mte_incorrect_formatting.png
new file mode 100644
index 0000000..4da93fa
Binary files /dev/null and b/fws/infection/docs/mte_incorrect_formatting.png differ
diff --git a/fws/infection/infection.go b/fws/infection/infection.go
new file mode 100644
index 0000000..bb19fa7
--- /dev/null
+++ b/fws/infection/infection.go
@@ -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
+}
diff --git a/fws/mull/mull.go b/fws/mull/mull.go
new file mode 100644
index 0000000..a0653e6
--- /dev/null
+++ b/fws/mull/mull.go
@@ -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)
+ }
+ }
+}
+
+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
+}
diff --git a/fws/mutest_rs/mutest_rs.go b/fws/mutest_rs/mutest_rs.go
index f23bb56..859223c 100644
--- a/fws/mutest_rs/mutest_rs.go
+++ b/fws/mutest_rs/mutest_rs.go
@@ -3,7 +3,6 @@ package mutest_rs
import (
"encoding/json"
"errors"
- "fmt"
"os"
"path"
"strconv"
@@ -134,18 +133,12 @@ func (m *MutestRS) LoadResults() error {
func (m *MutestRS) TransformResults() error {
log.Info().Msgf("%s - transforming results", m.Meta().Name)
- bar := progressbar.NewOptions(
- len(m.muts.Mutations),
- progressbar.OptionSetWriter(os.Stdout),
- progressbar.OptionSetDescription("transforming"),
- progressbar.OptionSetRenderBlankState(true))
-
+ bar := fwlib.NewProgressbar(len(m.muts.Mutations), "transforming")
ms, err := m.transformResults(bar)
if err != nil {
return err
}
- bar.Finish()
- fmt.Println()
+ fwlib.FinishProgressbar(bar)
m.ms = ms
return nil
diff --git a/fws/pitest/README.md b/fws/pitest/README.md
new file mode 100644
index 0000000..8a63145
--- /dev/null
+++ b/fws/pitest/README.md
@@ -0,0 +1,29 @@
+### Pitest
+
+Pitest must be run with the `-Dfeatures="+EXPORT"` flag which exports the mutated class files. This is required because
+Marv will decompile these class files to construct each mutants replacement string.
+
+```cli
+mvn org.pitest:pitest-maven:mutationCoverage -Dfeatures="+EXPORT
+```
+
+> [!NOTE]
+> The replacement strings (inserted lines) that Marv produces are correct, however they are occasionally flanked by
+> incorrectly formatted deleted lines due to formatting differences between the source code and decompiled class code.
+
+A new Marv Pitest configuration can be created by running the `marv init -f Pitest` command.
+
+#### Decompilers
+
+Marv has a range of decompiler options that can be used with to construct the Pitest mutant replacement strings. They
+are listed below.
+
+> [!CAUTION]
+> The `garlic` decompiler is currently unstable and using it could cause some mutants to be skipped due to a
+> segmentation fault that occurs when running `garlic` on some class files.
+
+* [vineflower-server](https://github.com/SecretSheppy/vineflower-server) (recommended)
+* [vineflower](https://github.com/Vineflower/vineflower)
+* [garlic](https://github.com/neocanable/garlic)
+
+For installation location see [Installation - Libraries](../../README.md#libraries)
diff --git a/fws/pitest/pitest.go b/fws/pitest/pitest.go
index 5b75361..557954c 100644
--- a/fws/pitest/pitest.go
+++ b/fws/pitest/pitest.go
@@ -13,7 +13,6 @@ import (
"github.com/SecretSheppy/marv/internal/mutations"
"github.com/SecretSheppy/marv/pkg/fio"
"github.com/rs/zerolog/log"
- "github.com/schollz/progressbar/v3"
"gopkg.in/yaml.v3"
)
@@ -147,45 +146,26 @@ func (p *Pitest) LoadResults() error {
func (p *Pitest) TransformResults() error {
log.Info().Msgf("%s - transforming results", p.Meta().Name)
- groupBar := progressbar.NewOptions(
- len(p.muts),
- progressbar.OptionSetWriter(os.Stdout),
- progressbar.OptionSetDescription("[1/3] grouping"),
- progressbar.OptionSetRenderBlankState(true),
- progressbar.OptionShowCount())
+ groupBar := fwlib.NewProgressbar(len(p.muts), "[1/3] grouping")
fileMutations := p.groupMutants(groupBar)
- groupBar.Finish()
- fmt.Println()
-
- indexBar := progressbar.NewOptions(
- len(p.muts),
- progressbar.OptionSetWriter(os.Stdout),
- progressbar.OptionSetDescription("[2/3] indexing"),
- progressbar.OptionSetRenderBlankState(true),
- progressbar.OptionShowCount())
- err := p.indexMutants(fileMutations, indexBar)
- if err != nil {
+ fwlib.FinishProgressbar(groupBar)
+
+ indexBar := fwlib.NewProgressbar(len(p.muts), "[2/3] indexing")
+ if err := p.indexMutants(fileMutations, indexBar); err != nil {
return err
}
- indexBar.Finish()
- fmt.Println()
+ fwlib.FinishProgressbar(indexBar)
log.Info().Msgf("%s - using %s", p.Meta().Name, p.dcomp)
if err := p.dcomp.Setup(); err != nil {
return err
}
- transformBar := progressbar.NewOptions(
- len(fileMutations),
- progressbar.OptionSetWriter(os.Stdout),
- progressbar.OptionSetDescription("[3/3] transforming"),
- progressbar.OptionSetRenderBlankState(true),
- progressbar.OptionShowCount())
+ transformBar := fwlib.NewProgressbar(len(fileMutations), "[3/3] transforming")
var errs []error
p.ms, errs = transform(p, fileMutations, transformBar)
// NOTE: perform stdout cleanup before printing errors.
- transformBar.Finish()
- fmt.Println()
+ fwlib.FinishProgressbar(transformBar)
if len(errs) > 0 {
for _, err := range errs {
err.(*transformError).log()
diff --git a/fws/stryker4s/stryker4s.go b/fws/stryker4s/stryker4s.go
new file mode 100644
index 0000000..a90982f
--- /dev/null
+++ b/fws/stryker4s/stryker4s.go
@@ -0,0 +1,80 @@
+package stryker4s
+
+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: "stryker4s",
+ Language: languages.Scala,
+ URL: "https://github.com/stryker-mutator/stryker4s",
+}
+
+type YamlConfig struct {
+ MTEJson string `yaml:"mte-json"`
+}
+
+type YamlWrapper struct {
+ Cfg *YamlConfig `yaml:"stryker4s"`
+}
+
+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 Stryker4s struct {
+ yml *YamlWrapper
+ mte *mtelib.MTE
+}
+
+func NewStryker4s() *Stryker4s {
+ return &Stryker4s{yml: &YamlWrapper{}}
+}
+
+func (s *Stryker4s) Meta() *fwlib.Meta {
+ return &meta
+}
+
+func (s *Stryker4s) Yaml() fwlib.FWConfig {
+ return s.yml
+}
+
+func (s *Stryker4s) LoadResults() error {
+ log.Info().Msgf("%s - loading results", s.Meta().Name)
+ var err error
+ s.mte, err = mtelib.NewMTE(s.yml.Cfg.MTEJson)
+ return err
+}
+
+func (s *Stryker4s) TransformResults() error {
+ log.Info().Msgf("%s - transforming results", s.Meta().Name)
+
+ bar := fwlib.NewProgressbar(s.mte.RawMutationsCount(), "transforming")
+ s.mte.Transform(bar)
+ fwlib.FinishProgressbar(bar)
+
+ return nil
+}
+
+func (s *Stryker4s) Mutations() mutations.Mutations {
+ return s.mte.Mutations()
+}
+
+func (s *Stryker4s) ReadLines(file string) ([]string, error) {
+ return s.mte.ReadLines(file), nil
+}
diff --git a/fws/stryker_js/stryker_javascript.go b/fws/stryker_js/stryker_javascript.go
new file mode 100644
index 0000000..90112db
--- /dev/null
+++ b/fws/stryker_js/stryker_javascript.go
@@ -0,0 +1,84 @@
+package stryker_js
+
+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: "stryker-js",
+ Language: languages.JavaScript,
+ URL: "https://github.com/stryker-mutator/stryker-net",
+}
+
+type YamlConfig struct {
+ MTEJson string `yaml:"mte-json"`
+ Language string `yaml:"language"`
+}
+
+type YamlWrapper struct {
+ Cfg *YamlConfig `yaml:"stryker-js"`
+}
+
+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
+ }
+ if y.Cfg.Language == "ts" || y.Cfg.Language == "typescript" {
+ meta.Language = languages.TypeScript
+ }
+ return y.Cfg.MTEJson != "" && y.Cfg.Language != "", nil
+}
+
+type StrykerJS struct {
+ yml *YamlWrapper
+ mte *mtelib.MTE
+}
+
+func NewStrykerJS() *StrykerJS {
+ return &StrykerJS{yml: &YamlWrapper{}}
+}
+
+func (s *StrykerJS) Meta() *fwlib.Meta {
+ return &meta
+}
+
+func (s *StrykerJS) Yaml() fwlib.FWConfig {
+ return s.yml
+}
+
+func (s *StrykerJS) LoadResults() error {
+ log.Info().Msgf("%s - loading results", s.Meta().Name)
+ var err error
+ s.mte, err = mtelib.NewMTE(s.yml.Cfg.MTEJson)
+ return err
+}
+
+func (s *StrykerJS) TransformResults() error {
+ log.Info().Msgf("%s - transforming results", s.Meta().Name)
+
+ bar := fwlib.NewProgressbar(s.mte.RawMutationsCount(), "transforming")
+ s.mte.Transform(bar)
+ fwlib.FinishProgressbar(bar)
+
+ return nil
+}
+
+func (s *StrykerJS) Mutations() mutations.Mutations {
+ return s.mte.Mutations()
+}
+
+func (s *StrykerJS) ReadLines(file string) ([]string, error) {
+ return s.mte.ReadLines(file), nil
+}
diff --git a/fws/stryker_net/stryker_net.go b/fws/stryker_net/stryker_net.go
new file mode 100644
index 0000000..3d9c464
--- /dev/null
+++ b/fws/stryker_net/stryker_net.go
@@ -0,0 +1,80 @@
+package stryker_net
+
+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: "stryker-net",
+ Language: languages.CSharp,
+ URL: "https://github.com/stryker-mutator/stryker-net",
+}
+
+type YamlConfig struct {
+ MTEJson string `yaml:"mte-json"`
+}
+
+type YamlWrapper struct {
+ Cfg *YamlConfig `yaml:"stryker-net"`
+}
+
+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 StrykerNet struct {
+ yml *YamlWrapper
+ mte *mtelib.MTE
+}
+
+func NewStrykerNet() *StrykerNet {
+ return &StrykerNet{yml: &YamlWrapper{}}
+}
+
+func (s *StrykerNet) Meta() *fwlib.Meta {
+ return &meta
+}
+
+func (s *StrykerNet) Yaml() fwlib.FWConfig {
+ return s.yml
+}
+
+func (s *StrykerNet) LoadResults() error {
+ log.Info().Msgf("%s - loading results", s.Meta().Name)
+ var err error
+ s.mte, err = mtelib.NewMTE(s.yml.Cfg.MTEJson)
+ return err
+}
+
+func (s *StrykerNet) TransformResults() error {
+ log.Info().Msgf("%s - transforming results", s.Meta().Name)
+
+ bar := fwlib.NewProgressbar(s.mte.RawMutationsCount(), "transforming")
+ s.mte.Transform(bar)
+ fwlib.FinishProgressbar(bar)
+
+ return nil
+}
+
+func (s *StrykerNet) Mutations() mutations.Mutations {
+ return s.mte.Mutations()
+}
+
+func (s *StrykerNet) ReadLines(file string) ([]string, error) {
+ return s.mte.ReadLines(file), nil
+}
diff --git a/icons.md b/icons.md
new file mode 100644
index 0000000..d5fdcb1
--- /dev/null
+++ b/icons.md
@@ -0,0 +1,25 @@
+## Language Icon Licenses
+
+* C++ [Terms of Use](https://isocpp.org/home/terms-of-use)
+* Dotnet [CC0 1.0 Universal license](https://github.com/dotnet/brand)
+* Go [Fair Use](https://go.dev/brand)
+* Java (Duke) [BSD License](https://www.oracle.com/a/ocom/docs/java-licensing-logo-guidelines-1908204.pdf)
+* JavaScript [MIT](https://github.com/voodootikigod/logo.js/)
+* Php [CC BY-SA 4.0](https://www.php.net/download-logos.php)
+* Rust [CC-BY](https://github.com/rust-lang/rust-artwork/blob/master/logo/README.md)
+* TypeScript [Branding Guidlines](https://www.typescriptlang.org/branding/)
+
+For other languages see [Custom Language Icons](#custom-language-icons)
+
+## Font Awesome
+
+All Font Awesome svg icons are used under the [Font Awesome Free License](https://fontawesome.com/license/free).
+
+## Marv UI Icons
+
+Marv contains a swathe of icons that have been created specifically for its UI. These icons are available for use
+under the same [BSD 3-Clause License](LICENSE) that Marv is.
+
+### Custom Language Icons
+
+* [Scala Icon](web/static/languages/marv_scala_icon.png) is custom-made due to lack of available license for official Scala logo.
diff --git a/internal/cmds/list.go b/internal/cmds/list.go
index 0ccd17e..e4e47da 100644
--- a/internal/cmds/list.go
+++ b/internal/cmds/list.go
@@ -11,8 +11,8 @@ import (
)
var listCmd = &cobra.Command{
- Use: "frameworks",
- Aliases: []string{"list"},
+ Use: "list",
+ Aliases: []string{"frameworks"},
Short: "lists all installed frameworks",
Long: "lists all installed frameworks by name",
Run: func(cmd *cobra.Command, args []string) {
diff --git a/internal/cmds/root.go b/internal/cmds/root.go
index 1d6bf07..e0ca643 100644
--- a/internal/cmds/root.go
+++ b/internal/cmds/root.go
@@ -86,7 +86,7 @@ func mergeFlagsWithConfig(cfg *config.Config) error {
return nil
}
-func transformMutations(activeFws []fwlib.Framework) error {
+func transformMutations(conf *config.Config, activeFws []fwlib.Framework) error {
for _, fw := range activeFws {
if decompiling, ok := fw.(fwlib.Decompiling); ok {
decompiling.SetDecompiler()
@@ -96,11 +96,31 @@ func transformMutations(activeFws []fwlib.Framework) error {
return err
}
+ if err := extractBrokenMutations(conf, fw); err != nil {
+ return err
+ }
+
+ fw.Mutations().MergeConflicting()
fw.Mutations().GenerateIDs()
}
return nil
}
+// extracts and removes broken mutations (se IsBroken method on mutations.Mutation)
+func extractBrokenMutations(conf *config.Config, fw fwlib.Framework) error {
+ broken := fw.Mutations().ExtractBrokenMutations()
+ if len(broken) > 0 {
+ out := path.Join(conf.Marv.Output.Path, fw.Meta().Name+"-broken.json")
+ marshal, err := json.Marshal(broken)
+ if err != nil {
+ return err
+ }
+ os.WriteFile(out, marshal, 0644)
+ log.Warn().Msgf("%s - extracted %d broken mutations and dumped them in %s", fw.Meta().Name, len(broken), out)
+ }
+ return nil
+}
+
func export(conf *config.Config, activeFws []fwlib.Framework) error {
for _, fw := range activeFws {
marshal, err := json.Marshal(fw.Mutations())
@@ -186,7 +206,7 @@ func exportCommand() (*config.Config, []fwlib.Framework) {
os.Exit(1)
}
- if err := transformMutations(activeFws); err != nil {
+ if err := transformMutations(conf, activeFws); err != nil {
log.Fatal().Err(err).Msg("Failed to transform results")
os.Exit(1)
}
diff --git a/internal/html/code.go b/internal/html/code.go
index 6c9bdc9..6cc6664 100644
--- a/internal/html/code.go
+++ b/internal/html/code.go
@@ -263,7 +263,7 @@ func (r *codeRenderer) highlightMutationParts(pre, diff, post string) ([]string,
func (r *codeRenderer) renderMutationHeader(buff *bytes.Buffer, m *mutations.Mutation) {
buff.WriteString(" |