mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-06-11 19:01:02 +00:00
Implement basic reporting facility (#18)
* wip Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * save to file after all tasks done Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * error out early if the workdir does not exist Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * the compiler statistics Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * allow compiler statistics per implementation Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * save compiler problems Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * add flag whether to extract compiler errors Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * whitespace Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com> --------- Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//! The report analyzer enriches the raw report data.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::reporter::CompilationTask;
|
||||
|
||||
/// Provides insights into how well the compilers perform.
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
|
||||
pub struct CompilerStatistics {
|
||||
/// The sum of contracts observed.
|
||||
pub n_contracts: usize,
|
||||
/// The mean size of compiled contracts.
|
||||
pub mean_code_size: usize,
|
||||
/// The mean size of the optimized YUL IR.
|
||||
pub mean_yul_size: usize,
|
||||
/// Is a proxy because the YUL also containes a lot of comments.
|
||||
pub yul_to_bytecode_size_ratio: f32,
|
||||
}
|
||||
|
||||
impl CompilerStatistics {
|
||||
/// Cumulatively update the statistics with the next compiler task.
|
||||
pub fn sample(&mut self, compilation_task: &CompilationTask) {
|
||||
let Some(output) = &compilation_task.json_output else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(contracts) = &output.contracts else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (_solidity, contracts) in contracts.iter() {
|
||||
for (_name, contract) in contracts.iter() {
|
||||
let Some(evm) = &contract.evm else {
|
||||
continue;
|
||||
};
|
||||
let Some(deploy_code) = &evm.deployed_bytecode else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// The EVM bytecode can be unlinked and thus is not necessarily a decodable hex
|
||||
// string; for our statistics this is a good enough approximation.
|
||||
let bytecode_size = deploy_code.object.len() / 2;
|
||||
|
||||
let yul_size = contract
|
||||
.ir_optimized
|
||||
.as_ref()
|
||||
.expect("if the contract has a deploy code it should also have the opimized IR")
|
||||
.len();
|
||||
|
||||
self.update_sizes(bytecode_size, yul_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the size statistics cumulatively.
|
||||
fn update_sizes(&mut self, bytecode_size: usize, yul_size: usize) {
|
||||
let n_previous = self.n_contracts;
|
||||
let n_current = self.n_contracts + 1;
|
||||
|
||||
self.n_contracts = n_current;
|
||||
|
||||
self.mean_code_size = (n_previous * self.mean_code_size + bytecode_size) / n_current;
|
||||
self.mean_yul_size = (n_previous * self.mean_yul_size + yul_size) / n_current;
|
||||
|
||||
if self.mean_code_size > 0 {
|
||||
self.yul_to_bytecode_size_ratio =
|
||||
self.mean_yul_size as f32 / self.mean_code_size as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CompilerStatistics;
|
||||
|
||||
#[test]
|
||||
fn compiler_statistics() {
|
||||
let mut received = CompilerStatistics::default();
|
||||
received.update_sizes(0, 0);
|
||||
received.update_sizes(3, 37);
|
||||
received.update_sizes(123, 456);
|
||||
|
||||
let mean_code_size = 41; // rounding error from integer truncation
|
||||
let mean_yul_size = 164;
|
||||
let expected = CompilerStatistics {
|
||||
n_contracts: 3,
|
||||
mean_code_size,
|
||||
mean_yul_size,
|
||||
yul_to_bytecode_size_ratio: mean_yul_size as f32 / mean_code_size as f32,
|
||||
};
|
||||
|
||||
assert_eq!(received, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! The revive differential tests reporting facility.
|
||||
|
||||
pub mod analyzer;
|
||||
pub mod reporter;
|
||||
@@ -0,0 +1,243 @@
|
||||
//! The reporter is the central place observing test execution by collecting data.
|
||||
//!
|
||||
//! The data collected gives useful insights into the outcome of the test run
|
||||
//! and helps identifying and reproducing failing cases.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File, create_dir_all},
|
||||
path::PathBuf,
|
||||
sync::{Mutex, OnceLock},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_dt_config::{Arguments, TestingPlatform};
|
||||
use revive_dt_format::{corpus::Corpus, mode::SolcMode};
|
||||
use revive_solc_json_interface::{SolcStandardJsonInput, SolcStandardJsonOutput};
|
||||
|
||||
use crate::analyzer::CompilerStatistics;
|
||||
|
||||
pub(crate) static REPORTER: OnceLock<Mutex<Report>> = OnceLock::new();
|
||||
|
||||
/// The `Report` datastructure stores all relevant inforamtion required for generating reports.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Report {
|
||||
/// The configuration used during the test.
|
||||
pub config: Arguments,
|
||||
/// The observed test corpora.
|
||||
pub corpora: Vec<Corpus>,
|
||||
/// The observed test definitions.
|
||||
pub metadata_files: Vec<PathBuf>,
|
||||
/// The observed compilation results.
|
||||
pub compiler_results: HashMap<TestingPlatform, Vec<CompilationResult>>,
|
||||
/// The observed compilation statistics.
|
||||
pub compiler_statistics: HashMap<TestingPlatform, CompilerStatistics>,
|
||||
/// The file name this is serialized to.
|
||||
#[serde(skip)]
|
||||
directory: PathBuf,
|
||||
}
|
||||
|
||||
/// Contains a compiled contract.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CompilationTask {
|
||||
/// The observed compiler input.
|
||||
pub json_input: SolcStandardJsonInput,
|
||||
/// The observed compiler output.
|
||||
pub json_output: Option<SolcStandardJsonOutput>,
|
||||
/// The observed compiler mode.
|
||||
pub mode: SolcMode,
|
||||
/// The observed compiler version.
|
||||
pub compiler_version: String,
|
||||
/// The observed error, if any.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a report about a compilation task.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CompilationResult {
|
||||
/// The observed compilation task.
|
||||
pub compilation_task: CompilationTask,
|
||||
/// The linked span.
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
/// The [Span] struct indicates the context of what is being reported.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct Span {
|
||||
/// The corpus index this belongs to.
|
||||
corpus: usize,
|
||||
/// The metadata file this belongs to.
|
||||
metadata_file: usize,
|
||||
/// The index of the case definition this belongs to.
|
||||
case: usize,
|
||||
/// The index of the case input this belongs to.
|
||||
input: usize,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// The file name where this report will be written to.
|
||||
pub const FILE_NAME: &str = "report.json";
|
||||
|
||||
/// The [Span] is expected to initialize the reporter by providing the config.
|
||||
const INITIALIZED_VIA_SPAN: &str = "requires a Span which initializes the reporter";
|
||||
|
||||
/// Create a new [Report].
|
||||
fn new(config: Arguments) -> anyhow::Result<Self> {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
|
||||
let directory = config.directory().join("report").join(format!("{now}"));
|
||||
if !directory.exists() {
|
||||
create_dir_all(&directory)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
directory,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a compilation task to the report.
|
||||
pub fn compilation(span: Span, platform: TestingPlatform, compilation_task: CompilationTask) {
|
||||
let mut report = REPORTER
|
||||
.get()
|
||||
.expect(Report::INITIALIZED_VIA_SPAN)
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
report
|
||||
.compiler_statistics
|
||||
.entry(platform)
|
||||
.or_default()
|
||||
.sample(&compilation_task);
|
||||
|
||||
report
|
||||
.compiler_results
|
||||
.entry(platform)
|
||||
.or_default()
|
||||
.push(CompilationResult {
|
||||
compilation_task,
|
||||
span,
|
||||
});
|
||||
}
|
||||
|
||||
/// Write the report to disk.
|
||||
pub fn save() -> anyhow::Result<()> {
|
||||
let Some(reporter) = REPORTER.get() else {
|
||||
return Ok(());
|
||||
};
|
||||
let report = reporter.lock().unwrap();
|
||||
|
||||
if let Err(error) = report.write_to_file() {
|
||||
anyhow::bail!("can not write report: {error}");
|
||||
}
|
||||
|
||||
if report.config.extract_problems {
|
||||
if let Err(error) = report.save_compiler_problems() {
|
||||
anyhow::bail!("can not write compiler problems: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write compiler problems to disk for later debugging.
|
||||
pub fn save_compiler_problems(&self) -> anyhow::Result<()> {
|
||||
for (platform, results) in self.compiler_results.iter() {
|
||||
for result in results {
|
||||
// ignore if there were no errors
|
||||
if result.compilation_task.error.is_none()
|
||||
&& result
|
||||
.compilation_task
|
||||
.json_output
|
||||
.as_ref()
|
||||
.and_then(|output| output.errors.as_ref())
|
||||
.map(|errors| errors.is_empty())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = &self.metadata_files[result.span.metadata_file]
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(format!("{platform}_errors"));
|
||||
if !path.exists() {
|
||||
create_dir_all(path)?;
|
||||
}
|
||||
|
||||
if let Some(error) = result.compilation_task.error.as_ref() {
|
||||
fs::write(path.join("compiler_error.txt"), error)?;
|
||||
}
|
||||
|
||||
if let Some(errors) = result.compilation_task.json_output.as_ref() {
|
||||
let file = File::create(path.join("compiler_output.txt"))?;
|
||||
serde_json::to_writer_pretty(file, &errors)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_file(&self) -> anyhow::Result<()> {
|
||||
let path = self.directory.join(Self::FILE_NAME);
|
||||
|
||||
let file = File::create(&path).context(path.display().to_string())?;
|
||||
serde_json::to_writer_pretty(file, &self)?;
|
||||
|
||||
log::info!("report written to: {}", path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Create a new [Span] with case and input index at 0.
|
||||
///
|
||||
/// Initializes the reporting facility on the first call.
|
||||
pub fn new(corpus: Corpus, config: Arguments) -> anyhow::Result<Self> {
|
||||
let report = Mutex::new(Report::new(config)?);
|
||||
let mut reporter = REPORTER.get_or_init(|| report).lock().unwrap();
|
||||
reporter.corpora.push(corpus);
|
||||
|
||||
Ok(Self {
|
||||
corpus: reporter.corpora.len() - 1,
|
||||
metadata_file: 0,
|
||||
case: 0,
|
||||
input: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Advance to the next metadata file: Resets the case input index to 0.
|
||||
pub fn next_metadata(&mut self, metadata_file: PathBuf) {
|
||||
let mut reporter = REPORTER
|
||||
.get()
|
||||
.expect(Report::INITIALIZED_VIA_SPAN)
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
reporter.metadata_files.push(metadata_file);
|
||||
|
||||
self.metadata_file = reporter.metadata_files.len() - 1;
|
||||
self.case = 0;
|
||||
self.input = 0;
|
||||
}
|
||||
|
||||
/// Advance to the next case: Increas the case index by one and resets the input index to 0.
|
||||
pub fn next_case(&mut self) {
|
||||
self.case += 1;
|
||||
self.input = 0;
|
||||
}
|
||||
|
||||
/// Advance to the next input.
|
||||
pub fn next_input(&mut self) {
|
||||
self.input += 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user