runtime

package
v0.49.1 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2025 License: MIT Imports: 6 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.

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 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 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 State

type State struct {
	AddressBook fdeployment.AddressBook                // Legacy address book (deprecated)
	DataStore   fdatastore.DataStore                   // Datastore containing address references and metadata
	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) 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.

Jump to

Keyboard shortcuts

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