Compare commits

..

5 Commits

Author SHA1 Message Date
Omar Abdulla a04a4a88cd Lower gas limit for failed estimates 2025-08-15 14:59:06 +03:00
Omar Abdulla 64d0a7f995 Resolve the OS problems 2025-08-15 09:57:05 +03:00
Omar Abdulla 59e50973d4 Merge branch 'feature/chached-fs-sync' into feature/compiler-modes 2025-08-15 07:19:53 +03:00
Omar Abdulla 49f99f6152 Implement compilation modes 2025-08-15 06:50:06 +03:00
Omar Abdulla 7247eca2e8 Add a cached fs abstraction 2025-08-14 17:38:12 +03:00
23 changed files with 705 additions and 710 deletions
Generated
-3
View File
@@ -4093,7 +4093,6 @@ dependencies = [
"moka",
"once_cell",
"semver 1.0.26",
"serde",
"tokio",
]
@@ -4160,7 +4159,6 @@ dependencies = [
"alloy-primitives",
"alloy-sol-types",
"anyhow",
"regex",
"revive-common",
"revive-dt-common",
"semver 1.0.26",
@@ -4203,7 +4201,6 @@ name = "revive-dt-report"
version = "0.1.0"
dependencies = [
"anyhow",
"revive-dt-common",
"revive-dt-compiler",
"revive-dt-config",
"revive-dt-format",
-2
View File
@@ -29,7 +29,6 @@ clap = { version = "4", features = ["derive"] }
foundry-compilers-artifacts = { version = "0.18.0" }
futures = { version = "0.3.31" }
hex = "0.4.3"
regex = "1"
moka = "0.12.10"
reqwest = { version = "0.12.15", features = ["json"] }
once_cell = "1.21"
@@ -45,7 +44,6 @@ sp-core = "36.1.0"
sp-runtime = "41.1.0"
temp-dir = { version = "0.1.16" }
tempfile = "3.3"
thiserror = "2"
tokio = { version = "1.47.0", default-features = false, features = [
"rt-multi-thread",
"process",
+2 -1
View File
@@ -1,7 +1,8 @@
{
"modes": [
"Y >=0.8.9",
"E"
"E",
"I"
],
"cases": [
{
-1
View File
@@ -13,5 +13,4 @@ anyhow = { workspace = true }
moka = { workspace = true, features = ["sync"] }
once_cell = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["time"] }
+24
View File
@@ -47,3 +47,27 @@ pub fn read_dir(path: impl AsRef<Path>) -> Result<Box<dyn Iterator<Item = Result
}
}
}
pub trait PathExt {
fn cached_canonicalize(&self) -> Result<PathBuf>;
}
impl<T> PathExt for T
where
T: AsRef<Path>,
{
fn cached_canonicalize(&self) -> Result<PathBuf> {
static CANONICALIZATION_CACHE: Lazy<Cache<PathBuf, PathBuf>> =
Lazy::new(|| Cache::new(10_000));
let path = self.as_ref().to_path_buf();
match CANONICALIZATION_CACHE.get(&path) {
Some(canonicalized) => Ok(canonicalized),
None => {
let canonicalized = path.canonicalize()?;
CANONICALIZATION_CACHE.insert(path, canonicalized.clone());
Ok(canonicalized)
}
}
}
}
-2
View File
@@ -1,5 +1,3 @@
mod mode;
mod version_or_requirement;
pub use mode::*;
pub use version_or_requirement::*;
-167
View File
@@ -1,167 +0,0 @@
use crate::types::VersionOrRequirement;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
/// This represents a mode that a given test should be run with, if possible.
///
/// We obtain this by taking a [`ParsedMode`], which may be looser or more strict
/// in its requirements, and then expanding it out into a list of [`Mode`]s.
///
/// Use [`ParsedMode::to_test_modes()`] to do this.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Mode {
pub pipeline: ModePipeline,
pub optimize_setting: ModeOptimizerSetting,
pub version: Option<semver::VersionReq>,
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.pipeline.fmt(f)?;
f.write_str(" ")?;
self.optimize_setting.fmt(f)?;
if let Some(version) = &self.version {
f.write_str(" ")?;
version.fmt(f)?;
}
Ok(())
}
}
impl Mode {
/// Return all of the available mode combinations.
pub fn all() -> impl Iterator<Item = Mode> {
ModePipeline::test_cases().flat_map(|pipeline| {
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: None,
})
})
}
/// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if
/// the requirement is present on the object. Otherwise, the passed default version is used.
pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement {
match self.version {
Some(ref requirement) => requirement.clone().into(),
None => default.into(),
}
}
}
/// What do we want the compiler to do?
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModePipeline {
/// Compile Solidity code via Yul IR
ViaYulIR,
/// Compile Solidity direct to assembly
ViaEVMAssembly,
}
impl FromStr for ModePipeline {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
// via Yul IR
"Y" => Ok(ModePipeline::ViaYulIR),
// Don't go via Yul IR
"E" => Ok(ModePipeline::ViaEVMAssembly),
// Anything else that we see isn't a mode at all
_ => Err(anyhow::anyhow!(
"Unsupported pipeline '{s}': expected 'Y' or 'E'"
)),
}
}
}
impl Display for ModePipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModePipeline::ViaYulIR => f.write_str("Y"),
ModePipeline::ViaEVMAssembly => f.write_str("E"),
}
}
}
impl ModePipeline {
/// Should we go via Yul IR?
pub fn via_yul_ir(&self) -> bool {
matches!(self, ModePipeline::ViaYulIR)
}
/// An iterator over the available pipelines that we'd like to test,
/// when an explicit pipeline was not specified.
pub fn test_cases() -> impl Iterator<Item = ModePipeline> + Clone {
[ModePipeline::ViaYulIR, ModePipeline::ViaEVMAssembly].into_iter()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModeOptimizerSetting {
/// 0 / -: Don't apply any optimizations
M0,
/// 1: Apply less than default optimizations
M1,
/// 2: Apply the default optimizations
M2,
/// 3 / +: Apply aggressive optimizations
M3,
/// s: Optimize for size
Ms,
/// z: Aggressively optimize for size
Mz,
}
impl FromStr for ModeOptimizerSetting {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"M0" => Ok(ModeOptimizerSetting::M0),
"M1" => Ok(ModeOptimizerSetting::M1),
"M2" => Ok(ModeOptimizerSetting::M2),
"M3" => Ok(ModeOptimizerSetting::M3),
"Ms" => Ok(ModeOptimizerSetting::Ms),
"Mz" => Ok(ModeOptimizerSetting::Mz),
_ => Err(anyhow::anyhow!(
"Unsupported optimizer setting '{s}': expected 'M0', 'M1', 'M2', 'M3', 'Ms' or 'Mz'"
)),
}
}
}
impl Display for ModeOptimizerSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeOptimizerSetting::M0 => f.write_str("M0"),
ModeOptimizerSetting::M1 => f.write_str("M1"),
ModeOptimizerSetting::M2 => f.write_str("M2"),
ModeOptimizerSetting::M3 => f.write_str("M3"),
ModeOptimizerSetting::Ms => f.write_str("Ms"),
ModeOptimizerSetting::Mz => f.write_str("Mz"),
}
}
}
impl ModeOptimizerSetting {
/// An iterator over the available optimizer settings that we'd like to test,
/// when an explicit optimizer setting was not specified.
pub fn test_cases() -> impl Iterator<Item = ModeOptimizerSetting> + Clone {
[
// No optimizations:
ModeOptimizerSetting::M0,
// Aggressive optimizations:
ModeOptimizerSetting::M3,
]
.into_iter()
}
/// Are any optimizations enabled?
pub fn optimizations_enabled(&self) -> bool {
!matches!(self, ModeOptimizerSetting::M0)
}
}
@@ -1,6 +1,9 @@
use std::{fmt::Display, str::FromStr};
use anyhow::{Error, bail};
use semver::{Version, VersionReq};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum VersionOrRequirement {
Version(Version),
Requirement(VersionReq),
@@ -39,3 +42,26 @@ impl TryFrom<VersionOrRequirement> for VersionReq {
Ok(requirement)
}
}
impl FromStr for VersionOrRequirement {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(version) = Version::parse(s) {
Ok(Self::Version(version))
} else if let Ok(version_req) = VersionReq::parse(s) {
Ok(Self::Requirement(version_req))
} else {
bail!("Not a valid version or version requirement")
}
}
}
impl Display for VersionOrRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionOrRequirement::Version(version) => version.fmt(f),
VersionOrRequirement::Requirement(version_req) => version_req.fmt(f),
}
}
}
-4
View File
@@ -1,4 +0,0 @@
use semver::Version;
/// This is the first version of solc that supports the `--via-ir` flag / "viaIR" input JSON.
pub const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
+8 -20
View File
@@ -3,8 +3,6 @@
//! - Polkadot revive resolc compiler
//! - Polkadot revive Wasm compiler
mod constants;
use std::{
collections::HashMap,
hash::Hash,
@@ -21,9 +19,6 @@ use revive_dt_common::cached_fs::read_to_string;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
// Re-export this as it's a part of the compiler interface.
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
pub mod revive_js;
pub mod revive_resolc;
pub mod solc;
@@ -48,20 +43,13 @@ pub trait SolidityCompiler {
) -> impl Future<Output = anyhow::Result<PathBuf>>;
fn version(&self) -> anyhow::Result<Version>;
/// Does the compiler support the provided mode and version settings?
fn supports_mode(
compiler_version: &Version,
optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool;
}
/// The generic compilation input configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilerInput {
pub pipeline: Option<ModePipeline>,
pub optimization: Option<ModeOptimizerSetting>,
pub enable_optimization: Option<bool>,
pub via_ir: Option<bool>,
pub evm_version: Option<EVMVersion>,
pub allow_paths: Vec<PathBuf>,
pub base_path: Option<PathBuf>,
@@ -97,8 +85,8 @@ where
pub fn new() -> Self {
Self {
input: CompilerInput {
pipeline: Default::default(),
optimization: Default::default(),
enable_optimization: Default::default(),
via_ir: Default::default(),
evm_version: Default::default(),
allow_paths: Default::default(),
base_path: Default::default(),
@@ -110,13 +98,13 @@ where
}
}
pub fn with_optimization(mut self, value: impl Into<Option<ModeOptimizerSetting>>) -> Self {
self.input.optimization = value.into();
pub fn with_optimization(mut self, value: impl Into<Option<bool>>) -> Self {
self.input.enable_optimization = value.into();
self
}
pub fn with_pipeline(mut self, value: impl Into<Option<ModePipeline>>) -> Self {
self.input.pipeline = value.into();
pub fn with_via_ir(mut self, value: impl Into<Option<bool>>) -> Self {
self.input.via_ir = value.into();
self
}
+23 -36
View File
@@ -2,6 +2,7 @@
//! compiling contracts to PolkaVM (PVM) bytecode.
use std::{
os::unix::process::CommandExt,
path::PathBuf,
process::{Command, Stdio},
};
@@ -14,8 +15,7 @@ use revive_solc_json_interface::{
SolcStandardJsonOutput,
};
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use alloy::json_abi::JsonAbi;
use anyhow::Context;
@@ -40,8 +40,9 @@ impl SolidityCompiler for Resolc {
async fn build(
&self,
CompilerInput {
pipeline,
optimization,
enable_optimization,
// Ignored and not honored since this is required for the resolc compilation.
via_ir: _via_ir,
evm_version,
allow_paths,
base_path,
@@ -53,12 +54,6 @@ impl SolidityCompiler for Resolc {
}: CompilerInput,
additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput> {
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
anyhow::bail!(
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
);
}
let input = SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity,
sources: sources
@@ -87,9 +82,7 @@ impl SolidityCompiler for Resolc {
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
via_ir: Some(true),
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
optimization
.unwrap_or(ModeOptimizerSetting::M0)
.optimizations_enabled(),
enable_optimization.unwrap_or(false),
None,
&Version::new(0, 0, 0),
false,
@@ -100,11 +93,14 @@ impl SolidityCompiler for Resolc {
};
let mut command = AsyncCommand::new(&self.resolc_path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--standard-json");
unsafe {
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--standard-json")
.pre_exec(|| Ok(()))
};
if let Some(ref base_path) = base_path {
command.arg("--base-path").arg(base_path);
@@ -223,12 +219,15 @@ impl SolidityCompiler for Resolc {
// Logic for parsing the resolc version from the following string:
// Solidity frontend for the revive compiler version 0.3.0+commit.b238913.llvm-18.1.8
let output = Command::new(self.resolc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()?
.stdout;
let output = unsafe {
Command::new(self.resolc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.pre_exec(|| Ok(()))
.spawn()?
.wait_with_output()?
.stdout
};
let output = String::from_utf8_lossy(&output);
let version_string = output
.split("version ")
@@ -240,18 +239,6 @@ impl SolidityCompiler for Resolc {
Version::parse(version_string).map_err(Into::into)
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
// needs this to translate to LLVM IR and then RISCV.
pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
}
}
#[cfg(test)]
+39 -45
View File
@@ -2,6 +2,7 @@
//! compiling contracts to EVM bytecode.
use std::{
os::unix::process::CommandExt,
path::PathBuf,
process::{Command, Stdio},
};
@@ -10,8 +11,7 @@ use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_dt_solc_binaries::download_solc;
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use anyhow::Context;
use foundry_compilers_artifacts::{
@@ -32,12 +32,12 @@ pub struct Solc {
impl SolidityCompiler for Solc {
type Options = ();
#[tracing::instrument(level = "debug", ret)]
#[tracing::instrument(level = "info", ret)]
async fn build(
&self,
CompilerInput {
pipeline,
optimization,
enable_optimization,
via_ir,
evm_version,
allow_paths,
base_path,
@@ -47,17 +47,6 @@ impl SolidityCompiler for Solc {
}: CompilerInput,
_: Self::Options,
) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self.version()? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
// Be careful to entirely omit the viaIR field if the compiler does not support it,
// as it will error if you provide fields it does not know about. Because
// `supports_mode` is called prior to instantiating a compiler, we should never
// ask for something which is invalid.
let via_ir = match (pipeline, compiler_supports_via_ir) {
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
(_pipeline, false) => None,
};
let input = SolcInput {
language: SolcLanguage::Solidity,
sources: Sources(
@@ -68,7 +57,7 @@ impl SolidityCompiler for Solc {
),
settings: Settings {
optimizer: Optimizer {
enabled: optimization.map(|o| o.optimizations_enabled()),
enabled: enable_optimization,
details: Some(Default::default()),
..Default::default()
},
@@ -114,11 +103,14 @@ impl SolidityCompiler for Solc {
};
let mut command = AsyncCommand::new(&self.solc_path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--standard-json");
unsafe {
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--standard-json")
.pre_exec(|| Ok(()))
};
if let Some(ref base_path) = base_path {
command.arg("--base-path").arg(base_path);
@@ -132,12 +124,20 @@ impl SolidityCompiler for Solc {
.join(","),
);
}
let mut child = command.spawn()?;
let mut child = command
.spawn()
.inspect_err(|err| tracing::error!(%err, "Failed to spawn the solc command"))?;
let stdin = child.stdin.as_mut().expect("should be piped");
let serialized_input = serde_json::to_vec(&input)?;
stdin.write_all(&serialized_input).await?;
let output = child.wait_with_output().await?;
stdin
.write_all(&serialized_input)
.await
.inspect_err(|err| tracing::error!(%err, "Failed to write standard JSON to stdin"))?;
let output = child
.wait_with_output()
.await
.inspect_err(|err| tracing::error!(%err, "Failed to get the output of solc"))?;
if !output.status.success() {
let json_in = serde_json::to_string_pretty(&input)?;
@@ -174,10 +174,13 @@ impl SolidityCompiler for Solc {
let mut compiler_output = CompilerOutput::default();
for (contract_path, contracts) in parsed.contracts {
let map = compiler_output
.contracts
.entry(contract_path.canonicalize()?)
.or_default();
let map =
compiler_output
.contracts
.entry(contract_path.canonicalize().inspect_err(
|err| tracing::error!(%err, "Canonicalization of path failed"),
)?)
.or_default();
for (contract_name, contract_info) in contracts.into_iter() {
let source_code = contract_info
.evm
@@ -217,10 +220,13 @@ impl SolidityCompiler for Solc {
// Version: 0.8.30+commit.73712a01.Darwin.appleclang
// ```
let child = Command::new(self.solc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()?;
let child = unsafe {
Command::new(self.solc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.pre_exec(|| Ok(()))
.spawn()?
};
let output = child.wait_with_output()?;
let output = String::from_utf8_lossy(&output.stdout);
let version_line = output
@@ -234,18 +240,6 @@ impl SolidityCompiler for Solc {
Version::parse(version_string).map_err(Into::into)
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
pipeline == ModePipeline::ViaEVMAssembly
|| (pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
}
}
#[cfg(test)]
+4
View File
@@ -58,6 +58,10 @@ pub struct Arguments {
#[arg(long = "geth-start-timeout", default_value = "5000")]
pub geth_start_timeout: u64,
/// The test network chain ID.
#[arg(short, long = "network-id", default_value = "420420420")]
pub network_id: u64,
/// Configure nodes according to this genesis.json file.
#[arg(long = "genesis", default_value = "genesis.json")]
pub genesis_file: PathBuf,
+100 -94
View File
@@ -13,17 +13,15 @@ use alloy::{
};
use anyhow::Context;
use clap::Parser;
use futures::stream::futures_unordered::FuturesUnordered;
use futures::{Stream, StreamExt};
use futures::StreamExt;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_node_interaction::EthereumNode;
use semver::Version;
use temp_dir::TempDir;
use tokio::sync::{Mutex, RwLock, mpsc};
use tracing::{Instrument, Level};
use tracing::{Instrument, Level, instrument};
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use revive_dt_common::types::Mode;
use revive_dt_compiler::SolidityCompiler;
use revive_dt_compiler::{Compiler, CompilerOutput};
use revive_dt_config::*;
@@ -36,6 +34,7 @@ use revive_dt_format::{
corpus::Corpus,
input::{Input, Step},
metadata::{ContractInstance, ContractPathAndIdent, Metadata, MetadataFile},
mode::{Mode, SolcMode},
};
use revive_dt_node::pool::NodePool;
use revive_dt_report::reporter::{Report, Span};
@@ -45,7 +44,7 @@ static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
type CompilationCache = Arc<
RwLock<
HashMap<
(PathBuf, Mode, TestingPlatform),
(PathBuf, SolcMode, TestingPlatform),
Arc<Mutex<Option<Arc<(Version, CompilerOutput)>>>>,
>,
>,
@@ -56,8 +55,8 @@ type CompilationCache = Arc<
struct Test {
metadata: Metadata,
path: PathBuf,
mode: Mode,
case_idx: CaseIdx,
mode: SolcMode,
case_idx: usize,
case: Case,
}
@@ -125,7 +124,7 @@ fn collect_corpora(args: &Arguments) -> anyhow::Result<HashMap<Corpus, Vec<Metad
let corpus = Corpus::try_from_path(path)?;
tracing::info!("found corpus: {}", path.display());
let tests = corpus.enumerate_tests();
tracing::info!("corpus '{}' contains {} tests", &corpus.name(), tests.len());
tracing::info!("corpus '{}' contains {} tests", &corpus.name, tests.len());
corpora.insert(corpus, tests);
}
@@ -145,7 +144,7 @@ where
{
let (report_tx, report_rx) = mpsc::unbounded_channel::<(Test, CaseResult)>();
let tests = prepare_tests::<L, F>(args, metadata_files);
let tests = prepare_tests::<L, F>(metadata_files);
let driver_task = start_driver_task::<L, F>(args, tests, span, report_tx)?;
let status_reporter_task = start_reporter_task(report_rx);
@@ -154,10 +153,7 @@ where
Ok(())
}
fn prepare_tests<L, F>(
args: &Arguments,
metadata_files: &[MetadataFile],
) -> impl Stream<Item = Test>
fn prepare_tests<L, F>(metadata_files: &[MetadataFile]) -> impl Iterator<Item = Test>
where
L: Platform,
F: Platform,
@@ -176,8 +172,7 @@ where
.iter()
.enumerate()
.flat_map(move |(case_idx, case)| {
metadata
.solc_modes()
SolcMode::ALL
.into_iter()
.map(move |solc_mode| (path, metadata, case_idx, case, solc_mode))
})
@@ -230,58 +225,41 @@ where
}
None => true,
})
.flat_map(|(path, metadata, case_idx, case, solc_mode)| {
if let Some(ref case_modes) = case.modes {
case_modes
.iter()
.filter_map(Mode::as_solc_mode)
.filter(|case_mode| case_mode.matches(&solc_mode))
.map(|case_mode| (path, metadata, case_idx, case, case_mode.clone()))
.collect::<Vec<_>>()
.into_iter()
} else if let Some(ref metadata_modes) = metadata.modes {
metadata_modes
.iter()
.filter_map(Mode::as_solc_mode)
.filter(|metadata_mode| metadata_mode.matches(&solc_mode))
.map(|metadata_mode| (path, metadata, case_idx, case, metadata_mode.clone()))
.collect::<Vec<_>>()
.into_iter()
} else {
vec![(path, metadata, case_idx, case, solc_mode)].into_iter()
}
})
.map(|(metadata_file_path, metadata, case_idx, case, solc_mode)| {
Test {
metadata: metadata.clone(),
path: metadata_file_path.to_path_buf(),
mode: solc_mode,
case_idx: case_idx.into(),
case_idx,
case: case.clone(),
}
})
.map(async |test| test)
.collect::<FuturesUnordered<_>>()
.filter_map(async move |test| {
// Check that both compilers support this test, else we skip it
let is_supported = does_compiler_support_mode::<L>(args, &test.mode).await.ok().unwrap_or(false) &&
does_compiler_support_mode::<F>(args, &test.mode).await.ok().unwrap_or(false);
tracing::warn!(
metadata_file_path = %test.path.display(),
case_idx = %test.case_idx,
case_name = ?test.case.name,
mode = %test.mode,
"Skipping test as one or both of the compilers don't support it"
);
// We filter_map to avoid needing to clone `test`, but return it as-is.
if is_supported {
Some(test)
} else {
None
}
})
}
async fn does_compiler_support_mode<P: Platform>(
args: &Arguments,
mode: &Mode,
) -> anyhow::Result<bool> {
let compiler_version_or_requirement = mode.compiler_version_to_use(args.solc.clone());
let compiler_path =
P::Compiler::get_compiler_executable(args, compiler_version_or_requirement).await?;
let compiler_version = P::Compiler::new(compiler_path.clone()).version()?;
Ok(P::Compiler::supports_mode(
&compiler_version,
mode.optimize_setting,
mode.pipeline,
))
}
fn start_driver_task<L, F>(
args: &Arguments,
tests: impl Stream<Item = Test>,
tests: impl Iterator<Item = Test>,
span: Span,
report_tx: mpsc::UnboundedSender<(Test, CaseResult)>,
) -> anyhow::Result<impl Future<Output = ()>>
@@ -296,7 +274,7 @@ where
let compilation_cache = Arc::new(RwLock::new(HashMap::new()));
let number_concurrent_tasks = args.number_of_concurrent_tasks();
Ok(tests.for_each_concurrent(
Ok(futures::stream::iter(tests).for_each_concurrent(
// We want to limit the concurrent tasks here because:
//
// 1. We don't want to overwhelm the nodes with too many requests, leading to responses timing out.
@@ -320,13 +298,13 @@ where
"Running driver",
metadata_file_path = %test.path.display(),
case_idx = ?test.case_idx,
solc_mode = ?test.mode,
solc_mode = %test.mode,
);
let result = handle_case_driver::<L, F>(
&test.path,
&test.metadata,
test.case_idx,
test.case_idx.into(),
&test.case,
test.mode.clone(),
args,
@@ -351,7 +329,7 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const COLOUR_RESET: &str = "\x1B[0m";
const COLOR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m";
@@ -370,13 +348,13 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR
Ok(_inputs) => {
number_of_successes += 1;
eprintln!(
"{GREEN}Case Succeeded:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
"{GREEN}Case Succeeded:{COLOR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
);
}
Err(err) => {
number_of_failures += 1;
eprintln!(
"{RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
"{RED}Case Failed:{COLOR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
);
failures.push((test, err));
}
@@ -399,14 +377,14 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR
let test_mode = test.mode.clone();
eprintln!(
"---- {RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n"
"---- {RED}Case Failed:{COLOR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n"
);
}
}
// Summary at the end.
eprintln!(
"{} cases: {GREEN}{number_of_successes}{COLOUR_RESET} cases succeeded, {RED}{number_of_failures}{COLOUR_RESET} cases failed in {} seconds",
"{} cases: {GREEN}{number_of_successes}{COLOR_RESET} cases succeeded, {RED}{number_of_failures}{COLOR_RESET} cases failed in {} seconds",
number_of_successes + number_of_failures,
elapsed.as_secs()
);
@@ -418,7 +396,7 @@ async fn handle_case_driver<L, F>(
metadata: &Metadata,
case_idx: CaseIdx,
case: &Case,
mode: Mode,
mode: SolcMode,
config: &Arguments,
compilation_cache: CompilationCache,
leader_node: &L::Blockchain,
@@ -659,7 +637,7 @@ where
async fn get_or_build_contracts<P: Platform>(
metadata: &Metadata,
metadata_file_path: &Path,
mode: Mode,
mode: SolcMode,
config: &Arguments,
compilation_cache: CompilationCache,
deployed_libraries: &HashMap<ContractInstance, (Address, JsonAbi)>,
@@ -678,16 +656,16 @@ async fn get_or_build_contracts<P: Platform>(
}
None => {
tracing::debug!(?key, "Compiled contracts cache miss");
let compiled_contracts = compile_contracts::<P>(
metadata,
metadata_file_path,
&mode,
config,
deployed_libraries,
)
.await?;
let compiled_contracts = Arc::new(compiled_contracts);
let compiled_contracts = Arc::new(
compile_contracts::<P>(
metadata,
metadata_file_path,
&mode,
config,
deployed_libraries,
)
.await?,
);
*compilation_artifact = Some(compiled_contracts.clone());
return Ok(compiled_contracts.clone());
}
@@ -702,44 +680,59 @@ async fn get_or_build_contracts<P: Platform>(
mutex
};
let mut compilation_artifact = mutex.lock().await;
let compiled_contracts = compile_contracts::<P>(
metadata,
metadata_file_path,
&mode,
config,
deployed_libraries,
)
.await?;
let compiled_contracts = Arc::new(compiled_contracts);
let compiled_contracts = Arc::new(
compile_contracts::<P>(
metadata,
metadata_file_path,
&mode,
config,
deployed_libraries,
)
.await?,
);
*compilation_artifact = Some(compiled_contracts.clone());
Ok(compiled_contracts.clone())
}
#[instrument(
level = "info",
skip_all,
fields(
metadata_file_path = %metadata_file_path.display(),
mode = %mode,
deployed_libraries = deployed_libraries.len(),
),
err
)]
async fn compile_contracts<P: Platform>(
metadata: &Metadata,
metadata_file_path: &Path,
mode: &Mode,
mode: &SolcMode,
config: &Arguments,
deployed_libraries: &HashMap<ContractInstance, (Address, JsonAbi)>,
) -> anyhow::Result<(Version, CompilerOutput)> {
let compiler_version_or_requirement = mode.compiler_version_to_use(config.solc.clone());
let compiler_path =
P::Compiler::get_compiler_executable(config, compiler_version_or_requirement).await?;
let compiler_version = P::Compiler::new(compiler_path.clone()).version()?;
let compiler_version = P::Compiler::new(compiler_path.clone())
.version()
.inspect_err(|err| tracing::error!(%err, "Failed to get compiler version"))?;
tracing::info!(
%compiler_version,
metadata_file_path = %metadata_file_path.display(),
mode = ?mode,
mode = %mode,
"Compiling contracts"
);
let compiler = Compiler::<P::Compiler>::new()
.with_allow_path(metadata.directory()?)
.with_optimization(mode.optimize_setting)
.with_pipeline(mode.pipeline);
.with_allow_path(
metadata
.directory()
.inspect_err(|err| tracing::error!(%err, "Failed to get the metadata directory"))?,
)
.with_optimization(mode.optimize)
.with_via_ir(mode.via_ir);
let mut compiler = metadata
.files_to_compile()?
.try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
@@ -756,7 +749,10 @@ async fn compile_contracts<P: Platform>(
// 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()?)
compiler =
FilesWithExtensionIterator::new(metadata.directory().inspect_err(
|err| tracing::error!(%err, "Failed to get the metadata directory"),
)?)
.with_allowed_extension("sol")
.with_use_cached_fs(true)
.fold(compiler, |compiler, path| {
@@ -764,7 +760,17 @@ async fn compile_contracts<P: Platform>(
});
}
let compiler_output = compiler.try_build(compiler_path).await?;
let compiler_output = compiler
.try_build(compiler_path)
.await
.inspect_err(|err| tracing::error!(%err, "Contract compilation failed"))?;
tracing::info!(
%compiler_version,
metadata_file_path = %metadata_file_path.display(),
mode = %mode,
"Compiled contracts"
);
Ok((compiler_version, compiler_output))
}
-1
View File
@@ -17,7 +17,6 @@ alloy = { workspace = true }
alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
anyhow = { workspace = true }
regex = { workspace = true }
tracing = { workspace = true }
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+11 -10
View File
@@ -1,10 +1,13 @@
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type;
use crate::{
input::{Expected, Step},
mode::ParsedMode,
metadata::{deserialize_compilation_modes, serialize_compilation_modes},
mode::Mode,
};
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
@@ -15,8 +18,13 @@ pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modes: Option<Vec<ParsedMode>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_compilation_modes",
serialize_with = "serialize_compilation_modes"
)]
pub modes: Option<HashSet<Mode>>,
#[serde(rename = "inputs")]
pub steps: Vec<Step>,
@@ -32,7 +40,6 @@ pub struct Case {
}
impl Case {
#[allow(irrefutable_let_patterns)]
pub fn steps_iterator(&self) -> impl Iterator<Item = Step> {
let steps_len = self.steps.len();
self.steps
@@ -67,9 +74,3 @@ define_wrapper_type!(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CaseIdx(usize);
);
impl std::fmt::Display for CaseIdx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
+25 -56
View File
@@ -8,74 +8,43 @@ use serde::{Deserialize, Serialize};
use crate::metadata::MetadataFile;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Corpus {
SinglePath { name: String, path: PathBuf },
MultiplePaths { name: String, paths: Vec<PathBuf> },
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct Corpus {
pub name: String,
pub path: PathBuf,
}
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)
.and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?;
/// Try to read and parse the corpus definition file at given `path`.
pub fn try_from_path(path: &Path) -> anyhow::Result<Self> {
let file = File::open(path)?;
let mut corpus: Corpus = serde_json::from_reader(file)?;
for path in corpus.paths_iter_mut() {
*path = file_path
.as_ref()
.parent()
.ok_or_else(|| {
anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display())
})?
.canonicalize()
.map_err(|error| {
anyhow::anyhow!(
"Failed to canonicalize path to corpus '{}': {error}",
path.display()
)
})?
.join(path.as_path())
}
// Ensure that the path mentioned in the corpus is relative to the corpus file.
// Canonicalizing also helps make the path in any errors unambiguous.
corpus.path = path
.parent()
.ok_or_else(|| {
anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display())
})?
.canonicalize()
.map_err(|error| {
anyhow::anyhow!(
"Failed to canonicalize path to corpus '{}': {error}",
path.display()
)
})?
.join(corpus.path);
Ok(corpus)
}
/// Scan the corpus base directory and return all tests found.
pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
let mut tests = Vec::new();
for path in self.paths_iter() {
collect_metadata(path, &mut tests);
}
collect_metadata(&self.path, &mut tests);
tests
}
pub fn name(&self) -> &str {
match self {
Corpus::SinglePath { name, .. } | Corpus::MultiplePaths { name, .. } => name.as_str(),
}
}
pub fn paths_iter(&self) -> impl Iterator<Item = &Path> {
match self {
Corpus::SinglePath { path, .. } => {
Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>
}
Corpus::MultiplePaths { paths, .. } => {
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>
}
}
}
pub fn paths_iter_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> {
match self {
Corpus::SinglePath { path, .. } => {
Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>>
}
Corpus::MultiplePaths { paths, .. } => {
Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>>
}
}
}
}
/// Recursively walks `path` and parses any JSON or Solidity file into a test
+59 -12
View File
@@ -1,6 +1,6 @@
use std::{
cmp::Ordering,
collections::BTreeMap,
collections::{BTreeMap, HashSet},
fmt::Display,
fs::File,
ops::Deref,
@@ -8,15 +8,17 @@ use std::{
str::FromStr,
};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use revive_common::EVMVersion;
use revive_dt_common::{
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
types::Mode,
};
use crate::{case::Case, mode::ParsedMode};
use crate::{
case::Case,
mode::{Mode, SolcMode},
};
pub const METADATA_FILE_EXTENSION: &str = "json";
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
@@ -65,8 +67,13 @@ pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modes: Option<Vec<ParsedMode>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_compilation_modes",
serialize_with = "serialize_compilation_modes"
)]
pub modes: Option<HashSet<Mode>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<PathBuf>,
@@ -84,12 +91,21 @@ pub struct Metadata {
}
impl Metadata {
/// Returns the modes that we should test from this metadata.
pub fn solc_modes(&self) -> Vec<Mode> {
match &self.modes {
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
None => Mode::all().collect(),
}
/// Returns the solc modes of this metadata, inserting a default mode if not present.
pub fn solc_modes(&self) -> Vec<SolcMode> {
self.modes
.to_owned()
.unwrap_or_else(|| SolcMode::ALL.map(Mode::Solidity).iter().cloned().collect())
.iter()
.filter_map(|mode| match mode {
Mode::Solidity(solc_mode) => Some(solc_mode),
Mode::Unknown(mode) => {
tracing::debug!("compiler: ignoring unknown mode '{mode}'");
None
}
})
.cloned()
.collect()
}
/// Returns the base directory of this metadata.
@@ -258,6 +274,37 @@ impl Metadata {
}
}
pub fn deserialize_compilation_modes<'de, D>(
deserializer: D,
) -> Result<Option<HashSet<Mode>>, D::Error>
where
D: Deserializer<'de>,
{
let maybe_strings = Option::<Vec<String>>::deserialize(deserializer)?;
Ok(maybe_strings.map(|strings| {
strings
.into_iter()
.flat_map(Mode::parse_from_string)
.collect()
}))
}
pub fn serialize_compilation_modes<S>(
value: &Option<HashSet<Mode>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
None => serializer.serialize_none(),
Some(modes) => {
let strings: Vec<String> = modes.iter().cloned().map(Into::<String>::into).collect();
serializer.serialize_some(&strings)
}
}
}
define_wrapper_type!(
/// Represents a contract instance found a metadata file.
///
+352 -221
View File
@@ -1,261 +1,392 @@
use regex::Regex;
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
use std::{fmt::Display, str::FromStr};
/// This represents a mode that has been parsed from test metadata.
use revive_dt_common::types::VersionOrRequirement;
use semver::Version;
use serde::Serialize;
/// Specifies a compilation mode for the test artifact that it requires. This is used as a filter
/// when used in the [`Metadata`] and is used as a directive when used in the core crate.
///
/// Mode strings can take the following form (in pseudo-regex):
///
/// ```text
/// [YEILV][+-]? (M[0123sz])? <semver>?
/// ```
///
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(try_from = "String", into = "String")]
pub struct ParsedMode {
pub pipeline: Option<ModePipeline>,
pub optimize_flag: Option<bool>,
pub optimize_setting: Option<ModeOptimizerSetting>,
pub version: Option<semver::VersionReq>,
/// [`Metadata`]: crate::metadata::Metadata
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Mode {
/// A compilation mode that's been parsed from a String and into its contents.
Solidity(SolcMode),
/// An unknown compilation mode.
Unknown(String),
}
impl FromStr for ParsedMode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?x)
^
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
\s*
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
\s*
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
$
").unwrap()
});
let Some(caps) = REGEX.captures(s) else {
anyhow::bail!("Cannot parse mode '{s}' from string");
};
let pipeline = match caps.name("pipeline") {
Some(m) => Some(ModePipeline::from_str(m.as_str())?),
None => None,
};
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
let optimize_setting = match caps.name("optimize_setting") {
Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?),
None => None,
};
let version = match caps.name("version") {
Some(m) => Some(semver::VersionReq::parse(m.as_str()).map_err(|e| {
anyhow::anyhow!("Cannot parse the version requirement '{}': {e}", m.as_str())
})?),
None => None,
};
Ok(ParsedMode {
pipeline,
optimize_flag,
optimize_setting,
version,
})
impl From<Mode> for String {
fn from(value: Mode) -> Self {
value.to_string()
}
}
impl Display for ParsedMode {
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut has_written = false;
match self {
Mode::Solidity(mode) => mode.fmt(f),
Mode::Unknown(string) => string.fmt(f),
}
}
}
if let Some(pipeline) = self.pipeline {
pipeline.fmt(f)?;
if let Some(optimize_flag) = self.optimize_flag {
f.write_str(if optimize_flag { "+" } else { "-" })?;
impl Mode {
pub fn parse_from_string(str: impl AsRef<str>) -> Vec<Self> {
let mut chars = str.as_ref().chars().peekable();
let compile_via_ir = match chars.next() {
Some('Y') => true,
Some('E') => false,
_ => {
tracing::warn!("Encountered an unknown mode {}", str.as_ref());
return vec![Self::Unknown(str.as_ref().to_string())];
}
has_written = true;
};
let optimize_flag = match chars.peek() {
Some('+') => {
let _ = chars.next();
Some(true)
}
Some('-') => {
let _ = chars.next();
Some(false)
}
_ => None,
};
let mut chars = chars.skip_while(|char| *char == ' ').peekable();
let version_requirement = match chars.peek() {
Some('=' | '>' | '<' | '~' | '^' | '*' | '0'..='9') => {
let version_requirement = chars.take_while(|char| *char != ' ').collect::<String>();
let Ok(version_requirement) = VersionOrRequirement::from_str(&version_requirement)
else {
return vec![Self::Unknown(str.as_ref().to_string())];
};
Some(version_requirement)
}
_ => None,
};
match optimize_flag {
Some(flag) => {
vec![Self::Solidity(SolcMode {
via_ir: compile_via_ir,
optimize: flag,
compiler_version_requirement: version_requirement,
})]
}
None => {
vec![
Self::Solidity(SolcMode {
via_ir: compile_via_ir,
optimize: true,
compiler_version_requirement: version_requirement.clone(),
}),
Self::Solidity(SolcMode {
via_ir: compile_via_ir,
optimize: false,
compiler_version_requirement: version_requirement,
}),
]
}
}
}
pub fn matches(&self, other: &Self) -> bool {
match (self, other) {
(
Mode::Solidity(SolcMode {
via_ir: self_via_ir,
optimize: self_optimize,
compiler_version_requirement: self_compiler_version_requirement,
}),
Mode::Solidity(SolcMode {
via_ir: other_via_ir,
optimize: other_optimize,
compiler_version_requirement: other_compiler_version_requirement,
}),
) => {
let mut matches = true;
matches &= self_via_ir == other_via_ir;
matches &= self_optimize == other_optimize;
match (
self_compiler_version_requirement,
other_compiler_version_requirement,
) {
(
Some(VersionOrRequirement::Version(self_version)),
Some(VersionOrRequirement::Version(other_version)),
) => {
matches &= self_version == other_version;
}
(
Some(VersionOrRequirement::Version(version)),
Some(VersionOrRequirement::Requirement(requirement)),
)
| (
Some(VersionOrRequirement::Requirement(requirement)),
Some(VersionOrRequirement::Version(version)),
) => matches &= requirement.matches(version),
(
Some(VersionOrRequirement::Requirement(..)),
Some(VersionOrRequirement::Requirement(..)),
) => matches = false,
(Some(_), None) | (None, Some(_)) | (None, None) => {}
}
matches
}
(Mode::Solidity { .. }, Mode::Unknown(_))
| (Mode::Unknown(_), Mode::Solidity { .. })
| (Mode::Unknown(_), Mode::Unknown(_)) => false,
}
}
pub fn as_solc_mode(&self) -> Option<&SolcMode> {
if let Self::Solidity(mode) = self {
Some(mode)
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(into = "String")]
pub struct SolcMode {
pub via_ir: bool,
pub optimize: bool,
pub compiler_version_requirement: Option<VersionOrRequirement>,
}
impl SolcMode {
pub const ALL: [Self; 4] = [
SolcMode {
via_ir: false,
optimize: false,
compiler_version_requirement: None,
},
SolcMode {
via_ir: false,
optimize: true,
compiler_version_requirement: None,
},
SolcMode {
via_ir: true,
optimize: false,
compiler_version_requirement: None,
},
SolcMode {
via_ir: true,
optimize: true,
compiler_version_requirement: None,
},
];
pub fn matches(&self, other: &Self) -> bool {
Mode::Solidity(self.clone()).matches(&Mode::Solidity(other.clone()))
}
/// Resolves the [`SolcMode`]'s solidity version requirement into a [`VersionOrRequirement`] if
/// the requirement is present on the object. Otherwise, the passed default version is used.
pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement {
match self.compiler_version_requirement {
Some(ref requirement) => requirement.clone(),
None => default.into(),
}
}
}
impl Display for SolcMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
via_ir,
optimize,
compiler_version_requirement,
} = self;
if *via_ir {
write!(f, "Y")?;
} else {
write!(f, "E")?;
}
if let Some(optimize_setting) = self.optimize_setting {
if has_written {
f.write_str(" ")?;
}
optimize_setting.fmt(f)?;
has_written = true;
if *optimize {
write!(f, "+")?;
} else {
write!(f, "-")?;
}
if let Some(version) = &self.version {
if has_written {
f.write_str(" ")?;
}
version.fmt(f)?;
if let Some(req) = compiler_version_requirement {
write!(f, " {req}")?;
}
Ok(())
}
}
impl From<ParsedMode> for String {
fn from(parsed_mode: ParsedMode) -> Self {
parsed_mode.to_string()
}
}
impl TryFrom<String> for ParsedMode {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
ParsedMode::from_str(&value)
}
}
impl ParsedMode {
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|| EitherIter::A(ModePipeline::test_cases()),
|p| EitherIter::B(std::iter::once(*p)),
);
let optimize_flag_setting = self.optimize_flag.map(|flag| {
if flag {
ModeOptimizerSetting::M3
} else {
ModeOptimizerSetting::M0
}
});
let optimize_flag_iter = match optimize_flag_setting {
Some(setting) => EitherIter::A(std::iter::once(setting)),
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
};
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|| EitherIter::A(optimize_flag_iter),
|s| EitherIter::B(std::iter::once(*s)),
);
pipeline_iter.flat_map(move |pipeline| {
optimize_settings_iter
.clone()
.map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: self.version.clone(),
})
})
}
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
/// This avoids any duplicate entries.
pub fn many_to_modes<'a>(
parsed: impl Iterator<Item = &'a ParsedMode>,
) -> impl Iterator<Item = Mode> {
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
modes.into_iter()
}
}
/// An iterator that could be either of two iterators.
#[derive(Clone, Debug)]
enum EitherIter<A, B> {
A(A),
B(B),
}
impl<A, B> Iterator for EitherIter<A, B>
where
A: Iterator,
B: Iterator<Item = A::Item>,
{
type Item = A::Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
EitherIter::A(iter) => iter.next(),
EitherIter::B(iter) => iter.next(),
}
impl From<SolcMode> for String {
fn from(value: SolcMode) -> Self {
value.to_string()
}
}
#[cfg(test)]
mod tests {
mod test {
use semver::Version;
use super::*;
#[test]
fn test_parsed_mode_from_str() {
let strings = vec![
("Mz", "Mz"),
("Y", "Y"),
("Y+", "Y+"),
("Y-", "Y-"),
("E", "E"),
("E+", "E+"),
("E-", "E-"),
("Y M0", "Y M0"),
("Y M1", "Y M1"),
("Y M2", "Y M2"),
("Y M3", "Y M3"),
("Y Ms", "Y Ms"),
("Y Mz", "Y Mz"),
("E M0", "E M0"),
("E M1", "E M1"),
("E M2", "E M2"),
("E M3", "E M3"),
("E Ms", "E Ms"),
("E Mz", "E Mz"),
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
("Y 0.8.0", "Y ^0.8.0"),
("E+ 0.8.0", "E+ ^0.8.0"),
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
("E Mz <0.7.0", "E Mz <0.7.0"),
// We can parse +- _and_ M1/M2 but the latter takes priority.
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
("E- M2 0.7.0", "E- M2 ^0.7.0"),
// We don't see this in the wild but it is parsed.
("<=0.8", "<=0.8"),
fn mode_can_be_parsed_as_expected() {
// Arrange
let fixtures = [
(
"Y",
vec![
Mode::Solidity(SolcMode {
via_ir: true,
optimize: true,
compiler_version_requirement: None,
}),
Mode::Solidity(SolcMode {
via_ir: true,
optimize: false,
compiler_version_requirement: None,
}),
],
),
(
"Y+",
vec![Mode::Solidity(SolcMode {
via_ir: true,
optimize: true,
compiler_version_requirement: None,
})],
),
(
"Y-",
vec![Mode::Solidity(SolcMode {
via_ir: true,
optimize: false,
compiler_version_requirement: None,
})],
),
(
"E",
vec![
Mode::Solidity(SolcMode {
via_ir: false,
optimize: true,
compiler_version_requirement: None,
}),
Mode::Solidity(SolcMode {
via_ir: false,
optimize: false,
compiler_version_requirement: None,
}),
],
),
(
"E+",
vec![Mode::Solidity(SolcMode {
via_ir: false,
optimize: true,
compiler_version_requirement: None,
})],
),
(
"E-",
vec![Mode::Solidity(SolcMode {
via_ir: false,
optimize: false,
compiler_version_requirement: None,
})],
),
(
"Y >=0.8.3",
vec![
Mode::Solidity(SolcMode {
via_ir: true,
optimize: true,
compiler_version_requirement: Some(VersionOrRequirement::Requirement(
">=0.8.3".parse().unwrap(),
)),
}),
Mode::Solidity(SolcMode {
via_ir: true,
optimize: false,
compiler_version_requirement: Some(VersionOrRequirement::Requirement(
">=0.8.3".parse().unwrap(),
)),
}),
],
),
(
"Y 0.8.3",
vec![
Mode::Solidity(SolcMode {
via_ir: true,
optimize: true,
compiler_version_requirement: Some(VersionOrRequirement::Version(
Version {
major: 0,
minor: 8,
patch: 3,
pre: Default::default(),
build: Default::default(),
},
)),
}),
Mode::Solidity(SolcMode {
via_ir: true,
optimize: false,
compiler_version_requirement: Some(VersionOrRequirement::Version(
Version {
major: 0,
minor: 8,
patch: 3,
pre: Default::default(),
build: Default::default(),
},
)),
}),
],
),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.expect(format!("Failed to parse mode string '{actual}'").as_str());
for (string, expectation) in fixtures {
// Act
let actual = Mode::parse_from_string(string);
// Assert
assert_eq!(
expected,
parsed.to_string(),
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
);
actual, expectation,
"Parsed {string} into {actual:?} but expected {expectation:?}"
)
}
}
#[test]
fn test_parsed_mode_to_test_modes() {
let strings = vec![
("Mz", vec!["Y Mz", "E Mz"]),
("Y", vec!["Y M0", "Y M3"]),
("E", vec!["E M0", "E M3"]),
("Y+", vec!["Y M3"]),
("Y-", vec!["Y M0"]),
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
(
"<=0.8",
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
),
];
#[allow(clippy::uninlined_format_args)]
fn mode_matches_as_expected() {
// Arrange
let fixtures = [("Y+", "Y+", true), ("Y+ >=0.8.3", "Y+", true)];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.expect(format!("Failed to parse mode string '{actual}'").as_str());
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();
for (self_mode, other_mode, expected_result) in fixtures {
let self_mode = Mode::parse_from_string(self_mode).pop().unwrap();
let other_mode = Mode::parse_from_string(other_mode).pop().unwrap();
// Act
let actual = self_mode.matches(&other_mode);
// Assert
assert_eq!(
expected_set, actual_set,
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
actual, expected_result,
"Match of {} and {} failed. Expected {} but got {}",
self_mode, other_mode, expected_result, actual
);
}
}
+25 -26
View File
@@ -33,7 +33,7 @@ use alloy::{
};
use anyhow::Context;
use revive_common::EVMVersion;
use tracing::{Instrument, Level};
use tracing::Instrument;
use revive_dt_common::{fs::clear_directory, futures::poll};
use revive_dt_config::Arguments;
@@ -48,7 +48,7 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
///
/// Implements helpers to initialize, spawn and wait the node.
///
/// Assumes dev mode and IPC only (`P2P`, `http`` etc. are kept disabled).
/// Assumes dev mode and IPC only (`P2P`, `http` etc. are kept disabled).
///
/// Prunes the child process and the base directory on drop.
#[derive(Debug)]
@@ -60,6 +60,7 @@ pub struct GethNode {
geth: PathBuf,
id: u32,
handle: Option<Child>,
network_id: u64,
start_timeout: u64,
wallet: EthereumWallet,
nonce_manager: CachedNonceManager,
@@ -164,6 +165,8 @@ impl GethNode {
.arg(&self.data_directory)
.arg("--ipcpath")
.arg(&self.connection_string)
.arg("--networkid")
.arg(self.network_id.to_string())
.arg("--nodiscover")
.arg("--maxpeers")
.arg("0")
@@ -210,7 +213,6 @@ impl GethNode {
let maximum_wait_time = Duration::from_millis(self.start_timeout);
let mut stderr = BufReader::new(logs_file).lines();
let mut lines = vec![];
loop {
if let Some(Ok(line)) = stderr.next() {
if line.contains(Self::ERROR_MARKER) {
@@ -219,24 +221,19 @@ impl GethNode {
if line.contains(Self::READY_MARKER) {
return Ok(self);
}
lines.push(line);
}
if Instant::now().duration_since(start_time) > maximum_wait_time {
anyhow::bail!(
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
self.start_timeout,
lines.join("\n")
);
anyhow::bail!("Timeout in starting geth");
}
}
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = Level::TRACE)]
#[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = "trace")]
fn geth_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = Level::TRACE)]
#[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = "trace")]
fn geth_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME)
}
@@ -262,7 +259,7 @@ impl GethNode {
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
100_000_000_000,
1_000_000_000,
))
.filler(ChainIdFiller::default())
@@ -271,6 +268,7 @@ impl GethNode {
.connect(&connection_string)
.await
.map_err(Into::into)
.inspect_err(|err| tracing::error!(%err, "Failed to create the alloy provider"))
})
}
}
@@ -284,27 +282,26 @@ impl EthereumNode for GethNode {
let span = tracing::debug_span!("Submitting transaction", ?transaction);
let _guard = span.enter();
let provider = Arc::new(self.provider().await?);
let provider = self.provider().await.map(Arc::new)?;
let transaction_hash = *provider.send_transaction(transaction).await?.tx_hash();
// The following is a fix for the "transaction indexing is in progress" error that we
// used to get. You can find more information on this in the following GH issue in geth
// The following is a fix for the "transaction indexing is in progress" error that we used
// to get. You can find more information on this in the following GH issue in geth
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
// before we can get the receipt of the transaction it needs to have been indexed by the
// node's indexer. Just because the transaction has been confirmed it doesn't mean that
// it has been indexed. When we call alloy's `get_receipt` it checks if the transaction
// was confirmed. If it has been, then it will call `eth_getTransactionReceipt` method
// which _might_ return the above error if the tx has not yet been indexed yet. So, we
// need to implement a retry mechanism for the receipt to keep retrying to get it until
// it eventually works, but we only do that if the error we get back is the "transaction
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to
// implement a retry mechanism for the receipt to keep retrying to get it until it
// eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None.
//
// Getting the transaction indexed and taking a receipt can take a long time especially
// when a lot of transactions are being submitted to the node. Thus, while initially we
// only allowed for 60 seconds of waiting with a 1 second delay in polling, we need to
// allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
// with exponential backoff each time we attempt to get the receipt and find that it's
// not available.
// when a lot of transactions are being submitted to the node. Thus, while initially we only
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
// backoff each time we attempt to get the receipt and find that it's not available.
poll(
Self::RECEIPT_POLLING_DURATION,
Default::default(),
@@ -330,6 +327,7 @@ impl EthereumNode for GethNode {
?transaction_hash
))
.await
.inspect(|receipt| tracing::info!(gas_used = receipt.gas_used, "Gas used on transaction"))
}
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
@@ -524,6 +522,7 @@ impl Node for GethNode {
geth: config.geth.clone(),
id,
handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
wallet,
// We know that we only need to be storing 2 files so we can specify that when creating
+3 -3
View File
@@ -367,9 +367,9 @@ impl KitchensinkNode {
.disable_recommended_fillers()
.network::<KitchenSinkNetwork>()
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
1_000_000_000,
30_000_000,
200_000_000_000,
3_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
-1
View File
@@ -8,7 +8,6 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true }
+3 -4
View File
@@ -12,12 +12,11 @@ use std::{
};
use anyhow::Context;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use serde::Serialize;
use revive_dt_common::types::Mode;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use revive_dt_config::{Arguments, TestingPlatform};
use revive_dt_format::corpus::Corpus;
use revive_dt_format::{corpus::Corpus, mode::SolcMode};
use crate::analyzer::CompilerStatistics;
@@ -49,7 +48,7 @@ pub struct CompilationTask {
/// The observed compiler output.
pub json_output: Option<CompilerOutput>,
/// The observed compiler mode.
pub mode: Mode,
pub mode: SolcMode,
/// The observed compiler version.
pub compiler_version: String,
/// The observed error, if any.