diff --git a/build/devenv/tests/e2e/smoke_test.go b/build/devenv/tests/e2e/smoke_test.go index ebdf8f9c5..7afdb13fa 100644 --- a/build/devenv/tests/e2e/smoke_test.go +++ b/build/devenv/tests/e2e/smoke_test.go @@ -40,13 +40,15 @@ func TestE2ESmoke_Basic(t *testing.T) { basicArgs := basic.Args{Send: sendCfg} t.Run("extra args v3 messaging", func(t *testing.T) { for _, tc := range basic.All(lib, src.ChainSelector(), dest.ChainSelector(), basicArgs) { - if tc.HavePrerequisites(ctx) { + if missing, err := tc.CheckPrerequisites(ctx); err != nil { + t.Fatalf("prerequisite check failed for %s: %v", tc.Name(), err) + } else if len(missing) > 0 { + t.Logf("Skipping %s: missing prerequisites: %v", tc.Name(), missing) + } else { t.Run(tc.Name(), func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) require.NoError(t, tc.Run(subtestCtx)) }) - } else { - t.Logf("Skipping %s because current environment does not have the prerequisites", tc.Name()) } } }) @@ -54,23 +56,27 @@ func TestE2ESmoke_Basic(t *testing.T) { t.Run("extra args v3 token transfer", func(t *testing.T) { combos := common.AllTokenCombinations() for _, tc := range token_transfer.All(lib, src.ChainSelector(), dest.ChainSelector(), combos, token_transfer.Args{Send: sendCfg}) { - if tc.HavePrerequisites(ctx) { + if missing, err := tc.CheckPrerequisites(ctx); err != nil { + t.Fatalf("prerequisite check failed for %s: %v", tc.Name(), err) + } else if len(missing) > 0 { + t.Logf("Skipping %s: missing prerequisites: %v", tc.Name(), missing) + } else { t.Run(tc.Name(), func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) require.NoError(t, tc.Run(subtestCtx)) }) - } else { - t.Logf("Skipping %s because current environment does not have the prerequisites", tc.Name()) } } for _, tc := range token_transfer.All17(lib, src.ChainSelector(), dest.ChainSelector(), combos, token_transfer.Args{Send: sendCfg}) { - if tc.HavePrerequisites(ctx) { + if missing, err := tc.CheckPrerequisites(ctx); err != nil { + t.Fatalf("prerequisite check failed for %s: %v", tc.Name(), err) + } else if len(missing) > 0 { + t.Logf("Skipping %s: missing prerequisites: %v", tc.Name(), missing) + } else { t.Run(tc.Name(), func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) require.NoError(t, tc.Run(subtestCtx)) }) - } else { - t.Logf("Skipping %s because current environment does not have the prerequisites", tc.Name()) } } }) diff --git a/build/devenv/tests/e2e/tcapi/basic/v3.go b/build/devenv/tests/e2e/tcapi/basic/v3.go index 590683beb..54d7f1fd9 100644 --- a/build/devenv/tests/e2e/tcapi/basic/v3.go +++ b/build/devenv/tests/e2e/tcapi/basic/v3.go @@ -3,6 +3,7 @@ package basic import ( "bytes" "context" + "errors" "fmt" "time" @@ -39,7 +40,7 @@ type v3TestCase struct { receiver protocol.UnknownAddress ccvs []protocol.CCV executor protocol.UnknownAddress - hydrate func(ctx context.Context, tc *v3TestCase) bool + hydrate func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) hydrated bool } @@ -47,24 +48,35 @@ func (tc *v3TestCase) Name() string { return tc.name } -func (tc *v3TestCase) ensureHydrated(ctx context.Context) error { +func (tc *v3TestCase) ensureHydrated(ctx context.Context) ([]tcapi.MissingPrerequisite, error) { if tc.hydrated { - return nil + return nil, nil } if tc.hydrate == nil { - return fmt.Errorf("%s: missing hydrate func", tc.name) + return nil, fmt.Errorf("%s: missing hydrate func", tc.name) } - if !tc.hydrate(ctx, tc) { - return fmt.Errorf("%s: prerequisites not met", tc.name) + missing, err := tc.hydrate(ctx, tc) + if err != nil { + return nil, err } - tc.hydrated = true - return nil + if len(missing) == 0 { + tc.hydrated = true + } + return missing, nil } func (tc *v3TestCase) Run(ctx context.Context) error { - if err := tc.ensureHydrated(ctx); err != nil { + missing, err := tc.ensureHydrated(ctx) + if err != nil { return err } + if len(missing) > 0 { + errs := make([]error, len(missing)) + for i, m := range missing { + errs[i] = m + } + return errors.Join(errs...) + } chainMap, err := tc.lib.ChainsMap(ctx) if err != nil { return fmt.Errorf("failed to get chains map: %w", err) @@ -157,8 +169,8 @@ func (tc *v3TestCase) Run(ctx context.Context) error { return nil } -func (tc *v3TestCase) HavePrerequisites(ctx context.Context) bool { - return tc.ensureHydrated(ctx) == nil +func (tc *v3TestCase) CheckPrerequisites(ctx context.Context) ([]tcapi.MissingPrerequisite, error) { + return tc.ensureHydrated(ctx) } func getCommitteeCCV(resolver chainreg.AddressResolver, ds datastore.DataStore, srcChainSelector uint64, qualifier string) (protocol.CCV, error) { @@ -181,50 +193,53 @@ type v3Env struct { DstResolver chainreg.AddressResolver } -func loadV3Env(ctx context.Context, lib ccv.Lib, src, dst uint64) (v3Env, bool) { +func loadV3Env(ctx context.Context, lib ccv.Lib, src, dst uint64) (v3Env, error) { var env v3Env ds, err := lib.DataStore() if err != nil { - return env, false + return env, fmt.Errorf("data store: %w", err) } env.DS = ds chainMap, err := lib.ChainsMap(ctx) if err != nil { - return env, false + return env, fmt.Errorf("chains map: %w", err) } dstChain, ok := chainMap[dst] if !ok { - return env, false + return env, fmt.Errorf("chain %d not found in chains map", dst) } env.Dst = dstChain srcFamily, err := chain_selectors.GetSelectorFamily(src) if err != nil { - return env, false + return env, fmt.Errorf("source chain family: %w", err) } dstFamily, err := chain_selectors.GetSelectorFamily(dst) if err != nil { - return env, false + return env, fmt.Errorf("dest chain family: %w", err) } srcReg, err := chainreg.GetRegistry().Get(srcFamily) if err != nil { - return env, false + return env, fmt.Errorf("source chain registry: %w", err) } dstReg, err := chainreg.GetRegistry().Get(dstFamily) if err != nil { - return env, false + return env, fmt.Errorf("dest chain registry: %w", err) } - if srcReg.AddressResolver == nil || dstReg.AddressResolver == nil { - return env, false + if srcReg.AddressResolver == nil { + return env, fmt.Errorf("source chain registry has no address resolver") + } + if dstReg.AddressResolver == nil { + return env, fmt.Errorf("dest chain registry has no address resolver") } env.SrcResolver = srcReg.AddressResolver env.DstResolver = dstReg.AddressResolver - return env, true + return env, nil } // CustomExecutor returns a test case that uses the custom executor. @@ -246,30 +261,36 @@ func customExecutor(lib ccv.Lib, src, dest uint64, args Args) *v3TestCase { numExpectedVerifications: 1, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.DefaultReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (default)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver ccv, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) + } else { + tc.ccvs = []protocol.CCV{ccv} } - tc.ccvs = []protocol.CCV{ccv} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.CustomExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "custom executor", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + + return missing, nil }, } } @@ -292,29 +313,36 @@ func eoaReceiverDefaultVerifier(lib ccv.Lib, src, dest uint64, args Args) *v3Tes numExpectedVerifications: 1, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + + var missing []tcapi.MissingPrerequisite + receiver, err := env.Dst.GetEOAReceiverAddress() if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "EOA receiver address", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver + ccv, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) + } else { + tc.ccvs = []protocol.CCV{ccv} } - tc.ccvs = []protocol.CCV{ccv} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + return missing, nil }, } } @@ -337,33 +365,41 @@ func eoaReceiverSecondaryVerifier(lib ccv.Lib, src, dest uint64, args Args) *v3T numExpectedVerifications: 2, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + + var missing []tcapi.MissingPrerequisite + receiver, err := env.Dst.GetEOAReceiverAddress() if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "EOA receiver address", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver sec, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.SecondaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (secondary)", Err: err}) } def, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) + } + if len(missing) == 0 { + tc.ccvs = []protocol.CCV{sec, def} } - tc.ccvs = []protocol.CCV{sec, def} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + + return missing, nil }, } } @@ -387,30 +423,36 @@ func receiverSecondaryVerifierRequired(lib ccv.Lib, src, dest uint64, args Args) aggregatorQualifier: common.SecondaryCommitteeVerifierQualifier, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.SecondaryReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (secondary)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver ccv, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.SecondaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (secondary)", Err: err}) + } else { + tc.ccvs = []protocol.CCV{ccv} } - tc.ccvs = []protocol.CCV{ccv} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + + return missing, nil }, } } @@ -434,35 +476,41 @@ func receiverSecondaryRequiredTertiaryOptionalThreshold1(lib ccv.Lib, src, dest aggregatorQualifier: common.SecondaryCommitteeVerifierQualifier, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.SecondaryReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (secondary)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver sec, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.SecondaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (secondary)", Err: err}) } ter, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.TertiaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (tertiary)", Err: err}) + } + if len(missing) == 0 { + tc.ccvs = []protocol.CCV{sec, ter} } - tc.ccvs = []protocol.CCV{sec, ter} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + return missing, nil }, } } @@ -485,35 +533,45 @@ func receiverQuaternaryAllThreeVerifiers(lib ccv.Lib, src, dest uint64, args Arg numExpectedVerifications: 3, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.QuaternaryReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (quaternary)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver + def, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) } sec, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.SecondaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (secondary)", Err: err}) } ter, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.TertiaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (tertiary)", Err: err}) + } + if len(missing) == 0 { + tc.ccvs = []protocol.CCV{def, sec, ter} } - tc.ccvs = []protocol.CCV{def, sec, ter} + executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + + return missing, nil }, } } @@ -536,35 +594,41 @@ func receiverQuaternaryDefaultAndSecondary(lib ccv.Lib, src, dest uint64, args A numExpectedVerifications: 2, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.QuaternaryReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (quaternary)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver def, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) } sec, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.SecondaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (secondary)", Err: err}) + } + if len(missing) == 0 { + tc.ccvs = []protocol.CCV{def, sec} } - tc.ccvs = []protocol.CCV{def, sec} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + return missing, nil }, } } @@ -587,33 +651,41 @@ func receiverQuaternaryDefaultAndTertiary(lib ccv.Lib, src, dest uint64, args Ar numExpectedVerifications: 2, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.QuaternaryReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (quaternary)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver + def, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) } ter, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.TertiaryCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (tertiary)", Err: err}) + } + if len(missing) == 0 { + tc.ccvs = []protocol.CCV{def, ter} } - tc.ccvs = []protocol.CCV{def, ter} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + + return missing, nil }, } } @@ -636,35 +708,42 @@ func maxDataSize(lib ccv.Lib, src, dest uint64, args Args) *v3TestCase { numExpectedVerifications: 1, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + maxDataBytes, err := env.Dst.GetMaxDataBytes(ctx, tc.dst) if err != nil { - return false + return nil, fmt.Errorf("get max data bytes: %w", err) } tc.msgData = bytes.Repeat([]byte("a"), int(maxDataBytes)) + var missing []tcapi.MissingPrerequisite + receiver, err := env.DstResolver.GetContractReceiver(env.DS, tc.dst, common.DefaultReceiverQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "contract receiver (default)", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver ccv, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) + } else { + tc.ccvs = []protocol.CCV{ccv} } - tc.ccvs = []protocol.CCV{ccv} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + + return missing, nil }, } } @@ -689,30 +768,36 @@ func eoaReceiverDefaultVerifierSafeTag(lib ccv.Lib, src, dest uint64, args Args) numExpectedVerifications: 1, args: args, }, - hydrate: func(ctx context.Context, tc *v3TestCase) bool { - env, ok := loadV3Env(ctx, tc.lib, tc.src, tc.dst) - if !ok { - return false + hydrate: func(ctx context.Context, tc *v3TestCase) ([]tcapi.MissingPrerequisite, error) { + env, err := loadV3Env(ctx, tc.lib, tc.src, tc.dst) + if err != nil { + return nil, err } + + var missing []tcapi.MissingPrerequisite + receiver, err := env.Dst.GetEOAReceiverAddress() if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "EOA receiver address", Err: err}) + } else { + tc.receiver = receiver } - tc.receiver = receiver ccv, err := getCommitteeCCV(env.SrcResolver, env.DS, tc.src, common.DefaultCommitteeVerifierQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "committee CCV (default)", Err: err}) + } else { + tc.ccvs = []protocol.CCV{ccv} } - tc.ccvs = []protocol.CCV{ccv} executorAddr, err := env.SrcResolver.GetExecutor(env.DS, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executorAddr } - tc.executor = executorAddr - return true + return missing, nil }, } } diff --git a/build/devenv/tests/e2e/tcapi/token_transfer/v3.go b/build/devenv/tests/e2e/tcapi/token_transfer/v3.go index ab44af377..66a74c590 100644 --- a/build/devenv/tests/e2e/tcapi/token_transfer/v3.go +++ b/build/devenv/tests/e2e/tcapi/token_transfer/v3.go @@ -2,6 +2,7 @@ package token_transfer import ( "context" + "errors" "fmt" "math/big" "time" @@ -39,7 +40,7 @@ type tokenTransferV3TestCase struct { srcToken protocol.UnknownAddress destToken protocol.UnknownAddress executor protocol.UnknownAddress - hydrate func(ctx context.Context, tc *tokenTransferV3TestCase) bool + hydrate func(ctx context.Context, tc *tokenTransferV3TestCase) ([]tcapi.MissingPrerequisite, error) hydrated bool } @@ -47,24 +48,35 @@ func (tc *tokenTransferV3TestCase) Name() string { return tc.name } -func (tc *tokenTransferV3TestCase) ensureHydrated(ctx context.Context) error { +func (tc *tokenTransferV3TestCase) ensureHydrated(ctx context.Context) ([]tcapi.MissingPrerequisite, error) { if tc.hydrated { - return nil + return nil, nil } if tc.hydrate == nil { - return fmt.Errorf("%s: missing hydrate func", tc.name) + return nil, fmt.Errorf("%s: missing hydrate func", tc.name) } - if !tc.hydrate(ctx, tc) { - return fmt.Errorf("%s: prerequisites not met", tc.name) + missing, err := tc.hydrate(ctx, tc) + if err != nil { + return nil, err } - tc.hydrated = true - return nil + if len(missing) == 0 { + tc.hydrated = true + } + return missing, nil } func (tc *tokenTransferV3TestCase) Run(ctx context.Context) error { - if err := tc.ensureHydrated(ctx); err != nil { + missing, err := tc.ensureHydrated(ctx) + if err != nil { return err } + if len(missing) > 0 { + errs := make([]error, len(missing)) + for i, m := range missing { + errs[i] = m + } + return errors.Join(errs...) + } l := zerolog.Ctx(ctx) chainMap, err := tc.lib.ChainsMap(ctx) if err != nil { @@ -195,8 +207,8 @@ func (tc *tokenTransferV3TestCase) Run(ctx context.Context) error { return nil } -func (tc *tokenTransferV3TestCase) HavePrerequisites(ctx context.Context) bool { - return tc.ensureHydrated(ctx) == nil +func (tc *tokenTransferV3TestCase) CheckPrerequisites(ctx context.Context) ([]tcapi.MissingPrerequisite, error) { + return tc.ensureHydrated(ctx) } // TokenTransfer returns a single token transfer test case for the given combo, finality, receiver type, and name. @@ -218,68 +230,88 @@ func tokenTransferCase(lib ccv.Lib, src, dest uint64, combo common.TokenCombinat numExpectedVer: combo.ExpectedVerifierResults(), args: args, }, - hydrate: func(ctx context.Context, tc *tokenTransferV3TestCase) bool { + hydrate: func(ctx context.Context, tc *tokenTransferV3TestCase) ([]tcapi.MissingPrerequisite, error) { srcFamily, err := chain_selectors.GetSelectorFamily(tc.src) if err != nil { - return false + return nil, fmt.Errorf("source chain family: %w", err) } srcReg, err := chainreg.GetRegistry().Get(srcFamily) if err != nil { - return false + return nil, fmt.Errorf("source chain registry: %w", err) } dstFamily, err := chain_selectors.GetSelectorFamily(tc.dst) if err != nil { - return false + return nil, fmt.Errorf("dest chain family: %w", err) } dstReg, err := chainreg.GetRegistry().Get(dstFamily) if err != nil { - return false + return nil, fmt.Errorf("dest chain registry: %w", err) } if srcReg.AddressResolver == nil || dstReg.AddressResolver == nil { - return false + return nil, fmt.Errorf("missing address resolver") } ds, err := tc.lib.DataStore() if err != nil { - return false + return nil, fmt.Errorf("data store: %w", err) } chainMap, err := tc.lib.ChainsMap(ctx) if err != nil { - return false + return nil, fmt.Errorf("chains map: %w", err) } src, ok := chainMap[tc.src] if !ok { - return false + return nil, fmt.Errorf("chain %d not found in chains map", tc.src) } dst, ok := chainMap[tc.dst] if !ok { - return false + return nil, fmt.Errorf("chain %d not found in chains map", tc.dst) } sender, err := src.GetSenderAddress() if err != nil { - return false + return nil, fmt.Errorf("sender address: %w", err) } tc.sender = sender + var missing []tcapi.MissingPrerequisite + + var receiverName string + var receiver protocol.UnknownAddress + var receiverErr error if tc.useEOAReceiver { - tc.receiver, err = dst.GetEOAReceiverAddress() + receiver, receiverErr = dst.GetEOAReceiverAddress() + receiverName = "EOA receiver address" } else { - tc.receiver, err = dstReg.AddressResolver.GetContractReceiver(ds, tc.dst, common.DefaultReceiverQualifier) + receiver, receiverErr = dstReg.AddressResolver.GetContractReceiver(ds, tc.dst, common.DefaultReceiverQualifier) + receiverName = "contract receiver (default)" } + if receiverErr != nil { + missing = append(missing, tcapi.MissingPrerequisite{Name: receiverName, Err: receiverErr}) + } else { + tc.receiver = receiver + } + + srcToken, err := srcReg.AddressResolver.GetToken(ds, tc.src, tc.combo.LocalPoolAddressRef()) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: fmt.Sprintf("source token (%s)", tc.combo.LocalPoolAddressRef().Qualifier), Err: err}) + } else { + tc.srcToken = srcToken } - tc.srcToken, err = srcReg.AddressResolver.GetToken(ds, tc.src, tc.combo.LocalPoolAddressRef()) + destToken, err := dstReg.AddressResolver.GetToken(ds, tc.dst, tc.combo.RemotePoolAddressRef()) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: fmt.Sprintf("dest token (%s)", tc.combo.RemotePoolAddressRef().Qualifier), Err: err}) + } else { + tc.destToken = destToken } - tc.destToken, err = dstReg.AddressResolver.GetToken(ds, tc.dst, tc.combo.RemotePoolAddressRef()) + + executor, err := srcReg.AddressResolver.GetExecutor(ds, tc.src, common.DefaultExecutorQualifier) if err != nil { - return false + missing = append(missing, tcapi.MissingPrerequisite{Name: "executor (default)", Err: err}) + } else { + tc.executor = executor } - tc.executor, err = srcReg.AddressResolver.GetExecutor(ds, tc.src, common.DefaultExecutorQualifier) - return err == nil + return missing, nil }, } } diff --git a/build/devenv/tests/e2e/tcapi/types.go b/build/devenv/tests/e2e/tcapi/types.go index 42e7612f9..00f1890aa 100644 --- a/build/devenv/tests/e2e/tcapi/types.go +++ b/build/devenv/tests/e2e/tcapi/types.go @@ -23,9 +23,24 @@ const ( DefaultSentTimeout = 10 * time.Second ) +// MissingPrerequisite describes a single absent prerequisite in the environment, +// such as a contract that has not been deployed or a service that is not running. +// It implements the error interface so it can be used with errors.Join and +// testify assertions. +type MissingPrerequisite struct { + // Name is a human-readable label identifying what is missing. + Name string + // Err is the underlying error returned by the lookup that failed. + Err error +} + +func (m MissingPrerequisite) Error() string { + return fmt.Sprintf("%s: %v", m.Name, m.Err) +} + // TestCase represents a test case that can be run in a variety of environments. // Implementations may resolve environment-specific configuration (e.g. contract -// addresses) during HavePrerequisites or Run. +// addresses) during CheckPrerequisites or Run. type TestCase interface { // Name returns the name of the test case. Name() string @@ -33,17 +48,20 @@ type TestCase interface { // Run runs the test case. // The context is typically derived from the *testing.T's Context() method. // Implementations hydrate any required configuration before executing; callers - // do not need to call HavePrerequisites first. Returns an error if prerequisites + // do not need to call CheckPrerequisites first. Returns an error if prerequisites // are not met. Run(ctx context.Context) error - // HavePrerequisites reports whether this test case can run in the current + // CheckPrerequisites checks whether this test case can run in the current // environment (e.g. required contracts deployed, services running). // The context is typically derived from the *testing.T's Context() method. - // Implementations typically perform the same hydration as Run; when it succeeds, - // subsequent Run calls reuse that state. Returns false to skip the test without - // treating it as a failure. - HavePrerequisites(ctx context.Context) bool + // Returns a non-empty slice of MissingPrerequisite when the environment lacks + // one or more expected components — callers should skip the test without + // treating it as a failure. Returns a non-nil error when the check itself + // could not complete (e.g. data store unavailable, unknown chain selector); + // callers should treat this as an unexpected failure. When it succeeds, + // subsequent Run calls reuse the hydration state. + CheckPrerequisites(ctx context.Context) ([]MissingPrerequisite, error) } // DefaultV3ExecutionGasLimit is the execution gas limit used when SendConfig and MessageOptions omit it. diff --git a/changelog/2026-06-09_tcapi_check_prerequisites.md b/changelog/2026-06-09_tcapi_check_prerequisites.md new file mode 100644 index 000000000..6b06eae08 --- /dev/null +++ b/changelog/2026-06-09_tcapi_check_prerequisites.md @@ -0,0 +1,152 @@ +# tcapi: CheckPrerequisites replaces HavePrerequisites + +## Executive Summary + +- `TestCase.HavePrerequisites(ctx) bool` is replaced by `CheckPrerequisites(ctx) ([]MissingPrerequisite, error)`. +- The old signature discarded all diagnostic information; callers could not tell why a test was skipped or whether the skip was expected. +- Affects every `tcapi.TestCase` implementation and every call site that gates test execution on prerequisite checks. +- Introduces a **breaking change**: the method is renamed and its return type changes completely. + +## AI Adapter Index + +| Symbol | Kind | Search | Location | Section | +|---|---|---|---|---| +| `tcapi.TestCase.HavePrerequisites → tcapi.TestCase.CheckPrerequisites` | renamed + signature-changed | `\.HavePrerequisites\b` | `build/devenv/tests/e2e/tcapi/types.go:64` | [#haveprerequisites-renamed-to-checkprerequisites](#haveprerequisites-renamed-to-checkprerequisites) | +| `tcapi.MissingPrerequisite` | added | `MissingPrerequisite` | `build/devenv/tests/e2e/tcapi/types.go:30` | [#missingprerequisite-new-type](#missingprerequisite-new-type) | +| `basic.loadV3Env` | signature-changed | `loadV3Env\(` | `build/devenv/tests/e2e/tcapi/basic/v3.go:196` | [#loadv3env-return-type](#loadv3env-return-type) | + +## Breaking Changes + +### HavePrerequisites renamed to CheckPrerequisites + +- **What changed:** `tcapi.TestCase` interface method `HavePrerequisites(ctx context.Context) bool` +- **Before:** `HavePrerequisites(ctx context.Context) bool` +- **After:** `CheckPrerequisites(ctx context.Context) ([]MissingPrerequisite, error)` +- **Why:** A `bool` return discards all information about *what* was missing. The new signature lets callers distinguish between "some expected component is absent" (non-empty `[]MissingPrerequisite`, skip gracefully) and "the check itself failed" (non-nil `error`, fail loudly). +- **Who is affected:** Every implementation of `tcapi.TestCase` and every caller that gates test execution on the prerequisite check. + +## Migration Guide + +1. **Rename the method** in every `TestCase` implementation from `HavePrerequisites` to `CheckPrerequisites` and update its signature: + +```go +// Before +func (tc *myTestCase) HavePrerequisites(ctx context.Context) bool { + return tc.ensureHydrated(ctx) == nil +} +``` + +```go +// After +func (tc *myTestCase) CheckPrerequisites(ctx context.Context) ([]tcapi.MissingPrerequisite, error) { + return tc.ensureHydrated(ctx) +} +``` + +2. **Update `ensureHydrated`** (or equivalent internal function) to return `([]tcapi.MissingPrerequisite, error)`: + +```go +// Before +func (tc *myTestCase) ensureHydrated(ctx context.Context) error { + if !tc.hydrate(ctx, tc) { + return fmt.Errorf("prerequisites not met") + } + tc.hydrated = true + return nil +} +``` + +```go +// After +func (tc *myTestCase) ensureHydrated(ctx context.Context) ([]tcapi.MissingPrerequisite, error) { + missing, err := tc.hydrate(ctx, tc) + if err != nil { + return nil, err + } + if len(missing) == 0 { + tc.hydrated = true + } + return missing, nil +} +``` + +3. **Update the `hydrate` closure type** from `func(...) bool` to `func(...) ([]tcapi.MissingPrerequisite, error)`. Inside the closure: + - Infrastructure failures (data store, chain selectors, registry lookups, sender address) → `return nil, fmt.Errorf("...")` + - Contract/service lookups that may simply be absent → `append(missing, tcapi.MissingPrerequisite{Name: "...", Err: err})` + +4. **Update `Run`** to handle the new `ensureHydrated` signature. When missing prerequisites are present, return them joined as a single error: + +```go +// Before +func (tc *myTestCase) Run(ctx context.Context) error { + if err := tc.ensureHydrated(ctx); err != nil { + return err + } + // ... +} +``` + +```go +// After +func (tc *myTestCase) Run(ctx context.Context) error { + missing, err := tc.ensureHydrated(ctx) + if err != nil { + return err + } + if len(missing) > 0 { + errs := make([]error, len(missing)) + for i, m := range missing { + errs[i] = m + } + return errors.Join(errs...) + } + // ... +} +``` + +5. **Update call sites** that gate test execution: + +```go +// Before +if tc.HavePrerequisites(ctx) { + t.Run(tc.Name(), func(t *testing.T) { ... }) +} else { + t.Logf("Skipping %s because current environment does not have the prerequisites", tc.Name()) +} +``` + +```go +// After +if missing, err := tc.CheckPrerequisites(ctx); err != nil { + t.Fatalf("prerequisite check failed for %s: %v", tc.Name(), err) +} else if len(missing) > 0 { + t.Logf("Skipping %s: missing prerequisites: %v", tc.Name(), missing) +} else { + t.Run(tc.Name(), func(t *testing.T) { ... }) +} +``` + +## New Features / Additions + +### MissingPrerequisite new type + +`tcapi.MissingPrerequisite` is a new struct that represents a single expected-absent component in the environment: + +```go +type MissingPrerequisite struct { + Name string // human-readable label, e.g. "contract receiver (default)" + Err error // underlying lookup error +} + +func (m MissingPrerequisite) Error() string // implements error +``` + +It implements the `error` interface so it can be used with `errors.Join` in `Run` and with testify assertions (`require.Empty(t, missing)`). + +The distinction between `MissingPrerequisite` and the `error` return channel: +- `[]MissingPrerequisite` — the environment lacks an expected component; callers should skip the test without treating it as a failure. +- `error` — the prerequisite check itself could not complete (data store unavailable, unknown chain selector, missing registry); callers should fail loudly. + +## References + +- Prior changelog: `changelog/2026-05-18_simplify_tcapi.md`