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 }