mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-22 10:17:56 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cec992f80a | |||
| b53550e43e | |||
| 9491263857 | |||
| b41c0e61c0 | |||
| 9d1c71756f | |||
| 8b0a0c3518 |
@@ -41,6 +41,10 @@ inputs:
|
||||
description: "The id of the parachain to spawn with the polkadot-omni-node. This is only required if the polkadot-omni-node is one of the selected platforms."
|
||||
type: number
|
||||
required: false
|
||||
expectations-file-path:
|
||||
description: "Path to the expectations file to use to compare against."
|
||||
type: string
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -79,6 +83,12 @@ runs:
|
||||
run: |
|
||||
${{ inputs['cargo-command'] }} build --locked --profile release -p pallet-revive-eth-rpc -p revive-dev-node --manifest-path ${{ inputs['polkadot-sdk-path'] }}/Cargo.toml
|
||||
${{ inputs['cargo-command'] }} build --locked --profile release --bin polkadot-omni-node --manifest-path ${{ inputs['polkadot-sdk-path'] }}/Cargo.toml
|
||||
- name: Installing retester
|
||||
shell: bash
|
||||
run: ${{ inputs['cargo-command'] }} install --path ./revive-differential-tests/crates/core
|
||||
- name: Installing report-processor
|
||||
shell: bash
|
||||
run: ${{ inputs['cargo-command'] }} install --path ./revive-differential-tests/crates/report-processor
|
||||
- name: Running the Differential Tests
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -96,11 +106,12 @@ runs:
|
||||
)
|
||||
fi
|
||||
|
||||
${{ inputs['cargo-command'] }} run --locked --manifest-path revive-differential-tests/Cargo.toml -- test \
|
||||
retester test \
|
||||
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/simple \
|
||||
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/complex \
|
||||
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/translated_semantic_tests \
|
||||
--platform ${{ inputs['platform'] }} \
|
||||
--report.file-name report.json \
|
||||
--concurrency.number-of-nodes 10 \
|
||||
--concurrency.number-of-threads 10 \
|
||||
--concurrency.number-of-concurrent-tasks 100 \
|
||||
@@ -110,22 +121,21 @@ runs:
|
||||
--eth-rpc.path ${{ inputs['polkadot-sdk-path'] }}/target/release/eth-rpc \
|
||||
--polkadot-omni-node.path ${{ inputs['polkadot-sdk-path'] }}/target/release/polkadot-omni-node \
|
||||
--resolc.path ./resolc \
|
||||
"${OMNI_ARGS[@]}"
|
||||
- name: Creating a markdown report of the test execution
|
||||
"${OMNI_ARGS[@]}" || true
|
||||
- name: Generate the expectation file
|
||||
shell: bash
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
mv ./workdir/*.json report.json
|
||||
python3 revive-differential-tests/scripts/process-differential-tests-report.py report.json ${{ inputs['platform'] }}
|
||||
run: report-processor generate-expectations-file --report-path ./workdir/report.json --output-path ./workdir/expectations.json --remove-prefix ./revive-differential-tests/resolc-compiler-tests
|
||||
- name: Upload the Report to the CI
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: report-${{ inputs['platform'] }}.md
|
||||
path: report.md
|
||||
- name: Posting the report as a comment on the PR
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405
|
||||
if: ${{ always() }}
|
||||
name: ${{ inputs['platform'] }}-report.json
|
||||
path: ./workdir/report.json
|
||||
- name: Upload the Report to the CI
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
header: diff-tests-report-${{ inputs['platform'] }}
|
||||
path: report.md
|
||||
name: ${{ inputs['platform'] }}.json
|
||||
path: ./workdir/expectations.json
|
||||
- name: Check Expectations
|
||||
shell: bash
|
||||
if: ${{ inputs['expectations-file-path'] != '' }}
|
||||
run: report-processor compare-expectation-files --base-expectation-path ${{ inputs['expectations-file-path'] }} --other-expectation-path ./workdir/expectations.json
|
||||
|
||||
Generated
+1076
-1076
File diff suppressed because it is too large
Load Diff
+10
-4
@@ -21,6 +21,7 @@ revive-dt-node-interaction = { version = "0.1.0", path = "crates/node-interactio
|
||||
revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" }
|
||||
revive-dt-report = { version = "0.1.0", path = "crates/report" }
|
||||
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
|
||||
revive-dt-report-processor = { version = "0.1.0", path = "crates/report-processor" }
|
||||
|
||||
alloy = { version = "1.4.1", features = ["full", "genesis", "json-rpc"] }
|
||||
ansi_term = "0.12.1"
|
||||
@@ -73,15 +74,20 @@ indexmap = { version = "2.10.0", default-features = false }
|
||||
itertools = { version = "0.14.0" }
|
||||
|
||||
# revive compiler
|
||||
revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||
revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||
revive-solc-json-interface = { version = "0.5.0" }
|
||||
revive-common = { version = "0.3.0" }
|
||||
revive-differential = { version = "0.3.0" }
|
||||
|
||||
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = "891f6554354ce466abd496366dbf8b4f82141241" }
|
||||
|
||||
[profile.bench]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[profile.production]
|
||||
inherits = "release"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[workspace.lints.clippy]
|
||||
|
||||
@@ -23,6 +23,18 @@ pub struct Mode {
|
||||
pub version: Option<semver::VersionReq>,
|
||||
}
|
||||
|
||||
impl Ord for Mode {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.to_string().cmp(&other.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Mode {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.pipeline.fmt(f)?;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::{fmt::Display, path::PathBuf, str::FromStr};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::Mode;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ParsedTestSpecifier {
|
||||
/// All of the test cases in the file should be ran across all of the specified modes
|
||||
FileOrDirectory {
|
||||
@@ -34,6 +39,22 @@ pub enum ParsedTestSpecifier {
|
||||
},
|
||||
}
|
||||
|
||||
impl ParsedTestSpecifier {
|
||||
pub fn metadata_path(&self) -> &Path {
|
||||
match self {
|
||||
ParsedTestSpecifier::FileOrDirectory {
|
||||
metadata_or_directory_file_path: metadata_file_path,
|
||||
}
|
||||
| ParsedTestSpecifier::Case {
|
||||
metadata_file_path, ..
|
||||
}
|
||||
| ParsedTestSpecifier::CaseWithMode {
|
||||
metadata_file_path, ..
|
||||
} => metadata_file_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParsedTestSpecifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -131,3 +152,22 @@ impl TryFrom<&str> for ParsedTestSpecifier {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ParsedTestSpecifier {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ParsedTestSpecifier {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
string.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,13 @@ use dashmap::DashMap;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||
use revive_solc_json_interface::{
|
||||
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
||||
SolcStandardJsonOutput,
|
||||
PolkaVMDefaultHeapMemorySize, PolkaVMDefaultStackMemorySize, SolcStandardJsonInput,
|
||||
SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
||||
SolcStandardJsonInputSettingsLibraries, SolcStandardJsonInputSettingsMetadata,
|
||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsPolkaVM,
|
||||
SolcStandardJsonInputSettingsPolkaVMMemory, SolcStandardJsonInputSettingsSelection,
|
||||
SolcStandardJsonOutput, standard_json::input::settings::optimizer::Optimizer,
|
||||
standard_json::input::settings::optimizer::details::Details,
|
||||
};
|
||||
use tracing::{Span, field::display};
|
||||
|
||||
@@ -25,6 +29,7 @@ use crate::{
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use anyhow::{Context as _, Result};
|
||||
use semver::Version;
|
||||
use std::collections::BTreeSet;
|
||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||
|
||||
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
|
||||
@@ -37,6 +42,10 @@ struct ResolcInner {
|
||||
solc: Solc,
|
||||
/// Path to the `resolc` executable
|
||||
resolc_path: PathBuf,
|
||||
/// The PVM heap size in bytes.
|
||||
pvm_heap_size: u32,
|
||||
/// The PVM stack size in bytes.
|
||||
pvm_stack_size: u32,
|
||||
}
|
||||
|
||||
impl Resolc {
|
||||
@@ -63,10 +72,35 @@ impl Resolc {
|
||||
Self(Arc::new(ResolcInner {
|
||||
solc,
|
||||
resolc_path: resolc_configuration.path.clone(),
|
||||
pvm_heap_size: resolc_configuration
|
||||
.heap_size
|
||||
.unwrap_or(PolkaVMDefaultHeapMemorySize),
|
||||
pvm_stack_size: resolc_configuration
|
||||
.stack_size
|
||||
.unwrap_or(PolkaVMDefaultStackMemorySize),
|
||||
}))
|
||||
})
|
||||
.clone())
|
||||
}
|
||||
|
||||
fn polkavm_settings(&self) -> SolcStandardJsonInputSettingsPolkaVM {
|
||||
SolcStandardJsonInputSettingsPolkaVM::new(
|
||||
Some(SolcStandardJsonInputSettingsPolkaVMMemory::new(
|
||||
Some(self.0.pvm_heap_size),
|
||||
Some(self.0.pvm_stack_size),
|
||||
)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn inject_polkavm_settings(&self, input: &SolcStandardJsonInput) -> Result<serde_json::Value> {
|
||||
let mut input_value = serde_json::to_value(input)
|
||||
.context("Failed to serialize Standard JSON input for resolc")?;
|
||||
if let Some(settings) = input_value.get_mut("settings") {
|
||||
settings["polkavm"] = serde_json::to_value(self.polkavm_settings()).unwrap();
|
||||
}
|
||||
Ok(input_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl SolidityCompiler for Resolc {
|
||||
@@ -121,8 +155,8 @@ impl SolidityCompiler for Resolc {
|
||||
.collect(),
|
||||
settings: SolcStandardJsonInputSettings {
|
||||
evm_version,
|
||||
libraries: Some(
|
||||
libraries
|
||||
libraries: SolcStandardJsonInputSettingsLibraries {
|
||||
inner: libraries
|
||||
.into_iter()
|
||||
.map(|(source_code, libraries_map)| {
|
||||
(
|
||||
@@ -136,23 +170,29 @@ impl SolidityCompiler for Resolc {
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
remappings: None,
|
||||
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
|
||||
},
|
||||
remappings: BTreeSet::<String>::new(),
|
||||
output_selection: SolcStandardJsonInputSettingsSelection::new_required(),
|
||||
via_ir: Some(true),
|
||||
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
|
||||
optimization
|
||||
.unwrap_or(ModeOptimizerSetting::M0)
|
||||
.optimizations_enabled(),
|
||||
None,
|
||||
&Version::new(0, 0, 0),
|
||||
false,
|
||||
Optimizer::default_mode(),
|
||||
Details::disabled(&Version::new(0, 0, 0)),
|
||||
),
|
||||
metadata: None,
|
||||
polkavm: None,
|
||||
polkavm: self.polkavm_settings(),
|
||||
metadata: SolcStandardJsonInputSettingsMetadata::default(),
|
||||
detect_missing_libraries: false,
|
||||
},
|
||||
};
|
||||
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
|
||||
// Manually inject polkavm settings since it's marked skip_serializing in the upstream crate
|
||||
let std_input_json = self.inject_polkavm_settings(&input)?;
|
||||
|
||||
Span::current().record(
|
||||
"json_in",
|
||||
display(serde_json::to_string(&std_input_json).unwrap()),
|
||||
);
|
||||
|
||||
let path = &self.0.resolc_path;
|
||||
let mut command = AsyncCommand::new(path);
|
||||
@@ -181,8 +221,9 @@ impl SolidityCompiler for Resolc {
|
||||
.with_context(|| format!("Failed to spawn resolc at {}", path.display()))?;
|
||||
|
||||
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
|
||||
let serialized_input = serde_json::to_vec(&input)
|
||||
let serialized_input = serde_json::to_vec(&std_input_json)
|
||||
.context("Failed to serialize Standard JSON input for resolc")?;
|
||||
|
||||
stdin_pipe
|
||||
.write_all(&serialized_input)
|
||||
.await
|
||||
@@ -228,7 +269,7 @@ impl SolidityCompiler for Resolc {
|
||||
|
||||
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||
// errors instead of returning the compiler output that might contain errors.
|
||||
for error in parsed.errors.iter().flatten() {
|
||||
for error in parsed.errors.iter() {
|
||||
if error.severity == "error" {
|
||||
tracing::error!(
|
||||
?error,
|
||||
@@ -240,12 +281,12 @@ impl SolidityCompiler for Resolc {
|
||||
}
|
||||
}
|
||||
|
||||
let Some(contracts) = parsed.contracts else {
|
||||
if parsed.contracts.is_empty() {
|
||||
anyhow::bail!("Unexpected error - resolc output doesn't have a contracts section");
|
||||
};
|
||||
}
|
||||
|
||||
let mut compiler_output = CompilerOutput::default();
|
||||
for (source_path, contracts) in contracts.into_iter() {
|
||||
for (source_path, contracts) in parsed.contracts.into_iter() {
|
||||
let src_for_msg = source_path.clone();
|
||||
let source_path = PathBuf::from(source_path)
|
||||
.canonicalize()
|
||||
@@ -253,15 +294,22 @@ impl SolidityCompiler for Resolc {
|
||||
|
||||
let map = compiler_output.contracts.entry(source_path).or_default();
|
||||
for (contract_name, contract_information) in contracts.into_iter() {
|
||||
let bytecode = contract_information
|
||||
let Some(bytecode) = contract_information
|
||||
.evm
|
||||
.and_then(|evm| evm.bytecode.clone())
|
||||
.context("Unexpected - Contract compiled with resolc has no bytecode")?;
|
||||
else {
|
||||
tracing::debug!(
|
||||
"Skipping abstract or interface contract {} - no bytecode",
|
||||
contract_name
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let abi = {
|
||||
let metadata = contract_information
|
||||
.metadata
|
||||
.as_ref()
|
||||
.context("No metadata found for the contract")?;
|
||||
let metadata = &contract_information.metadata;
|
||||
if metadata.is_null() {
|
||||
anyhow::bail!("No metadata found for the contract");
|
||||
}
|
||||
|
||||
let solc_metadata_str = match metadata {
|
||||
serde_json::Value::String(solc_metadata_str) => {
|
||||
solc_metadata_str.as_str()
|
||||
|
||||
@@ -800,6 +800,17 @@ pub struct ResolcConfiguration {
|
||||
/// provided in the user's $PATH.
|
||||
#[clap(id = "resolc.path", long = "resolc.path", default_value = "resolc")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Specifies the PVM heap size in bytes.
|
||||
///
|
||||
/// If unspecified, the revive compiler default is used
|
||||
#[clap(id = "resolc.heap-size", long = "resolc.heap-size")]
|
||||
pub heap_size: Option<u32>,
|
||||
/// Specifies the PVM stack size in bytes.
|
||||
///
|
||||
/// If unspecified, the revive compiler default is used
|
||||
#[clap(id = "resolc.stack-size", long = "resolc.stack-size")]
|
||||
pub stack_size: Option<u32>,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for Polkadot Parachain.
|
||||
@@ -1113,6 +1124,10 @@ pub struct ReportConfiguration {
|
||||
/// Controls if the compiler output is included in the final report.
|
||||
#[clap(long = "report.include-compiler-output")]
|
||||
pub include_compiler_output: bool,
|
||||
|
||||
/// The filename to use for the report.
|
||||
#[clap(long = "report.file-name")]
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "revive-dt-report-processor"
|
||||
description = "revive differential testing report processor utility"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "report-processor"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
revive-dt-report = { workspace = true }
|
||||
revive-dt-common = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,329 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt::Display,
|
||||
fs::{File, OpenOptions},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
|
||||
use revive_dt_common::types::{Mode, ParsedTestSpecifier};
|
||||
use revive_dt_report::{Report, TestCaseStatus};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::try_parse().context("Failed to parse the CLI arguments")?;
|
||||
|
||||
match cli {
|
||||
Cli::GenerateExpectationsFile {
|
||||
report_path,
|
||||
output_path: output_file,
|
||||
remove_prefix,
|
||||
} => {
|
||||
let remove_prefix = remove_prefix
|
||||
.into_iter()
|
||||
.map(|path| path.canonicalize().context("Failed to canonicalize path"))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let expectations = report_path
|
||||
.execution_information
|
||||
.iter()
|
||||
.flat_map(|(metadata_file_path, metadata_file_report)| {
|
||||
metadata_file_report
|
||||
.case_reports
|
||||
.iter()
|
||||
.map(move |(case_idx, case_report)| {
|
||||
(metadata_file_path, case_idx, case_report)
|
||||
})
|
||||
})
|
||||
.flat_map(|(metadata_file_path, case_idx, case_report)| {
|
||||
case_report.mode_execution_reports.iter().map(
|
||||
move |(mode, execution_report)| {
|
||||
(
|
||||
metadata_file_path,
|
||||
case_idx,
|
||||
mode,
|
||||
execution_report.status.as_ref(),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.filter_map(|(metadata_file_path, case_idx, mode, status)| {
|
||||
status.map(|status| (metadata_file_path, case_idx, mode, status))
|
||||
})
|
||||
.map(|(metadata_file_path, case_idx, mode, status)| {
|
||||
(
|
||||
TestSpecifier {
|
||||
metadata_file_path: Cow::Borrowed(
|
||||
remove_prefix
|
||||
.iter()
|
||||
.filter_map(|prefix| {
|
||||
metadata_file_path.as_inner().strip_prefix(prefix).ok()
|
||||
})
|
||||
.next()
|
||||
.unwrap_or(metadata_file_path.as_inner()),
|
||||
),
|
||||
case_idx: case_idx.into_inner(),
|
||||
mode: Cow::Borrowed(mode),
|
||||
},
|
||||
Status::from(status),
|
||||
)
|
||||
})
|
||||
.filter(|(_, status)| *status == Status::Failed)
|
||||
.collect::<Expectations>();
|
||||
|
||||
let output_file = OpenOptions::new()
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(output_file)
|
||||
.context("Failed to create the output file")?;
|
||||
serde_json::to_writer_pretty(output_file, &expectations)
|
||||
.context("Failed to write the expectations to file")?;
|
||||
}
|
||||
Cli::CompareExpectationFiles {
|
||||
base_expectation_path,
|
||||
other_expectation_path,
|
||||
} => {
|
||||
let keys = base_expectation_path
|
||||
.keys()
|
||||
.chain(other_expectation_path.keys())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
for key in keys {
|
||||
let base_status = base_expectation_path.get(key).context(format!(
|
||||
"Entry not found in the base expectations: \"{}\"",
|
||||
key
|
||||
))?;
|
||||
let other_status = other_expectation_path.get(key).context(format!(
|
||||
"Entry not found in the other expectations: \"{}\"",
|
||||
key
|
||||
))?;
|
||||
|
||||
if base_status != other_status {
|
||||
bail!(
|
||||
"Expectations for entry \"{}\" have changed. They were {:?} and now they are {:?}",
|
||||
key,
|
||||
base_status,
|
||||
other_status
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type Expectations<'a> = BTreeMap<TestSpecifier<'a>, Status>;
|
||||
|
||||
/// A tool that's used to process the reports generated by the retester binary in various ways.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[command(name = "retester", term_width = 100)]
|
||||
pub enum Cli {
|
||||
/// Generates an expectation file out of a given report.
|
||||
GenerateExpectationsFile {
|
||||
/// The path of the report's JSON file to generate the expectation's file for.
|
||||
#[clap(long)]
|
||||
report_path: JsonFile<Report>,
|
||||
|
||||
/// The path of the output file to generate.
|
||||
///
|
||||
/// Note that we expect that:
|
||||
/// 1. The provided path points to a JSON file.
|
||||
/// 1. The ancestor's of the provided path already exist such that no directory creations
|
||||
/// are required.
|
||||
#[clap(long)]
|
||||
output_path: PathBuf,
|
||||
|
||||
/// Prefix paths to remove from the paths in the final expectations file.
|
||||
#[clap(long)]
|
||||
remove_prefix: Vec<PathBuf>,
|
||||
},
|
||||
|
||||
/// Compares two expectation files to ensure that they match each other.
|
||||
CompareExpectationFiles {
|
||||
/// The path of the base expectation file.
|
||||
#[clap(long)]
|
||||
base_expectation_path: JsonFile<Expectations<'static>>,
|
||||
|
||||
/// The path of the other expectation file.
|
||||
#[clap(long)]
|
||||
other_expectation_path: JsonFile<Expectations<'static>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum Status {
|
||||
Succeeded,
|
||||
Failed,
|
||||
Ignored,
|
||||
}
|
||||
|
||||
impl From<TestCaseStatus> for Status {
|
||||
fn from(value: TestCaseStatus) -> Self {
|
||||
match value {
|
||||
TestCaseStatus::Succeeded { .. } => Self::Succeeded,
|
||||
TestCaseStatus::Failed { .. } => Self::Failed,
|
||||
TestCaseStatus::Ignored { .. } => Self::Ignored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a TestCaseStatus> for Status {
|
||||
fn from(value: &'a TestCaseStatus) -> Self {
|
||||
match value {
|
||||
TestCaseStatus::Succeeded { .. } => Self::Succeeded,
|
||||
TestCaseStatus::Failed { .. } => Self::Failed,
|
||||
TestCaseStatus::Ignored { .. } => Self::Ignored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct JsonFile<T> {
|
||||
path: PathBuf,
|
||||
content: Box<T>,
|
||||
}
|
||||
|
||||
impl<T> Deref for JsonFile<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.content
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for JsonFile<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.content
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromStr for JsonFile<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let path = PathBuf::from(s);
|
||||
let file = File::open(&path).context("Failed to open the file")?;
|
||||
serde_json::from_reader(&file)
|
||||
.map(|content| Self { path, content })
|
||||
.context(format!(
|
||||
"Failed to deserialize file's content as {}",
|
||||
std::any::type_name::<T>()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Display for JsonFile<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.path.display(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<JsonFile<T>> for String {
|
||||
fn from(value: JsonFile<T>) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct TestSpecifier<'a> {
|
||||
pub metadata_file_path: Cow<'a, Path>,
|
||||
pub case_idx: usize,
|
||||
pub mode: Cow<'a, Mode>,
|
||||
}
|
||||
|
||||
impl<'a> Display for TestSpecifier<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}::{}::{}",
|
||||
self.metadata_file_path.display(),
|
||||
self.case_idx,
|
||||
self.mode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<TestSpecifier<'a>> for ParsedTestSpecifier {
|
||||
fn from(
|
||||
TestSpecifier {
|
||||
metadata_file_path,
|
||||
case_idx,
|
||||
mode,
|
||||
}: TestSpecifier,
|
||||
) -> Self {
|
||||
Self::CaseWithMode {
|
||||
metadata_file_path: metadata_file_path.to_path_buf(),
|
||||
case_idx,
|
||||
mode: mode.into_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ParsedTestSpecifier> for TestSpecifier<'static> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: ParsedTestSpecifier) -> Result<Self> {
|
||||
let ParsedTestSpecifier::CaseWithMode {
|
||||
metadata_file_path,
|
||||
case_idx,
|
||||
mode,
|
||||
} = value
|
||||
else {
|
||||
bail!("Expected a full test case specifier")
|
||||
};
|
||||
Ok(Self {
|
||||
metadata_file_path: Cow::Owned(metadata_file_path),
|
||||
case_idx,
|
||||
mode: Cow::Owned(mode),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for TestSpecifier<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d, 'a> Deserialize<'d> for TestSpecifier<'a> {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'d>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
let mut splitted = string.split("::");
|
||||
let (Some(metadata_file_path), Some(case_idx), Some(mode), None) = (
|
||||
splitted.next(),
|
||||
splitted.next(),
|
||||
splitted.next(),
|
||||
splitted.next(),
|
||||
) else {
|
||||
return Err(serde::de::Error::custom(
|
||||
"Test specifier doesn't contain the components required",
|
||||
));
|
||||
};
|
||||
let metadata_file_path = PathBuf::from(metadata_file_path);
|
||||
let case_idx = usize::from_str(case_idx)
|
||||
.map_err(|_| serde::de::Error::custom("Case idx is not a usize"))?;
|
||||
let mode = Mode::from_str(mode).map_err(|_| serde::de::Error::custom("Invalid mode"))?;
|
||||
|
||||
Ok(Self {
|
||||
metadata_file_path: Cow::Owned(metadata_file_path),
|
||||
case_idx,
|
||||
mode: Cow::Owned(mode),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ pub struct ReportAggregator {
|
||||
runner_tx: Option<UnboundedSender<RunnerEvent>>,
|
||||
runner_rx: UnboundedReceiver<RunnerEvent>,
|
||||
listener_tx: Sender<ReporterEvent>,
|
||||
/* Context */
|
||||
file_name: Option<String>,
|
||||
}
|
||||
|
||||
impl ReportAggregator {
|
||||
@@ -43,6 +45,11 @@ impl ReportAggregator {
|
||||
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
||||
let (listener_tx, _) = channel::<ReporterEvent>(0xFFFF);
|
||||
Self {
|
||||
file_name: match context {
|
||||
Context::Test(ref context) => context.report_configuration.file_name.clone(),
|
||||
Context::Benchmark(ref context) => context.report_configuration.file_name.clone(),
|
||||
Context::ExportJsonSchema | Context::ExportGenesis(..) => None,
|
||||
},
|
||||
report: Report::new(context),
|
||||
remaining_cases: Default::default(),
|
||||
runner_tx: Some(runner_tx),
|
||||
@@ -121,7 +128,7 @@ impl ReportAggregator {
|
||||
self.handle_completion(CompletionEvent {});
|
||||
debug!("Report aggregation completed");
|
||||
|
||||
let file_name = {
|
||||
let default_file_name = {
|
||||
let current_timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("System clock is before UNIX_EPOCH; cannot compute report timestamp")?
|
||||
@@ -130,6 +137,7 @@ impl ReportAggregator {
|
||||
file_name.push_str(".json");
|
||||
file_name
|
||||
};
|
||||
let file_name = self.file_name.unwrap_or(default_file_name);
|
||||
let file_path = self
|
||||
.report
|
||||
.context
|
||||
@@ -562,7 +570,7 @@ pub struct Report {
|
||||
/// The list of metadata files that were found by the tool.
|
||||
pub metadata_files: BTreeSet<MetadataFilePath>,
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// Information relating to each test case.
|
||||
pub execution_information: BTreeMap<MetadataFilePath, MetadataFileReport>,
|
||||
@@ -582,7 +590,7 @@ impl Report {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct MetadataFileReport {
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// The report of each case keyed by the case idx.
|
||||
pub case_reports: BTreeMap<CaseIdx, CaseReport>,
|
||||
@@ -592,7 +600,7 @@ pub struct MetadataFileReport {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct CaseReport {
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// The [`ExecutionReport`] for each one of the [`Mode`]s.
|
||||
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
|
||||
@@ -602,31 +610,31 @@ pub struct CaseReport {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ExecutionReport {
|
||||
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<TestCaseStatus>,
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// Information related to the execution on one of the platforms.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub platform_execution: PlatformKeyedInformation<Option<ExecutionInformation>>,
|
||||
/// Information on the compiled contracts.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub compiled_contracts: BTreeMap<PathBuf, BTreeMap<String, ContractInformation>>,
|
||||
/// The addresses of the deployed contracts
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub contract_addresses: BTreeMap<ContractInstance, PlatformKeyedInformation<Vec<Address>>>,
|
||||
/// Information on the mined blocks as part of this execution.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub mined_block_information: PlatformKeyedInformation<Vec<MinedBlockInformation>>,
|
||||
/// Information tracked for each step that was executed.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub steps: BTreeMap<StepPath, StepReport>,
|
||||
}
|
||||
|
||||
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
||||
/// it was ignored.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum TestCaseStatus {
|
||||
/// The test case succeeded.
|
||||
@@ -664,19 +672,19 @@ pub struct TestCaseNodeInformation {
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ExecutionInformation {
|
||||
/// Information related to the node assigned to this test case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub node: Option<TestCaseNodeInformation>,
|
||||
/// Information on the pre-link compiled contracts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pre_link_compilation_status: Option<CompilationStatus>,
|
||||
/// Information on the post-link compiled contracts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub post_link_compilation_status: Option<CompilationStatus>,
|
||||
/// Information on the deployed libraries.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub deployed_libraries: Option<BTreeMap<ContractInstance, Address>>,
|
||||
/// Information on the deployed contracts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub deployed_contracts: Option<BTreeMap<ContractInstance, Address>>,
|
||||
}
|
||||
|
||||
@@ -695,11 +703,11 @@ pub enum CompilationStatus {
|
||||
/// 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.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, 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.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
compiler_output: Option<CompilerOutput>,
|
||||
},
|
||||
/// The compilation failed.
|
||||
@@ -707,15 +715,15 @@ pub enum CompilationStatus {
|
||||
/// The failure reason.
|
||||
reason: String,
|
||||
/// The version of the compiler used to compile the contracts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
compiler_version: Option<Version>,
|
||||
/// The path of the compiler used to compile the contracts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, 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.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
compiler_input: Option<CompilerInput>,
|
||||
},
|
||||
}
|
||||
@@ -743,24 +751,24 @@ pub struct Metrics {
|
||||
pub gas_per_second: Metric<u64>,
|
||||
/* Block Fullness */
|
||||
pub gas_block_fullness: Metric<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ref_time_block_fullness: Option<Metric<u64>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub proof_size_block_fullness: Option<Metric<u64>>,
|
||||
}
|
||||
|
||||
/// The data that we store for a given metric (e.g., TPS).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Metric<T> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub maximum: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mean: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub median: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub raw: Option<PlatformKeyedInformation<Vec<T>>>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user