erigon-pulse/cmd/devnet/scenarios/suite.go
Mark Holt cc74e74a60
Devnet contract utils (#7928)
This request is extending the devnet functionality to more fully handle
contract processing by adding support for the following calls:
 * trace_call,
* trace_transaction
* debug_accountAt,
* eth_getCode
* eth_estimateGas
* eth_gasPrice

It also contains an initial rationalization of the devnet subscription
code to use the erigon client code directly rather than using its own
intermediate subscription management.

This is used to implement a general purpose block waiter - which can be
used in any scenario step - rather than being specific to transaction
processing.

This pull also contains an end to end tested sync processor for bor and
associated support services:
* Heimdall (supports sync event transfer)
* Faucet - allows the creation and funding of arbitary test specific
accounts (cross chain)

Notes and Caveats:
 
* Code generation for contracts requires `--evm-version paris`. For
chains which don't support push0 for solc over 0.8.19
* The bor log processing post the application of sync events causes a
panic - this will be the subject of a seperate smaller push as it is not
devnet specific
* The bor code seems to make repeated calls for the same sync events and
also reverts requests - this needs further investigation. This is the
behaviour of the current implementation and may be required - although
it does seem to generate repeat processing - which could be avoided.
2023-07-28 14:03:32 +01:00

430 lines
9.4 KiB
Go

package scenarios
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/ledgerwatch/erigon/cmd/devnet/devnet"
"github.com/ledgerwatch/log/v3"
)
type SimulationContext struct {
suite *suite
}
type BeforeScenarioHook func(context.Context, *Scenario) (context.Context, error)
type AfterScenarioHook func(context.Context, *Scenario, error) (context.Context, error)
type BeforeStepHook func(context.Context, *Step) (context.Context, error)
type AfterStepHook func(context.Context, *Step, StepStatus, error) (context.Context, error)
var TimeNowFunc = func() time.Time {
return time.Now()
}
type suite struct {
stepRunners []*stepRunner
failed bool
randomize bool
strict bool
stopOnFailure bool
testingT *testing.T
defaultContext context.Context
// suite event handlers
beforeScenarioHandlers []BeforeScenarioHook
afterScenarioHandlers []AfterScenarioHook
beforeStepHandlers []BeforeStepHook
afterStepHandlers []AfterStepHook
}
func (s *suite) runSteps(ctx context.Context, scenario *Scenario, steps []*Step) (context.Context, []StepResult, error) {
var results = make([]StepResult, 0, len(steps))
var err error
logger := devnet.Logger(ctx)
for i, step := range steps {
isLast := i == len(steps)-1
isFirst := i == 0
var stepResult StepResult
ctx, stepResult = s.runStep(ctx, scenario, step, err, isFirst, isLast, logger)
switch {
case stepResult.Err == nil:
case errors.Is(stepResult.Err, ErrUndefined):
// do not overwrite failed error
if err == nil {
err = stepResult.Err
}
default:
err = stepResult.Err
logger.Error("Step failed with error", "scenario", scenario.Name, "step", step.Text, "err", err)
}
results = append(results, stepResult)
}
return ctx, results, err
}
func (s *suite) runStep(ctx context.Context, scenario *Scenario, step *Step, prevStepErr error, isFirst, isLast bool, logger log.Logger) (rctx context.Context, sr StepResult) {
var match *stepRunner
sr = StepResult{Status: Undefined}
rctx = ctx
// user multistep definitions may panic
defer func() {
if e := recover(); e != nil {
logger.Error("Step failed with panic", "scenario", scenario.Name, "step", step.Text, "err", e)
sr.Err = &traceError{
msg: fmt.Sprintf("%v", e),
stack: callStack(),
}
}
earlyReturn := prevStepErr != nil || sr.Err == ErrUndefined
// Run after step handlers.
rctx, sr.Err = s.runAfterStepHooks(ctx, step, sr.Status, sr.Err)
// Trigger after scenario on failing or last step to attach possible hook error to step.
if isLast || (sr.Status != Skipped && sr.Status != Undefined && sr.Err != nil) {
rctx, sr.Err = s.runAfterScenarioHooks(rctx, scenario, sr.Err)
}
if earlyReturn {
return
}
switch sr.Err {
case nil:
sr.Status = Passed
default:
sr.Status = Failed
}
}()
// run before scenario handlers
if isFirst {
ctx, sr.Err = s.runBeforeScenarioHooks(ctx, scenario)
}
// run before step handlers
ctx, sr.Err = s.runBeforeStepHooks(ctx, step, sr.Err)
if sr.Err != nil {
sr = NewStepResult(step.Id, step)
sr.Status = Failed
return ctx, sr
}
ctx, undef, match, err := s.maybeUndefined(ctx, step.Text, step.Args, logger)
if err != nil {
return ctx, sr
} else if len(undef) > 0 {
sr = NewStepResult(scenario.Id, step)
sr.Status = Undefined
sr.Err = ErrUndefined
logger.Error("Step failed undefined step", "scenario", scenario.Name, "step", step.Text)
return ctx, sr
}
if prevStepErr != nil {
sr = NewStepResult(scenario.Id, step)
sr.Status = Skipped
return ctx, sr
}
ctx, res := match.Run(ctx, step.Text, step.Args, logger)
ctx, sr.Returns, sr.Err = s.maybeSubSteps(ctx, res, logger)
return ctx, sr
}
func (s *suite) maybeUndefined(ctx context.Context, text string, args []interface{}, logger log.Logger) (context.Context, []string, *stepRunner, error) {
step := s.matchStep(text)
if nil == step {
return ctx, []string{text}, nil, nil
}
var undefined []string
if !step.Nested {
return ctx, undefined, step, nil
}
ctx, steps := step.Run(ctx, text, args, logger)
for _, next := range steps.([]Step) {
ctx, undef, _, err := s.maybeUndefined(ctx, next.Text, nil, logger)
if err != nil {
return ctx, undefined, nil, err
}
undefined = append(undefined, undef...)
}
return ctx, undefined, nil, nil
}
func (s *suite) matchStep(text string) *stepRunner {
var matches []*stepRunner
for _, r := range s.stepRunners {
for _, expr := range r.Exprs {
if m := expr.FindStringSubmatch(text); len(m) > 0 {
matches = append(matches, r)
}
}
}
if len(matches) == 1 {
return matches[0]
}
for _, r := range matches {
for _, expr := range r.Exprs {
if m := expr.FindStringSubmatch(text); m[0] == text {
return r
}
}
}
for _, r := range stepRunnerRegistry {
for _, expr := range r.Exprs {
if m := expr.FindStringSubmatch(text); len(m) > 0 {
matches = append(matches, r)
}
}
}
if len(matches) == 1 {
return matches[0]
}
for _, r := range matches {
for _, expr := range r.Exprs {
if m := expr.FindStringSubmatch(text); m[0] == text {
return r
}
}
}
return nil
}
func (s *suite) maybeSubSteps(ctx context.Context, result interface{}, logger log.Logger) (context.Context, []interface{}, error) {
if nil == result {
return ctx, nil, nil
}
var steps []Step
switch result := result.(type) {
case error:
return ctx, nil, result
case []Step:
steps = result
case []interface{}:
if len(result) == 0 {
return ctx, result, nil
}
if err, ok := result[len(result)-1].(error); ok {
return ctx, result[0 : len(result)-1], err
}
return ctx, result, nil
default:
return ctx, nil, fmt.Errorf("unexpected results type: %T - %+v", result, result)
}
var err error
for _, step := range steps {
if def := s.matchStep(step.Text); def == nil {
return ctx, nil, ErrUndefined
} else {
ctx, res := def.Run(ctx, step.Text, step.Args, logger)
if ctx, _, err = s.maybeSubSteps(ctx, res, logger); err != nil {
return ctx, nil, fmt.Errorf("%s: %+v", step.Text, err)
}
}
}
return ctx, nil, nil
}
func (s *suite) runScenario(scenario *Scenario) (sr *ScenarioResult, err error) {
ctx := s.defaultContext
if scenario.Context != nil {
ctx = JoinContexts(scenario.Context, ctx)
}
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if len(scenario.Steps) == 0 {
return &ScenarioResult{ScenarioId: scenario.Id, StartedAt: TimeNowFunc()}, ErrUndefined
}
// Before scenario hooks are called in context of first evaluated step
// so that error from handler can be added to step.
sr = &ScenarioResult{ScenarioId: scenario.Id, StartedAt: TimeNowFunc()}
// scenario
if s.testingT != nil {
// Running scenario as a subtest.
s.testingT.Run(scenario.Name, func(t *testing.T) {
ctx, sr.StepResults, err = s.runSteps(ctx, scenario, scenario.Steps)
if s.shouldFail(err) {
t.Error(err)
}
})
} else {
ctx, sr.StepResults, err = s.runSteps(ctx, scenario, scenario.Steps)
}
// After scenario handlers are called in context of last evaluated step
// so that error from handler can be added to step.
return sr, err
}
func (s *suite) shouldFail(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrUndefined) {
return s.strict
}
return true
}
func (s *suite) runBeforeStepHooks(ctx context.Context, step *Step, err error) (context.Context, error) {
hooksFailed := false
for _, f := range s.beforeStepHandlers {
hctx, herr := f(ctx, step)
if herr != nil {
hooksFailed = true
if err == nil {
err = herr
} else {
err = fmt.Errorf("%v, %w", herr, err)
}
}
if hctx != nil {
ctx = hctx
}
}
if hooksFailed {
err = fmt.Errorf("before step hook failed: %w", err)
}
return ctx, err
}
func (s *suite) runAfterStepHooks(ctx context.Context, step *Step, status StepStatus, err error) (context.Context, error) {
for _, f := range s.afterStepHandlers {
hctx, herr := f(ctx, step, status, err)
// Adding hook error to resulting error without breaking hooks loop.
if herr != nil {
if err == nil {
err = herr
} else {
err = fmt.Errorf("%v, %w", herr, err)
}
}
if hctx != nil {
ctx = hctx
}
}
return ctx, err
}
func (s *suite) runBeforeScenarioHooks(ctx context.Context, scenario *Scenario) (context.Context, error) {
var err error
// run before scenario handlers
for _, f := range s.beforeScenarioHandlers {
hctx, herr := f(ctx, scenario)
if herr != nil {
if err == nil {
err = herr
} else {
err = fmt.Errorf("%v, %w", herr, err)
}
}
if hctx != nil {
ctx = hctx
}
}
if err != nil {
err = fmt.Errorf("before scenario hook failed: %w", err)
}
return ctx, err
}
func (s *suite) runAfterScenarioHooks(ctx context.Context, scenario *Scenario, lastStepErr error) (context.Context, error) {
err := lastStepErr
hooksFailed := false
isStepErr := true
// run after scenario handlers
for _, f := range s.afterScenarioHandlers {
hctx, herr := f(ctx, scenario, err)
// Adding hook error to resulting error without breaking hooks loop.
if herr != nil {
hooksFailed = true
if err == nil {
isStepErr = false
err = herr
} else {
if isStepErr {
err = fmt.Errorf("step error: %w", err)
isStepErr = false
}
err = fmt.Errorf("%v, %w", herr, err)
}
}
if hctx != nil {
ctx = hctx
}
}
if hooksFailed {
err = fmt.Errorf("after scenario hook failed: %w", err)
}
return ctx, err
}