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:
Omar
2025-08-19 09:47:36 +03:00
committed by GitHub
parent c58551803d
commit 76d6a154c1
33 changed files with 773 additions and 720 deletions
+9 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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"
)),
+26 -35
View File
@@ -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.
+2 -2
View File
@@ -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();