runtime

package
v0.52.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2025 License: MIT Imports: 15 Imported by: 0

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:

  1. Execute the task against the current environment
  2. Task is responsible for updating the runtime state
  3. Generate a new environment incorporating the updated state
  4. 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

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

type ProposalState struct {
	ID         string
	JSON       string
	IsExecuted bool
}

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

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

func (*Runtime) State

func (r *Runtime) State() *State

State returns the current state of the runtime.

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

func (s *State) MarkProposalExecuted(id string) error

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.

func (*State) UpdateProposalJSON added in v0.50.1

func (s *State) UpdateProposalJSON(id string, propJSON string) error

UpdateProposalJSON updates the proposal state with the given ID and JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL