├── .vscode └── settings.json ├── example ├── sub │ ├── sub.go │ └── sub_test.go ├── b.go ├── example_test.go ├── a.go └── example.go ├── mutator ├── mutation.go ├── branch │ ├── mutateif_test.go │ ├── mutatecase_test.go │ ├── mutateelse_test.go │ ├── mutatecase.go │ ├── mutateif.go │ └── mutateelse.go ├── expression │ ├── remove_test.go │ ├── comparison_test.go │ ├── comparison.go │ └── remove.go ├── statement │ ├── remove_test.go │ └── remove.go ├── mutator_test.go └── mutator.go ├── parse_test.go ├── cmd └── go-mutesting │ ├── go-mutesting-bash_completion.sh │ ├── main_test.go │ └── main.go ├── testdata ├── branch │ ├── mutateif.go │ ├── mutateelse.go │ ├── mutateelse.go.0.go │ ├── mutateif.go.1.go │ ├── mutatecase.go │ ├── mutateif.go.0.go │ ├── mutatecase.go.0.go │ ├── mutatecase.go.1.go │ └── mutatecase.go.2.go ├── expression │ ├── remove.go.2.go │ ├── remove.go │ ├── remove.go.0.go │ ├── remove.go.1.go │ ├── remove.go.3.go │ ├── remove.go.4.go │ ├── remove.go.5.go │ ├── comparison.go │ ├── comparison.go.2.go │ ├── comparison.go.3.go │ ├── comparison.go.0.go │ └── comparison.go.1.go └── statement │ ├── remove.go.16.go │ ├── remove.go.14.go │ ├── remove.go.2.go │ ├── remove.go.3.go │ ├── remove.go.1.go │ ├── remove.go │ ├── remove.go.0.go │ ├── remove.go.4.go │ ├── remove.go.5.go │ ├── remove.go.12.go │ ├── remove.go.13.go │ ├── remove.go.15.go │ ├── remove.go.6.go │ ├── remove.go.7.go │ ├── remove.go.8.go │ ├── remove.go.9.go │ ├── remove.go.10.go │ └── remove.go.11.go ├── .gitignore ├── .travis.yml ├── go.mod ├── astutil ├── create.go └── query.go ├── scripts ├── lint.sh └── exec │ ├── test-current-directory.sh │ └── test-mutated-package.sh ├── LICENSE ├── Makefile ├── test └── mutator.go ├── parse.go ├── walk.go ├── go.sum └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.formatFlags": [ 3 | "-s" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/sub/sub.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | func baz() int { 4 | i := 1 5 | i = i + i 6 | 7 | return i 8 | } 9 | -------------------------------------------------------------------------------- /example/b.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func fooB() (a A, b http.Header) { 8 | a, b = A{}, http.Header{} 9 | 10 | return a, b 11 | } 12 | -------------------------------------------------------------------------------- /example/sub/sub_test.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBaz(t *testing.T) { 10 | Equal(t, baz(), 2) 11 | } 12 | -------------------------------------------------------------------------------- /example/example_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFoo(t *testing.T) { 10 | Equal(t, foo(), 16) 11 | } 12 | -------------------------------------------------------------------------------- /example/a.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type A struct{} 8 | 9 | func fooA() (a A, b http.Header) { 10 | _, _, _ = a, b, http.Header{} 11 | 12 | return a, b 13 | } 14 | -------------------------------------------------------------------------------- /mutator/mutation.go: -------------------------------------------------------------------------------- 1 | package mutator 2 | 3 | // Mutation defines the behavior of one mutation 4 | type Mutation struct { 5 | // Change is called before executing the exec command. 6 | Change func() 7 | // Reset is called after executing the exec command. 8 | Reset func() 9 | } 10 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package mutesting 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParseAndTypeCheckFileTypeCheckWholePackage(t *testing.T) { 10 | _, _, _, _, err := ParseAndTypeCheckFile("astutil/create.go") 11 | assert.Nil(t, err) 12 | } 13 | -------------------------------------------------------------------------------- /cmd/go-mutesting/go-mutesting-bash_completion.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | _go_mutesting() { 4 | args=("${COMP_WORDS[@]:1:$COMP_CWORD}") 5 | 6 | local IFS=$'\n' 7 | COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) 8 | return 1 9 | } 10 | 11 | complete -F _go_mutesting go-mutesting 12 | -------------------------------------------------------------------------------- /mutator/branch/mutateif_test.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zimmski/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorIf(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorIf, 13 | "../../testdata/branch/mutateif.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/branch/mutatecase_test.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zimmski/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorCase(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorCase, 13 | "../../testdata/branch/mutatecase.go", 14 | 3, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/branch/mutateelse_test.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zimmski/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorElse(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorElse, 13 | "../../testdata/branch/mutateelse.go", 14 | 1, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/expression/remove_test.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zimmski/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorRemoveTerm(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorRemoveTerm, 13 | "../../testdata/expression/remove.go", 14 | 6, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/expression/comparison_test.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zimmski/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorComparison(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorComparison, 13 | "../../testdata/expression/comparison.go", 14 | 4, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/statement/remove_test.go: -------------------------------------------------------------------------------- 1 | package statement 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zimmski/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorRemoveStatement(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorRemoveStatement, 13 | "../../testdata/statement/remove.go", 14 | 17, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/branch/mutateif.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i == 1 { 14 | fmt.Println(i) 15 | } else if i == 2 { 16 | fmt.Println(i * 2) 17 | } else { 18 | fmt.Println(i * 3) 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/branch/mutateelse.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i == 1 { 14 | fmt.Println(i) 15 | } else if i == 2 { 16 | fmt.Println(i * 2) 17 | } else { 18 | fmt.Println(i * 3) 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/branch/mutateelse.go.0.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i == 1 { 14 | fmt.Println(i) 15 | } else if i == 2 { 16 | fmt.Println(i * 2) 17 | } else { 18 | _, _ = fmt.Println, i 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/branch/mutateif.go.1.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i == 1 { 14 | fmt.Println(i) 15 | } else if i == 2 { 16 | _, _ = fmt.Println, i 17 | } else { 18 | fmt.Println(i * 3) 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.2.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i >= 1 && i <= 1 { 14 | fmt.Println(i) 15 | } else if false || i*1 == 1+1 { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | switch { 14 | case i == 1: 15 | fmt.Println(i) 16 | case i == 2: 17 | fmt.Println(i * 2) 18 | default: 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutateif.go.0.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i == 1 { 14 | _, _ = fmt.Println, i 15 | } else if i == 2 { 16 | fmt.Println(i * 2) 17 | } else { 18 | fmt.Println(i * 3) 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i >= 1 && i <= 1 { 14 | fmt.Println(i) 15 | } else if (i >= 2 && i <= 2) || i*1 == 1+1 { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.0.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if true && i <= 1 { 14 | fmt.Println(i) 15 | } else if (i >= 2 && i <= 2) || i*1 == 1+1 { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.1.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i >= 1 && true { 14 | fmt.Println(i) 15 | } else if (i >= 2 && i <= 2) || i*1 == 1+1 { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.3.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i >= 1 && i <= 1 { 14 | fmt.Println(i) 15 | } else if (i >= 2 && i <= 2) || false { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.4.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i >= 1 && i <= 1 { 14 | fmt.Println(i) 15 | } else if (true && i <= 2) || i*1 == 1+1 { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.5.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | if i >= 1 && i <= 1 { 14 | fmt.Println(i) 15 | } else if (i >= 2 && true) || i*1 == 1+1 { 16 | fmt.Println(i * 2) 17 | } else { 18 | 19 | } 20 | 21 | i++ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go.0.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | switch { 14 | case i == 1: 15 | _, _ = fmt.Println, i 16 | case i == 2: 17 | fmt.Println(i * 2) 18 | default: 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go.1.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | switch { 14 | case i == 1: 15 | fmt.Println(i) 16 | case i == 2: 17 | _, _ = fmt.Println, i 18 | default: 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go.2.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func main() { 10 | i := 1 11 | 12 | for i != 4 { 13 | switch { 14 | case i == 1: 15 | fmt.Println(i) 16 | case i == 2: 17 | fmt.Println(i * 2) 18 | default: 19 | _, _ = fmt.Println, i 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | go: 8 | - 1.11.x 9 | - 1.12.x 10 | 11 | env: 12 | global: 13 | - GO111MODULE: "on" 14 | 15 | install: 16 | - make install-dependencies 17 | - make install-tools 18 | - make install 19 | 20 | script: 21 | - make lint 22 | - make test-verbose-with-coverage 23 | - gover 24 | - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then goveralls -coverprofile=gover.coverprofile -service=travis-ci; fi 25 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func main() { 8 | if 1 > 2 { 9 | fmt.Printf("1 is greater than 2!") 10 | } 11 | 12 | if 1 < 2 { 13 | fmt.Printf("1 is less than 2!") 14 | } 15 | 16 | if 1 <= 2 { 17 | fmt.Printf("1 is less than or equal to 2!") 18 | } 19 | 20 | if 1 >= 2 { 21 | fmt.Printf("1 is greater than or equal to 2!") 22 | } 23 | 24 | if 1 == 2 { 25 | fmt.Print("1 is equal to 2!") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.2.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func main() { 8 | if 1 > 2 { 9 | fmt.Printf("1 is greater than 2!") 10 | } 11 | 12 | if 1 < 2 { 13 | fmt.Printf("1 is less than 2!") 14 | } 15 | 16 | if 1 < 2 { 17 | fmt.Printf("1 is less than or equal to 2!") 18 | } 19 | 20 | if 1 >= 2 { 21 | fmt.Printf("1 is greater than or equal to 2!") 22 | } 23 | 24 | if 1 == 2 { 25 | fmt.Print("1 is equal to 2!") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.3.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func main() { 8 | if 1 > 2 { 9 | fmt.Printf("1 is greater than 2!") 10 | } 11 | 12 | if 1 < 2 { 13 | fmt.Printf("1 is less than 2!") 14 | } 15 | 16 | if 1 <= 2 { 17 | fmt.Printf("1 is less than or equal to 2!") 18 | } 19 | 20 | if 1 > 2 { 21 | fmt.Printf("1 is greater than or equal to 2!") 22 | } 23 | 24 | if 1 == 2 { 25 | fmt.Print("1 is equal to 2!") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.0.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func main() { 8 | if 1 >= 2 { 9 | fmt.Printf("1 is greater than 2!") 10 | } 11 | 12 | if 1 < 2 { 13 | fmt.Printf("1 is less than 2!") 14 | } 15 | 16 | if 1 <= 2 { 17 | fmt.Printf("1 is less than or equal to 2!") 18 | } 19 | 20 | if 1 >= 2 { 21 | fmt.Printf("1 is greater than or equal to 2!") 22 | } 23 | 24 | if 1 == 2 { 25 | fmt.Print("1 is equal to 2!") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.1.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func main() { 8 | if 1 > 2 { 9 | fmt.Printf("1 is greater than 2!") 10 | } 11 | 12 | if 1 <= 2 { 13 | fmt.Printf("1 is less than 2!") 14 | } 15 | 16 | if 1 <= 2 { 17 | fmt.Printf("1 is less than or equal to 2!") 18 | } 19 | 20 | if 1 >= 2 { 21 | fmt.Printf("1 is greater than or equal to 2!") 22 | } 23 | 24 | if 1 == 2 { 25 | fmt.Print("1 is equal to 2!") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | func foo() int { 4 | n := 1 5 | 6 | for i := 0; i < 3; i++ { 7 | if i == 0 { 8 | n++ 9 | } else if i*1 == 2-1 { 10 | n += 2 11 | } else { 12 | n += 3 13 | } 14 | 15 | n++ 16 | } 17 | 18 | if n < 0 { 19 | n = 0 20 | } 21 | 22 | n++ 23 | 24 | n += bar() 25 | 26 | bar() 27 | bar() 28 | 29 | switch { 30 | case n < 20: 31 | n++ 32 | case n > 20: 33 | n-- 34 | default: 35 | n = 0 36 | } 37 | 38 | skip := true 39 | if true { 40 | _ = skip 41 | } 42 | 43 | return n 44 | } 45 | 46 | func bar() int { 47 | return 4 48 | } 49 | 50 | func baz() int { 51 | i := 1 52 | i = i + i 53 | 54 | return i 55 | } 56 | -------------------------------------------------------------------------------- /mutator/branch/mutatecase.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/zimmski/go-mutesting/astutil" 8 | "github.com/zimmski/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("branch/case", MutatorCase) 13 | } 14 | 15 | // MutatorCase implements a mutator for case clauses. 16 | func MutatorCase(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.CaseClause) 18 | if !ok { 19 | return nil 20 | } 21 | 22 | old := n.Body 23 | 24 | return []mutator.Mutation{ 25 | { 26 | Change: func() { 27 | n.Body = []ast.Stmt{ 28 | astutil.CreateNoopOfStatements(pkg, info, n.Body), 29 | } 30 | }, 31 | Reset: func() { 32 | n.Body = old 33 | }, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mutator/branch/mutateif.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/zimmski/go-mutesting/astutil" 8 | "github.com/zimmski/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("branch/if", MutatorIf) 13 | } 14 | 15 | // MutatorIf implements a mutator for if and else if branches. 16 | func MutatorIf(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.IfStmt) 18 | if !ok { 19 | return nil 20 | } 21 | 22 | old := n.Body.List 23 | 24 | return []mutator.Mutation{ 25 | { 26 | Change: func() { 27 | n.Body.List = []ast.Stmt{ 28 | astutil.CreateNoopOfStatement(pkg, info, n.Body), 29 | } 30 | }, 31 | Reset: func() { 32 | n.Body.List = old 33 | }, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mutator/branch/mutateelse.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/zimmski/go-mutesting/astutil" 8 | "github.com/zimmski/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("branch/else", MutatorElse) 13 | } 14 | 15 | // MutatorElse implements a mutator for else branches. 16 | func MutatorElse(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.IfStmt) 18 | if !ok { 19 | return nil 20 | } 21 | // We ignore else ifs and nil blocks 22 | _, ok = n.Else.(*ast.IfStmt) 23 | if ok || n.Else == nil { 24 | return nil 25 | } 26 | 27 | old := n.Else 28 | 29 | return []mutator.Mutation{ 30 | { 31 | Change: func() { 32 | n.Else = astutil.CreateNoopOfStatement(pkg, info, old) 33 | }, 34 | Reset: func() { 35 | n.Else = old 36 | }, 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zimmski/go-mutesting 2 | 3 | go 1.10 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.0 7 | github.com/hpcloud/tail v1.0.0 // indirect 8 | github.com/jessevdk/go-flags v1.4.0 9 | github.com/kisielk/errcheck v1.2.0 // indirect 10 | github.com/mattn/goveralls v0.0.3 // indirect 11 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 // indirect 12 | github.com/onsi/ginkgo v1.10.2 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 14 | github.com/stretchr/testify v1.4.0 15 | github.com/zimmski/go-tool v0.0.0-20150119110811-2dfdc9ac8439 16 | github.com/zimmski/osutil v0.0.0-20190128123334-0d0b3ca231ac 17 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect 18 | golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a 19 | gopkg.in/fsnotify.v1 v1.4.7 // indirect 20 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 21 | gopkg.in/yaml.v2 v2.2.2 22 | honnef.co/go/tools v0.0.1-2019.2.3 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /mutator/expression/comparison.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/zimmski/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("expression/comparison", MutatorComparison) 13 | } 14 | 15 | var comparisonMutations = map[token.Token]token.Token{ 16 | token.LSS: token.LEQ, 17 | token.LEQ: token.LSS, 18 | token.GTR: token.GEQ, 19 | token.GEQ: token.GTR, 20 | } 21 | 22 | // MutatorComparison implements a mutator to change comparisons. 23 | func MutatorComparison(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 24 | n, ok := node.(*ast.BinaryExpr) 25 | if !ok { 26 | return nil 27 | } 28 | 29 | o := n.Op 30 | r, ok := comparisonMutations[n.Op] 31 | if !ok { 32 | return nil 33 | } 34 | 35 | return []mutator.Mutation{ 36 | { 37 | Change: func() { 38 | n.Op = r 39 | }, 40 | Reset: func() { 41 | n.Op = o 42 | }, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /astutil/create.go: -------------------------------------------------------------------------------- 1 | package astutil 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | ) 8 | 9 | // CreateNoopOfStatement creates a syntactically safe noop statement out of a given statement. 10 | func CreateNoopOfStatement(pkg *types.Package, info *types.Info, stmt ast.Stmt) ast.Stmt { 11 | return CreateNoopOfStatements(pkg, info, []ast.Stmt{stmt}) 12 | } 13 | 14 | // CreateNoopOfStatements creates a syntactically safe noop statement out of a given statement. 15 | func CreateNoopOfStatements(pkg *types.Package, info *types.Info, stmts []ast.Stmt) ast.Stmt { 16 | var ids []ast.Expr 17 | for _, stmt := range stmts { 18 | ids = append(ids, IdentifiersInStatement(pkg, info, stmt)...) 19 | } 20 | 21 | if len(ids) == 0 { 22 | return &ast.EmptyStmt{ 23 | Semicolon: token.NoPos, 24 | } 25 | } 26 | 27 | lhs := make([]ast.Expr, len(ids)) 28 | for i := range ids { 29 | lhs[i] = ast.NewIdent("_") 30 | } 31 | 32 | return &ast.AssignStmt{ 33 | Lhs: lhs, 34 | Rhs: ids, 35 | Tok: token.ASSIGN, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.16.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | _, _ = hdr, hash 69 | 70 | return hdr 71 | } 72 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.14.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.2.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | 35 | switch { 36 | case n < 20: 37 | n++ 38 | case n > 20: 39 | n-- 40 | default: 41 | n = 0 42 | fmt.Println(n) 43 | func() {}() 44 | } 45 | 46 | var x = 0 47 | x++ 48 | 49 | return n 50 | } 51 | 52 | func bar() int { 53 | return 4 54 | } 55 | 56 | func statementRemoveStructInitialization() (a http.Header, b error) { 57 | var err error 58 | 59 | a, b = http.Header{}, err 60 | 61 | return 62 | } 63 | 64 | func statementRemoveStringArrayMap() map[string][]string { 65 | hash := "ok" 66 | var hdr = make(map[string][]string) 67 | 68 | hdr["Hash"] = []string{hash} 69 | 70 | return hdr 71 | } 72 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.3.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | 35 | switch { 36 | case n < 20: 37 | n++ 38 | case n > 20: 39 | n-- 40 | default: 41 | n = 0 42 | fmt.Println(n) 43 | func() {}() 44 | } 45 | 46 | var x = 0 47 | x++ 48 | 49 | return n 50 | } 51 | 52 | func bar() int { 53 | return 4 54 | } 55 | 56 | func statementRemoveStructInitialization() (a http.Header, b error) { 57 | var err error 58 | 59 | a, b = http.Header{}, err 60 | 61 | return 62 | } 63 | 64 | func statementRemoveStringArrayMap() map[string][]string { 65 | hash := "ok" 66 | var hdr = make(map[string][]string) 67 | 68 | hdr["Hash"] = []string{hash} 69 | 70 | return hdr 71 | } 72 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.1.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | _ = n 31 | 32 | bar() 33 | bar() 34 | 35 | switch { 36 | case n < 20: 37 | n++ 38 | case n > 20: 39 | n-- 40 | default: 41 | n = 0 42 | fmt.Println(n) 43 | func() {}() 44 | } 45 | 46 | var x = 0 47 | x++ 48 | 49 | return n 50 | } 51 | 52 | func bar() int { 53 | return 4 54 | } 55 | 56 | func statementRemoveStructInitialization() (a http.Header, b error) { 57 | var err error 58 | 59 | a, b = http.Header{}, err 60 | 61 | return 62 | } 63 | 64 | func statementRemoveStringArrayMap() map[string][]string { 65 | hash := "ok" 66 | var hdr = make(map[string][]string) 67 | 68 | hdr["Hash"] = []string{hash} 69 | 70 | return hdr 71 | } 72 | -------------------------------------------------------------------------------- /testdata/statement/remove.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.0.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | _ = n 29 | 30 | n += bar() 31 | 32 | bar() 33 | bar() 34 | 35 | switch { 36 | case n < 20: 37 | n++ 38 | case n > 20: 39 | n-- 40 | default: 41 | n = 0 42 | fmt.Println(n) 43 | func() {}() 44 | } 45 | 46 | var x = 0 47 | x++ 48 | 49 | return n 50 | } 51 | 52 | func bar() int { 53 | return 4 54 | } 55 | 56 | func statementRemoveStructInitialization() (a http.Header, b error) { 57 | var err error 58 | 59 | a, b = http.Header{}, err 60 | 61 | return 62 | } 63 | 64 | func statementRemoveStringArrayMap() map[string][]string { 65 | hash := "ok" 66 | var hdr = make(map[string][]string) 67 | 68 | hdr["Hash"] = []string{hash} 69 | 70 | return hdr 71 | } 72 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.4.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | _ = x 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.5.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | _ = n 22 | 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.12.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | _ = n 43 | 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.13.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | _, _ = fmt.Println, n 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.15.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | _, _, _, _ = a, b, http.Header{}, err 60 | 61 | return 62 | } 63 | 64 | func statementRemoveStringArrayMap() map[string][]string { 65 | hash := "ok" 66 | var hdr = make(map[string][]string) 67 | 68 | hdr["Hash"] = []string{hash} 69 | 70 | return hdr 71 | } 72 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.6.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | _ = n 16 | 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.7.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | _ = n 18 | 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.8.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | _ = n 20 | 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.9.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | _ = n 27 | 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.10.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | _ = n 39 | 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.11.go: -------------------------------------------------------------------------------- 1 | // +build test 2 | 3 | package example 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func foo() int { 11 | n := 1 12 | 13 | for i := 0; i < 3; i++ { 14 | if i == 0 { 15 | n++ 16 | } else if i == 1 { 17 | n += 2 18 | } else { 19 | n += 3 20 | } 21 | 22 | n++ 23 | } 24 | 25 | if n < 0 { 26 | n = 0 27 | } 28 | 29 | n++ 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | _ = n 41 | 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z ${PKG+x} ]; then echo "PKG is not set"; exit 1; fi 4 | if [ -z ${ROOT_DIR+x} ]; then echo "ROOT_DIR is not set"; exit 1; fi 5 | 6 | echo "gofmt:" 7 | OUT=$(gofmt -l -s $ROOT_DIR 2>&1 | grep --invert-match -E "(/(example|vendor))") 8 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 9 | 10 | echo "errcheck:" 11 | OUT=$(errcheck $PKG/... 2>&1 | grep --invert-match -E "(/(example|vendor))") 12 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 13 | 14 | echo "go vet:" 15 | OUT=$(go vet -all=true $ROOT_DIR 2>&1 | grep --invert-match -E "(Checking file|\%p of wrong type|can't check non-constant format|/example|/vendor)") 16 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 17 | 18 | echo "golint:" 19 | OUT=$(golint $PKG/... 2>&1 | grep --invert-match -E "(/(example|vendor))") 20 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 21 | 22 | echo "staticcheck:" 23 | OUT=$(staticcheck $PKG/... 2>&1 | grep --invert-match -E "((example|vendor)/)") 24 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 25 | 26 | if [ -n "$PROBLEM" ]; then exit 1; fi 27 | -------------------------------------------------------------------------------- /mutator/expression/remove.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/zimmski/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("expression/remove", MutatorRemoveTerm) 13 | } 14 | 15 | // MutatorRemoveTerm implements a mutator to remove expression terms. 16 | func MutatorRemoveTerm(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.BinaryExpr) 18 | if !ok { 19 | return nil 20 | } 21 | if n.Op != token.LAND && n.Op != token.LOR { 22 | return nil 23 | } 24 | 25 | var r *ast.Ident 26 | 27 | switch n.Op { 28 | case token.LAND: 29 | r = ast.NewIdent("true") 30 | case token.LOR: 31 | r = ast.NewIdent("false") 32 | } 33 | 34 | x := n.X 35 | y := n.Y 36 | 37 | return []mutator.Mutation{ 38 | { 39 | Change: func() { 40 | n.X = r 41 | }, 42 | Reset: func() { 43 | n.X = x 44 | }, 45 | }, 46 | { 47 | Change: func() { 48 | n.Y = r 49 | }, 50 | Reset: func() { 51 | n.Y = y 52 | }, 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Markus Zimmermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /mutator/statement/remove.go: -------------------------------------------------------------------------------- 1 | package statement 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/zimmski/go-mutesting/astutil" 9 | "github.com/zimmski/go-mutesting/mutator" 10 | ) 11 | 12 | func init() { 13 | mutator.Register("statement/remove", MutatorRemoveStatement) 14 | } 15 | 16 | func checkRemoveStatement(node ast.Stmt) bool { 17 | switch n := node.(type) { 18 | case *ast.AssignStmt: 19 | if n.Tok != token.DEFINE { 20 | return true 21 | } 22 | case *ast.ExprStmt, *ast.IncDecStmt: 23 | return true 24 | } 25 | 26 | return false 27 | } 28 | 29 | // MutatorRemoveStatement implements a mutator to remove statements. 30 | func MutatorRemoveStatement(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 31 | var l []ast.Stmt 32 | 33 | switch n := node.(type) { 34 | case *ast.BlockStmt: 35 | l = n.List 36 | case *ast.CaseClause: 37 | l = n.Body 38 | } 39 | 40 | var mutations []mutator.Mutation 41 | 42 | for i, ni := range l { 43 | if checkRemoveStatement(ni) { 44 | li := i 45 | old := l[li] 46 | 47 | mutations = append(mutations, mutator.Mutation{ 48 | Change: func() { 49 | l[li] = astutil.CreateNoopOfStatement(pkg, info, old) 50 | }, 51 | Reset: func() { 52 | l[li] = old 53 | }, 54 | }) 55 | } 56 | } 57 | 58 | return mutations 59 | } 60 | -------------------------------------------------------------------------------- /mutator/mutator_test.go: -------------------------------------------------------------------------------- 1 | package mutator 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func mockMutator(pkg *types.Package, info *types.Info, node ast.Node) []Mutation { 12 | // Do nothing 13 | 14 | return nil 15 | } 16 | 17 | func TestMockMutator(t *testing.T) { 18 | // Mock is not registered 19 | for _, name := range List() { 20 | if name == "mock" { 21 | assert.Fail(t, "mock should not be in the mutator list yet") 22 | } 23 | } 24 | 25 | m, err := New("mock") 26 | assert.Nil(t, m) 27 | assert.NotNil(t, err) 28 | 29 | // Register mock 30 | Register("mock", mockMutator) 31 | 32 | // Mock is registered 33 | found := false 34 | for _, name := range List() { 35 | if name == "mock" { 36 | found = true 37 | 38 | break 39 | } 40 | } 41 | assert.True(t, found) 42 | 43 | m, err = New("mock") 44 | assert.NotNil(t, m) 45 | assert.Nil(t, err) 46 | 47 | // Register mock a second time 48 | caught := false 49 | func() { 50 | defer func() { 51 | if r := recover(); r != nil { 52 | caught = true 53 | } 54 | }() 55 | 56 | Register("mock", mockMutator) 57 | }() 58 | assert.True(t, caught) 59 | 60 | // Register nil function 61 | caught = false 62 | func() { 63 | defer func() { 64 | if r := recover(); r != nil { 65 | caught = true 66 | } 67 | }() 68 | 69 | Register("mockachino", nil) 70 | }() 71 | assert.True(t, caught) 72 | } 73 | -------------------------------------------------------------------------------- /mutator/mutator.go: -------------------------------------------------------------------------------- 1 | package mutator 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "sort" 8 | ) 9 | 10 | // Mutator defines a mutator for mutation testing by returning a list of possible mutations for the given node. 11 | type Mutator func(pkg *types.Package, info *types.Info, node ast.Node) []Mutation 12 | 13 | var mutatorLookup = make(map[string]Mutator) 14 | 15 | // New returns a new mutator instance given the registered name of the mutator. 16 | // The error return argument is not nil, if the name does not exist in the registered mutator list. 17 | func New(name string) (Mutator, error) { 18 | mutator, ok := mutatorLookup[name] 19 | if !ok { 20 | return nil, fmt.Errorf("unknown mutator %q", name) 21 | } 22 | 23 | return mutator, nil 24 | } 25 | 26 | // List returns a list of all registered mutator names. 27 | func List() []string { 28 | keyMutatorLookup := make([]string, 0, len(mutatorLookup)) 29 | 30 | for key := range mutatorLookup { 31 | keyMutatorLookup = append(keyMutatorLookup, key) 32 | } 33 | 34 | sort.Strings(keyMutatorLookup) 35 | 36 | return keyMutatorLookup 37 | } 38 | 39 | // Register registers a mutator instance function with the given name. 40 | func Register(name string, mutator Mutator) { 41 | if mutator == nil { 42 | panic("mutator function is nil") 43 | } 44 | 45 | if _, ok := mutatorLookup[name]; ok { 46 | panic(fmt.Sprintf("mutator %q already registered", name)) 47 | } 48 | 49 | mutatorLookup[name] = mutator 50 | } 51 | -------------------------------------------------------------------------------- /scripts/exec/test-current-directory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This exec script implements 4 | # - the replacement of the original file with the mutation, 5 | # - the execution of all tests originating from the current directory, 6 | # - and the reporting if the mutation was killed. 7 | 8 | if [ -z ${MUTATE_CHANGED+x} ]; then echo "MUTATE_CHANGED is not set"; exit 1; fi 9 | if [ -z ${MUTATE_ORIGINAL+x} ]; then echo "MUTATE_ORIGINAL is not set"; exit 1; fi 10 | if [ -z ${MUTATE_PACKAGE+x} ]; then echo "MUTATE_PACKAGE is not set"; exit 1; fi 11 | 12 | function clean_up { 13 | if [ -f $MUTATE_ORIGINAL.tmp ]; 14 | then 15 | mv $MUTATE_ORIGINAL.tmp $MUTATE_ORIGINAL 16 | fi 17 | } 18 | 19 | function sig_handler { 20 | clean_up 21 | 22 | exit $GOMUTESTING_RESULT 23 | } 24 | trap sig_handler SIGHUP SIGINT SIGTERM 25 | 26 | export GOMUTESTING_DIFF=$(diff -u $MUTATE_ORIGINAL $MUTATE_CHANGED) 27 | 28 | mv $MUTATE_ORIGINAL $MUTATE_ORIGINAL.tmp 29 | cp $MUTATE_CHANGED $MUTATE_ORIGINAL 30 | 31 | export MUTATE_TIMEOUT=${MUTATE_TIMEOUT:-10} 32 | 33 | if [ -n "$TEST_RECURSIVE" ]; then 34 | TEST_RECURSIVE="/..." 35 | fi 36 | 37 | GOMUTESTING_TEST=$(go test -timeout $(printf '%ds' $MUTATE_TIMEOUT) .$TEST_RECURSIVE 2>&1) 38 | export GOMUTESTING_RESULT=$? 39 | 40 | if [ "$MUTATE_DEBUG" = true ] ; then 41 | echo "$GOMUTESTING_TEST" 42 | fi 43 | 44 | clean_up 45 | 46 | case $GOMUTESTING_RESULT in 47 | 0) # tests passed -> FAIL 48 | echo "$GOMUTESTING_DIFF" 49 | 50 | exit 1 51 | ;; 52 | 1) # tests failed -> PASS 53 | if [ "$MUTATE_DEBUG" = true ] ; then 54 | echo "$GOMUTESTING_DIFF" 55 | fi 56 | 57 | exit 0 58 | ;; 59 | 2) # did not compile -> SKIP 60 | if [ "$MUTATE_VERBOSE" = true ] ; then 61 | echo "Mutation did not compile" 62 | fi 63 | 64 | if [ "$MUTATE_DEBUG" = true ] ; then 65 | echo "$GOMUTESTING_DIFF" 66 | fi 67 | 68 | exit 2 69 | ;; 70 | *) # Unkown exit code -> SKIP 71 | echo "Unknown exit code" 72 | echo "$GOMUTESTING_DIFF" 73 | 74 | exit $GOMUTESTING_RESULT 75 | ;; 76 | esac 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean clean-coverage generate install install-dependencies install-tools lint test test-verbose test-verbose-with-coverage 2 | 3 | export ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 4 | export PKG := github.com/zimmski/go-mutesting 5 | export ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 6 | 7 | export TEST_TIMEOUT_IN_SECONDS := 240 8 | 9 | $(eval $(ARGS):;@:) # turn arguments into do-nothing targets 10 | export ARGS 11 | 12 | ifdef ARGS 13 | PKG_TEST := $(ARGS) 14 | else 15 | PKG_TEST := $(PKG)/... 16 | endif 17 | 18 | all: install-dependencies install-tools install lint test 19 | .PHONY: all 20 | 21 | clean: 22 | go clean -i $(PKG)/... 23 | go clean -i -race $(PKG)/... 24 | .PHONY: clean 25 | 26 | clean-coverage: 27 | find $(ROOT_DIR) | grep .coverprofile | xargs rm 28 | .PHONY: clean-coverage 29 | 30 | generate: clean 31 | go generate $(PKG)/... 32 | .PHONY: generate 33 | 34 | install: 35 | go install -v $(PKG)/... 36 | .PHONY: install 37 | 38 | install-dependencies: 39 | go mod vendor 40 | go test -i -v $(PKG)/... 41 | .PHONY: install-dependencies 42 | 43 | install-tools: 44 | # generation 45 | go get golang.org/x/tools/cmd/stringer 46 | 47 | # linting 48 | go get golang.org/x/lint/golint/... 49 | go get github.com/kisielk/errcheck/... 50 | go get honnef.co/go/tools/... 51 | 52 | # code coverage 53 | go get golang.org/x/tools/cmd/cover 54 | go get github.com/onsi/ginkgo/ginkgo/... 55 | go get github.com/modocache/gover/... 56 | go get github.com/mattn/goveralls/... 57 | .PHONY: install-tools 58 | 59 | lint: 60 | $(ROOT_DIR)/scripts/lint.sh 61 | .PHONY: lint 62 | 63 | test: 64 | go test -race -test.timeout "$(TEST_TIMEOUT_IN_SECONDS)s" $(PKG_TEST) 65 | .PHONY: test 66 | 67 | test-verbose: 68 | go test -race -test.timeout "$(TEST_TIMEOUT_IN_SECONDS)s" -v $(PKG_TEST) 69 | .PHONY: test-verbose 70 | 71 | test-verbose-with-coverage: 72 | ginkgo -r -v -cover -race -skipPackage="testdata" 73 | .PHONY: test-verbose-with-coverage 74 | -------------------------------------------------------------------------------- /scripts/exec/test-mutated-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This exec script implements 4 | # - the replacement of the original file with the mutation, 5 | # - the execution of all tests originating from the package of the mutated file, 6 | # - and the reporting if the mutation was killed. 7 | 8 | if [ -z ${MUTATE_CHANGED+x} ]; then echo "MUTATE_CHANGED is not set"; exit 1; fi 9 | if [ -z ${MUTATE_ORIGINAL+x} ]; then echo "MUTATE_ORIGINAL is not set"; exit 1; fi 10 | if [ -z ${MUTATE_PACKAGE+x} ]; then echo "MUTATE_PACKAGE is not set"; exit 1; fi 11 | 12 | function clean_up { 13 | if [ -f $MUTATE_ORIGINAL.tmp ]; 14 | then 15 | mv $MUTATE_ORIGINAL.tmp $MUTATE_ORIGINAL 16 | fi 17 | } 18 | 19 | function sig_handler { 20 | clean_up 21 | 22 | exit $GOMUTESTING_RESULT 23 | } 24 | trap sig_handler SIGHUP SIGINT SIGTERM 25 | 26 | export GOMUTESTING_DIFF=$(diff -u $MUTATE_ORIGINAL $MUTATE_CHANGED) 27 | 28 | mv $MUTATE_ORIGINAL $MUTATE_ORIGINAL.tmp 29 | cp $MUTATE_CHANGED $MUTATE_ORIGINAL 30 | 31 | export MUTATE_TIMEOUT=${MUTATE_TIMEOUT:-10} 32 | 33 | if [ -n "$TEST_RECURSIVE" ]; then 34 | TEST_RECURSIVE="/..." 35 | fi 36 | 37 | GOMUTESTING_TEST=$(go test -timeout $(printf '%ds' $MUTATE_TIMEOUT) $MUTATE_PACKAGE$TEST_RECURSIVE 2>&1) 38 | export GOMUTESTING_RESULT=$? 39 | 40 | if [ "$MUTATE_DEBUG" = true ] ; then 41 | echo "$GOMUTESTING_TEST" 42 | fi 43 | 44 | clean_up 45 | 46 | case $GOMUTESTING_RESULT in 47 | 0) # tests passed -> FAIL 48 | echo "$GOMUTESTING_DIFF" 49 | 50 | exit 1 51 | ;; 52 | 1) # tests failed -> PASS 53 | if [ "$MUTATE_DEBUG" = true ] ; then 54 | echo "$GOMUTESTING_DIFF" 55 | fi 56 | 57 | exit 0 58 | ;; 59 | 2) # did not compile -> SKIP 60 | if [ "$MUTATE_VERBOSE" = true ] ; then 61 | echo "Mutation did not compile" 62 | fi 63 | 64 | if [ "$MUTATE_DEBUG" = true ] ; then 65 | echo "$GOMUTESTING_DIFF" 66 | fi 67 | 68 | exit 2 69 | ;; 70 | *) # Unkown exit code -> SKIP 71 | echo "Unknown exit code" 72 | echo "$GOMUTESTING_DIFF" 73 | 74 | exit $GOMUTESTING_RESULT 75 | ;; 76 | esac 77 | -------------------------------------------------------------------------------- /test/mutator.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/printer" 7 | "io/ioutil" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/zimmski/go-mutesting" 13 | "github.com/zimmski/go-mutesting/mutator" 14 | ) 15 | 16 | // Mutator tests a mutator. 17 | // It mutates the given original file with the given mutator. Every mutation is then validated with the given changed file. The mutation overall count is validated with the given count. 18 | func Mutator(t *testing.T, m mutator.Mutator, testFile string, count int) { 19 | // Test if mutator is not nil 20 | assert.NotNil(t, m) 21 | 22 | // Read the origianl source code 23 | data, err := ioutil.ReadFile(testFile) 24 | assert.Nil(t, err) 25 | 26 | // Parse and type-check the original source code 27 | src, fset, pkg, info, err := mutesting.ParseAndTypeCheckFile(testFile, `-tags=test`) 28 | assert.Nil(t, err) 29 | 30 | // Mutate a non relevant node 31 | assert.Nil(t, m(pkg, info, src)) 32 | 33 | // Count the actual mutations 34 | n := mutesting.CountWalk(pkg, info, src, m) 35 | assert.Equal(t, count, n) 36 | 37 | // Mutate all relevant nodes -> test whole mutation process 38 | changed := mutesting.MutateWalk(pkg, info, src, m) 39 | 40 | for i := 0; i < count; i++ { 41 | assert.True(t, <-changed) 42 | 43 | buf := new(bytes.Buffer) 44 | err = printer.Fprint(buf, fset, src) 45 | assert.Nil(t, err) 46 | 47 | changedFilename := fmt.Sprintf("%s.%d.go", testFile, i) 48 | changedFile, err := ioutil.ReadFile(changedFilename) 49 | assert.Nil(t, err) 50 | 51 | if !assert.Equal(t, string(changedFile), buf.String(), fmt.Sprintf("For change file %q", changedFilename)) { 52 | err = ioutil.WriteFile(fmt.Sprintf("%s.%d.go.new", testFile, i), buf.Bytes(), 0644) 53 | assert.Nil(t, err) 54 | } 55 | 56 | changed <- true 57 | 58 | assert.True(t, <-changed) 59 | 60 | buf = new(bytes.Buffer) 61 | err = printer.Fprint(buf, fset, src) 62 | assert.Nil(t, err) 63 | 64 | assert.Equal(t, string(data), buf.String()) 65 | 66 | changed <- true 67 | } 68 | 69 | _, ok := <-changed 70 | assert.False(t, ok) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/go-mutesting/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMain(t *testing.T) { 13 | testMain( 14 | t, 15 | "../../example", 16 | []string{"--debug", "--exec-timeout", "1"}, 17 | returnOk, 18 | "The mutation score is 0.450000 (9 passed, 11 failed, 8 duplicated, 0 skipped, total is 20)", 19 | ) 20 | } 21 | 22 | func TestMainRecursive(t *testing.T) { 23 | testMain( 24 | t, 25 | "../../example", 26 | []string{"--debug", "--exec-timeout", "1", "./..."}, 27 | returnOk, 28 | "The mutation score is 0.476190 (10 passed, 11 failed, 8 duplicated, 0 skipped, total is 21)", 29 | ) 30 | } 31 | 32 | func TestMainFromOtherDirectory(t *testing.T) { 33 | testMain( 34 | t, 35 | "../..", 36 | []string{"--debug", "--exec-timeout", "1", "github.com/zimmski/go-mutesting/example"}, 37 | returnOk, 38 | "The mutation score is 0.450000 (9 passed, 11 failed, 8 duplicated, 0 skipped, total is 20)", 39 | ) 40 | } 41 | 42 | func TestMainMatch(t *testing.T) { 43 | testMain( 44 | t, 45 | "../../example", 46 | []string{"--debug", "--exec", "../scripts/exec/test-mutated-package.sh", "--exec-timeout", "1", "--match", "baz", "./..."}, 47 | returnOk, 48 | "The mutation score is 0.500000 (1 passed, 1 failed, 0 duplicated, 0 skipped, total is 2)", 49 | ) 50 | } 51 | 52 | func testMain(t *testing.T, root string, exec []string, expectedExitCode int, contains string) { 53 | saveStderr := os.Stderr 54 | saveStdout := os.Stdout 55 | saveCwd, err := os.Getwd() 56 | assert.Nil(t, err) 57 | 58 | r, w, err := os.Pipe() 59 | assert.Nil(t, err) 60 | 61 | os.Stderr = w 62 | os.Stdout = w 63 | assert.Nil(t, os.Chdir(root)) 64 | 65 | bufChannel := make(chan string) 66 | 67 | go func() { 68 | buf := new(bytes.Buffer) 69 | _, err = io.Copy(buf, r) 70 | assert.Nil(t, err) 71 | assert.Nil(t, r.Close()) 72 | 73 | bufChannel <- buf.String() 74 | }() 75 | 76 | exitCode := mainCmd(exec) 77 | 78 | assert.Nil(t, w.Close()) 79 | 80 | os.Stderr = saveStderr 81 | os.Stdout = saveStdout 82 | assert.Nil(t, os.Chdir(saveCwd)) 83 | 84 | out := <-bufChannel 85 | 86 | assert.Equal(t, expectedExitCode, exitCode) 87 | assert.Contains(t, out, contains) 88 | } 89 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package mutesting 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/build" 7 | "go/parser" 8 | "go/token" 9 | "go/types" 10 | "io/ioutil" 11 | "path/filepath" 12 | 13 | "golang.org/x/tools/go/packages" 14 | ) 15 | 16 | // ParseFile parses the content of the given file and returns the corresponding ast.File node and its file set for positional information. 17 | // If a fatal error is encountered the error return argument is not nil. 18 | func ParseFile(file string) (*ast.File, *token.FileSet, error) { 19 | data, err := ioutil.ReadFile(file) 20 | if err != nil { 21 | return nil, nil, err 22 | } 23 | 24 | return ParseSource(data) 25 | } 26 | 27 | // ParseSource parses the given source and returns the corresponding ast.File node and its file set for positional information. 28 | // If a fatal error is encountered the error return argument is not nil. 29 | func ParseSource(data interface{}) (*ast.File, *token.FileSet, error) { 30 | fset := token.NewFileSet() 31 | 32 | src, err := parser.ParseFile(fset, "", data, parser.ParseComments|parser.AllErrors) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | 37 | return src, fset, err 38 | } 39 | 40 | // ParseAndTypeCheckFile parses and type-checks the given file, and returns everything interesting about the file. 41 | // If a fatal error is encountered the error return argument is not nil. 42 | func ParseAndTypeCheckFile(file string, flags ...string) (*ast.File, *token.FileSet, *types.Package, *types.Info, error) { 43 | fileAbs, err := filepath.Abs(file) 44 | if err != nil { 45 | return nil, nil, nil, nil, fmt.Errorf("could not absolute the file path of %q: %v", file, err) 46 | } 47 | dir := filepath.Dir(fileAbs) 48 | 49 | buildPkg, err := build.ImportDir(dir, build.FindOnly) 50 | if err != nil { 51 | return nil, nil, nil, nil, fmt.Errorf("could not create build package of %q: %v", file, err) 52 | } 53 | 54 | pkgPath := buildPkg.ImportPath 55 | if buildPkg.ImportPath == "." { 56 | pkgPath = dir 57 | } 58 | 59 | prog, err := packages.Load(&packages.Config{ 60 | ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { 61 | return parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors) 62 | }, 63 | BuildFlags: flags, 64 | Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedDeps | packages.NeedName | packages.NeedImports | packages.NeedTypesInfo | packages.NeedFiles, 65 | }, pkgPath) 66 | if err != nil { 67 | fmt.Println(err) 68 | return nil, nil, nil, nil, fmt.Errorf("could not load package of file %q: %v", file, err) 69 | } 70 | 71 | pkgInfo := prog[0] 72 | 73 | var src *ast.File 74 | for _, f := range pkgInfo.Syntax { 75 | if pkgInfo.Fset.Position(f.Pos()).Filename == fileAbs { 76 | src = f 77 | 78 | break 79 | } 80 | } 81 | 82 | return src, pkgInfo.Fset, pkgInfo.Types, pkgInfo.TypesInfo, nil 83 | } 84 | -------------------------------------------------------------------------------- /astutil/query.go: -------------------------------------------------------------------------------- 1 | package astutil 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | ) 8 | 9 | // IdentifiersInStatement returns all identifiers with their found in a statement. 10 | func IdentifiersInStatement(pkg *types.Package, info *types.Info, stmt ast.Stmt) []ast.Expr { 11 | w := &identifierWalker{ 12 | pkg: pkg, 13 | info: info, 14 | } 15 | 16 | ast.Walk(w, stmt) 17 | 18 | return w.identifiers 19 | } 20 | 21 | type identifierWalker struct { 22 | identifiers []ast.Expr 23 | pkg *types.Package 24 | info *types.Info 25 | } 26 | 27 | func checkForSelectorExpr(node ast.Expr) bool { 28 | switch n := node.(type) { 29 | case *ast.Ident: 30 | return true 31 | case *ast.SelectorExpr: 32 | return checkForSelectorExpr(n.X) 33 | } 34 | 35 | return false 36 | } 37 | 38 | func (w *identifierWalker) Visit(node ast.Node) ast.Visitor { 39 | switch n := node.(type) { 40 | case *ast.Ident: 41 | // Ignore the blank identifier 42 | if n.Name == "_" { 43 | return nil 44 | } 45 | 46 | // Ignore keywords 47 | if token.Lookup(n.Name) != token.IDENT { 48 | return nil 49 | } 50 | 51 | // We are only interested in variables 52 | if obj, ok := w.info.Uses[n]; ok { 53 | if _, ok := obj.(*types.Var); !ok { 54 | return nil 55 | } 56 | } 57 | 58 | // FIXME instead of manually creating a new node, clone it and trim the node from its comments and position https://github.com/zimmski/go-mutesting/issues/49 59 | w.identifiers = append(w.identifiers, &ast.Ident{ 60 | Name: n.Name, 61 | }) 62 | 63 | return nil 64 | case *ast.SelectorExpr: 65 | if !checkForSelectorExpr(n) { 66 | return nil 67 | } 68 | 69 | // Check if we need to instantiate the expression 70 | initialize := false 71 | if n.Sel != nil { 72 | if obj, ok := w.info.Uses[n.Sel]; ok { 73 | t := obj.Type() 74 | 75 | switch t.Underlying().(type) { 76 | case *types.Array, *types.Map, *types.Slice, *types.Struct: 77 | initialize = true 78 | } 79 | } 80 | } 81 | 82 | if initialize { 83 | // FIXME we need to clone the node and trim comments and position recursively https://github.com/zimmski/go-mutesting/issues/49 84 | w.identifiers = append(w.identifiers, &ast.CompositeLit{ 85 | Type: n, 86 | }) 87 | } else { 88 | // FIXME we need to clone the node and trim comments and position recursively https://github.com/zimmski/go-mutesting/issues/49 89 | w.identifiers = append(w.identifiers, n) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | return w 96 | } 97 | 98 | // Functions returns all found functions. 99 | func Functions(n ast.Node) []*ast.FuncDecl { 100 | w := &functionWalker{} 101 | 102 | ast.Walk(w, n) 103 | 104 | return w.functions 105 | } 106 | 107 | type functionWalker struct { 108 | functions []*ast.FuncDecl 109 | } 110 | 111 | func (w *functionWalker) Visit(node ast.Node) ast.Visitor { 112 | switch n := node.(type) { 113 | case *ast.FuncDecl: 114 | w.functions = append(w.functions, n) 115 | 116 | return nil 117 | } 118 | 119 | return w 120 | } 121 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | package mutesting 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "strings" 8 | 9 | "github.com/zimmski/go-mutesting/mutator" 10 | ) 11 | 12 | // CountWalk returns the number of corresponding mutations for a given mutator. 13 | // It traverses the AST of the given node and calls the method Check of the given mutator for every node and sums up the returned counts. After completion of the traversal the final counter is returned. 14 | func CountWalk(pkg *types.Package, info *types.Info, node ast.Node, m mutator.Mutator) int { 15 | w := &countWalk{ 16 | count: 0, 17 | mutator: m, 18 | pkg: pkg, 19 | info: info, 20 | } 21 | 22 | ast.Walk(w, node) 23 | 24 | return w.count 25 | } 26 | 27 | type countWalk struct { 28 | count int 29 | mutator mutator.Mutator 30 | pkg *types.Package 31 | info *types.Info 32 | } 33 | 34 | // Visit implements the Visit method of the ast.Visitor interface 35 | func (w *countWalk) Visit(node ast.Node) ast.Visitor { 36 | if node == nil { 37 | return w 38 | } 39 | 40 | w.count += len(w.mutator(w.pkg, w.info, node)) 41 | 42 | return w 43 | } 44 | 45 | // MutateWalk mutates the given node with the given mutator returning a channel to control the mutation steps. 46 | // It traverses the AST of the given node and calls the method Check of the given mutator to verify that a node can be mutated by the mutator. If a node can be mutated the method Mutate of the given mutator is executed with the node and the control channel. After completion of the traversal the control channel is closed. 47 | func MutateWalk(pkg *types.Package, info *types.Info, node ast.Node, m mutator.Mutator) chan bool { 48 | w := &mutateWalk{ 49 | changed: make(chan bool), 50 | mutator: m, 51 | pkg: pkg, 52 | info: info, 53 | } 54 | 55 | go func() { 56 | ast.Walk(w, node) 57 | 58 | close(w.changed) 59 | }() 60 | 61 | return w.changed 62 | } 63 | 64 | type mutateWalk struct { 65 | changed chan bool 66 | mutator mutator.Mutator 67 | pkg *types.Package 68 | info *types.Info 69 | } 70 | 71 | // Visit implements the Visit method of the ast.Visitor interface 72 | func (w *mutateWalk) Visit(node ast.Node) ast.Visitor { 73 | if node == nil { 74 | return w 75 | } 76 | 77 | for _, m := range w.mutator(w.pkg, w.info, node) { 78 | m.Change() 79 | w.changed <- true 80 | <-w.changed 81 | 82 | m.Reset() 83 | w.changed <- true 84 | <-w.changed 85 | } 86 | 87 | return w 88 | } 89 | 90 | // PrintWalk traverses the AST of the given node and prints every node to STDOUT. 91 | func PrintWalk(node ast.Node) { 92 | w := &printWalk{ 93 | level: 0, 94 | } 95 | 96 | ast.Walk(w, node) 97 | } 98 | 99 | type printWalk struct { 100 | level int 101 | } 102 | 103 | // Visit implements the Visit method of the ast.Visitor interface 104 | func (w *printWalk) Visit(node ast.Node) ast.Visitor { 105 | if node != nil { 106 | w.level++ 107 | 108 | fmt.Printf("%s(%p)%#v\n", strings.Repeat("\t", w.level), node, node) 109 | } else { 110 | w.level-- 111 | } 112 | 113 | return w 114 | } 115 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= 6 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 7 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 8 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 9 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 10 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 11 | github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E= 12 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 13 | github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= 14 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 15 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 16 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 17 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 18 | github.com/mattn/goveralls v0.0.3 h1:GnFhBAK0wJmxZBum88FqDzcDPLjAk9sL0HzhmW+9bo8= 19 | github.com/mattn/goveralls v0.0.3/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 20 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= 21 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 22 | github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= 23 | github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= 27 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 28 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 31 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 32 | github.com/zimmski/go-tool v0.0.0-20150119110811-2dfdc9ac8439 h1:yHqsjUkj0HWbKPw/6ZqC0/eMklaRpqubA199vaRLzzE= 33 | github.com/zimmski/go-tool v0.0.0-20150119110811-2dfdc9ac8439/go.mod h1:G4FVqCRvfz74AEB1crDNdQuvMfOoKtk7DlePsnV2yGs= 34 | github.com/zimmski/osutil v0.0.0-20190128123334-0d0b3ca231ac h1:uiFRlKzyIzHeLOthe0ethUkSGW7POlqxU3Tc21R8QpQ= 35 | github.com/zimmski/osutil v0.0.0-20190128123334-0d0b3ca231ac/go.mod h1:wJ9WGevuM/rw8aB2pQPFMUgXZWeaouI0ueFamR0DUPE= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 38 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 39 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 40 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e h1:JgcxKXxCjrA2tyDP/aNU9K0Ck5Czfk6C7e2tMw7+bSI= 41 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 42 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 43 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 44 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 45 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 52 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 53 | golang.org/x/tools v0.0.0-20190820033707-85edb9ef3283 h1:Seblypk5Prvsc9UDjPTV/1N+YjNIXQntlZt0EpobDww= 54 | golang.org/x/tools v0.0.0-20190820033707-85edb9ef3283/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 55 | golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a h1:UuQ+70Pi/ZdWHuP4v457pkXeOynTdgd/4enxeIO/98k= 56 | golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 57 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 62 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 63 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 64 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 65 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 66 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 67 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 68 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 69 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 70 | -------------------------------------------------------------------------------- /cmd/go-mutesting/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "go/ast" 8 | "go/format" 9 | "go/printer" 10 | "go/token" 11 | "go/types" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "regexp" 18 | "strings" 19 | "syscall" 20 | 21 | "github.com/jessevdk/go-flags" 22 | "github.com/zimmski/go-tool/importing" 23 | "github.com/zimmski/osutil" 24 | 25 | "github.com/zimmski/go-mutesting" 26 | "github.com/zimmski/go-mutesting/astutil" 27 | "github.com/zimmski/go-mutesting/mutator" 28 | _ "github.com/zimmski/go-mutesting/mutator/branch" 29 | _ "github.com/zimmski/go-mutesting/mutator/expression" 30 | _ "github.com/zimmski/go-mutesting/mutator/statement" 31 | ) 32 | 33 | const ( 34 | returnOk = iota 35 | returnHelp 36 | returnBashCompletion 37 | returnError 38 | ) 39 | 40 | type options struct { 41 | General struct { 42 | Debug bool `long:"debug" description:"Debug log output"` 43 | DoNotRemoveTmpFolder bool `long:"do-not-remove-tmp-folder" description:"Do not remove the tmp folder where all mutations are saved to"` 44 | Help bool `long:"help" description:"Show this help message"` 45 | Verbose bool `long:"verbose" description:"Verbose log output"` 46 | } `group:"General options"` 47 | 48 | Files struct { 49 | Blacklist []string `long:"blacklist" description:"List of MD5 checksums of mutations which should be ignored. Each checksum must end with a new line character."` 50 | ListFiles bool `long:"list-files" description:"List found files"` 51 | PrintAST bool `long:"print-ast" description:"Print the ASTs of all given files and exit"` 52 | } `group:"File options"` 53 | 54 | Mutator struct { 55 | DisableMutators []string `long:"disable" description:"Disable mutator by their name or using * as a suffix pattern"` 56 | ListMutators bool `long:"list-mutators" description:"List all available mutators"` 57 | } `group:"Mutator options"` 58 | 59 | Filter struct { 60 | Match string `long:"match" description:"Only functions are mutated that confirm to the arguments regex"` 61 | } `group:"Filter options"` 62 | 63 | Exec struct { 64 | Exec string `long:"exec" description:"Execute this command for every mutation (by default the built-in exec command is used)"` 65 | NoExec bool `long:"no-exec" description:"Skip the built-in exec command and just generate the mutations"` 66 | Timeout uint `long:"exec-timeout" description:"Sets a timeout for the command execution (in seconds)" default:"10"` 67 | } `group:"Exec options"` 68 | 69 | Test struct { 70 | Recursive bool `long:"test-recursive" description:"Defines if the executer should test recursively"` 71 | } `group:"Test options"` 72 | 73 | Remaining struct { 74 | Targets []string `description:"Packages, directories and files even with patterns (by default the current directory)"` 75 | } `positional-args:"true" required:"true"` 76 | } 77 | 78 | func checkArguments(args []string, opts *options) (bool, int) { 79 | p := flags.NewNamedParser("go-mutesting", flags.None) 80 | 81 | p.ShortDescription = "Mutation testing for Go source code" 82 | 83 | if _, err := p.AddGroup("go-mutesting", "go-mutesting arguments", opts); err != nil { 84 | return true, exitError(err.Error()) 85 | } 86 | 87 | completion := len(os.Getenv("GO_FLAGS_COMPLETION")) > 0 88 | 89 | _, err := p.ParseArgs(args) 90 | if (opts.General.Help || len(args) == 0) && !completion { 91 | p.WriteHelp(os.Stdout) 92 | 93 | return true, returnHelp 94 | } else if opts.Mutator.ListMutators { 95 | for _, name := range mutator.List() { 96 | fmt.Println(name) 97 | } 98 | 99 | return true, returnOk 100 | } 101 | 102 | if err != nil { 103 | return true, exitError(err.Error()) 104 | } 105 | 106 | if completion { 107 | return true, returnBashCompletion 108 | } 109 | 110 | if opts.General.Debug { 111 | opts.General.Verbose = true 112 | } 113 | 114 | return false, 0 115 | } 116 | 117 | func debug(opts *options, format string, args ...interface{}) { 118 | if opts.General.Debug { 119 | fmt.Printf(format+"\n", args...) 120 | } 121 | } 122 | 123 | func verbose(opts *options, format string, args ...interface{}) { 124 | if opts.General.Verbose || opts.General.Debug { 125 | fmt.Printf(format+"\n", args...) 126 | } 127 | } 128 | 129 | func exitError(format string, args ...interface{}) int { 130 | _, _ = fmt.Fprintf(os.Stderr, format+"\n", args...) 131 | 132 | return returnError 133 | } 134 | 135 | type mutatorItem struct { 136 | Name string 137 | Mutator mutator.Mutator 138 | } 139 | 140 | type mutationStats struct { 141 | passed int 142 | failed int 143 | duplicated int 144 | skipped int 145 | } 146 | 147 | func (ms *mutationStats) Score() float64 { 148 | total := ms.Total() 149 | 150 | if total == 0 { 151 | return 0.0 152 | } 153 | 154 | return float64(ms.passed) / float64(total) 155 | } 156 | 157 | func (ms *mutationStats) Total() int { 158 | return ms.passed + ms.failed + ms.skipped 159 | } 160 | 161 | func mainCmd(args []string) int { 162 | var opts = &options{} 163 | var mutationBlackList = map[string]struct{}{} 164 | 165 | if exit, exitCode := checkArguments(args, opts); exit { 166 | return exitCode 167 | } 168 | 169 | files := importing.FilesOfArgs(opts.Remaining.Targets) 170 | if len(files) == 0 { 171 | return exitError("Could not find any suitable Go source files") 172 | } 173 | 174 | if opts.Files.ListFiles { 175 | for _, file := range files { 176 | fmt.Println(file) 177 | } 178 | 179 | return returnOk 180 | } else if opts.Files.PrintAST { 181 | for _, file := range files { 182 | fmt.Println(file) 183 | 184 | src, _, err := mutesting.ParseFile(file) 185 | if err != nil { 186 | return exitError("Could not open file %q: %v", file, err) 187 | } 188 | 189 | mutesting.PrintWalk(src) 190 | 191 | fmt.Println() 192 | } 193 | 194 | return returnOk 195 | } 196 | 197 | if len(opts.Files.Blacklist) > 0 { 198 | for _, f := range opts.Files.Blacklist { 199 | c, err := ioutil.ReadFile(f) 200 | if err != nil { 201 | return exitError("Cannot read blacklist file %q: %v", f, err) 202 | } 203 | 204 | for _, line := range strings.Split(string(c), "\n") { 205 | if line == "" { 206 | continue 207 | } 208 | 209 | if len(line) != 32 { 210 | return exitError("%q is not a MD5 checksum", line) 211 | } 212 | 213 | mutationBlackList[line] = struct{}{} 214 | } 215 | } 216 | } 217 | 218 | var mutators []mutatorItem 219 | 220 | MUTATOR: 221 | for _, name := range mutator.List() { 222 | if len(opts.Mutator.DisableMutators) > 0 { 223 | for _, d := range opts.Mutator.DisableMutators { 224 | pattern := strings.HasSuffix(d, "*") 225 | 226 | if (pattern && strings.HasPrefix(name, d[:len(d)-2])) || (!pattern && name == d) { 227 | continue MUTATOR 228 | } 229 | } 230 | } 231 | 232 | verbose(opts, "Enable mutator %q", name) 233 | 234 | m, _ := mutator.New(name) 235 | mutators = append(mutators, mutatorItem{ 236 | Name: name, 237 | Mutator: m, 238 | }) 239 | } 240 | 241 | tmpDir, err := ioutil.TempDir("", "go-mutesting-") 242 | if err != nil { 243 | panic(err) 244 | } 245 | verbose(opts, "Save mutations into %q", tmpDir) 246 | 247 | var execs []string 248 | if opts.Exec.Exec != "" { 249 | execs = strings.Split(opts.Exec.Exec, " ") 250 | } 251 | 252 | stats := &mutationStats{} 253 | 254 | for _, file := range files { 255 | verbose(opts, "Mutate %q", file) 256 | 257 | src, fset, pkg, info, err := mutesting.ParseAndTypeCheckFile(file) 258 | if err != nil { 259 | return exitError(err.Error()) 260 | } 261 | 262 | err = os.MkdirAll(tmpDir+"/"+filepath.Dir(file), 0755) 263 | if err != nil { 264 | panic(err) 265 | } 266 | 267 | tmpFile := tmpDir + "/" + file 268 | 269 | originalFile := fmt.Sprintf("%s.original", tmpFile) 270 | err = osutil.CopyFile(file, originalFile) 271 | if err != nil { 272 | panic(err) 273 | } 274 | debug(opts, "Save original into %q", originalFile) 275 | 276 | mutationID := 0 277 | 278 | if opts.Filter.Match != "" { 279 | m, err := regexp.Compile(opts.Filter.Match) 280 | if err != nil { 281 | return exitError("Match regex is not valid: %v", err) 282 | } 283 | 284 | for _, f := range astutil.Functions(src) { 285 | if m.MatchString(f.Name.Name) { 286 | mutationID = mutate(opts, mutators, mutationBlackList, mutationID, pkg, info, file, fset, src, f, tmpFile, execs, stats) 287 | } 288 | } 289 | } else { 290 | _ = mutate(opts, mutators, mutationBlackList, mutationID, pkg, info, file, fset, src, src, tmpFile, execs, stats) 291 | } 292 | } 293 | 294 | if !opts.General.DoNotRemoveTmpFolder { 295 | err = os.RemoveAll(tmpDir) 296 | if err != nil { 297 | panic(err) 298 | } 299 | debug(opts, "Remove %q", tmpDir) 300 | } 301 | 302 | if !opts.Exec.NoExec { 303 | fmt.Printf("The mutation score is %f (%d passed, %d failed, %d duplicated, %d skipped, total is %d)\n", stats.Score(), stats.passed, stats.failed, stats.duplicated, stats.skipped, stats.Total()) 304 | } else { 305 | fmt.Println("Cannot do a mutation testing summary since no exec command was executed.") 306 | } 307 | 308 | return returnOk 309 | } 310 | 311 | func mutate(opts *options, mutators []mutatorItem, mutationBlackList map[string]struct{}, mutationID int, pkg *types.Package, info *types.Info, file string, fset *token.FileSet, src ast.Node, node ast.Node, tmpFile string, execs []string, stats *mutationStats) int { 312 | for _, m := range mutators { 313 | debug(opts, "Mutator %s", m.Name) 314 | 315 | changed := mutesting.MutateWalk(pkg, info, node, m.Mutator) 316 | 317 | for { 318 | _, ok := <-changed 319 | 320 | if !ok { 321 | break 322 | } 323 | 324 | mutationFile := fmt.Sprintf("%s.%d", tmpFile, mutationID) 325 | checksum, duplicate, err := saveAST(mutationBlackList, mutationFile, fset, src) 326 | if err != nil { 327 | fmt.Printf("INTERNAL ERROR %s\n", err.Error()) 328 | } else if duplicate { 329 | debug(opts, "%q is a duplicate, we ignore it", mutationFile) 330 | 331 | stats.duplicated++ 332 | } else { 333 | debug(opts, "Save mutation into %q with checksum %s", mutationFile, checksum) 334 | 335 | if !opts.Exec.NoExec { 336 | execExitCode := mutateExec(opts, pkg, file, src, mutationFile, execs) 337 | 338 | debug(opts, "Exited with %d", execExitCode) 339 | 340 | msg := fmt.Sprintf("%q with checksum %s", mutationFile, checksum) 341 | 342 | switch execExitCode { 343 | case 0: 344 | fmt.Printf("PASS %s\n", msg) 345 | 346 | stats.passed++ 347 | case 1: 348 | fmt.Printf("FAIL %s\n", msg) 349 | 350 | stats.failed++ 351 | case 2: 352 | fmt.Printf("SKIP %s\n", msg) 353 | 354 | stats.skipped++ 355 | default: 356 | fmt.Printf("UNKOWN exit code for %s\n", msg) 357 | } 358 | } 359 | } 360 | 361 | changed <- true 362 | 363 | // Ignore original state 364 | <-changed 365 | changed <- true 366 | 367 | mutationID++ 368 | } 369 | } 370 | 371 | return mutationID 372 | } 373 | 374 | func mutateExec(opts *options, pkg *types.Package, file string, src ast.Node, mutationFile string, execs []string) (execExitCode int) { 375 | if len(execs) == 0 { 376 | debug(opts, "Execute built-in exec command for mutation") 377 | 378 | diff, err := exec.Command("diff", "-u", file, mutationFile).CombinedOutput() 379 | if err == nil { 380 | execExitCode = 0 381 | } else if e, ok := err.(*exec.ExitError); ok { 382 | execExitCode = e.Sys().(syscall.WaitStatus).ExitStatus() 383 | } else { 384 | panic(err) 385 | } 386 | if execExitCode != 0 && execExitCode != 1 { 387 | fmt.Printf("%s\n", diff) 388 | 389 | panic("Could not execute diff on mutation file") 390 | } 391 | 392 | defer func() { 393 | _ = os.Rename(file+".tmp", file) 394 | }() 395 | 396 | err = os.Rename(file, file+".tmp") 397 | if err != nil { 398 | panic(err) 399 | } 400 | err = osutil.CopyFile(mutationFile, file) 401 | if err != nil { 402 | panic(err) 403 | } 404 | 405 | pkgName := pkg.Path() 406 | if opts.Test.Recursive { 407 | pkgName += "/..." 408 | } 409 | 410 | test, err := exec.Command("go", "test", "-timeout", fmt.Sprintf("%ds", opts.Exec.Timeout), pkgName).CombinedOutput() 411 | if err == nil { 412 | execExitCode = 0 413 | } else if e, ok := err.(*exec.ExitError); ok { 414 | execExitCode = e.Sys().(syscall.WaitStatus).ExitStatus() 415 | } else { 416 | panic(err) 417 | } 418 | 419 | if opts.General.Debug { 420 | fmt.Printf("%s\n", test) 421 | } 422 | 423 | switch execExitCode { 424 | case 0: // Tests passed -> FAIL 425 | fmt.Printf("%s\n", diff) 426 | 427 | execExitCode = 1 428 | case 1: // Tests failed -> PASS 429 | if opts.General.Debug { 430 | fmt.Printf("%s\n", diff) 431 | } 432 | 433 | execExitCode = 0 434 | case 2: // Did not compile -> SKIP 435 | if opts.General.Verbose { 436 | fmt.Println("Mutation did not compile") 437 | } 438 | 439 | if opts.General.Debug { 440 | fmt.Printf("%s\n", diff) 441 | } 442 | default: // Unknown exit code -> SKIP 443 | fmt.Println("Unknown exit code") 444 | fmt.Printf("%s\n", diff) 445 | } 446 | 447 | return execExitCode 448 | } 449 | 450 | debug(opts, "Execute %q for mutation", opts.Exec.Exec) 451 | 452 | execCommand := exec.Command(execs[0], execs[1:]...) 453 | 454 | execCommand.Stderr = os.Stderr 455 | execCommand.Stdout = os.Stdout 456 | 457 | execCommand.Env = append(os.Environ(), []string{ 458 | "MUTATE_CHANGED=" + mutationFile, 459 | fmt.Sprintf("MUTATE_DEBUG=%t", opts.General.Debug), 460 | "MUTATE_ORIGINAL=" + file, 461 | "MUTATE_PACKAGE=" + pkg.Path(), 462 | fmt.Sprintf("MUTATE_TIMEOUT=%d", opts.Exec.Timeout), 463 | fmt.Sprintf("MUTATE_VERBOSE=%t", opts.General.Verbose), 464 | }...) 465 | if opts.Test.Recursive { 466 | execCommand.Env = append(execCommand.Env, "TEST_RECURSIVE=true") 467 | } 468 | 469 | err := execCommand.Start() 470 | if err != nil { 471 | panic(err) 472 | } 473 | 474 | // TODO timeout here 475 | 476 | err = execCommand.Wait() 477 | 478 | if err == nil { 479 | execExitCode = 0 480 | } else if e, ok := err.(*exec.ExitError); ok { 481 | execExitCode = e.Sys().(syscall.WaitStatus).ExitStatus() 482 | } else { 483 | panic(err) 484 | } 485 | 486 | return execExitCode 487 | } 488 | 489 | func main() { 490 | os.Exit(mainCmd(os.Args[1:])) 491 | } 492 | 493 | func saveAST(mutationBlackList map[string]struct{}, file string, fset *token.FileSet, node ast.Node) (string, bool, error) { 494 | var buf bytes.Buffer 495 | 496 | h := md5.New() 497 | 498 | err := printer.Fprint(io.MultiWriter(h, &buf), fset, node) 499 | if err != nil { 500 | return "", false, err 501 | } 502 | 503 | checksum := fmt.Sprintf("%x", h.Sum(nil)) 504 | 505 | if _, ok := mutationBlackList[checksum]; ok { 506 | return checksum, true, nil 507 | } 508 | 509 | mutationBlackList[checksum] = struct{}{} 510 | 511 | src, err := format.Source(buf.Bytes()) 512 | if err != nil { 513 | return "", false, err 514 | } 515 | 516 | err = ioutil.WriteFile(file, src, 0666) 517 | if err != nil { 518 | return "", false, err 519 | } 520 | 521 | return checksum, false, nil 522 | } 523 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mutesting [![GoDoc](https://godoc.org/github.com/zimmski/go-mutesting?status.png)](https://godoc.org/github.com/zimmski/go-mutesting) [![Build Status](https://travis-ci.org/zimmski/go-mutesting.svg?branch=master)](https://travis-ci.org/zimmski/go-mutesting) [![Coverage Status](https://coveralls.io/repos/zimmski/go-mutesting/badge.png?branch=master)](https://coveralls.io/r/zimmski/go-mutesting?branch=master) 2 | 3 | go-mutesting is a framework for performing mutation testing on Go source code. Its main purpose is to find source code, which is not covered by any tests. 4 | 5 | ## Quick example 6 | 7 | The following command mutates the go-mutesting project with all available mutators. 8 | 9 | ```bash 10 | go-mutesting github.com/zimmski/go-mutesting/... 11 | ``` 12 | 13 | The execution of this command prints for every mutation if it was successfully tested or not. If not, the source code patch is printed out, so the mutation can be investigated. The following shows an example for a patch of a mutation. 14 | 15 | ```diff 16 | for _, d := range opts.Mutator.DisableMutators { 17 | pattern := strings.HasSuffix(d, "*") 18 | 19 | - if (pattern && strings.HasPrefix(name, d[:len(d)-2])) || (!pattern && name == d) { 20 | + if (pattern && strings.HasPrefix(name, d[:len(d)-2])) || false { 21 | continue MUTATOR 22 | } 23 | } 24 | ``` 25 | 26 | The example shows that the right term `(!pattern && name == d)` of the `||` operator is made irrelevant by substituting it with `false`. Since this change of the source code is not detected by the test suite, meaning the test suite did not fail, we can mark it as untested code. 27 | 28 | The next mutation shows code from the `removeNode` method of a [linked list](https://github.com/zimmski/container/blob/master/list/linkedlist/linkedlist.go) implementation. 29 | 30 | ```diff 31 | } 32 | 33 | l.first = nil 34 | - l.last = nil 35 | + 36 | l.len = 0 37 | } 38 | ``` 39 | 40 | We know that the code originates from a remove method which means that the mutation introduces a leak by ignoring the removal of a reference. This can be [tested](https://github.com/zimmski/container/commit/142c3e16a249095b0d63f2b41055d17cf059f045) with [go-leaks](https://github.com/zimmski/go-leak). 41 | 42 | ## Table of content 43 | 44 | - [What is mutation testing?](#what-is-mutation-testing) 45 | - [How do I use go-mutesting?](#how-do-i-use-go-mutesting) 46 | - [How do I write my own mutation exec commands?](#write-mutation-exec-commands) 47 | - [Which mutators are implemented?](#list-of-mutators) 48 | - [Other mutation testing projects and their flaws](#other-projects) 49 | - [Can I make feature requests and report bugs and problems?](#feature-request) 50 | 51 | ## What is mutation testing? 52 | 53 | The definition of mutation testing is best quoted from Wikipedia: 54 | 55 | > Mutation testing (or Mutation analysis or Program mutation) is used to design new software tests and evaluate the quality of existing software tests. Mutation testing involves modifying a program in small ways. Each mutated version is called a mutant and tests detect and reject mutants by causing the behavior of the original version to differ from the mutant. This is called killing the mutant. Test suites are measured by the percentage of mutants that they kill. New tests can be designed to kill additional mutants. 56 | >
-- [https://en.wikipedia.org/wiki/Mutation_testing](https://en.wikipedia.org/wiki/Mutation_testing) 57 | 58 | > Tests can be created to verify the correctness of the implementation of a given software system, but the creation of tests still poses the question whether the tests are correct and sufficiently cover the requirements that have originated the implementation. 59 | >
-- [https://en.wikipedia.org/wiki/Mutation_testing](https://en.wikipedia.org/wiki/Mutation_testing) 60 | 61 | Although the definition states that the main purpose of mutation testing is finding implementation cases which are not covered by tests, other implementation flaws can be found too. Mutation testing can for example uncover dead and unneeded code. 62 | 63 | Mutation testing is also especially interesting for comparing automatically generated test suites with manually written test suites. This was the original intention of go-mutesting which is used to evaluate the generic fuzzing and delta-debugging framework [Tavor](https://github.com/zimmski/tavor). 64 | 65 | ## How do I use go-mutesting? 66 | 67 | go-mutesting includes a binary which is go-getable. 68 | 69 | ```bash 70 | go get -t -v github.com/zimmski/go-mutesting/... 71 | ``` 72 | 73 | The binary's help can be invoked by executing the binary without arguments or with the `--help` argument. 74 | 75 | ```bash 76 | go-mutesting --help 77 | ``` 78 | 79 | > **Note**: This README describes only a few of the available arguments. It is therefore advisable to examine the output of the `--help` argument. 80 | 81 | The targets of the mutation testing can be defined as arguments to the binary. Every target can be either a Go source file, a directory or a package. Directories and packages can also include the `...` wildcard pattern which will search recursively for Go source files. Test source files with the suffix `_test` are excluded, since this would interfere with the testing process most of the time. 82 | 83 | The following example gathers all Go files which are defined by the targets and generate mutations with all available mutators of the binary. 84 | 85 | ```bash 86 | go-mutesting parse.go example/ github.com/zimmski/go-mutesting/mutator/... 87 | ``` 88 | 89 | Every mutation has to be tested using an [exec command](#write-mutation-exec-commands). By default the built-in exec command is used, which tests a mutation using the following steps: 90 | 91 | - Replace the original file with the mutation. 92 | - Execute all tests of the package of the mutated file. 93 | - Report if the mutation was killed. 94 | 95 | Alternatively the `--exec` argument can be used to invoke an external exec command. The [/scripts/exec](/scripts/exec) directory holds basic exec commands for Go projects. The [test-mutated-package.sh](/scripts/exec/test-mutated-package.sh) script implements all steps and almost all features of the built-in exec command. It can be for example used to test the [github.com/zimmski/go-mutesting/example](/example) package. 96 | 97 | ```bash 98 | go-mutesting --exec "$GOPATH/src/github.com/zimmski/go-mutesting/scripts/exec/test-mutated-package.sh" github.com/zimmski/go-mutesting/example 99 | ``` 100 | 101 | The execution will print the following output. 102 | 103 | > **Note**: This output is from an older version of go-mutesting. Up to date versions of go-mutesting will have different mutations. 104 | 105 | ```diff 106 | PASS "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.0" with checksum b705f4c99e6d572de509609eb0a625be 107 | PASS "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.1" with checksum eb54efffc5edfc7eba2b276371b29836 108 | PASS "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.2" with checksum 011df9567e5fee9bf75cbe5d5dc1c81f 109 | --- /home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go 110 | +++ /tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.3 111 | @@ -16,7 +16,7 @@ 112 | } 113 | 114 | if n < 0 { 115 | - n = 0 116 | + 117 | } 118 | 119 | n++ 120 | FAIL "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.3" with checksum 82fc14acf7b561598bfce25bf3a162a2 121 | PASS "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.4" with checksum 5720f1bf404abea121feb5a50caf672c 122 | PASS "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.5" with checksum d6c1b5e25241453128f9f3bf1b9e7741 123 | --- /home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go 124 | +++ /tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.6 125 | @@ -24,7 +24,6 @@ 126 | n += bar() 127 | 128 | bar() 129 | - bar() 130 | 131 | return n 132 | } 133 | FAIL "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.6" with checksum 5b1ca0cfedd786d9df136a0e042df23a 134 | PASS "/tmp/go-mutesting-422402775//home/zimmski/go/src/github.com/zimmski/go-mutesting/example/example.go.8" with checksum 6928f4458787c7042c8b4505888300a6 135 | The mutation score is 0.750000 (6 passed, 2 failed, 0 skipped, total is 8) 136 | ``` 137 | 138 | The output shows that eight mutations have been found and tested. Six of them passed which means that the test suite failed for these mutations and the mutations were therefore killed. However, two mutations did not fail the test suite. Their source code patches are shown in the output which can be used to investigate these mutations. 139 | 140 | The summary also shows the **mutation score** which is a metric on how many mutations are killed by the test suite and therefore states the quality of the test suite. The mutation score is calculated by dividing the number of passed mutations by the number of total mutations, for the example above this would be 6/8=0.75. A score of 1.0 means that all mutations have been killed. 141 | 142 | ### Blacklist false positives 143 | 144 | Mutation testing can generate many false positives since mutation algorithms do not fully understand the given source code. `early exits` are one common example. They can be implemented as optimizations and will almost always trigger a false-positive since the unoptimized code path will be used which will lead to the same result. go-mutesting is meant to be used as an addition to automatic test suites. It is therefore necessary to mark such mutations as false-positives. This is done with the `--blacklist` argument. The argument defines a file which contains in every line a MD5 checksum of a mutation. These checksums can then be used to ignore mutations. 145 | 146 | > **Note**: The blacklist feature is currently badly implemented as a change in the original source code will change all checksums. 147 | 148 | The example output of the [How do I use go-mutesting?](#how-do-i-use-go-mutesting) section describes a mutation `example.go.6` which has the checksum `5b1ca0cfedd786d9df136a0e042df23a`. If we want to mark this mutation as a false-positive, we simple create a file with the following content. 149 | 150 | ``` 151 | 5b1ca0cfedd786d9df136a0e042df23a 152 | ``` 153 | 154 | The blacklist file, which is named `example.blacklist` in this example, can then be used to invoke go-mutesting. 155 | 156 | ```bash 157 | go-mutesting --blacklist example.blacklist github.com/zimmski/go-mutesting/example 158 | ``` 159 | 160 | The execution will print the following output. 161 | 162 | > **Note**: This output is from an older version of go-mutesting. Up to date versions of go-mutesting will have different mutations. 163 | 164 | ```diff 165 | PASS "/tmp/go-mutesting-208240643/example.go.0" with checksum b705f4c99e6d572de509609eb0a625be 166 | PASS "/tmp/go-mutesting-208240643/example.go.1" with checksum eb54efffc5edfc7eba2b276371b29836 167 | PASS "/tmp/go-mutesting-208240643/example.go.2" with checksum 011df9567e5fee9bf75cbe5d5dc1c81f 168 | --- example.go 2014-12-29 23:37:42.813320040 +0100 169 | +++ /tmp/go-mutesting-208240643/example.go.3 2014-12-30 00:49:33.573285038 +0100 170 | @@ -16,7 +16,7 @@ 171 | } 172 | 173 | if n < 0 { 174 | - n = 0 175 | + 176 | } 177 | 178 | n++ 179 | FAIL "/tmp/go-mutesting-208240643/example.go.3" with checksum 82fc14acf7b561598bfce25bf3a162a2 180 | PASS "/tmp/go-mutesting-208240643/example.go.4" with checksum 5720f1bf404abea121feb5a50caf672c 181 | PASS "/tmp/go-mutesting-208240643/example.go.5" with checksum d6c1b5e25241453128f9f3bf1b9e7741 182 | PASS "/tmp/go-mutesting-208240643/example.go.8" with checksum 6928f4458787c7042c8b4505888300a6 183 | The mutation score is 0.857143 (6 passed, 1 failed, 0 skipped, total is 7) 184 | ``` 185 | 186 | By comparing this output to the original output we can state that we now have 7 mutations instead of 8. 187 | 188 | ## How do I write my own mutation exec commands? 189 | 190 | A mutation exec command is invoked for every mutation which is necessary to test a mutation. Commands should handle at least the following phases. 191 | 192 | 1. **Setup** the source to include the mutation. 193 | 2. **Test** the source by invoking the test suite and possible other test functionality. 194 | 3. **Cleanup** all changes and remove all temporary assets. 195 | 4. **Report** if the mutation was killed. 196 | 197 | It is important to note that each invocation should be isolated and therefore stateless. This means that an invocation must not interfere with other invocations. 198 | 199 | A set of environment variables, which define exactly one mutation, is passed on to the command. 200 | 201 | | Name | Description | 202 | | :-------------- | :------------------------------------------------------------------------ | 203 | | MUTATE_CHANGED | Defines the filename to the mutation of the original file. | 204 | | MUTATE_DEBUG | Defines if debugging output should be printed. | 205 | | MUTATE_ORIGINAL | Defines the filename to the original file which was mutated. | 206 | | MUTATE_PACKAGE | Defines the import path of the origianl file. | 207 | | MUTATE_TIMEOUT | Defines a timeout which should be taken into account by the exec command. | 208 | | MUTATE_VERBOSE | Defines if verbose output should be printed. | 209 | | TEST_RECURSIVE | Defines if tests should be run recursively. | 210 | 211 | A command must exit with an appropriate exit code. 212 | 213 | | Exit code | Description | 214 | | :------ | :-------- | 215 | | 0 | The mutation was killed. Which means that the test led to a failed test after the mutation was applied. | 216 | | 1 | The mutation is alive. Which means that this could be a flaw in the test suite or even in the implementation. | 217 | | 2 | The mutation was skipped, since there are other problems e.g. compilation errors. | 218 | | >2 | The mutation produced an unknown exit code which might be a flaw in the exec command. | 219 | 220 | Examples for exec commands can be found in the [scripts](/scripts/exec) directory. 221 | 222 | ## Which mutators are implemented? 223 | 224 | ### Branch mutators 225 | 226 | | Name | Description | 227 | | :------------ | :------------------------------------------------- | 228 | | branch/case | Empties case bodies. | 229 | | branch/if | Empties branches of `if` and `else if` statements. | 230 | | branch/else | Empties branches of `else` statements. | 231 | 232 | ### Expression mutators 233 | 234 | | Name | Description | 235 | | :-------------------- | :--------------------------------------------- | 236 | | expression/comparison | Searches for comparison operators, such as `>` and `<=`, and replaces them with similar operators to catch off-by-one errors, e.g. `>` is replaced by `>=`. | 237 | | expression/remove | Searches for `&&` and \|\| operators and makes each term of the operator irrelevant by using `true` or `false` as replacements. | 238 | 239 | ### Statement mutators 240 | 241 | | Name | Description | 242 | | :------------------ | :--------------------------------------------- | 243 | | statement/remove | Removes assignment, increment, decrement and expression statements. | 244 | 245 | ## How do I write my own mutators? 246 | 247 | Each mutator must implement the `Mutator` interface of the [github.com/zimmski/go-mutesting/mutator](https://godoc.org/github.com/zimmski/go-mutesting/mutator#Mutator) package. The methods of the interface are described in detail in the source code documentation. 248 | 249 | Additionally each mutator has to be registered with the `Register` function of the [github.com/zimmski/go-mutesting/mutator](https://godoc.org/github.com/zimmski/go-mutesting/mutator#Mutator) package to make it usable by the binary. 250 | 251 | Examples for mutators can be found in the [github.com/zimmski/go-mutesting/mutator](https://godoc.org/github.com/zimmski/go-mutesting/mutator) package and its sub-packages. 252 | 253 | ## Other mutation testing projects and their flaws 254 | 255 | go-mutesting is not the first project to implement mutation testing for Go source code. A quick search uncovers the following projects. 256 | 257 | - https://github.com/darkhelmet/manbearpig 258 | - https://github.com/kisielk/mutator 259 | - https://github.com/StefanSchroeder/Golang-Mutation-testing 260 | 261 | All of them have significant flaws in comparison to go-mutesting: 262 | 263 | - Only one type (or even one case) of mutation is implemented. 264 | - Can only be used for one mutator at a time (manbearpig, Golang-Mutation-testing). 265 | - Mutation is done by content which can lead to lots of invalid mutations (Golang-Mutation-testing). 266 | - New mutators are not easily implemented and integrated. 267 | - Can only be used for one package or file at a time. 268 | - Other scenarios as `go test` cannot be applied. 269 | - Do not properly clean up or handle fatal failures. 270 | - No automatic tests to ensure that the algorithms are working at all. 271 | - Uses another language (Golang-Mutation-testing). 272 | 273 | ## Can I make feature requests and report bugs and problems? 274 | 275 | Sure, just submit an [issue via the project tracker](https://github.com/zimmski/go-mutesting/issues/new) and I will see what I can do. Please note that I do not guarantee to implement anything soon and bugs and problems are more important to me than new features. If you need something implemented or fixed right away you can contact me via mail to do contract work for you. 276 | --------------------------------------------------------------------------------