Skip to main content

omni_dev/cli/
coverage.rs

1//! Coverage analysis CLI commands.
2
3pub(crate) mod diff;
4pub(crate) mod lint_markers;
5pub(crate) mod merge;
6
7use anyhow::Result;
8use clap::{Parser, Subcommand};
9
10/// Coverage analysis: diff/patch coverage for PR comments.
11#[derive(Parser)]
12pub struct CoverageCommand {
13    /// The coverage subcommand to execute.
14    #[command(subcommand)]
15    pub command: CoverageSubcommands,
16
17    /// `-C/--repo`, inherited by every coverage subcommand.
18    #[command(flatten)]
19    pub repo: crate::cli::repo_arg::RepoArg,
20}
21
22/// Coverage subcommands.
23#[derive(Subcommand)]
24pub enum CoverageSubcommands {
25    /// Analyses diff/patch coverage from a per-line report and a git diff.
26    Diff(Box<diff::DiffCommand>),
27    /// Checks source coverage markers without generating a coverage report.
28    LintMarkers(lint_markers::LintMarkersCommand),
29    /// Merges the per-shard reports of a sharded run into one lcov file.
30    Merge(merge::MergeCommand),
31}
32
33impl CoverageCommand {
34    /// Executes the coverage command.
35    ///
36    /// `-C/--repo` is resolved here (`None` = current working directory) and
37    /// threaded explicitly to the leaf.
38    pub fn execute(self) -> Result<()> {
39        let repo = self.repo.path();
40        match self.command {
41            CoverageSubcommands::Diff(cmd) => cmd.execute(repo),
42            CoverageSubcommands::LintMarkers(cmd) => cmd.execute(repo),
43            CoverageSubcommands::Merge(cmd) => cmd.execute(repo),
44        }
45    }
46}
47
48#[cfg(test)]
49#[allow(clippy::unwrap_used, clippy::expect_used)]
50mod tests {
51    use super::*;
52
53    /// The `coverage` command dispatches to `diff`; a missing report makes the
54    /// leaf command error, which exercises the dispatch path end-to-end.
55    #[test]
56    fn dispatches_to_diff() {
57        let cmd = CoverageCommand {
58            command: CoverageSubcommands::Diff(Box::new(diff::DiffCommand {
59                report: vec![std::path::PathBuf::from("/nonexistent/report.lcov")],
60                report_format: diff::ReportFormat::Auto,
61                base_ref: Some("HEAD".to_string()),
62                head_ref: None,
63                baseline_report: None,
64                baseline_report_format: diff::ReportFormat::Auto,
65                output: diff::OutputFormatArg::Markdown,
66                format: None,
67                fail_under_patch: None,
68                fail_under_lines: None,
69                strip_prefix: None,
70                ignore_filename_regex: Vec::new(),
71                context_dir: None,
72                collapse_ranges: false,
73                all_files: false,
74                artifact_url: None,
75                run_url: None,
76                base_sha: None,
77                head_sha: None,
78                commit_url: None,
79            })),
80            repo: crate::cli::repo_arg::RepoArg::default(),
81        };
82        // Reaches the leaf command and fails on the missing report file.
83        assert!(cmd.execute().is_err());
84    }
85
86    /// The same for `merge`: a missing shard fails the leaf, and the output is
87    /// never created.
88    #[test]
89    fn dispatches_to_merge() {
90        let dir = tempfile::tempdir().unwrap();
91        let output = dir.path().join("merged.lcov");
92        let cmd = CoverageCommand {
93            command: CoverageSubcommands::Merge(merge::MergeCommand {
94                report: vec![dir.path().join("missing.lcov")],
95                report_format: diff::ReportFormat::Auto,
96                output: output.clone(),
97                strip_prefix: None,
98            }),
99            repo: crate::cli::repo_arg::RepoArg::default(),
100        };
101        assert!(cmd.execute().is_err());
102        assert!(!output.exists());
103    }
104}