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    /// Check that every Python source file has a colocated `_test.py` unit test.
25    UnitLocation {
26        /// Directory to scan recursively for `*.py` sources.
27        path: PathBuf,
28    },
29}
30
31pub fn run<I, T>(args: I) -> anyhow::Result<i32>
32where
33    I: IntoIterator<Item = T>,
34    T: Into<std::ffi::OsString> + Clone,
35{
36    let cli = Cli::try_parse_from(args)?;
37    match cli.command {
38        // The config-driven `check` umbrella isn't wired yet; the scaffold
39        // proves the wiring while individual rules land as their own subcommand.
40        Some(Command::Check) | None => Ok(0),
41        Some(Command::UnitLocation { path }) => run_unit_location(&path),
42    }
43}
44
45/// Run the Python unit-test location check over `root`, reporting orphans.
46///
47/// Returns `0` when every source file has its colocated `_test.py`; otherwise
48/// prints each orphan to stderr and returns `1`.
49fn run_unit_location(root: &Path) -> anyhow::Result<i32> {
50    let orphans = location::missing_unit_tests(root)?;
51    if orphans.is_empty() {
52        return Ok(0);
53    }
54    for orphan in &orphans {
55        eprintln!("missing colocated unit test: {}", orphan.display());
56    }
57    eprintln!(
58        "error: {} source file(s) missing a colocated `_test.py`",
59        orphans.len()
60    );
61    Ok(1)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn no_args_returns_ok_zero() {
70        assert_eq!(run(["testing-conventions"]).unwrap(), 0);
71    }
72
73    #[test]
74    fn check_returns_ok_zero() {
75        assert_eq!(run(["testing-conventions", "check"]).unwrap(), 0);
76    }
77
78    #[test]
79    fn unknown_flag_errors() {
80        assert!(run(["testing-conventions", "--bogus"]).is_err());
81    }
82
83    #[test]
84    fn help_flag_returns_clap_display_help() {
85        let err = run(["testing-conventions", "--help"]).expect_err("--help should bubble");
86        let clap_err = err
87            .downcast_ref::<clap::Error>()
88            .expect("error should be a clap::Error");
89        assert_eq!(clap_err.kind(), clap::error::ErrorKind::DisplayHelp);
90    }
91
92    #[test]
93    fn version_flag_returns_clap_display_version() {
94        let err = run(["testing-conventions", "--version"]).expect_err("--version should bubble");
95        let clap_err = err
96            .downcast_ref::<clap::Error>()
97            .expect("error should be a clap::Error");
98        assert_eq!(clap_err.kind(), clap::error::ErrorKind::DisplayVersion);
99    }
100}