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