Skip to main content

testing_conventions/
lib.rs

1pub mod config;
2pub mod location;
3
4use std::path::{Path, PathBuf};
5
6use clap::{Parser, Subcommand};
7
8#[derive(Parser, Debug)]
9#[command(
10    name = "testing-conventions",
11    version,
12    about = "Enforce testing conventions in libraries (Python, TypeScript, and Rust).",
13    long_about = None,
14)]
15pub struct Cli {
16    #[command(subcommand)]
17    command: Option<Command>,
18}
19
20#[derive(Subcommand, Debug)]
21enum Command {
22    /// Check the repository against its testing-conventions config.
23    Check,
24    /// Unit-test conventions.
25    Unit {
26        #[command(subcommand)]
27        rule: UnitRule,
28    },
29}
30
31/// Rules enforced on the unit-test suite (the README's "Unit" taxonomy).
32#[derive(Subcommand, Debug)]
33enum UnitRule {
34    /// Check that every source file has a colocated unit test.
35    Location {
36        /// Directory to scan recursively.
37        path: PathBuf,
38        /// Language convention to enforce (required).
39        #[arg(long, value_enum)]
40        language: location::Language,
41    },
42}
43
44pub fn run<I, T>(args: I) -> anyhow::Result<i32>
45where
46    I: IntoIterator<Item = T>,
47    T: Into<std::ffi::OsString> + Clone,
48{
49    let cli = Cli::try_parse_from(args)?;
50    match cli.command {
51        // The config-driven `check` umbrella isn't wired yet; the scaffold
52        // proves the wiring while individual rules land under their test-kind
53        // group (e.g. `unit location`).
54        Some(Command::Check) | None => Ok(0),
55        Some(Command::Unit {
56            rule: UnitRule::Location { path, language },
57        }) => run_unit_location(&path, language),
58    }
59}
60
61/// Run the unit-test location check over `root` for `language`, reporting orphans.
62///
63/// Returns `0` when every source file has its colocated unit test; otherwise
64/// prints each orphan to stderr and returns `1`.
65fn run_unit_location(root: &Path, language: location::Language) -> anyhow::Result<i32> {
66    let orphans = location::missing_unit_tests(root, language)?;
67    if orphans.is_empty() {
68        return Ok(0);
69    }
70    for orphan in &orphans {
71        eprintln!("missing colocated unit test: {}", orphan.display());
72    }
73    eprintln!(
74        "error: {} source file(s) missing a colocated unit test",
75        orphans.len()
76    );
77    Ok(1)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn no_args_returns_ok_zero() {
86        assert_eq!(run(["testing-conventions"]).unwrap(), 0);
87    }
88
89    #[test]
90    fn check_returns_ok_zero() {
91        assert_eq!(run(["testing-conventions", "check"]).unwrap(), 0);
92    }
93
94    #[test]
95    fn unknown_flag_errors() {
96        assert!(run(["testing-conventions", "--bogus"]).is_err());
97    }
98
99    #[test]
100    fn help_flag_returns_clap_display_help() {
101        let err = run(["testing-conventions", "--help"]).expect_err("--help should bubble");
102        let clap_err = err
103            .downcast_ref::<clap::Error>()
104            .expect("error should be a clap::Error");
105        assert_eq!(clap_err.kind(), clap::error::ErrorKind::DisplayHelp);
106    }
107
108    #[test]
109    fn version_flag_returns_clap_display_version() {
110        let err = run(["testing-conventions", "--version"]).expect_err("--version should bubble");
111        let clap_err = err
112            .downcast_ref::<clap::Error>()
113            .expect("error should be a clap::Error");
114        assert_eq!(clap_err.kind(), clap::error::ErrorKind::DisplayVersion);
115    }
116}