github.com/go-maxhub/gremlins@v1.0.1-0.20231227222204-b03a6a1e3e09/cmd/unleash.go (about)

     1  /*
     2   * Copyright 2022 The Gremlins Authors
     3   *
     4   *    Licensed under the Apache License, Version 2.0 (the "License");
     5   *    you may not use this file except in compliance with the License.
     6   *    You may obtain a copy of the License at
     7   *
     8   *        http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   *    Unless required by applicable law or agreed to in writing, software
    11   *    distributed under the License is distributed on an "AS IS" BASIS,
    12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   *    See the License for the specific language governing permissions and
    14   *    limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  	"sync"
    25  
    26  	"github.com/MakeNowJust/heredoc"
    27  	"github.com/spf13/cobra"
    28  	"github.com/spf13/pflag"
    29  
    30  	"github.com/go-maxhub/gremlins/core/coverage"
    31  	"github.com/go-maxhub/gremlins/core/diff"
    32  	"github.com/go-maxhub/gremlins/core/engine"
    33  	"github.com/go-maxhub/gremlins/core/engine/workdir"
    34  	"github.com/go-maxhub/gremlins/core/log"
    35  	"github.com/go-maxhub/gremlins/core/mutator"
    36  	"github.com/go-maxhub/gremlins/core/report"
    37  
    38  	"github.com/go-maxhub/gremlins/cmd/flags"
    39  	"github.com/go-maxhub/gremlins/core/configuration"
    40  	"github.com/go-maxhub/gremlins/core/gomodule"
    41  )
    42  
    43  type unleashCmd struct {
    44  	cmd *cobra.Command
    45  }
    46  
    47  const (
    48  	commandName = "unleash"
    49  
    50  	paramDiff               = "diff"
    51  	paramBuildTags          = "tags"
    52  	paramCoverPackages      = "coverpkg"
    53  	paramDryRun             = "dry-run"
    54  	paramOutput             = "output"
    55  	paramIntegrationMode    = "integration"
    56  	paramTestCPU            = "test-cpu"
    57  	paramWorkers            = "workers"
    58  	paramTimeoutCoefficient = "timeout-coefficient"
    59  
    60  	// Thresholds.
    61  	paramThresholdEfficacy  = "threshold-efficacy"
    62  	paramThresholdMCoverage = "threshold-mcover"
    63  )
    64  
    65  func newUnleashCmd(ctx context.Context) (*unleashCmd, error) {
    66  	cmd := &cobra.Command{
    67  		Use:     fmt.Sprintf("%s [path]", commandName),
    68  		Aliases: []string{"run", "r"},
    69  		Args:    cobra.MaximumNArgs(1),
    70  		Short:   "Unleash the gremlins",
    71  		Long:    longExplainer(),
    72  		RunE:    runUnleash(ctx),
    73  	}
    74  
    75  	if err := setFlagsOnCmd(cmd); err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	return &unleashCmd{cmd: cmd}, nil
    80  }
    81  
    82  func longExplainer() string {
    83  	return heredoc.Doc(`
    84  		Unleashes the gremlins and performs mutation testing on a Go module. It works by
    85  		first gathering the coverage of the test suite and then analysing the source
    86  		code to look for supported mutants.
    87  
    88  		Unleash only tests covered mutants, since it doesn't make sense to test mutants 
    89  		that no test case is able to catch.
    90  
    91  		In 'dry-run' mode, unleash only performs the analysis of the source code, but it
    92  		doesn't actually perform the test.
    93  
    94  		Thresholds are configurable quality gates that make gremlins exit with an error 
    95  		if those values are not met. Efficacy is the percent of KILLED mutants over
    96  		the total KILLED and LIVED mutants. Mutant coverage is the percent of total
    97  		KILLED + LIVED mutants, over the total mutants.
    98  	`)
    99  }
   100  
   101  func runUnleash(ctx context.Context) func(cmd *cobra.Command, args []string) error {
   102  	return func(cmd *cobra.Command, args []string) error {
   103  		log.Infoln("Starting...")
   104  		path, _ := os.Getwd()
   105  		if len(args) > 0 {
   106  			path = args[0]
   107  		}
   108  		mod, err := gomodule.Init(path)
   109  		if err != nil {
   110  			return fmt.Errorf("not in a Go module: %w", err)
   111  		}
   112  
   113  		workDir, err := os.MkdirTemp(os.TempDir(), "gremlins-")
   114  		if err != nil {
   115  			return fmt.Errorf("impossible to create the workdir: %w", err)
   116  		}
   117  		defer cleanUp(workDir)
   118  
   119  		wg := &sync.WaitGroup{}
   120  		wg.Add(1)
   121  		cancelled := false
   122  		var results report.Results
   123  		go runWithCancel(ctx, wg, func(c context.Context) {
   124  			results, err = run(c, mod, workDir)
   125  		}, func() {
   126  			cancelled = true
   127  		})
   128  		wg.Wait()
   129  		if err != nil {
   130  			return err
   131  		}
   132  		if cancelled {
   133  			return nil
   134  		}
   135  
   136  		return report.Do(results)
   137  	}
   138  }
   139  
   140  func runWithCancel(ctx context.Context, wg *sync.WaitGroup, runner func(c context.Context), onCancel func()) {
   141  	c, cancel := context.WithCancel(ctx)
   142  	go func() {
   143  		<-ctx.Done()
   144  		log.Infof("\nShutting down gracefully...\n")
   145  		cancel()
   146  		onCancel()
   147  	}()
   148  	runner(c)
   149  	wg.Done()
   150  }
   151  
   152  func cleanUp(wd string) {
   153  	if err := os.RemoveAll(wd); err != nil {
   154  		log.Errorf("impossible to remove temporary folder: %s\n\t%s", err, wd)
   155  	}
   156  }
   157  
   158  func run(ctx context.Context, mod gomodule.GoModule, workDir string) (report.Results, error) {
   159  	fDiff, err := diff.New()
   160  	if err != nil {
   161  		return report.Results{}, err
   162  	}
   163  
   164  	c := coverage.New(workDir, mod)
   165  
   166  	cProfile, err := c.Run()
   167  	if err != nil {
   168  		return report.Results{}, fmt.Errorf("failed to gather coverage: %w", err)
   169  	}
   170  
   171  	wdDealer := workdir.NewCachedDealer(workDir, mod.Root)
   172  	defer wdDealer.Clean()
   173  
   174  	jDealer := engine.NewExecutorDealer(mod, wdDealer, cProfile.Elapsed)
   175  
   176  	codeData := engine.CodeData{
   177  		Cov:  cProfile.Profile,
   178  		Diff: fDiff,
   179  	}
   180  
   181  	mut := engine.New(mod, codeData, jDealer)
   182  	results := mut.Run(ctx)
   183  
   184  	return results, nil
   185  }
   186  
   187  func setFlagsOnCmd(cmd *cobra.Command) error {
   188  	cmd.Flags().SortFlags = false
   189  	cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
   190  		from := []string{".", "_"}
   191  		to := "-"
   192  		for _, sep := range from {
   193  			name = strings.ReplaceAll(name, sep, to)
   194  		}
   195  
   196  		return pflag.NormalizedName(name)
   197  	})
   198  
   199  	fls := []*flags.Flag{
   200  		{Name: paramDryRun, CfgKey: configuration.UnleashDryRunKey, Shorthand: "d", DefaultV: false, Usage: "find mutations but do not executes tests"},
   201  		{Name: paramBuildTags, CfgKey: configuration.UnleashTagsKey, Shorthand: "t", DefaultV: "", Usage: "a comma-separated list of build tags"},
   202  		{Name: paramCoverPackages, CfgKey: configuration.UnleashCoverPkgKey, DefaultV: "", Usage: "a comma-separated list of package patterns"},
   203  		{Name: paramDiff, CfgKey: configuration.UnleashDiffRef, Shorthand: "D", DefaultV: "", Usage: "diff branch or commit"},
   204  		{Name: paramOutput, CfgKey: configuration.UnleashOutputKey, Shorthand: "o", DefaultV: "", Usage: "set the output file for machine readable results"},
   205  		{Name: paramIntegrationMode, CfgKey: configuration.UnleashIntegrationMode, Shorthand: "i", DefaultV: false, Usage: "makes Gremlins run the complete test suite for each mutation"},
   206  		{Name: paramThresholdEfficacy, CfgKey: configuration.UnleashThresholdEfficacyKey, DefaultV: float64(0), Usage: "threshold for code-efficacy percent"},
   207  		{Name: paramThresholdMCoverage, CfgKey: configuration.UnleashThresholdMCoverageKey, DefaultV: float64(0), Usage: "threshold for mutant-coverage percent"},
   208  		{Name: paramWorkers, CfgKey: configuration.UnleashWorkersKey, DefaultV: 0, Usage: "the number of workers to use in mutation testing"},
   209  		{Name: paramTestCPU, CfgKey: configuration.UnleashTestCPUKey, DefaultV: 0, Usage: "the number of CPUs to allow each test run to use"},
   210  		{Name: paramTimeoutCoefficient, CfgKey: configuration.UnleashTimeoutCoefficientKey, DefaultV: 0, Usage: "the coefficient by which the timeout is increased"},
   211  	}
   212  
   213  	for _, f := range fls {
   214  		err := flags.Set(cmd, f)
   215  		if err != nil {
   216  			return err
   217  		}
   218  	}
   219  
   220  	return setMutantTypeFlags(cmd)
   221  }
   222  
   223  func setMutantTypeFlags(cmd *cobra.Command) error {
   224  	for _, mt := range mutator.Types {
   225  		name := mt.String()
   226  		usage := fmt.Sprintf("enable %q mutants", name)
   227  		param := strings.ReplaceAll(name, "_", "-")
   228  		param = strings.ToLower(param)
   229  		confKey := configuration.MutantTypeEnabledKey(mt)
   230  
   231  		err := flags.Set(cmd, &flags.Flag{
   232  			Name:     param,
   233  			CfgKey:   confKey,
   234  			DefaultV: configuration.IsDefaultEnabled(mt),
   235  			Usage:    usage,
   236  		})
   237  		if err != nil {
   238  			return err
   239  		}
   240  	}
   241  
   242  	return nil
   243  }