govctl 0.9.3

Project governance CLI for RFC, ADR, and Work Item management
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};

type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;

#[test]
fn test_tests_do_not_use_include_macro_for_suite_splitting() -> TestResult {
    let tests_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
    let mut offenders = Vec::new();

    for path in rust_files_under(&tests_dir)? {
        let content = fs::read_to_string(&path)?;
        let rel_path = path.strip_prefix(env!("CARGO_MANIFEST_DIR"))?;
        offenders.extend(macro_injection_locations(rel_path, &content)?);
    }

    assert!(
        offenders.is_empty(),
        "tests must use normal Rust modules instead of macro-based source injection:\n{}",
        offenders.join("\n")
    );
    Ok(())
}

#[test]
fn test_include_macro_detector_handles_multiline_spacing() -> TestResult {
    let sample = ["fn test() {", "include", "!", "(\"case.rs\");", "}"].join("\n");
    let offenders = macro_injection_locations(Path::new("tests/example.rs"), &sample)?;

    assert_eq!(offenders, vec!["tests/example.rs:2"]);
    Ok(())
}

fn macro_injection_locations(path: &Path, content: &str) -> TestResult<Vec<String>> {
    let pattern = Regex::new(&format!(r"{name}\s*!\s*\(", name = "include"))?;
    Ok(pattern
        .find_iter(content)
        .map(|matched| {
            format!(
                "{}:{}",
                path.display(),
                line_number(content, matched.start())
            )
        })
        .collect())
}

fn line_number(content: &str, byte_offset: usize) -> usize {
    content[..byte_offset]
        .bytes()
        .filter(|byte| *byte == b'\n')
        .count()
        + 1
}

fn rust_files_under(root: &Path) -> TestResult<Vec<PathBuf>> {
    let mut files = Vec::new();
    collect_rust_files(root, &mut files)?;
    files.sort();
    Ok(files)
}

fn collect_rust_files(dir: &Path, files: &mut Vec<PathBuf>) -> TestResult {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            collect_rust_files(&path, files)?;
        } else if path.extension().is_some_and(|ext| ext == "rs") {
            files.push(path);
        }
    }
    Ok(())
}