commonware-conformance-macros 2026.9.0

Augment the development of commonware-conformance with procedural macros.
Documentation
#[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<String, BTreeSet<String>>;

#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
    Check,
    Prune,
}

impl Mode {
    fn parse() -> Result<Self, String> {
        match env::args().nth(1).as_deref() {
            Some("check") => Ok(Self::Check),
            Some("prune") => Ok(Self::Prune),
            _ => Err("usage: fixtures <check|prune>".to_string()),
        }
    }
}

#[derive(Deserialize)]
struct TestList {
    #[serde(rename = "rust-suites")]
    suites: BTreeMap<String, Suite>,
}

#[derive(Deserialize)]
struct Suite {
    #[serde(rename = "binary-name")]
    binary_name: String,
    cwd: PathBuf,
    status: String,
    #[serde(default)]
    testcases: BTreeMap<String, serde::de::IgnoredAny>,
}

fn collect_inventories(test_list: TestList) -> BTreeMap<PathBuf, Inventories> {
    let mut files = BTreeMap::<PathBuf, Inventories>::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<String>) -> 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<String> {
    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<String> {
    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<Vec<String>, 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<Vec<u16>>");
        let unrelated = "commonware_other::tests::conformance::CodecConformance<Vec<u8>>";
        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()])
        );
    }
}