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}