Better logging and fix concurrency issues

This commit is contained in:
Omar Abdulla
2025-08-18 16:16:33 +03:00
parent fb3959d345
commit 609ececea6
24 changed files with 433 additions and 534 deletions
+3
View File
@@ -25,3 +25,6 @@ serde_json = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
[lints]
workspace = true
+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(),
}
}
}
+14 -31
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},
@@ -14,6 +13,7 @@ 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 +33,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")]
@@ -268,15 +273,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 +301,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).
@@ -436,15 +428,12 @@ impl Calldata {
}
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);
}
};
buffer.extend(
arg.resolve(resolver, context)
.instrument(info_span!("Resolving argument", %arg, arg_idx))
.await?
.to_be_bytes::<32>(),
);
}
}
};
@@ -498,6 +487,7 @@ impl Calldata {
}
impl CalldataItem {
#[instrument(level = "info", skip_all, err)]
async fn resolve(
&self,
resolver: &impl ResolverApi,
@@ -548,14 +538,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"
)),
+24 -33
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
}
}