#[path = "../naming.rs"] mod naming; use naming::type_to_ident; use serde::Deserialize; use std::{ collections::{BTreeMap, BTreeSet}, env, fs, io::stdin, path::{Path, PathBuf}, process::ExitCode, }; type Inventories = BTreeMap>; #[derive(Clone, Copy, PartialEq, Eq)] enum Mode { Check, Prune, } impl Mode { fn parse() -> Result { match env::args().nth(1).as_deref() { Some("check") => Ok(Self::Check), Some("prune") => Ok(Self::Prune), _ => Err("usage: fixtures ".to_string()), } } } #[derive(Deserialize)] struct TestList { #[serde(rename = "rust-suites")] suites: BTreeMap, } #[derive(Deserialize)] struct Suite { #[serde(rename = "binary-name")] binary_name: String, cwd: PathBuf, status: String, #[serde(default)] testcases: BTreeMap, } fn collect_inventories(test_list: TestList) -> BTreeMap { let mut files = BTreeMap::::new(); for suite in test_list.suites.into_values() { if suite.status != "listed" { continue; } let binary_name = suite.binary_name.replace('-', "_"); let tests = files .entry(suite.cwd.join("conformance.toml")) .or_default() .entry(binary_name.clone()) .or_default(); tests.extend( suite .testcases .into_keys() .filter(|name| name.ends_with("_conformance_")) .map(|name| format!("{binary_name}::{name}")), ); } files } fn has_test(fixture: &str, tests: &BTreeSet) -> bool { tests.iter().any(|test| { let Some((module, test_name)) = test.rsplit_once("::test_") else { return false; }; let Some(type_name) = fixture .strip_prefix(module) .and_then(|suffix| suffix.strip_prefix("::")) else { return false; }; test_name == format!("{}_conformance_", type_to_ident(type_name)) }) } fn dangling_entries(file: &toml::Table, inventories: &Inventories) -> Vec { file.keys() .filter(|fixture| { let Some((binary_name, _)) = fixture.split_once("::") else { return false; }; let Some(tests) = inventories.get(binary_name) else { return false; }; !has_test(fixture, tests) }) .cloned() .collect() } fn inspect_file(mode: Mode, file: &mut toml::Table, inventories: &Inventories) -> Vec { let dangling = dangling_entries(file, inventories); if mode == Mode::Prune { for fixture in &dangling { file.remove(fixture); } } dangling } fn process_file(mode: Mode, path: &Path, inventories: &Inventories) -> Result, String> { if !path.exists() { return Ok(Vec::new()); } let contents = fs::read_to_string(path) .map_err(|error| format!("failed to read {}: {error}", path.display()))?; let mut file = toml::from_str(&contents).map_err(|error| error.to_string())?; let dangling = inspect_file(mode, &mut file, inventories); if mode == Mode::Prune && !dangling.is_empty() { let contents = toml::to_string_pretty(&file).map_err(|error| error.to_string())?; fs::write(path, contents) .map_err(|error| format!("failed to write {}: {error}", path.display()))?; } Ok(dangling) } fn run() -> Result<(), String> { let mode = Mode::parse()?; let test_list = serde_json::from_reader(stdin().lock()).map_err(|error| error.to_string())?; let files = collect_inventories(test_list); let mut dangling_count = 0usize; for (path, inventories) in files { let dangling = process_file(mode, &path, &inventories)?; for fixture in &dangling { eprintln!("{}: dangling fixture `{fixture}`", path.display()); } dangling_count += dangling.len(); } if mode == Mode::Check && dangling_count > 0 { return Err(format!( "found {dangling_count} dangling conformance fixtures" )); } Ok(()) } fn main() -> ExitCode { if let Err(error) = run() { eprintln!("error: {error}"); return ExitCode::FAILURE; } ExitCode::SUCCESS } #[cfg(test)] mod tests { use super::*; fn fixture_file(fixtures: &[&str]) -> toml::Table { fixtures .iter() .map(|fixture| { ( (*fixture).to_string(), toml::Value::Table(toml::Table::new()), ) }) .collect() } fn inventory(tests: &[&str]) -> Inventories { BTreeMap::from([( "commonware_example".to_string(), tests.iter().map(|test| (*test).to_string()).collect(), )]) } #[test] fn checks_and_prunes_dangling_entries_in_selected_crates() { const MODULE: &str = "commonware_example::tests::conformance"; let live = format!("{MODULE}::CodecConformance<[u8;32]>"); let dangling = format!("{MODULE}::CodecConformance>"); let unrelated = "commonware_other::tests::conformance::CodecConformance>"; let mut file = fixture_file(&[&live, &dangling, unrelated]); let inventories = inventory(&[&format!( "{MODULE}::test_codec_conformance_u8_32_conformance_" )]); assert_eq!( inspect_file(Mode::Check, &mut file, &inventories), std::slice::from_ref(&dangling) ); assert_eq!(file.len(), 3); assert_eq!( inspect_file(Mode::Prune, &mut file, &inventories), [dangling] ); assert_eq!(file.len(), 2); assert!(file.contains_key(unrelated)); } #[test] fn collects_only_conformance_tests() { let test_list = TestList { suites: BTreeMap::from([( "commonware-example".to_string(), Suite { binary_name: "commonware_example".to_string(), cwd: PathBuf::from("/workspace/example"), status: "listed".to_string(), testcases: BTreeMap::from([ ( "tests::test_live_conformance_".to_string(), serde::de::IgnoredAny, ), ("tests::test_ordinary".to_string(), serde::de::IgnoredAny), ]), }, )]), }; let inventories = collect_inventories(test_list); assert_eq!( inventories[&PathBuf::from("/workspace/example/conformance.toml")]["commonware_example"], BTreeSet::from(["commonware_example::tests::test_live_conformance_".to_string()]) ); } }