go.undefinedlabs.com/scopeagent@v0.4.2/runner/main.go (about) 1 package runner 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "log" 7 "regexp" 8 "strconv" 9 "strings" 10 "sync" 11 "testing" 12 13 goerrors "github.com/go-errors/errors" 14 15 "go.undefinedlabs.com/scopeagent/reflection" 16 ) 17 18 type ( 19 testRunner struct { 20 m *testing.M 21 options Options 22 failed bool 23 failedLock sync.Mutex 24 } 25 testDescriptor struct { 26 runner *testRunner 27 test testing.InternalTest 28 ran int 29 failed bool 30 flaky bool 31 error bool 32 skipped bool 33 ignoreRetries bool 34 } 35 Options struct { 36 FailRetries int 37 PanicAsFail bool 38 Logger *log.Logger 39 OnPanic func(t *testing.T, err interface{}) 40 } 41 ) 42 43 var ( 44 runner *testRunner 45 runnerRegexName = regexp.MustCompile(`(?m)([\w -:_]*)\/\[runner.[\w:]*](\/[\w -:_]*)?`) 46 47 descByTestMutex = sync.RWMutex{} 48 descByTestMap = map[*testing.T]*testDescriptor{} 49 ) 50 51 // Gets the test name 52 func GetOriginalTestName(name string) string { 53 match := runnerRegexName.FindStringSubmatch(name) 54 if match == nil || len(match) == 0 { 55 return name 56 } 57 return match[1] + match[2] 58 } 59 60 // Runs a test suite 61 func Run(m *testing.M, options Options) int { 62 if options.Logger == nil { 63 options.Logger = log.New(ioutil.Discard, "", 0) 64 } 65 if options.OnPanic == nil { 66 options.OnPanic = func(t *testing.T, err interface{}) {} 67 } 68 runner = &testRunner{ 69 m: m, 70 options: options, 71 failed: false, 72 } 73 runner.init(options.FailRetries > 0 || options.PanicAsFail) 74 return runner.m.Run() 75 } 76 77 // Initialize test runner, replace the internal test with an indirection 78 func (r *testRunner) init(enableRunner bool) { 79 if tPointer, err := reflection.GetFieldPointerOf(r.m, "tests"); err == nil { 80 tests := make([]testing.InternalTest, 0) 81 internalTests := (*[]testing.InternalTest)(tPointer) 82 for _, test := range *internalTests { 83 if enableRunner { 84 td := &testDescriptor{ 85 runner: r, 86 test: test, 87 ran: 0, 88 failed: false, 89 } 90 tests = append(tests, testing.InternalTest{ 91 Name: test.Name, 92 F: td.run, 93 }) 94 } else { 95 cTest := test 96 tests = append(tests, testing.InternalTest{ 97 Name: test.Name, 98 F: func(t *testing.T) { 99 defer func() { 100 if rc := recover(); rc != nil { 101 r.options.OnPanic(t, rc) 102 panic(rc) 103 } 104 }() 105 cTest.F(t) 106 }, 107 }) 108 } 109 } 110 // Replace internal tests 111 *internalTests = tests 112 } 113 } 114 115 // Internal test runner, each test calls this method in order to handle retries and process exiting 116 func (td *testDescriptor) run(t *testing.T) { 117 run := 1 118 options := td.runner.options 119 var innerError *goerrors.Error 120 121 for { 122 var innerTest *testing.T 123 title := "Run" 124 if run > 1 { 125 title = "Retry:" + strconv.Itoa(run-1) 126 } 127 title = "[runner." + title + "]" 128 t.Run(title, func(it *testing.T) { 129 // We need to run another subtest in order to support t.Parallel() 130 // https://stackoverflow.com/a/53950628 131 setChattyFlag(it, false) // avoid the [exec] subtest in stdout 132 it.Run("[exec]", func(gt *testing.T) { 133 defer func() { 134 if rc := recover(); rc != nil { 135 // using go-errors to preserve stacktrace 136 innerError = goerrors.Wrap(rc, 2) 137 gt.FailNow() 138 } 139 unlinkTestDescriptor(gt) 140 }() 141 setChattyFlag(gt, true) // enable inner test in stdout 142 setTestName(gt, strings.Replace(it.Name(), "[exec]", "", -1)) // removes [exec] from name 143 linkTestDescriptor(gt, td) 144 innerTest = gt 145 td.test.F(gt) 146 }) 147 if reflection.GetIsParallel(innerTest) && !reflection.GetIsParallel(t) { 148 t.Parallel() 149 } 150 }) 151 if innerError != nil { 152 if !options.PanicAsFail { 153 options.OnPanic(t, innerError) 154 panic(innerError.ErrorStack()) 155 } 156 options.Logger.Printf("test '%s' %s - panic recover: %v", t.Name(), title, innerError) 157 td.error = true 158 } 159 td.skipped = innerTest.Skipped() 160 if td.skipped { 161 t.SkipNow() 162 break 163 } 164 td.ran++ 165 166 if innerTest.Failed() { 167 // Current run failure 168 td.failed = true 169 } else if td.failed { 170 // Current run ok but previous run with fail -> Flaky 171 td.failed = false 172 td.flaky = true 173 options.Logger.Printf("test '%s' %s - is a flaky test!", t.Name(), title) 174 break 175 } else { 176 // Current run ok and previous run (if any) not marked as failed 177 break 178 } 179 180 if run > options.FailRetries { 181 break 182 } 183 if td.ignoreRetries { 184 break 185 } 186 run++ 187 } 188 189 // Set the global failed flag 190 td.refreshGlobalFailedFlag(t) 191 192 if td.error { 193 if !options.PanicAsFail { 194 // If after all recovers and retries the test finish with error and we have the exitOnError flag, 195 // we panic with the latest recovered data 196 options.OnPanic(t, innerError) 197 panic(innerError) 198 } 199 fmt.Printf("panic info for test '%s': %v\n", t.Name(), innerError) 200 options.Logger.Printf("panic info for test '%s': %v", t.Name(), innerError) 201 } 202 if !td.error && !td.failed { 203 // If test pass or flaky 204 setTestFailureFlag(t, false) 205 } 206 } 207 208 func (td *testDescriptor) refreshGlobalFailedFlag(t *testing.T) { 209 td.runner.failedLock.Lock() 210 defer td.runner.failedLock.Unlock() 211 td.runner.failed = td.runner.failed || td.failed || td.error 212 tParent := getTestParent(t) 213 if tParent != nil { 214 setTestFailureFlag(tParent, td.runner.failed) 215 } 216 } 217 218 // Sets the test failure flag 219 func setTestFailureFlag(t *testing.T, value bool) { 220 mu := reflection.GetTestMutex(t) 221 if mu != nil { 222 mu.Lock() 223 defer mu.Unlock() 224 } 225 226 if ptr, err := reflection.GetFieldPointerOf(t, "failed"); err == nil { 227 *(*bool)(ptr) = value 228 } 229 } 230 231 // Gets the parent from a test 232 func getTestParent(t *testing.T) *testing.T { 233 mu := reflection.GetTestMutex(t) 234 if mu != nil { 235 mu.RLock() 236 defer mu.RUnlock() 237 } 238 239 if parentPtr, err := reflection.GetFieldPointerOf(t, "parent"); err == nil { 240 parentTPointer := (**testing.T)(parentPtr) 241 if parentTPointer != nil && *parentTPointer != nil { 242 return *parentTPointer 243 } 244 } 245 return nil 246 } 247 248 // Sets the chatty flag 249 func setChattyFlag(t *testing.T, value bool) { 250 mu := reflection.GetTestMutex(t) 251 if mu != nil { 252 mu.Lock() 253 defer mu.Unlock() 254 } 255 256 if ptr, err := reflection.GetFieldPointerOf(t, "chatty"); err == nil { 257 *(*bool)(ptr) = value 258 } 259 } 260 261 // Sets the test name 262 func setTestName(t *testing.T, value string) { 263 mu := reflection.GetTestMutex(t) 264 if mu != nil { 265 mu.Lock() 266 defer mu.Unlock() 267 } 268 269 if ptr, err := reflection.GetFieldPointerOf(t, "name"); err == nil { 270 *(*string)(ptr) = value 271 } 272 } 273 274 // links a child test to a test descriptor 275 func linkTestDescriptor(t *testing.T, td *testDescriptor) { 276 descByTestMutex.Lock() 277 defer descByTestMutex.Unlock() 278 descByTestMap[t] = td 279 } 280 281 // unlink any descriptor of a child test 282 func unlinkTestDescriptor(t *testing.T) { 283 descByTestMutex.Lock() 284 defer descByTestMutex.Unlock() 285 delete(descByTestMap, t) 286 } 287 288 // gets a test descriptor from a child test 289 func getTestDescriptor(t *testing.T) *testDescriptor { 290 descByTestMutex.RLock() 291 defer descByTestMutex.RUnlock() 292 if td, ok := descByTestMap[t]; ok { 293 return td 294 } 295 return nil 296 } 297 298 // Ignore runner retries for this test 299 func IgnoreRetries(t *testing.T) { 300 td := getTestDescriptor(t) 301 if td != nil { 302 td.ignoreRetries = true 303 } 304 } 305 306 // Gets the runner options 307 func GetRunnerOptions() *Options { 308 if runner == nil { 309 return nil 310 } 311 return &runner.options 312 }