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), ) }) .filter(|(_, status)| *status == Status::Failed) .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), }) } }