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  }