testing_conventions/entrypoint.rs
1//! The binary target's entry point. It lives here rather than in `src/main.rs` so the binary
2//! root stays declaration-only: Rust requires a `main` in that file, and a `fn main` with a body
3//! is a unit-gate subject whose own `#[cfg(test)]` module the unit suite would have to run. A
4//! re-export carries no function, so the gates read `main.rs` as declarations and the logic sits
5//! here, under test.
6
7use std::process::ExitCode;
8
9/// Run the CLI over the process arguments and map the outcome onto an exit code.
10///
11/// `cfg(not(test))` keeps this out of the unit tier's coverage report. A test cannot execute it:
12/// `cargo test` replaces a binary's `main` with the harness's own, and calling it directly would
13/// run the real CLI over the harness's argv — `cargo test install` would write to the repo. It
14/// carries no decision to cover either; [`report`] holds all of them and the e2e tier runs the
15/// real binary. Only the process-boundary argv read lives here.
16#[cfg(not(test))]
17pub fn main() -> ExitCode {
18 report(crate::run(std::env::args_os()))
19}
20
21/// The exit code a finished [`crate::run`] earns, after printing whatever the caller should see.
22///
23/// A clap failure renders itself — `--help` and `--version` are clap "errors" that belong on
24/// stdout with code 0, a usage mistake on stderr with code 2 — so it prints itself and reports
25/// its own code. `print` plus `exit_code` is what `clap::Error::exit` does either side of
26/// `process::exit`; splitting them keeps this function callable from a test.
27fn report(result: anyhow::Result<i32>) -> ExitCode {
28 match result {
29 Ok(code) => ExitCode::from(code as u8),
30 Err(err) => match err.downcast_ref::<clap::Error>() {
31 Some(clap_err) => {
32 let _ = clap_err.print();
33 ExitCode::from(clap_err.exit_code() as u8)
34 }
35 None => {
36 // `{err:#}` prints the whole anyhow chain, so a wrapped failure keeps its context.
37 eprintln!("error: {err:#}");
38 ExitCode::from(1)
39 }
40 },
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47 use clap::error::ErrorKind;
48
49 #[test]
50 fn a_success_code_becomes_that_exit_code() {
51 assert_eq!(report(Ok(0)), ExitCode::from(0));
52 assert_eq!(report(Ok(3)), ExitCode::from(3));
53 }
54
55 #[test]
56 fn a_plain_failure_exits_one() {
57 assert_eq!(
58 report(Err(anyhow::anyhow!("the gate found violations"))),
59 ExitCode::from(1)
60 );
61 }
62
63 #[test]
64 fn a_usage_mistake_carries_claps_own_exit_code() {
65 let clap_err = clap::Error::new(ErrorKind::InvalidValue);
66 let expected = clap_err.exit_code() as u8;
67
68 assert_eq!(report(Err(clap_err.into())), ExitCode::from(expected));
69 assert_eq!(expected, 2, "clap reports a usage mistake as 2");
70 }
71
72 #[test]
73 fn a_help_request_exits_zero_rather_than_as_a_failure() {
74 let help = clap::Error::new(ErrorKind::DisplayHelp);
75
76 assert_eq!(report(Err(help.into())), ExitCode::from(0));
77 }
78}