github.com/mshitrit/go-mutesting@v0.0.0-20210528084812-ff81dcaedfea/cmd/go-mutesting/main.go (about)

     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(total - ms.failed) / 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  }