core/testsuite/suite.go
// Package testsuite contains the standard test suite
package testsuite
import (
"context"
"fmt"
"github.com/ipfs/go-log"
"github.com/stretchr/testify/suite"
"sync"
"sync/atomic"
"testing"
"time"
)
// cancellableContext is a context object
// note: we don't really have a choice here, but note this is vulnerable to context leaks if setup/teardown
// events are not called.
type cancellableContext struct {
// ctx is the context object
//nolint: containedctx
ctx context.Context
// cancelFunc allows the context to be canceled
cancelFunc context.CancelFunc
}
// newCancellableContext creates a new cancellable context.
func newCancellableContext(parentContext context.Context) cancellableContext {
ctx, cancel := context.WithCancel(parentContext)
return cancellableContext{
ctx: ctx,
cancelFunc: cancel,
}
}
// TestSuite defines the basic test suite.
// TODO: we should make sure global vars don't get mutated. Namely eth params.
type TestSuite struct {
suite.Suite
// logDir is the directory where logs will be written for the docker containers that host the anvil nodes
// this allows you to do tai -f /path/to/logs/*.combined.log to see all logs
LogDir string
// context is the context object for the test suite. All other context objects inherit
// from it. unlike suiteContext and testContext it is not canceled and functionally does
// not have a lifecycle. TODO this should probably replace suiteContext
//nolint: containedctx
context context.Context
// suiteContext is the context object for the duration of the test. It is canceled at the end
// of the test
suiteContext cancellableContext
// testContext is the context object for the test. It is canceled at the end of the test
// inheritance of test contexts is as follows:
//
// context->suiteContext->testContext
//
testContext cancellableContext
// testID is an autoincrementing that can be used as a unique, per test, identifier.
testID int
// runAfterTest are functions that should be run after the test
runAfterTest []func()
// runAfterSuite are functions that are run after the suite
runAfterSuite []func()
// structMux protects non thread safe operations
structMux sync.Mutex
// setupSuiteCalled is an assertion that makes sure setupSuite was called on the parent
setupSuiteCalled atomic.Bool
// setupTestCalled is an assertion that setupTest was called on the parent
setupTestCalled atomic.Bool
// beforeTestCalled is wether or not before test was called
beforeTestCalled atomic.Bool
}
// NewTestSuite creates a new test suite and performs some basic checks afterward.
// Every test suite in the synapse library should inherit from this suite and override where necessary.
func NewTestSuite(tb testing.TB) *TestSuite {
tb.Helper()
log.SetAllLoggers(log.LevelWarn)
ctx := context.Background()
return &TestSuite{
context: ctx,
testID: 0,
}
}
// Eventually asserts something is eventually true.
// Note: this uses the suite context instead of the test context.
// TODO: either separate suite and test context calls or deprecte and require generic.
func (s *TestSuite) Eventually(willBeTrue func() bool) {
Eventually(s.GetSuiteContext(), s.T(), willBeTrue)
}
// SetupSuite sets up the test suite.
func (s *TestSuite) SetupSuite() {
s.structMux.Lock()
defer s.structMux.Unlock()
s.runAfterSuite = nil
s.suiteContext = newCancellableContext(s.context)
s.setupSuiteCalled.Store(true)
}
// BeforeTest performs some assertions and sets up the test.
func (s *TestSuite) BeforeTest(_, _ string) {
if !s.setupTestCalled.Load() {
panic("make sure SetupTest is called on the parent")
}
if !s.setupSuiteCalled.Load() {
panic("make sure SetupSuite is called on the parent")
}
s.beforeTestCalled.Store(true)
s.DeferAfterTest(func() {
if !s.beforeTestCalled.Load() {
panic("make sure you call before test on the parent")
}
})
}
// runDefferedFunctions runs deferred functions.
func runDeferredFunctions(deferredFuncs []func()) {
for _, deferredFunc := range deferredFuncs {
deferredFunc()
}
}
// TearDownSuite tears down the test suite.
func (s *TestSuite) TearDownSuite() {
runDeferredFunctions(s.runAfterSuite)
s.suiteContext.cancelFunc()
s.setupSuiteCalled.Store(false)
}
// SetupTest runs checks at the end of the test suite.
func (s *TestSuite) SetupTest() {
s.runAfterTest = nil
s.testContext = newCancellableContext(s.suiteContext.ctx)
s.setupTestCalled.Store(true)
fmt.Printf("running test %s with id %d \n", s.T().Name(), s.testID)
}
// SetTestTimeout will create a test timout override for the context.
// this will wrap s.testContext.
// TODO: consider enabling a value by default.
func (s *TestSuite) SetTestTimeout(timeout time.Duration) {
ctx, cancel := context.WithTimeout(s.testContext.ctx, timeout)
oldCancel := s.testContext.cancelFunc
s.testContext = cancellableContext{ctx: ctx, cancelFunc: func() {
// cancel parent
cancel()
// then underlying
oldCancel()
}}
}
// GetTestID gets the unique test id for the current test.
// uniqueness is per-suite.
func (s *TestSuite) GetTestID() int {
return s.testID
}
// TearDownTest runs checks at the end of the test suite.
func (s *TestSuite) TearDownTest() {
s.testID++
runDeferredFunctions(s.runAfterTest)
// this will panic if you failed to call SetupTest() from an inheriting suite
s.testContext.cancelFunc()
fmt.Printf("finished running test %s with id %d \n", s.T().Name(), s.testID)
s.setupTestCalled.Store(false)
}
// DeferAfterTest runs a function after the test. This will run before context cancellation
// if you'd like to do otherwise you can from a new goroutine that watches TestContext()
// TODO: in cases of crashes this will not be done so fix this.
func (s *TestSuite) DeferAfterTest(newFunc func()) {
s.structMux.Lock()
defer s.structMux.Unlock()
s.runAfterTest = append(s.runAfterTest, newFunc)
}
// DeferAfterSuite runs a function after the suite. This will run before context cancellation
// if you'd like to do otherwise you can from a new goroutine that watches SuiteContext()
// TODO: in cases of crashes this will not be done so fix this.
func (s *TestSuite) DeferAfterSuite(newFunc func()) {
s.structMux.Lock()
defer s.structMux.Unlock()
s.runAfterSuite = append(s.runAfterSuite, newFunc)
}
// GetTestContext gets the context object for the suite. This is an alias for GetTestContext()
// TODO: right now this is run as a test because of naming. this is mostly harmless.
func (s *TestSuite) GetTestContext() context.Context {
return s.testContext.ctx
}
// GetSuiteContext returns the context for the test suite.
func (s *TestSuite) GetSuiteContext() context.Context {
return s.suiteContext.ctx
}
// MustMarshall is a helper method that attempts to marshall, otherwise it
// fails the test.
func (s *TestSuite) MustMarshall(v any) []byte {
return MustMarshall(s.T(), v)
}