gotest.tools/gotestsum@v1.11.0/cmd/rerunfails.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "regexp" 8 "sort" 9 "strings" 10 11 "gotest.tools/gotestsum/testjson" 12 ) 13 14 type rerunOpts struct { 15 runFlag string 16 pkg string 17 } 18 19 func (o rerunOpts) Args() []string { 20 var result []string 21 if o.runFlag != "" { 22 result = append(result, o.runFlag) 23 } 24 if o.pkg != "" { 25 result = append(result, o.pkg) 26 } 27 return result 28 } 29 30 func newRerunOptsFromTestCase(tc testjson.TestCase) rerunOpts { 31 return rerunOpts{ 32 runFlag: goTestRunFlagForTestCase(tc.Test), 33 pkg: tc.Package, 34 } 35 } 36 37 type testCaseFilter func([]testjson.TestCase) []testjson.TestCase 38 39 func rerunFailsFilter(o *options) testCaseFilter { 40 if o.rerunFailsRunRootCases { 41 return func(tcs []testjson.TestCase) []testjson.TestCase { 42 var result []testjson.TestCase 43 for _, tc := range tcs { 44 if !tc.Test.IsSubTest() { 45 result = append(result, tc) 46 } 47 } 48 return result 49 } 50 } 51 return testjson.FilterFailedUnique 52 } 53 54 func rerunFailed(ctx context.Context, opts *options, scanConfig testjson.ScanConfig) error { 55 ctx, cancel := context.WithCancel(ctx) 56 defer cancel() 57 tcFilter := rerunFailsFilter(opts) 58 59 rec := newFailureRecorderFromExecution(scanConfig.Execution) 60 for attempts := 0; rec.count() > 0 && attempts < opts.rerunFailsMaxAttempts; attempts++ { 61 testjson.PrintSummary(opts.stdout, scanConfig.Execution, testjson.SummarizeNone) 62 opts.stdout.Write([]byte("\n")) // nolint: errcheck 63 64 nextRec := newFailureRecorder(scanConfig.Handler) 65 for _, tc := range tcFilter(rec.failures) { 66 goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, newRerunOptsFromTestCase(tc))) 67 if err != nil { 68 return err 69 } 70 71 cfg := testjson.ScanConfig{ 72 RunID: attempts + 1, 73 Stdout: goTestProc.stdout, 74 Stderr: goTestProc.stderr, 75 Handler: nextRec, 76 Execution: scanConfig.Execution, 77 Stop: cancel, 78 } 79 if _, err := testjson.ScanTestOutput(cfg); err != nil { 80 return err 81 } 82 exitErr := goTestProc.cmd.Wait() 83 if exitErr != nil { 84 nextRec.lastErr = exitErr 85 } 86 if err := hasErrors(exitErr, scanConfig.Execution); err != nil { 87 return err 88 } 89 } 90 rec = nextRec 91 } 92 return rec.lastErr 93 } 94 95 // startGoTestFn is a shim for testing 96 var startGoTestFn = startGoTest 97 98 func hasErrors(err error, exec *testjson.Execution) error { 99 switch { 100 case len(exec.Errors()) > 0: 101 return fmt.Errorf("rerun aborted because previous run had errors") 102 // Exit code 0 and 1 are expected. 103 case ExitCodeWithDefault(err) > 1: 104 return fmt.Errorf("unexpected go test exit code: %v", err) 105 case exec.HasPanic(): 106 return fmt.Errorf("rerun aborted because previous run had a suspected panic and some test may not have run") 107 default: 108 return nil 109 } 110 } 111 112 type failureRecorder struct { 113 testjson.EventHandler 114 failures []testjson.TestCase 115 lastErr error 116 } 117 118 func newFailureRecorder(handler testjson.EventHandler) *failureRecorder { 119 return &failureRecorder{EventHandler: handler} 120 } 121 122 func newFailureRecorderFromExecution(exec *testjson.Execution) *failureRecorder { 123 return &failureRecorder{failures: exec.Failed()} 124 } 125 126 func (r *failureRecorder) Event(event testjson.TestEvent, execution *testjson.Execution) error { 127 if !event.PackageEvent() && event.Action == testjson.ActionFail { 128 pkg := execution.Package(event.Package) 129 tc := pkg.LastFailedByName(event.Test) 130 r.failures = append(r.failures, tc) 131 } 132 return r.EventHandler.Event(event, execution) 133 } 134 135 func (r *failureRecorder) count() int { 136 return len(r.failures) 137 } 138 139 func goTestRunFlagForTestCase(test testjson.TestName) string { 140 if test.IsSubTest() { 141 parts := strings.Split(string(test), "/") 142 var sb strings.Builder 143 sb.WriteString("-test.run=") 144 for i, p := range parts { 145 if i > 0 { 146 sb.WriteByte('/') 147 } 148 sb.WriteByte('^') 149 sb.WriteString(regexp.QuoteMeta(p)) 150 sb.WriteByte('$') 151 } 152 return sb.String() 153 } 154 return "-test.run=^" + regexp.QuoteMeta(test.Name()) + "$" 155 } 156 157 func writeRerunFailsReport(opts *options, exec *testjson.Execution) error { 158 if opts.rerunFailsMaxAttempts == 0 || opts.rerunFailsReportFile == "" { 159 return nil 160 } 161 162 type testCaseCounts struct { 163 total int 164 failed int 165 } 166 167 names := []string{} 168 results := map[string]testCaseCounts{} 169 for _, failure := range exec.Failed() { 170 name := failure.Package + "." + failure.Test.Name() 171 if _, ok := results[name]; ok { 172 continue 173 } 174 names = append(names, name) 175 176 pkg := exec.Package(failure.Package) 177 counts := testCaseCounts{} 178 179 for _, tc := range pkg.Failed { 180 if tc.Test == failure.Test { 181 counts.total++ 182 counts.failed++ 183 } 184 } 185 for _, tc := range pkg.Passed { 186 if tc.Test == failure.Test { 187 counts.total++ 188 } 189 } 190 // Skipped tests are not counted, but presumably skipped tests can not fail 191 results[name] = counts 192 } 193 194 fh, err := os.Create(opts.rerunFailsReportFile) 195 if err != nil { 196 return err 197 } 198 199 sort.Strings(names) 200 for _, name := range names { 201 counts := results[name] 202 fmt.Fprintf(fh, "%s: %d runs, %d failures\n", name, counts.total, counts.failed) 203 } 204 return nil 205 }