the test driver

Signed-off-by: xermicus <bigcyrill@hotmail.com>
This commit is contained in:
xermicus
2025-03-24 15:48:26 +01:00
parent 9bba37b7a9
commit ad4901550d
12 changed files with 217 additions and 74 deletions
Generated
+2
View File
@@ -2873,6 +2873,7 @@ dependencies = [
name = "revive-dt-core"
version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"clap",
"env_logger",
@@ -2882,6 +2883,7 @@ dependencies = [
"revive-dt-config",
"revive-dt-format",
"revive-dt-node",
"revive-dt-node-interaction",
"revive-solc-json-interface",
"semver 1.0.26",
"serde",
+49 -10
View File
@@ -3,7 +3,7 @@
//! - Polkadot revive resolc compiler
//! - Polkadot revive Wasm compiler
use std::{fs::read_to_string, path::Path};
use std::{fs::read_to_string, hash::Hash, path::Path};
use revive_common::EVMVersion;
use revive_solc_json_interface::{
@@ -20,21 +20,57 @@ pub mod solc;
/// A common interface for all supported Solidity compilers.
pub trait SolidityCompiler {
/// Extra options specific to the compiler.
type Options;
type Options: Default + PartialEq + Eq + Hash;
/// The low-level compiler interface.
fn build(
&self,
input: &SolcStandardJsonInput,
extra_options: &Option<Self::Options>,
) -> anyhow::Result<SolcStandardJsonOutput>;
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>>;
fn new(solc_version: &Version) -> Self;
}
/// The generic compilation input configuration.
#[derive(Debug)]
pub struct CompilerInput<T: PartialEq + Eq + Hash> {
pub extra_options: T,
pub input: SolcStandardJsonInput,
}
/// The generic compilation output configuration.
pub struct CompilerOutput<T: PartialEq + Eq + Hash> {
pub input: CompilerInput<T>,
pub output: SolcStandardJsonOutput,
}
impl<T> PartialEq for CompilerInput<T>
where
T: PartialEq + Eq + Hash,
{
fn eq(&self, other: &Self) -> bool {
let self_input = serde_json::to_vec(&self.input).unwrap_or_default();
let other_input = serde_json::to_vec(&self.input).unwrap_or_default();
self.extra_options.eq(&other.extra_options) && self_input == other_input
}
}
impl<T> Eq for CompilerInput<T> where T: PartialEq + Eq + Hash {}
impl<T> Hash for CompilerInput<T>
where
T: PartialEq + Eq + Hash,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.extra_options.hash(state);
state.write(&serde_json::to_vec(&self.input).unwrap_or_default());
}
}
/// A generic builder style interface for configuring all compiler options.
pub struct Compiler<T: SolidityCompiler> {
input: SolcStandardJsonInput,
extra_options: Option<T::Options>,
extra_options: T::Options,
allow_paths: Vec<String>,
base_path: Option<String>,
}
@@ -68,7 +104,7 @@ where
None,
),
},
extra_options: None,
extra_options: Default::default(),
allow_paths: Default::default(),
base_path: None,
}
@@ -92,7 +128,7 @@ where
}
pub fn extra_options(mut self, extra_options: T::Options) -> Self {
self.extra_options = Some(extra_options);
self.extra_options = extra_options;
self
}
@@ -106,7 +142,10 @@ where
self
}
pub fn try_build(&self, solc_version: &Version) -> anyhow::Result<SolcStandardJsonOutput> {
T::new(solc_version).build(&self.input, &self.extra_options)
pub fn try_build(self, solc_version: &Version) -> anyhow::Result<CompilerOutput<T::Options>> {
T::new(solc_version).build(CompilerInput {
extra_options: self.extra_options,
input: self.input,
})
}
}
+8 -7
View File
@@ -6,10 +6,9 @@ use std::{
process::{Command, Stdio},
};
use revive_solc_json_interface::{SolcStandardJsonInput, SolcStandardJsonOutput};
use semver::Version;
use crate::SolidityCompiler;
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
pub struct Solc {
binary_path: PathBuf,
@@ -20,9 +19,8 @@ impl SolidityCompiler for Solc {
fn build(
&self,
input: &SolcStandardJsonInput,
_extra_options: &Option<Self::Options>,
) -> anyhow::Result<SolcStandardJsonOutput> {
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>> {
let mut child = Command::new(&self.binary_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -31,10 +29,13 @@ impl SolidityCompiler for Solc {
.spawn()?;
let stdin = child.stdin.as_mut().expect("should be piped");
serde_json::to_writer(stdin, input)?;
serde_json::to_writer(stdin, &input.input)?;
let output = child.wait_with_output()?.stdout;
Ok(serde_json::from_slice(&output)?)
Ok(CompilerOutput {
input,
output: serde_json::from_slice(&output)?,
})
}
fn new(_solc_version: &Version) -> Self {
+25 -1
View File
@@ -2,7 +2,7 @@
use std::path::PathBuf;
use clap::Parser;
use clap::{Parser, ValueEnum};
#[derive(Debug, Parser, Clone)]
#[command(name = "retester")]
@@ -48,6 +48,18 @@ pub struct Arguments {
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
)]
pub account: String,
/// The differential testing leader node implementation.
#[arg(short, long = "leader", default_value = "geth")]
pub leader: TestingPlatform,
/// The differential testing follower node implementation.
#[arg(short, long = "follower", default_value = "kitchensink")]
pub follower: TestingPlatform,
/// Only compile against this testing platform (doesn't execute the tests).
#[arg(short, long = "compile-only")]
pub compile_only: bool,
}
impl Default for Arguments {
@@ -55,3 +67,15 @@ impl Default for Arguments {
Arguments::parse_from(["retester"])
}
}
/// The Solidity compatible node implementation.
///
/// This describes the solutions to be tested against on a high level.
#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)]
#[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 implentation.
Kitchensink,
}
+2
View File
@@ -18,7 +18,9 @@ revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-solc-json-interface = { workspace = true }
revive-dt-node = { workspace = true }
revive-dt-node-interaction = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
log = { workspace = true }
+6 -19
View File
@@ -2,7 +2,7 @@
use std::collections::HashMap;
use revive_dt_compiler::{Compiler, solc::Solc};
use revive_dt_compiler::{Compiler, CompilerInput, SolidityCompiler};
use revive_dt_format::{
metadata::Metadata,
mode::{Mode, SolcMode},
@@ -10,15 +10,9 @@ use revive_dt_format::{
use revive_solc_json_interface::SolcStandardJsonOutput;
use semver::Version;
#[derive(Hash, Eq, PartialEq)]
pub struct SolcSettings {
pub optimizer: bool,
pub solc_version: Version,
}
pub fn build_evm(
pub fn build<T: SolidityCompiler>(
metadata: &Metadata,
) -> anyhow::Result<HashMap<SolcSettings, SolcStandardJsonOutput>> {
) -> anyhow::Result<HashMap<CompilerInput<T::Options>, SolcStandardJsonOutput>> {
let sources = metadata.contract_sources()?;
let base_path = metadata.directory()?.display().to_string();
let modes = metadata
@@ -28,7 +22,7 @@ pub fn build_evm(
let mut result = HashMap::new();
for mode in modes {
let mut compiler = Compiler::<Solc>::new().base_path(base_path.clone());
let mut compiler = Compiler::<T>::new().base_path(base_path.clone());
for (file, _contract) in sources.values() {
compiler = compiler.with_source(file)?;
}
@@ -41,15 +35,8 @@ pub fn build_evm(
}) => {
let optimizer = solc_optimize.unwrap_or(true);
let version = Version::new(0, 8, 29);
let out = compiler.solc_optimizer(optimizer).try_build(&version)?;
result.insert(
SolcSettings {
optimizer: true,
solc_version: version,
},
out,
);
let output = compiler.solc_optimizer(optimizer).try_build(&version)?;
result.insert(output.input, output.output);
}
Mode::Unknown(mode) => log::debug!("compiler: ignoring unknown mode '{mode}'"),
}
+56
View File
@@ -1,4 +1,60 @@
//! The test driver handles the compilation and execution of the test cases.
use alloy::primitives::map::HashMap;
use compiler::build;
use revive_dt_compiler::{CompilerInput, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::metadata::Metadata;
use revive_dt_node::Node;
use revive_solc_json_interface::SolcStandardJsonOutput;
use crate::Platform;
pub mod compiler;
pub mod input;
type Contracts<T> = HashMap<
CompilerInput<<<T as Platform>::Compiler as SolidityCompiler>::Options>,
SolcStandardJsonOutput,
>;
pub struct Driver<'a, Leader: Platform, Follower: Platform> {
metadata: &'a Metadata,
config: &'a Arguments,
leader_contracts: Contracts<Leader>,
leader_node: <Leader as Platform>::Blockchain,
follower_contracts: Contracts<Follower>,
follower_node: <Follower as Platform>::Blockchain,
}
impl<'a, L, F> Driver<'a, L, F>
where
L: Platform + Default,
F: Platform + Default,
{
pub fn new(metadata: &'a Metadata, config: &'a Arguments) -> Driver<'a, L, F> {
Self {
metadata,
config,
leader_node: <<L as Platform>::Blockchain as Node>::new(config),
leader_contracts: Default::default(),
follower_node: <<F as Platform>::Blockchain as Node>::new(config),
follower_contracts: Default::default(),
}
}
pub fn execute(&mut self) -> anyhow::Result<()> {
self.leader_contracts = build::<L::Compiler>(self.metadata)?;
self.follower_contracts = build::<F::Compiler>(self.metadata)?;
if self.config.compile_only {
return Ok(());
}
todo!()
}
}
+27 -2
View File
@@ -2,7 +2,32 @@
//!
//! This crate defines the testing configuration and
//! provides a helper utilty to execute tests.
//!
//!
use revive_dt_compiler::{SolidityCompiler, solc};
use revive_dt_node::{Node, geth};
pub mod driver;
/// One platform can be tested differentially against another.
///
/// For this we need a blockchain node implementation and a compiler.
pub trait Platform {
type Blockchain: Node;
type Compiler: SolidityCompiler;
}
#[derive(Default)]
pub struct Geth;
impl Platform for Geth {
type Blockchain = geth::Instance;
type Compiler = solc::Solc;
}
#[derive(Default)]
pub struct Kitchensink;
impl Platform for Kitchensink {
type Blockchain = geth::Instance;
type Compiler = solc::Solc;
}
+12 -6
View File
@@ -4,7 +4,7 @@ use clap::Parser;
use rayon::prelude::*;
use revive_dt_config::*;
use revive_dt_core::driver::compiler::build_evm;
use revive_dt_core::{Geth, Kitchensink, driver::Driver};
use revive_dt_format::corpus::Corpus;
use temp_dir::TempDir;
@@ -27,22 +27,28 @@ fn main() -> anyhow::Result<()> {
log::info!("found {} tests", tests.len());
tests.par_iter().for_each(|metadata| {
let _ = match build_evm(metadata) {
let mut driver = match (&args.leader, &args.follower) {
(TestingPlatform::Geth, TestingPlatform::Kitchensink) => {
Driver::<Geth, Kitchensink>::new(metadata, &args)
}
_ => unimplemented!(),
};
match driver.execute() {
Ok(build) => {
log::info!(
"metadata {} compilation success",
"metadata {} success",
metadata.file_path.as_ref().unwrap().display()
);
build
}
Err(error) => {
log::warn!(
"metadata {} compilation failure: {error:?}",
"metadata {} failure: {error:?}",
metadata.file_path.as_ref().unwrap().display()
);
return;
}
};
}
});
}
+4 -4
View File
@@ -4,12 +4,12 @@ use serde::{Deserialize, de::Deserializer};
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Input {
instance: String,
pub instance: String,
#[serde(deserialize_with = "deserialize_method")]
method: Method,
pub method: Method,
#[serde(deserialize_with = "deserialize_calldata")]
calldata: Vec<u8>,
expected: Option<Vec<String>>,
pub calldata: Vec<u8>,
pub expected: Option<Vec<String>>,
}
/// Specify how the contract is called.
+22 -25
View File
@@ -52,28 +52,6 @@ impl Instance {
const READY_MARKER: &str = "IPC endpoint opened";
const ERROR_MARKER: &str = "Fatal:";
/// Create a new uninitialized instance.
pub fn new(config: &Arguments) -> anyhow::Result<Self> {
let geth_directory = config
.working_directory
.as_ref()
.ok_or_else(|| anyhow::anyhow!("config did not provide working directory"))?
.join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = geth_directory.join(id.to_string());
Ok(Self {
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
data_directory: base_directory.join(Self::DATA_DIRECTORY),
base_directory,
geth: config.geth.clone(),
id,
handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
})
}
/// Create the node directory and call `geth init` to configure the genesis.
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
let geth_directory = self.base_directory.parent().expect("the id should be set");
@@ -182,6 +160,27 @@ impl EthereumNode for Instance {
}
impl Node for Instance {
fn new(config: &Arguments) -> Self {
let geth_directory = config
.working_directory
.as_ref()
.expect("config should provide working directory")
.join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = geth_directory.join(id.to_string());
Self {
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
data_directory: base_directory.join(Self::DATA_DIRECTORY),
base_directory,
geth: config.geth.clone(),
id,
handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
}
}
fn connection_string(&self) -> String {
self.connection_string.clone()
}
@@ -252,7 +251,6 @@ mod tests {
#[test]
fn init_works() {
Instance::new(&test_config().0)
.unwrap()
.init(GENESIS_JSON.to_string())
.unwrap();
}
@@ -260,14 +258,13 @@ mod tests {
#[test]
fn spawn_works() {
Instance::new(&test_config().0)
.unwrap()
.spawn(GENESIS_JSON.to_string())
.unwrap();
}
#[test]
fn version_works() {
let version = Instance::new(&test_config().0).unwrap().version().unwrap();
let version = Instance::new(&test_config().0).version().unwrap();
assert!(
version.starts_with("geth version"),
"expected version string, got: '{version}'"
+4
View File
@@ -1,6 +1,7 @@
//! This crate implements the testing nodes.
use alloy::rpc::types::{TransactionReceipt, trace::geth::DiffMode};
use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode;
pub mod geth;
@@ -10,6 +11,9 @@ 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(config: &Arguments) -> Self;
/// Spawns a node configured according to the genesis json.
///
/// Blocking until it's ready to accept transactions.