mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-22 17:17:58 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dde74af22b | |||
| 453fcca4d6 | |||
| 31f69de3cb | |||
| f36a132e05 | |||
| a5a27957d5 | |||
| 680cc5c174 | |||
| 90214f1afe | |||
| b69aab903e | |||
| af1eba0ccf | |||
| 4b641b947b |
+1
-3
@@ -10,6 +10,4 @@ node_modules
|
||||
|
||||
profile.json.gz
|
||||
resolc-compiler-tests
|
||||
workdir
|
||||
|
||||
!/schema.json
|
||||
workdir
|
||||
Generated
+3
-32
@@ -4501,12 +4501,9 @@ name = "revive-dt-config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"anyhow",
|
||||
"clap",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"temp-dir",
|
||||
]
|
||||
|
||||
@@ -4528,10 +4525,10 @@ dependencies = [
|
||||
"revive-dt-node",
|
||||
"revive-dt-node-interaction",
|
||||
"revive-dt-report",
|
||||
"schemars 1.0.4",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"temp-dir",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
@@ -4550,7 +4547,6 @@ dependencies = [
|
||||
"regex",
|
||||
"revive-common",
|
||||
"revive-dt-common",
|
||||
"schemars 1.0.4",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4874,24 +4870,10 @@ checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schnellru"
|
||||
version = "0.2.4"
|
||||
@@ -5076,17 +5058,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
@@ -5719,9 +5690,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
@@ -37,7 +37,6 @@ moka = "0.12.10"
|
||||
paste = "1.0.15"
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
once_cell = "1.21"
|
||||
schemars = { version = "1.0.4", features = ["semver1"] }
|
||||
semver = { version = "1.0", features = ["serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0", default-features = false, features = [
|
||||
@@ -49,7 +48,6 @@ serde_with = { version = "3.14.0" }
|
||||
sha2 = { version = "0.10.9" }
|
||||
sp-core = "36.1.0"
|
||||
sp-runtime = "41.1.0"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
temp-dir = { version = "0.1.16" }
|
||||
tempfile = "3.3"
|
||||
thiserror = "2"
|
||||
|
||||
@@ -187,11 +187,10 @@ The above corpus file instructs the tool to look for all of the test cases conta
|
||||
The simplest command to run this tool is the following:
|
||||
|
||||
```bash
|
||||
RUST_LOG="info" cargo run --release -- execute-tests \
|
||||
--follower geth \
|
||||
RUST_LOG="info" cargo run --release -- \
|
||||
--corpus path_to_your_corpus_file.json \
|
||||
--working-directory path_to_a_temporary_directory_to_cache_things_in \
|
||||
--concurrency.number-of-nodes 5 \
|
||||
--workdir path_to_a_temporary_directory_to_cache_things_in \
|
||||
--number-of-nodes 5 \
|
||||
> logs.log \
|
||||
2> output.log
|
||||
```
|
||||
|
||||
@@ -11,14 +11,14 @@ use std::{
|
||||
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use alloy_primitives::Address;
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_common::cached_fs::read_to_string;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||
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};
|
||||
@@ -31,13 +31,11 @@ pub mod solc;
|
||||
pub trait SolidityCompiler: Sized {
|
||||
/// Instantiates a new compiler object.
|
||||
///
|
||||
/// Based on the given [`Context`] and [`VersionOrRequirement`] this function instantiates a
|
||||
/// Based on the given [`Arguments`] and [`VersionOrRequirement`] this function instantiates a
|
||||
/// new compiler object. Certain implementations of this trait might choose to cache cache the
|
||||
/// compiler objects and return the same ones over and over again.
|
||||
fn new(
|
||||
context: impl AsRef<SolcConfiguration>
|
||||
+ AsRef<ResolcConfiguration>
|
||||
+ AsRef<WorkingDirectoryConfiguration>,
|
||||
config: &Arguments,
|
||||
version: impl Into<Option<VersionOrRequirement>>,
|
||||
) -> impl Future<Output = Result<Self>>;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
|
||||
use dashmap::DashMap;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_solc_json_interface::{
|
||||
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use semver::Version;
|
||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||
|
||||
@@ -39,9 +39,7 @@ struct ResolcInner {
|
||||
|
||||
impl SolidityCompiler for Resolc {
|
||||
async fn new(
|
||||
context: impl AsRef<SolcConfiguration>
|
||||
+ AsRef<ResolcConfiguration>
|
||||
+ AsRef<WorkingDirectoryConfiguration>,
|
||||
config: &Arguments,
|
||||
version: impl Into<Option<VersionOrRequirement>>,
|
||||
) -> Result<Self> {
|
||||
/// This is a cache of all of the resolc compiler objects. Since we do not currently support
|
||||
@@ -49,9 +47,7 @@ impl SolidityCompiler for Resolc {
|
||||
/// its version to the resolc compiler.
|
||||
static COMPILERS_CACHE: LazyLock<DashMap<Solc, Resolc>> = LazyLock::new(Default::default);
|
||||
|
||||
let resolc_configuration = AsRef::<ResolcConfiguration>::as_ref(&context);
|
||||
|
||||
let solc = Solc::new(&context, version)
|
||||
let solc = Solc::new(config, version)
|
||||
.await
|
||||
.context("Failed to create the solc compiler frontend for resolc")?;
|
||||
|
||||
@@ -60,7 +56,7 @@ impl SolidityCompiler for Resolc {
|
||||
.or_insert_with(|| {
|
||||
Self(Arc::new(ResolcInner {
|
||||
solc,
|
||||
resolc_path: resolc_configuration.path.clone(),
|
||||
resolc_path: config.resolc.clone(),
|
||||
}))
|
||||
})
|
||||
.clone())
|
||||
|
||||
@@ -9,12 +9,12 @@ use std::{
|
||||
|
||||
use dashmap::DashMap;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_solc_binaries::download_solc;
|
||||
|
||||
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use foundry_compilers_artifacts::{
|
||||
output_selection::{
|
||||
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
|
||||
@@ -38,35 +38,25 @@ struct SolcInner {
|
||||
|
||||
impl SolidityCompiler for Solc {
|
||||
async fn new(
|
||||
context: impl AsRef<SolcConfiguration>
|
||||
+ AsRef<ResolcConfiguration>
|
||||
+ AsRef<WorkingDirectoryConfiguration>,
|
||||
config: &Arguments,
|
||||
version: impl Into<Option<VersionOrRequirement>>,
|
||||
) -> Result<Self> {
|
||||
// This is a cache for the compiler objects so that whenever the same compiler version is
|
||||
// requested the same object is returned. We do this as we do not want to keep cloning the
|
||||
// compiler around.
|
||||
static COMPILERS_CACHE: LazyLock<DashMap<(PathBuf, Version), Solc>> =
|
||||
LazyLock::new(Default::default);
|
||||
|
||||
let working_directory_configuration =
|
||||
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
|
||||
let solc_configuration = AsRef::<SolcConfiguration>::as_ref(&context);
|
||||
static COMPILERS_CACHE: LazyLock<DashMap<Version, Solc>> = LazyLock::new(Default::default);
|
||||
|
||||
// We attempt to download the solc binary. Note the following: this call does the version
|
||||
// resolution for us. Therefore, even if the download didn't proceed, this function will
|
||||
// resolve the version requirement into a canonical version of the compiler. It's then up
|
||||
// to us to either use the provided path or not.
|
||||
let version = version
|
||||
.into()
|
||||
.unwrap_or_else(|| solc_configuration.version.clone().into());
|
||||
let (version, path) =
|
||||
download_solc(working_directory_configuration.as_path(), version, false)
|
||||
.await
|
||||
.context("Failed to download/get path to solc binary")?;
|
||||
let version = version.into().unwrap_or_else(|| config.solc.clone().into());
|
||||
let (version, path) = download_solc(config.directory(), version, false)
|
||||
.await
|
||||
.context("Failed to download/get path to solc binary")?;
|
||||
|
||||
Ok(COMPILERS_CACHE
|
||||
.entry((path.clone(), version.clone()))
|
||||
.entry(version.clone())
|
||||
.or_insert_with(|| {
|
||||
Self(Arc::new(SolcInner {
|
||||
solc_path: path,
|
||||
|
||||
@@ -2,13 +2,13 @@ use std::path::PathBuf;
|
||||
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_compiler::{Compiler, SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
||||
use revive_dt_config::ExecutionContext;
|
||||
use revive_dt_config::Arguments;
|
||||
use semver::Version;
|
||||
|
||||
#[tokio::test]
|
||||
async fn contracts_can_be_compiled_with_solc() {
|
||||
// Arrange
|
||||
let args = ExecutionContext::default();
|
||||
let args = Arguments::default();
|
||||
let solc = Solc::new(&args, VersionOrRequirement::Version(Version::new(0, 8, 30)))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -49,7 +49,7 @@ async fn contracts_can_be_compiled_with_solc() {
|
||||
#[tokio::test]
|
||||
async fn contracts_can_be_compiled_with_resolc() {
|
||||
// Arrange
|
||||
let args = ExecutionContext::default();
|
||||
let args = Arguments::default();
|
||||
let resolc = Resolc::new(&args, VersionOrRequirement::Version(Version::new(0, 8, 30)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -10,13 +10,10 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
temp-dir = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+134
-484
@@ -2,565 +2,215 @@
|
||||
|
||||
use std::{
|
||||
fmt::Display,
|
||||
fs::read_to_string,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock, OnceLock},
|
||||
time::Duration,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
genesis::Genesis,
|
||||
hex::ToHexExt,
|
||||
network::EthereumWallet,
|
||||
primitives::{FixedBytes, U256},
|
||||
signers::local::PrivateKeySigner,
|
||||
};
|
||||
use clap::{Parser, ValueEnum, ValueHint};
|
||||
use alloy::{network::EthereumWallet, signers::local::PrivateKeySigner};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use semver::Version;
|
||||
use serde::{Serialize, Serializer};
|
||||
use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use temp_dir::TempDir;
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
#[derive(Debug, Parser, Clone, Serialize, Deserialize)]
|
||||
#[command(name = "retester")]
|
||||
pub enum Context {
|
||||
/// Executes tests in the MatterLabs format differentially against a leader and a follower.
|
||||
ExecuteTests(Box<ExecutionContext>),
|
||||
/// Exports the JSON schema of the MatterLabs test format used by the tool.
|
||||
ExportJsonSchema,
|
||||
}
|
||||
pub struct Arguments {
|
||||
/// The `solc` version to use if the test didn't specify it explicitly.
|
||||
#[arg(long = "solc", short, default_value = "0.8.29")]
|
||||
pub solc: Version,
|
||||
|
||||
impl Context {
|
||||
pub fn working_directory_configuration(&self) -> &WorkingDirectoryConfiguration {
|
||||
self.as_ref()
|
||||
}
|
||||
/// Use the Wasm compiler versions.
|
||||
#[arg(long = "wasm")]
|
||||
pub wasm: bool,
|
||||
|
||||
pub fn report_configuration(&self) -> &ReportConfiguration {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<WorkingDirectoryConfiguration> for Context {
|
||||
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
|
||||
match self {
|
||||
Context::ExecuteTests(execution_context) => &execution_context.working_directory,
|
||||
Context::ExportJsonSchema => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ReportConfiguration> for Context {
|
||||
fn as_ref(&self) -> &ReportConfiguration {
|
||||
match self {
|
||||
Context::ExecuteTests(execution_context) => &execution_context.report_configuration,
|
||||
Context::ExportJsonSchema => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct ExecutionContext {
|
||||
/// The working directory that the program will use for all of the temporary artifacts needed at
|
||||
/// runtime.
|
||||
/// The path to the `resolc` executable to be tested.
|
||||
///
|
||||
/// If not specified, then a temporary directory will be created and used by the program for all
|
||||
/// temporary artifacts.
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
default_value = "",
|
||||
value_hint = ValueHint::DirPath,
|
||||
)]
|
||||
pub working_directory: WorkingDirectoryConfiguration,
|
||||
|
||||
/// The differential testing leader node implementation.
|
||||
#[arg(short, long = "leader", default_value_t = TestingPlatform::Geth)]
|
||||
pub leader: TestingPlatform,
|
||||
|
||||
/// The differential testing follower node implementation.
|
||||
#[arg(short, long = "follower", default_value_t = TestingPlatform::Kitchensink)]
|
||||
pub follower: TestingPlatform,
|
||||
/// By default it uses the `resolc` binary found in `$PATH`.
|
||||
///
|
||||
/// If `--wasm` is set, this should point to the resolc Wasm ile.
|
||||
#[arg(long = "resolc", short, default_value = "resolc")]
|
||||
pub resolc: PathBuf,
|
||||
|
||||
/// A list of test corpus JSON files to be tested.
|
||||
#[arg(long = "corpus", short)]
|
||||
pub corpus: Vec<PathBuf>,
|
||||
|
||||
/// Configuration parameters for the solc compiler.
|
||||
#[clap(flatten, next_help_heading = "Solc Configuration")]
|
||||
pub solc_configuration: SolcConfiguration,
|
||||
|
||||
/// Configuration parameters for the resolc compiler.
|
||||
#[clap(flatten, next_help_heading = "Resolc Configuration")]
|
||||
pub resolc_configuration: ResolcConfiguration,
|
||||
|
||||
/// Configuration parameters for the geth node.
|
||||
#[clap(flatten, next_help_heading = "Geth Configuration")]
|
||||
pub geth_configuration: GethConfiguration,
|
||||
|
||||
/// Configuration parameters for the Kitchensink.
|
||||
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
|
||||
pub kitchensink_configuration: KitchensinkConfiguration,
|
||||
|
||||
/// Configuration parameters for the Revive Dev Node.
|
||||
#[clap(flatten, next_help_heading = "Revive Dev Node Configuration")]
|
||||
pub revive_dev_node_configuration: ReviveDevNodeConfiguration,
|
||||
|
||||
/// Configuration parameters for the Eth Rpc.
|
||||
#[clap(flatten, next_help_heading = "Eth RPC Configuration")]
|
||||
pub eth_rpc_configuration: EthRpcConfiguration,
|
||||
|
||||
/// Configuration parameters for the genesis.
|
||||
#[clap(flatten, next_help_heading = "Genesis Configuration")]
|
||||
pub genesis_configuration: GenesisConfiguration,
|
||||
|
||||
/// Configuration parameters for the wallet.
|
||||
#[clap(flatten, next_help_heading = "Wallet Configuration")]
|
||||
pub wallet_configuration: WalletConfiguration,
|
||||
|
||||
/// Configuration parameters for concurrency.
|
||||
#[clap(flatten, next_help_heading = "Concurrency Configuration")]
|
||||
pub concurrency_configuration: ConcurrencyConfiguration,
|
||||
|
||||
/// Configuration parameters for the compilers and compilation.
|
||||
#[clap(flatten, next_help_heading = "Compilation Configuration")]
|
||||
pub compilation_configuration: CompilationConfiguration,
|
||||
|
||||
/// Configuration parameters for the report.
|
||||
#[clap(flatten, next_help_heading = "Report Configuration")]
|
||||
pub report_configuration: ReportConfiguration,
|
||||
}
|
||||
|
||||
impl Default for ExecutionContext {
|
||||
fn default() -> Self {
|
||||
Self::parse_from(["execution-context"])
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<WorkingDirectoryConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
|
||||
&self.working_directory
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<SolcConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &SolcConfiguration {
|
||||
&self.solc_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ResolcConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &ResolcConfiguration {
|
||||
&self.resolc_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<GethConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &GethConfiguration {
|
||||
&self.geth_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<KitchensinkConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &KitchensinkConfiguration {
|
||||
&self.kitchensink_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ReviveDevNodeConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
|
||||
&self.revive_dev_node_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<EthRpcConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &EthRpcConfiguration {
|
||||
&self.eth_rpc_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<GenesisConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &GenesisConfiguration {
|
||||
&self.genesis_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<WalletConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &WalletConfiguration {
|
||||
&self.wallet_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ConcurrencyConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &ConcurrencyConfiguration {
|
||||
&self.concurrency_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<CompilationConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &CompilationConfiguration {
|
||||
&self.compilation_configuration
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ReportConfiguration> for ExecutionContext {
|
||||
fn as_ref(&self) -> &ReportConfiguration {
|
||||
&self.report_configuration
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for Solc.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct SolcConfiguration {
|
||||
/// Specifies the default version of the Solc compiler that should be used if there is no
|
||||
/// override specified by one of the test cases.
|
||||
#[clap(long = "solc.version", default_value = "0.8.29")]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for Resolc.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct ResolcConfiguration {
|
||||
/// Specifies the path of the resolc compiler to be used by the tool.
|
||||
/// A place to store temporary artifacts during test execution.
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the resolc binary that's
|
||||
/// provided in the user's $PATH.
|
||||
#[clap(id = "resolc.path", long = "resolc.path", default_value = "resolc")]
|
||||
pub path: PathBuf,
|
||||
}
|
||||
/// Creates a temporary dir if not specified.
|
||||
#[arg(long = "workdir", short)]
|
||||
pub working_directory: Option<PathBuf>,
|
||||
|
||||
/// A set of configuration parameters for Geth.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct GethConfiguration {
|
||||
/// Specifies the path of the geth node to be used by the tool.
|
||||
/// Add a tempdir manually if `working_directory` was not given.
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the geth binary that's
|
||||
/// provided in the user's $PATH.
|
||||
#[clap(id = "geth.path", long = "geth.path", default_value = "geth")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||
#[clap(
|
||||
id = "geth.start-timeout-ms",
|
||||
long = "geth.start-timeout-ms",
|
||||
default_value = "5000",
|
||||
value_parser = parse_duration
|
||||
)]
|
||||
pub start_timeout_ms: Duration,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for Kitchensink.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct KitchensinkConfiguration {
|
||||
/// Specifies the path of the kitchensink node to be used by the tool.
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the kitchensink binary
|
||||
/// that's provided in the user's $PATH.
|
||||
#[clap(
|
||||
id = "kitchensink.path",
|
||||
long = "kitchensink.path",
|
||||
default_value = "substrate-node"
|
||||
)]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||
#[clap(
|
||||
id = "kitchensink.start-timeout-ms",
|
||||
long = "kitchensink.start-timeout-ms",
|
||||
default_value = "5000",
|
||||
value_parser = parse_duration
|
||||
)]
|
||||
pub start_timeout_ms: Duration,
|
||||
|
||||
/// This configures the tool to use Kitchensink instead of using the revive-dev-node.
|
||||
#[clap(long = "kitchensink.dont-use-dev-node")]
|
||||
pub use_kitchensink: bool,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for the revive dev node.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct ReviveDevNodeConfiguration {
|
||||
/// Specifies the path of the revive dev node to be used by the tool.
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the revive dev node binary
|
||||
/// that's provided in the user's $PATH.
|
||||
#[clap(
|
||||
id = "revive-dev-node.path",
|
||||
long = "revive-dev-node.path",
|
||||
default_value = "revive-dev-node"
|
||||
)]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||
#[clap(
|
||||
id = "revive-dev-node.start-timeout-ms",
|
||||
long = "revive-dev-node.start-timeout-ms",
|
||||
default_value = "5000",
|
||||
value_parser = parse_duration
|
||||
)]
|
||||
pub start_timeout_ms: Duration,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for the ETH RPC.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct EthRpcConfiguration {
|
||||
/// Specifies the path of the ETH RPC to be used by the tool.
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the ETH RPC binary
|
||||
/// that's provided in the user's $PATH.
|
||||
#[clap(id = "eth-rpc.path", long = "eth-rpc.path", default_value = "eth-rpc")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||
#[clap(
|
||||
id = "eth-rpc.start-timeout-ms",
|
||||
long = "eth-rpc.start-timeout-ms",
|
||||
default_value = "5000",
|
||||
value_parser = parse_duration
|
||||
)]
|
||||
pub start_timeout_ms: Duration,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for the genesis.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct GenesisConfiguration {
|
||||
/// Specifies the path of the genesis file to use for the nodes that are started.
|
||||
///
|
||||
/// This is expected to be the path of a JSON geth genesis file.
|
||||
#[clap(id = "genesis.path", long = "genesis.path")]
|
||||
path: Option<PathBuf>,
|
||||
|
||||
/// The genesis object found at the provided path.
|
||||
/// We attach it here because [TempDir] prunes itself on drop.
|
||||
#[clap(skip)]
|
||||
#[serde(skip)]
|
||||
genesis: OnceLock<Genesis>,
|
||||
}
|
||||
pub temp_dir: Option<&'static TempDir>,
|
||||
|
||||
impl GenesisConfiguration {
|
||||
pub fn genesis(&self) -> anyhow::Result<&Genesis> {
|
||||
static DEFAULT_GENESIS: LazyLock<Genesis> = LazyLock::new(|| {
|
||||
let genesis = include_str!("../../../genesis.json");
|
||||
serde_json::from_str(genesis).unwrap()
|
||||
});
|
||||
/// The path to the `geth` executable.
|
||||
///
|
||||
/// By default it uses `geth` binary found in `$PATH`.
|
||||
#[arg(short, long = "geth", default_value = "geth")]
|
||||
pub geth: PathBuf,
|
||||
|
||||
match self.genesis.get() {
|
||||
Some(genesis) => Ok(genesis),
|
||||
None => {
|
||||
let genesis = match self.path.as_ref() {
|
||||
Some(genesis_path) => {
|
||||
let genesis_content = read_to_string(genesis_path)?;
|
||||
serde_json::from_str(genesis_content.as_str())?
|
||||
}
|
||||
None => DEFAULT_GENESIS.clone(),
|
||||
};
|
||||
Ok(self.genesis.get_or_init(|| genesis))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The maximum time in milliseconds to wait for geth to start.
|
||||
#[arg(long = "geth-start-timeout", default_value = "5000")]
|
||||
pub geth_start_timeout: u64,
|
||||
|
||||
/// A set of configuration parameters for the wallet.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct WalletConfiguration {
|
||||
/// The private key of the default signer.
|
||||
#[clap(
|
||||
long = "wallet.default-private-key",
|
||||
/// Configure nodes according to this genesis.json file.
|
||||
#[arg(long = "genesis", default_value = "genesis.json")]
|
||||
pub genesis_file: PathBuf,
|
||||
|
||||
/// The signing account private key.
|
||||
#[arg(
|
||||
short,
|
||||
long = "account",
|
||||
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_private_key")]
|
||||
default_key: PrivateKeySigner,
|
||||
pub account: String,
|
||||
|
||||
/// This argument controls which private keys the nodes should have access to and be added to
|
||||
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
|
||||
/// of the node.
|
||||
#[clap(long = "wallet.additional-keys", default_value_t = 100_000)]
|
||||
additional_keys: usize,
|
||||
#[arg(long = "private-keys-count", default_value_t = 100_000)]
|
||||
pub private_keys_to_add: usize,
|
||||
|
||||
/// The wallet object that will be used.
|
||||
#[clap(skip)]
|
||||
#[serde(skip)]
|
||||
wallet: OnceLock<Arc<EthereumWallet>>,
|
||||
}
|
||||
/// The differential testing leader node implementation.
|
||||
#[arg(short, long = "leader", default_value = "geth")]
|
||||
pub leader: TestingPlatform,
|
||||
|
||||
impl WalletConfiguration {
|
||||
pub fn wallet(&self) -> Arc<EthereumWallet> {
|
||||
self.wallet
|
||||
.get_or_init(|| {
|
||||
let mut wallet = EthereumWallet::new(self.default_key.clone());
|
||||
for signer in (1..=self.additional_keys)
|
||||
.map(|id| U256::from(id))
|
||||
.map(|id| id.to_be_bytes::<32>())
|
||||
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
|
||||
{
|
||||
wallet.register_signer(signer);
|
||||
}
|
||||
Arc::new(wallet)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
/// The differential testing follower node implementation.
|
||||
#[arg(short, long = "follower", default_value = "kitchensink")]
|
||||
pub follower: TestingPlatform,
|
||||
|
||||
fn serialize_private_key<S>(value: &PrivateKeySigner, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
value.to_bytes().encode_hex().serialize(serializer)
|
||||
}
|
||||
|
||||
/// A set of configuration for concurrency.
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct ConcurrencyConfiguration {
|
||||
/// Determines the amount of nodes that will be spawned for each chain.
|
||||
#[clap(long = "concurrency.number-of-nodes", default_value_t = 5)]
|
||||
#[arg(long, default_value = "1")]
|
||||
pub number_of_nodes: usize,
|
||||
|
||||
/// Determines the amount of tokio worker threads that will will be used.
|
||||
#[arg(
|
||||
long = "concurrency.number-of-threads",
|
||||
long,
|
||||
default_value_t = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
)]
|
||||
pub number_of_threads: usize,
|
||||
|
||||
/// Determines the amount of concurrent tasks that will be spawned to run tests.
|
||||
/// Determines the amount of concurrent tasks that will be spawned to run tests. Defaults to 10 x the number of nodes.
|
||||
#[arg(long)]
|
||||
pub number_concurrent_tasks: Option<usize>,
|
||||
|
||||
/// Extract problems back to the test corpus.
|
||||
#[arg(short, long = "extract-problems")]
|
||||
pub extract_problems: bool,
|
||||
|
||||
/// The path to the `kitchensink` executable.
|
||||
///
|
||||
/// Defaults to 10 x the number of nodes.
|
||||
#[arg(long = "concurrency.number-of-concurrent-tasks")]
|
||||
number_concurrent_tasks: Option<usize>,
|
||||
/// By default it uses `substrate-node` binary found in `$PATH`.
|
||||
#[arg(short, long = "kitchensink", default_value = "substrate-node")]
|
||||
pub kitchensink: PathBuf,
|
||||
|
||||
/// Determines if the concurrency limit should be ignored or not.
|
||||
#[arg(long = "concurrency.ignore-concurrency-limit")]
|
||||
ignore_concurrency_limit: bool,
|
||||
}
|
||||
/// The path to the `revive-dev-node` executable.
|
||||
///
|
||||
/// By default it uses `revive-dev-node` binary found in `$PATH`.
|
||||
#[arg(long = "revive-dev-node", default_value = "revive-dev-node")]
|
||||
pub revive_dev_node: PathBuf,
|
||||
|
||||
impl ConcurrencyConfiguration {
|
||||
pub fn concurrency_limit(&self) -> Option<usize> {
|
||||
match self.ignore_concurrency_limit {
|
||||
true => None,
|
||||
false => Some(
|
||||
self.number_concurrent_tasks
|
||||
.unwrap_or(20 * self.number_of_nodes),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// By default the tool uses the revive-dev-node when it's running differential tests against
|
||||
/// PolkaVM since the dev-node is much faster than kitchensink. This flag allows the caller to
|
||||
/// configure the tool to use kitchensink rather than the dev-node.
|
||||
#[arg(long)]
|
||||
pub use_kitchensink_not_dev_node: bool,
|
||||
|
||||
/// The path to the `eth_proxy` executable.
|
||||
///
|
||||
/// By default it uses `eth-rpc` binary found in `$PATH`.
|
||||
#[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")]
|
||||
pub eth_proxy: PathBuf,
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct CompilationConfiguration {
|
||||
/// Controls if the compilation cache should be invalidated or not.
|
||||
#[arg(long = "compilation.invalidate-cache")]
|
||||
#[arg(short, long)]
|
||||
pub invalidate_compilation_cache: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct ReportConfiguration {
|
||||
/// Controls if the compiler input is included in the final report.
|
||||
#[clap(long = "report.include-compiler-input")]
|
||||
pub include_compiler_input: bool,
|
||||
pub report_include_compiler_input: bool,
|
||||
|
||||
/// Controls if the compiler output is included in the final report.
|
||||
#[clap(long = "report.include-compiler-output")]
|
||||
pub include_compiler_output: bool,
|
||||
pub report_include_compiler_output: bool,
|
||||
}
|
||||
|
||||
/// Represents the working directory that the program uses.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WorkingDirectoryConfiguration {
|
||||
/// A temporary directory is used as the working directory. This will be removed when dropped.
|
||||
TemporaryDirectory(Arc<TempDir>),
|
||||
/// A directory with a path is used as the working directory.
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
impl WorkingDirectoryConfiguration {
|
||||
pub fn as_path(&self) -> &Path {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WorkingDirectoryConfiguration {
|
||||
type Target = Path;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for WorkingDirectoryConfiguration {
|
||||
fn as_ref(&self) -> &Path {
|
||||
match self {
|
||||
WorkingDirectoryConfiguration::TemporaryDirectory(temp_dir) => temp_dir.path(),
|
||||
WorkingDirectoryConfiguration::Path(path) => path.as_path(),
|
||||
impl Arguments {
|
||||
/// Return the configured working directory with the following precedence:
|
||||
/// 1. `self.working_directory` if it was provided.
|
||||
/// 2. `self.temp_dir` if it it was provided
|
||||
/// 3. Panic.
|
||||
pub fn directory(&self) -> &Path {
|
||||
if let Some(path) = &self.working_directory {
|
||||
return path.as_path();
|
||||
}
|
||||
|
||||
if let Some(temp_dir) = &self.temp_dir {
|
||||
return temp_dir.path();
|
||||
}
|
||||
|
||||
panic!("should have a workdir configured")
|
||||
}
|
||||
|
||||
/// Return the number of concurrent tasks to run. This is provided via the
|
||||
/// `--number-concurrent-tasks` argument, and otherwise defaults to --number-of-nodes * 20.
|
||||
pub fn number_of_concurrent_tasks(&self) -> usize {
|
||||
self.number_concurrent_tasks
|
||||
.unwrap_or(20 * self.number_of_nodes)
|
||||
}
|
||||
|
||||
/// Try to parse `self.account` into a [PrivateKeySigner],
|
||||
/// panicing on error.
|
||||
pub fn wallet(&self) -> EthereumWallet {
|
||||
let signer = self
|
||||
.account
|
||||
.parse::<PrivateKeySigner>()
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("private key '{}' parsing error: {error}", self.account);
|
||||
});
|
||||
EthereumWallet::new(signer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WorkingDirectoryConfiguration {
|
||||
impl Default for Arguments {
|
||||
fn default() -> Self {
|
||||
TempDir::new()
|
||||
.map(Arc::new)
|
||||
.map(Self::TemporaryDirectory)
|
||||
.expect("Failed to create the temporary directory")
|
||||
}
|
||||
}
|
||||
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
|
||||
|
||||
impl FromStr for WorkingDirectoryConfiguration {
|
||||
type Err = anyhow::Error;
|
||||
let default = Arguments::parse_from(["retester"]);
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"" => Ok(Default::default()),
|
||||
_ => Ok(Self::Path(PathBuf::from(s))),
|
||||
Arguments {
|
||||
temp_dir: Some(&TEMP_DIR),
|
||||
..default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for WorkingDirectoryConfiguration {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.as_path().display(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for WorkingDirectoryConfiguration {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.as_path().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> anyhow::Result<Duration> {
|
||||
u64::from_str(s)
|
||||
.map(Duration::from_millis)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// The Solidity compatible node implementation.
|
||||
///
|
||||
/// This describes the solutions to be tested against on a high level.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
ValueEnum,
|
||||
EnumString,
|
||||
Display,
|
||||
AsRefStr,
|
||||
IntoStaticStr,
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum, Serialize, Deserialize,
|
||||
)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[clap(rename_all = "lower")]
|
||||
pub enum TestingPlatform {
|
||||
/// The go-ethereum reference full node EVM implementation.
|
||||
Geth,
|
||||
/// The kitchensink runtime provides the PolkaVM (PVM) based node implementation.
|
||||
/// The kitchensink runtime provides the PolkaVM (PVM) based node implentation.
|
||||
Kitchensink,
|
||||
}
|
||||
|
||||
impl Display for TestingPlatform {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Geth => f.write_str("geth"),
|
||||
Self::Kitchensink => f.write_str("revive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
temp-dir = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -18,7 +18,7 @@ use alloy::{
|
||||
primitives::Address,
|
||||
rpc::types::{TransactionRequest, trace::geth::DiffMode},
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use futures::TryStreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
|
||||
|
||||
+98
-69
@@ -5,7 +5,7 @@ use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
io::{BufWriter, Write, stderr},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ use alloy::{
|
||||
network::{Ethereum, TransactionBuilder},
|
||||
rpc::types::TransactionRequest,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use futures::stream;
|
||||
use futures::{Stream, StreamExt};
|
||||
@@ -23,15 +23,16 @@ use revive_dt_report::{
|
||||
NodeDesignation, ReportAggregator, Reporter, ReporterEvent, TestCaseStatus,
|
||||
TestSpecificReporter, TestSpecifier,
|
||||
};
|
||||
use schemars::schema_for;
|
||||
use serde_json::{Value, json};
|
||||
use temp_dir::TempDir;
|
||||
use tokio::try_join;
|
||||
use tracing::{debug, error, info, info_span, instrument};
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
||||
|
||||
use revive_dt_common::{iterators::EitherIter, types::Mode};
|
||||
use revive_dt_compiler::{CompilerOutput, SolidityCompiler};
|
||||
use revive_dt_config::{Context, *};
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_core::{
|
||||
Geth, Kitchensink, Platform,
|
||||
driver::{CaseDriver, CaseState},
|
||||
@@ -40,15 +41,65 @@ use revive_dt_format::{
|
||||
case::{Case, CaseIdx},
|
||||
corpus::Corpus,
|
||||
input::{Input, Step},
|
||||
metadata::{ContractPathAndIdent, Metadata, MetadataFile},
|
||||
metadata::{ContractPathAndIdent, MetadataFile},
|
||||
mode::ParsedMode,
|
||||
};
|
||||
use revive_dt_node::{Node, pool::NodePool};
|
||||
|
||||
use crate::cached_compiler::CachedCompiler;
|
||||
|
||||
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
|
||||
let (args, _guard) = init_cli().context("Failed to initialize CLI and tracing subscriber")?;
|
||||
info!(
|
||||
leader = args.leader.to_string(),
|
||||
follower = args.follower.to_string(),
|
||||
working_directory = %args.directory().display(),
|
||||
number_of_nodes = args.number_of_nodes,
|
||||
invalidate_compilation_cache = args.invalidate_compilation_cache,
|
||||
"Differential testing tool has been initialized"
|
||||
);
|
||||
|
||||
let (reporter, report_aggregator_task) = ReportAggregator::new(args.clone()).into_task();
|
||||
|
||||
let number_of_threads = args.number_of_threads;
|
||||
let body = async move {
|
||||
let tests = collect_corpora(&args)
|
||||
.context("Failed to collect corpus files from provided arguments")?
|
||||
.into_iter()
|
||||
.inspect(|(corpus, _)| {
|
||||
reporter
|
||||
.report_corpus_file_discovery_event(corpus.clone())
|
||||
.expect("Can't fail")
|
||||
})
|
||||
.flat_map(|(_, files)| files.into_iter())
|
||||
.inspect(|metadata_file| {
|
||||
reporter
|
||||
.report_metadata_file_discovery_event(
|
||||
metadata_file.metadata_file_path.clone(),
|
||||
metadata_file.content.clone(),
|
||||
)
|
||||
.expect("Can't fail")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
execute_corpus(&args, &tests, reporter, report_aggregator_task)
|
||||
.await
|
||||
.context("Failed to execute corpus")?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(number_of_threads)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed building the Runtime")
|
||||
.block_on(body)
|
||||
}
|
||||
|
||||
fn init_cli() -> anyhow::Result<(Arguments, WorkerGuard)> {
|
||||
let (writer, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
|
||||
.lossy(false)
|
||||
// Assuming that each line contains 255 characters and that each character is one byte, then
|
||||
// this means that our buffer is about 4GBs large.
|
||||
@@ -67,56 +118,31 @@ fn main() -> anyhow::Result<()> {
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
info!("Differential testing tool is starting");
|
||||
|
||||
let context = Context::try_parse()?;
|
||||
let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task();
|
||||
let mut args = Arguments::parse();
|
||||
|
||||
match context {
|
||||
Context::ExecuteTests(context) => {
|
||||
let tests = collect_corpora(&context)
|
||||
.context("Failed to collect corpus files from provided arguments")?
|
||||
.into_iter()
|
||||
.inspect(|(corpus, _)| {
|
||||
reporter
|
||||
.report_corpus_file_discovery_event(corpus.clone())
|
||||
.expect("Can't fail")
|
||||
})
|
||||
.flat_map(|(_, files)| files.into_iter())
|
||||
.inspect(|metadata_file| {
|
||||
reporter
|
||||
.report_metadata_file_discovery_event(
|
||||
metadata_file.metadata_file_path.clone(),
|
||||
metadata_file.content.clone(),
|
||||
)
|
||||
.expect("Can't fail")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if args.corpus.is_empty() {
|
||||
anyhow::bail!("no test corpus specified");
|
||||
}
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(context.concurrency_configuration.number_of_threads)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed building the Runtime")
|
||||
.block_on(async move {
|
||||
execute_corpus(*context, &tests, reporter, report_aggregator_task)
|
||||
.await
|
||||
.context("Failed to execute corpus")
|
||||
})
|
||||
match args.working_directory.as_ref() {
|
||||
Some(dir) => {
|
||||
if !dir.exists() {
|
||||
anyhow::bail!("workdir {} does not exist", dir.display());
|
||||
}
|
||||
}
|
||||
Context::ExportJsonSchema => {
|
||||
let schema = schema_for!(Metadata);
|
||||
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||
Ok(())
|
||||
None => {
|
||||
args.temp_dir = Some(&TEMP_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((args, guard))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
|
||||
fn collect_corpora(
|
||||
context: &ExecutionContext,
|
||||
) -> anyhow::Result<HashMap<Corpus, Vec<MetadataFile>>> {
|
||||
fn collect_corpora(args: &Arguments) -> anyhow::Result<HashMap<Corpus, Vec<MetadataFile>>> {
|
||||
let mut corpora = HashMap::new();
|
||||
|
||||
for path in &context.corpus {
|
||||
for path in &args.corpus {
|
||||
let span = info_span!("Processing corpus file", path = %path.display());
|
||||
let _guard = span.enter();
|
||||
|
||||
@@ -134,7 +160,7 @@ fn collect_corpora(
|
||||
}
|
||||
|
||||
async fn run_driver<L, F>(
|
||||
context: ExecutionContext,
|
||||
args: &Arguments,
|
||||
metadata_files: &[MetadataFile],
|
||||
reporter: Reporter,
|
||||
report_aggregator_task: impl Future<Output = anyhow::Result<()>>,
|
||||
@@ -145,20 +171,20 @@ where
|
||||
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
|
||||
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
|
||||
{
|
||||
let leader_nodes = NodePool::<L::Blockchain>::new(context.clone())
|
||||
.context("Failed to initialize leader node pool")?;
|
||||
let follower_nodes = NodePool::<F::Blockchain>::new(context.clone())
|
||||
.context("Failed to initialize follower node pool")?;
|
||||
let leader_nodes =
|
||||
NodePool::<L::Blockchain>::new(args).context("Failed to initialize leader node pool")?;
|
||||
let follower_nodes =
|
||||
NodePool::<F::Blockchain>::new(args).context("Failed to initialize follower node pool")?;
|
||||
|
||||
let tests_stream = tests_stream(
|
||||
&context,
|
||||
args,
|
||||
metadata_files.iter(),
|
||||
&leader_nodes,
|
||||
&follower_nodes,
|
||||
reporter.clone(),
|
||||
)
|
||||
.await;
|
||||
let driver_task = start_driver_task::<L, F>(&context, tests_stream)
|
||||
let driver_task = start_driver_task::<L, F>(args, tests_stream)
|
||||
.await
|
||||
.context("Failed to start driver task")?;
|
||||
let cli_reporting_task = start_cli_reporting_task(reporter);
|
||||
@@ -170,7 +196,7 @@ where
|
||||
}
|
||||
|
||||
async fn tests_stream<'a, L, F>(
|
||||
args: &ExecutionContext,
|
||||
args: &Arguments,
|
||||
metadata_files: impl IntoIterator<Item = &'a MetadataFile> + Clone,
|
||||
leader_node_pool: &'a NodePool<L::Blockchain>,
|
||||
follower_node_pool: &'a NodePool<F::Blockchain>,
|
||||
@@ -294,7 +320,7 @@ where
|
||||
}
|
||||
|
||||
async fn start_driver_task<'a, L, F>(
|
||||
context: &ExecutionContext,
|
||||
args: &Arguments,
|
||||
tests: impl Stream<Item = Test<'a, L, F>>,
|
||||
) -> anyhow::Result<impl Future<Output = ()>>
|
||||
where
|
||||
@@ -307,22 +333,25 @@ where
|
||||
{
|
||||
info!("Starting driver task");
|
||||
|
||||
let number_concurrent_tasks = args.number_of_concurrent_tasks();
|
||||
let cached_compiler = Arc::new(
|
||||
CachedCompiler::new(
|
||||
context
|
||||
.working_directory
|
||||
.as_path()
|
||||
.join("compilation_cache"),
|
||||
context
|
||||
.compilation_configuration
|
||||
.invalidate_compilation_cache,
|
||||
args.directory().join("compilation_cache"),
|
||||
args.invalidate_compilation_cache,
|
||||
)
|
||||
.await
|
||||
.context("Failed to initialize cached compiler")?,
|
||||
);
|
||||
|
||||
Ok(tests.for_each_concurrent(
|
||||
context.concurrency_configuration.concurrency_limit(),
|
||||
// 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.
|
||||
// 2. We don't want to open too many files at once, leading to the OS running out of file descriptors.
|
||||
//
|
||||
// By default, we allow maximum of 10 ongoing requests per node in order to limit (1), and assume that
|
||||
// this number will automatically be low enough to address (2). The user can override this.
|
||||
Some(number_concurrent_tasks),
|
||||
move |test| {
|
||||
let cached_compiler = cached_compiler.clone();
|
||||
|
||||
@@ -358,7 +387,8 @@ where
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
|
||||
#[allow(clippy::uninlined_format_args)]
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
async fn start_cli_reporting_task(reporter: Reporter) {
|
||||
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
|
||||
drop(reporter);
|
||||
@@ -680,18 +710,17 @@ where
|
||||
}
|
||||
|
||||
async fn execute_corpus(
|
||||
context: ExecutionContext,
|
||||
args: &Arguments,
|
||||
tests: &[MetadataFile],
|
||||
reporter: Reporter,
|
||||
report_aggregator_task: impl Future<Output = anyhow::Result<()>>,
|
||||
) -> anyhow::Result<()> {
|
||||
match (&context.leader, &context.follower) {
|
||||
match (&args.leader, &args.follower) {
|
||||
(TestingPlatform::Geth, TestingPlatform::Kitchensink) => {
|
||||
run_driver::<Geth, Kitchensink>(context, tests, reporter, report_aggregator_task)
|
||||
.await?
|
||||
run_driver::<Geth, Kitchensink>(args, tests, reporter, report_aggregator_task).await?
|
||||
}
|
||||
(TestingPlatform::Geth, TestingPlatform::Geth) => {
|
||||
run_driver::<Geth, Geth>(context, tests, reporter, report_aggregator_task).await?
|
||||
run_driver::<Geth, Geth>(args, tests, reporter, report_aggregator_task).await?
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ anyhow = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
|
||||
@@ -8,48 +7,26 @@ use crate::{
|
||||
mode::ParsedMode,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
pub struct Case {
|
||||
/// An optional name of the test case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// An optional comment on the case which has no impact on the execution in any way.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
/// Mode strings can take the following form (in pseudo-regex):
|
||||
///
|
||||
/// ```text
|
||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||
/// ```
|
||||
///
|
||||
/// If this is provided then it takes higher priority than the modes specified in the metadata
|
||||
/// file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub modes: Option<Vec<ParsedMode>>,
|
||||
|
||||
/// The set of steps to run as part of this test case.
|
||||
#[serde(rename = "inputs")]
|
||||
pub steps: Vec<Step>,
|
||||
|
||||
/// An optional name of the group of tests that this test belongs to.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
|
||||
/// An optional set of expectations and assertions to make about the transaction after it ran.
|
||||
///
|
||||
/// If this is not specified then the only assertion that will be ran is that the transaction
|
||||
/// was successful.
|
||||
///
|
||||
/// This expectation that's on the case itself will be attached to the final step of the case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expected: Option<Expected>,
|
||||
|
||||
/// An optional boolean which defines if the case as a whole should be ignored. If null then the
|
||||
/// case will not be ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<bool>,
|
||||
}
|
||||
|
||||
+16
-61
@@ -8,9 +8,8 @@ use alloy::{
|
||||
rpc::types::TransactionRequest,
|
||||
};
|
||||
use alloy_primitives::{FixedBytes, utils::parse_units};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
||||
use schemars::JsonSchema;
|
||||
use semver::VersionReq;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -24,7 +23,7 @@ use crate::{metadata::ContractInstance, traits::ResolutionContext};
|
||||
///
|
||||
/// A test step can be anything. It could be an invocation to a function, an assertion, or any other
|
||||
/// action that needs to be run or executed on the nodes used in the tests.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Step {
|
||||
/// A function call or an invocation to some function on some smart contract.
|
||||
@@ -40,51 +39,36 @@ define_wrapper_type!(
|
||||
pub struct StepIdx(usize) impl Display;
|
||||
);
|
||||
|
||||
/// This is an input step which is a transaction description that the framework translates into a
|
||||
/// transaction and executes on the nodes.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Input {
|
||||
/// The address of the account performing the call and paying the fees for it.
|
||||
#[serde(default = "Input::default_caller")]
|
||||
#[schemars(with = "String")]
|
||||
pub caller: Address,
|
||||
|
||||
/// An optional comment on the step which has no impact on the execution in any way.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// The contract instance that's being called in this transaction step.
|
||||
#[serde(default = "Input::default_instance")]
|
||||
pub instance: ContractInstance,
|
||||
|
||||
/// The method that's being called in this step.
|
||||
pub method: Method,
|
||||
|
||||
/// The calldata that the function should be invoked with.
|
||||
#[serde(default)]
|
||||
pub calldata: Calldata,
|
||||
|
||||
/// A set of assertions and expectations to have for the transaction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expected: Option<Expected>,
|
||||
|
||||
/// An optional value to provide as part of the transaction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<EtherValue>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(skip)]
|
||||
pub storage: Option<HashMap<String, Calldata>>,
|
||||
|
||||
/// Variable assignment to perform in the framework allowing us to reference them again later on
|
||||
/// during the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub variable_assignments: Option<VariableAssignments>,
|
||||
}
|
||||
|
||||
/// This represents a balance assertion step where the framework needs to query the balance of some
|
||||
/// account or contract and assert that it's some amount.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct BalanceAssertion {
|
||||
/// An optional comment on the balance assertion.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -98,13 +82,11 @@ pub struct BalanceAssertion {
|
||||
/// followed in the calldata.
|
||||
pub address: String,
|
||||
|
||||
/// The amount of balance to assert that the account or contract has. This is a 256 bit string
|
||||
/// that's serialized and deserialized into a decimal string.
|
||||
#[schemars(with = "String")]
|
||||
/// The amount of balance to assert that the account or contract has.
|
||||
pub expected_balance: U256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct StorageEmptyAssertion {
|
||||
/// An optional comment on the storage empty assertion.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -122,52 +104,31 @@ pub struct StorageEmptyAssertion {
|
||||
pub is_storage_empty: bool,
|
||||
}
|
||||
|
||||
/// A set of expectations and assertions to make about the transaction after it ran.
|
||||
///
|
||||
/// If this is not specified then the only assertion that will be ran is that the transaction
|
||||
/// was successful.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Expected {
|
||||
/// An assertion that the transaction succeeded and returned the provided set of data.
|
||||
Calldata(Calldata),
|
||||
/// A more complex assertion.
|
||||
Expected(ExpectedOutput),
|
||||
/// A set of assertions.
|
||||
ExpectedMany(Vec<ExpectedOutput>),
|
||||
}
|
||||
|
||||
/// A set of assertions to run on the transaction.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct ExpectedOutput {
|
||||
/// An optional compiler version that's required in order for this assertion to run.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
pub compiler_version: Option<VersionReq>,
|
||||
|
||||
/// An optional field of the expected returns from the invocation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub return_data: Option<Calldata>,
|
||||
|
||||
/// An optional set of assertions to run on the emitted events from the transaction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub events: Option<Vec<Event>>,
|
||||
|
||||
/// A boolean which defines whether we expect the transaction to succeed or fail.
|
||||
#[serde(default)]
|
||||
pub exception: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Event {
|
||||
/// An optional field of the address of the emitter of the event.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<String>,
|
||||
|
||||
/// The set of topics to expect the event to have.
|
||||
pub topics: Vec<String>,
|
||||
|
||||
/// The set of values to expect the event to have.
|
||||
pub values: Calldata,
|
||||
}
|
||||
|
||||
@@ -222,17 +183,16 @@ pub struct Event {
|
||||
/// [`Single`]: Calldata::Single
|
||||
/// [`Compound`]: Calldata::Compound
|
||||
/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Calldata {
|
||||
Single(#[schemars(with = "String")] Bytes),
|
||||
Single(Bytes),
|
||||
Compound(Vec<CalldataItem>),
|
||||
}
|
||||
|
||||
define_wrapper_type! {
|
||||
/// This represents an item in the [`Calldata::Compound`] variant. Each item will be resolved
|
||||
/// according to the resolution rules of the tool.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
/// 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) impl Display;
|
||||
}
|
||||
@@ -257,7 +217,7 @@ enum Operation {
|
||||
}
|
||||
|
||||
/// Specify how the contract is called.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
pub enum Method {
|
||||
/// Initiate a deploy transaction, calling contracts constructor.
|
||||
///
|
||||
@@ -278,16 +238,11 @@ pub enum Method {
|
||||
}
|
||||
|
||||
define_wrapper_type!(
|
||||
/// Defines an Ether value.
|
||||
///
|
||||
/// This is an unsigned 256 bit integer that's followed by some denomination which can either be
|
||||
/// eth, ether, gwei, or wei.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
|
||||
#[schemars(with = "String")]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct EtherValue(U256) impl Display;
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct VariableAssignments {
|
||||
/// A vector of the variable names to assign to the return data.
|
||||
///
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_common::EVMVersion;
|
||||
@@ -57,62 +56,30 @@ impl Deref for MetadataFile {
|
||||
}
|
||||
}
|
||||
|
||||
/// A MatterLabs metadata file.
|
||||
///
|
||||
/// This defines the structure that the MatterLabs metadata files follow for defining the tests or
|
||||
/// the workloads.
|
||||
///
|
||||
/// Each metadata file is composed of multiple test cases where each test case is isolated from the
|
||||
/// others and runs in a completely different address space. Each test case is composed of a number
|
||||
/// of steps and assertions that should be performed as part of the test case.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
/// This is an optional comment on the metadata file which has no impact on the execution in any
|
||||
/// way.
|
||||
/// A comment on the test case that's added for human-readability.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// An optional boolean which defines if the metadata file as a whole should be ignored. If null
|
||||
/// then the metadata file will not be ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<bool>,
|
||||
|
||||
/// An optional vector of targets that this Metadata file's cases can be executed on. As an
|
||||
/// example, if we wish for the metadata file's cases to only be run on PolkaVM then we'd
|
||||
/// specify a target of "PolkaVM" in here.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub targets: Option<Vec<String>>,
|
||||
|
||||
/// A vector of the test cases and workloads contained within the metadata file. This is their
|
||||
/// primary description.
|
||||
pub cases: Vec<Case>,
|
||||
|
||||
/// A map of all of the contracts that the test requires to run.
|
||||
///
|
||||
/// This is a map where the key is the name of the contract instance and the value is the
|
||||
/// contract's path and ident in the file.
|
||||
///
|
||||
/// If any contract is to be used by the test then it must be included in here first so that the
|
||||
/// framework is aware of its path, compiles it, and prepares it.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
|
||||
|
||||
/// The set of libraries that this metadata file requires.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
/// Mode strings can take the following form (in pseudo-regex):
|
||||
///
|
||||
/// ```text
|
||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||
/// ```
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub modes: Option<Vec<ParsedMode>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(skip)]
|
||||
pub file_path: Option<PathBuf>,
|
||||
|
||||
/// This field specifies an EVM version requirement that the test case has where the test might
|
||||
@@ -120,9 +87,9 @@ pub struct Metadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub required_evm_version: Option<EvmVersionRequirement>,
|
||||
|
||||
/// A set of compilation directives that will be passed to the compiler whenever the contracts
|
||||
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]
|
||||
/// is just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
|
||||
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
|
||||
/// just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub compiler_directives: Option<CompilationDirectives>,
|
||||
}
|
||||
@@ -295,7 +262,7 @@ define_wrapper_type!(
|
||||
///
|
||||
/// Typically, this is used as the key to the "contracts" field of metadata files.
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContractInstance(String) impl Display;
|
||||
@@ -306,7 +273,7 @@ define_wrapper_type!(
|
||||
///
|
||||
/// A contract identifier is the name of the contract in the source code.
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContractIdent(String) impl Display;
|
||||
@@ -319,9 +286,7 @@ define_wrapper_type!(
|
||||
/// ```text
|
||||
/// ${path}:${contract_ident}
|
||||
/// ```
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ContractPathAndIdent {
|
||||
/// The path of the contract source code relative to the directory containing the metadata file.
|
||||
@@ -398,15 +363,9 @@ impl From<ContractPathAndIdent> for String {
|
||||
}
|
||||
}
|
||||
|
||||
/// An EVM version requirement that the test case has. This gets serialized and deserialized from
|
||||
/// and into [`String`]. This follows a simple format of (>=|<=|=|>|<) followed by a string of the
|
||||
/// EVM version.
|
||||
///
|
||||
/// When specified, the framework will only run the test if the node's EVM version matches that
|
||||
/// required by the metadata file.
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
/// An EVM version requirement that the test case has. This gets serialized and
|
||||
/// deserialized from and into [`String`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct EvmVersionRequirement {
|
||||
ordering: Ordering,
|
||||
@@ -534,18 +493,7 @@ impl From<EvmVersionRequirement> for String {
|
||||
/// just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
/// Defines how the compiler should handle revert strings.
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct CompilationDirectives {
|
||||
/// Defines how the revert strings should be handled.
|
||||
@@ -554,29 +502,14 @@ pub struct CompilationDirectives {
|
||||
|
||||
/// Defines how the compiler should handle revert strings.
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum RevertString {
|
||||
/// The default handling of the revert strings.
|
||||
#[default]
|
||||
Default,
|
||||
/// The debug handling of the revert strings.
|
||||
Debug,
|
||||
/// Strip the revert strings.
|
||||
Strip,
|
||||
/// Provide verbose debug strings for the revert string.
|
||||
VerboseDebug,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use regex::Regex;
|
||||
use revive_dt_common::iterators::EitherIter;
|
||||
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
@@ -18,7 +17,7 @@ use std::sync::LazyLock;
|
||||
/// ```
|
||||
///
|
||||
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ParsedMode {
|
||||
pub pipeline: Option<ModePipeline>,
|
||||
|
||||
+65
-46
@@ -17,7 +17,9 @@ use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
genesis::{Genesis, GenesisAccount},
|
||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||
primitives::{
|
||||
Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256,
|
||||
},
|
||||
providers::{
|
||||
Provider, ProviderBuilder,
|
||||
ext::DebugApi,
|
||||
@@ -27,8 +29,9 @@ use alloy::{
|
||||
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
||||
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
||||
},
|
||||
signers::local::PrivateKeySigner,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use revive_common::EVMVersion;
|
||||
use tracing::{Instrument, instrument};
|
||||
|
||||
@@ -36,7 +39,7 @@ use revive_dt_common::{
|
||||
fs::clear_directory,
|
||||
futures::{PollingWaitBehavior, poll},
|
||||
};
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
@@ -61,7 +64,7 @@ pub struct GethNode {
|
||||
geth: PathBuf,
|
||||
id: u32,
|
||||
handle: Option<Child>,
|
||||
start_timeout: Duration,
|
||||
start_timeout: u64,
|
||||
wallet: Arc<EthereumWallet>,
|
||||
nonce_manager: CachedNonceManager,
|
||||
chain_id_filler: ChainIdFiller,
|
||||
@@ -94,7 +97,7 @@ impl GethNode {
|
||||
|
||||
/// Create the node directory and call `geth init` to configure the genesis.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
|
||||
let _ = clear_directory(&self.base_directory);
|
||||
let _ = clear_directory(&self.logs_directory);
|
||||
|
||||
@@ -103,6 +106,8 @@ impl GethNode {
|
||||
create_dir_all(&self.logs_directory)
|
||||
.context("Failed to create logs directory for geth node")?;
|
||||
|
||||
let mut genesis = serde_json::from_str::<Genesis>(&genesis)
|
||||
.context("Failed to deserialize geth genesis JSON")?;
|
||||
for signer_address in
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
|
||||
{
|
||||
@@ -235,7 +240,7 @@ impl GethNode {
|
||||
.open(self.geth_stderr_log_file_path())
|
||||
.context("Failed to open geth stderr logs file for readiness check")?;
|
||||
|
||||
let maximum_wait_time = self.start_timeout;
|
||||
let maximum_wait_time = Duration::from_millis(self.start_timeout);
|
||||
let mut stderr = BufReader::new(logs_file).lines();
|
||||
let mut lines = vec![];
|
||||
loop {
|
||||
@@ -251,7 +256,7 @@ impl GethNode {
|
||||
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.as_millis(),
|
||||
self.start_timeout,
|
||||
lines.join("\n")
|
||||
);
|
||||
}
|
||||
@@ -551,40 +556,30 @@ impl ResolverApi for GethNode {
|
||||
}
|
||||
|
||||
impl Node for GethNode {
|
||||
fn new(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
+ AsRef<ConcurrencyConfiguration>
|
||||
+ AsRef<GenesisConfiguration>
|
||||
+ AsRef<WalletConfiguration>
|
||||
+ AsRef<GethConfiguration>
|
||||
+ AsRef<KitchensinkConfiguration>
|
||||
+ AsRef<ReviveDevNodeConfiguration>
|
||||
+ AsRef<EthRpcConfiguration>
|
||||
+ Clone,
|
||||
) -> Self {
|
||||
let working_directory_configuration =
|
||||
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
|
||||
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
|
||||
let geth_configuration = AsRef::<GethConfiguration>::as_ref(&context);
|
||||
|
||||
let geth_directory = working_directory_configuration
|
||||
.as_path()
|
||||
.join(Self::BASE_DIRECTORY);
|
||||
fn new(config: &Arguments) -> Self {
|
||||
let geth_directory = config.directory().join(Self::BASE_DIRECTORY);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = geth_directory.join(id.to_string());
|
||||
|
||||
let wallet = wallet_configuration.wallet();
|
||||
let mut wallet = config.wallet();
|
||||
for signer in (1..=config.private_keys_to_add)
|
||||
.map(|id| U256::from(id))
|
||||
.map(|id| id.to_be_bytes::<32>())
|
||||
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
|
||||
{
|
||||
wallet.register_signer(signer);
|
||||
}
|
||||
|
||||
Self {
|
||||
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
|
||||
data_directory: base_directory.join(Self::DATA_DIRECTORY),
|
||||
logs_directory: base_directory.join(Self::LOGS_DIRECTORY),
|
||||
base_directory,
|
||||
geth: geth_configuration.path.clone(),
|
||||
geth: config.geth.clone(),
|
||||
id,
|
||||
handle: None,
|
||||
start_timeout: geth_configuration.start_timeout_ms,
|
||||
wallet: wallet.clone(),
|
||||
start_timeout: config.geth_start_timeout,
|
||||
wallet: Arc::new(wallet),
|
||||
chain_id_filler: Default::default(),
|
||||
nonce_manager: Default::default(),
|
||||
// We know that we only need to be storing 2 files so we can specify that when creating
|
||||
@@ -626,7 +621,7 @@ impl Node for GethNode {
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
|
||||
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
|
||||
self.init(genesis)?.spawn_process()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -667,25 +662,49 @@ impl Drop for GethNode {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use revive_dt_config::Arguments;
|
||||
|
||||
use temp_dir::TempDir;
|
||||
|
||||
use crate::{GENESIS_JSON, Node};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> ExecutionContext {
|
||||
ExecutionContext::default()
|
||||
fn test_config() -> (Arguments, TempDir) {
|
||||
let mut config = Arguments::default();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
config.working_directory = temp_dir.path().to_path_buf().into();
|
||||
|
||||
(config, temp_dir)
|
||||
}
|
||||
|
||||
fn new_node() -> (ExecutionContext, GethNode) {
|
||||
let context = test_config();
|
||||
let mut node = GethNode::new(&context);
|
||||
node.init(context.genesis_configuration.genesis().unwrap().clone())
|
||||
fn new_node() -> (GethNode, TempDir) {
|
||||
let (args, temp_dir) = test_config();
|
||||
let mut node = GethNode::new(&args);
|
||||
node.init(GENESIS_JSON.to_owned())
|
||||
.expect("Failed to initialize the node")
|
||||
.spawn_process()
|
||||
.expect("Failed to spawn the node process");
|
||||
(context, node)
|
||||
(node, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_works() {
|
||||
GethNode::new(&test_config().0)
|
||||
.init(GENESIS_JSON.to_string())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_works() {
|
||||
GethNode::new(&test_config().0)
|
||||
.spawn(GENESIS_JSON.to_string())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_works() {
|
||||
let version = GethNode::new(&test_config()).version().unwrap();
|
||||
let version = GethNode::new(&test_config().0).version().unwrap();
|
||||
assert!(
|
||||
version.starts_with("geth version"),
|
||||
"expected version string, got: '{version}'"
|
||||
@@ -695,7 +714,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_chain_id_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let chain_id = node.chain_id().await;
|
||||
@@ -708,7 +727,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_gas_limit_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest).await;
|
||||
@@ -721,7 +740,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_coinbase_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest).await;
|
||||
@@ -734,7 +753,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_block_difficulty_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest).await;
|
||||
@@ -747,7 +766,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_block_hash_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_hash = node.block_hash(BlockNumberOrTag::Latest).await;
|
||||
@@ -759,7 +778,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_block_timestamp_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest).await;
|
||||
@@ -771,7 +790,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn can_get_block_number_from_node() {
|
||||
// Arrange
|
||||
let (_context, node) = new_node();
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_number = node.last_block_number().await;
|
||||
|
||||
@@ -19,8 +19,8 @@ use alloy::{
|
||||
TransactionBuilderError, UnbuiltTransactionError,
|
||||
},
|
||||
primitives::{
|
||||
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, StorageKey,
|
||||
TxHash, U256,
|
||||
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes,
|
||||
StorageKey, TxHash, U256,
|
||||
},
|
||||
providers::{
|
||||
Provider, ProviderBuilder,
|
||||
@@ -32,8 +32,9 @@ use alloy::{
|
||||
eth::{Block, Header, Transaction},
|
||||
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
||||
},
|
||||
signers::local::PrivateKeySigner,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_common::fs::clear_directory;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
@@ -42,7 +43,7 @@ use serde_json::{Value as JsonValue, json};
|
||||
use sp_core::crypto::Ss58Codec;
|
||||
use sp_runtime::AccountId32;
|
||||
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
|
||||
@@ -91,7 +92,7 @@ impl KitchensinkNode {
|
||||
const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log";
|
||||
const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log";
|
||||
|
||||
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||
fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> {
|
||||
let _ = clear_directory(&self.base_directory);
|
||||
let _ = clear_directory(&self.logs_directory);
|
||||
|
||||
@@ -152,6 +153,8 @@ impl KitchensinkNode {
|
||||
})
|
||||
.collect();
|
||||
let mut eth_balances = {
|
||||
let mut genesis = serde_json::from_str::<Genesis>(genesis)
|
||||
.context("Failed to deserialize EVM genesis JSON for kitchensink")?;
|
||||
for signer_address in
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
|
||||
{
|
||||
@@ -583,47 +586,35 @@ impl ResolverApi for KitchensinkNode {
|
||||
}
|
||||
|
||||
impl Node for KitchensinkNode {
|
||||
fn new(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
+ AsRef<ConcurrencyConfiguration>
|
||||
+ AsRef<GenesisConfiguration>
|
||||
+ AsRef<WalletConfiguration>
|
||||
+ AsRef<GethConfiguration>
|
||||
+ AsRef<KitchensinkConfiguration>
|
||||
+ AsRef<ReviveDevNodeConfiguration>
|
||||
+ AsRef<EthRpcConfiguration>
|
||||
+ Clone,
|
||||
) -> Self {
|
||||
let kitchensink_configuration = AsRef::<KitchensinkConfiguration>::as_ref(&context);
|
||||
let dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||
let eth_rpc_configuration = AsRef::<EthRpcConfiguration>::as_ref(&context);
|
||||
let working_directory_configuration =
|
||||
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
|
||||
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
|
||||
|
||||
let kitchensink_directory = working_directory_configuration
|
||||
.as_path()
|
||||
.join(Self::BASE_DIRECTORY);
|
||||
fn new(config: &Arguments) -> Self {
|
||||
let kitchensink_directory = config.directory().join(Self::BASE_DIRECTORY);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = kitchensink_directory.join(id.to_string());
|
||||
let logs_directory = base_directory.join(Self::LOGS_DIRECTORY);
|
||||
|
||||
let wallet = wallet_configuration.wallet();
|
||||
let mut wallet = config.wallet();
|
||||
for signer in (1..=config.private_keys_to_add)
|
||||
.map(|id| U256::from(id))
|
||||
.map(|id| id.to_be_bytes::<32>())
|
||||
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
|
||||
{
|
||||
wallet.register_signer(signer);
|
||||
}
|
||||
|
||||
Self {
|
||||
id,
|
||||
substrate_binary: kitchensink_configuration.path.clone(),
|
||||
dev_node_binary: dev_node_configuration.path.clone(),
|
||||
eth_proxy_binary: eth_rpc_configuration.path.clone(),
|
||||
substrate_binary: config.kitchensink.clone(),
|
||||
dev_node_binary: config.revive_dev_node.clone(),
|
||||
eth_proxy_binary: config.eth_proxy.clone(),
|
||||
rpc_url: String::new(),
|
||||
base_directory,
|
||||
logs_directory,
|
||||
process_substrate: None,
|
||||
process_proxy: None,
|
||||
wallet: wallet.clone(),
|
||||
wallet: Arc::new(wallet),
|
||||
chain_id_filler: Default::default(),
|
||||
nonce_manager: Default::default(),
|
||||
use_kitchensink_not_dev_node: kitchensink_configuration.use_kitchensink,
|
||||
use_kitchensink_not_dev_node: config.use_kitchensink_not_dev_node,
|
||||
// We know that we only need to be storing 4 files so we can specify that when creating
|
||||
// the vector. It's the stdout and stderr of the substrate-node and the eth-rpc.
|
||||
logs_file_to_flush: Vec::with_capacity(4),
|
||||
@@ -664,8 +655,8 @@ impl Node for KitchensinkNode {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
|
||||
self.init(genesis)?.spawn_process()
|
||||
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
|
||||
self.init(&genesis)?.spawn_process()
|
||||
}
|
||||
|
||||
fn version(&self) -> anyhow::Result<String> {
|
||||
@@ -1130,20 +1121,25 @@ impl BlockHeader for KitchenSinkHeader {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloy::rpc::types::TransactionRequest;
|
||||
use revive_dt_config::Arguments;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use std::fs;
|
||||
|
||||
use super::*;
|
||||
use crate::Node;
|
||||
use crate::{GENESIS_JSON, Node};
|
||||
|
||||
fn test_config() -> ExecutionContext {
|
||||
let mut context = ExecutionContext::default();
|
||||
context.kitchensink_configuration.use_kitchensink = true;
|
||||
context
|
||||
fn test_config() -> Arguments {
|
||||
Arguments {
|
||||
kitchensink: PathBuf::from("substrate-node"),
|
||||
eth_proxy: PathBuf::from("eth-rpc"),
|
||||
use_kitchensink_not_dev_node: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn new_node() -> (ExecutionContext, KitchensinkNode) {
|
||||
fn new_node() -> (KitchensinkNode, Arguments) {
|
||||
// Note: When we run the tests in the CI we found that if they're all
|
||||
// run in parallel then the CI is unable to start all of the nodes in
|
||||
// time and their start up times-out. Therefore, we want all of the
|
||||
@@ -1162,36 +1158,32 @@ mod tests {
|
||||
static NODE_START_MUTEX: Mutex<()> = Mutex::new(());
|
||||
let _guard = NODE_START_MUTEX.lock().unwrap();
|
||||
|
||||
let context = test_config();
|
||||
let mut node = KitchensinkNode::new(&context);
|
||||
node.init(context.genesis_configuration.genesis().unwrap().clone())
|
||||
let args = test_config();
|
||||
let mut node = KitchensinkNode::new(&args);
|
||||
node.init(GENESIS_JSON)
|
||||
.expect("Failed to initialize the node")
|
||||
.spawn_process()
|
||||
.expect("Failed to spawn the node process");
|
||||
(context, node)
|
||||
(node, args)
|
||||
}
|
||||
|
||||
/// A shared node that multiple tests can use. It starts up once.
|
||||
fn shared_node() -> &'static KitchensinkNode {
|
||||
static NODE: LazyLock<(ExecutionContext, KitchensinkNode)> = LazyLock::new(|| {
|
||||
let (context, node) = new_node();
|
||||
(context, node)
|
||||
static NODE: LazyLock<(KitchensinkNode, Arguments)> = LazyLock::new(|| {
|
||||
let (node, args) = new_node();
|
||||
(node, args)
|
||||
});
|
||||
&NODE.1
|
||||
&NODE.0
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
|
||||
// Arrange
|
||||
let (context, node) = new_node();
|
||||
let (node, args) = new_node();
|
||||
|
||||
let provider = node.provider().await.expect("Failed to create provider");
|
||||
|
||||
let account_address = context
|
||||
.wallet_configuration
|
||||
.wallet()
|
||||
.default_signer()
|
||||
.address();
|
||||
let account_address = args.wallet().default_signer().address();
|
||||
let transaction = TransactionRequest::default()
|
||||
.to(account_address)
|
||||
.value(U256::from(100_000_000_000_000u128));
|
||||
@@ -1225,9 +1217,7 @@ mod tests {
|
||||
let mut dummy_node = KitchensinkNode::new(&test_config());
|
||||
|
||||
// Call `init()`
|
||||
dummy_node
|
||||
.init(serde_json::from_str(genesis_content).unwrap())
|
||||
.expect("init failed");
|
||||
dummy_node.init(genesis_content).expect("init failed");
|
||||
|
||||
// Check that the patched chainspec file was generated
|
||||
let final_chainspec_path = dummy_node
|
||||
@@ -1338,9 +1328,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_works() {
|
||||
let node = shared_node();
|
||||
fn spawn_works() {
|
||||
let config = test_config();
|
||||
|
||||
let mut node = KitchensinkNode::new(&config);
|
||||
|
||||
node.spawn(GENESIS_JSON.to_string()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_works() {
|
||||
let config = test_config();
|
||||
|
||||
let node = KitchensinkNode::new(&config);
|
||||
let version = node.version().unwrap();
|
||||
|
||||
assert!(
|
||||
@@ -1351,8 +1351,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn eth_rpc_version_works() {
|
||||
let node = shared_node();
|
||||
let config = test_config();
|
||||
|
||||
let node = KitchensinkNode::new(&config);
|
||||
let version = node.eth_rpc_version().unwrap();
|
||||
|
||||
assert!(
|
||||
|
||||
+6
-14
@@ -1,8 +1,7 @@
|
||||
//! This crate implements the testing nodes.
|
||||
|
||||
use alloy::genesis::Genesis;
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
pub mod common;
|
||||
@@ -11,20 +10,13 @@ pub mod geth;
|
||||
pub mod kitchensink;
|
||||
pub mod pool;
|
||||
|
||||
/// The default genesis configuration.
|
||||
pub const GENESIS_JSON: &str = include_str!("../../../genesis.json");
|
||||
|
||||
/// An abstract interface for testing nodes.
|
||||
pub trait Node: EthereumNode {
|
||||
/// Create a new uninitialized instance.
|
||||
fn new(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
+ AsRef<ConcurrencyConfiguration>
|
||||
+ AsRef<GenesisConfiguration>
|
||||
+ AsRef<WalletConfiguration>
|
||||
+ AsRef<GethConfiguration>
|
||||
+ AsRef<KitchensinkConfiguration>
|
||||
+ AsRef<ReviveDevNodeConfiguration>
|
||||
+ AsRef<EthRpcConfiguration>
|
||||
+ Clone,
|
||||
) -> Self;
|
||||
fn new(config: &Arguments) -> Self;
|
||||
|
||||
/// Returns the identifier of the node.
|
||||
fn id(&self) -> usize;
|
||||
@@ -32,7 +24,7 @@ pub trait Node: EthereumNode {
|
||||
/// Spawns a node configured according to the genesis json.
|
||||
///
|
||||
/// Blocking until it's ready to accept transactions.
|
||||
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()>;
|
||||
fn spawn(&mut self, genesis: String) -> anyhow::Result<()>;
|
||||
|
||||
/// Prune the node instance and related data.
|
||||
///
|
||||
|
||||
+14
-42
@@ -5,13 +5,10 @@ use std::{
|
||||
thread,
|
||||
};
|
||||
|
||||
use alloy::genesis::Genesis;
|
||||
use anyhow::Context as _;
|
||||
use revive_dt_config::{
|
||||
ConcurrencyConfiguration, EthRpcConfiguration, GenesisConfiguration, GethConfiguration,
|
||||
KitchensinkConfiguration, ReviveDevNodeConfiguration, WalletConfiguration,
|
||||
WorkingDirectoryConfiguration,
|
||||
};
|
||||
use revive_dt_common::cached_fs::read_to_string;
|
||||
|
||||
use anyhow::Context;
|
||||
use revive_dt_config::Arguments;
|
||||
use tracing::info;
|
||||
|
||||
use crate::Node;
|
||||
@@ -28,31 +25,18 @@ where
|
||||
T: Node + Send + 'static,
|
||||
{
|
||||
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
|
||||
pub fn new(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
+ AsRef<ConcurrencyConfiguration>
|
||||
+ AsRef<GenesisConfiguration>
|
||||
+ AsRef<WalletConfiguration>
|
||||
+ AsRef<GethConfiguration>
|
||||
+ AsRef<KitchensinkConfiguration>
|
||||
+ AsRef<ReviveDevNodeConfiguration>
|
||||
+ AsRef<EthRpcConfiguration>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Clone
|
||||
+ 'static,
|
||||
) -> anyhow::Result<Self> {
|
||||
let concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context);
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
|
||||
let nodes = concurrency_configuration.number_of_nodes;
|
||||
let genesis = genesis_configuration.genesis()?;
|
||||
pub fn new(config: &Arguments) -> anyhow::Result<Self> {
|
||||
let nodes = config.number_of_nodes;
|
||||
let genesis = read_to_string(&config.genesis_file).context(format!(
|
||||
"can not read genesis file: {}",
|
||||
config.genesis_file.display()
|
||||
))?;
|
||||
|
||||
let mut handles = Vec::with_capacity(nodes);
|
||||
for _ in 0..nodes {
|
||||
let context = context.clone();
|
||||
let config = config.clone();
|
||||
let genesis = genesis.clone();
|
||||
handles.push(thread::spawn(move || spawn_node::<T>(context, genesis)));
|
||||
handles.push(thread::spawn(move || spawn_node::<T>(&config, genesis)));
|
||||
}
|
||||
|
||||
let mut nodes = Vec::with_capacity(nodes);
|
||||
@@ -80,20 +64,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_node<T: Node + Send>(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
+ AsRef<ConcurrencyConfiguration>
|
||||
+ AsRef<GenesisConfiguration>
|
||||
+ AsRef<WalletConfiguration>
|
||||
+ AsRef<GethConfiguration>
|
||||
+ AsRef<KitchensinkConfiguration>
|
||||
+ AsRef<ReviveDevNodeConfiguration>
|
||||
+ AsRef<EthRpcConfiguration>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
genesis: Genesis,
|
||||
) -> anyhow::Result<T> {
|
||||
let mut node = T::new(context);
|
||||
fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> {
|
||||
let mut node = T::new(args);
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
|
||||
@@ -12,7 +12,7 @@ use alloy_primitives::Address;
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
|
||||
use revive_dt_config::{Context, TestingPlatform};
|
||||
use revive_dt_config::{Arguments, TestingPlatform};
|
||||
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
|
||||
use semver::Version;
|
||||
use serde::Serialize;
|
||||
@@ -36,11 +36,11 @@ pub struct ReportAggregator {
|
||||
}
|
||||
|
||||
impl ReportAggregator {
|
||||
pub fn new(context: Context) -> Self {
|
||||
pub fn new(config: Arguments) -> Self {
|
||||
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
||||
let (listener_tx, _) = channel::<ReporterEvent>(1024);
|
||||
Self {
|
||||
report: Report::new(context),
|
||||
report: Report::new(config),
|
||||
remaining_cases: Default::default(),
|
||||
runner_tx: Some(runner_tx),
|
||||
runner_rx,
|
||||
@@ -121,12 +121,7 @@ impl ReportAggregator {
|
||||
file_name.push_str(".json");
|
||||
file_name
|
||||
};
|
||||
let file_path = self
|
||||
.report
|
||||
.context
|
||||
.working_directory_configuration()
|
||||
.as_path()
|
||||
.join(file_name);
|
||||
let file_path = self.report.config.directory().join(file_name);
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
@@ -287,16 +282,8 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PreLinkContractsCompilationSucceededEvent,
|
||||
) {
|
||||
let include_input = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_input;
|
||||
let include_output = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_output;
|
||||
let include_input = self.report.config.report_include_compiler_input;
|
||||
let include_output = self.report.config.report_include_compiler_output;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
@@ -324,16 +311,8 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PostLinkContractsCompilationSucceededEvent,
|
||||
) {
|
||||
let include_input = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_input;
|
||||
let include_output = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_output;
|
||||
let include_input = self.report.config.report_include_compiler_input;
|
||||
let include_output = self.report.config.report_include_compiler_output;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
@@ -427,8 +406,12 @@ impl ReportAggregator {
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Report {
|
||||
/// The context that the tool was started up with.
|
||||
pub context: Context,
|
||||
/// The configuration that the tool was started up with.
|
||||
pub config: Arguments,
|
||||
/// The platform of the leader chain.
|
||||
pub leader_platform: TestingPlatform,
|
||||
/// The platform of the follower chain.
|
||||
pub follower_platform: TestingPlatform,
|
||||
/// The list of corpus files that the tool found.
|
||||
pub corpora: Vec<Corpus>,
|
||||
/// The list of metadata files that were found by the tool.
|
||||
@@ -440,9 +423,11 @@ pub struct Report {
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn new(context: Context) -> Self {
|
||||
pub fn new(config: Arguments) -> Self {
|
||||
Self {
|
||||
context,
|
||||
leader_platform: config.leader,
|
||||
follower_platform: config.follower,
|
||||
config,
|
||||
corpora: Default::default(),
|
||||
metadata_files: Default::default(),
|
||||
test_case_information: Default::default(),
|
||||
@@ -532,12 +517,12 @@ pub enum CompilationStatus {
|
||||
/// The path of the compiler used to compile the contracts.
|
||||
compiler_path: PathBuf,
|
||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
||||
/// the appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||
/// the compiler was invoked.
|
||||
/// the appropriate flag is set in the CLI configuration and if the contracts were not
|
||||
/// cached and the compiler was invoked.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_input: Option<CompilerInput>,
|
||||
/// The output of the compiler. This is only included if the appropriate flag is set in the
|
||||
/// CLI contexts.
|
||||
/// CLI configurations.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_output: Option<CompilerOutput>,
|
||||
},
|
||||
@@ -552,8 +537,8 @@ pub enum CompilationStatus {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_path: Option<PathBuf>,
|
||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
||||
/// the appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||
/// the compiler was invoked.
|
||||
/// the appropriate flag is set in the CLI configuration and if the contracts were not
|
||||
/// cached and the compiler was invoked.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_input: Option<CompilerInput>,
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ use semver::Version;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::download::SolcDownloader;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
|
||||
pub const SOLC_CACHE_DIRECTORY: &str = "solc";
|
||||
pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
|
||||
|
||||
@@ -11,7 +11,7 @@ use semver::Version;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::list::List;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
|
||||
pub static LIST_CACHE: LazyLock<Mutex<HashMap<&'static str, List>>> =
|
||||
LazyLock::new(Default::default);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
use cache::get_or_download;
|
||||
use download::SolcDownloader;
|
||||
|
||||
|
||||
+6
-6
@@ -89,13 +89,13 @@ echo "This may take a while..."
|
||||
echo ""
|
||||
|
||||
# Run the tool
|
||||
RUST_LOG="error" cargo run --release -- execute-tests \
|
||||
RUST_LOG="error" cargo run --release -- \
|
||||
--corpus "$CORPUS_FILE" \
|
||||
--working-directory "$WORKDIR" \
|
||||
--concurrency.number-of-nodes 5 \
|
||||
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
|
||||
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
|
||||
--eth-rpc.path "$ETH_RPC_BIN" \
|
||||
--workdir "$WORKDIR" \
|
||||
--number-of-nodes 5 \
|
||||
--kitchensink "$SUBSTRATE_NODE_BIN" \
|
||||
--revive-dev-node "$REVIVE_DEV_NODE_BIN" \
|
||||
--eth_proxy "$ETH_RPC_BIN" \
|
||||
> logs.log \
|
||||
2> output.log
|
||||
|
||||
|
||||
-497
@@ -1,497 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Metadata",
|
||||
"description": "A MatterLabs metadata file.\n\nThis defines the structure that the MatterLabs metadata files follow for defining the tests or\nthe workloads.\n\nEach metadata file is composed of multiple test cases where each test case is isolated from the\nothers and runs in a completely different address space. Each test case is composed of a number\nof steps and assertions that should be performed as part of the test case.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "This is an optional comment on the metadata file which has no impact on the execution in any\nway.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ignore": {
|
||||
"description": "An optional boolean which defines if the metadata file as a whole should be ignored. If null\nthen the metadata file will not be ignored.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"targets": {
|
||||
"description": "An optional vector of targets that this Metadata file's cases can be executed on. As an\nexample, if we wish for the metadata file's cases to only be run on PolkaVM then we'd\nspecify a target of \"PolkaVM\" in here.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"cases": {
|
||||
"description": "A vector of the test cases and workloads contained within the metadata file. This is their\nprimary description.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Case"
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"description": "A map of all of the contracts that the test requires to run.\n\nThis is a map where the key is the name of the contract instance and the value is the\ncontract's path and ident in the file.\n\nIf any contract is to be used by the test then it must be included in here first so that the\nframework is aware of its path, compiles it, and prepares it.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/ContractPathAndIdent"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"description": "The set of libraries that this metadata file requires.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/ContractInstance"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modes": {
|
||||
"description": "This represents a mode that has been parsed from test metadata.\n\nMode strings can take the following form (in pseudo-regex):\n\n```text\n[YEILV][+-]? (M[0123sz])? <semver>?\n```",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/ParsedMode"
|
||||
}
|
||||
},
|
||||
"required_evm_version": {
|
||||
"description": "This field specifies an EVM version requirement that the test case has where the test might\nbe run of the evm version of the nodes match the evm version specified here.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/EvmVersionRequirement"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"compiler_directives": {
|
||||
"description": "A set of compilation directives that will be passed to the compiler whenever the contracts\nfor the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]\nis just a filter for when a test can run whereas this is an instruction to the compiler.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CompilationDirectives"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cases"
|
||||
],
|
||||
"$defs": {
|
||||
"Case": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "An optional name of the test case.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"comment": {
|
||||
"description": "An optional comment on the case which has no impact on the execution in any way.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modes": {
|
||||
"description": "This represents a mode that has been parsed from test metadata.\n\nMode strings can take the following form (in pseudo-regex):\n\n```text\n[YEILV][+-]? (M[0123sz])? <semver>?\n```\n\nIf this is provided then it takes higher priority than the modes specified in the metadata\nfile.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/ParsedMode"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"description": "The set of steps to run as part of this test case.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Step"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"description": "An optional name of the group of tests that this test belongs to.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"description": "An optional set of expectations and assertions to make about the transaction after it ran.\n\nIf this is not specified then the only assertion that will be ran is that the transaction\nwas successful.\n\nThis expectation that's on the case itself will be attached to the final step of the case.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Expected"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignore": {
|
||||
"description": "An optional boolean which defines if the case as a whole should be ignored. If null then the\ncase will not be ignored.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"inputs"
|
||||
]
|
||||
},
|
||||
"ParsedMode": {
|
||||
"description": "This represents a mode that has been parsed from test metadata.\n\nMode strings can take the following form (in pseudo-regex):\n\n```text\n[YEILV][+-]? (M[0123sz])? <semver>?\n```\n\nWe can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].",
|
||||
"type": "string"
|
||||
},
|
||||
"Step": {
|
||||
"description": "A test step.\n\nA test step can be anything. It could be an invocation to a function, an assertion, or any other\naction that needs to be run or executed on the nodes used in the tests.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A function call or an invocation to some function on some smart contract.",
|
||||
"$ref": "#/$defs/Input"
|
||||
},
|
||||
{
|
||||
"description": "A step for performing a balance assertion on some account or contract.",
|
||||
"$ref": "#/$defs/BalanceAssertion"
|
||||
},
|
||||
{
|
||||
"description": "A step for asserting that the storage of some contract or account is empty.",
|
||||
"$ref": "#/$defs/StorageEmptyAssertion"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Input": {
|
||||
"description": "This is an input step which is a transaction description that the framework translates into a\ntransaction and executes on the nodes.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"caller": {
|
||||
"description": "The address of the account performing the call and paying the fees for it.",
|
||||
"type": "string",
|
||||
"default": "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1"
|
||||
},
|
||||
"comment": {
|
||||
"description": "An optional comment on the step which has no impact on the execution in any way.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instance": {
|
||||
"description": "The contract instance that's being called in this transaction step.",
|
||||
"$ref": "#/$defs/ContractInstance",
|
||||
"default": "Test"
|
||||
},
|
||||
"method": {
|
||||
"description": "The method that's being called in this step.",
|
||||
"$ref": "#/$defs/Method"
|
||||
},
|
||||
"calldata": {
|
||||
"description": "The calldata that the function should be invoked with.",
|
||||
"$ref": "#/$defs/Calldata",
|
||||
"default": []
|
||||
},
|
||||
"expected": {
|
||||
"description": "A set of assertions and expectations to have for the transaction.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Expected"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"description": "An optional value to provide as part of the transaction.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/EtherValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable_assignments": {
|
||||
"description": "Variable assignment to perform in the framework allowing us to reference them again later on\nduring the execution.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/VariableAssignments"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"ContractInstance": {
|
||||
"description": "Represents a contract instance found a metadata file.\n\nTypically, this is used as the key to the \"contracts\" field of metadata files.",
|
||||
"type": "string"
|
||||
},
|
||||
"Method": {
|
||||
"description": "Specify how the contract is called.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Initiate a deploy transaction, calling contracts constructor.\n\nIndicated by `#deployer`.",
|
||||
"type": "string",
|
||||
"const": "#deployer"
|
||||
},
|
||||
{
|
||||
"description": "Does not calculate and insert a function selector.\n\nIndicated by `#fallback`.",
|
||||
"type": "string",
|
||||
"const": "#fallback"
|
||||
},
|
||||
{
|
||||
"description": "Call the public function with the given name.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Calldata": {
|
||||
"description": "A type definition for the calldata supported by the testing framework.\n\nWe choose to document all of the types used in [`Calldata`] in this one doc comment to elaborate\non why they exist and consolidate all of the documentation for calldata in a single place where\nit can be viewed and understood.\n\nThe [`Single`] variant of this enum is quite simple and straightforward: it's a hex-encoded byte\narray of the calldata.\n\nThe [`Compound`] type is more intricate and allows for capabilities such as resolution and some\nsimple arithmetic operations. It houses a vector of [`CalldataItem`]s which is just a wrapper\naround an owned string.\n\nA [`CalldataItem`] could be a simple hex string of a single calldata argument, but it could also\nbe something that requires resolution such as `MyContract.address` which is a variable that is\nunderstood by the resolution logic to mean \"Lookup the address of this particular contract\ninstance\".\n\nIn addition to the above, the format supports some simple arithmetic operations like add, sub,\ndivide, multiply, bitwise AND, bitwise OR, and bitwise XOR. Our parser understands the [reverse\npolish notation] simply because it's easy to write a calculator for that notation and since we\ndo not have plans to use arithmetic too often in tests. In reverse polish notation a typical\n`2 + 4` would be written as `2 4 +` which makes this notation very simple to implement through\na stack.\n\nCombining the above, a single [`CalldataItem`] could employ both resolution and arithmetic at\nthe same time. For example, a [`CalldataItem`] of `$BLOCK_NUMBER $BLOCK_NUMBER +` means that\nthe block number should be retrieved and then it should be added to itself.\n\nInternally, we split the [`CalldataItem`] by spaces. Therefore, `$BLOCK_NUMBER $BLOCK_NUMBER+`\nis invalid but `$BLOCK_NUMBER $BLOCK_NUMBER +` is valid and can be understood by the parser and\ncalculator. After the split is done, each token is parsed into a [`CalldataToken<&str>`] forming\nan [`Iterator`] over [`CalldataToken<&str>`]. A [`CalldataToken<&str>`] can then be resolved\ninto a [`CalldataToken<U256>`] through the resolution logic. Finally, after resolution is done,\nthis iterator of [`CalldataToken<U256>`] is collapsed into the final result by applying the\narithmetic operations requested.\n\nFor example, supplying a [`Compound`] calldata of `0xdeadbeef` produces an iterator of a single\n[`CalldataToken<&str>`] items of the value [`CalldataToken::Item`] of the string value 12 which\nwe can then resolve into the appropriate [`U256`] value and convert into calldata.\n\nIn summary, the various types used in [`Calldata`] represent the following:\n- [`CalldataItem`]: A calldata string from the metadata files.\n- [`CalldataToken<&str>`]: Typically used in an iterator of items from the space splitted\n [`CalldataItem`] and represents a token that has not yet been resolved into its value.\n- [`CalldataToken<U256>`]: Represents a token that's been resolved from being a string and into\n the word-size calldata argument on which we can perform arithmetic.\n\n[`Single`]: Calldata::Single\n[`Compound`]: Calldata::Compound\n[reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/CalldataItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"CalldataItem": {
|
||||
"description": "This represents an item in the [`Calldata::Compound`] variant. Each item will be resolved\naccording to the resolution rules of the tool.",
|
||||
"type": "string"
|
||||
},
|
||||
"Expected": {
|
||||
"description": "A set of expectations and assertions to make about the transaction after it ran.\n\nIf this is not specified then the only assertion that will be ran is that the transaction\nwas successful.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "An assertion that the transaction succeeded and returned the provided set of data.",
|
||||
"$ref": "#/$defs/Calldata"
|
||||
},
|
||||
{
|
||||
"description": "A more complex assertion.",
|
||||
"$ref": "#/$defs/ExpectedOutput"
|
||||
},
|
||||
{
|
||||
"description": "A set of assertions.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ExpectedOutput"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExpectedOutput": {
|
||||
"description": "A set of assertions to run on the transaction.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"compiler_version": {
|
||||
"description": "An optional compiler version that's required in order for this assertion to run.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"return_data": {
|
||||
"description": "An optional field of the expected returns from the invocation.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Calldata"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": {
|
||||
"description": "An optional set of assertions to run on the emitted events from the transaction.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/Event"
|
||||
}
|
||||
},
|
||||
"exception": {
|
||||
"description": "A boolean which defines whether we expect the transaction to succeed or fail.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "An optional field of the address of the emitter of the event.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"topics": {
|
||||
"description": "The set of topics to expect the event to have.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"values": {
|
||||
"description": "The set of values to expect the event to have.",
|
||||
"$ref": "#/$defs/Calldata"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"topics",
|
||||
"values"
|
||||
]
|
||||
},
|
||||
"EtherValue": {
|
||||
"description": "Defines an Ether value.\n\nThis is an unsigned 256 bit integer that's followed by some denomination which can either be\neth, ether, gwei, or wei.",
|
||||
"type": "string"
|
||||
},
|
||||
"VariableAssignments": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"return_data": {
|
||||
"description": "A vector of the variable names to assign to the return data.\n\nExample: `UniswapV3PoolAddress`",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"return_data"
|
||||
]
|
||||
},
|
||||
"BalanceAssertion": {
|
||||
"description": "This represents a balance assertion step where the framework needs to query the balance of some\naccount or contract and assert that it's some amount.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "An optional comment on the balance assertion.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"address": {
|
||||
"description": "The address that the balance assertion should be done on.\n\nThis is a string which will be resolved into an address when being processed. Therefore,\nthis could be a normal hex address, a variable such as `Test.address`, or perhaps even a\nfull on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are\nfollowed in the calldata.",
|
||||
"type": "string"
|
||||
},
|
||||
"expected_balance": {
|
||||
"description": "The amount of balance to assert that the account or contract has. This is a 256 bit string\nthat's serialized and deserialized into a decimal string.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"address",
|
||||
"expected_balance"
|
||||
]
|
||||
},
|
||||
"StorageEmptyAssertion": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "An optional comment on the storage empty assertion.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"address": {
|
||||
"description": "The address that the balance assertion should be done on.\n\nThis is a string which will be resolved into an address when being processed. Therefore,\nthis could be a normal hex address, a variable such as `Test.address`, or perhaps even a\nfull on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are\nfollowed in the calldata.",
|
||||
"type": "string"
|
||||
},
|
||||
"is_storage_empty": {
|
||||
"description": "A boolean of whether the storage of the address is empty or not.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"address",
|
||||
"is_storage_empty"
|
||||
]
|
||||
},
|
||||
"ContractPathAndIdent": {
|
||||
"description": "Represents an identifier used for contracts.\n\nThe type supports serialization from and into the following string format:\n\n```text\n${path}:${contract_ident}\n```",
|
||||
"type": "string"
|
||||
},
|
||||
"EvmVersionRequirement": {
|
||||
"description": "An EVM version requirement that the test case has. This gets serialized and deserialized from\nand into [`String`]. This follows a simple format of (>=|<=|=|>|<) followed by a string of the\nEVM version.\n\nWhen specified, the framework will only run the test if the node's EVM version matches that\nrequired by the metadata file.",
|
||||
"type": "string"
|
||||
},
|
||||
"CompilationDirectives": {
|
||||
"description": "A set of compilation directives that will be passed to the compiler whenever the contracts for\nthe test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is\njust a filter for when a test can run whereas this is an instruction to the compiler.\nDefines how the compiler should handle revert strings.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revert_string_handling": {
|
||||
"description": "Defines how the revert strings should be handled.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RevertString"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RevertString": {
|
||||
"description": "Defines how the compiler should handle revert strings.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The default handling of the revert strings.",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
},
|
||||
{
|
||||
"description": "The debug handling of the revert strings.",
|
||||
"type": "string",
|
||||
"const": "debug"
|
||||
},
|
||||
{
|
||||
"description": "Strip the revert strings.",
|
||||
"type": "string",
|
||||
"const": "strip"
|
||||
},
|
||||
{
|
||||
"description": "Provide verbose debug strings for the revert string.",
|
||||
"type": "string",
|
||||
"const": "verboseDebug"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user