e2etest/newe2e_scenario_manager.go (102 lines of code) (raw):
package e2etest
import (
"github.com/google/uuid"
"reflect"
"runtime/debug"
"strings"
"sync"
"testing"
)
type ScenarioManager struct {
testingT *testing.T
Func reflect.Value
// Skip the line, don't run parallel.
runNow bool
suite string
scenario string
runLock *sync.Mutex
varStack []*ScenarioVariationManager
}
func NewScenarioManager(t *testing.T, targetFunc reflect.Value) *ScenarioManager {
nameSplits := strings.Split(t.Name(), "/")
nameSplits = nameSplits[1:] // Remove "Test"
Suite := nameSplits[0]
Scenario := nameSplits[1]
return &ScenarioManager{
testingT: t,
Func: targetFunc,
suite: Suite,
scenario: Scenario,
runLock: &sync.Mutex{},
}
}
func (sm *ScenarioManager) NewVariation(origin *ScenarioVariationManager, id string, setting []any) {
if origin.isInvalid {
return // IsInvalid variations shouldn't spawn new variations
}
for i := len(setting) - 1; i >= 0; i-- { // Because the stack is FIFO, insert the first terms last to match expected variation ordering.
v := setting[i]
clone := &ScenarioVariationManager{
VariationData: origin.VariationData.Insert(id, v),
VariationUUID: uuid.New(),
Parent: sm,
callcounts: make(map[string]uint),
}
sm.varStack = append(sm.varStack, clone)
}
}
func (sm *ScenarioManager) RunScenario() {
sm.runLock.Lock()
sm.testingT.Cleanup(func() { sm.runLock.Unlock() })
/*
When appending to the stack, the newest item is always from the closest ancestor of the tree.
Thus, we can retain good (read: brain happy) test ordering by doing FIFO. Engineering at its finest.
*/
sm.varStack = []*ScenarioVariationManager{
{Parent: sm, callcounts: make(map[string]uint), VariationUUID: uuid.New()}, // Root svm, no variations, no nothing.
}
for len(sm.varStack) > 0 {
svm := sm.varStack[len(sm.varStack)-1] // *pop*!
sm.varStack = sm.varStack[:len(sm.varStack)-1]
panicked := false
var panicError any
var panicStack []byte
func() {
defer func() {
if err := recover(); err != nil {
panicError = err
panicStack = debug.Stack()
panicked = true
}
}()
sm.Func.Call([]reflect.Value{reflect.ValueOf(svm)}) // Test will push onto the stack a bunch
}()
if !svm.isInvalid { // If we made a real test
svm.runNow = sm.runNow
sm.testingT.Run(svm.VariationName(), func(t *testing.T) {
defer func() {
if err := recover(); err != nil {
stack := debug.Stack()
t.Logf("scenario variation panicked: %v\n\n%s", err, string(stack))
t.FailNow()
}
}()
svm.t = t
svm.callcounts = make(map[string]uint)
if panicked {
t.Logf("Variation %s dryrun panicked: %v;\n%v", svm.VariationName(), panicError, string(panicStack))
t.FailNow()
}
if !svm.runNow {
t.Parallel()
}
t.Cleanup(func() {
c := ScenarioVariationManagerCleanupAsserter{svm: svm}
// Reverted to LIFO
for i := len(svm.CleanupFuncs) - 1; i >= 0; i-- {
c.WrapCleanup(svm.CleanupFuncs[i])
}
svm.DeleteCreatedResources()
})
sm.Func.Call([]reflect.Value{reflect.ValueOf(svm)})
if svm.isInvalid {
t.Fail() // If FailNow hasn't already been called, we should fail.
}
})
}
}
}