Skip to content

Commit

Permalink
Merge pull request #46 from zimmski/switch-to-go-types
Browse files Browse the repository at this point in the history
Do type-checking for perfect statement elimination, and add duplicates to the cmd statistic
  • Loading branch information
zimmski committed Sep 12, 2016
2 parents f1fdf12 + fc38c53 commit adf20bc
Show file tree
Hide file tree
Showing 32 changed files with 625 additions and 144 deletions.
9 changes: 5 additions & 4 deletions astutil/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ package astutil
import (
"go/ast"
"go/token"
"go/types"
)

// CreateNoopOfStatement creates a syntactically safe noop statement out of a given statement.
func CreateNoopOfStatement(stmt ast.Stmt) ast.Stmt {
return CreateNoopOfStatements([]ast.Stmt{stmt})
func CreateNoopOfStatement(pkg *types.Package, info *types.Info, stmt ast.Stmt) ast.Stmt {
return CreateNoopOfStatements(pkg, info, []ast.Stmt{stmt})
}

// CreateNoopOfStatements creates a syntactically safe noop statement out of a given statement.
func CreateNoopOfStatements(stmts []ast.Stmt) ast.Stmt {
func CreateNoopOfStatements(pkg *types.Package, info *types.Info, stmts []ast.Stmt) ast.Stmt {
var ids []ast.Expr
for _, stmt := range stmts {
ids = append(ids, IdentifiersInStatement(stmt)...)
ids = append(ids, IdentifiersInStatement(pkg, info, stmt)...)
}

if len(ids) == 0 {
Expand Down
125 changes: 48 additions & 77 deletions astutil/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package astutil

import (
"go/ast"
"go/token"
"go/types"
)

// IdentifiersInStatement returns all identifiers with their found in a statement.
func IdentifiersInStatement(stmt ast.Stmt) []ast.Expr {
w := &identifierWalker{}
func IdentifiersInStatement(pkg *types.Package, info *types.Info, stmt ast.Stmt) []ast.Expr {
w := &identifierWalker{
pkg: pkg,
info: info,
}

ast.Walk(w, stmt)

Expand All @@ -15,78 +20,8 @@ func IdentifiersInStatement(stmt ast.Stmt) []ast.Expr {

type identifierWalker struct {
identifiers []ast.Expr
}

var blacklistedIdentifiers = map[string]bool{
// blank identifier
"_": true,
// builtin - can be used as identifier but are unlikely to be in practice
// (except perhaps with panic, defer, recover, print, prinln?)
"bool": true,
"true": true,
"false": true,
"uint8": true,
"uint16": true,
"uint32": true,
"uint64": true,
"int8": true,
"int16": true,
"int32": true,
"int64": true,
"float32": true,
"float64": true,
"complex64": true,
"complex128": true,
"string": true,
"int": true,
"uint": true,
"uintptr": true,
"byte": true,
"rune": true,
"iota": true,
"nil": true,
"append": true,
"copy": true,
"delete": true,
"len": true,
"cap": true,
"make": true,
"new": true,
"complex": true,
"real": true,
"imag": true,
"close": true,
"panic": true,
"recover": true,
"print": true,
"println": true,
"error": true,
// reserved keywords - cannot be used as identifier.
"break": true,
"default": true,
"func": true,
"interface": true,
"select": true,
"case": true,
"defer": true,
"go": true,
"map": true,
"struct": true,
"chan": true,
"else": true,
"goto": true,
"package": true,
"switch": true,
"const": true,
"fallthrough": true,
"if": true,
"range": true,
"type": true,
"continue": true,
"for": true,
"import": true,
"return": true,
"var": true,
pkg *types.Package
info *types.Info
}

func checkForSelectorExpr(node ast.Expr) bool {
Expand All @@ -103,13 +38,49 @@ func checkForSelectorExpr(node ast.Expr) bool {
func (w *identifierWalker) Visit(node ast.Node) ast.Visitor {
switch n := node.(type) {
case *ast.Ident:
if _, ok := blacklistedIdentifiers[n.Name]; !ok {
w.identifiers = append(w.identifiers, n)
// Ignore the blank identifier
if n.Name == "_" {
return nil
}

// Ignore keywords
if token.Lookup(n.Name) != token.IDENT {
return nil
}

// We are only interested in variables
if obj, ok := w.info.Uses[n]; ok {
if _, ok := obj.(*types.Var); !ok {
return nil
}
}

w.identifiers = append(w.identifiers, n)

return nil
case *ast.SelectorExpr:
if checkForSelectorExpr(n) {
if !checkForSelectorExpr(n) {
return nil
}

// Check if we need to instantiate the expression
initialize := false
if n.Sel != nil {
if obj, ok := w.info.Uses[n.Sel]; ok {
t := obj.Type()

switch t.Underlying().(type) {
case *types.Array, *types.Map, *types.Slice, *types.Struct:
initialize = true
}
}
}

if initialize {
w.identifiers = append(w.identifiers, &ast.CompositeLit{
Type: n,
})
} else {
w.identifiers = append(w.identifiers, n)
}

Expand Down
60 changes: 39 additions & 21 deletions cmd/go-mutesting/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"go/ast"
"go/build"
"go/format"
"go/importer"
"go/printer"
"go/token"
"go/types"
"io"
"io/ioutil"
"os"
Expand Down Expand Up @@ -144,9 +146,10 @@ type mutatorItem struct {
}

type mutationStats struct {
passed int
failed int
skipped int
passed int
failed int
duplicated int
skipped int
}

func mainCmd(args []string) int {
Expand Down Expand Up @@ -250,6 +253,29 @@ MUTATOR:
return exitError("Could not open file %q: %v", file, err)
}

dir, err := filepath.Abs(filepath.Dir(file))
if err != nil {
return exitError("Could not absolute the file path of %q: %v", file, err)
}

buildPkg, err := build.ImportDir(dir, build.FindOnly)
if err != nil {
return exitError("Could create build package of %q: %v", file, err)
}

conf := types.Config{
Importer: importer.Default(),
}

info := &types.Info{
Uses: make(map[*ast.Ident]types.Object),
}

pkg, err := conf.Check(buildPkg.ImportPath, fset, []*ast.File{src}, info) // TODO query the import path without the additional go/build.ImportDirt step
if err != nil {
return exitError("Could not type check file %q: %v", file, err)
}

err = os.MkdirAll(tmpDir+"/"+filepath.Dir(file), 0755)
if err != nil {
panic(err)
Expand All @@ -274,11 +300,11 @@ MUTATOR:

for _, f := range astutil.Functions(src) {
if m.MatchString(f.Name.Name) {
mutationID = mutate(opts, mutators, mutationBlackList, mutationID, file, fset, src, f, tmpFile, execs, stats)
mutationID = mutate(opts, mutators, mutationBlackList, mutationID, pkg, info, file, fset, src, f, tmpFile, execs, stats)
}
}
} else {
mutationID = mutate(opts, mutators, mutationBlackList, mutationID, file, fset, src, src, tmpFile, execs, stats)
mutationID = mutate(opts, mutators, mutationBlackList, mutationID, pkg, info, file, fset, src, src, tmpFile, execs, stats)
}
}

Expand All @@ -291,29 +317,19 @@ MUTATOR:
}

if !opts.Exec.NoExec {
fmt.Printf("The mutation score is %f (%d passed, %d failed, %d skipped, total is %d)\n", float64(stats.passed)/float64(stats.passed+stats.failed), stats.passed, stats.failed, stats.skipped, stats.passed+stats.failed+stats.skipped)
fmt.Printf("The mutation score is %f (%d passed, %d failed, %d duplicated, %d skipped, total is %d)\n", float64(stats.passed)/float64(stats.passed+stats.failed), stats.passed, stats.failed, stats.duplicated, stats.skipped, stats.passed+stats.failed+stats.duplicated+stats.skipped)
} else {
fmt.Println("Cannot do a mutation testing summary since no exec command was executed.")
}

return returnOk
}

func mutate(opts *options, mutators []mutatorItem, mutationBlackList map[string]struct{}, mutationID int, file string, fset *token.FileSet, src ast.Node, node ast.Node, tmpFile string, execs []string, stats *mutationStats) int {
dir, err := filepath.Abs(filepath.Dir(file))
if err != nil {
panic(err)
}

pkg, err := build.ImportDir(dir, build.FindOnly)
if err != nil {
panic(err)
}

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 {
for _, m := range mutators {
debug(opts, "Mutator %s", m.Name)

changed := mutesting.MutateWalk(node, m.Mutator)
changed := mutesting.MutateWalk(pkg, info, node, m.Mutator)

for {
_, ok := <-changed
Expand All @@ -329,6 +345,8 @@ func mutate(opts *options, mutators []mutatorItem, mutationBlackList map[string]
}
if duplicate {
debug(opts, "%q is a duplicate, we ignore it", mutationFile)

stats.duplicated++
} else {
debug(opts, "Save mutation into %q with checksum %s", mutationFile, checksum)

Expand Down Expand Up @@ -371,7 +389,7 @@ func mutate(opts *options, mutators []mutatorItem, mutationBlackList map[string]
return mutationID
}

func mutateExec(opts *options, pkg *build.Package, file string, src ast.Node, mutationFile string, execs []string) (execExitCode int) {
func mutateExec(opts *options, pkg *types.Package, file string, src ast.Node, mutationFile string, execs []string) (execExitCode int) {
if len(execs) == 0 {
debug(opts, "Execute built-in exec command for mutation")

Expand Down Expand Up @@ -402,7 +420,7 @@ func mutateExec(opts *options, pkg *build.Package, file string, src ast.Node, mu
panic(err)
}

pkgName := pkg.ImportPath
pkgName := pkg.Path()
if opts.Test.Recursive {
pkgName += "/..."
}
Expand Down Expand Up @@ -458,7 +476,7 @@ func mutateExec(opts *options, pkg *build.Package, file string, src ast.Node, mu
"MUTATE_CHANGED=" + mutationFile,
fmt.Sprintf("MUTATE_DEBUG=%t", opts.General.Debug),
"MUTATE_ORIGINAL=" + file,
"MUTATE_PACKAGE=" + pkg.ImportPath,
"MUTATE_PACKAGE=" + pkg.Path(),
fmt.Sprintf("MUTATE_TIMEOUT=%d", opts.Exec.Timeout),
fmt.Sprintf("MUTATE_VERBOSE=%t", opts.General.Verbose),
}...)
Expand Down
8 changes: 4 additions & 4 deletions cmd/go-mutesting/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestMain(t *testing.T) {
"../../example",
[]string{"--debug", "--exec-timeout", "1"},
returnOk,
"The mutation score is 0.500000 (7 passed, 7 failed, 0 skipped, total is 14)",
"The mutation score is 0.538462 (7 passed, 6 failed, 9 duplicated, 0 skipped, total is 22)",
)
}

Expand All @@ -25,7 +25,7 @@ func TestMainRecursive(t *testing.T) {
"../../example",
[]string{"--debug", "--exec-timeout", "1", "./..."},
returnOk,
"The mutation score is 0.533333 (8 passed, 7 failed, 0 skipped, total is 15)",
"The mutation score is 0.571429 (8 passed, 6 failed, 9 duplicated, 0 skipped, total is 23)",
)
}

Expand All @@ -35,7 +35,7 @@ func TestMainFromOtherDirectory(t *testing.T) {
"../..",
[]string{"--debug", "--exec-timeout", "1", "github.com/zimmski/go-mutesting/example"},
returnOk,
"The mutation score is 0.500000 (7 passed, 7 failed, 0 skipped, total is 14)",
"The mutation score is 0.538462 (7 passed, 6 failed, 9 duplicated, 0 skipped, total is 22)",
)
}

Expand All @@ -45,7 +45,7 @@ func TestMainMatch(t *testing.T) {
"../../example",
[]string{"--debug", "--exec", "../scripts/exec/test-mutated-package.sh", "--exec-timeout", "1", "--match", "baz", "./..."},
returnOk,
"The mutation score is 0.500000 (1 passed, 1 failed, 0 skipped, total is 2)",
"The mutation score is 0.500000 (1 passed, 1 failed, 0 duplicated, 0 skipped, total is 2)",
)
}

Expand Down
5 changes: 3 additions & 2 deletions mutator/branch/mutatecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package branch

import (
"go/ast"
"go/types"

"github.com/zimmski/go-mutesting/astutil"
"github.com/zimmski/go-mutesting/mutator"
Expand All @@ -12,7 +13,7 @@ func init() {
}

// MutatorCase implements a mutator for case clauses.
func MutatorCase(node ast.Node) []mutator.Mutation {
func MutatorCase(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation {
n, ok := node.(*ast.CaseClause)
if !ok {
return nil
Expand All @@ -24,7 +25,7 @@ func MutatorCase(node ast.Node) []mutator.Mutation {
mutator.Mutation{
Change: func() {
n.Body = []ast.Stmt{
astutil.CreateNoopOfStatements(n.Body),
astutil.CreateNoopOfStatements(pkg, info, n.Body),
}
},
Reset: func() {
Expand Down

0 comments on commit adf20bc

Please sign in to comment.