Skip to main content

thruput_testkit/
error.rs

1//! Error type for the test harness.
2
3use std::fmt;
4
5use thruput_json_rpc_types::{responses::TransactionStatus, transaction::TxHash};
6use thruput_node_supervisor::BlockProductionError;
7
8/// Result alias used throughout `thruput-testkit`.
9pub type Result<T, E = TestkitError> = std::result::Result<T, E>;
10
11/// One agent error retained after every non-panicking agent has finished.
12#[derive(Debug)]
13pub struct AgentFailure {
14    name: String,
15    index: usize,
16    error: TestkitError,
17}
18
19impl AgentFailure {
20    pub(crate) fn new(name: String, index: usize, error: TestkitError) -> Self {
21        Self { name, index, error }
22    }
23
24    /// Name of the failed agent.
25    pub fn name(&self) -> &str {
26        &self.name
27    }
28
29    /// Position of the failed agent in the scenario's agent list.
30    pub fn index(&self) -> usize {
31        self.index
32    }
33
34    /// Error returned by the agent body.
35    pub fn error(&self) -> &TestkitError {
36        &self.error
37    }
38}
39
40/// Ordered collection of errors returned by agents in one scenario run.
41#[derive(Debug)]
42pub struct AgentFailures {
43    failures: Vec<AgentFailure>,
44}
45
46impl AgentFailures {
47    pub(crate) fn new(failures: Vec<AgentFailure>) -> Self {
48        debug_assert!(!failures.is_empty());
49        Self { failures }
50    }
51
52    /// Agent failures ordered by their position in the scenario's agent list.
53    pub fn failures(&self) -> &[AgentFailure] {
54        &self.failures
55    }
56}
57
58impl fmt::Display for AgentFailures {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        let count = self.failures.len();
61        write!(
62            f,
63            "{count} agent{} failed",
64            if count == 1 { "" } else { "s" }
65        )?;
66        for failure in &self.failures {
67            write!(
68                f,
69                "; {} (index {}): {}",
70                failure.name, failure.index, failure.error
71            )?;
72        }
73        Ok(())
74    }
75}
76
77/// Errors returned by the harness while building or running a scenario.
78#[derive(Debug, thiserror::Error)]
79#[non_exhaustive]
80pub enum TestkitError {
81    /// The runtime manifest has no faucet account to fund test accounts.
82    #[error("runtime manifest has no bootstrap faucet")]
83    MissingFaucet,
84    /// A pending request must be reconciled before changing its amount.
85    #[error(
86        "a retained funding request for {recipient:?} has a different amount; retry the original amount on this Network handle"
87    )]
88    FundingRequestMismatch {
89        /// Recipient with retained funding intent.
90        recipient: thruput_xdr::types::ContractAddress,
91    },
92    /// A receipt belongs to another transaction.
93    #[error("receipt hash does not match {expected:?}")]
94    ReceiptHashMismatch {
95        /// Locally computed transaction identity.
96        expected: TxHash,
97        /// Complete conflicting receipt, including its claimed commitment and execution.
98        receipt: Box<thruput_json_rpc_types::responses::TransactionReceipt>,
99    },
100    /// An ambiguous submission retains the exact original signed transaction.
101    #[error("submission of {tx_hash:?} was not verified: {detail}")]
102    Submission {
103        /// Locally computed transaction identity.
104        tx_hash: TxHash,
105        /// Exact signed bytes. Observe the original hash before any further action.
106        signed_transaction: String,
107        /// Transport error or unexpected returned identity.
108        detail: String,
109        /// Original structured submission or funding failure.
110        #[source]
111        source: Box<dyn std::error::Error + Send + Sync>,
112    },
113    /// A submitted deployment could not be verified; preserve the original attempt.
114    #[error("deployment {tx_hash:?} at {predicted_address:?} was not verified: {detail}")]
115    UnverifiedDeployment {
116        /// Locally computed transaction identity.
117        tx_hash: TxHash,
118        /// Address predicted from the signed deployment effects.
119        predicted_address: thruput_xdr::types::ContractAddress,
120        /// Exact original signed bytes for reconciliation.
121        signed_transaction: String,
122        /// Failed receipt lookup or verification.
123        detail: Box<str>,
124        /// Original receipt-wait evidence, when waiting stopped before validation.
125        #[source]
126        source: Option<Box<TestkitError>>,
127        /// Authoritative receipt whose deployment return did not match the prediction.
128        receipt: Option<Box<thruput_json_rpc_types::responses::TransactionReceipt>>,
129    },
130    /// Retained deployment effects differ from a new request by the same caller.
131    #[error("artifact {name} has a retained deployment with different funding or initialization")]
132    DeploymentRequestMismatch {
133        /// Artifact with the retained attempt.
134        name: String,
135    },
136    /// A scenario was built without a network configuration.
137    #[error("no network configured for the scenario")]
138    NoNetwork,
139
140    /// A JSON-RPC call to the target failed.
141    #[error("rpc `{method}` failed: {detail}")]
142    Rpc {
143        /// The RPC method that failed.
144        method: &'static str,
145        /// Underlying error description.
146        detail: String,
147    },
148
149    /// A chain operation was attempted against an offline target.
150    #[error("operation `{operation}` requires a live target, but this scenario is offline")]
151    Offline {
152        /// The attempted operation.
153        operation: &'static str,
154    },
155
156    /// The runtime manifest could not be read or parsed.
157    #[error("failed to load runtime manifest `{path}`: {detail}")]
158    ManifestLoad {
159        /// Manifest path.
160        path: String,
161        /// Underlying error description.
162        detail: String,
163    },
164
165    /// The runtime manifest declared an unsupported schema version.
166    #[error("unsupported runtime manifest schema version {actual}")]
167    ManifestSchema {
168        /// The version found in the manifest.
169        actual: u32,
170    },
171
172    /// A field could not be read from the runtime manifest.
173    #[error("runtime manifest field `{field}` is invalid: {detail}")]
174    Manifest {
175        /// The manifest field at fault.
176        field: &'static str,
177        /// Underlying error description.
178        detail: String,
179    },
180
181    /// The manifest used a bootstrap-account derivation scheme we do not support.
182    #[error("unsupported bootstrap-account derivation scheme `{scheme}`")]
183    UnsupportedDerivationScheme {
184        /// The scheme string found in the manifest.
185        scheme: String,
186    },
187
188    /// The manifest carried no bootstrap accounts to assign to agents.
189    #[error("runtime manifest declares no bootstrap accounts")]
190    NoBootstrapAccounts,
191
192    /// A derived signer did not match the public key the manifest advertised.
193    #[error("bootstrap account {index} public key does not match the derived signer")]
194    BootstrapPublicKeyMismatch {
195        /// The bootstrap account index at fault.
196        index: u32,
197    },
198
199    /// The scenario has fewer accounts than it has agents.
200    #[error("scenario needs {needed} accounts but only {available} are available")]
201    NotEnoughAccounts {
202        /// Number of agents that need accounts.
203        needed: usize,
204        /// Number of accounts available.
205        available: usize,
206    },
207
208    /// Two agents in one run were given the same name.
209    #[error("agent name `{name}` appears more than once in the scenario")]
210    DuplicateAgent {
211        /// The duplicated agent name.
212        name: String,
213    },
214
215    /// One or more agent bodies returned errors.
216    #[error("{failures}")]
217    AgentsFailed {
218        /// All returned agent errors, ordered by agent position.
219        failures: AgentFailures,
220    },
221
222    /// An account exhausted its per-account nonce sequence.
223    #[error("account {index} exhausted its nonce sequence")]
224    NonceExhausted {
225        /// The account index at fault.
226        index: u32,
227    },
228
229    /// Building or signing a transaction failed.
230    #[error("transaction build/sign failed: {detail}")]
231    Signing {
232        /// Underlying error description.
233        detail: String,
234    },
235
236    /// Encoding calldata or a contract failed.
237    #[error("xdr encoding failed: {detail}")]
238    Encode {
239        /// Underlying error description.
240        detail: String,
241    },
242
243    /// Decoding a contract return value failed.
244    #[error("xdr decoding failed: {detail}")]
245    Decode {
246        /// Underlying error description.
247        detail: String,
248    },
249
250    /// A contract interface version is not valid `SemVer`.
251    #[error("contract interface version `{version}` is not valid SemVer")]
252    InvalidContractVersion {
253        /// Version string used to derive the method selector.
254        version: String,
255    },
256
257    /// A deploy transaction did not execute successfully.
258    #[error("deploy did not succeed: {detail}")]
259    Deploy {
260        /// Description of the failing execution status.
261        detail: String,
262    },
263
264    /// A contract call executed but its status was not `Ok`.
265    ///
266    /// Carries the method name; add call-site context with `map_err` where
267    /// that alone does not identify which call reverted.
268    #[error("contract call to `{method}` reverted: {status}")]
269    Reverted {
270        /// The method that was called.
271        method: String,
272        /// Description of the failing execution status.
273        status: String,
274    },
275
276    /// A contract call or view did not return successfully.
277    #[error("contract {kind} did not return a value: {detail}")]
278    NoReturn {
279        /// Whether it was a `call` or a `view`.
280        kind: &'static str,
281        /// Description of the non-returning status.
282        detail: String,
283    },
284
285    /// Receipt waiting stopped with retained status, commitment and read errors.
286    #[error("{source}")]
287    ReceiptWait {
288        /// Wait cause plus the strongest observed commitment and status.
289        #[source]
290        source: Box<thruput_json_rpc_client::native::receipt::ReceiptPollError>,
291    },
292    /// Native RPC failure, retaining the original application or malformed result.
293    #[error("rpc `{method}` failed: {source}")]
294    NativeCall {
295        /// Native method that failed.
296        method: &'static str,
297        /// Original RPC error or malformed result, including raw data.
298        #[source]
299        source: Box<thruput_json_rpc_client::native::NativeClientError>,
300    },
301    /// Funding evidence could not be validated; retain the original RPC failure.
302    #[error("invalid local funding evidence: {detail}")]
303    InvalidFundingEvidence {
304        /// Why the supplied evidence could not be accepted.
305        detail: String,
306        /// Original validated hash and signed bytes, if already retained.
307        retained: Option<Box<(TxHash, thruput_json_rpc_types::hex::HexBytes)>>,
308        /// Original RPC error carrying the conflicting or invalid evidence.
309        #[source]
310        source: Box<thruput_json_rpc_client::native::NativeClientError>,
311    },
312    /// A funding result conflicts with the retained request or its signed identity.
313    #[error("invalid local funding response: {detail}")]
314    InvalidFundingResponse {
315        /// Original validated hash and signed bytes, if already retained.
316        retained: Option<Box<(TxHash, thruput_json_rpc_types::hex::HexBytes)>>,
317        /// Complete supplied result, retained without accepting its identity.
318        response: Box<thruput_json_rpc_types::responses::FundingSubmission>,
319        /// Why the response conflicts with the request or original evidence.
320        detail: String,
321    },
322    /// An unfamiliar response is retained without asserting its meaning.
323    #[error("unsupported `{method}` response")]
324    UnsupportedResponse {
325        /// Native method that returned an unfamiliar result.
326        method: &'static str,
327        /// Complete result, retained without interpreting unknown variants.
328        raw: serde_json::Value,
329    },
330    /// Endpoint compatibility or identity validation failed.
331    #[error("{source}")]
332    NativeEndpoint {
333        /// Compatibility, identity or discovery failure with its original evidence.
334        #[source]
335        source: Box<thruput_client::NativeRpcError>,
336    },
337
338    /// A submitted transaction did not become pending before the polling budget elapsed.
339    #[error("timed out waiting for `{tx_hash}` to become pending (last status: {last_status:?})")]
340    PendingTimeout {
341        /// Transaction hash that never appeared as pending.
342        tx_hash: String,
343        /// Last status observed before timeout.
344        last_status: Box<TransactionStatus>,
345    },
346
347    /// A submitted transaction reached a terminal or unexpected state before becoming pending.
348    #[error("transaction `{tx_hash}` did not become pending: {status:?}")]
349    TransactionNotPending {
350        /// Transaction hash whose status was observed.
351        tx_hash: String,
352        /// Status observed instead of `Pending`.
353        status: Box<TransactionStatus>,
354    },
355
356    /// Manual block production is unavailable for this target or driver mode.
357    #[error("manual block production requires a harness-owned local node in manual mode")]
358    ManualBlockProductionUnavailable,
359
360    /// The local supervisor failed to produce a manually-requested block.
361    #[error("manual block production failed: {source}")]
362    BlockProduction {
363        /// Underlying supervisor block-production error.
364        #[source]
365        source: BlockProductionError,
366    },
367
368    /// A contract artifact could not be read from disk.
369    #[error("failed to load artifact `{name}` from `{path}`: {detail}")]
370    ArtifactLoad {
371        /// Registry name of the artifact.
372        name: String,
373        /// Path that failed to load.
374        path: String,
375        /// Underlying error description.
376        detail: String,
377    },
378
379    /// A file artifact had an extension the harness does not recognize.
380    #[error("cannot infer artifact kind from `{path}` (expected .rv32a)")]
381    UnknownArtifactExtension {
382        /// The path whose extension was unrecognized.
383        path: String,
384    },
385
386    /// Two artifacts were registered under the same name.
387    #[error("artifact `{name}` is registered more than once")]
388    DuplicateArtifact {
389        /// The duplicated artifact name.
390        name: String,
391    },
392
393    /// An agent referenced an artifact name that was never registered.
394    #[error("no artifact named `{name}` was registered with the scenario")]
395    UnknownArtifact {
396        /// The unrecognized artifact name.
397        name: String,
398    },
399
400    /// An artifact registration transaction is still awaiting a receipt.
401    #[error("artifact `{name}` registration transaction `{tx_hash:?}` is still pending")]
402    ArtifactRegistrationPending {
403        /// Registry artifact name.
404        name: String,
405        /// Transaction currently registering the artifact.
406        tx_hash: TxHash,
407        /// Original signed bytes for explicit caller-controlled reconciliation.
408        signed_transaction: String,
409    },
410
411    /// The local node supervisor failed to start or stop the target.
412    #[error("local node supervisor error: {detail}")]
413    NodeSupervisor {
414        /// Underlying error description.
415        detail: String,
416    },
417
418    /// A started local node did not advertise a runtime manifest.
419    #[error("local node did not advertise a runtime manifest")]
420    MissingRuntimeManifest,
421
422    /// A message was addressed to an agent that is not part of the scenario.
423    #[error("no agent named `{name}` in the scenario roster")]
424    UnknownAgent {
425        /// The unrecognized recipient name.
426        name: String,
427    },
428
429    /// The recipient agent's mailbox was already closed.
430    #[error("mailbox for agent `{name}` is closed")]
431    MailboxClosed {
432        /// The recipient whose mailbox is closed.
433        name: String,
434    },
435
436    /// An agent task failed to join for a reason other than a panic.
437    #[error("agent task failed to join: {detail}")]
438    AgentJoin {
439        /// Description of the join failure.
440        detail: String,
441    },
442}
443
444impl TestkitError {
445    pub(crate) fn native(source: thruput_client::NativeRpcError) -> Self {
446        Self::NativeEndpoint {
447            source: Box::new(source),
448        }
449    }
450    /// Build an [`TestkitError::Rpc`] from a failing call.
451    pub(crate) fn rpc(method: &'static str, error: impl std::fmt::Display) -> Self {
452        Self::Rpc {
453            method,
454            detail: error.to_string(),
455        }
456    }
457
458    pub(crate) fn signing(error: impl std::fmt::Display) -> Self {
459        Self::Signing {
460            detail: error.to_string(),
461        }
462    }
463
464    pub(crate) fn encode(error: impl std::fmt::Display) -> Self {
465        Self::Encode {
466            detail: error.to_string(),
467        }
468    }
469
470    pub(crate) fn decode(error: impl std::fmt::Display) -> Self {
471        Self::Decode {
472            detail: error.to_string(),
473        }
474    }
475}