mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-23 09:37:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cec992f80a | |||
| b53550e43e | |||
| 9491263857 | |||
| b41c0e61c0 | |||
| 9d1c71756f | |||
| 8b0a0c3518 | |||
| 94b04c0189 | |||
| 2d3602aaed |
@@ -33,14 +33,18 @@ inputs:
|
|||||||
description: "The identifier of the platform to run the tests on (e.g., geth-evm-solc, revive-dev-node-revm-solc)"
|
description: "The identifier of the platform to run the tests on (e.g., geth-evm-solc, revive-dev-node-revm-solc)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
polkadot-omnichain-node-runtime-path:
|
polkadot-omnichain-node-chain-spec-path:
|
||||||
description: "The path of the WASM runtime to use with the polkadot-omni-node. This is only required if the polkadot-omni-node is one of the selected platforms."
|
description: "The path of the chain-spec of the chain we're spawning'. This is only required if the polkadot-omni-node is one of the selected platforms."
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
polkadot-omnichain-node-parachain-id:
|
polkadot-omnichain-node-parachain-id:
|
||||||
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."
|
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
|
type: number
|
||||||
required: false
|
required: false
|
||||||
|
expectations-file-path:
|
||||||
|
description: "Path to the expectations file to use to compare against."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@@ -79,6 +83,12 @@ runs:
|
|||||||
run: |
|
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 -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
|
${{ 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
|
- name: Running the Differential Tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -89,18 +99,19 @@ runs:
|
|||||||
"${{ inputs['polkadot-omnichain-node-parachain-id'] }}"
|
"${{ inputs['polkadot-omnichain-node-parachain-id'] }}"
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
if [[ -n "${{ inputs['polkadot-omnichain-node-runtime-path'] }}" ]]; then
|
if [[ -n "${{ inputs['polkadot-omnichain-node-chain-spec-path'] }}" ]]; then
|
||||||
OMNI_ARGS+=(
|
OMNI_ARGS+=(
|
||||||
--polkadot-omni-node.runtime-wasm-path
|
--polkadot-omni-node.chain-spec-path
|
||||||
"${{ inputs['polkadot-omnichain-node-runtime-path'] }}"
|
"${{ inputs['polkadot-omnichain-node-chain-spec-path'] }}"
|
||||||
)
|
)
|
||||||
fi
|
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/simple \
|
||||||
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/complex \
|
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/complex \
|
||||||
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/translated_semantic_tests \
|
--test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/translated_semantic_tests \
|
||||||
--platform ${{ inputs['platform'] }} \
|
--platform ${{ inputs['platform'] }} \
|
||||||
|
--report.file-name report.json \
|
||||||
--concurrency.number-of-nodes 10 \
|
--concurrency.number-of-nodes 10 \
|
||||||
--concurrency.number-of-threads 10 \
|
--concurrency.number-of-threads 10 \
|
||||||
--concurrency.number-of-concurrent-tasks 100 \
|
--concurrency.number-of-concurrent-tasks 100 \
|
||||||
@@ -110,22 +121,21 @@ runs:
|
|||||||
--eth-rpc.path ${{ inputs['polkadot-sdk-path'] }}/target/release/eth-rpc \
|
--eth-rpc.path ${{ inputs['polkadot-sdk-path'] }}/target/release/eth-rpc \
|
||||||
--polkadot-omni-node.path ${{ inputs['polkadot-sdk-path'] }}/target/release/polkadot-omni-node \
|
--polkadot-omni-node.path ${{ inputs['polkadot-sdk-path'] }}/target/release/polkadot-omni-node \
|
||||||
--resolc.path ./resolc \
|
--resolc.path ./resolc \
|
||||||
"${OMNI_ARGS[@]}"
|
"${OMNI_ARGS[@]}" || true
|
||||||
- name: Creating a markdown report of the test execution
|
- name: Generate the expectation file
|
||||||
shell: bash
|
shell: bash
|
||||||
if: ${{ always() }}
|
run: report-processor generate-expectations-file --report-path ./workdir/report.json --output-path ./workdir/expectations.json --remove-prefix ./revive-differential-tests/resolc-compiler-tests
|
||||||
run: |
|
|
||||||
mv ./workdir/*.json report.json
|
|
||||||
python3 revive-differential-tests/scripts/process-differential-tests-report.py report.json ${{ inputs['platform'] }}
|
|
||||||
- name: Upload the Report to the CI
|
- name: Upload the Report to the CI
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
with:
|
||||||
name: report-${{ inputs['platform'] }}.md
|
name: ${{ inputs['platform'] }}-report.json
|
||||||
path: report.md
|
path: ./workdir/report.json
|
||||||
- name: Posting the report as a comment on the PR
|
- name: Upload the Report to the CI
|
||||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
with:
|
||||||
header: diff-tests-report-${{ inputs['platform'] }}
|
name: ${{ inputs['platform'] }}.json
|
||||||
path: report.md
|
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
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ node_modules
|
|||||||
*.log
|
*.log
|
||||||
|
|
||||||
profile.json.gz
|
profile.json.gz
|
||||||
workdir
|
workdir*
|
||||||
|
|
||||||
!/schema.json
|
!/schema.json
|
||||||
!/dev-genesis.json
|
!/dev-genesis.json
|
||||||
|
|||||||
Generated
+1268
-1098
File diff suppressed because it is too large
Load Diff
+11
-25
@@ -21,7 +21,9 @@ 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-node-pool = { version = "0.1.0", path = "crates/node-pool" }
|
||||||
revive-dt-report = { version = "0.1.0", path = "crates/report" }
|
revive-dt-report = { version = "0.1.0", path = "crates/report" }
|
||||||
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
|
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"
|
ansi_term = "0.12.1"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bson = { version = "2.15.0" }
|
bson = { version = "2.15.0" }
|
||||||
@@ -72,36 +74,20 @@ indexmap = { version = "2.10.0", default-features = false }
|
|||||||
itertools = { version = "0.14.0" }
|
itertools = { version = "0.14.0" }
|
||||||
|
|
||||||
# revive compiler
|
# revive compiler
|
||||||
revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
revive-solc-json-interface = { version = "0.5.0" }
|
||||||
revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
revive-common = { version = "0.3.0" }
|
||||||
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
revive-differential = { version = "0.3.0" }
|
||||||
|
|
||||||
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = "891f6554354ce466abd496366dbf8b4f82141241" }
|
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = "891f6554354ce466abd496366dbf8b4f82141241" }
|
||||||
|
|
||||||
[workspace.dependencies.alloy]
|
|
||||||
version = "1.0.37"
|
|
||||||
default-features = false
|
|
||||||
features = [
|
|
||||||
"json-abi",
|
|
||||||
"providers",
|
|
||||||
"provider-ws",
|
|
||||||
"provider-ipc",
|
|
||||||
"provider-http",
|
|
||||||
"provider-debug-api",
|
|
||||||
"reqwest",
|
|
||||||
"rpc-types",
|
|
||||||
"signer-local",
|
|
||||||
"std",
|
|
||||||
"network",
|
|
||||||
"serde",
|
|
||||||
"rpc-types-eth",
|
|
||||||
"genesis",
|
|
||||||
"sol-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[profile.bench]
|
[profile.bench]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
|
||||||
|
[profile.production]
|
||||||
|
inherits = "release"
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ semver = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
tokio = { workspace = true, default-features = false, features = ["time"] }
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
mod poll;
|
|
||||||
|
|
||||||
pub use poll::*;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
use std::ops::ControlFlow;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
|
||||||
|
|
||||||
const EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION: Duration = Duration::from_secs(60);
|
|
||||||
|
|
||||||
/// A function that polls for a fallible future for some period of time and errors if it fails to
|
|
||||||
/// get a result after polling.
|
|
||||||
///
|
|
||||||
/// Given a future that returns a [`Result<ControlFlow<O, ()>>`], this function calls the future
|
|
||||||
/// repeatedly (with some wait period) until the future returns a [`ControlFlow::Break`] or until it
|
|
||||||
/// returns an [`Err`] in which case the function stops polling and returns the error.
|
|
||||||
///
|
|
||||||
/// If the future keeps returning [`ControlFlow::Continue`] and fails to return a [`Break`] within
|
|
||||||
/// the permitted polling duration then this function returns an [`Err`]
|
|
||||||
///
|
|
||||||
/// [`Break`]: ControlFlow::Break
|
|
||||||
/// [`Continue`]: ControlFlow::Continue
|
|
||||||
pub async fn poll<F, O>(
|
|
||||||
polling_duration: Duration,
|
|
||||||
polling_wait_behavior: PollingWaitBehavior,
|
|
||||||
mut future: impl FnMut() -> F,
|
|
||||||
) -> Result<O>
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<ControlFlow<O, ()>>>,
|
|
||||||
{
|
|
||||||
let mut retries = 0;
|
|
||||||
let mut total_wait_duration = Duration::ZERO;
|
|
||||||
let max_allowed_wait_duration = polling_duration;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if total_wait_duration >= max_allowed_wait_duration {
|
|
||||||
break Err(anyhow!(
|
|
||||||
"Polling failed after {} retries and a total of {:?} of wait time",
|
|
||||||
retries,
|
|
||||||
total_wait_duration
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
match future()
|
|
||||||
.await
|
|
||||||
.context("Polled future returned an error during polling loop")?
|
|
||||||
{
|
|
||||||
ControlFlow::Continue(()) => {
|
|
||||||
let next_wait_duration = match polling_wait_behavior {
|
|
||||||
PollingWaitBehavior::Constant(duration) => duration,
|
|
||||||
PollingWaitBehavior::ExponentialBackoff => {
|
|
||||||
Duration::from_secs(2u64.pow(retries))
|
|
||||||
.min(EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let next_wait_duration =
|
|
||||||
next_wait_duration.min(max_allowed_wait_duration - total_wait_duration);
|
|
||||||
total_wait_duration += next_wait_duration;
|
|
||||||
retries += 1;
|
|
||||||
|
|
||||||
tokio::time::sleep(next_wait_duration).await;
|
|
||||||
}
|
|
||||||
ControlFlow::Break(output) => {
|
|
||||||
break Ok(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
||||||
pub enum PollingWaitBehavior {
|
|
||||||
Constant(Duration),
|
|
||||||
#[default]
|
|
||||||
ExponentialBackoff,
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
pub mod cached_fs;
|
pub mod cached_fs;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
pub mod futures;
|
|
||||||
pub mod iterators;
|
pub mod iterators;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ pub struct Mode {
|
|||||||
pub version: Option<semver::VersionReq>,
|
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 {
|
impl Display for Mode {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
self.pipeline.fmt(f)?;
|
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 anyhow::{Context as _, bail};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::types::Mode;
|
use crate::types::Mode;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum ParsedTestSpecifier {
|
pub enum ParsedTestSpecifier {
|
||||||
/// All of the test cases in the file should be ran across all of the specified modes
|
/// All of the test cases in the file should be ran across all of the specified modes
|
||||||
FileOrDirectory {
|
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 {
|
impl Display for ParsedTestSpecifier {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -131,3 +152,22 @@ impl TryFrom<&str> for ParsedTestSpecifier {
|
|||||||
value.parse()
|
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_common::types::VersionOrRequirement;
|
||||||
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||||
use revive_solc_json_interface::{
|
use revive_solc_json_interface::{
|
||||||
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
PolkaVMDefaultHeapMemorySize, PolkaVMDefaultStackMemorySize, SolcStandardJsonInput,
|
||||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
||||||
SolcStandardJsonOutput,
|
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};
|
use tracing::{Span, field::display};
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@ use crate::{
|
|||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||||
|
|
||||||
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
|
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
|
||||||
@@ -37,6 +42,10 @@ struct ResolcInner {
|
|||||||
solc: Solc,
|
solc: Solc,
|
||||||
/// Path to the `resolc` executable
|
/// Path to the `resolc` executable
|
||||||
resolc_path: PathBuf,
|
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 {
|
impl Resolc {
|
||||||
@@ -63,10 +72,35 @@ impl Resolc {
|
|||||||
Self(Arc::new(ResolcInner {
|
Self(Arc::new(ResolcInner {
|
||||||
solc,
|
solc,
|
||||||
resolc_path: resolc_configuration.path.clone(),
|
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())
|
.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 {
|
impl SolidityCompiler for Resolc {
|
||||||
@@ -121,8 +155,8 @@ impl SolidityCompiler for Resolc {
|
|||||||
.collect(),
|
.collect(),
|
||||||
settings: SolcStandardJsonInputSettings {
|
settings: SolcStandardJsonInputSettings {
|
||||||
evm_version,
|
evm_version,
|
||||||
libraries: Some(
|
libraries: SolcStandardJsonInputSettingsLibraries {
|
||||||
libraries
|
inner: libraries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(source_code, libraries_map)| {
|
.map(|(source_code, libraries_map)| {
|
||||||
(
|
(
|
||||||
@@ -136,23 +170,29 @@ impl SolidityCompiler for Resolc {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
},
|
||||||
remappings: None,
|
remappings: BTreeSet::<String>::new(),
|
||||||
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
|
output_selection: SolcStandardJsonInputSettingsSelection::new_required(),
|
||||||
via_ir: Some(true),
|
via_ir: Some(true),
|
||||||
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
|
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
|
||||||
optimization
|
optimization
|
||||||
.unwrap_or(ModeOptimizerSetting::M0)
|
.unwrap_or(ModeOptimizerSetting::M0)
|
||||||
.optimizations_enabled(),
|
.optimizations_enabled(),
|
||||||
None,
|
Optimizer::default_mode(),
|
||||||
&Version::new(0, 0, 0),
|
Details::disabled(&Version::new(0, 0, 0)),
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
metadata: None,
|
polkavm: self.polkavm_settings(),
|
||||||
polkavm: None,
|
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 path = &self.0.resolc_path;
|
||||||
let mut command = AsyncCommand::new(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()))?;
|
.with_context(|| format!("Failed to spawn resolc at {}", path.display()))?;
|
||||||
|
|
||||||
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
|
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")?;
|
.context("Failed to serialize Standard JSON input for resolc")?;
|
||||||
|
|
||||||
stdin_pipe
|
stdin_pipe
|
||||||
.write_all(&serialized_input)
|
.write_all(&serialized_input)
|
||||||
.await
|
.await
|
||||||
@@ -228,7 +269,7 @@ impl SolidityCompiler for Resolc {
|
|||||||
|
|
||||||
// Detecting if the compiler output contained errors and reporting them through logs and
|
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||||
// errors instead of returning the compiler output that might contain errors.
|
// 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" {
|
if error.severity == "error" {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
?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");
|
anyhow::bail!("Unexpected error - resolc output doesn't have a contracts section");
|
||||||
};
|
}
|
||||||
|
|
||||||
let mut compiler_output = CompilerOutput::default();
|
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 src_for_msg = source_path.clone();
|
||||||
let source_path = PathBuf::from(source_path)
|
let source_path = PathBuf::from(source_path)
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
@@ -253,15 +294,22 @@ impl SolidityCompiler for Resolc {
|
|||||||
|
|
||||||
let map = compiler_output.contracts.entry(source_path).or_default();
|
let map = compiler_output.contracts.entry(source_path).or_default();
|
||||||
for (contract_name, contract_information) in contracts.into_iter() {
|
for (contract_name, contract_information) in contracts.into_iter() {
|
||||||
let bytecode = contract_information
|
let Some(bytecode) = contract_information
|
||||||
.evm
|
.evm
|
||||||
.and_then(|evm| evm.bytecode.clone())
|
.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 abi = {
|
||||||
let metadata = contract_information
|
let metadata = &contract_information.metadata;
|
||||||
.metadata
|
if metadata.is_null() {
|
||||||
.as_ref()
|
anyhow::bail!("No metadata found for the contract");
|
||||||
.context("No metadata found for the contract")?;
|
}
|
||||||
|
|
||||||
let solc_metadata_str = match metadata {
|
let solc_metadata_str = match metadata {
|
||||||
serde_json::Value::String(solc_metadata_str) => {
|
serde_json::Value::String(solc_metadata_str) => {
|
||||||
solc_metadata_str.as_str()
|
solc_metadata_str.as_str()
|
||||||
|
|||||||
@@ -800,6 +800,17 @@ pub struct ResolcConfiguration {
|
|||||||
/// provided in the user's $PATH.
|
/// provided in the user's $PATH.
|
||||||
#[clap(id = "resolc.path", long = "resolc.path", default_value = "resolc")]
|
#[clap(id = "resolc.path", long = "resolc.path", default_value = "resolc")]
|
||||||
pub path: PathBuf,
|
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.
|
/// A set of configuration parameters for Polkadot Parachain.
|
||||||
@@ -943,13 +954,12 @@ pub struct PolkadotOmnichainNodeConfiguration {
|
|||||||
)]
|
)]
|
||||||
pub block_time: Duration,
|
pub block_time: Duration,
|
||||||
|
|
||||||
/// The path of the WASM runtime to use for the polkadot-omni-node. This argument is required if
|
/// The path of the chainspec of the chain that we're spawning
|
||||||
/// the polkadot-omni-node is one of the selected platforms for running the tests or benchmarks.
|
|
||||||
#[clap(
|
#[clap(
|
||||||
id = "polkadot-omni-node.runtime-wasm-path",
|
id = "polkadot-omni-node.chain-spec-path",
|
||||||
long = "polkadot-omni-node.runtime-wasm-path"
|
long = "polkadot-omni-node.chain-spec-path"
|
||||||
)]
|
)]
|
||||||
pub runtime_wasm_path: Option<PathBuf>,
|
pub chain_spec_path: Option<PathBuf>,
|
||||||
|
|
||||||
/// The ID of the parachain that the polkadot-omni-node will spawn. This argument is required if
|
/// The ID of the parachain that the polkadot-omni-node will spawn. This argument is required if
|
||||||
/// the polkadot-omni-node is one of the selected platforms for running the tests or benchmarks.
|
/// the polkadot-omni-node is one of the selected platforms for running the tests or benchmarks.
|
||||||
@@ -1114,6 +1124,10 @@ pub struct ReportConfiguration {
|
|||||||
/// Controls if the compiler output is included in the final report.
|
/// Controls if the compiler output is included in the final report.
|
||||||
#[clap(long = "report.include-compiler-output")]
|
#[clap(long = "report.include-compiler-output")]
|
||||||
pub include_compiler_output: bool,
|
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)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -409,7 +409,6 @@ where
|
|||||||
.handle_function_call_execution(step, deployment_receipts)
|
.handle_function_call_execution(step, deployment_receipts)
|
||||||
.await
|
.await
|
||||||
.context("Failed to handle the function call execution")?;
|
.context("Failed to handle the function call execution")?;
|
||||||
tracing::Span::current().record("block_number", execution_receipt.block_number);
|
|
||||||
let tracing_result = self
|
let tracing_result = self
|
||||||
.handle_function_call_call_frame_tracing(execution_receipt.transaction_hash)
|
.handle_function_call_call_frame_tracing(execution_receipt.transaction_hash)
|
||||||
.await
|
.await
|
||||||
|
|||||||
+2
-10
@@ -486,13 +486,9 @@ impl Platform for PolkadotOmniNodePolkavmResolcPlatform {
|
|||||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||||
|
|
||||||
PolkadotOmnichainNode::node_genesis(
|
PolkadotOmnichainNode::node_genesis(
|
||||||
&polkadot_omnichain_node_configuration.path,
|
|
||||||
&wallet,
|
&wallet,
|
||||||
polkadot_omnichain_node_configuration
|
polkadot_omnichain_node_configuration
|
||||||
.parachain_id
|
.chain_spec_path
|
||||||
.context("No parachain id found in the configuration of the polkadot-omni-node")?,
|
|
||||||
polkadot_omnichain_node_configuration
|
|
||||||
.runtime_wasm_path
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("No WASM runtime path found in the polkadot-omni-node configuration")?,
|
.context("No WASM runtime path found in the polkadot-omni-node configuration")?,
|
||||||
)
|
)
|
||||||
@@ -550,13 +546,9 @@ impl Platform for PolkadotOmniNodeRevmSolcPlatform {
|
|||||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||||
|
|
||||||
PolkadotOmnichainNode::node_genesis(
|
PolkadotOmnichainNode::node_genesis(
|
||||||
&polkadot_omnichain_node_configuration.path,
|
|
||||||
&wallet,
|
&wallet,
|
||||||
polkadot_omnichain_node_configuration
|
polkadot_omnichain_node_configuration
|
||||||
.parachain_id
|
.chain_spec_path
|
||||||
.context("No parachain id found in the configuration of the polkadot-omni-node")?,
|
|
||||||
polkadot_omnichain_node_configuration
|
|
||||||
.runtime_wasm_path
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("No WASM runtime path found in the polkadot-omni-node configuration")?,
|
.context("No WASM runtime path found in the polkadot-omni-node configuration")?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ serde_yaml_ng = { workspace = true }
|
|||||||
sp-core = { workspace = true }
|
sp-core = { workspace = true }
|
||||||
sp-runtime = { workspace = true }
|
sp-runtime = { workspace = true }
|
||||||
subxt = { workspace = true }
|
subxt = { workspace = true }
|
||||||
temp-dir = { workspace = true }
|
|
||||||
zombienet-sdk = { workspace = true }
|
zombienet-sdk = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::{File, create_dir_all, remove_dir_all},
|
fs::{File, create_dir_all, remove_dir_all},
|
||||||
io::Read,
|
io::Read,
|
||||||
ops::ControlFlow,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
@@ -35,12 +34,9 @@ use anyhow::Context as _;
|
|||||||
use futures::{FutureExt, Stream, StreamExt};
|
use futures::{FutureExt, Stream, StreamExt};
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use tracing::{Instrument, error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
use revive_dt_common::{
|
use revive_dt_common::fs::clear_directory;
|
||||||
fs::clear_directory,
|
|
||||||
futures::{PollingWaitBehavior, poll},
|
|
||||||
};
|
|
||||||
use revive_dt_config::*;
|
use revive_dt_config::*;
|
||||||
use revive_dt_format::traits::ResolverApi;
|
use revive_dt_format::traits::ResolverApi;
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
@@ -90,12 +86,6 @@ impl GethNode {
|
|||||||
const READY_MARKER: &str = "IPC endpoint opened";
|
const READY_MARKER: &str = "IPC endpoint opened";
|
||||||
const ERROR_MARKER: &str = "Fatal:";
|
const ERROR_MARKER: &str = "Fatal:";
|
||||||
|
|
||||||
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
|
|
||||||
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
|
|
||||||
|
|
||||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
|
||||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
|
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||||
+ AsRef<WalletConfiguration>
|
+ AsRef<WalletConfiguration>
|
||||||
@@ -341,62 +331,15 @@ impl EthereumNode for GethNode {
|
|||||||
transaction: TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = self
|
self.provider()
|
||||||
.provider()
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create provider for transaction submission")?;
|
.context("Failed to create provider for transaction submission")?
|
||||||
|
|
||||||
let pending_transaction = provider
|
|
||||||
.send_transaction(transaction)
|
.send_transaction(transaction)
|
||||||
.await
|
.await
|
||||||
.inspect_err(
|
.context("Encountered an error when submitting a transaction")?
|
||||||
|err| error!(%err, "Encountered an error when submitting the transaction"),
|
.get_receipt()
|
||||||
)
|
.await
|
||||||
.context("Failed to submit transaction to geth node")?;
|
.context("Failed to get the receipt for the transaction")
|
||||||
let transaction_hash = *pending_transaction.tx_hash();
|
|
||||||
|
|
||||||
// The following is a fix for the "transaction indexing is in progress" error that we used
|
|
||||||
// to get. You can find more information on this in the following GH issue in geth
|
|
||||||
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
|
|
||||||
// before we can get the receipt of the transaction it needs to have been indexed by the
|
|
||||||
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it
|
|
||||||
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
|
|
||||||
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
|
|
||||||
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to
|
|
||||||
// implement a retry mechanism for the receipt to keep retrying to get it until it
|
|
||||||
// eventually works, but we only do that if the error we get back is the "transaction
|
|
||||||
// indexing is in progress" error or if the receipt is None.
|
|
||||||
//
|
|
||||||
// Getting the transaction indexed and taking a receipt can take a long time especially when
|
|
||||||
// a lot of transactions are being submitted to the node. Thus, while initially we only
|
|
||||||
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
|
|
||||||
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
|
|
||||||
// backoff each time we attempt to get the receipt and find that it's not available.
|
|
||||||
poll(
|
|
||||||
Self::RECEIPT_POLLING_DURATION,
|
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
|
||||||
move || {
|
|
||||||
let provider = provider.clone();
|
|
||||||
async move {
|
|
||||||
match provider.get_transaction_receipt(transaction_hash).await {
|
|
||||||
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
|
|
||||||
Ok(None) => Ok(ControlFlow::Continue(())),
|
|
||||||
Err(error) => {
|
|
||||||
let error_string = error.to_string();
|
|
||||||
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
|
|
||||||
true => Ok(ControlFlow::Continue(())),
|
|
||||||
false => Err(error.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.instrument(tracing::info_span!(
|
|
||||||
"Awaiting transaction receipt",
|
|
||||||
?transaction_hash
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,34 +350,12 @@ impl EthereumNode for GethNode {
|
|||||||
trace_options: GethDebugTracingOptions,
|
trace_options: GethDebugTracingOptions,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = self
|
self.provider()
|
||||||
.provider()
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create provider for tracing")?;
|
.context("Failed to create provider for tracing")?
|
||||||
poll(
|
.debug_trace_transaction(tx_hash, trace_options)
|
||||||
Self::TRACE_POLLING_DURATION,
|
.await
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
.context("Failed to get the transaction trace")
|
||||||
move || {
|
|
||||||
let provider = provider.clone();
|
|
||||||
let trace_options = trace_options.clone();
|
|
||||||
async move {
|
|
||||||
match provider
|
|
||||||
.debug_trace_transaction(tx_hash, trace_options)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(trace) => Ok(ControlFlow::Break(trace)),
|
|
||||||
Err(error) => {
|
|
||||||
let error_string = error.to_string();
|
|
||||||
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
|
|
||||||
true => Ok(ControlFlow::Continue(())),
|
|
||||||
false => Err(error.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use std::{
|
|||||||
collections::{BTreeMap, HashSet},
|
collections::{BTreeMap, HashSet},
|
||||||
fs::{File, create_dir_all},
|
fs::{File, create_dir_all},
|
||||||
io::Read,
|
io::Read,
|
||||||
ops::ControlFlow,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
@@ -48,12 +47,9 @@ use revive_common::EVMVersion;
|
|||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use tracing::{Instrument, info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
use revive_dt_common::{
|
use revive_dt_common::fs::clear_directory;
|
||||||
fs::clear_directory,
|
|
||||||
futures::{PollingWaitBehavior, poll},
|
|
||||||
};
|
|
||||||
use revive_dt_config::*;
|
use revive_dt_config::*;
|
||||||
use revive_dt_format::traits::ResolverApi;
|
use revive_dt_format::traits::ResolverApi;
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
@@ -116,12 +112,6 @@ impl LighthouseGethNode {
|
|||||||
|
|
||||||
const CONFIG_FILE_NAME: &str = "config.yaml";
|
const CONFIG_FILE_NAME: &str = "config.yaml";
|
||||||
|
|
||||||
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
|
|
||||||
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
|
|
||||||
|
|
||||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
|
||||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
|
|
||||||
|
|
||||||
const VALIDATOR_MNEMONIC: &str = "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete";
|
const VALIDATOR_MNEMONIC: &str = "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete";
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -481,73 +471,6 @@ impl LighthouseGethNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal_execute_transaction<'a>(
|
|
||||||
transaction: TransactionRequest,
|
|
||||||
provider: FillProvider<
|
|
||||||
impl TxFiller<Ethereum> + 'a,
|
|
||||||
impl Provider<Ethereum> + Clone + 'a,
|
|
||||||
Ethereum,
|
|
||||||
>,
|
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + 'a>> {
|
|
||||||
Box::pin(async move {
|
|
||||||
let pending_transaction = provider
|
|
||||||
.send_transaction(transaction)
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::error!(
|
|
||||||
%err,
|
|
||||||
"Encountered an error when submitting the transaction"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.context("Failed to submit transaction to geth node")?;
|
|
||||||
let transaction_hash = *pending_transaction.tx_hash();
|
|
||||||
|
|
||||||
// The following is a fix for the "transaction indexing is in progress" error that we
|
|
||||||
// used to get. You can find more information on this in the following GH issue in geth
|
|
||||||
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
|
|
||||||
// before we can get the receipt of the transaction it needs to have been indexed by the
|
|
||||||
// node's indexer. Just because the transaction has been confirmed it doesn't mean that
|
|
||||||
// it has been indexed. When we call alloy's `get_receipt` it checks if the transaction
|
|
||||||
// was confirmed. If it has been, then it will call `eth_getTransactionReceipt` method
|
|
||||||
// which _might_ return the above error if the tx has not yet been indexed yet. So, we
|
|
||||||
// need to implement a retry mechanism for the receipt to keep retrying to get it until
|
|
||||||
// it eventually works, but we only do that if the error we get back is the "transaction
|
|
||||||
// indexing is in progress" error or if the receipt is None.
|
|
||||||
//
|
|
||||||
// Getting the transaction indexed and taking a receipt can take a long time especially
|
|
||||||
// when a lot of transactions are being submitted to the node. Thus, while initially we
|
|
||||||
// only allowed for 60 seconds of waiting with a 1 second delay in polling, we need to
|
|
||||||
// allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
|
|
||||||
// with exponential backoff each time we attempt to get the receipt and find that it's
|
|
||||||
// not available.
|
|
||||||
poll(
|
|
||||||
Self::RECEIPT_POLLING_DURATION,
|
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(500)),
|
|
||||||
move || {
|
|
||||||
let provider = provider.clone();
|
|
||||||
async move {
|
|
||||||
match provider.get_transaction_receipt(transaction_hash).await {
|
|
||||||
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
|
|
||||||
Ok(None) => Ok(ControlFlow::Continue(())),
|
|
||||||
Err(error) => {
|
|
||||||
let error_string = error.to_string();
|
|
||||||
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
|
|
||||||
true => Ok(ControlFlow::Continue(())),
|
|
||||||
false => Err(error.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.instrument(tracing::info_span!(
|
|
||||||
"Awaiting transaction receipt",
|
|
||||||
?transaction_hash
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn node_genesis(mut genesis: Genesis, wallet: &EthereumWallet) -> Genesis {
|
pub fn node_genesis(mut genesis: Genesis, wallet: &EthereumWallet) -> Genesis {
|
||||||
for signer_address in NetworkWallet::<Ethereum>::signer_addresses(&wallet) {
|
for signer_address in NetworkWallet::<Ethereum>::signer_addresses(&wallet) {
|
||||||
genesis
|
genesis
|
||||||
@@ -626,11 +549,15 @@ impl EthereumNode for LighthouseGethNode {
|
|||||||
transaction: TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = self
|
self.provider()
|
||||||
.http_provider()
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create provider for transaction execution")?;
|
.context("Failed to create provider for transaction submission")?
|
||||||
Self::internal_execute_transaction(transaction, provider).await
|
.send_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.context("Encountered an error when submitting a transaction")?
|
||||||
|
.get_receipt()
|
||||||
|
.await
|
||||||
|
.context("Failed to get the receipt for the transaction")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,35 +568,12 @@ impl EthereumNode for LighthouseGethNode {
|
|||||||
trace_options: GethDebugTracingOptions,
|
trace_options: GethDebugTracingOptions,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = Arc::new(
|
self.provider()
|
||||||
self.http_provider()
|
.await
|
||||||
.await
|
.context("Failed to create provider for tracing")?
|
||||||
.context("Failed to create provider for tracing")?,
|
.debug_trace_transaction(tx_hash, trace_options)
|
||||||
);
|
.await
|
||||||
poll(
|
.context("Failed to get the transaction trace")
|
||||||
Self::TRACE_POLLING_DURATION,
|
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
|
||||||
move || {
|
|
||||||
let provider = provider.clone();
|
|
||||||
let trace_options = trace_options.clone();
|
|
||||||
async move {
|
|
||||||
match provider
|
|
||||||
.debug_trace_transaction(tx_hash, trace_options)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(trace) => Ok(ControlFlow::Break(trace)),
|
|
||||||
Err(error) => {
|
|
||||||
let error_string = error.to_string();
|
|
||||||
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
|
|
||||||
true => Ok(ControlFlow::Continue(())),
|
|
||||||
false => Err(error.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ use revive_dt_report::{
|
|||||||
EthereumMinedBlockInformation, MinedBlockInformation, SubstrateMinedBlockInformation,
|
EthereumMinedBlockInformation, MinedBlockInformation, SubstrateMinedBlockInformation,
|
||||||
};
|
};
|
||||||
use subxt::{OnlineClient, SubstrateConfig};
|
use subxt::{OnlineClient, SubstrateConfig};
|
||||||
use temp_dir::TempDir;
|
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use tracing::{instrument, trace};
|
use tracing::{instrument, trace};
|
||||||
|
|
||||||
@@ -50,10 +49,7 @@ use crate::{
|
|||||||
Node,
|
Node,
|
||||||
constants::INITIAL_BALANCE,
|
constants::INITIAL_BALANCE,
|
||||||
helpers::{Process, ProcessReadinessWaitBehavior},
|
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||||
provider_utils::{
|
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
|
||||||
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
|
|
||||||
execute_transaction,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||||
@@ -73,7 +69,7 @@ pub struct PolkadotOmnichainNode {
|
|||||||
/// The path of the eth-rpc binary.
|
/// The path of the eth-rpc binary.
|
||||||
eth_rpc_binary_path: PathBuf,
|
eth_rpc_binary_path: PathBuf,
|
||||||
/// The path of the runtime's WASM that this node will be spawned with.
|
/// The path of the runtime's WASM that this node will be spawned with.
|
||||||
runtime_wasm_path: Option<PathBuf>,
|
chain_spec_path: Option<PathBuf>,
|
||||||
/// The path of the base directory which contains all of the stored data for this node.
|
/// The path of the base directory which contains all of the stored data for this node.
|
||||||
base_directory_path: PathBuf,
|
base_directory_path: PathBuf,
|
||||||
/// The path of the logs directory which contains all of the stored logs.
|
/// The path of the logs directory which contains all of the stored logs.
|
||||||
@@ -147,8 +143,8 @@ impl PolkadotOmnichainNode {
|
|||||||
.path
|
.path
|
||||||
.to_path_buf(),
|
.to_path_buf(),
|
||||||
eth_rpc_binary_path: eth_rpc_path.to_path_buf(),
|
eth_rpc_binary_path: eth_rpc_path.to_path_buf(),
|
||||||
runtime_wasm_path: polkadot_omnichain_node_configuration
|
chain_spec_path: polkadot_omnichain_node_configuration
|
||||||
.runtime_wasm_path
|
.chain_spec_path
|
||||||
.clone(),
|
.clone(),
|
||||||
base_directory_path: base_directory,
|
base_directory_path: base_directory,
|
||||||
logs_directory_path: logs_directory,
|
logs_directory_path: logs_directory,
|
||||||
@@ -180,10 +176,8 @@ impl PolkadotOmnichainNode {
|
|||||||
let template_chainspec_path = self.base_directory_path.join(Self::CHAIN_SPEC_JSON_FILE);
|
let template_chainspec_path = self.base_directory_path.join(Self::CHAIN_SPEC_JSON_FILE);
|
||||||
|
|
||||||
let chainspec_json = Self::node_genesis(
|
let chainspec_json = Self::node_genesis(
|
||||||
&self.polkadot_omnichain_node_binary_path,
|
|
||||||
&self.wallet,
|
&self.wallet,
|
||||||
self.parachain_id.context("No parachain id provided")?,
|
self.chain_spec_path
|
||||||
self.runtime_wasm_path
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("No runtime path provided")?,
|
.context("No runtime path provided")?,
|
||||||
)
|
)
|
||||||
@@ -202,7 +196,7 @@ impl PolkadotOmnichainNode {
|
|||||||
fn spawn_process(&mut self) -> anyhow::Result<()> {
|
fn spawn_process(&mut self) -> anyhow::Result<()> {
|
||||||
// Error out if the runtime's path or the parachain id are not set which means that the
|
// Error out if the runtime's path or the parachain id are not set which means that the
|
||||||
// arguments we require were not provided.
|
// arguments we require were not provided.
|
||||||
self.runtime_wasm_path
|
self.chain_spec_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("No WASM path provided for the runtime")?;
|
.context("No WASM path provided for the runtime")?;
|
||||||
self.parachain_id
|
self.parachain_id
|
||||||
@@ -361,40 +355,11 @@ impl PolkadotOmnichainNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn node_genesis(
|
pub fn node_genesis(
|
||||||
node_path: &Path,
|
|
||||||
wallet: &EthereumWallet,
|
wallet: &EthereumWallet,
|
||||||
parachain_id: usize,
|
chain_spec_path: &Path,
|
||||||
runtime_wasm_path: &Path,
|
|
||||||
) -> anyhow::Result<serde_json::Value> {
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
let tempdir = TempDir::new().context("Failed to create a temporary directory")?;
|
let unmodified_chainspec_file =
|
||||||
let unmodified_chainspec_path = tempdir.path().join("chainspec.json");
|
File::open(chain_spec_path).context("Failed to open the unmodified chainspec file")?;
|
||||||
|
|
||||||
let output = Command::new(node_path)
|
|
||||||
.arg("chain-spec-builder")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(unmodified_chainspec_path.as_path())
|
|
||||||
.arg("create")
|
|
||||||
.arg("--para-id")
|
|
||||||
.arg(parachain_id.to_string())
|
|
||||||
.arg("--relay-chain")
|
|
||||||
.arg("dontcare")
|
|
||||||
.arg("--runtime")
|
|
||||||
.arg(runtime_wasm_path)
|
|
||||||
.arg("named-preset")
|
|
||||||
.arg("development")
|
|
||||||
.env_remove("RUST_LOG")
|
|
||||||
.output()
|
|
||||||
.context("Failed to export the chain-spec")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Exporting chainspec from polkadot-omni-node failed: {}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let unmodified_chainspec_file = File::open(unmodified_chainspec_path.as_path())
|
|
||||||
.context("Failed to open the unmodified chainspec file")?;
|
|
||||||
let mut chainspec_json =
|
let mut chainspec_json =
|
||||||
serde_json::from_reader::<_, serde_json::Value>(&unmodified_chainspec_file)
|
serde_json::from_reader::<_, serde_json::Value>(&unmodified_chainspec_file)
|
||||||
.context("Failed to read the unmodified chainspec JSON")?;
|
.context("Failed to read the unmodified chainspec JSON")?;
|
||||||
@@ -464,11 +429,15 @@ impl EthereumNode for PolkadotOmnichainNode {
|
|||||||
transaction: TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = self
|
self.provider()
|
||||||
.provider()
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create the provider")?;
|
.context("Failed to create provider for transaction submission")?
|
||||||
execute_transaction(provider, transaction).await
|
.send_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.context("Encountered an error when submitting a transaction")?
|
||||||
|
.get_receipt()
|
||||||
|
.await
|
||||||
|
.context("Failed to get the receipt for the transaction")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,7 @@ use crate::{
|
|||||||
Node,
|
Node,
|
||||||
constants::INITIAL_BALANCE,
|
constants::INITIAL_BALANCE,
|
||||||
helpers::{Process, ProcessReadinessWaitBehavior},
|
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||||
provider_utils::{
|
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
|
||||||
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
|
|
||||||
execute_transaction,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||||
@@ -434,11 +431,15 @@ impl EthereumNode for SubstrateNode {
|
|||||||
transaction: TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = self
|
self.provider()
|
||||||
.provider()
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create the provider")?;
|
.context("Failed to create provider for transaction submission")?
|
||||||
execute_transaction(provider, transaction).await
|
.send_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.context("Encountered an error when submitting a transaction")?
|
||||||
|
.get_receipt()
|
||||||
|
.await
|
||||||
|
.context("Failed to get the receipt for the transaction")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,7 @@ use crate::{
|
|||||||
Node,
|
Node,
|
||||||
constants::INITIAL_BALANCE,
|
constants::INITIAL_BALANCE,
|
||||||
helpers::{Process, ProcessReadinessWaitBehavior},
|
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||||
provider_utils::{
|
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
|
||||||
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
|
|
||||||
execute_transaction,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||||
@@ -433,14 +430,18 @@ impl EthereumNode for ZombienetNode {
|
|||||||
|
|
||||||
fn execute_transaction(
|
fn execute_transaction(
|
||||||
&self,
|
&self,
|
||||||
transaction: alloy::rpc::types::TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let provider = self
|
self.provider()
|
||||||
.provider()
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create the provider")?;
|
.context("Failed to create provider for transaction submission")?
|
||||||
execute_transaction(provider, transaction).await
|
.send_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.context("Encountered an error when submitting a transaction")?
|
||||||
|
.get_receipt()
|
||||||
|
.await
|
||||||
|
.context("Failed to get the receipt for the transaction")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::{borrow::Cow, fmt::Display};
|
|
||||||
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
eips::BlockNumberOrTag,
|
eips::BlockNumberOrTag,
|
||||||
network::{Network, TransactionBuilder},
|
network::{Network, TransactionBuilder},
|
||||||
@@ -111,28 +109,23 @@ where
|
|||||||
},
|
},
|
||||||
state_overrides: Default::default(),
|
state_overrides: Default::default(),
|
||||||
block_overrides: Default::default(),
|
block_overrides: Default::default(),
|
||||||
|
tx_index: Default::default(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.try_into_call_frame()
|
.try_into_call_frame()
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
RpcError::LocalUsageError(
|
RpcError::local_usage_str(
|
||||||
FallbackGasFillerError::new(format!(
|
format!("Expected a callframe trace, but got: {err:?}").as_str(),
|
||||||
"Expected a callframe trace, but got: {err:?}"
|
|
||||||
))
|
|
||||||
.boxed(),
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let gas_used = u64::try_from(trace.gas_used).map_err(|_| {
|
let gas_used = u64::try_from(trace.gas_used).map_err(|_| {
|
||||||
RpcError::LocalUsageError(
|
RpcError::local_usage_str(
|
||||||
FallbackGasFillerError::new(
|
"Transaction trace returned a value of gas used that exceeds u64",
|
||||||
"Transaction trace returned a value of gas used that exceeds u64",
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let gas_limit = gas_used.saturating_mul(120) / 100;
|
let gas_limit = gas_used.saturating_mul(2);
|
||||||
|
|
||||||
if let Some(gas_price) = tx.gas_price() {
|
if let Some(gas_price) = tx.gas_price() {
|
||||||
return Ok(GasFillable::Legacy {
|
return Ok(GasFillable::Legacy {
|
||||||
@@ -174,24 +167,3 @@ impl Default for FallbackGasFiller {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
struct FallbackGasFillerError(Cow<'static, str>);
|
|
||||||
|
|
||||||
impl FallbackGasFillerError {
|
|
||||||
pub fn new(string: impl Into<Cow<'static, str>>) -> Self {
|
|
||||||
Self(string.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn boxed(self) -> Box<Self> {
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for FallbackGasFillerError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
Display::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for FallbackGasFillerError {}
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
mod concurrency_limiter;
|
mod concurrency_limiter;
|
||||||
mod fallback_gas_filler;
|
mod fallback_gas_filler;
|
||||||
mod provider;
|
mod provider;
|
||||||
|
mod receipt_retry_layer;
|
||||||
|
|
||||||
pub use concurrency_limiter::*;
|
pub use concurrency_limiter::*;
|
||||||
pub use fallback_gas_filler::*;
|
pub use fallback_gas_filler::*;
|
||||||
pub use provider::*;
|
pub use provider::*;
|
||||||
|
pub use receipt_retry_layer::*;
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
use std::{ops::ControlFlow, sync::LazyLock, time::Duration};
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
network::{Ethereum, Network, NetworkWallet, TransactionBuilder4844},
|
network::{Network, NetworkWallet, TransactionBuilder4844},
|
||||||
providers::{
|
providers::{
|
||||||
Identity, PendingTransactionBuilder, Provider, ProviderBuilder, RootProvider,
|
Identity, ProviderBuilder, RootProvider,
|
||||||
fillers::{ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, WalletFiller},
|
fillers::{ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, WalletFiller},
|
||||||
},
|
},
|
||||||
rpc::client::ClientBuilder,
|
rpc::client::ClientBuilder,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use revive_dt_common::futures::{PollingWaitBehavior, poll};
|
|
||||||
use tracing::{Instrument, debug, info, info_span};
|
|
||||||
|
|
||||||
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller};
|
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller, RetryLayer};
|
||||||
|
|
||||||
pub type ConcreteProvider<N, W> = FillProvider<
|
pub type ConcreteProvider<N, W> = FillProvider<
|
||||||
JoinFill<
|
JoinFill<
|
||||||
@@ -48,6 +46,7 @@ where
|
|||||||
|
|
||||||
let client = ClientBuilder::default()
|
let client = ClientBuilder::default()
|
||||||
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
|
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
|
||||||
|
.layer(RetryLayer::default())
|
||||||
.connect(rpc_url)
|
.connect(rpc_url)
|
||||||
.await
|
.await
|
||||||
.context("Failed to construct the RPC client")?;
|
.context("Failed to construct the RPC client")?;
|
||||||
@@ -63,70 +62,3 @@ where
|
|||||||
|
|
||||||
Ok(provider)
|
Ok(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_transaction<N, W>(
|
|
||||||
provider: ConcreteProvider<N, W>,
|
|
||||||
transaction: N::TransactionRequest,
|
|
||||||
) -> Result<N::ReceiptResponse>
|
|
||||||
where
|
|
||||||
N: Network<
|
|
||||||
TransactionRequest: TransactionBuilder4844,
|
|
||||||
TxEnvelope = <Ethereum as Network>::TxEnvelope,
|
|
||||||
>,
|
|
||||||
W: NetworkWallet<N>,
|
|
||||||
Identity: TxFiller<N>,
|
|
||||||
FallbackGasFiller: TxFiller<N>,
|
|
||||||
ChainIdFiller: TxFiller<N>,
|
|
||||||
NonceFiller: TxFiller<N>,
|
|
||||||
WalletFiller<W>: TxFiller<N>,
|
|
||||||
{
|
|
||||||
let sendable_transaction = provider
|
|
||||||
.fill(transaction)
|
|
||||||
.await
|
|
||||||
.context("Failed to fill transaction")?;
|
|
||||||
|
|
||||||
let transaction_envelope = sendable_transaction
|
|
||||||
.try_into_envelope()
|
|
||||||
.context("Failed to convert transaction into an envelope")?;
|
|
||||||
let tx_hash = *transaction_envelope.tx_hash();
|
|
||||||
|
|
||||||
let mut pending_transaction = match provider.send_tx_envelope(transaction_envelope).await {
|
|
||||||
Ok(pending_transaction) => pending_transaction,
|
|
||||||
Err(error) => {
|
|
||||||
let error_string = error.to_string();
|
|
||||||
|
|
||||||
if error_string.contains("Transaction Already Imported") {
|
|
||||||
PendingTransactionBuilder::<N>::new(provider.root().clone(), tx_hash)
|
|
||||||
} else {
|
|
||||||
return Err(error).context(format!("Failed to submit transaction {tx_hash}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
debug!(%tx_hash, "Submitted Transaction");
|
|
||||||
|
|
||||||
pending_transaction.set_timeout(Some(Duration::from_secs(120)));
|
|
||||||
let tx_hash = pending_transaction.watch().await.context(format!(
|
|
||||||
"Transaction inclusion watching timeout for {tx_hash}"
|
|
||||||
))?;
|
|
||||||
|
|
||||||
poll(
|
|
||||||
Duration::from_secs(60),
|
|
||||||
PollingWaitBehavior::Constant(Duration::from_secs(3)),
|
|
||||||
|| {
|
|
||||||
let provider = provider.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
match provider.get_transaction_receipt(tx_hash).await {
|
|
||||||
Ok(Some(receipt)) => {
|
|
||||||
info!("Found the transaction receipt");
|
|
||||||
Ok(ControlFlow::Break(receipt))
|
|
||||||
}
|
|
||||||
_ => Ok(ControlFlow::Continue(())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.instrument(info_span!("Polling for receipt", %tx_hash))
|
|
||||||
.await
|
|
||||||
.context(format!("Polling for receipt failed for {tx_hash}"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use alloy::{
|
||||||
|
network::{AnyNetwork, Network},
|
||||||
|
rpc::json_rpc::{RequestPacket, ResponsePacket},
|
||||||
|
transports::{TransportError, TransportErrorKind, TransportFut},
|
||||||
|
};
|
||||||
|
use tokio::time::{interval, timeout};
|
||||||
|
use tower::{Layer, Service};
|
||||||
|
|
||||||
|
/// A layer that allows for automatic retries for getting the receipt.
|
||||||
|
///
|
||||||
|
/// There are certain cases where getting the receipt of a committed transaction might fail. In Geth
|
||||||
|
/// this can happen if the transaction has been committed to the ledger but has not been indexed, in
|
||||||
|
/// the substrate and revive stack it can also happen for other reasons.
|
||||||
|
///
|
||||||
|
/// Therefore, just because the first attempt to get the receipt (after transaction confirmation)
|
||||||
|
/// has failed it doesn't mean that it will continue to fail. This layer can be added to any alloy
|
||||||
|
/// provider to allow the provider to retry getting the receipt for some period of time before it
|
||||||
|
/// considers that a timeout. It attempts to poll for the receipt for the `polling_duration` with an
|
||||||
|
/// interval of `polling_interval` between each poll. If by the end of the `polling_duration` it was
|
||||||
|
/// not able to get the receipt successfully then this is considered to be a timeout.
|
||||||
|
///
|
||||||
|
/// Additionally, this layer allows for retries for other rpc methods such as all tracing methods.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct RetryLayer {
|
||||||
|
/// The amount of time to keep polling for the receipt before considering it a timeout.
|
||||||
|
polling_duration: Duration,
|
||||||
|
|
||||||
|
/// The interval of time to wait between each poll for the receipt.
|
||||||
|
polling_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetryLayer {
|
||||||
|
pub fn new(polling_duration: Duration, polling_interval: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
polling_duration,
|
||||||
|
polling_interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_polling_duration(mut self, polling_duration: Duration) -> Self {
|
||||||
|
self.polling_duration = polling_duration;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_polling_interval(mut self, polling_interval: Duration) -> Self {
|
||||||
|
self.polling_interval = polling_interval;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RetryLayer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
polling_duration: Duration::from_secs(90),
|
||||||
|
polling_interval: Duration::from_millis(500),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Layer<S> for RetryLayer {
|
||||||
|
type Service = RetryService<S>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
RetryService {
|
||||||
|
service: inner,
|
||||||
|
polling_duration: self.polling_duration,
|
||||||
|
polling_interval: self.polling_interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct RetryService<S> {
|
||||||
|
/// The internal service.
|
||||||
|
service: S,
|
||||||
|
|
||||||
|
/// The amount of time to keep polling for the receipt before considering it a timeout.
|
||||||
|
polling_duration: Duration,
|
||||||
|
|
||||||
|
/// The interval of time to wait between each poll for the receipt.
|
||||||
|
polling_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Service<RequestPacket> for RetryService<S>
|
||||||
|
where
|
||||||
|
S: Service<RequestPacket, Future = TransportFut<'static>, Error = TransportError>
|
||||||
|
+ Send
|
||||||
|
+ 'static
|
||||||
|
+ Clone,
|
||||||
|
{
|
||||||
|
type Response = ResponsePacket;
|
||||||
|
type Error = TransportError;
|
||||||
|
type Future = TransportFut<'static>;
|
||||||
|
|
||||||
|
fn poll_ready(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||||
|
self.service.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::nonminimal_bool)]
|
||||||
|
fn call(&mut self, req: RequestPacket) -> Self::Future {
|
||||||
|
type ReceiptOutput = <AnyNetwork as Network>::ReceiptResponse;
|
||||||
|
|
||||||
|
let mut service = self.service.clone();
|
||||||
|
let polling_interval = self.polling_interval;
|
||||||
|
let polling_duration = self.polling_duration;
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let request = req.as_single().ok_or_else(|| {
|
||||||
|
TransportErrorKind::custom_str("Retry layer doesn't support batch requests")
|
||||||
|
})?;
|
||||||
|
let method = request.method();
|
||||||
|
let requires_retries = method == "eth_getTransactionReceipt"
|
||||||
|
|| (method.contains("debug") && method.contains("trace"));
|
||||||
|
|
||||||
|
if !requires_retries {
|
||||||
|
return service.call(req).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout(polling_duration, async {
|
||||||
|
let mut interval = interval(polling_interval);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
let Ok(resp) = service.call(req.clone()).await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let response = resp.as_single().expect("Can't fail");
|
||||||
|
if response.is_error() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == "eth_getTransactionReceipt"
|
||||||
|
&& response
|
||||||
|
.payload()
|
||||||
|
.clone()
|
||||||
|
.deserialize_success::<ReceiptOutput>()
|
||||||
|
.ok()
|
||||||
|
.and_then(|resp| resp.try_into_success().ok())
|
||||||
|
.is_some()
|
||||||
|
|| method != "eth_getTransactionReceipt"
|
||||||
|
{
|
||||||
|
return resp;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| TransportErrorKind::custom_str("Timeout when retrying request"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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_tx: Option<UnboundedSender<RunnerEvent>>,
|
||||||
runner_rx: UnboundedReceiver<RunnerEvent>,
|
runner_rx: UnboundedReceiver<RunnerEvent>,
|
||||||
listener_tx: Sender<ReporterEvent>,
|
listener_tx: Sender<ReporterEvent>,
|
||||||
|
/* Context */
|
||||||
|
file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReportAggregator {
|
impl ReportAggregator {
|
||||||
@@ -43,6 +45,11 @@ impl ReportAggregator {
|
|||||||
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
||||||
let (listener_tx, _) = channel::<ReporterEvent>(0xFFFF);
|
let (listener_tx, _) = channel::<ReporterEvent>(0xFFFF);
|
||||||
Self {
|
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),
|
report: Report::new(context),
|
||||||
remaining_cases: Default::default(),
|
remaining_cases: Default::default(),
|
||||||
runner_tx: Some(runner_tx),
|
runner_tx: Some(runner_tx),
|
||||||
@@ -121,7 +128,7 @@ impl ReportAggregator {
|
|||||||
self.handle_completion(CompletionEvent {});
|
self.handle_completion(CompletionEvent {});
|
||||||
debug!("Report aggregation completed");
|
debug!("Report aggregation completed");
|
||||||
|
|
||||||
let file_name = {
|
let default_file_name = {
|
||||||
let current_timestamp = SystemTime::now()
|
let current_timestamp = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.context("System clock is before UNIX_EPOCH; cannot compute report timestamp")?
|
.context("System clock is before UNIX_EPOCH; cannot compute report timestamp")?
|
||||||
@@ -130,6 +137,7 @@ impl ReportAggregator {
|
|||||||
file_name.push_str(".json");
|
file_name.push_str(".json");
|
||||||
file_name
|
file_name
|
||||||
};
|
};
|
||||||
|
let file_name = self.file_name.unwrap_or(default_file_name);
|
||||||
let file_path = self
|
let file_path = self
|
||||||
.report
|
.report
|
||||||
.context
|
.context
|
||||||
@@ -562,7 +570,7 @@ pub struct Report {
|
|||||||
/// The list of metadata files that were found by the tool.
|
/// The list of metadata files that were found by the tool.
|
||||||
pub metadata_files: BTreeSet<MetadataFilePath>,
|
pub metadata_files: BTreeSet<MetadataFilePath>,
|
||||||
/// Metrics from the execution.
|
/// Metrics from the execution.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub metrics: Option<Metrics>,
|
pub metrics: Option<Metrics>,
|
||||||
/// Information relating to each test case.
|
/// Information relating to each test case.
|
||||||
pub execution_information: BTreeMap<MetadataFilePath, MetadataFileReport>,
|
pub execution_information: BTreeMap<MetadataFilePath, MetadataFileReport>,
|
||||||
@@ -582,7 +590,7 @@ impl Report {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct MetadataFileReport {
|
pub struct MetadataFileReport {
|
||||||
/// Metrics from the execution.
|
/// Metrics from the execution.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub metrics: Option<Metrics>,
|
pub metrics: Option<Metrics>,
|
||||||
/// The report of each case keyed by the case idx.
|
/// The report of each case keyed by the case idx.
|
||||||
pub case_reports: BTreeMap<CaseIdx, CaseReport>,
|
pub case_reports: BTreeMap<CaseIdx, CaseReport>,
|
||||||
@@ -592,7 +600,7 @@ pub struct MetadataFileReport {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct CaseReport {
|
pub struct CaseReport {
|
||||||
/// Metrics from the execution.
|
/// Metrics from the execution.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub metrics: Option<Metrics>,
|
pub metrics: Option<Metrics>,
|
||||||
/// The [`ExecutionReport`] for each one of the [`Mode`]s.
|
/// The [`ExecutionReport`] for each one of the [`Mode`]s.
|
||||||
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
|
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
|
||||||
@@ -602,31 +610,31 @@ pub struct CaseReport {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct ExecutionReport {
|
pub struct ExecutionReport {
|
||||||
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
/// 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>,
|
pub status: Option<TestCaseStatus>,
|
||||||
/// Metrics from the execution.
|
/// Metrics from the execution.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub metrics: Option<Metrics>,
|
pub metrics: Option<Metrics>,
|
||||||
/// Information related to the execution on one of the platforms.
|
/// 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>>,
|
pub platform_execution: PlatformKeyedInformation<Option<ExecutionInformation>>,
|
||||||
/// Information on the compiled contracts.
|
/// 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>>,
|
pub compiled_contracts: BTreeMap<PathBuf, BTreeMap<String, ContractInformation>>,
|
||||||
/// The addresses of the deployed contracts
|
/// 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>>>,
|
pub contract_addresses: BTreeMap<ContractInstance, PlatformKeyedInformation<Vec<Address>>>,
|
||||||
/// Information on the mined blocks as part of this execution.
|
/// 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>>,
|
pub mined_block_information: PlatformKeyedInformation<Vec<MinedBlockInformation>>,
|
||||||
/// Information tracked for each step that was executed.
|
/// 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>,
|
pub steps: BTreeMap<StepPath, StepReport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
||||||
/// it was ignored.
|
/// it was ignored.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "status")]
|
#[serde(tag = "status")]
|
||||||
pub enum TestCaseStatus {
|
pub enum TestCaseStatus {
|
||||||
/// The test case succeeded.
|
/// The test case succeeded.
|
||||||
@@ -664,19 +672,19 @@ pub struct TestCaseNodeInformation {
|
|||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct ExecutionInformation {
|
pub struct ExecutionInformation {
|
||||||
/// Information related to the node assigned to this test case.
|
/// 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>,
|
pub node: Option<TestCaseNodeInformation>,
|
||||||
/// Information on the pre-link compiled contracts.
|
/// 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>,
|
pub pre_link_compilation_status: Option<CompilationStatus>,
|
||||||
/// Information on the post-link compiled contracts.
|
/// 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>,
|
pub post_link_compilation_status: Option<CompilationStatus>,
|
||||||
/// Information on the deployed libraries.
|
/// 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>>,
|
pub deployed_libraries: Option<BTreeMap<ContractInstance, Address>>,
|
||||||
/// Information on the deployed contracts.
|
/// 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>>,
|
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 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 appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||||
/// the compiler was invoked.
|
/// the compiler was invoked.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
compiler_input: Option<CompilerInput>,
|
compiler_input: Option<CompilerInput>,
|
||||||
/// The output of the compiler. This is only included if the appropriate flag is set in the
|
/// The output of the compiler. This is only included if the appropriate flag is set in the
|
||||||
/// CLI contexts.
|
/// CLI contexts.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
compiler_output: Option<CompilerOutput>,
|
compiler_output: Option<CompilerOutput>,
|
||||||
},
|
},
|
||||||
/// The compilation failed.
|
/// The compilation failed.
|
||||||
@@ -707,15 +715,15 @@ pub enum CompilationStatus {
|
|||||||
/// The failure reason.
|
/// The failure reason.
|
||||||
reason: String,
|
reason: String,
|
||||||
/// The version of the compiler used to compile the contracts.
|
/// 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>,
|
compiler_version: Option<Version>,
|
||||||
/// The path of the compiler used to compile the contracts.
|
/// 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>,
|
compiler_path: Option<PathBuf>,
|
||||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
/// 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 appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||||
/// the compiler was invoked.
|
/// the compiler was invoked.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
compiler_input: Option<CompilerInput>,
|
compiler_input: Option<CompilerInput>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -743,24 +751,24 @@ pub struct Metrics {
|
|||||||
pub gas_per_second: Metric<u64>,
|
pub gas_per_second: Metric<u64>,
|
||||||
/* Block Fullness */
|
/* Block Fullness */
|
||||||
pub gas_block_fullness: Metric<u64>,
|
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>>,
|
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>>,
|
pub proof_size_block_fullness: Option<Metric<u64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The data that we store for a given metric (e.g., TPS).
|
/// The data that we store for a given metric (e.g., TPS).
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct Metric<T> {
|
pub struct Metric<T> {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub minimum: Option<PlatformKeyedInformation<T>>,
|
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>>,
|
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>>,
|
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>>,
|
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>>>,
|
pub raw: Option<PlatformKeyedInformation<Vec<T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user