koala-core 1.0.4

Shared types, invariant evaluator, and primitives for the koala framework.
Documentation
use crate::invariant::rules::util::{list_cargo_tomls, rel};
use crate::invariant::{Category, Context, Invariant, Outcome};
use std::fs;
use std::path::Path;

pub struct CrateTested;

const PUBLISHABLE_PARENT: &str = "crates";

impl Invariant for CrateTested {
    fn id(&self) -> &'static str {
        "arch.crate-tested"
    }
    fn category(&self) -> Category {
        Category::Arch
    }
    fn intent(&self) -> &'static str {
        "Every workspace crate under `crates/` exposes at least one \
         `#[test]` (inline mod, tests/, or benches/). Untested crates \
         can't be refactored safely."
    }
    fn adr(&self) -> Option<&'static str> {
        Some("ADR-0001")
    }

    fn evaluate(&self, ctx: &Context) -> Outcome {
        let mut missing: Vec<String> = Vec::new();
        for cargo in list_cargo_tomls(ctx.root()) {
            let parent = match cargo.parent() {
                Some(p) => p,
                None => continue,
            };
            // Only inspect crates that live under `crates/<name>/Cargo.toml`.
            if !parent
                .components()
                .any(|c| c.as_os_str() == PUBLISHABLE_PARENT)
            {
                continue;
            }
            // Workspace root Cargo.toml has no `[package]` section.
            let Ok(text) = fs::read_to_string(&cargo) else {
                continue;
            };
            if !text.lines().any(|l| l.trim() == "[package]") {
                continue;
            }
            // Pure-binary crates (CLI entry points with no library
            // surface) are exempt: their logic lives in libraries
            // that are tested separately. A binary+lib hybrid must
            // still test its lib portion — `[[bin]]` alone isn't a
            // free pass.
            let has_bin = text.contains("[[bin]]");
            let lib_path = parent.join("src/lib.rs");
            let has_lib = lib_path.is_file() || text.contains("[lib]");
            if has_bin && !has_lib {
                continue;
            }
            if !crate_has_tests(parent) {
                missing.push(rel(parent, ctx.root()));
            }
        }
        if missing.is_empty() {
            Outcome::pass()
        } else {
            Outcome::fail_repro(
                format!(
                    "{} crate(s) without any #[test]:\n  {}",
                    missing.len(),
                    missing.join("\n  ")
                ),
                "rg -l '#\\[test\\]' crates/<name>/",
            )
        }
    }
}

fn crate_has_tests(crate_dir: &Path) -> bool {
    if dir_has_test_files(&crate_dir.join("tests")) {
        return true;
    }
    if dir_has_test_files(&crate_dir.join("benches")) {
        return true;
    }
    let src = crate_dir.join("src");
    if src.is_dir() {
        for entry in walkdir::WalkDir::new(&src).into_iter().flatten() {
            if !entry.file_type().is_file() {
                continue;
            }
            if entry.path().extension().and_then(|s| s.to_str()) != Some("rs") {
                continue;
            }
            let Ok(text) = fs::read_to_string(entry.path()) else {
                continue;
            };
            if text.contains("#[test]") {
                return true;
            }
        }
    }
    false
}

fn dir_has_test_files(dir: &Path) -> bool {
    let Ok(read) = fs::read_dir(dir) else {
        return false;
    };
    read.flatten().any(|e| {
        e.path()
            .extension()
            .and_then(|s| s.to_str())
            .map(|s| s == "rs")
            .unwrap_or(false)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    fn write_crate(root: &std::path::Path, name: &str, body: &str, lib: &str) {
        let dir = root.join("crates").join(name);
        fs::create_dir_all(dir.join("src")).unwrap();
        fs::write(
            dir.join("Cargo.toml"),
            format!("[package]\nname = \"{name}\"\nversion = \"0.1\"\n{body}"),
        )
        .unwrap();
        fs::write(dir.join("src/lib.rs"), lib).unwrap();
    }

    #[test]
    fn each_crate_self_testable_passes_when_all_have_tests() {
        let tmp = TempDir::new().unwrap();
        write_crate(
            tmp.path(),
            "alpha",
            "",
            "pub fn k() -> u32 { 1 }\n#[cfg(test)] mod t { #[test] fn t() {} }\n",
        );
        let ctx = Context::new(tmp.path().to_path_buf());
        assert!(matches!(CrateTested.evaluate(&ctx), Outcome::Pass { .. }));
    }

    #[test]
    fn each_crate_self_testable_fails_when_one_crate_has_no_tests() {
        let tmp = TempDir::new().unwrap();
        write_crate(
            tmp.path(),
            "alpha",
            "",
            "pub fn k() {}\n#[cfg(test)] mod t { #[test] fn t() {} }\n",
        );
        write_crate(tmp.path(), "beta", "", "pub fn no_tests_here() {}\n");
        let ctx = Context::new(tmp.path().to_path_buf());
        let out = CrateTested.evaluate(&ctx);
        assert!(matches!(out, Outcome::Fail { .. }), "{out:?}");
    }

    #[test]
    fn binary_crate_is_exempt() {
        let tmp = TempDir::new().unwrap();
        // Pure-binary crate: [[bin]] + main.rs only, no lib.rs. Must pass.
        let dir = tmp.path().join("crates/shell");
        fs::create_dir_all(dir.join("src")).unwrap();
        fs::write(
            dir.join("Cargo.toml"),
            "[package]\nname = \"shell\"\nversion = \"0.1\"\n\n\
             [[bin]]\nname = \"shell\"\npath = \"src/main.rs\"\n",
        )
        .unwrap();
        fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
        let ctx = Context::new(tmp.path().to_path_buf());
        assert!(matches!(CrateTested.evaluate(&ctx), Outcome::Pass { .. }));
    }

    #[test]
    fn binary_lib_hybrid_must_test_lib() {
        let tmp = TempDir::new().unwrap();
        // Hybrid crate: ships a [[bin]] AND a src/lib.rs with logic.
        // The lib portion is testable, so the rule must NOT exempt it.
        write_crate(
            tmp.path(),
            "hybrid",
            "\n[[bin]]\nname = \"hybrid\"\npath = \"src/main.rs\"\n",
            "pub fn library_logic() -> u32 { 42 }\n",
        );
        let dir = tmp.path().join("crates/hybrid/src");
        fs::write(dir.join("main.rs"), "fn main() {}\n").unwrap();
        let ctx = Context::new(tmp.path().to_path_buf());
        let out = CrateTested.evaluate(&ctx);
        assert!(
            matches!(out, Outcome::Fail { .. }),
            "binary+lib hybrid without #[test] should Fail: {out:?}"
        );
    }

    #[test]
    fn binary_lib_hybrid_with_tests_passes() {
        let tmp = TempDir::new().unwrap();
        write_crate(
            tmp.path(),
            "hybrid",
            "\n[[bin]]\nname = \"hybrid\"\npath = \"src/main.rs\"\n",
            "pub fn k() -> u32 { 1 }\n#[cfg(test)] mod t { #[test] fn t() {} }\n",
        );
        let dir = tmp.path().join("crates/hybrid/src");
        fs::write(dir.join("main.rs"), "fn main() {}\n").unwrap();
        let ctx = Context::new(tmp.path().to_path_buf());
        assert!(matches!(CrateTested.evaluate(&ctx), Outcome::Pass { .. }));
    }
}