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  }