github.com/fnando/bolt@v0.0.4-0.20231107225351-5241e4d187b8/internal/commands/run.go (about) 1 package commands 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "flag" 8 "fmt" 9 "os" 10 "os/exec" 11 "strings" 12 "time" 13 14 c "github.com/fnando/bolt/common" 15 "github.com/fnando/bolt/internal/reporters" 16 "github.com/joho/godotenv" 17 ) 18 19 type RunArgs struct { 20 Compat bool 21 CoverageCount int 22 CoverageThreshold float64 23 Debug bool 24 Dotenv string 25 HideCoverage bool 26 HideSlowest bool 27 HomeDir string 28 NoColor bool 29 Raw bool 30 Replay string 31 Reporter string 32 SlowestCount int 33 SlowestThreshold string 34 WorkingDir string 35 PostRunCommand string 36 } 37 38 var usage string = ` 39 Run tests by wrapping "go tests". 40 41 Usage: bolt [options] [packages...] -- [additional "go test" arguments] 42 43 Options: 44 %s 45 46 Available reporters: 47 progress 48 Print a character for each test, with a test summary and list of 49 failed/skipped tests. 50 51 json 52 Print a JSON representation of the bolt state. 53 54 55 How it works: 56 This is what bolt runs if you execute "bolt ./...": 57 58 $ go test ./... -cover -json -fullpath 59 60 You can pass additional arguments to the "go test" command like this: 61 62 $ bolt ./... -- -run TestExample 63 64 These arguments will be appended to the default arguments used by bolt. 65 The example above would be executed like this: 66 67 $ go test -cover -json -fullpath -run TestExample ./... 68 69 To execute a raw "go test" command, use the switch --raw. This will avoid 70 default arguments from being added to the final execution. In practice, it 71 means you'll need to run the whole command: 72 73 $ bolt --raw -- ./some_module -run TestExample 74 75 Note: -fullpath was introduced on go 1.21. If you're using an older 76 version, you can use --compat or manually set arguments by using --raw. 77 78 79 Env files: 80 bolt will load .env.test by default. You can also set it to a 81 different file by using --env. If you want to disable env files 82 completely, use --env=false. 83 84 85 Color: 86 bolt will output colored text based on ANSI colors. By default, the 87 following env vars will be used and you can override any of them to set 88 a custom color: 89 90 export BOLT_TEXT_COLOR="30" 91 export BOLT_FAIL_COLOR="31" 92 export BOLT_PASS_COLOR="32" 93 export BOLT_SKIP_COLOR="33" 94 export BOLT_DETAIL_COLOR="34" 95 96 To disable colored output you can use "--no-color" or 97 set the env var NO_COLOR=1. 98 99 100 Progress reporter: 101 You can override the default progress symbols by setting env vars. The 102 following example shows how to use emojis instead: 103 104 export BOLT_FAIL_SYMBOL=❌ 105 export BOLT_PASS_SYMBOL=⚡️ 106 export BOLT_SKIP_SYMBOL=😴 107 108 109 Post run command: 110 You can run any commands after the runner is done by using 111 --post-run-command. The command will receive the following environment 112 variables. 113 114 BOLT_SUMMARY 115 a text summarizing the tests 116 BOLT_TITLE 117 a text that can be used as the title (e.g. Passed!) 118 BOLT_TEST_COUNT 119 a number representing the total number of tests 120 BOLT_FAIL_COUNT 121 a number representing the total number of failed tests 122 BOLT_PASS_COUNT 123 a number representing the total number of passing tests 124 BOLT_SKIP_COUNT 125 a number representing the total number of skipped tests 126 BOLT_BENCHMARK_COUNT 127 a number representing the total number of benchmarks 128 BOLT_ELAPSED 129 a string representing the duration (e.g. 1m20s) 130 BOLT_ELAPSED_NANOSECONDS 131 an integer string representing the duration in nanoseconds 132 133 ` 134 135 func Run(args []string, options RunArgs, output *c.Output) int { 136 flags := flag.NewFlagSet("bolt", flag.ContinueOnError) 137 flags.Usage = func() {} 138 139 flags.BoolVar( 140 &options.NoColor, 141 "no-color", 142 false, 143 "Disable colored output. When unset, respects the NO_COLOR=1 env var", 144 ) 145 146 flags.BoolVar(&options.Raw, "raw", false, "Don't append arguments to `go test`") 147 flags.BoolVar(&options.Compat, "compat", false, "Don't append -fullpath, available on go 1.21 or new") 148 flags.BoolVar(&options.HideCoverage, "hide-coverage", false, "Don't display the coverage section") 149 flags.BoolVar(&options.HideSlowest, "hide-slowest", false, "Don't display the slowest tests section") 150 flags.StringVar(&options.Dotenv, "env", ".env.test", "Load env file") 151 flags.IntVar(&options.CoverageCount, "coverage-count", 10, "Number of coverate items to show") 152 flags.Float64Var(&options.CoverageThreshold, "coverage-threshold", 100.0, "Anything below this threshold will be listed") 153 flags.StringVar(&options.SlowestThreshold, "slowest-threshold", "1s", "Anything above this threshold will be listed. Must be a valid duration string") 154 flags.IntVar(&options.SlowestCount, "slowest-count", 10, "Number of slowest tests to show") 155 flags.StringVar(&options.PostRunCommand, "post-run-command", "", "Run a command after runner is done") 156 157 flags.BoolVar(&options.Debug, "debug", false, "") 158 flags.StringVar(&options.Replay, "replay", "", "") 159 flags.StringVar(&options.Reporter, "reporter", "progress", "") 160 161 flags.SetOutput(bufio.NewWriter(&bytes.Buffer{})) 162 err := flags.Parse(args) 163 164 if options.Dotenv != "false" { 165 dotenvErr := godotenv.Load(options.Dotenv) 166 167 if dotenvErr != nil { 168 ignore := strings.Contains(dotenvErr.Error(), "no such file") && 169 options.Dotenv == ".env.test" 170 171 if !ignore { 172 err = dotenvErr 173 } 174 } 175 } 176 177 if options.Debug { 178 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" version:", c.Version) 179 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" arch:", c.Arch) 180 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" commit:", c.Commit) 181 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" working dir:", options.WorkingDir) 182 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" home dir:", options.HomeDir) 183 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" reporter:", options.Reporter) 184 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" env file:", options.Dotenv) 185 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" compat:", options.Compat) 186 187 if options.Replay != "" { 188 fmt.Fprintln(output.Stdout, c.Color.Detail("⚡️")+" replay file:", options.Replay) 189 } 190 } 191 192 if err == flag.ErrHelp { 193 fmt.Fprintf(output.Stdout, usage, getFlagsUsage(flags)) 194 return 0 195 } else if err != nil { 196 fmt.Fprintf(output.Stderr, "%s %v\n", c.Color.Fail("ERROR:"), err) 197 return 1 198 } 199 200 slowestThreshold, err := time.ParseDuration(options.SlowestThreshold) 201 202 if err != nil { 203 fmt.Fprintf(output.Stderr, "%s %v\n", c.Color.Fail("ERROR:"), err) 204 return 1 205 } 206 207 exitcode := 1 208 consumer := c.StreamConsumer{ 209 Aggregation: &c.Aggregation{ 210 TestsMap: map[string]*c.Test{}, 211 CoverageMap: map[string]*c.Coverage{}, 212 BenchmarksMap: map[string]*c.Benchmark{}, 213 CoverageThreshold: options.CoverageThreshold, 214 CoverageCount: options.CoverageCount, 215 SlowestThreshold: slowestThreshold, 216 SlowestCount: options.SlowestCount, 217 }, 218 } 219 220 reporterList := []reporters.Reporter{ 221 reporters.PostRunCommandReporter{Output: output, Command: options.PostRunCommand}, 222 } 223 224 if options.Reporter == "progress" { 225 reporterList = append(reporterList, reporters.ProgressReporter{Output: output}) 226 } else if options.Reporter == "standard" { 227 reporterList = append(reporterList, reporters.StandardReporter{Output: output}) 228 } else if options.Reporter == "json" { 229 reporterList = append(reporterList, reporters.JSONReporter{Output: output}) 230 } else { 231 fmt.Fprintf(output.Stderr, "%s %s\n", c.Color.Fail("ERROR:"), "Invalid reporter") 232 return 1 233 } 234 235 consumer.OnData = func(line string) { 236 for _, reporter := range reporterList { 237 reporter.OnData(line) 238 } 239 } 240 241 consumer.OnProgress = func(test c.Test) { 242 for _, reporter := range reporterList { 243 reporter.OnProgress(test) 244 } 245 } 246 247 consumer.OnFinished = func(aggregation *c.Aggregation) { 248 reporterOptions := reporters.ReporterFinishedOptions{ 249 Aggregation: aggregation, 250 HideCoverage: options.HideCoverage, 251 HideSlowest: options.HideSlowest, 252 Debug: options.Debug, 253 } 254 255 for _, reporter := range reporterList { 256 reporter.OnFinished(reporterOptions) 257 } 258 } 259 260 if options.Replay == "" { 261 execArgs := append([]string{"-json", "-cover"}) 262 263 if !options.Compat { 264 execArgs = append(execArgs, "-fullpath") 265 } 266 267 extraArgs := []string{} 268 269 for _, arg := range flags.Args() { 270 if arg != "--" { 271 extraArgs = append(extraArgs, arg) 272 } 273 } 274 275 execArgs = append(execArgs, extraArgs...) 276 277 if options.Raw { 278 execArgs = flags.Args() 279 } 280 281 if options.Debug { 282 fmt.Fprintln( 283 output.Stdout, 284 c.Color.Detail("⚡️"), 285 "command:", 286 "go test", 287 strings.Join(execArgs, " "), 288 ) 289 } 290 291 exitcode, err = Exec(&consumer, output, execArgs) 292 } else { 293 exitcode, err = Replay(&consumer, &options) 294 } 295 296 if err != nil { 297 fmt.Fprintf(output.Stderr, "%s %v\n", c.Color.Fail("ERROR:"), err) 298 return 1 299 } 300 301 return exitcode 302 } 303 304 func Replay(consumer *c.StreamConsumer, options *RunArgs) (int, error) { 305 stat, err := os.Stat(options.Replay) 306 307 if os.IsNotExist(err) { 308 return 1, errors.New("replay file doesn't exist") 309 } 310 311 if stat.IsDir() { 312 return 1, errors.New("can't read directory (" + options.Replay + ")") 313 } 314 315 file, err := os.Open(options.Replay) 316 317 if err != nil { 318 return 1, err 319 } 320 321 defer file.Close() 322 scanner := bufio.NewScanner(file) 323 scanner.Split(bufio.ScanLines) 324 325 consumer.Ingest(scanner) 326 327 return consumer.Aggregation.CountBy("fail"), nil 328 } 329 330 func Exec(consumer *c.StreamConsumer, output *c.Output, args []string) (int, error) { 331 args = append([]string{"test"}, args...) 332 cmd := exec.Command("go", args...) 333 cmd.Stderr = cmd.Stdout 334 out, _ := cmd.StdoutPipe() 335 scanner := bufio.NewScanner(out) 336 scanner.Split(bufio.ScanLines) 337 338 err := cmd.Start() 339 340 if err != nil { 341 return 1, err 342 } 343 344 consumer.Ingest(scanner) 345 346 err = cmd.Wait() 347 348 if err != nil { 349 return 1, err 350 } 351 352 exitcode := 0 353 354 if exiterr, ok := err.(*exec.ExitError); ok { 355 exitcode = exiterr.ExitCode() 356 return exitcode, nil 357 } 358 359 return exitcode, err 360 }