From c3c7203af858da969d6cea5e8b5e860a346a0040 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 29 Sep 2025 21:10:37 +0300 Subject: [PATCH] Implement the platform driver --- crates/core/src/differential_tests/driver.rs | 1080 ++++++++++++++--- .../src/differential_tests/execution_state.rs | 3 +- crates/format/src/case.rs | 5 +- 3 files changed, 913 insertions(+), 175 deletions(-) diff --git a/crates/core/src/differential_tests/driver.rs b/crates/core/src/differential_tests/driver.rs index 8e842d4..a10ae10 100644 --- a/crates/core/src/differential_tests/driver.rs +++ b/crates/core/src/differential_tests/driver.rs @@ -1,211 +1,951 @@ -use std::{ - collections::{BTreeMap, HashMap}, - sync::{Arc, Mutex}, -}; +use std::{collections::HashMap, sync::Arc}; use alloy::{ + consensus::EMPTY_ROOT_HASH, + hex, + json_abi::JsonAbi, network::{Ethereum, TransactionBuilder}, - rpc::types::TransactionRequest, + primitives::{Address, TxHash, U256}, + rpc::types::{ + TransactionReceipt, TransactionRequest, + trace::geth::{ + CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, + GethDebugTracingOptions, + }, + }, }; -use anyhow::{Context as _, Result}; -use futures::{StreamExt, stream}; -use revive_dt_common::types::{PlatformIdentifier, PrivateKeyAllocator}; +use anyhow::{Context as _, Result, bail}; +use futures::TryStreamExt; +use indexmap::IndexMap; +use revive_dt_common::types::PrivateKeyAllocator; use revive_dt_format::{ - metadata::ContractPathAndIdent, - steps::{FunctionCallStep, Step}, + metadata::{ContractInstance, ContractPathAndIdent}, + steps::{ + AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, + FunctionCallStep, Method, RepeatStep, Step, StepAddress, StepIdx, StepPath, + StorageEmptyAssertionStep, + }, + traits::ResolutionContext, }; -use tracing::{debug, error}; +use tokio::sync::Mutex; +use tracing::{debug, error, info, instrument}; use crate::{ differential_tests::ExecutionState, - helpers::{CachedCompiler, TestDefinition}, + helpers::{CachedCompiler, TestDefinition, TestPlatformInformation}, }; -pub struct DifferentialTestsDriver<'a> { +/// The differential tests driver for a single platform. +pub struct DifferentialTestsPlatformDriver<'a, I> { + /// The information of the platform that this driver is for. + platform_information: &'a TestPlatformInformation<'a>, + /// The definition of the test that the driver is instructed to execute. - test_definition: TestDefinition<'a>, + test_definition: &'a TestDefinition<'a>, /// The private key allocator used by this driver and other drivers when account allocations are /// needed. private_key_allocator: Arc>, - /// The execution state associated with each one of the platforms that the test definition - /// is instructed to run on. - execution_state: BTreeMap, - // TODO(driver-refactor): Explore the idea of keeping an iterator over the steps in here that - // provides both the steps and the step path. + /// The execution state associated with the platform. + execution_state: ExecutionState, + + /// This is the queue of steps that are to be executed by the driver for this test case. Each + /// time `execute_step` is called one of the steps is executed. + steps_iterator: I, } -impl<'a> DifferentialTestsDriver<'a> { - pub async fn new( - test_definition: TestDefinition<'a>, +impl<'a, I> DifferentialTestsPlatformDriver<'a, I> +where + I: Iterator, +{ + // region:Constructors & Initialization + pub async fn new_root( + platform_information: &'a TestPlatformInformation<'a>, + test_definition: &'a TestDefinition<'a>, private_key_allocator: Arc>, cached_compiler: &CachedCompiler<'a>, - ) -> Result { - let mut this = DifferentialTestsDriver { + ) -> Result>> { + let steps_iterator = test_definition + .case + .steps + .clone() + .into_iter() + .enumerate() + .map(|(step_idx, step)| (StepPath::new(vec![StepIdx::new(step_idx)]), step)); + DifferentialTestsPlatformDriver::new( + platform_information, test_definition, private_key_allocator, - execution_state: Default::default(), - }; - this.init_state(cached_compiler) - .await - .context("Failed to initialize the state of the differential tests driver")?; - Ok(this) + cached_compiler, + steps_iterator, + ) + .await } - async fn init_state(&mut self, cached_compiler: &CachedCompiler<'a>) -> Result<()> { - let test_definition = &self.test_definition; - self.execution_state = stream::iter(self.test_definition.platforms.iter()) - // Compiling the pre-link contracts. - .filter_map(|(platform_identifier, platform_information)| async move { - let compiler_output = cached_compiler - .compile_contracts( - test_definition.metadata, - test_definition.metadata_file_path, - test_definition.mode.clone(), - None, - platform_information.compiler.as_ref(), - platform_information.platform, - &platform_information.reporter, - ) - .await - .inspect_err(|err| { - error!( - ?err, - %platform_identifier, - "Pre-linking compilation failed" - ) - }) - .ok()?; - Some((platform_identifier, platform_information, compiler_output)) + pub async fn new( + platform_information: &'a TestPlatformInformation<'a>, + test_definition: &'a TestDefinition<'a>, + private_key_allocator: Arc>, + cached_compiler: &CachedCompiler<'a>, + steps: I, + ) -> Result { + let execution_state = + Self::init_execution_state(platform_information, test_definition, cached_compiler) + .await + .context("Failed to initialize the execution state of the platform")?; + Ok(DifferentialTestsPlatformDriver { + platform_information, + test_definition, + private_key_allocator, + execution_state, + steps_iterator: steps, + }) + } + + async fn init_execution_state( + platform_information: &'a TestPlatformInformation<'a>, + test_definition: &'a TestDefinition<'a>, + cached_compiler: &CachedCompiler<'a>, + ) -> Result { + let compiler_output = cached_compiler + .compile_contracts( + test_definition.metadata, + test_definition.metadata_file_path, + test_definition.mode.clone(), + None, + platform_information.compiler.as_ref(), + platform_information.platform, + &platform_information.reporter, + ) + .await + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %platform_information.platform.platform_identifier(), + "Pre-linking compilation failed" + ) }) - // Deploying the libraries for the platform. - .filter_map( - |(platform_identifier, platform_information, compiler_output)| async move { - let mut deployed_libraries = None::>; - let mut contract_sources = test_definition - .metadata - .contract_sources() - .inspect_err(|err| { - error!( - ?err, - %platform_identifier, - "Failed to retrieve contract sources from metadata" - ) - }) - .ok()?; - for library_instance in test_definition - .metadata - .libraries - .iter() - .flatten() - .flat_map(|(_, map)| map.values()) - { - debug!(%library_instance, "Deploying Library Instance"); + .context("Failed to produce the pre-linking compiled contracts")?; - let ContractPathAndIdent { - contract_source_path: library_source_path, - contract_ident: library_ident, - } = contract_sources.remove(library_instance)?; + let mut deployed_libraries = None::>; + let mut contract_sources = test_definition + .metadata + .contract_sources() + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %platform_information.platform.platform_identifier(), + "Failed to retrieve contract sources from metadata" + ) + }) + .context("Failed to get the contract instances from the metadata file")?; + for library_instance in test_definition + .metadata + .libraries + .iter() + .flatten() + .flat_map(|(_, map)| map.values()) + { + debug!(%library_instance, "Deploying Library Instance"); - let (code, abi) = compiler_output - .contracts - .get(&library_source_path) - .and_then(|contracts| contracts.get(library_ident.as_str()))?; + let ContractPathAndIdent { + contract_source_path: library_source_path, + contract_ident: library_ident, + } = contract_sources + .remove(library_instance) + .context("Failed to get the contract sources of the contract instance")?; - let code = alloy::hex::decode(code).ok()?; + let (code, abi) = compiler_output + .contracts + .get(&library_source_path) + .and_then(|contracts| contracts.get(library_ident.as_str())) + .context("Failed to get the code and abi for the instance")?; - // Getting the deployer address from the cases themselves. This is to ensure - // that we're doing the deployments from different accounts and therefore we're - // not slowed down by the nonce. - let deployer_address = test_definition - .case - .steps - .iter() - .filter_map(|step| match step { - Step::FunctionCall(input) => input.caller.as_address().copied(), - Step::BalanceAssertion(..) => None, - Step::StorageEmptyAssertion(..) => None, - Step::Repeat(..) => None, - Step::AllocateAccount(..) => None, - }) - .next() - .unwrap_or(FunctionCallStep::default_caller_address()); - let tx = TransactionBuilder::::with_deploy_code( - TransactionRequest::default().from(deployer_address), - code, - ); - let receipt = platform_information - .node - .execute_transaction(tx) - .await - .inspect_err(|err| { - error!( - ?err, - %library_instance, - %platform_identifier, - "Failed to deploy the library" - ) - }) - .ok()?; + let code = alloy::hex::decode(code)?; - debug!( - ?library_instance, - %platform_identifier, - "Deployed library" - ); + // Getting the deployer address from the cases themselves. This is to ensure + // that we're doing the deployments from different accounts and therefore we're + // not slowed down by the nonce. + let deployer_address = test_definition + .case + .steps + .iter() + .filter_map(|step| match step { + Step::FunctionCall(input) => input.caller.as_address().copied(), + Step::BalanceAssertion(..) => None, + Step::StorageEmptyAssertion(..) => None, + Step::Repeat(..) => None, + Step::AllocateAccount(..) => None, + }) + .next() + .unwrap_or(FunctionCallStep::default_caller_address()); + let tx = TransactionBuilder::::with_deploy_code( + TransactionRequest::default().from(deployer_address), + code, + ); + let receipt = platform_information + .node + .execute_transaction(tx) + .await + .inspect_err(|err| { + error!( + ?err, + %library_instance, + platform_identifier = %platform_information.platform.platform_identifier(), + "Failed to deploy the library" + ) + })?; - let library_address = receipt.contract_address?; + debug!( + ?library_instance, + platform_identifier = %platform_information.platform.platform_identifier(), + "Deployed library" + ); - deployed_libraries.get_or_insert_default().insert( - library_instance.clone(), - (library_ident.clone(), library_address, abi.clone()), - ); - } + let library_address = receipt + .contract_address + .expect("Failed to deploy the library"); - Some(( - platform_identifier, - platform_information, - compiler_output, - deployed_libraries, - )) - }, + deployed_libraries.get_or_insert_default().insert( + library_instance.clone(), + (library_ident.clone(), library_address, abi.clone()), + ); + } + + let compiler_output = cached_compiler + .compile_contracts( + test_definition.metadata, + test_definition.metadata_file_path, + test_definition.mode.clone(), + deployed_libraries.as_ref(), + platform_information.compiler.as_ref(), + platform_information.platform, + &platform_information.reporter, ) - // Compiling the post-link contracts. - .filter_map( - |(platform_identifier, platform_information, _, deployed_libraries)| async move { - let compiler_output = cached_compiler - .compile_contracts( - test_definition.metadata, - test_definition.metadata_file_path, - test_definition.mode.clone(), - deployed_libraries.as_ref(), - platform_information.compiler.as_ref(), - platform_information.platform, - &platform_information.reporter, - ) - .await - .inspect_err(|err| { - error!( - ?err, - %platform_identifier, - "Pre-linking compilation failed" - ) - }) - .ok()?; + .await + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %platform_information.platform.platform_identifier(), + "Pre-linking compilation failed" + ) + }) + .context("Failed to compile the post-link contracts")?; - let state = ExecutionState::new( - compiler_output.contracts, - deployed_libraries.unwrap_or_default(), - ); + Ok(ExecutionState::new( + compiler_output.contracts, + deployed_libraries.unwrap_or_default(), + )) + } + // endregion:Constructors & Initialization - Some((*platform_identifier, state)) - }, - ) - // Collect - .collect::>() - .await; + // region:Step Handling + #[instrument(level = "info", skip_all)] + pub async fn execute_all(mut self) -> Result<()> { + while let Some(result) = self.execute_next_step().await { + result? + } + Ok(()) + } + #[instrument( + level = "info", + skip_all, + fields( + platform_identifier = %self.platform_information.platform.platform_identifier(), + node_id = self.platform_information.node.id(), + ), + )] + pub async fn execute_next_step(&mut self) -> Option> { + let (step_path, step) = self.steps_iterator.next()?; + Some(self.execute_step(&step_path, &step).await) + } + + #[instrument( + level = "info", + skip_all, + fields( + platform_identifier = %self.platform_information.platform.platform_identifier(), + %step_path, + ), + err(Debug), + )] + async fn execute_step(&mut self, step_path: &StepPath, step: &Step) -> Result<()> { + match step { + Step::FunctionCall(step) => self + .execute_function_call(step_path, step.as_ref()) + .await + .context("Function call step Failed"), + Step::BalanceAssertion(step) => self + .execute_balance_assertion(step_path, step.as_ref()) + .await + .context("Balance Assertion Step Failed"), + Step::StorageEmptyAssertion(step) => self + .execute_storage_empty_assertion_step(step_path, step.as_ref()) + .await + .context("Storage Empty Assertion Step Failed"), + Step::Repeat(step) => self + .execute_repeat_step(step_path, step.as_ref()) + .await + .context("Repetition Step Failed"), + Step::AllocateAccount(step) => self + .execute_account_allocation(step_path, step.as_ref()) + .await + .context("Account Allocation Step Failed"), + } + } + + #[instrument(level = "info", skip_all)] + pub async fn execute_function_call( + &mut self, + _: &StepPath, + step: &FunctionCallStep, + ) -> Result<()> { + let deployment_receipts = self + .handle_function_call_contract_deployment(step) + .await + .context("Failed to deploy contracts for the function call step")?; + let execution_receipt = self + .handle_function_call_execution(step, deployment_receipts) + .await + .context("Failed to handle the function call execution")?; + let tracing_result = self + .handle_function_call_call_frame_tracing(execution_receipt.transaction_hash) + .await + .context("Failed to handle the function call call frame tracing")?; + self.handle_function_call_variable_assignment(step, &tracing_result) + .await + .context("Failed to handle function call variable assignment")?; todo!() } + + async fn handle_function_call_contract_deployment( + &mut self, + step: &FunctionCallStep, + ) -> Result> { + let mut instances_we_must_deploy = IndexMap::::new(); + for instance in step.find_all_contract_instances().into_iter() { + if !self + .execution_state + .deployed_contracts + .contains_key(&instance) + { + instances_we_must_deploy.entry(instance).or_insert(false); + } + } + if let Method::Deployer = step.method { + instances_we_must_deploy.swap_remove(&step.instance); + instances_we_must_deploy.insert(step.instance.clone(), true); + } + + let mut receipts = HashMap::new(); + for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { + let calldata = deploy_with_constructor_arguments.then_some(&step.calldata); + let value = deploy_with_constructor_arguments + .then_some(step.value) + .flatten(); + + let caller = { + let context = self.default_resolution_context(); + let resolver = self.platform_information.node.resolver().await?; + step.caller + .resolve_address(resolver.as_ref(), context) + .await? + }; + if let (_, _, Some(receipt)) = self + .get_or_deploy_contract_instance(&instance, caller, calldata, value) + .await + .context("Failed to get or deploy contract instance during input execution")? + { + receipts.insert(instance.clone(), receipt); + } + } + + Ok(receipts) + } + + async fn handle_function_call_execution( + &mut self, + step: &FunctionCallStep, + mut deployment_receipts: HashMap, + ) -> Result { + match step.method { + // This step was already executed when `handle_step` was called. We just need to + // lookup the transaction receipt in this case and continue on. + Method::Deployer => deployment_receipts + .remove(&step.instance) + .context("Failed to find deployment receipt for constructor call"), + Method::Fallback | Method::FunctionName(_) => { + let resolver = self.platform_information.node.resolver().await?; + let tx = match step + .legacy_transaction(resolver.as_ref(), self.default_resolution_context()) + .await + { + Ok(tx) => tx, + Err(err) => { + return Err(err); + } + }; + + match self.platform_information.node.execute_transaction(tx).await { + Ok(receipt) => Ok(receipt), + Err(err) => Err(err), + } + } + } + } + + async fn handle_function_call_call_frame_tracing( + &mut self, + tx_hash: TxHash, + ) -> Result { + self.platform_information + .node + .trace_transaction( + tx_hash, + GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::BuiltInTracer( + GethDebugBuiltInTracerType::CallTracer, + )), + tracer_config: GethDebugTracerConfig(serde_json::json! {{ + "onlyTopCall": true, + "withLog": false, + "withStorage": false, + "withMemory": false, + "withStack": false, + "withReturnData": true + }}), + ..Default::default() + }, + ) + .await + .map(|trace| { + trace + .try_into_call_frame() + .expect("Impossible - we requested a callframe trace so we must get it back") + }) + } + + async fn handle_function_call_variable_assignment( + &mut self, + step: &FunctionCallStep, + tracing_result: &CallFrame, + ) -> Result<()> { + let Some(ref assignments) = step.variable_assignments else { + return Ok(()); + }; + + // Handling the return data variable assignments. + for (variable_name, output_word) in assignments.return_data.iter().zip( + tracing_result + .output + .as_ref() + .unwrap_or_default() + .to_vec() + .chunks(32), + ) { + let value = U256::from_be_slice(output_word); + self.execution_state + .variables + .insert(variable_name.clone(), value); + tracing::info!( + variable_name, + variable_value = hex::encode(value.to_be_bytes::<32>()), + "Assigned variable" + ); + } + + Ok(()) + } + + async fn handle_function_call_assertions( + &mut self, + step: &FunctionCallStep, + receipt: &TransactionReceipt, + tracing_result: &CallFrame, + ) -> Result<()> { + // Resolving the `step.expected` into a series of expectations that we can then assert on. + let mut expectations = match step { + FunctionCallStep { + expected: Some(Expected::Calldata(calldata)), + .. + } => vec![ExpectedOutput::new().with_calldata(calldata.clone())], + FunctionCallStep { + expected: Some(Expected::Expected(expected)), + .. + } => vec![expected.clone()], + FunctionCallStep { + expected: Some(Expected::ExpectedMany(expected)), + .. + } => expected.clone(), + 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 + // a call to the deployer method, then the tests will assert that it "returns" the address + // of the contract. Deployments do not return the address of the contract but the runtime + // code of the contracts. Therefore, this assertion would always fail. So, we replace it + // with an assertion of "check if it succeeded" + if let Method::Deployer = &step.method { + for expectation in expectations.iter_mut() { + expectation.return_data = None; + } + } + + futures::stream::iter(expectations.into_iter().map(Ok)) + .try_for_each_concurrent(None, |expectation| async { + self.handle_function_call_assertion_item(receipt, tracing_result, expectation) + .await + }) + .await + } + + async fn handle_function_call_assertion_item( + &self, + receipt: &TransactionReceipt, + tracing_result: &CallFrame, + assertion: ExpectedOutput, + ) -> Result<()> { + let resolver = self + .platform_information + .node + .resolver() + .await + .context("Failed to create the resolver for the node")?; + + if let Some(ref version_requirement) = assertion.compiler_version { + if !version_requirement.matches(self.platform_information.compiler.version()) { + return Ok(()); + } + } + + let resolution_context = self + .default_resolution_context() + .with_block_number(receipt.block_number.as_ref()) + .with_transaction_hash(&receipt.transaction_hash); + + // Handling the receipt state assertion. + let expected = !assertion.exception; + let actual = receipt.status(); + if actual != expected { + tracing::error!( + expected, + actual, + ?receipt, + ?tracing_result, + "Transaction status assertion failed" + ); + anyhow::bail!( + "Transaction status assertion failed - Expected {expected} but got {actual}", + ); + } + + // Handling the calldata assertion + if let Some(ref expected_calldata) = assertion.return_data { + let expected = expected_calldata; + let actual = &tracing_result.output.as_ref().unwrap_or_default(); + if !expected + .is_equivalent(actual, resolver.as_ref(), resolution_context) + .await + .context("Failed to resolve calldata equivalence for return data assertion")? + { + tracing::error!( + ?receipt, + ?expected, + %actual, + "Calldata assertion failed" + ); + anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",); + } + } + + // Handling the events assertion + if let Some(ref expected_events) = assertion.events { + // Handling the events length assertion. + let expected = expected_events.len(); + let actual = receipt.logs().len(); + if actual != expected { + tracing::error!(expected, actual, "Event count assertion failed",); + anyhow::bail!( + "Event count assertion failed - Expected {expected} but got {actual}", + ); + } + + // Handling the events assertion. + for (event_idx, (expected_event, actual_event)) in + expected_events.iter().zip(receipt.logs()).enumerate() + { + // Handling the emitter assertion. + if let Some(ref expected_address) = expected_event.address { + let expected = expected_address + .resolve_address(resolver.as_ref(), resolution_context) + .await?; + let actual = actual_event.address(); + if actual != expected { + tracing::error!( + event_idx, + %expected, + %actual, + "Event emitter assertion failed", + ); + anyhow::bail!( + "Event emitter assertion failed - Expected {expected} but got {actual}", + ); + } + } + + // Handling the topics assertion. + for (expected, actual) in expected_event + .topics + .as_slice() + .iter() + .zip(actual_event.topics()) + { + let expected = Calldata::new_compound([expected]); + if !expected + .is_equivalent(&actual.0, resolver.as_ref(), resolution_context) + .await + .context("Failed to resolve event topic equivalence")? + { + tracing::error!( + event_idx, + ?receipt, + ?expected, + ?actual, + "Event topics assertion failed", + ); + anyhow::bail!( + "Event topics assertion failed - Expected {expected:?} but got {actual:?}", + ); + } + } + + // Handling the values assertion. + let expected = &expected_event.values; + let actual = &actual_event.data().data; + if !expected + .is_equivalent(&actual.0, resolver.as_ref(), resolution_context) + .await + .context("Failed to resolve event value equivalence")? + { + tracing::error!( + event_idx, + ?receipt, + ?expected, + ?actual, + "Event value assertion failed", + ); + anyhow::bail!( + "Event value assertion failed - Expected {expected:?} but got {actual:?}", + ); + } + } + } + + Ok(()) + } + + #[instrument(level = "info", skip_all)] + pub async fn execute_balance_assertion( + &mut self, + _: &StepPath, + step: &BalanceAssertionStep, + ) -> anyhow::Result<()> { + self.step_address_auto_deployment(&step.address) + .await + .context("Failed to perform auto-deployment for the step address")?; + + let resolver = self.platform_information.node.resolver().await?; + let address = step + .address + .resolve_address(resolver.as_ref(), self.default_resolution_context()) + .await?; + + let balance = self.platform_information.node.balance_of(address).await?; + + let expected = step.expected_balance; + let actual = balance; + if expected != actual { + tracing::error!(%expected, %actual, %address, "Balance assertion failed"); + anyhow::bail!( + "Balance assertion failed - Expected {} but got {} for {} resolved to {}", + expected, + actual, + address, + address, + ) + } + + Ok(()) + } + + #[instrument(level = "info", skip_all, err(Debug))] + async fn execute_storage_empty_assertion_step( + &mut self, + _: &StepPath, + step: &StorageEmptyAssertionStep, + ) -> Result<()> { + self.step_address_auto_deployment(&step.address) + .await + .context("Failed to perform auto-deployment for the step address")?; + + let resolver = self.platform_information.node.resolver().await?; + let address = step + .address + .resolve_address(resolver.as_ref(), self.default_resolution_context()) + .await?; + + let storage = self + .platform_information + .node + .latest_state_proof(address, Default::default()) + .await?; + let is_empty = storage.storage_hash == EMPTY_ROOT_HASH; + + let expected = step.is_storage_empty; + let actual = is_empty; + + if expected != actual { + tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed"); + anyhow::bail!( + "Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}", + expected, + actual, + address, + address, + ) + }; + + Ok(()) + } + + #[instrument(level = "info", skip_all, err(Debug))] + async fn execute_repeat_step(&mut self, step_path: &StepPath, step: &RepeatStep) -> Result<()> { + let tasks = + (0..step.repeat) + .map(|_| DifferentialTestsPlatformDriver { + platform_information: self.platform_information, + test_definition: self.test_definition, + private_key_allocator: self.private_key_allocator.clone(), + execution_state: self.execution_state.clone(), + steps_iterator: step.steps.iter().cloned().enumerate().map( + |(step_idx, step)| { + let step_idx = StepIdx::new(step_idx); + let step_path = step_path.append(step_idx); + (step_path, step) + }, + ), + }) + .map(|driver| driver.execute_all()) + .collect::>(); + futures::future::try_join_all(tasks) + .await + .context("Repetition execution failed")?; + Ok(()) + } + + #[instrument(level = "info", skip_all, err(Debug))] + pub async fn execute_account_allocation( + &mut self, + _: &StepPath, + step: &AllocateAccountStep, + ) -> Result<()> { + let Some(variable_name) = step.variable_name.strip_prefix("$VARIABLE:") else { + bail!("Account allocation must start with $VARIABLE:"); + }; + + let private_key = self.private_key_allocator.lock().await.allocate()?; + let account = private_key.address(); + let variable = U256::from_be_slice(account.0.as_slice()); + + self.execution_state + .variables + .insert(variable_name.to_string(), variable); + + Ok(()) + } + // endregion:Step Handling + + // region:Contract Deployment + #[instrument( + level = "info", + skip_all, + fields( + platform_identifier = %self.platform_information.platform.platform_identifier(), + %contract_instance, + %deployer + ), + err(Debug), + )] + async fn get_or_deploy_contract_instance( + &mut self, + contract_instance: &ContractInstance, + deployer: Address, + calldata: Option<&Calldata>, + value: Option, + ) -> Result<(Address, JsonAbi, Option)> { + if let Some((_, address, abi)) = self + .execution_state + .deployed_contracts + .get(contract_instance) + { + info!( + + %address, + "Contract instance already deployed." + ); + Ok((*address, abi.clone(), None)) + } else { + info!("Contract instance requires deployment."); + let (address, abi, receipt) = self + .deploy_contract(contract_instance, deployer, calldata, value) + .await + .context("Failed to deploy contract")?; + info!( + %address, + "Contract instance has been deployed." + ); + Ok((address, abi, Some(receipt))) + } + } + + #[instrument( + level = "info", + skip_all, + fields( + platform_identifier = %self.platform_information.platform.platform_identifier(), + %contract_instance, + %deployer + ), + err(Debug), + )] + async fn deploy_contract( + &mut self, + contract_instance: &ContractInstance, + deployer: Address, + calldata: Option<&Calldata>, + value: Option, + ) -> Result<(Address, JsonAbi, TransactionReceipt)> { + let Some(ContractPathAndIdent { + contract_source_path, + contract_ident, + }) = self + .test_definition + .metadata + .contract_sources()? + .remove(contract_instance) + else { + anyhow::bail!( + "Contract source not found for instance {:?}", + contract_instance + ) + }; + + let Some((code, abi)) = self + .execution_state + .compiled_contracts + .get(&contract_source_path) + .and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref())) + .cloned() + else { + anyhow::bail!( + "Failed to find information for contract {:?}", + contract_instance + ) + }; + + let mut code = match alloy::hex::decode(&code) { + Ok(code) => code, + Err(error) => { + tracing::error!( + ?error, + contract_source_path = contract_source_path.display().to_string(), + contract_ident = contract_ident.as_ref(), + "Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking" + ); + anyhow::bail!("Failed to hex-decode the byte code {}", error) + } + }; + + if let Some(calldata) = calldata { + let resolver = self.platform_information.node.resolver().await?; + let calldata = calldata + .calldata(resolver.as_ref(), self.default_resolution_context()) + .await?; + code.extend(calldata); + } + + let tx = { + let tx = TransactionRequest::default().from(deployer); + let tx = match value { + Some(ref value) => tx.value(value.into_inner()), + _ => tx, + }; + TransactionBuilder::::with_deploy_code(tx, code) + }; + + let receipt = match self.platform_information.node.execute_transaction(tx).await { + Ok(receipt) => receipt, + Err(error) => { + tracing::error!(?error, "Contract deployment transaction failed."); + return Err(error); + } + }; + + let Some(address) = receipt.contract_address else { + anyhow::bail!("Contract deployment didn't return an address"); + }; + tracing::info!( + instance_name = ?contract_instance, + instance_address = ?address, + "Deployed contract" + ); + self.platform_information + .reporter + .report_contract_deployed_event(contract_instance.clone(), address)?; + + self.execution_state.deployed_contracts.insert( + contract_instance.clone(), + (contract_ident, address, abi.clone()), + ); + + Ok((address, abi, receipt)) + } + + #[instrument(level = "info", skip_all)] + async fn step_address_auto_deployment( + &mut self, + step_address: &StepAddress, + ) -> Result
{ + match step_address { + StepAddress::Address(address) => Ok(*address), + StepAddress::ResolvableAddress(resolvable) => { + let Some(instance) = resolvable + .strip_suffix(".address") + .map(ContractInstance::new) + else { + bail!("Not an address variable"); + }; + + self.get_or_deploy_contract_instance( + &instance, + FunctionCallStep::default_caller_address(), + None, + None, + ) + .await + .map(|v| v.0) + } + } + } + // endregion:Contract Deployment + + // region:Resolution & Resolver + fn default_resolution_context(&self) -> ResolutionContext<'_> { + ResolutionContext::default() + .with_deployed_contracts(&self.execution_state.deployed_contracts) + .with_variables(&self.execution_state.variables) + } + // endregion:Resolution & Resolver } diff --git a/crates/core/src/differential_tests/execution_state.rs b/crates/core/src/differential_tests/execution_state.rs index a881430..5cae329 100644 --- a/crates/core/src/differential_tests/execution_state.rs +++ b/crates/core/src/differential_tests/execution_state.rs @@ -7,6 +7,7 @@ use alloy::{ use revive_dt_format::metadata::{ContractIdent, ContractInstance}; +#[derive(Clone)] /// The state associated with the test execution of one of the tests. pub struct ExecutionState { /// The compiled contracts, these contracts have been compiled and have had the libraries linked @@ -17,7 +18,7 @@ pub struct ExecutionState { pub deployed_contracts: HashMap, /// This map stores the variables used for each one of the cases contained in the metadata file. - variables: HashMap, + pub variables: HashMap, } impl ExecutionState { diff --git a/crates/format/src/case.rs b/crates/format/src/case.rs index 75eca22..7c48279 100644 --- a/crates/format/src/case.rs +++ b/crates/format/src/case.rs @@ -3,10 +3,7 @@ use serde::{Deserialize, Serialize}; use revive_dt_common::{macros::define_wrapper_type, types::Mode}; -use crate::{ - mode::ParsedMode, - steps::{Expected, RepeatStep, Step}, -}; +use crate::{mode::ParsedMode, steps::*}; #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)] pub struct Case {