go.undefinedlabs.com/scopeagent@v0.4.2/instrumentation/gocheck/types.go (about)

     1  package gocheck
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"path/filepath"
     7  	"reflect"
     8  	"runtime"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  	_ "unsafe"
    14  
    15  	"github.com/undefinedlabs/go-mpatch"
    16  
    17  	"go.undefinedlabs.com/scopeagent/instrumentation"
    18  	scopetesting "go.undefinedlabs.com/scopeagent/instrumentation/testing"
    19  	"go.undefinedlabs.com/scopeagent/instrumentation/testing/config"
    20  	"go.undefinedlabs.com/scopeagent/reflection"
    21  	"go.undefinedlabs.com/scopeagent/runner"
    22  	"go.undefinedlabs.com/scopeagent/tags"
    23  
    24  	goerrors "github.com/go-errors/errors"
    25  	"github.com/opentracing/opentracing-go/log"
    26  	chk "gopkg.in/check.v1"
    27  )
    28  
    29  type (
    30  	methodType struct {
    31  		reflect.Value
    32  		Info reflect.Method
    33  	}
    34  
    35  	resultTracker struct {
    36  		result          chk.Result
    37  		_lastWasProblem bool
    38  		_waiting        int
    39  		_missed         int
    40  		_expectChan     chan *chk.C
    41  		_doneChan       chan *chk.C
    42  		_stopChan       chan bool
    43  	}
    44  
    45  	tempDir struct {
    46  		sync.Mutex
    47  		path    string
    48  		counter int
    49  	}
    50  
    51  	outputWriter struct {
    52  		m                    sync.Mutex
    53  		writer               io.Writer
    54  		wroteCallProblemLast bool
    55  		Stream               bool
    56  		Verbose              bool
    57  	}
    58  
    59  	suiteRunner struct {
    60  		suite                     interface{}
    61  		setUpSuite, tearDownSuite *methodType
    62  		setUpTest, tearDownTest   *methodType
    63  		tests                     []*methodType
    64  		tracker                   *resultTracker
    65  		tempDir                   *tempDir
    66  		keepDir                   bool
    67  		output                    *outputWriter
    68  		reportedProblemLast       bool
    69  		benchTime                 time.Duration
    70  		benchMem                  bool
    71  	}
    72  
    73  	timer struct {
    74  		start     time.Time // Time test or benchmark started
    75  		duration  time.Duration
    76  		N         int
    77  		bytes     int64
    78  		timerOn   bool
    79  		benchTime time.Duration
    80  		// The initial states of memStats.Mallocs and memStats.TotalAlloc.
    81  		startAllocs uint64
    82  		startBytes  uint64
    83  		// The net total of this test after being run.
    84  		netAllocs uint64
    85  		netBytes  uint64
    86  	}
    87  
    88  	testStatus uint32
    89  
    90  	testData struct {
    91  		c       *chk.C
    92  		fn      func(*chk.C)
    93  		err     *goerrors.Error
    94  		options *runner.Options
    95  		test    *Test
    96  		writer  io.Writer
    97  	}
    98  
    99  	testLogWriter struct {
   100  		test *Test
   101  	}
   102  )
   103  
   104  const (
   105  	testSucceeded testStatus = iota
   106  	testFailed
   107  	testSkipped
   108  	testPanicked
   109  	testFixturePanicked
   110  	testMissed
   111  )
   112  
   113  var (
   114  	testMap      = map[*chk.C]*testData{}
   115  	testMapMutex = sync.RWMutex{}
   116  )
   117  
   118  //go:linkname nSRunner gopkg.in/check%2ev1.newSuiteRunner
   119  func nSRunner(suite interface{}, runConf *chk.RunConf) *suiteRunner
   120  
   121  //go:linkname lTestingT gopkg.in/check%2ev1.TestingT
   122  func lTestingT(testingT *testing.T)
   123  
   124  //go:linkname writeLog gopkg.in/check%2ev1.(*C).writeLog
   125  func writeLog(c *chk.C, buf []byte)
   126  
   127  //go:linkname lreportCallDone gopkg.in/check%2ev1.(*suiteRunner).reportCallDone
   128  func lreportCallDone(runner *suiteRunner, c *chk.C)
   129  
   130  func Init() {
   131  	var nSRunnerPatch *mpatch.Patch
   132  	var err error
   133  	nSRunnerPatch, err = mpatch.PatchMethod(nSRunner, func(suite interface{}, runConf *chk.RunConf) *suiteRunner {
   134  		nSRunnerPatch.Unpatch()
   135  		defer nSRunnerPatch.Patch()
   136  		runnerOptions := runner.GetRunnerOptions()
   137  
   138  		tWriter := &testLogWriter{}
   139  
   140  		r := nSRunner(suite, runConf)
   141  		for idx := range r.tests {
   142  			item := r.tests[idx]
   143  
   144  			if strings.HasPrefix(item.Info.Name, "Benchmark") {
   145  				continue
   146  			}
   147  
   148  			tData := &testData{options: runnerOptions, writer: tWriter}
   149  			instTest := func(c *chk.C) {
   150  				if isTestCached(c) {
   151  					writeCachedResult(item)
   152  					return
   153  				}
   154  				tData.c = c
   155  				testMapMutex.Lock()
   156  				testMap[c] = tData
   157  				setLogWriter(c, &tData.writer)
   158  				testMapMutex.Unlock()
   159  				defer func() {
   160  					testMapMutex.Lock()
   161  					delete(testMap, c)
   162  					testMapMutex.Unlock()
   163  				}()
   164  
   165  				test := startTest(item, c)
   166  				tData.test = test
   167  				tData.writer.(*testLogWriter).test = test
   168  				defer test.end(c)
   169  				item.Call([]reflect.Value{reflect.ValueOf(c)})
   170  			}
   171  			tData.fn = instTest
   172  
   173  			if runnerOptions != nil {
   174  				instTest = getRunnerTestFunc(tData)
   175  			}
   176  
   177  			r.tests[idx] = &methodType{reflect.ValueOf(instTest), item.Info}
   178  		}
   179  		return r
   180  	})
   181  	logOnError(err)
   182  
   183  	var lTestingTPatch *mpatch.Patch
   184  	lTestingTPatch, err = mpatch.PatchMethod(lTestingT, func(testingT *testing.T) {
   185  		lTestingTPatch.Unpatch()
   186  		defer lTestingTPatch.Patch()
   187  
   188  		// We tell the runner to ignore retries on this testing.T
   189  		runner.IgnoreRetries(testingT)
   190  
   191  		// We get the instrumented test struct and clean it, that removes the results of that test to be sent to scope
   192  		*scopetesting.GetTest(testingT) = scopetesting.Test{}
   193  
   194  		// We call the original go-check TestingT func
   195  		lTestingT(testingT)
   196  	})
   197  	logOnError(err)
   198  
   199  	var lreportCallDonePatch *mpatch.Patch
   200  	lreportCallDonePatch, err = mpatch.PatchMethod(lreportCallDone, func(runner *suiteRunner, c *chk.C) {
   201  		lreportCallDonePatch.Unpatch()
   202  		defer lreportCallDonePatch.Patch()
   203  
   204  		if ptrMethod, err := reflection.GetFieldPointerOf(c, "method"); err == nil {
   205  			method := *(**methodType)(ptrMethod)
   206  
   207  			if strings.HasPrefix(method.Info.Name, "Benchmark") {
   208  				if ptr, err := reflection.GetFieldPointerOf(c, "timer"); err == nil {
   209  					tm := *(*timer)(ptr)
   210  					writeBenchmarkResult(method, c, tm)
   211  				}
   212  			}
   213  		}
   214  
   215  		lreportCallDone(runner, c)
   216  	})
   217  	logOnError(err)
   218  }
   219  
   220  func logOnError(err error) {
   221  	if err != nil {
   222  		instrumentation.Logger().Println(err)
   223  	}
   224  }
   225  
   226  // gets test status
   227  func getTestStatus(c *chk.C) testStatus {
   228  	var status uint32
   229  	if ptr, err := reflection.GetFieldPointerOf(c, "_status"); err == nil {
   230  		status = *(*uint32)(ptr)
   231  	}
   232  	return testStatus(status)
   233  }
   234  
   235  // sets test status
   236  func setTestStatus(c *chk.C, status testStatus) {
   237  	sValue := uint32(status)
   238  	if ptr, err := reflection.GetFieldPointerOf(c, "_status"); err == nil {
   239  		*(*uint32)(ptr) = sValue
   240  	}
   241  }
   242  
   243  // gets the test reason
   244  func getTestReason(c *chk.C) string {
   245  	if ptr, err := reflection.GetFieldPointerOf(c, "reason"); err == nil {
   246  		return *(*string)(ptr)
   247  	}
   248  	return ""
   249  }
   250  
   251  // gets if the test must fail
   252  func getTestMustFail(c *chk.C) bool {
   253  	if ptr, err := reflection.GetFieldPointerOf(c, "mustFail"); err == nil {
   254  		return *(*bool)(ptr)
   255  	}
   256  	return false
   257  }
   258  
   259  func setLogWriter(c *chk.C, w *io.Writer) {
   260  	if ptr, err := reflection.GetFieldPointerOf(c, "logw"); err == nil {
   261  		cWriter := *(*io.Writer)(ptr)
   262  		if cWriter == nil {
   263  			*(*io.Writer)(ptr) = *w
   264  		} else if cWriter != *w {
   265  			*w = io.MultiWriter(cWriter, *w)
   266  			*(*io.Writer)(ptr) = *w
   267  		}
   268  	}
   269  }
   270  
   271  // gets if the test should retry
   272  func shouldRetry(c *chk.C) bool {
   273  	switch status := getTestStatus(c); status {
   274  	case testFailed, testPanicked, testFixturePanicked:
   275  		if getTestMustFail(c) {
   276  			return false
   277  		}
   278  		return true
   279  	}
   280  	if getTestMustFail(c) {
   281  		return true
   282  	}
   283  	return false
   284  }
   285  
   286  // gets the test func with the test runner algorithm
   287  func getRunnerTestFunc(tData *testData) func(*chk.C) {
   288  	runnerExecution := func(td *testData) {
   289  		defer func() {
   290  			if rc := recover(); rc != nil {
   291  				// using go-errors to preserve stacktrace
   292  				td.err = goerrors.Wrap(rc, 2)
   293  				td.c.Fail()
   294  			}
   295  		}()
   296  		td.fn(td.c)
   297  	}
   298  	return func(c *chk.C) {
   299  		if isTestCached(c) {
   300  			tData.fn(c)
   301  			return
   302  		}
   303  		tData.c = c
   304  		run := 1
   305  		for {
   306  			wg := new(sync.WaitGroup)
   307  			wg.Add(1)
   308  			go func() {
   309  				defer wg.Done()
   310  				runnerExecution(tData)
   311  			}()
   312  			wg.Wait()
   313  			if !shouldRetry(tData.c) {
   314  				break
   315  			}
   316  			if run > tData.options.FailRetries {
   317  				break
   318  			}
   319  			if tData.err != nil {
   320  				if !tData.options.PanicAsFail {
   321  					tData.options.OnPanic(nil, tData.err)
   322  					panic(tData.err.ErrorStack())
   323  				}
   324  				tData.options.Logger.Printf("test '%s' - panic recover: %v",
   325  					tData.c.TestName(), tData.err)
   326  			}
   327  			setTestStatus(tData.c, testSucceeded)
   328  			fmt.Printf("FAIL: Retrying '%s' [%d/%d]\n", c.TestName(), run, tData.options.FailRetries)
   329  			run++
   330  		}
   331  	}
   332  }
   333  
   334  // gets if the test is cached
   335  func isTestCached(c *chk.C) bool {
   336  	fqn := c.TestName()
   337  	cachedMap := config.GetCachedTestsMap()
   338  	if _, ok := cachedMap[fqn]; ok {
   339  		instrumentation.Logger().Printf("Test '%v' is cached.", fqn)
   340  		fmt.Print("[SCOPE CACHED] ")
   341  		return true
   342  	}
   343  	instrumentation.Logger().Printf("Test '%v' is not cached.", fqn)
   344  	return false
   345  }
   346  
   347  // Write data to a test event
   348  func (w *testLogWriter) Write(p []byte) (n int, err error) {
   349  	if w.test == nil || w.test.span == nil {
   350  		return 0, nil
   351  	}
   352  
   353  	pcs := make([]uintptr, 64)
   354  	count := runtime.Callers(2, pcs)
   355  	pcs = pcs[:count]
   356  	frames := runtime.CallersFrames(pcs)
   357  	for {
   358  		frame, more := frames.Next()
   359  		name := frame.Function
   360  
   361  		// If the frame is not in the gopkg.in/check we skip it
   362  		if !strings.Contains(name, "gopkg.in/check") {
   363  			if !more {
   364  				break
   365  			}
   366  			continue
   367  		}
   368  
   369  		// we only log if in the stackframe we see the log or lof method
   370  		if strings.HasSuffix(name, "log") || strings.HasSuffix(name, "logf") {
   371  			frame, more = frames.Next()
   372  			if !more {
   373  				break
   374  			}
   375  			frame, more = frames.Next()
   376  			helperName := frame.Function
   377  			if strings.HasSuffix(helperName, "logCaller") {
   378  				break
   379  			}
   380  			eventType := tags.LogEvent
   381  			eventLevel := tags.LogLevel_INFO
   382  			_, file, line, _ := getCallerInsideSourceRoot(frame, more, frames)
   383  			source := fmt.Sprintf("%s:%d", file, line)
   384  			if strings.HasSuffix(helperName, "Fatal") {
   385  				eventType = tags.EventTestFailure
   386  				eventLevel = tags.LogLevel_ERROR
   387  
   388  			} else if strings.HasSuffix(helperName, "Error") || strings.HasSuffix(helperName, "Errorf") {
   389  				eventLevel = tags.LogLevel_ERROR
   390  			}
   391  			fields := []log.Field{
   392  				log.String(tags.EventType, eventType),
   393  				log.String(tags.EventMessage, string(p)),
   394  				log.String(tags.LogEventLevel, eventLevel),
   395  				log.String("log.logger", "gopkg.in/check.v1"),
   396  			}
   397  			if file != "" {
   398  				fields = append(fields, log.String(tags.EventSource, source))
   399  			}
   400  			w.test.span.LogFields(fields...)
   401  			return len(p), nil
   402  		}
   403  		if !more {
   404  			break
   405  		}
   406  	}
   407  	return 0, nil
   408  }
   409  
   410  //go:noinline
   411  func getCallerInsideSourceRoot(frame runtime.Frame, more bool, frames *runtime.Frames) (pc uintptr, file string, line int, ok bool) {
   412  	isWindows := runtime.GOOS == "windows"
   413  	sourceRoot := instrumentation.GetSourceRoot()
   414  	if isWindows {
   415  		sourceRoot = strings.ToLower(sourceRoot)
   416  	}
   417  	for {
   418  		file := filepath.Clean(frame.File)
   419  		dir := filepath.Dir(file)
   420  		if isWindows {
   421  			dir = strings.ToLower(dir)
   422  		}
   423  		if strings.Index(dir, sourceRoot) != -1 {
   424  			return frame.PC, file, frame.Line, true
   425  		}
   426  		if !more {
   427  			break
   428  		}
   429  		frame, more = frames.Next()
   430  	}
   431  	return
   432  }