mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-25 12:47:59 +00:00
Fix concurrency issues (#142)
* Fix the OS FD error * Cache the compiler versions * Allow for auto display impl in declare wrapper type macro * Better logging and fix concurrency issues * Fix tests * Format * Make the code even more concurrent
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_dt_common::macros::define_wrapper_type;
|
||||
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
|
||||
|
||||
use crate::{
|
||||
input::{Expected, Step},
|
||||
@@ -60,16 +60,17 @@ impl Case {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn solc_modes(&self) -> Vec<Mode> {
|
||||
match &self.modes {
|
||||
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
||||
None => Mode::all().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
define_wrapper_type!(
|
||||
/// A wrapper type for the index of test cases found in metadata file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CaseIdx(usize);
|
||||
pub struct CaseIdx(usize) impl Display;
|
||||
);
|
||||
|
||||
impl std::fmt::Display for CaseIdx {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
+54
-55
@@ -3,10 +3,11 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use revive_dt_common::cached_fs::read_dir;
|
||||
use revive_dt_common::iterators::FilesWithExtensionIterator;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::metadata::MetadataFile;
|
||||
use crate::metadata::{Metadata, MetadataFile};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
@@ -18,7 +19,7 @@ pub enum Corpus {
|
||||
impl Corpus {
|
||||
pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
let mut corpus = File::open(file_path.as_ref())
|
||||
.map_err(Into::<anyhow::Error>::into)
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?;
|
||||
|
||||
for path in corpus.paths_iter_mut() {
|
||||
@@ -42,10 +43,52 @@ impl Corpus {
|
||||
}
|
||||
|
||||
pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
|
||||
let mut tests = Vec::new();
|
||||
for path in self.paths_iter() {
|
||||
collect_metadata(path, &mut tests);
|
||||
}
|
||||
let mut tests = self
|
||||
.paths_iter()
|
||||
.flat_map(|root_path| {
|
||||
if !root_path.is_dir() {
|
||||
Box::new(std::iter::once(root_path.to_path_buf()))
|
||||
as Box<dyn Iterator<Item = _>>
|
||||
} else {
|
||||
Box::new(
|
||||
FilesWithExtensionIterator::new(root_path)
|
||||
.with_use_cached_fs(true)
|
||||
.with_allowed_extension("sol")
|
||||
.with_allowed_extension("json"),
|
||||
)
|
||||
}
|
||||
.map(move |metadata_file_path| (root_path, metadata_file_path))
|
||||
})
|
||||
.filter_map(|(root_path, metadata_file_path)| {
|
||||
Metadata::try_from_file(&metadata_file_path)
|
||||
.or_else(|| {
|
||||
debug!(
|
||||
discovered_from = %root_path.display(),
|
||||
metadata_file_path = %metadata_file_path.display(),
|
||||
"Skipping file since it doesn't contain valid metadata"
|
||||
);
|
||||
None
|
||||
})
|
||||
.map(|metadata| MetadataFile {
|
||||
metadata_file_path,
|
||||
corpus_file_path: root_path.to_path_buf(),
|
||||
content: metadata,
|
||||
})
|
||||
.inspect(|metadata_file| {
|
||||
debug!(
|
||||
metadata_file_path = %metadata_file.relative_path().display(),
|
||||
"Loaded metadata file"
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
|
||||
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
|
||||
info!(
|
||||
len = tests.len(),
|
||||
corpus_name = self.name(),
|
||||
"Found tests in Corpus"
|
||||
);
|
||||
tests
|
||||
}
|
||||
|
||||
@@ -76,55 +119,11 @@ impl Corpus {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively walks `path` and parses any JSON or Solidity file into a test
|
||||
/// definition [Metadata].
|
||||
///
|
||||
/// Found tests are inserted into `tests`.
|
||||
///
|
||||
/// `path` is expected to be a directory.
|
||||
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
|
||||
if path.is_dir() {
|
||||
let dir_entry = match read_dir(path) {
|
||||
Ok(dir_entry) => dir_entry,
|
||||
Err(error) => {
|
||||
tracing::error!("failed to read dir '{}': {error}", path.display());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for path in dir_entry {
|
||||
let path = match path {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => {
|
||||
tracing::error!("error reading dir entry: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
collect_metadata(&path, tests);
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
if let Some(metadata) = MetadataFile::try_from_file(&path) {
|
||||
tests.push(metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let Some(extension) = path.extension() else {
|
||||
tracing::error!("Failed to get file extension");
|
||||
return;
|
||||
};
|
||||
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") {
|
||||
if let Some(metadata) = MetadataFile::try_from_file(path) {
|
||||
tests.push(metadata)
|
||||
}
|
||||
} else {
|
||||
tracing::error!(?extension, "Unsupported file extension");
|
||||
pub fn path_count(&self) -> usize {
|
||||
match self {
|
||||
Corpus::SinglePath { .. } => 1,
|
||||
Corpus::MultiplePaths { paths, .. } => paths.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-57
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
||||
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
hex::ToHexExt,
|
||||
json_abi::Function,
|
||||
network::TransactionBuilder,
|
||||
primitives::{Address, Bytes, U256},
|
||||
@@ -10,10 +9,12 @@ use alloy::{
|
||||
};
|
||||
use alloy_primitives::{FixedBytes, utils::parse_units};
|
||||
use anyhow::Context;
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
||||
use semver::VersionReq;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_dt_common::macros::define_wrapper_type;
|
||||
use tracing::{Instrument, info_span, instrument};
|
||||
|
||||
use crate::traits::ResolverApi;
|
||||
use crate::{metadata::ContractInstance, traits::ResolutionContext};
|
||||
@@ -33,6 +34,11 @@ pub enum Step {
|
||||
StorageEmptyAssertion(Box<StorageEmptyAssertion>),
|
||||
}
|
||||
|
||||
define_wrapper_type!(
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct StepIdx(usize) impl Display;
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Input {
|
||||
#[serde(default = "Input::default_caller")]
|
||||
@@ -188,7 +194,7 @@ define_wrapper_type! {
|
||||
/// This represents an item in the [`Calldata::Compound`] variant.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct CalldataItem(String);
|
||||
pub struct CalldataItem(String) impl Display;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
@@ -233,7 +239,7 @@ pub enum Method {
|
||||
|
||||
define_wrapper_type!(
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct EtherValue(U256);
|
||||
pub struct EtherValue(U256) impl Display;
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
@@ -268,15 +274,9 @@ impl Input {
|
||||
}
|
||||
Method::FunctionName(ref function_name) => {
|
||||
let Some(abi) = context.deployed_contract_abi(&self.instance) else {
|
||||
tracing::error!(
|
||||
contract_name = self.instance.as_ref(),
|
||||
"Attempted to lookup ABI of contract but it wasn't found"
|
||||
);
|
||||
anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref());
|
||||
};
|
||||
|
||||
tracing::trace!("ABI found for instance: {}", &self.instance.as_ref());
|
||||
|
||||
// We follow the same logic that's implemented in the matter-labs-tester where they resolve
|
||||
// the function name into a function selector and they assume that he function doesn't have
|
||||
// any existing overloads.
|
||||
@@ -302,13 +302,6 @@ impl Input {
|
||||
.selector()
|
||||
};
|
||||
|
||||
tracing::trace!("Functions found for instance: {}", self.instance.as_ref());
|
||||
|
||||
tracing::trace!(
|
||||
"Starting encoding ABI's parameters for instance: {}",
|
||||
self.instance.as_ref()
|
||||
);
|
||||
|
||||
// Allocating a vector that we will be using for the calldata. The vector size will be:
|
||||
// 4 bytes for the function selector.
|
||||
// function.inputs.len() * 32 bytes for the arguments (each argument is a U256).
|
||||
@@ -435,17 +428,18 @@ impl Calldata {
|
||||
buffer.extend_from_slice(bytes);
|
||||
}
|
||||
Calldata::Compound(items) => {
|
||||
for (arg_idx, arg) in items.iter().enumerate() {
|
||||
match arg.resolve(resolver, context).await {
|
||||
Ok(resolved) => {
|
||||
buffer.extend(resolved.to_be_bytes::<32>());
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument");
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
let resolved = stream::iter(items.iter().enumerate())
|
||||
.map(|(arg_idx, arg)| async move {
|
||||
arg.resolve(resolver, context)
|
||||
.instrument(info_span!("Resolving argument", %arg, arg_idx))
|
||||
.map_ok(|value| value.to_be_bytes::<32>())
|
||||
.await
|
||||
})
|
||||
.buffered(0xFF)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
buffer.extend(resolved.into_iter().flatten());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
@@ -468,36 +462,37 @@ impl Calldata {
|
||||
match self {
|
||||
Calldata::Single(calldata) => Ok(calldata == other),
|
||||
Calldata::Compound(items) => {
|
||||
// Chunking the "other" calldata into 32 byte chunks since each
|
||||
// one of the items in the compound calldata represents 32 bytes
|
||||
for (this, other) in items.iter().zip(other.chunks(32)) {
|
||||
// The matterlabs format supports wildcards and therefore we
|
||||
// also need to support them.
|
||||
if this.as_ref() == "*" {
|
||||
continue;
|
||||
}
|
||||
stream::iter(items.iter().zip(other.chunks(32)))
|
||||
.map(|(this, other)| async move {
|
||||
// The matterlabs format supports wildcards and therefore we
|
||||
// also need to support them.
|
||||
if this.as_ref() == "*" {
|
||||
return Ok::<_, anyhow::Error>(true);
|
||||
}
|
||||
|
||||
let other = if other.len() < 32 {
|
||||
let mut vec = other.to_vec();
|
||||
vec.resize(32, 0);
|
||||
std::borrow::Cow::Owned(vec)
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(other)
|
||||
};
|
||||
let other = if other.len() < 32 {
|
||||
let mut vec = other.to_vec();
|
||||
vec.resize(32, 0);
|
||||
std::borrow::Cow::Owned(vec)
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(other)
|
||||
};
|
||||
|
||||
let this = this.resolve(resolver, context).await?;
|
||||
let other = U256::from_be_slice(&other);
|
||||
if this != other {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
let this = this.resolve(resolver, context).await?;
|
||||
let other = U256::from_be_slice(&other);
|
||||
Ok(this == other)
|
||||
})
|
||||
.buffered(0xFF)
|
||||
.all(|v| async move { v.is_ok_and(|v| v) })
|
||||
.map(Ok)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalldataItem {
|
||||
#[instrument(level = "info", skip_all, err)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
resolver: &impl ResolverApi,
|
||||
@@ -548,14 +543,7 @@ impl CalldataItem {
|
||||
match stack.as_slice() {
|
||||
// Empty stack means that we got an empty compound calldata which we resolve to zero.
|
||||
[] => Ok(U256::ZERO),
|
||||
[CalldataToken::Item(item)] => {
|
||||
tracing::debug!(
|
||||
original = self.0,
|
||||
resolved = item.to_be_bytes::<32>().encode_hex(),
|
||||
"Resolved a Calldata item"
|
||||
);
|
||||
Ok(*item)
|
||||
}
|
||||
[CalldataToken::Item(item)] => Ok(*item),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Invalid calldata arithmetic operation - Invalid stack"
|
||||
)),
|
||||
|
||||
@@ -15,6 +15,7 @@ use revive_dt_common::{
|
||||
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
|
||||
types::Mode,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{case::Case, mode::ParsedMode};
|
||||
|
||||
@@ -24,16 +25,26 @@ pub const SOLIDITY_CASE_COMMENT_MARKER: &str = "//!";
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
|
||||
pub struct MetadataFile {
|
||||
pub path: PathBuf,
|
||||
/// The path of the metadata file. This will either be a JSON or solidity file.
|
||||
pub metadata_file_path: PathBuf,
|
||||
|
||||
/// This is the path contained within the corpus file. This could either be the path of some dir
|
||||
/// or could be the actual metadata file path.
|
||||
pub corpus_file_path: PathBuf,
|
||||
|
||||
/// The metadata contained within the file.
|
||||
pub content: Metadata,
|
||||
}
|
||||
|
||||
impl MetadataFile {
|
||||
pub fn try_from_file(path: &Path) -> Option<Self> {
|
||||
Metadata::try_from_file(path).map(|metadata| Self {
|
||||
path: path.to_owned(),
|
||||
content: metadata,
|
||||
})
|
||||
pub fn relative_path(&self) -> &Path {
|
||||
if self.corpus_file_path.is_file() {
|
||||
&self.corpus_file_path
|
||||
} else {
|
||||
self.metadata_file_path
|
||||
.strip_prefix(&self.corpus_file_path)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,10 +156,7 @@ impl Metadata {
|
||||
pub fn try_from_file(path: &Path) -> Option<Self> {
|
||||
assert!(path.is_file(), "not a file: {}", path.display());
|
||||
|
||||
let Some(file_extension) = path.extension() else {
|
||||
tracing::debug!("skipping corpus file: {}", path.display());
|
||||
return None;
|
||||
};
|
||||
let file_extension = path.extension()?;
|
||||
|
||||
if file_extension == METADATA_FILE_EXTENSION {
|
||||
return Self::try_from_json(path);
|
||||
@@ -158,18 +166,12 @@ impl Metadata {
|
||||
return Self::try_from_solidity(path);
|
||||
}
|
||||
|
||||
tracing::debug!("ignoring invalid corpus file: {}", path.display());
|
||||
None
|
||||
}
|
||||
|
||||
fn try_from_json(path: &Path) -> Option<Self> {
|
||||
let file = File::open(path)
|
||||
.inspect_err(|error| {
|
||||
tracing::error!(
|
||||
"opening JSON test metadata file '{}' error: {error}",
|
||||
path.display()
|
||||
);
|
||||
})
|
||||
.inspect_err(|err| error!(path = %path.display(), %err, "Failed to open file"))
|
||||
.ok()?;
|
||||
|
||||
match serde_json::from_reader::<_, Metadata>(file) {
|
||||
@@ -177,11 +179,8 @@ impl Metadata {
|
||||
metadata.file_path = Some(path.to_path_buf());
|
||||
Some(metadata)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"parsing JSON test metadata file '{}' error: {error}",
|
||||
path.display()
|
||||
);
|
||||
Err(err) => {
|
||||
error!(path = %path.display(), %err, "Deserialization of metadata failed");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -189,12 +188,7 @@ impl Metadata {
|
||||
|
||||
fn try_from_solidity(path: &Path) -> Option<Self> {
|
||||
let spec = read_to_string(path)
|
||||
.inspect_err(|error| {
|
||||
tracing::error!(
|
||||
"opening JSON test metadata file '{}' error: {error}",
|
||||
path.display()
|
||||
);
|
||||
})
|
||||
.inspect_err(|err| error!(path = %path.display(), %err, "Failed to read file content"))
|
||||
.ok()?
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix(SOLIDITY_CASE_COMMENT_MARKER))
|
||||
@@ -222,11 +216,8 @@ impl Metadata {
|
||||
);
|
||||
Some(metadata)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"parsing Solidity test metadata file '{}' error: '{error}' from data: {spec}",
|
||||
path.display()
|
||||
);
|
||||
Err(err) => {
|
||||
error!(path = %path.display(), %err, "Failed to deserialize metadata");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -266,7 +257,7 @@ define_wrapper_type!(
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContractInstance(String);
|
||||
pub struct ContractInstance(String) impl Display;
|
||||
);
|
||||
|
||||
define_wrapper_type!(
|
||||
@@ -277,7 +268,7 @@ define_wrapper_type!(
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContractIdent(String);
|
||||
pub struct ContractIdent(String) impl Display;
|
||||
);
|
||||
|
||||
/// Represents an identifier used for contracts.
|
||||
|
||||
@@ -223,7 +223,7 @@ mod tests {
|
||||
|
||||
for (actual, expected) in strings {
|
||||
let parsed = ParsedMode::from_str(actual)
|
||||
.expect(format!("Failed to parse mode string '{actual}'").as_str());
|
||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||
assert_eq!(
|
||||
expected,
|
||||
parsed.to_string(),
|
||||
@@ -249,7 +249,7 @@ mod tests {
|
||||
|
||||
for (actual, expected) in strings {
|
||||
let parsed = ParsedMode::from_str(actual)
|
||||
.expect(format!("Failed to parse mode string '{actual}'").as_str());
|
||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
|
||||
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user