Expand description
A harness for writing multi-agent integration tests as ordinary application code.
A test builds a Scenario against an explicit NetworkConfig, hands it
a set of Agents, and asserts on what they observe. Test scenarios are
run in #[tokio::test] functions.
Each agent receives a deterministic native account derived from the scenario
seed. Agents deploy contracts with explicit funding and optional
initialization, call them, await receipts, and read chain state, in
addition to coordinating with other agents off-chain and using the virtual
clock and deterministic randomness.
use thruput_testkit::{Agent, DeployOptions, LocalNetworkConfig, NetworkConfig, Scenario};
let mut config = LocalNetworkConfig::default();
config.hog_devnode_binary = Some("/path/to/supporting/hog-devnode".into());
config.contract_artifact_directories.push("/path/to/built_contracts".into());
let scenario = Scenario::builder("counter-test")
.network(NetworkConfig::local(config))
.artifact("counter", "target/thruput/counter.rv32a")
.build()
.await?;
scenario.run_agents([Agent::new("alice", |mut ctx| async move {
ctx.fund(1_000_000_000).await?; // 100 native tokens from the bootstrap faucet
let deployed = ctx.deploy_artifact("counter", DeployOptions::funded(100_000_000)).await?;
assert!(deployed.registered);
Ok(())
})]).await?;
scenario.stop().await?;§Networks and accounts
NetworkConfig::existing attaches to a running node,
NetworkConfig::local launches and owns an ephemeral hog-devnode, and
NetworkConfig::offline runs agents with locally-derived accounts and no chain
(for coordination/timing/messaging tests). Scenario::run_agents returns an
error when any agent returns an error, so the ordinary .await? test path cannot
silently ignore agent failures.
Before using a live network, the scenario checks that the endpoint is compatible and serves the network hash named in its runtime manifest, so nothing is signed for a different network.
The scenario owns its fixture accounts’ keys, derived from its seed;
ScenarioBuilder::accounts sets how many there are. A local node receives
only their public allocations, of
LocalNetworkConfig::initial_account_tokens raw native token units each
(100 native tokens by default). On an existing network the accounts start
empty: fund them explicitly with AgentContext::fund or
AgentContext::submit_fund.
§Manual blocks and uncertain submissions
AgentContext::submit_fund, AgentContext::submit_deploy_artifact and
the submit forms of calls return handles without waiting, for tests that
drive blocks by hand (see BlockDriverConfig): await pending, produce a
block, then await the receipt. Receipt waits are read-only.
Each submission reserves its nonce before dispatch. When a submission’s
outcome is uncertain, the harness never resends it: the error retains the
original transaction hash and exact signed bytes for reconciliation
(TestkitError::Submission, or TestkitError::ReceiptWait for a wait
that stopped), as does a SubmittedTransaction. In particular:
- A deployment is accepted only once its successful receipt matches the
original hash and predicted address; until then
TestkitError::UnverifiedDeploymentretains both. A new request for the same artifact with different funding or initialization is refused (TestkitError::DeploymentRequestMismatch) until the retained attempt is resolved, and scheduler statuses never replay or discard it. - Local funding is retained on the
Networkhandle before it is sent. After an uncertain response, retry the same recipient and amount on that handle, and keep it alive during recovery; a changed amount is refused (TestkitError::FundingRequestMismatch). Conflicting or invalid funding evidence is kept inspectable inTestkitError::InvalidFundingEvidenceorTestkitError::InvalidFundingResponse; it does not prove admission.
§Typed calls
With thruput-sdk’s remote feature (typically on a dev-dependency),
#[contract] generates an <Iface>Remote proxy for each interface. Bound
to an agent’s Caller, each of its methods encodes the arguments and
returns a prepared invocation: a ContractView for a method declared
pure or readOnly, which awaiting simulates with the agent as
msg.sender, or otherwise a ContractCall, which awaiting submits
through the agent’s wallet. Either decodes the method’s return type. A
method with no effect annotation gets the same proxy method as one declared
stateChanging, so the annotation can be omitted; the omission is only
visible in the contract’s spec, where the effect is recorded as undeclared.
mod counter {
use thruput_sdk::contract;
#[contract(version = "0.1.0")]
pub trait Counter {
/// Add `by` to the count, returning the new count.
fn increment(&mut self, by: u64) -> u64;
#[contract(effect = "readOnly")]
fn get(&self) -> u64;
}
}
use counter::CounterRemote;
use thruput_testkit::Agent;
use thruput_testkit::xdr::types::ContractAddress;
fn alice(address: ContractAddress) -> Agent {
Agent::new("alice", move |ctx| async move {
let counter = CounterRemote::new(ctx.caller().clone(), &address);
let count = counter.increment(1).await?; // submitted
assert_eq!(counter.get().await?, count); // simulated
Ok(())
})
}See contract for receipts, deferred submission, and reads through
methods without a declared effect, and Contract for calling a method by
name when no interface crate is at hand.
Re-exports§
pub use agent::Agent;pub use agent::AgentContext;pub use agent::AgentId;pub use caller::Caller;pub use clock::Clock;pub use contract::Contract;pub use contract::ContractCall;pub use contract::ContractView;pub use contract::SubmittedContractCall;pub use contract::entrypoint_id;pub use contract::try_entrypoint_id;pub use error::AgentFailure;pub use error::AgentFailures;pub use error::Result;pub use error::TestkitError;pub use mailbox::Envelope;pub use mailbox::Mailbox;pub use mailbox::Message;pub use network::LocalNetworkConfig;pub use network::Network;pub use network::NetworkConfig;pub use network::SubmittedTransaction;pub use rng::AgentRng;pub use scenario::AgentOutcome;pub use scenario::Scenario;pub use scenario::ScenarioBuilder;pub use scenario::ScenarioReport;pub use thruput_json_rpc_types as rpc_types;pub use thruput_xdr as xdr;pub use xdrs;
Modules§
- agent
- Agents and the context the harness hands them.
- caller
- A cloneable handle for submitting transactions from an agent’s account.
- clock
- Virtual clock owned by the harness.
- contract
- Contract method helpers.
- error
- Error type for the test harness.
- mailbox
- Off-chain messaging between agents.
- network
- The network a scenario runs against: how to reach it (
NetworkConfig) and the live handle agents use (Network). - rng
- Deterministic per-agent randomness.
- scenario
- Scenario builder, runner, and result types.
Structs§
- Block
Driver Config - Local block production policy used by the hogd-dev supervisor.
- Contract
EntrypointID - 8-byte identifier for a contract method (entrypoint).
- Deploy
Options - Explicit funding and optional initialization in the deployment transaction.
- Deployed
- Result of a contract deploy.
- Produced
Block - Metadata returned after the scheduler commits a manually-produced block.
- Submitted
Deploy - A submitted deployment whose receipt can be awaited after manual block production.
- Wallet
Init Call - Optional initialization of the newly deployed contract, called by the wallet. A failed invocation rolls back funding and deployment; return bytes are discarded.
Enums§
- Artifact
Kind - Which artifact format a contract binary is in.
- Block
Driver Mode - Block driver wake mode.
- Block
Production Error - Errors returned while manually producing a local block.
- LogLevel
- Log verbosity level for hog-devnode
Functions§
- init_
tracing - Install a stderr
tracingsubscriber that prints forwardedhog-devnodelogs and supervisor diagnostics.