diff --git a/.github/actions/run-differential-tests/action.yml b/.github/actions/run-differential-tests/action.yml index 06f4112..477fd81 100644 --- a/.github/actions/run-differential-tests/action.yml +++ b/.github/actions/run-differential-tests/action.yml @@ -41,6 +41,10 @@ inputs: description: "The id of the parachain to spawn with the polkadot-omni-node. This is only required if the polkadot-omni-node is one of the selected platforms." type: number required: false + expectations-file-path: + description: "Path to the expectations file to use to compare against." + type: string + required: false runs: using: "composite" @@ -79,6 +83,12 @@ runs: run: | ${{ inputs['cargo-command'] }} build --locked --profile release -p pallet-revive-eth-rpc -p revive-dev-node --manifest-path ${{ inputs['polkadot-sdk-path'] }}/Cargo.toml ${{ inputs['cargo-command'] }} build --locked --profile release --bin polkadot-omni-node --manifest-path ${{ inputs['polkadot-sdk-path'] }}/Cargo.toml + - name: Installing retester + shell: bash + run: ${{ inputs['cargo-command'] }} install --path ./revive-differential-tests/crates/core + - name: Installing report-processor + shell: bash + run: ${{ inputs['cargo-command'] }} install --path ./revive-differential-tests/crates/report-processor - name: Running the Differential Tests shell: bash run: | @@ -96,11 +106,12 @@ runs: ) fi - ${{ inputs['cargo-command'] }} run --locked --manifest-path revive-differential-tests/Cargo.toml -- test \ + retester test \ --test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/simple \ --test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/complex \ --test ./revive-differential-tests/resolc-compiler-tests/fixtures/solidity/translated_semantic_tests \ --platform ${{ inputs['platform'] }} \ + --report.file-name report.json \ --concurrency.number-of-nodes 10 \ --concurrency.number-of-threads 10 \ --concurrency.number-of-concurrent-tasks 100 \ @@ -110,22 +121,21 @@ runs: --eth-rpc.path ${{ inputs['polkadot-sdk-path'] }}/target/release/eth-rpc \ --polkadot-omni-node.path ${{ inputs['polkadot-sdk-path'] }}/target/release/polkadot-omni-node \ --resolc.path ./resolc \ - "${OMNI_ARGS[@]}" - - name: Creating a markdown report of the test execution + "${OMNI_ARGS[@]}" || true + - name: Generate the expectation file shell: bash - if: ${{ always() }} - run: | - mv ./workdir/*.json report.json - python3 revive-differential-tests/scripts/process-differential-tests-report.py report.json ${{ inputs['platform'] }} + run: report-processor generate-expectations-file --report-path ./workdir/report.json --output-path ./workdir/expectations.json --remove-prefix ./revive-differential-tests/resolc-compiler-tests - name: Upload the Report to the CI uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f - if: ${{ always() }} with: - name: report-${{ inputs['platform'] }}.md - path: report.md - - name: Posting the report as a comment on the PR - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 - if: ${{ always() }} + name: ${{ inputs['platform'] }}-report.json + path: ./workdir/report.json + - name: Upload the Report to the CI + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: - header: diff-tests-report-${{ inputs['platform'] }} - path: report.md + name: ${{ inputs['platform'] }}.json + path: ./workdir/expectations.json + - name: Check Expectations + shell: bash + if: ${{ inputs['expectations-file-path'] != '' }} + run: report-processor compare-expectation-files --base-expectation-path ${{ inputs['expectations-file-path'] }} --other-expectation-path ./workdir/expectations.json diff --git a/Cargo.lock b/Cargo.lock index b91f302..7bf59a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5910,6 +5910,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "revive-dt-report-processor" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "revive-dt-common", + "revive-dt-report", + "serde", + "serde_json", +] + [[package]] name = "revive-dt-solc-binaries" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index dd251c9..8f90b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ revive-dt-node-interaction = { version = "0.1.0", path = "crates/node-interactio revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" } revive-dt-report = { version = "0.1.0", path = "crates/report" } revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" } +revive-dt-report-processor = { version = "0.1.0", path = "crates/report-processor" } alloy = { version = "1.4.1", features = ["full", "genesis", "json-rpc"] } ansi_term = "0.12.1" @@ -81,7 +82,12 @@ zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = [profile.bench] inherits = "release" -lto = true codegen-units = 1 +lto = true + +[profile.production] +inherits = "release" +codegen-units = 1 +lto = true [workspace.lints.clippy] diff --git a/crates/common/src/types/mode.rs b/crates/common/src/types/mode.rs index a3aff67..2ed2ffe 100644 --- a/crates/common/src/types/mode.rs +++ b/crates/common/src/types/mode.rs @@ -23,6 +23,18 @@ pub struct Mode { pub version: Option, } +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 { + Some(self.cmp(other)) + } +} + impl Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.pipeline.fmt(f)?; diff --git a/crates/common/src/types/parsed_test_specifier.rs b/crates/common/src/types/parsed_test_specifier.rs index 3460ae5..2710bf0 100644 --- a/crates/common/src/types/parsed_test_specifier.rs +++ b/crates/common/src/types/parsed_test_specifier.rs @@ -1,10 +1,15 @@ -use std::{fmt::Display, path::PathBuf, str::FromStr}; +use std::{ + fmt::Display, + path::{Path, PathBuf}, + str::FromStr, +}; use anyhow::{Context as _, bail}; +use serde::{Deserialize, Serialize}; use crate::types::Mode; -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ParsedTestSpecifier { /// All of the test cases in the file should be ran across all of the specified modes FileOrDirectory { @@ -34,6 +39,22 @@ pub enum ParsedTestSpecifier { }, } +impl ParsedTestSpecifier { + pub fn metadata_path(&self) -> &Path { + match self { + ParsedTestSpecifier::FileOrDirectory { + metadata_or_directory_file_path: metadata_file_path, + } + | ParsedTestSpecifier::Case { + metadata_file_path, .. + } + | ParsedTestSpecifier::CaseWithMode { + metadata_file_path, .. + } => metadata_file_path, + } + } +} + impl Display for ParsedTestSpecifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -131,3 +152,22 @@ impl TryFrom<&str> for ParsedTestSpecifier { value.parse() } } + +impl Serialize for ParsedTestSpecifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ParsedTestSpecifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + string.parse().map_err(serde::de::Error::custom) + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index be8838c..9679478 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1113,6 +1113,10 @@ pub struct ReportConfiguration { /// Controls if the compiler output is included in the final report. #[clap(long = "report.include-compiler-output")] pub include_compiler_output: bool, + + /// The filename to use for the report. + #[clap(long = "report.file-name")] + pub file_name: Option, } #[derive(Clone, Debug, Parser, Serialize, Deserialize)] diff --git a/crates/report-processor/Cargo.toml b/crates/report-processor/Cargo.toml new file mode 100644 index 0000000..ce71d10 --- /dev/null +++ b/crates/report-processor/Cargo.toml @@ -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 diff --git a/crates/report-processor/src/main.rs b/crates/report-processor/src/main.rs new file mode 100644 index 0000000..ae6fa46 --- /dev/null +++ b/crates/report-processor/src/main.rs @@ -0,0 +1,328 @@ +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::>>()?; + + 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), + ) + }) + .collect::(); + + 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::>(); + + 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, 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, + + /// 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, + }, + + /// 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>, + + /// The path of the other expectation file. + #[clap(long)] + other_expectation_path: JsonFile>, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum Status { + Succeeded, + Failed, + Ignored, +} + +impl From 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 { + path: PathBuf, + content: Box, +} + +impl Deref for JsonFile { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.content + } +} + +impl DerefMut for JsonFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.content + } +} + +impl FromStr for JsonFile +where + T: DeserializeOwned, +{ + type Err = Error; + + fn from_str(s: &str) -> Result { + 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::() + )) + } +} + +impl Display for JsonFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.path.display(), f) + } +} + +impl From> for String { + fn from(value: JsonFile) -> 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> 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 for TestSpecifier<'static> { + type Error = Error; + + fn try_from(value: ParsedTestSpecifier) -> Result { + 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(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'d, 'a> Deserialize<'d> for TestSpecifier<'a> { + fn deserialize(deserializer: D) -> std::result::Result + 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), + }) + } +} diff --git a/crates/report/src/aggregator.rs b/crates/report/src/aggregator.rs index c2d84a3..3f8b0de 100644 --- a/crates/report/src/aggregator.rs +++ b/crates/report/src/aggregator.rs @@ -36,6 +36,8 @@ pub struct ReportAggregator { runner_tx: Option>, runner_rx: UnboundedReceiver, listener_tx: Sender, + /* Context */ + file_name: Option, } impl ReportAggregator { @@ -43,6 +45,11 @@ impl ReportAggregator { let (runner_tx, runner_rx) = unbounded_channel::(); let (listener_tx, _) = channel::(0xFFFF); Self { + file_name: match context { + Context::Test(ref context) => context.report_configuration.file_name.clone(), + Context::Benchmark(ref context) => context.report_configuration.file_name.clone(), + Context::ExportJsonSchema | Context::ExportGenesis(..) => None, + }, report: Report::new(context), remaining_cases: Default::default(), runner_tx: Some(runner_tx), @@ -121,7 +128,7 @@ impl ReportAggregator { self.handle_completion(CompletionEvent {}); debug!("Report aggregation completed"); - let file_name = { + let default_file_name = { let current_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .context("System clock is before UNIX_EPOCH; cannot compute report timestamp")? @@ -130,6 +137,7 @@ impl ReportAggregator { file_name.push_str(".json"); file_name }; + let file_name = self.file_name.unwrap_or(default_file_name); let file_path = self .report .context @@ -562,7 +570,7 @@ pub struct Report { /// The list of metadata files that were found by the tool. pub metadata_files: BTreeSet, /// Metrics from the execution. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub metrics: Option, /// Information relating to each test case. pub execution_information: BTreeMap, @@ -582,7 +590,7 @@ impl Report { #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct MetadataFileReport { /// Metrics from the execution. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub metrics: Option, /// The report of each case keyed by the case idx. pub case_reports: BTreeMap, @@ -592,7 +600,7 @@ pub struct MetadataFileReport { #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct CaseReport { /// Metrics from the execution. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub metrics: Option, /// The [`ExecutionReport`] for each one of the [`Mode`]s. #[serde_as(as = "HashMap")] @@ -602,31 +610,31 @@ pub struct CaseReport { #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct ExecutionReport { /// Information on the status of the test case and whether it succeeded, failed, or was ignored. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub status: Option, /// Metrics from the execution. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub metrics: Option, /// 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>, /// Information on the compiled contracts. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub compiled_contracts: BTreeMap>, /// 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>>, /// 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>, /// 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, } /// Information related to the status of the test. Could be that the test succeeded, failed, or that /// it was ignored. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "status")] pub enum TestCaseStatus { /// The test case succeeded. @@ -664,19 +672,19 @@ pub struct TestCaseNodeInformation { #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct ExecutionInformation { /// Information related to the node assigned to this test case. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub node: Option, /// 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, /// 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, /// Information on the deployed libraries. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub deployed_libraries: Option>, /// Information on the deployed contracts. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub deployed_contracts: Option>, } @@ -695,11 +703,11 @@ pub enum CompilationStatus { /// The input provided to the compiler to compile the contracts. This is only included if /// the appropriate flag is set in the CLI context and if the contracts were not cached and /// the compiler was invoked. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] compiler_input: Option, /// The output of the compiler. This is only included if the appropriate flag is set in the /// CLI contexts. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] compiler_output: Option, }, /// The compilation failed. @@ -707,15 +715,15 @@ pub enum CompilationStatus { /// The failure reason. reason: String, /// The version of the compiler used to compile the contracts. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] compiler_version: Option, /// 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, /// The input provided to the compiler to compile the contracts. This is only included if /// the appropriate flag is set in the CLI context and if the contracts were not cached and /// the compiler was invoked. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] compiler_input: Option, }, } @@ -743,24 +751,24 @@ pub struct Metrics { pub gas_per_second: Metric, /* Block Fullness */ pub gas_block_fullness: Metric, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub ref_time_block_fullness: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub proof_size_block_fullness: Option>, } /// The data that we store for a given metric (e.g., TPS). #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Metric { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub minimum: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub maximum: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub mean: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub median: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub raw: Option>>, }