diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index b463c1555ef9d..e3c8068b183cb 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -14,6 +14,24 @@ const INLINE_CONFIG_PREFIX: &str = "forge-config:"; type DataMap = Map; +/// A compiler-agnostic inline configuration entry. +/// +/// This type mirrors `foundry_compilers::InlineConfigEntry` and serves as the +/// bridge between compiler-provided config overrides and Foundry's internal +/// NatSpec-based `InlineConfig`. +#[derive(Clone, Debug)] +pub struct InlineConfigEntry { + /// The contract identifier, in the form `path:ContractName`. + pub contract: String, + /// The function name, if this is a function-level override. + pub function: Option, + /// The location in source for error reporting, e.g. `"10:5"`. + pub line: String, + /// Raw configuration lines in the same key format as `foundry.toml`, + /// e.g. `"default.fuzz.runs = 1024"`. + pub config_values: Vec, +} + /// Errors returned when parsing inline config. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum InlineConfigErrorKind { @@ -68,6 +86,45 @@ impl InlineConfig { Ok(inline) } + /// Creates a new [`InlineConfig`] from pre-parsed [`NatSpec`] entries. + /// + /// This allows alternative compilers to provide inline config without going + /// through solc/solar AST parsing. + pub fn from_natspecs(natspecs: &[NatSpec], profiles: &[Profile]) -> eyre::Result { + let mut inline = Self::new(); + for natspec in natspecs { + inline.insert(natspec)?; + natspec.validate_profiles(profiles)?; + } + Ok(inline) + } + + /// Creates a new [`InlineConfig`] from [`InlineConfigEntry`] items. + /// + /// This bridges the compiler-agnostic [`InlineConfigEntry`] type to + /// Foundry's internal [`NatSpec`]-based inline config, enabling non-Solidity + /// compilers to provide per-test configuration overrides. + pub fn from_entries( + entries: impl IntoIterator, + profiles: &[Profile], + ) -> eyre::Result { + let natspecs: Vec = entries + .into_iter() + .map(|entry| NatSpec { + contract: entry.contract, + function: entry.function, + line: entry.line, + docs: entry + .config_values + .iter() + .map(|v| format!("{INLINE_CONFIG_PREFIX} {v}")) + .collect::>() + .join("\n"), + }) + .collect(); + Self::from_natspecs(&natspecs, profiles) + } + /// Inserts a new [`NatSpec`] into the [`InlineConfig`]. pub fn insert(&mut self, natspec: &NatSpec) -> Result<(), InlineConfigError> { let map = if let Some(function) = &natspec.function { @@ -184,3 +241,50 @@ fn extend_value(value: &mut Value, new: &Value) { (value, new) => *value = new.clone(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_entries_empty() { + let config = InlineConfig::from_entries(vec![], &[Profile::Default]).unwrap(); + assert!(!config.contains_contract("Foo")); + } + + #[test] + fn test_from_entries_function_level() { + let entry = InlineConfigEntry { + contract: "src/Test.sol:TestContract".to_string(), + function: Some("testFoo".to_string()), + line: "10:5".to_string(), + config_values: vec!["default.fuzz.runs = 512".to_string()], + }; + let config = InlineConfig::from_entries(vec![entry], &[Profile::Default]).unwrap(); + assert!(config.contains_function("src/Test.sol:TestContract", "testFoo")); + } + + #[test] + fn test_from_entries_contract_level() { + let entry = InlineConfigEntry { + contract: "src/Test.sol:TestContract".to_string(), + function: None, + line: "5:1".to_string(), + config_values: vec!["default.fuzz.runs = 256".to_string()], + }; + let config = InlineConfig::from_entries(vec![entry], &[Profile::Default]).unwrap(); + assert!(config.contains_contract("src/Test.sol:TestContract")); + } + + #[test] + fn test_from_entries_invalid_profile() { + let entry = InlineConfigEntry { + contract: "src/Test.sol:TestContract".to_string(), + function: Some("testBar".to_string()), + line: "10:5".to_string(), + config_values: vec!["nonexistent.fuzz.runs = 100".to_string()], + }; + let result = InlineConfig::from_entries(vec![entry], &[Profile::Default]); + assert!(result.is_err(), "Expected error for invalid profile"); + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 8bb8de586332a..fb2c140b806f2 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -111,7 +111,7 @@ mod invariant; pub use invariant::InvariantConfig; mod inline; -pub use inline::{InlineConfig, InlineConfigError, NatSpec}; +pub use inline::{InlineConfig, InlineConfigEntry, InlineConfigError, NatSpec}; pub mod soldeer; use soldeer::{SoldeerConfig, SoldeerDependencyConfig}; diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 44d71fb6deee3..fb1326845779a 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -28,7 +28,7 @@ pub use error::FuzzError; pub mod invariant; pub mod strategies; -pub use strategies::LiteralMaps; +pub use strategies::{FuzzLiteral, LiteralMaps}; mod inspector; pub use inspector::Fuzzer; diff --git a/crates/evm/fuzz/src/strategies/literals.rs b/crates/evm/fuzz/src/strategies/literals.rs index 6d767a8309e7b..2fff86022cddc 100644 --- a/crates/evm/fuzz/src/strategies/literals.rs +++ b/crates/evm/fuzz/src/strategies/literals.rs @@ -1,6 +1,6 @@ use alloy_dyn_abi::DynSolType; use alloy_primitives::{ - B256, Bytes, I256, U256, keccak256, + Address, B256, Bytes, I256, U256, keccak256, map::{B256IndexSet, HashMap, IndexSet}, }; use foundry_common::Analysis; @@ -14,6 +14,26 @@ use std::{ sync::{Arc, OnceLock}, }; +/// A compiler-agnostic literal value for fuzzer dictionary seeding. +/// +/// This type mirrors `foundry_compilers::FuzzLiteral` and serves as the bridge +/// between compiler-provided literals and Foundry's internal `LiteralMaps`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FuzzLiteral { + /// An Ethereum address literal. + Address(Address), + /// An unsigned integer literal. + Uint(U256), + /// A signed integer literal. + Int(I256), + /// A fixed-size byte array literal with explicit width. + FixedBytes { value: Bytes, size: u8 }, + /// A dynamic byte array literal. + DynBytes(Bytes), + /// A string literal. + String(String), +} + #[derive(Clone, Debug)] pub struct LiteralsDictionary { maps: Arc>, @@ -57,6 +77,121 @@ impl LiteralsDictionary { Self { maps } } + /// Creates a new `LiteralsDictionary` from pre-collected literal maps. + /// + /// This allows alternative compilers to provide fuzz dictionary literals + /// without going through solar's AST. + pub fn from_maps(maps: LiteralMaps) -> Self { + let lock = Arc::new(OnceLock::new()); + lock.set(maps).unwrap(); + Self { maps: lock } + } + + /// Creates a new `LiteralsDictionary` from compiler-provided fuzz literals. + /// + /// This bridges the compiler-agnostic `FuzzLiteral` type to Foundry's + /// internal `LiteralMaps`, enabling non-Solidity compilers to seed the + /// fuzz dictionary. Respects `max_values` to cap the dictionary size. + pub fn from_fuzz_literals( + literals: impl IntoIterator, + max_values: usize, + ) -> Self { + let mut maps = LiteralMaps::default(); + let mut total = 0usize; + + for lit in literals { + if total >= max_values { + break; + } + match lit { + FuzzLiteral::Address(addr) => { + if maps + .words + .entry(DynSolType::Address) + .or_default() + .insert(addr.into_word()) + { + total += 1; + } + } + FuzzLiteral::Uint(val) => { + let b = B256::from(val); + for bits in [8, 16, 32, 64, 128, 256] { + if can_fit_uint(val, bits) + && maps + .words + .entry(DynSolType::Uint(bits)) + .or_default() + .insert(b) + { + total += 1; + } + } + } + FuzzLiteral::Int(val) => { + let b = B256::from(val.into_raw()); + for bits in [16, 32, 64, 128, 256] { + if can_fit_int(val, bits) + && maps + .words + .entry(DynSolType::Int(bits)) + .or_default() + .insert(b) + { + total += 1; + } + } + } + FuzzLiteral::FixedBytes { value, size } => { + if (1..=32).contains(&size) && value.len() == size as usize { + let padded = B256::right_padding_from(&value); + if maps + .words + .entry(DynSolType::FixedBytes(size as usize)) + .or_default() + .insert(padded) + { + total += 1; + } + } + } + FuzzLiteral::DynBytes(val) => { + if maps.bytes.insert(val) { + total += 1; + } + } + FuzzLiteral::String(s) => { + // For strings, also store keccak hash and right-padded version + let hash = keccak256(s.as_bytes()); + if maps + .words + .entry(DynSolType::FixedBytes(32)) + .or_default() + .insert(hash) + { + total += 1; + } + if s.len() <= 32 { + let padded = B256::right_padding_from(s.as_bytes()); + if maps + .words + .entry(DynSolType::FixedBytes(32)) + .or_default() + .insert(padded) + { + total += 1; + } + } + if maps.strings.insert(s) { + total += 1; + } + } + } + } + + Self::from_maps(maps) + } + /// Returns a reference to the `LiteralMaps`. pub fn get(&self) -> &LiteralMaps { self.maps.wait() @@ -354,4 +489,120 @@ mod tests { fn assert_word(literals: &LiteralMaps, ty: DynSolType, value: B256, msg: &str) { assert!(literals.words.get(&ty).is_some_and(|set| set.contains(&value)), "{}", msg); } + + #[test] + fn test_from_fuzz_literals_address() { + let addr = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F"); + let dict = LiteralsDictionary::from_fuzz_literals( + vec![FuzzLiteral::Address(addr)], + usize::MAX, + ); + let maps = dict.get(); + assert!( + maps.words + .get(&DynSolType::Address) + .is_some_and(|set| set.contains(&addr.into_word())), + "Expected address in dictionary" + ); + } + + #[test] + fn test_from_fuzz_literals_uint() { + let val = U256::from(42u64); + let dict = LiteralsDictionary::from_fuzz_literals( + vec![FuzzLiteral::Uint(val)], + usize::MAX, + ); + let maps = dict.get(); + let b = B256::from(val); + // Should appear in all uint sizes that can fit 42 + for bits in [8, 16, 32, 64, 128, 256] { + assert!( + maps.words + .get(&DynSolType::Uint(bits)) + .is_some_and(|set| set.contains(&b)), + "Expected uint in Uint({bits}) set" + ); + } + } + + #[test] + fn test_from_fuzz_literals_int() { + let val = I256::try_from(-777i32).unwrap(); + let dict = LiteralsDictionary::from_fuzz_literals( + vec![FuzzLiteral::Int(val)], + usize::MAX, + ); + let maps = dict.get(); + let b = B256::from(val.into_raw()); + for bits in [16, 32, 64, 128, 256] { + assert!( + maps.words + .get(&DynSolType::Int(bits)) + .is_some_and(|set| set.contains(&b)), + "Expected int in Int({bits}) set" + ); + } + } + + #[test] + fn test_from_fuzz_literals_string() { + let dict = LiteralsDictionary::from_fuzz_literals( + vec![FuzzLiteral::String("hello".to_string())], + usize::MAX, + ); + let maps = dict.get(); + assert!(maps.strings.contains("hello"), "Expected string in dictionary"); + // Also check keccak hash was stored + let hash = keccak256(b"hello"); + assert!( + maps.words + .get(&DynSolType::FixedBytes(32)) + .is_some_and(|set| set.contains(&hash)), + "Expected keccak hash in FixedBytes(32) set" + ); + } + + #[test] + fn test_from_fuzz_literals_max_cap() { + // Create many literals but cap at 3 + let literals: Vec = (0..100u64) + .map(|i| FuzzLiteral::Address(Address::with_last_byte(i as u8))) + .collect(); + let dict = LiteralsDictionary::from_fuzz_literals(literals, 3); + let maps = dict.get(); + let addr_count = maps + .words + .get(&DynSolType::Address) + .map_or(0, |set| set.len()); + assert_eq!(addr_count, 3, "Expected exactly 3 addresses due to max cap"); + } + + #[test] + fn test_from_fuzz_literals_fixed_bytes() { + let value = Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]); + let dict = LiteralsDictionary::from_fuzz_literals( + vec![FuzzLiteral::FixedBytes { value: value.clone(), size: 4 }], + usize::MAX, + ); + let maps = dict.get(); + let padded = B256::right_padding_from(&value); + assert!( + maps.words + .get(&DynSolType::FixedBytes(4)) + .is_some_and(|set| set.contains(&padded)), + "Expected fixed bytes in FixedBytes(4) set" + ); + } + + #[test] + fn test_from_fuzz_literals_dyn_bytes() { + let value = Bytes::from_static(&[0xca, 0xfe]); + let dict = LiteralsDictionary::from_fuzz_literals( + vec![FuzzLiteral::DynBytes(value.clone())], + usize::MAX, + ); + let maps = dict.get(); + assert!(maps.bytes.contains(&value), "Expected dynamic bytes in dictionary"); + } } diff --git a/crates/evm/fuzz/src/strategies/mod.rs b/crates/evm/fuzz/src/strategies/mod.rs index ceed0df29f701..06b48b7797caa 100644 --- a/crates/evm/fuzz/src/strategies/mod.rs +++ b/crates/evm/fuzz/src/strategies/mod.rs @@ -23,4 +23,4 @@ mod mutators; pub use mutators::BoundMutator; mod literals; -pub use literals::{LiteralMaps, LiteralsCollector, LiteralsDictionary}; +pub use literals::{FuzzLiteral, LiteralMaps, LiteralsCollector, LiteralsDictionary}; diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index d898032a78fd9..4651c231c76b0 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -22,7 +22,7 @@ pub mod coverage; pub mod gas_report; pub mod multi_runner; -pub use multi_runner::{MultiContractRunner, MultiContractRunnerBuilder}; +pub use multi_runner::{MultiContractRunner, MultiContractRunnerBuilder, PreLinkedArtifacts}; mod runner; pub use runner::ContractRunner; diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index fd9b4b889c2e2..e8f773f7b0d93 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -47,6 +47,27 @@ pub struct TestContract { pub type DeployableContracts = BTreeMap; +/// Pre-linked artifacts ready for the test runner. +/// +/// This is the compiler-agnostic input to [`MultiContractRunnerBuilder::build_from_artifacts`]. +/// A compiler produces these artifacts after compilation and linking, enabling non-Solidity +/// compilers to use Foundry's test runner without going through `ProjectCompileOutput`. +#[derive(Debug)] +pub struct PreLinkedArtifacts { + /// Contracts that have test functions and can be deployed. + pub deployable_contracts: DeployableContracts, + /// All compiled contracts for trace decoding and cheatcode support. + pub known_contracts: ContractsByArtifact, + /// Libraries that need deploying, in topological order. + pub libs_to_deploy: Vec, + /// Library addresses used to link contracts. + pub libraries: Libraries, + /// Fuzz dictionary literals extracted from source. + pub fuzz_literals: LiteralsDictionary, + /// Per-test configuration overrides. + pub inline_config: InlineConfig, +} + /// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds /// to run all test functions in these contracts. #[derive(Clone, Debug)] @@ -63,7 +84,7 @@ pub struct MultiContractRunner { /// Library addresses used to link contracts. pub libraries: Libraries, /// Solar compiler instance, to grant syntactic and semantic analysis capabilities - pub analysis: Arc, + pub analysis: Option>, /// Literals dictionary for fuzzing. pub fuzz_literals: LiteralsDictionary, @@ -363,7 +384,7 @@ impl TestRunnerConfig { pub fn executor( &self, known_contracts: ContractsByArtifact, - analysis: Arc, + analysis: Option>, artifact_id: &ArtifactId, db: Backend, ) -> Executor { @@ -376,15 +397,19 @@ impl TestRunnerConfig { )); ExecutorBuilder::default() .inspectors(|stack| { - stack + let stack = stack .logs(self.config.live_logs) .cheatcodes(cheats_config) .trace_mode(self.trace_mode()) .line_coverage(self.line_coverage) .enable_isolation(self.isolation) .networks(self.evm_opts.networks) - .create2_deployer(self.evm_opts.create2_deployer) - .set_analysis(analysis) + .create2_deployer(self.evm_opts.create2_deployer); + if let Some(analysis) = analysis { + stack.set_analysis(analysis) + } else { + stack + } }) .spec_id(self.spec_id) .gas_limit(self.evm_opts.gas_limit()) @@ -579,7 +604,7 @@ impl MultiContractRunnerBuilder { known_contracts, libs_to_deploy, libraries, - analysis, + analysis: Some(analysis), fuzz_literals, tcfg: TestRunnerConfig { @@ -600,6 +625,49 @@ impl MultiContractRunnerBuilder { fork: self.fork, }) } + + /// Builds a test runner from pre-linked artifacts. + /// + /// This is the compiler-agnostic entry point. Unlike [`build`](Self::build), this method + /// does not require a `ProjectCompileOutput` and accepts pre-linked artifacts directly, + /// enabling non-Solidity compilers to use Foundry's test runner. + pub fn build_from_artifacts( + self, + artifacts: PreLinkedArtifacts, + evm_env: EvmEnvFor, + tx_env: TxEnvFor, + evm_opts: EvmOpts, + ) -> Result> { + let revert_decoder = + RevertDecoder::new().with_abis(artifacts.known_contracts.values().map(|c| &c.abi)); + + Ok(MultiContractRunner { + contracts: artifacts.deployable_contracts, + revert_decoder, + known_contracts: artifacts.known_contracts, + libs_to_deploy: artifacts.libs_to_deploy, + libraries: artifacts.libraries, + analysis: None, + fuzz_literals: artifacts.fuzz_literals, + + tcfg: TestRunnerConfig { + evm_opts, + evm_env, + tx_env, + spec_id: self.config.evm_spec_id(), + sender: self.sender.unwrap_or(self.config.sender), + line_coverage: self.line_coverage, + debug: self.debug, + decode_internal: self.decode_internal, + inline_config: Arc::new(artifacts.inline_config), + isolation: self.isolation, + early_exit: EarlyExit::new(self.fail_fast), + config: self.config, + }, + + fork: self.fork, + }) + } } pub fn matches_artifact(filter: &dyn TestFilter, id: &ArtifactId, abi: &JsonAbi) -> bool { @@ -615,3 +683,140 @@ pub(crate) fn matches_contract( (filter.matches_path(path) && filter.matches_contract(contract_name)) && functions.into_iter().any(|func| filter.matches_test_function(func.borrow())) } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_json_abi::StateMutability; + use alloy_primitives::hex; + use foundry_common::EmptyTestFilter; + use foundry_evm::core::evm::EthEvmNetwork; + use revm::context::{BlockEnv, TxEnv}; + use semver::Version; + use std::path::PathBuf; + + fn test_artifact_id(name: &str) -> ArtifactId { + ArtifactId { + path: PathBuf::from(format!("out/{name}.json")), + name: name.to_string(), + source: PathBuf::from(format!("src/{name}.raw")), + version: Version::new(0, 0, 1), + build_id: "test".to_string(), + profile: "default".to_string(), + } + } + + fn add_fn(abi: &mut JsonAbi, name: &str) { + abi.functions.entry(name.to_string()).or_default().push(Function { + name: name.to_string(), + inputs: vec![], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + }); + } + + /// Builds a `MultiContractRunner` from raw artifacts via `build_from_artifacts`. + async fn build_runner( + deployable: DeployableContracts, + ) -> MultiContractRunner { + let config = Arc::new(Config::default()); + let evm_opts: EvmOpts = Config::figment().extract().unwrap(); + let (evm_env, tx_env, _) = evm_opts.env::<_, BlockEnv, TxEnv>().await.unwrap(); + + let artifacts = PreLinkedArtifacts { + deployable_contracts: deployable, + known_contracts: ContractsByArtifact::default(), + libs_to_deploy: vec![], + libraries: Default::default(), + fuzz_literals: LiteralsDictionary::default(), + inline_config: InlineConfig::default(), + }; + + MultiContractRunnerBuilder::new(config) + .build_from_artifacts(artifacts, evm_env, tx_env, evm_opts) + .expect("build_from_artifacts should succeed") + } + + /// Contract with setUp + passing/failing/storage tests. + /// + /// Runtime dispatches on function selectors: + /// setUp() (0x0a9254e4) → stores 0x42 at slot 0 + /// testAlwaysPass() (0xacded0fd) → STOP (pass) + /// testAlwaysFail() (0xb0c7a037) → REVERT (fail) + /// testStorageWasSet() (0xbff50641) → loads slot 0, checks == 0x42, reverts if not + fn multi_function_contract() -> (ArtifactId, TestContract) { + // Built by a Python assembler — 12-byte constructor + 48-byte runtime + // See the runtime layout in the PR description for full disassembly. + let bytecode = hex!( + // Constructor: copies runtime and returns it + "6048600c60003960486000f3" + // Runtime: selector dispatch + "5f3560e01c" // PUSH0 CALLDATALOAD PUSH1_0xe0 SHR + "80630a9254e414602e57" // setUp → jump 0x2e + "8063acded0fd14603457" // testAlwaysPass → jump 0x34 + "8063b0c7a03714603657" // testAlwaysFail → jump 0x36 + "8063bff5064114603a57" // testStorageWasSet → jump 0x3a + "00" // default: STOP + // 0x2e: setUp + "5b60425f5500" // JUMPDEST PUSH1_0x42 PUSH0 SSTORE STOP + // 0x34: testAlwaysPass + "5b00" // JUMPDEST STOP + // 0x36: testAlwaysFail + "5b5f5ffd" // JUMPDEST PUSH0 PUSH0 REVERT + // 0x3a: testStorageWasSet + "5b5f546042146046575f5ffd5b00" // JUMPDEST PUSH0 SLOAD PUSH1_0x42 EQ + // PUSH1_0x46 JUMPI PUSH0 PUSH0 REVERT + // JUMPDEST STOP + ); + + let mut abi = JsonAbi::new(); + add_fn(&mut abi, "setUp"); + add_fn(&mut abi, "testAlwaysPass"); + add_fn(&mut abi, "testAlwaysFail"); + add_fn(&mut abi, "testStorageWasSet"); + + (test_artifact_id("MultiTest"), TestContract { abi, bytecode: bytecode.into() }) + } + + /// Full E2E: multi-function contract with setUp, passing tests, failing tests, + /// and storage verification — all from raw EVM bytecode, no Solidity. + #[tokio::test(flavor = "multi_thread")] + async fn test_build_from_artifacts_e2e() { + let (id, contract) = multi_function_contract(); + let mut deployable = DeployableContracts::default(); + deployable.insert(id, contract); + + let mut runner = build_runner(deployable).await; + assert_eq!(runner.contracts.len(), 1); + + let filter = EmptyTestFilter::default(); + let results = runner.test_collect(&filter).expect("test execution should succeed"); + + assert_eq!(results.len(), 1, "expected 1 test suite"); + let (name, suite) = results.iter().next().unwrap(); + + // testAlwaysPass + testStorageWasSet should pass + assert_eq!(suite.passed(), 2, "suite {name}: expected 2 passing tests"); + // testAlwaysFail should fail + assert_eq!(suite.failed(), 1, "suite {name}: expected 1 failing test"); + + // Verify specific test results + for (test_name, result) in suite.test_results.iter() { + match test_name.as_str() { + s if s.contains("testAlwaysPass") => { + assert!(result.status.is_success(), "testAlwaysPass should pass"); + } + s if s.contains("testAlwaysFail") => { + assert!(result.status.is_failure(), "testAlwaysFail should fail"); + } + s if s.contains("testStorageWasSet") => { + assert!( + result.status.is_success(), + "testStorageWasSet should pass (setUp stored 0x42)" + ); + } + _ => {} + } + } + } +}