go.undefinedlabs.com/scopeagent@v0.4.2/instrumentation/testing/testing.go (about) 1 package testing 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "regexp" 8 "runtime" 9 "strings" 10 "sync" 11 "testing" 12 "time" 13 "unsafe" 14 15 "github.com/opentracing/opentracing-go" 16 17 "go.undefinedlabs.com/scopeagent/errors" 18 "go.undefinedlabs.com/scopeagent/instrumentation" 19 "go.undefinedlabs.com/scopeagent/instrumentation/coverage" 20 "go.undefinedlabs.com/scopeagent/instrumentation/logging" 21 "go.undefinedlabs.com/scopeagent/instrumentation/testing/config" 22 "go.undefinedlabs.com/scopeagent/reflection" 23 "go.undefinedlabs.com/scopeagent/runner" 24 "go.undefinedlabs.com/scopeagent/tags" 25 "go.undefinedlabs.com/scopeagent/tracer" 26 ) 27 28 type ( 29 Test struct { 30 testing.TB 31 ctx context.Context 32 span opentracing.Span 33 t *testing.T 34 codePC uintptr 35 } 36 37 Option func(*Test) 38 ) 39 40 var ( 41 testMapMutex sync.RWMutex 42 testMap = map[*testing.T]*Test{} 43 autoInstrumentedTestsMutex sync.RWMutex 44 autoInstrumentedTests = map[*testing.T]bool{} 45 46 TESTING_LOG_REGEX = regexp.MustCompile(`(?m)^ {4}(?P<file>[\w\/\.]+):(?P<line>\d+): (?P<message>(.*\n {8}.*)*.*)`) 47 ) 48 49 // Options for starting a new test 50 func WithContext(ctx context.Context) Option { 51 return func(test *Test) { 52 test.ctx = ctx 53 } 54 } 55 56 // Starts a new test 57 func StartTest(t *testing.T, opts ...Option) *Test { 58 pc, _, _, _ := runtime.Caller(1) 59 return StartTestFromCaller(t, pc, opts...) 60 } 61 62 // Starts a new test with and uses the caller pc info for Name and Suite 63 func StartTestFromCaller(t *testing.T, pc uintptr, opts ...Option) *Test { 64 65 // check if the test is cached 66 if isTestCached(t, pc) { 67 68 test := &Test{t: t, ctx: context.Background()} 69 for _, opt := range opts { 70 opt(test) 71 } 72 73 // Extracting the testing func name (by removing any possible sub-test suffix `{test_func}/{sub_test}`) 74 // to search the func source code bounds and to calculate the package name. 75 fullTestName := runner.GetOriginalTestName(t.Name()) 76 pName, _ := instrumentation.GetPackageAndName(pc) 77 78 testTags := opentracing.Tags{ 79 "span.kind": "test", 80 "test.name": fullTestName, 81 "test.suite": pName, 82 "test.framework": "testing", 83 "test.language": "go", 84 } 85 span, _ := opentracing.StartSpanFromContextWithTracer(test.ctx, instrumentation.Tracer(), fullTestName, testTags) 86 span.SetBaggageItem("trace.kind", "test") 87 span.SetTag("test.status", tags.TestStatus_CACHE) 88 span.Finish() 89 t.SkipNow() 90 return test 91 92 } else { 93 94 // Get or create a new Test struct 95 // If we get an old struct we replace the current span and context with a new one. 96 // Useful if we want to overwrite the Start call with options 97 test, exist := getOrCreateTest(t) 98 if exist { 99 // If there is already one we want to replace it, so we clear the context 100 test.ctx = context.Background() 101 } 102 test.codePC = pc 103 104 for _, opt := range opts { 105 opt(test) 106 } 107 108 // Extracting the testing func name (by removing any possible sub-test suffix `{test_func}/{sub_test}`) 109 // to search the func source code bounds and to calculate the package name. 110 fullTestName := runner.GetOriginalTestName(t.Name()) 111 pName, _, testCode := instrumentation.GetPackageAndNameAndBoundaries(pc) 112 113 testTags := opentracing.Tags{ 114 "span.kind": "test", 115 "test.name": fullTestName, 116 "test.suite": pName, 117 "test.framework": "testing", 118 "test.language": "go", 119 } 120 121 if testCode != "" { 122 testTags["test.code"] = testCode 123 } 124 125 if test.ctx == nil { 126 test.ctx = context.Background() 127 } 128 129 span, ctx := opentracing.StartSpanFromContextWithTracer(test.ctx, instrumentation.Tracer(), fullTestName, testTags) 130 span.SetBaggageItem("trace.kind", "test") 131 test.span = span 132 test.ctx = ctx 133 134 logging.Reset() 135 coverage.StartCoverage() 136 137 return test 138 } 139 } 140 141 // Set test code 142 func (test *Test) SetTestCode(pc uintptr) { 143 test.codePC = pc 144 if test.span == nil { 145 return 146 } 147 pName, _, fBoundaries := instrumentation.GetPackageAndNameAndBoundaries(pc) 148 test.span.SetTag("test.suite", pName) 149 if fBoundaries != "" { 150 test.span.SetTag("test.code", fBoundaries) 151 } 152 } 153 154 // Ends the current test 155 func (test *Test) End() { 156 autoInstrumentedTestsMutex.RLock() 157 defer autoInstrumentedTestsMutex.RUnlock() 158 // First we detect if the current test is auto-instrumented, if not we call the end method (needed in sub tests) 159 if _, ok := autoInstrumentedTests[test.t]; !ok { 160 test.end() 161 } 162 } 163 164 // Gets the test context 165 func (test *Test) Context() context.Context { 166 return test.ctx 167 } 168 169 // Runs an auto instrumented sub test 170 func (test *Test) Run(name string, f func(t *testing.T)) bool { 171 if test.span == nil { // No span = not instrumented 172 return test.t.Run(name, f) 173 } 174 pc, _, _, _ := runtime.Caller(1) 175 return test.t.Run(name, func(childT *testing.T) { 176 addAutoInstrumentedTest(childT) 177 childTest := StartTestFromCaller(childT, pc) 178 defer childTest.end() 179 f(childT) 180 }) 181 } 182 183 // Ends the current test (this method is called from the auto-instrumentation) 184 func (test *Test) end() { 185 // We check if we have a span to work with, if not span is found we exit 186 if test == nil || test.span == nil { 187 return 188 } 189 190 finishTime := time.Now() 191 192 // If we have our own implementation of the span, we can set the exact start time from the test 193 if ownSpan, ok := test.span.(tracer.Span); ok { 194 if startTime, err := reflection.GetTestStartTime(test.t); err == nil { 195 ownSpan.SetStart(startTime) 196 } else { 197 instrumentation.Logger().Printf("error: %v", err) 198 } 199 } 200 201 // Remove the Test struct from the hash map, so a call to Start while we end this instance will create a new struct 202 removeTest(test.t) 203 // Stop and get records generated by loggers 204 logRecords := logging.GetRecords() 205 206 finishOptions := opentracing.FinishOptions{ 207 FinishTime: finishTime, 208 LogRecords: logRecords, 209 } 210 211 if testing.CoverMode() != "" { 212 // Checks if the current test is running parallel to extract the coverage or not 213 if reflection.GetIsParallel(test.t) && parallel > 1 { 214 instrumentation.Logger().Printf("CodePath in parallel test is not supported: %v\n", test.t.Name()) 215 coverage.RestoreCoverageCounters() 216 } else if cov := coverage.EndCoverage(); cov != nil { 217 if sp, ok := test.span.(tracer.Span); ok { 218 sp.UnsafeSetTag(tags.Coverage, *cov) 219 } else { 220 test.span.SetTag(tags.Coverage, *cov) 221 } 222 } 223 } 224 225 if r := recover(); r != nil { 226 test.span.SetTag("test.status", tags.TestStatus_FAIL) 227 errors.WriteExceptionEvent(test.span, r, 1) 228 test.span.FinishWithOptions(finishOptions) 229 panic(r) 230 } 231 if test.t.Failed() { 232 test.span.SetTag("test.status", tags.TestStatus_FAIL) 233 test.span.SetTag("error", true) 234 } else if test.t.Skipped() { 235 test.span.SetTag("test.status", tags.TestStatus_SKIP) 236 } else { 237 test.span.SetTag("test.status", tags.TestStatus_PASS) 238 } 239 240 test.span.FinishWithOptions(finishOptions) 241 } 242 243 func findMatchesLogRegex(output string) [][]string { 244 allMatches := TESTING_LOG_REGEX.FindAllStringSubmatch(output, -1) 245 for _, matches := range allMatches { 246 matches[3] = strings.Replace(matches[3], "\n ", "\n", -1) 247 } 248 return allMatches 249 } 250 251 func extractTestOutput(t *testing.T) *[]byte { 252 val := reflect.Indirect(reflect.ValueOf(t)) 253 member := val.FieldByName("output") 254 if member.IsValid() { 255 ptrToY := unsafe.Pointer(member.UnsafeAddr()) 256 return (*[]byte)(ptrToY) 257 } 258 return nil 259 } 260 261 // Gets or create a test struct 262 func getOrCreateTest(t *testing.T) (test *Test, exists bool) { 263 testMapMutex.Lock() 264 defer testMapMutex.Unlock() 265 if testPtr, ok := testMap[t]; ok { 266 test = testPtr 267 exists = true 268 } else { 269 test = &Test{t: t} 270 testMap[t] = test 271 exists = false 272 } 273 return 274 } 275 276 // Removes a test struct from the map 277 func removeTest(t *testing.T) { 278 testMapMutex.Lock() 279 defer testMapMutex.Unlock() 280 delete(testMap, t) 281 } 282 283 // Gets the Test struct from testing.T 284 func GetTest(t *testing.T) *Test { 285 testMapMutex.RLock() 286 defer testMapMutex.RUnlock() 287 if test, ok := testMap[t]; ok { 288 return test 289 } 290 return &Test{ 291 ctx: context.TODO(), 292 span: nil, 293 t: t, 294 } 295 } 296 297 // Fails and write panic on running tests 298 // Use this only if the process is going to crash 299 func PanicAllRunningTests(e interface{}, skip int) { 300 autoInstrumentedTestsMutex.Lock() 301 defer autoInstrumentedTestsMutex.Unlock() 302 303 // We copy the testMap because v.end() locks 304 testMapMutex.RLock() 305 tmp := map[*testing.T]*Test{} 306 for k, v := range testMap { 307 tmp[k] = v 308 } 309 testMapMutex.RUnlock() 310 311 for _, v := range tmp { 312 delete(autoInstrumentedTests, v.t) 313 v.t.Fail() 314 errors.WriteExceptionEvent(v.span, e, 1+skip) 315 v.end() 316 } 317 } 318 319 // Adds an auto instrumented test to the map 320 func addAutoInstrumentedTest(t *testing.T) { 321 autoInstrumentedTestsMutex.Lock() 322 defer autoInstrumentedTestsMutex.Unlock() 323 autoInstrumentedTests[t] = true 324 } 325 326 // Get if the test is cached 327 func isTestCached(t *testing.T, pc uintptr) bool { 328 pkgName, testName := instrumentation.GetPackageAndName(pc) 329 fqn := fmt.Sprintf("%s.%s", pkgName, testName) 330 cachedMap := config.GetCachedTestsMap() 331 if _, ok := cachedMap[fqn]; ok { 332 instrumentation.Logger().Printf("Test '%v' is cached.", fqn) 333 fmt.Print("[SCOPE CACHED] ") 334 reflection.SkipAndFinishTest(t) 335 return true 336 } 337 instrumentation.Logger().Printf("Test '%v' is not cached.", fqn) 338 return false 339 }