From 9b700bfec233adfb47e8fe9c8ece30b56907ef29 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 22 Sep 2025 06:03:59 +0300 Subject: [PATCH] Support repetitions in the tool (#160) --- crates/core/src/driver/mod.rs | 76 +++++++++++++++++++++++++---------- crates/core/src/main.rs | 5 ++- crates/format/src/input.rs | 39 ++++++++++++------ run_tests.sh | 1 - schema.json | 62 ++++++++++++++++++++++++---- 5 files changed, 140 insertions(+), 43 deletions(-) diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index 1088ae1..b9790a3 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -27,8 +27,8 @@ use semver::Version; use revive_dt_format::case::Case; use revive_dt_format::input::{ - BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, StepIdx, - StorageEmptyAssertion, + BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, FunctionCallStep, Method, + StepIdx, StorageEmptyAssertionStep, }; use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent}; use revive_dt_format::{input::Step, metadata::Metadata}; @@ -36,6 +36,7 @@ use revive_dt_node_interaction::EthereumNode; use tokio::try_join; use tracing::{Instrument, info, info_span, instrument}; +#[derive(Clone)] pub struct CaseState { /// A map of all of the compiled contracts for the given metadata file. compiled_contracts: HashMap>, @@ -96,6 +97,17 @@ impl CaseState { .context("Failed to handle storage empty assertion step")?; Ok(StepOutput::StorageEmptyAssertion) } + Step::Repeat(repetition_step) => { + self.handle_repeat( + metadata, + repetition_step.repeat, + &repetition_step.steps, + node, + ) + .await + .context("Failed to handle the repetition step")?; + Ok(StepOutput::Repetition) + } } .inspect(|_| info!("Step Succeeded")) } @@ -104,7 +116,7 @@ impl CaseState { pub async fn handle_input( &mut self, metadata: &Metadata, - input: &Input, + input: &FunctionCallStep, node: &dyn EthereumNode, ) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> { let resolver = node.resolver().await?; @@ -140,7 +152,7 @@ impl CaseState { pub async fn handle_balance_assertion( &mut self, metadata: &Metadata, - balance_assertion: &BalanceAssertion, + balance_assertion: &BalanceAssertionStep, node: &dyn EthereumNode, ) -> anyhow::Result<()> { self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node) @@ -156,7 +168,7 @@ impl CaseState { pub async fn handle_storage_empty( &mut self, metadata: &Metadata, - storage_empty: &StorageEmptyAssertion, + storage_empty: &StorageEmptyAssertionStep, node: &dyn EthereumNode, ) -> anyhow::Result<()> { self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node) @@ -168,12 +180,33 @@ impl CaseState { Ok(()) } + #[instrument(level = "info", name = "Handling Repetition", skip_all)] + pub async fn handle_repeat( + &mut self, + metadata: &Metadata, + repetitions: usize, + steps: &[Step], + node: &dyn EthereumNode, + ) -> anyhow::Result<()> { + let tasks = (0..repetitions).map(|_| { + let mut state = self.clone(); + async move { + for step in steps { + state.handle_step(metadata, step, node).await?; + } + Ok::<(), anyhow::Error>(()) + } + }); + try_join_all(tasks).await?; + Ok(()) + } + /// Handles the contract deployment for a given input performing it if it needs to be performed. #[instrument(level = "info", skip_all)] async fn handle_input_contract_deployment( &mut self, metadata: &Metadata, - input: &Input, + input: &FunctionCallStep, node: &dyn EthereumNode, ) -> anyhow::Result> { let mut instances_we_must_deploy = IndexMap::::new(); @@ -217,7 +250,7 @@ impl CaseState { #[instrument(level = "info", skip_all)] async fn handle_input_execution( &mut self, - input: &Input, + input: &FunctionCallStep, mut deployment_receipts: HashMap, node: &dyn EthereumNode, ) -> anyhow::Result { @@ -281,7 +314,7 @@ impl CaseState { #[instrument(level = "info", skip_all)] fn handle_input_variable_assignment( &mut self, - input: &Input, + input: &FunctionCallStep, tracing_result: &CallFrame, ) -> anyhow::Result<()> { let Some(ref assignments) = input.variable_assignments else { @@ -312,26 +345,26 @@ impl CaseState { #[instrument(level = "info", skip_all)] async fn handle_input_expectations( &self, - input: &Input, + input: &FunctionCallStep, execution_receipt: &TransactionReceipt, resolver: &(impl ResolverApi + ?Sized), tracing_result: &CallFrame, ) -> anyhow::Result<()> { // Resolving the `input.expected` into a series of expectations that we can then assert on. let mut expectations = match input { - Input { + FunctionCallStep { expected: Some(Expected::Calldata(calldata)), .. } => vec![ExpectedOutput::new().with_calldata(calldata.clone())], - Input { + FunctionCallStep { expected: Some(Expected::Expected(expected)), .. } => vec![expected.clone()], - Input { + FunctionCallStep { expected: Some(Expected::ExpectedMany(expected)), .. } => expected.clone(), - Input { expected: None, .. } => vec![ExpectedOutput::new().with_success()], + FunctionCallStep { expected: None, .. } => vec![ExpectedOutput::new().with_success()], }; // This is a bit of a special case and we have to support it separately on it's own. If it's @@ -532,7 +565,7 @@ impl CaseState { pub async fn handle_balance_assertion_contract_deployment( &mut self, metadata: &Metadata, - balance_assertion: &BalanceAssertion, + balance_assertion: &BalanceAssertionStep, node: &dyn EthereumNode, ) -> anyhow::Result<()> { let Some(instance) = balance_assertion @@ -545,7 +578,7 @@ impl CaseState { self.get_or_deploy_contract_instance( &instance, metadata, - Input::default_caller(), + FunctionCallStep::default_caller(), None, None, node, @@ -557,11 +590,11 @@ impl CaseState { #[instrument(level = "info", skip_all)] pub async fn handle_balance_assertion_execution( &mut self, - BalanceAssertion { + BalanceAssertionStep { address: address_string, expected_balance: amount, .. - }: &BalanceAssertion, + }: &BalanceAssertionStep, node: &dyn EthereumNode, ) -> anyhow::Result<()> { let resolver = node.resolver().await?; @@ -595,7 +628,7 @@ impl CaseState { pub async fn handle_storage_empty_assertion_contract_deployment( &mut self, metadata: &Metadata, - storage_empty_assertion: &StorageEmptyAssertion, + storage_empty_assertion: &StorageEmptyAssertionStep, node: &dyn EthereumNode, ) -> anyhow::Result<()> { let Some(instance) = storage_empty_assertion @@ -608,7 +641,7 @@ impl CaseState { self.get_or_deploy_contract_instance( &instance, metadata, - Input::default_caller(), + FunctionCallStep::default_caller(), None, None, node, @@ -620,11 +653,11 @@ impl CaseState { #[instrument(level = "info", skip_all)] pub async fn handle_storage_empty_assertion_execution( &mut self, - StorageEmptyAssertion { + StorageEmptyAssertionStep { address: address_string, is_storage_empty, .. - }: &StorageEmptyAssertion, + }: &StorageEmptyAssertionStep, node: &dyn EthereumNode, ) -> anyhow::Result<()> { let resolver = node.resolver().await?; @@ -841,4 +874,5 @@ pub enum StepOutput { FunctionCall(TransactionReceipt, GethTrace, DiffMode), BalanceAssertion, StorageEmptyAssertion, + Repetition, } diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index cb2574c..65ee25d 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -39,7 +39,7 @@ use revive_dt_core::{ use revive_dt_format::{ case::{Case, CaseIdx}, corpus::Corpus, - input::{Input, Step}, + input::{FunctionCallStep, Step}, metadata::{ContractPathAndIdent, Metadata, MetadataFile}, mode::ParsedMode, }; @@ -514,9 +514,10 @@ async fn handle_case_driver<'a>( Step::FunctionCall(input) => Some(input.caller), Step::BalanceAssertion(..) => None, Step::StorageEmptyAssertion(..) => None, + Step::Repeat(..) => None, }) .next() - .unwrap_or(Input::default_caller()); + .unwrap_or(FunctionCallStep::default_caller()); let tx = TransactionBuilder::::with_deploy_code( TransactionRequest::default().from(deployer_address), code, diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index bf187e2..63c1bf7 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -28,11 +28,13 @@ use crate::{metadata::ContractInstance, traits::ResolutionContext}; #[serde(untagged)] pub enum Step { /// A function call or an invocation to some function on some smart contract. - FunctionCall(Box), + FunctionCall(Box), /// A step for performing a balance assertion on some account or contract. - BalanceAssertion(Box), + BalanceAssertion(Box), /// A step for asserting that the storage of some contract or account is empty. - StorageEmptyAssertion(Box), + StorageEmptyAssertion(Box), + /// A special step for repeating a bunch of steps a certain number of times. + Repeat(Box), } define_wrapper_type!( @@ -43,9 +45,9 @@ define_wrapper_type!( /// This is an input step which is a transaction description that the framework translates into a /// transaction and executes on the nodes. #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)] -pub struct Input { +pub struct FunctionCallStep { /// The address of the account performing the call and paying the fees for it. - #[serde(default = "Input::default_caller")] + #[serde(default = "FunctionCallStep::default_caller")] #[schemars(with = "String")] pub caller: Address, @@ -54,7 +56,7 @@ pub struct Input { pub comment: Option, /// The contract instance that's being called in this transaction step. - #[serde(default = "Input::default_instance")] + #[serde(default = "FunctionCallStep::default_instance")] pub instance: ContractInstance, /// The method that's being called in this step. @@ -85,7 +87,7 @@ pub struct Input { /// This represents a balance assertion step where the framework needs to query the balance of some /// account or contract and assert that it's some amount. #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)] -pub struct BalanceAssertion { +pub struct BalanceAssertionStep { /// An optional comment on the balance assertion. #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, @@ -104,8 +106,10 @@ pub struct BalanceAssertion { pub expected_balance: U256, } +/// This represents an assertion for the storage of some contract or account and whether it's empty +/// or not. #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)] -pub struct StorageEmptyAssertion { +pub struct StorageEmptyAssertionStep { /// An optional comment on the storage empty assertion. #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, @@ -122,6 +126,17 @@ pub struct StorageEmptyAssertion { pub is_storage_empty: bool, } +/// This represents a repetition step which is a special step type that allows for a sequence of +/// steps to be repeated (on different drivers) a certain number of times. +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)] +pub struct RepeatStep { + /// The number of repetitions that the steps should be repeated for. + pub repeat: usize, + + /// The sequence of steps to repeat for the above defined number of repetitions. + pub steps: Vec, +} + /// A set of expectations and assertions to make about the transaction after it ran. /// /// If this is not specified then the only assertion that will be ran is that the transaction @@ -295,7 +310,7 @@ pub struct VariableAssignments { pub return_data: Vec, } -impl Input { +impl FunctionCallStep { pub const fn default_caller() -> Address { Address(FixedBytes(alloy::hex!( "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" @@ -890,7 +905,7 @@ mod tests { .selector() .0; - let input = Input { + let input = FunctionCallStep { instance: ContractInstance::new("Contract"), method: Method::FunctionName("store".to_owned()), calldata: Calldata::new_compound(["42"]), @@ -934,7 +949,7 @@ mod tests { .selector() .0; - let input: Input = Input { + let input: FunctionCallStep = FunctionCallStep { instance: "Contract".to_owned().into(), method: Method::FunctionName("send(address)".to_owned()), calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]), @@ -981,7 +996,7 @@ mod tests { .selector() .0; - let input: Input = Input { + let input: FunctionCallStep = FunctionCallStep { instance: ContractInstance::new("Contract"), method: Method::FunctionName("send".to_owned()), calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]), diff --git a/run_tests.sh b/run_tests.sh index 32791a8..9250f1a 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -95,7 +95,6 @@ RUST_LOG="info" cargo run --release -- execute-tests \ --corpus "$CORPUS_FILE" \ --working-directory "$WORKDIR" \ --concurrency.number-of-nodes 5 \ - --concurrency.ignore-concurrency-limit \ --kitchensink.path "$SUBSTRATE_NODE_BIN" \ --revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \ --eth-rpc.path "$ETH_RPC_BIN" \ diff --git a/schema.json b/schema.json index 03ff4cc..fb83bec 100644 --- a/schema.json +++ b/schema.json @@ -25,7 +25,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/$defs/VmIdentifier" } }, "cases": { @@ -95,6 +95,26 @@ "cases" ], "$defs": { + "VmIdentifier": { + "description": "An enum representing the identifiers of the supported VMs.", + "oneOf": [ + { + "description": "The ethereum virtual machine.", + "type": "string", + "const": "evm" + }, + { + "description": "The EraVM virtual machine.", + "type": "string", + "const": "eravm" + }, + { + "description": "Polkadot's PolaVM Risc-v based virtual machine.", + "type": "string", + "const": "polkavm" + } + ] + }, "Case": { "type": "object", "properties": { @@ -168,19 +188,23 @@ "anyOf": [ { "description": "A function call or an invocation to some function on some smart contract.", - "$ref": "#/$defs/Input" + "$ref": "#/$defs/FunctionCallStep" }, { "description": "A step for performing a balance assertion on some account or contract.", - "$ref": "#/$defs/BalanceAssertion" + "$ref": "#/$defs/BalanceAssertionStep" }, { "description": "A step for asserting that the storage of some contract or account is empty.", - "$ref": "#/$defs/StorageEmptyAssertion" + "$ref": "#/$defs/StorageEmptyAssertionStep" + }, + { + "description": "A special step for repeating a bunch of steps a certain number of times.", + "$ref": "#/$defs/RepeatStep" } ] }, - "Input": { + "FunctionCallStep": { "description": "This is an input step which is a transaction description that the framework translates into a\ntransaction and executes on the nodes.", "type": "object", "properties": { @@ -394,7 +418,7 @@ "return_data" ] }, - "BalanceAssertion": { + "BalanceAssertionStep": { "description": "This represents a balance assertion step where the framework needs to query the balance of some\naccount or contract and assert that it's some amount.", "type": "object", "properties": { @@ -419,7 +443,8 @@ "expected_balance" ] }, - "StorageEmptyAssertion": { + "StorageEmptyAssertionStep": { + "description": "This represents an assertion for the storage of some contract or account and whether it's empty\nor not.", "type": "object", "properties": { "comment": { @@ -443,6 +468,29 @@ "is_storage_empty" ] }, + "RepeatStep": { + "description": "This represents a repetition step which is a special step type that allows for a sequence of\nsteps to be repeated (on different drivers) a certain number of times.", + "type": "object", + "properties": { + "repeat": { + "description": "The number of repetitions that the steps should be repeated for.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "steps": { + "description": "The sequence of steps to repeat for the above defined number of repetitions.", + "type": "array", + "items": { + "$ref": "#/$defs/Step" + } + } + }, + "required": [ + "repeat", + "steps" + ] + }, "ContractPathAndIdent": { "description": "Represents an identifier used for contracts.\n\nThe type supports serialization from and into the following string format:\n\n```text\n${path}:${contract_ident}\n```", "type": "string"