Compare commits

...

4 Commits

Author SHA1 Message Date
Omar Abdulla 1689118c5d Fix contract discovery for simple tests 2025-07-28 09:49:26 +03:00
Omar 65f41f2038 Correct the type of address in matterlabs events (#82) 2025-07-28 05:01:52 +00:00
Omar 3ed8a1ca1c Support compiler-version aware exceptions (#81) 2025-07-25 14:23:17 +00:00
Omar 2923d675cd Support Compile-time Linking (#79)
* Use wrappers for libraries in metadata.

* Create a unified way to access deployed contracts

* Support linking at compile time
2025-07-25 07:03:21 +00:00
7 changed files with 412 additions and 157 deletions
Generated
+1
View File
@@ -3963,6 +3963,7 @@ dependencies = [
name = "revive-dt-compiler" name = "revive-dt-compiler"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy-primitives",
"anyhow", "anyhow",
"revive-common", "revive-common",
"revive-dt-config", "revive-dt-config",
+3 -1
View File
@@ -9,11 +9,13 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow = { workspace = true }
revive-solc-json-interface = { workspace = true } revive-solc-json-interface = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-solc-binaries = { workspace = true } revive-dt-solc-binaries = { workspace = true }
revive-common = { workspace = true } revive-common = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
+21
View File
@@ -9,6 +9,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use alloy_primitives::Address;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_common::EVMVersion; use revive_common::EVMVersion;
@@ -158,6 +159,26 @@ where
self self
} }
pub fn with_library(
mut self,
scope: impl AsRef<Path>,
library_ident: impl AsRef<str>,
library_address: Address,
) -> Self {
self.input
.settings
.libraries
.get_or_insert_with(Default::default)
.entry(scope.as_ref().display().to_string())
.or_default()
.insert(
library_ident.as_ref().to_owned(),
library_address.to_string(),
);
self
}
pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result<CompilerOutput<T::Options>> { pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result<CompilerOutput<T::Options>> {
T::new(solc_path).build(CompilerInput { T::new(solc_path).build(CompilerInput {
extra_options: self.extra_options, extra_options: self.extra_options,
+4
View File
@@ -10,6 +10,10 @@ use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_solc_json_interface::SolcStandardJsonOutput; use revive_solc_json_interface::SolcStandardJsonOutput;
// TODO: I believe that we need to also pass the solc compiler to resolc so that resolc uses the
// specified solc compiler. I believe that currently we completely ignore the specified solc binary
// when invoking resolc which doesn't seem right if we're using solc as a compiler frontend.
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode. /// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)] #[derive(Debug)]
pub struct Resolc { pub struct Resolc {
+318 -140
View File
@@ -3,6 +3,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::str::FromStr;
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder}; use alloy::network::{Ethereum, TransactionBuilder};
@@ -26,8 +27,8 @@ use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, SolidityCompiler}; use revive_dt_compiler::{Compiler, SolidityCompiler};
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_format::case::CaseIdx; use revive_dt_format::case::CaseIdx;
use revive_dt_format::input::{Calldata, Expected, ExpectedOutput, Method}; use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdentifier}; use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode}; use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode};
use revive_dt_node::Node; use revive_dt_node::Node;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
@@ -57,6 +58,12 @@ pub struct State<'a, T: Platform> {
/// files. /// files.
deployed_contracts: HashMap<CaseIdx, HashMap<ContractInstance, (Address, JsonAbi)>>, deployed_contracts: HashMap<CaseIdx, HashMap<ContractInstance, (Address, JsonAbi)>>,
/// This is a map of the deployed libraries.
///
/// This map is not per case, but rather, per metadata file. This means that we do not redeploy
/// the libraries with each case.
deployed_libraries: HashMap<ContractInstance, (Address, JsonAbi)>,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
@@ -70,6 +77,7 @@ where
span, span,
contracts: Default::default(), contracts: Default::default(),
deployed_contracts: Default::default(), deployed_contracts: Default::default(),
deployed_libraries: Default::default(),
phantom: Default::default(), phantom: Default::default(),
} }
} }
@@ -93,13 +101,49 @@ where
anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version); anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version);
}; };
// Note: if the metadata is contained within a solidity file then this is the only file that
// we wish to compile since this is a self-contained test. Otherwise, if it's a JSON file
// then we need to compile all of the contracts that are in the directory since imports are
// allowed in there.
let Some(ref metadata_file_path) = metadata.file_path else {
anyhow::bail!("The metadata file path is not defined");
};
let mut files_to_compile = if metadata_file_path
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("sol"))
{
Box::new(std::iter::once(metadata_file_path.clone())) as Box<dyn Iterator<Item = _>>
} else {
Box::new(
FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol"),
)
};
let compiler = Compiler::<T::Compiler>::new() let compiler = Compiler::<T::Compiler>::new()
.allow_path(metadata.directory()?) .allow_path(metadata.directory()?)
.solc_optimizer(mode.solc_optimize()); .solc_optimizer(mode.solc_optimize());
let mut compiler =
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
for (library_instance, (library_address, _)) in self.deployed_libraries.iter() {
let library_ident = &metadata
.contracts
.as_ref()
.and_then(|contracts| contracts.get(library_instance))
.expect("Impossible for library to not be found in contracts")
.contract_ident;
let compiler = FilesWithExtensionIterator::new(metadata.directory()?) // Note the following: we need to tell solc which files require the libraries to be
// linked into them. We do not have access to this information and therefore we choose
// an easier, yet more compute intensive route, of telling solc that all of the files
// need to link the library and it will only perform the linking for the files that do
// actually need the library.
compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol") .with_allowed_extension("sol")
.try_fold(compiler, |compiler, path| compiler.with_source(&path))?; .fold(compiler, |compiler, path| {
compiler.with_library(&path, library_ident.as_str(), *library_address)
});
}
let mut task = CompilationTask { let mut task = CompilationTask {
json_input: compiler.input(), json_input: compiler.input(),
@@ -141,18 +185,47 @@ where
} }
} }
pub fn build_and_publish_libraries(
&mut self,
metadata: &Metadata,
mode: &SolcMode,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.build_contracts(mode, metadata)?;
for library_instance in metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
self.get_or_deploy_contract_instance(
library_instance,
metadata,
None,
Input::default_caller(),
None,
None,
node,
)?;
}
Ok(())
}
pub fn handle_input( pub fn handle_input(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx, case_idx: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> { ) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts = let deployment_receipts =
self.handle_contract_deployment(metadata, case_idx, input, node)?; self.handle_contract_deployment(metadata, case_idx, input, node)?;
let execution_receipt = let execution_receipt =
self.handle_input_execution(case_idx, input, deployment_receipts, node)?; self.handle_input_execution(case_idx, input, deployment_receipts, node)?;
self.handle_input_expectations(case_idx, input, &execution_receipt, node)?; self.handle_input_expectations(case_idx, input, &execution_receipt, node, mode)?;
self.handle_input_diff(case_idx, execution_receipt, node) self.handle_input_diff(case_idx, execution_receipt, node)
} }
@@ -173,12 +246,7 @@ where
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new(); let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() { for instance in input.find_all_contract_instances().into_iter() {
if !self if !self.deployed_contracts(case_idx).contains_key(&instance) {
.deployed_contracts
.entry(case_idx)
.or_default()
.contains_key(&instance)
{
instances_we_must_deploy.entry(instance).or_insert(false); instances_we_must_deploy.entry(instance).or_insert(false);
} }
} }
@@ -194,125 +262,23 @@ where
let mut receipts = HashMap::new(); let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
// What we have at this moment is just a contract instance which is kind of like a variable let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
// name for an actual underlying contract. So, we need to resolve this instance to the info let value = deploy_with_constructor_arguments
// of the contract that it belongs to. .then_some(input.value)
let Some(ContractPathAndIdentifier { .flatten();
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(&instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!("Contract source not found for instance {:?}", instance)
};
let compiled_contract = self.contracts.iter().find_map(|output| {
output
.contracts
.as_ref()?
.get(&contract_source_path.display().to_string())
.and_then(|source_file_contracts| {
source_file_contracts.get(contract_ident.as_ref())
})
});
let Some(code) = compiled_contract
.and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref()))
else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find bytecode for contract"
);
anyhow::bail!("Failed to find bytecode for contract {:?}", instance)
};
// TODO: When we want to do linking it would be best to do it at this stage here. We have
// the context from the metadata files and therefore know what needs to be linked and in
// what order it needs to happen.
let mut code = match alloy::hex::decode(&code.object) {
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 deploy_with_constructor_arguments {
let encoded_input = input
.encoded_input(self.deployed_contracts.entry(case_idx).or_default(), node)?;
code.extend(encoded_input.to_vec());
}
let tx = {
let tx = TransactionRequest::default().from(input.caller);
let tx = match input.value {
Some(ref value) if deploy_with_constructor_arguments => {
tx.value(value.into_inner())
}
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<T>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?instance,
instance_address = ?address,
"Deployed contract"
);
let Some(Value::String(metadata)) =
compiled_contract.and_then(|contract| contract.metadata.as_ref())
else {
tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else {
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!(
"Failed to access the .output.abi field of the solc metadata {metadata}"
);
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
self.deployed_contracts
.entry(case_idx)
.or_default()
.insert(instance.clone(), (address, abi));
if let (_, _, Some(receipt)) = self.get_or_deploy_contract_instance(
&instance,
metadata,
case_idx,
input.caller,
calldata,
value,
node,
)? {
receipts.insert(instance.clone(), receipt); receipts.insert(instance.clone(), receipt);
} }
}
Ok(receipts) Ok(receipts)
} }
@@ -332,9 +298,7 @@ where
.remove(&input.instance) .remove(&input.instance)
.context("Failed to find deployment receipt"), .context("Failed to find deployment receipt"),
Method::Fallback | Method::FunctionName(_) => { Method::Fallback | Method::FunctionName(_) => {
let tx = match input let tx = match input.legacy_transaction(self.deployed_contracts(case_idx), node) {
.legacy_transaction(self.deployed_contracts.entry(case_idx).or_default(), node)
{
Ok(tx) => { Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}"); tracing::debug!("Legacy transaction data: {tx:#?}");
tx tx
@@ -368,6 +332,7 @@ where
input: &Input, input: &Input,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
node: &T::Blockchain, node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations"); let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter(); let _guard = span.enter();
@@ -423,6 +388,7 @@ where
node, node,
expectation, expectation,
&tracing_result, &tracing_result,
mode,
)?; )?;
} }
@@ -436,13 +402,18 @@ where
node: &T::Blockchain, node: &T::Blockchain,
expectation: &ExpectedOutput, expectation: &ExpectedOutput,
tracing_result: &CallFrame, tracing_result: &CallFrame,
mode: &SolcMode,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// TODO: We want to respect the compiler version filter on the expected output but would if let Some(ref version_requirement) = expectation.compiler_version {
// require some changes to the interfaces of the compiler and such. So, we add it later. let Some(compiler_version) = mode.last_patch_version(&self.config.solc) else {
// Additionally, what happens if the compiler filter doesn't match? Do we consider that the anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version);
// transaction should succeed? Do we just ignore the expectation? };
if !version_requirement.matches(&compiler_version) {
return Ok(());
}
}
let deployed_contracts = self.deployed_contracts.entry(case_idx).or_default(); let deployed_contracts = self.deployed_contracts(case_idx);
let chain_state_provider = node; let chain_state_provider = node;
// Handling the receipt state assertion. // Handling the receipt state assertion.
@@ -487,8 +458,18 @@ where
expected_events.iter().zip(execution_receipt.logs()) expected_events.iter().zip(execution_receipt.logs())
{ {
// Handling the emitter assertion. // Handling the emitter assertion.
if let Some(expected_address) = expected_event.address { if let Some(ref expected_address) = expected_event.address {
let expected = expected_address; let expected = if let Some(contract_instance) = expected_address
.strip_suffix(".address")
.map(ContractInstance::new)
{
deployed_contracts
.get(&contract_instance)
.map(|(address, _)| *address)
} else {
Address::from_str(expected_address).ok()
}
.context("Failed to get the address of the event")?;
let actual = actual_event.address(); let actual = actual_event.address();
if actual != expected { if actual != expected {
tracing::error!( tracing::error!(
@@ -567,6 +548,162 @@ where
Ok((execution_receipt, trace, diff)) Ok((execution_receipt, trace, diff))
} }
fn deployed_contracts(
&mut self,
case_idx: impl Into<Option<CaseIdx>>,
) -> &mut HashMap<ContractInstance, (Address, JsonAbi)> {
match case_idx.into() {
Some(case_idx) => self
.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone()),
None => &mut self.deployed_libraries,
}
}
/// Gets the information of a deployed contract or library from the state. If it's found to not
/// be deployed then it will be deployed.
///
/// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the
/// cross-case deployed contracts address mapping.
#[allow(clippy::too_many_arguments)]
pub fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
metadata: &Metadata,
case_idx: impl Into<Option<CaseIdx>>,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
node: &T::Blockchain,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
let case_idx = case_idx.into();
if let Some((address, abi)) = self.deployed_libraries.get(contract_instance) {
return Ok((*address, abi.clone(), None));
}
if let Some(case_idx) = case_idx {
if let Some((address, abi)) = self
.deployed_contracts
.get(&case_idx)
.and_then(|contracts| contracts.get(contract_instance))
{
return Ok((*address, abi.clone(), None));
}
}
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let compiled_contract = self.contracts.iter().rev().find_map(|output| {
output
.contracts
.as_ref()?
.get(&contract_source_path.display().to_string())
.and_then(|source_file_contracts| {
source_file_contracts.get(contract_ident.as_ref())
})
});
let Some(code) = compiled_contract
.and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref()))
else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find bytecode for contract"
);
anyhow::bail!(
"Failed to find bytecode for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code.object) {
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)
}
};
let Some(Value::String(metadata)) =
compiled_contract.and_then(|contract| contract.metadata.as_ref())
else {
tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else {
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!("Failed to access the .output.abi field of the solc metadata {metadata}");
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
if let Some(calldata) = calldata {
let calldata = calldata.calldata(self.deployed_contracts(case_idx), node)?;
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::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<T>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
self.deployed_contracts(case_idx)
.insert(contract_instance.clone(), (address, abi.clone()));
Ok((address, abi, Some(receipt)))
}
} }
pub struct Driver<'a, Leader: Platform, Follower: Platform> { pub struct Driver<'a, Leader: Platform, Follower: Platform> {
@@ -673,6 +810,42 @@ where
let mut leader_state = State::<L>::new(self.config, span); let mut leader_state = State::<L>::new(self.config, span);
let mut follower_state = State::<F>::new(self.config, span); let mut follower_state = State::<F>::new(self.config, span);
// Note: we are currently forced to do two compilation passes due to linking. In the
// first compilation pass we compile the libraries and publish them to the chain. In the
// second compilation pass we compile the contracts with the library addresses so that
// they're linked at compile-time.
let build_result = tracing::info_span!("Building and publishing libraries")
.in_scope(|| {
match leader_state.build_and_publish_libraries(self.metadata, &mode, self.leader_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Leader, "Library building succeeded");
execution_result.add_successful_build(Target::Leader, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Leader, ?error, "Library building failed");
execution_result.add_failed_build(Target::Leader, mode.clone(), error);
return Err(());
}
}
match follower_state.build_and_publish_libraries(self.metadata, &mode, self.follower_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Follower, "Library building succeeded");
execution_result.add_successful_build(Target::Follower, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Follower, ?error, "Library building failed");
execution_result.add_failed_build(Target::Follower, mode.clone(), error);
return Err(());
}
}
Ok(())
});
if build_result.is_err() {
// Note: We skip to the next solc mode as there's nothing that we can do at this
// point, the building has failed. We do NOT bail out of the execution as a whole.
continue;
}
// We build the contracts. If building the contracts for the metadata file fails then we // We build the contracts. If building the contracts for the metadata file fails then we
// have no other option but to keep note of this error and move on to the next solc mode // have no other option but to keep note of this error and move on to the next solc mode
// and NOT just bail out of the execution as a whole. // and NOT just bail out of the execution as a whole.
@@ -709,7 +882,6 @@ where
// For cases if one of the inputs fail then we move on to the next case and we do NOT // For cases if one of the inputs fail then we move on to the next case and we do NOT
// bail out of the whole thing. // bail out of the whole thing.
'case_loop: for (case_idx, case) in self.metadata.cases.iter().enumerate() { 'case_loop: for (case_idx, case) in self.metadata.cases.iter().enumerate() {
let tracing_span = tracing::info_span!( let tracing_span = tracing::info_span!(
"Handling case", "Handling case",
@@ -730,8 +902,13 @@ where
tracing::info_span!("Executing input", contract_name = ?input.instance) tracing::info_span!("Executing input", contract_name = ?input.instance)
.in_scope(|| { .in_scope(|| {
let (leader_receipt, _, leader_diff) = match leader_state let (leader_receipt, _, leader_diff) = match leader_state
.handle_input(self.metadata, case_idx, &input, self.leader_node) .handle_input(
{ self.metadata,
case_idx,
&input,
self.leader_node,
&mode,
) {
Ok(result) => result, Ok(result) => result,
Err(error) => { Err(error) => {
tracing::error!( tracing::error!(
@@ -760,6 +937,7 @@ where
case_idx, case_idx,
&input, &input,
self.follower_node, self.follower_node,
&mode,
) { ) {
Ok(result) => result, Ok(result) => result,
Err(error) => { Err(error) => {
+50 -1
View File
@@ -51,7 +51,7 @@ pub struct ExpectedOutput {
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Event { pub struct Event {
pub address: Option<Address>, pub address: Option<String>,
pub topics: Vec<String>, pub topics: Vec<String>,
pub values: Calldata, pub values: Calldata,
} }
@@ -1010,4 +1010,53 @@ mod tests {
// Assert // Assert
assert!(resolved.is_err()) assert!(resolved.is_err())
} }
#[test]
fn expected_json_can_be_deserialized1() {
// Arrange
let str = r#"
{
"return_data": [
"1"
],
"events": [
{
"topics": [],
"values": []
}
]
}
"#;
// Act
let expected = serde_json::from_str::<Expected>(str);
// Assert
expected.expect("Failed to deserialize");
}
#[test]
fn expected_json_can_be_deserialized2() {
// Arrange
let str = r#"
{
"return_data": [
"1"
],
"events": [
{
"address": "Main.address",
"topics": [],
"values": []
}
]
}
"#;
// Act
let expected = serde_json::from_str::<Expected>(str);
// Assert
expected.expect("Failed to deserialize");
}
} }
+14 -14
View File
@@ -47,9 +47,9 @@ impl Deref for MetadataFile {
pub struct Metadata { pub struct Metadata {
pub targets: Option<Vec<String>>, pub targets: Option<Vec<String>>,
pub cases: Vec<Case>, pub cases: Vec<Case>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdentifier>>, pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
// TODO: Convert into wrapper types for clarity. // TODO: Convert into wrapper types for clarity.
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>, pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
pub ignore: Option<bool>, pub ignore: Option<bool>,
pub modes: Option<Vec<Mode>>, pub modes: Option<Vec<Mode>>,
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
@@ -86,7 +86,7 @@ impl Metadata {
/// Returns the contract sources with canonicalized paths for the files /// Returns the contract sources with canonicalized paths for the files
pub fn contract_sources( pub fn contract_sources(
&self, &self,
) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdentifier>> { ) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdent>> {
let directory = self.directory()?; let directory = self.directory()?;
let mut sources = BTreeMap::new(); let mut sources = BTreeMap::new();
let Some(contracts) = &self.contracts else { let Some(contracts) = &self.contracts else {
@@ -95,7 +95,7 @@ impl Metadata {
for ( for (
alias, alias,
ContractPathAndIdentifier { ContractPathAndIdent {
contract_source_path, contract_source_path,
contract_ident, contract_ident,
}, },
@@ -107,7 +107,7 @@ impl Metadata {
sources.insert( sources.insert(
alias, alias,
ContractPathAndIdentifier { ContractPathAndIdent {
contract_source_path: absolute_path, contract_source_path: absolute_path,
contract_ident, contract_ident,
}, },
@@ -193,8 +193,8 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf()); metadata.file_path = Some(path.to_path_buf());
metadata.contracts = Some( metadata.contracts = Some(
[( [(
ContractInstance::new("test"), ContractInstance::new("Test"),
ContractPathAndIdentifier { ContractPathAndIdent {
contract_source_path: path.to_path_buf(), contract_source_path: path.to_path_buf(),
contract_ident: ContractIdent::new("Test"), contract_ident: ContractIdent::new("Test"),
}, },
@@ -245,7 +245,7 @@ define_wrapper_type!(
/// ``` /// ```
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
pub struct ContractPathAndIdentifier { pub struct ContractPathAndIdent {
/// The path of the contract source code relative to the directory containing the metadata file. /// The path of the contract source code relative to the directory containing the metadata file.
pub contract_source_path: PathBuf, pub contract_source_path: PathBuf,
@@ -253,7 +253,7 @@ pub struct ContractPathAndIdentifier {
pub contract_ident: ContractIdent, pub contract_ident: ContractIdent,
} }
impl Display for ContractPathAndIdentifier { impl Display for ContractPathAndIdent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
@@ -264,7 +264,7 @@ impl Display for ContractPathAndIdentifier {
} }
} }
impl FromStr for ContractPathAndIdentifier { impl FromStr for ContractPathAndIdent {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -300,7 +300,7 @@ impl FromStr for ContractPathAndIdentifier {
} }
} }
impl TryFrom<String> for ContractPathAndIdentifier { impl TryFrom<String> for ContractPathAndIdent {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
@@ -308,8 +308,8 @@ impl TryFrom<String> for ContractPathAndIdentifier {
} }
} }
impl From<ContractPathAndIdentifier> for String { impl From<ContractPathAndIdent> for String {
fn from(value: ContractPathAndIdentifier) -> Self { fn from(value: ContractPathAndIdent) -> Self {
value.to_string() value.to_string()
} }
} }
@@ -324,7 +324,7 @@ mod test {
let string = "ERC20/ERC20.sol:ERC20"; let string = "ERC20/ERC20.sol:ERC20";
// Act // Act
let identifier = ContractPathAndIdentifier::from_str(string); let identifier = ContractPathAndIdent::from_str(string);
// Assert // Assert
let identifier = identifier.expect("Failed to parse"); let identifier = identifier.expect("Failed to parse");