Documentation
¶
Overview ¶
Package runtime provides an execution environment for executing changesets in tests.
The runtime package is the core testing infrastructure that manages state accumulation across multiple task executions, ensuring that each task operates on a fresh environment that reflects the cumulative state changes from previous executions. This enables comprehensive integration testing of deployment workflows where multiple changesets or operations need to be executed in sequence.
Core Concepts ¶
The runtime operates on three main concepts:
- Runtime: The main orchestrator that manages task execution and state accumulation
- Executable: An interface that represents any task that can be executed by the runtime
- State: Internal state that accumulates changes from task executions
Thread Safety ¶
The runtime is thread-safe and ensures sequential execution of tasks through mutex protection. This guarantees that state consistency is maintained even when multiple goroutines attempt to execute tasks concurrently.
State Management ¶
Each task execution follows this process:
- Execute the task against the current environment
- Task is responsible for updating the runtime state
- Generate a new environment incorporating the updated state
- Proceed to the next task
The state accumulates:
- Address book entries from changeset deployments
- DataStore updates including contract addresses and metadata
- Changeset outputs for debugging and verification
Basic Usage ¶
Here's a simple example of using the runtime to execute a changeset:
import ( "testing" "github.com/stretchr/testify/require" testenv "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" ) func TestMyDeployment(t *testing.T) { // Test environment with a simulated EVM blockchain loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) // Create runtime instance runtime := NewFromEnvironment(*env) // Execute a changeset task := ChangesetTask(myChangeset, MyChangesetConfig{ Parameter1: "value1", Parameter2: 42, }) err := runtime.Exec(task) require.NoError(t, err) // Verify deployment results addrs, err := runtime.State().DataStore.Addresses().Fetch() require.NoError(t, err) assert.Len(t, addrs, 1) }
Sequential Changeset Execution ¶
The runtime handles executing multiple changesets in sequence, where later changesets depend on the results of earlier ones. The environment provided to the later changesets will include the data of the previous changesets execution.
func TestMultiStepDeployment(t *testing.T) { // Load test environment with multiple EVM chains loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) runtime := NewFromEnvironment(*env) // Define the first changeset coreTask := ChangesetTask(coreChangeset, CoreConfig{}) // Define the dependent second changeset // The environment provided to the dependent changeset will include the data of the previous changesets execution dependentTask := ChangesetTask(dependentChangeset, DependentConfig{}) // Execute in sequence - each task sees the results of previous tasks err := runtime.Exec(coreTask, dependentTask) require.NoError(t, err) // Verify final state contains all deployed contracts addrs, err := runtime.State().DataStore.Addresses().Fetch() require.NoError(t, err) assert.Len(t, addrs, 1) }
Environment Loading Options ¶
The engine/test/environment package provides various blockchain loading options:
// EVM simulated blockchains (fast, in-memory) env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 2)) // Specific chain selectors env, err := loader.Load(t, testenv.WithEVMSimulated(t, []uint64{ chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector, chainsel.POLYGON_TESTNET_MUMBAI.Selector, })) // Multiple blockchain types env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1), testenv.WithSolanaContainerN(t, 1, "/path/to/programs", programIDs), testenv.WithTonContainerN(t, 1), ) // Custom EVM configuration cfg := onchain.EVMSimLoaderConfig{ ChainID: 1337, BlockTime: time.Second, } env, err := loader.Load(t, testenv.WithEVMSimulatedWithConfigN(t, 1, cfg))
Error Handling ¶
Task execution is atomic - if any task fails, the runtime state remains unchanged and subsequent tasks are not executed:
func TestErrorHandling(t *testing.T) { // Load test environment loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) runtime := NewFromEnvironment(*env) successTask := ChangesetTask(workingChangeset, WorkingConfig{}) failingTask := ChangesetTask(brokenChangeset, BrokenConfig{}) shouldNotRunTask := ChangesetTask(afterFailureChangeset, AfterFailureConfig{}) err := runtime.Exec(successTask, failingTask, shouldNotRunTask) require.Error(t, err) assert.Contains(t, err.Error(), "expected failure message") // Only the successful task's output should be in state outputs := runtime.State().Outputs assert.Contains(t, outputs, "working-changeset-output") assert.NotContains(t, outputs, "broken-changeset-output") assert.NotContains(t, outputs, "after-failure-output") }
State Inspection ¶
The runtime provides access to accumulated state for verification and debugging:
func TestStateInspection(t *testing.T) { // Load test environment loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) runtime := NewFromEnvironment(*env) // Execute some tasks err := runtime.Exec(task1, task2, task3) require.NoError(t, err) // Inspect final state runtimeState := runtime.State() // Check deployed addresses addrs, err := runtimeState.DataStore.Addresses().Fetch() require.NoError(t, err) // Check address book entries (legacy) addressBook := runtimeState.AddressBook // Check task execution outputs taskOutputs := runtimeState.Outputs assert.Contains(t, taskOutputs, "task1-id") assert.Contains(t, taskOutputs, "task2-id") assert.Contains(t, taskOutputs, "task3-id") // Access current environment (reflects all changes) currentEnv := runtime.Environment() finalAddrs, err := currentEnv.DataStore.Addresses().Fetch() require.NoError(t, err) assert.Equal(t, addrs, finalAddrs) }
Integration with Testing Framework ¶
The runtime integrates seamlessly with Go's testing framework and popular assertion libraries like testify:
func TestIntegrationExample(t *testing.T) { t.Parallel() // Runtime is thread-safe // Subtests can each have their own runtime instance t.Run("deployment_scenario_1", func(t *testing.T) { loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) runtime := NewFromEnvironment(*env) err = runtime.Exec(scenario1Tasks...) require.NoError(t, err) // scenario1-specific assertions }) t.Run("deployment_scenario_2", func(t *testing.T) { loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 2)) require.NoError(t, err) runtime := NewFromEnvironment(*env) err = runtime.Exec(scenario2Tasks...) require.NoError(t, err) // scenario2-specific assertions }) }
Best Practices ¶
- Create a new runtime instance for each test to ensure isolation.
- Be wary when using containerized chains as they take a long time to start containers. It is better to write a longer test with a single containerized chain than to write a shorter test with multiple containerized chains.
- Verify both intermediate and final state in multi-step tests
- Leverage the runtime's error handling for negative test cases
Common Patterns ¶
## Setup Environment
func setupTestEnvironment(t *testing.T) *fdeployment.Environment { t.Helper() loader := testenv.NewLoader() env, err := loader.Load(t, testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) return env }
## Task Factory Pattern
func DeployTokenTask(config TokenConfig) Executable { return ChangesetTask(tokenChangeset, config) }
Index ¶
- func ChangesetTask[C any](changeset fdeployment.ChangeSetV2[C], config C) changesetTask[C]
- func ExecuteProposalTask(proposalID string) executeProposalTask
- func SignProposalTask(proposalID string, privateKey *ecdsa.PrivateKey) signProposalTask
- type Executable
- type ProposalState
- type Runtime
- type RuntimeOption
- type State
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ChangesetTask ¶
func ChangesetTask[C any](changeset fdeployment.ChangeSetV2[C], config C) changesetTask[C]
ChangesetTask creates a new executable task that runs a changeset. It generates a unique ID for the task and returns a task that can be executed by the runtime.
func ExecuteProposalTask ¶ added in v0.50.1
func ExecuteProposalTask(proposalID string) executeProposalTask
ExecuteProposalTask creates a new task for executing signed MCMS or Timelock proposals. The task will automatically detect the proposal type and apply the appropriate execution method when run.
Parameters:
- proposalID: The unique identifier of the signed proposal to execute from the runtime state
Returns an executeProposalTask that can be executed within the runtime framework.
Note: The proposal must be properly signed before execution. Unsigned proposals will fail during the execution process.
func SignProposalTask ¶ added in v0.50.1
func SignProposalTask(proposalID string, privateKey *ecdsa.PrivateKey) signProposalTask
SignProposalTask creates a new task for signing MCMS or Timelock proposals. The task will automatically detect the proposal type and apply the appropriate signing method when executed.
Parameters:
- proposalID: The unique identifier of the proposal to sign from the runtime state
- privateKey: The ECDSA private key to use for signing the proposal
Returns a signProposalTask that can be executed within the runtime framework.
Types ¶
type Executable ¶
type Executable interface { // ID returns a unique identifier for this executable task. ID() string // Run executes the task against the provided environment and updates the state. // The environment represents the current deployment state, and the state parameter // should be updated with any changes produced by the execution. Run(e fdeployment.Environment, state *State) error }
Executable represents a task that can be executed by the runtime.
Executables are used to manipulate the runtime state and environment. The most common implementation is to run a changeset, however this could be extended to perform offchain operations in the future.
type ProposalState ¶ added in v0.50.1
ProposalState is a wrapper around a proposal that includes an ID and contains helper methods for interacting with the proposal.
func (ProposalState) Kind ¶ added in v0.50.1
func (p ProposalState) Kind() (mcmstypes.ProposalKind, error)
Kind returns the kind of the proposal.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime provides an execution environment for running tasks in tests.
It manages the state accumulation across multiple task executions and ensures that each task operates on a fresh environment that reflects the cumulative state changes from previous executions.
The runtime is thread-safe and ensures sequential execution of task to maintain state consistency. Each task execution updates the internal state and regenerates the environment for subsequent executions.
func New ¶ added in v0.50.1
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error)
New creates a new Runtime instance initialized with the given options.
func NewFromEnvironment ¶
func NewFromEnvironment(e fdeployment.Environment) *Runtime
NewFromEnvironment creates a new Runtime instance initialized with the given environment.
Initial state is seeded from the provided environment's existing addresses and datastore. A fresh environment is immediately generated and cached for the first execution.
Returns a configured Runtime ready for task execution, or an error if initialization fails.
func (*Runtime) Environment ¶
func (r *Runtime) Environment() fdeployment.Environment
Environment returns the current environment of the runtime.
func (*Runtime) Exec ¶
func (r *Runtime) Exec(executables ...Executable) error
Exec executes a sequence of tasks in order, ensuring each operates on a fresh environment that reflects the cumulative state changes from previous executions.
The execution process for each task: 1. Execute the task against the current environment (The task is responsible for updating state) 2. Generate a new environment incorporating the updated state 3. Proceed to the next task
Execution is thread-safe and atomic - if any task fails, the runtime state remains unchanged and the error is returned immediately. All tasks must succeed for the execution to be considered successful.
Returns an error if any task execution fails
type RuntimeOption ¶ added in v0.50.1
type RuntimeOption func(*runtimeConfig)
RuntimeOption is a functional option type for configuring runtime.
func WithEnvOpts ¶ added in v0.50.1
func WithEnvOpts(opts ...environment.LoadOpt) RuntimeOption
WithEnvironmentOptions adds environment options to the runtime. This is used to load the environment with the given options.
type State ¶
type State struct { AddressBook fdeployment.AddressBook // Legacy address book (deprecated) DataStore fdatastore.DataStore // Datastore containing address references and metadata Proposals []ProposalState // All MCMS and timelock proposals keyed by proposal id Outputs map[string]fdeployment.ChangesetOutput // Changeset outputs keyed by changeset ID // contains filtered or unexported fields }
State represents the mutable State of a test runtime environment. It tracks the accumulated results of changeset executions including address book updates, datastore changes, and changeset outputs. All State modifications are thread-safe through the use of a mutex.
The State is updated after each changeset execution to reflect the changes made by that changeset, allowing subsequent changesets to build upon previous results.
func (*State) GetProposal ¶ added in v0.50.1
func (s *State) GetProposal(id string) (ProposalState, error)
GetProposal returns the ProposalState for the given ID. If the proposal is not found, it returns an error.
func (*State) MarkProposalExecuted ¶ added in v0.50.1
MarkProposalExecuted marks the proposal state as executed.
func (*State) MergeChangesetOutput ¶
func (s *State) MergeChangesetOutput(id string, out fdeployment.ChangesetOutput) error
MergeChangesetOutput updates the state with the results of a changeset execution. This method is thread-safe and updates all relevant state components based on the changeset output.
The update process includes: 1. Merging any address book changes from the changeset output 2. Merging any datastore changes from the changeset output 3. Storing the complete changeset output for future reference
Returns an error if the address book or datastore merge operations fail.