From 070cc6f54001a69a472510ee8ed805e15cad6787 Mon Sep 17 00:00:00 2001 From: Taiki Endo Date: Tue, 6 Sep 2022 22:29:36 +0900 Subject: [PATCH] Switch from clap to lexopt --- Cargo.toml | 2 +- README.md | 9 +- docs/cargo-llvm-cov-clean.txt | 16 + docs/cargo-llvm-cov-nextest.txt | 9 + .../cargo-llvm-cov-run.txt | 135 +- docs/cargo-llvm-cov-show-env.txt | 10 + .../long-help.txt => docs/cargo-llvm-cov.txt | 2 - src/cargo.rs | 151 +-- src/clean.rs | 20 +- src/cli.rs | 1110 ++++++++++------- src/config.rs | 5 +- src/context.rs | 6 +- src/main.rs | 81 +- src/term.rs | 17 +- tests/auxiliary/mod.rs | 26 +- tests/test.rs | 26 +- 16 files changed, 884 insertions(+), 741 deletions(-) create mode 100644 docs/cargo-llvm-cov-clean.txt create mode 100644 docs/cargo-llvm-cov-nextest.txt rename tests/short-help.txt => docs/cargo-llvm-cov-run.txt (56%) create mode 100644 docs/cargo-llvm-cov-show-env.txt rename tests/long-help.txt => docs/cargo-llvm-cov.txt (99%) diff --git a/Cargo.toml b/Cargo.toml index 59194033..08d38758 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,12 +28,12 @@ anyhow = "1.0.34" atty = "0.2.11" camino = "1.0.3" cargo_metadata = "0.14" -clap = { version = "3.2.3", features = ["derive"] } duct = "0.13.1" fs-err = "2.5" glob = "0.3" home = "0.5" is_executable = "1" +lexopt = "0.2" opener = "0.5" regex = { version = "1.3", default-features = false, features = ["perf", "std"] } rustc-demangle = "0.1.21" diff --git a/README.md b/README.md index 96cf8ef6..4f816733 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ This is a wrapper around rustc [`-C instrument-coverage`][instrument-coverage] a
Click to show a complete list of options +(See [docs](docs) directory for options of subcommands) + ```console $ cargo llvm-cov --help @@ -301,8 +303,6 @@ SUBCOMMANDS: Remove artifacts that cargo-llvm-cov has generated in the past nextest Run tests with cargo nextest - help - Print this message or the help of the given subcommand(s) ``` @@ -556,8 +556,8 @@ Note: AUR package is maintained by community, not maintainer of cargo-llvm-cov. - Branch coverage is not supported yet. See [#8] and [rust-lang/rust#79649] for more. - Support for doc tests is unstable and has known issues. See [#2] and [rust-lang/rust#79417] for more. -- All the tests are run with `RUST_TEST_THREADS=1` to work around [rust-lang/rust#91092]. You can pass `--test-threads` (e.g., `--test-threads=$(nproc)`) to override this behavior. - +- All the tests are run with `RUST_TEST_THREADS=1` to work around [rust-lang/rust#91092]. You can pass `--test-threads` (e.g., `--test-threads=$(nproc)`) to override this behavior. + See also [the code-coverage-related issues reported in rust-lang/rust](https://github.com/rust-lang/rust/labels/A-code-coverage). ## Related Projects @@ -566,7 +566,6 @@ See also [the code-coverage-related issues reported in rust-lang/rust](https://g - [cargo-hack]: Cargo subcommand to provide various options useful for testing and continuous integration. - [cargo-minimal-versions]: Cargo subcommand for proper use of `-Z minimal-versions`. -[#1]: https://github.com/taiki-e/cargo-llvm-cov/issues/1 [#2]: https://github.com/taiki-e/cargo-llvm-cov/issues/2 [#8]: https://github.com/taiki-e/cargo-llvm-cov/issues/8 [#12]: https://github.com/taiki-e/cargo-llvm-cov/issues/12 diff --git a/docs/cargo-llvm-cov-clean.txt b/docs/cargo-llvm-cov-clean.txt new file mode 100644 index 00000000..c3cdd5d4 --- /dev/null +++ b/docs/cargo-llvm-cov-clean.txt @@ -0,0 +1,16 @@ +cargo-llvm-cov-clean +Remove artifacts that cargo-llvm-cov has generated in the past + +USAGE: + cargo llvm-cov clean [OPTIONS] + +OPTIONS: + --workspace Remove artifacts that may affect the coverage results of packages + in the workspace + -v, --verbose Use verbose output + --color Coloring [possible values: auto, always, never] + --manifest-path Path to Cargo.toml + --frozen Require Cargo.lock and cache are up to date + --locked Require Cargo.lock is up to date + --offline Run without accessing the network + -h, --help Print help information diff --git a/docs/cargo-llvm-cov-nextest.txt b/docs/cargo-llvm-cov-nextest.txt new file mode 100644 index 00000000..08f86a03 --- /dev/null +++ b/docs/cargo-llvm-cov-nextest.txt @@ -0,0 +1,9 @@ +cargo-llvm-cov-nextest +Run tests with cargo nextest + +USAGE: + cargo llvm-cov nextest [OPTIONS] [NEXTEST_OPTIONS] + +ARGS: + [OPTIONS] Options for cargo-llvm-cov; For more information try `cargo llvm-cov --help` + [NEXTEST_OPTIONS] Options for cargo-nextest; For more information try `cargo nextest run --help` diff --git a/tests/short-help.txt b/docs/cargo-llvm-cov-run.txt similarity index 56% rename from tests/short-help.txt rename to docs/cargo-llvm-cov-run.txt index 1266e659..c1bbf582 100644 --- a/tests/short-help.txt +++ b/docs/cargo-llvm-cov-run.txt @@ -1,39 +1,69 @@ -cargo-llvm-cov -Cargo subcommand to easily use LLVM source-based code coverage (-C instrument-coverage). - -Use -h for short descriptions and --help for more details. +cargo-llvm-cov-run +Run a binary or example and generate coverage report USAGE: - cargo llvm-cov [OPTIONS] [-- ...] [SUBCOMMAND] + cargo llvm-cov run [OPTIONS] [-- ...] ARGS: - ... Arguments for the test binary + ... + Arguments for the test binary OPTIONS: --json Export coverage data in "json" format + If --output-path is not specified, the report will be printed to stdout. + + This internally calls `llvm-cov export -format=text`. See + for more. + --lcov Export coverage data in "lcov" format + If --output-path is not specified, the report will be printed to stdout. + + This internally calls `llvm-cov export -format=lcov`. See + for more. + --text Generate coverage report in “text” format + If --output-path or --output-dir is not specified, the report will be printed to stdout. + + This internally calls `llvm-cov show -format=text`. See + for more. + --html Generate coverage report in "html" format + If --output-dir is not specified, the report will be generated in `target/llvm-cov/html` + directory. + + This internally calls `llvm-cov show -format=html`. See + for more. + --open Generate coverage reports in "html" format and open them in a browser after the - operation + operation. + + See --html for more. --summary-only Export only summary information for each file in the coverage data + This flag can only be used together with either --json or --lcov. + --output-path - Specify a file to write coverage data into + Specify a file to write coverage data into. + + This flag can only be used together with --json, --lcov, or --text. See --output-dir for + --html and --open. --output-dir - Specify a directory to write coverage report into (default to `target/llvm-cov`) + Specify a directory to write coverage report into (default to `target/llvm-cov`). + + This flag can only be used together with --text, --html, or --open. See also + --output-path. --failure-mode Fail if `any` or `all` profiles cannot be merged (default to `any`) @@ -72,68 +102,17 @@ OPTIONS: --include-build-script Include build script in coverage report - --doctests - Including doc tests (unstable) - - --no-run - Generate coverage report without running tests - - --no-fail-fast - Run all tests regardless of failure - - --ignore-run-fail - Run all tests regardless of failure and generate report - -q, --quiet - Display one character per test instead of one line - - --lib - Test only this package's library unit tests + No output printed to stdout --bin - Test only the specified binary - - --bins - Test all binaries + Name of the bin target to run --example - Test only the specified example - - --examples - Test all examples - - --test - Test only the specified test target - - --tests - Test all tests - - --bench - Test only the specified bench target - - --benches - Test all benches - - --all-targets - Test all targets - - --doc - Test only this library's documentation (unstable) + Name of the example target to run -p, --package - Package to run tests for - - --workspace - Test all packages in the workspace [aliases: all] - - --exclude - Exclude packages from both the test and report - - --exclude-from-test - Exclude packages from the test (but not from the report) - - --exclude-from-report - Exclude packages from the report (but not from the test) + Package with the target to run -j, --jobs Number of parallel jobs, defaults to # of CPUs @@ -156,21 +135,37 @@ OPTIONS: --target Build for the target triple + When this option is used, coverage for proc-macro and build script will not be displayed + because cargo does not pass RUSTFLAGS to them. + --coverage-target-only Activate coverage reporting only for the target triple + Activate coverage reporting only for the target triple specified via `--target`. This is + important, if the project uses multiple targets via the cargo bindeps feature, and not + all targets can use `instrument-coverage`, e.g. a microkernel, or an embedded binary. + -v, --verbose Use verbose output + Use -vv (-vvv) to propagate verbosity to cargo. + --color - Coloring [possible values: auto, always, never] + Coloring + + [possible values: auto, always, never] --remap-path-prefix Use --remap-path-prefix for workspace root + Note that this does not fully compatible with doctest. + --include-ffi Include coverage of C/C++ code linked to Rust library/binary + Note that `CC`/`CXX`/`LLVM_COV`/`LLVM_PROFDATA` environment variables must be set to + Clang/LLVM compatible with the LLVM version used in rustc. + --manifest-path Path to Cargo.toml @@ -188,13 +183,3 @@ OPTIONS: -h, --help Print help information - - -V, --version - Print version information - -SUBCOMMANDS: - run Run a binary or example and generate coverage report - show-env Output the environment set by cargo-llvm-cov to build Rust projects - clean Remove artifacts that cargo-llvm-cov has generated in the past - nextest Run tests with cargo nextest - help Print this message or the help of the given subcommand(s) diff --git a/docs/cargo-llvm-cov-show-env.txt b/docs/cargo-llvm-cov-show-env.txt new file mode 100644 index 00000000..a5c05d24 --- /dev/null +++ b/docs/cargo-llvm-cov-show-env.txt @@ -0,0 +1,10 @@ +cargo-llvm-cov-show-env +Output the environment set by cargo-llvm-cov to build Rust projects + +USAGE: + cargo llvm-cov show-env [OPTIONS] + +OPTIONS: + --export-prefix Prepend "export " to each line, so that the output is suitable to be + sourced by bash + -h, --help Print help information diff --git a/tests/long-help.txt b/docs/cargo-llvm-cov.txt similarity index 99% rename from tests/long-help.txt rename to docs/cargo-llvm-cov.txt index c236d6c5..757d8bc5 100644 --- a/tests/long-help.txt +++ b/docs/cargo-llvm-cov.txt @@ -259,5 +259,3 @@ SUBCOMMANDS: Remove artifacts that cargo-llvm-cov has generated in the past nextest Run tests with cargo nextest - help - Print this message or the help of the given subcommand(s) diff --git a/src/cargo.rs b/src/cargo.rs index b92b0400..48c07ec1 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -7,7 +7,7 @@ use anyhow::{bail, format_err, Context as _, Result}; use camino::{Utf8Path, Utf8PathBuf}; use crate::{ - cli::{Args, ManifestOptions, RunOptions}, + cli::{Args, ManifestOptions, Subcommand}, config::Config, context::Context, env, @@ -45,7 +45,7 @@ impl Workspace { // Metadata and config let current_manifest = package_root(&cargo, options.manifest_path.as_deref())?; - let metadata = metadata(&cargo, ¤t_manifest, options)?; + let metadata = metadata(&cargo, ¤t_manifest)?; let config = Config::new(&cargo, target, Some(&host_triple))?; // TODO: Update comment based on https://github.com/rust-lang/cargo/pull/10896? @@ -189,90 +189,32 @@ fn locate_project(cargo: &OsStr) -> Result { } // https://doc.rust-lang.org/nightly/cargo/commands/cargo-metadata.html -fn metadata( - cargo: &OsStr, - manifest_path: &Utf8Path, - options: &ManifestOptions, -) -> Result { +fn metadata(cargo: &OsStr, manifest_path: &Utf8Path) -> Result { let mut cmd = cmd!(cargo, "metadata", "--format-version=1", "--manifest-path", manifest_path); - options.cargo_args(&mut cmd); serde_json::from_str(&cmd.read()?) .with_context(|| format!("failed to parse output from {}", cmd)) } // https://doc.rust-lang.org/nightly/cargo/commands/cargo-test.html -pub(crate) fn test_args(cx: &Context, args: &Args, cmd: &mut ProcessBuilder) { - let mut has_target_selection_options = false; - if args.lib { - has_target_selection_options = true; - cmd.arg("--lib"); - } - for name in &args.bin { - has_target_selection_options = true; - cmd.arg("--bin"); - cmd.arg(name); - } - if args.bins { - has_target_selection_options = true; - cmd.arg("--bins"); - } - for name in &args.example { - has_target_selection_options = true; - cmd.arg("--example"); - cmd.arg(name); - } - if args.examples { - has_target_selection_options = true; - cmd.arg("--examples"); - } - for name in &args.test { - has_target_selection_options = true; - cmd.arg("--test"); - cmd.arg(name); - } - if args.tests { - has_target_selection_options = true; - cmd.arg("--tests"); - } - for name in &args.bench { - has_target_selection_options = true; - cmd.arg("--bench"); - cmd.arg(name); - } - if args.benches { - has_target_selection_options = true; - cmd.arg("--benches"); - } - if args.all_targets { - has_target_selection_options = true; - cmd.arg("--all-targets"); - } - if args.doc { - has_target_selection_options = true; - cmd.arg("--doc"); - } - - if !has_target_selection_options && !cx.doctests { - cmd.arg("--tests"); +// https://doc.rust-lang.org/nightly/cargo/commands/cargo-run.html +pub(crate) fn test_or_run_args(cx: &Context, args: &Args, cmd: &mut ProcessBuilder) { + if args.subcommand == Subcommand::Test && !cx.doctests { + let has_target_selection_options = args.lib + | args.bins + | args.examples + | args.tests + | args.benches + | args.all_targets + | args.doc + | !args.bin.is_empty() + | !args.example.is_empty() + | !args.test.is_empty() + | !args.bench.is_empty(); + if !has_target_selection_options { + cmd.arg("--tests"); + } } - if args.quiet { - cmd.arg("--quiet"); - } - if args.no_fail_fast { - cmd.arg("--no-fail-fast"); - } - for package in &args.package { - cmd.arg("--package"); - cmd.arg(package); - } - if args.workspace { - cmd.arg("--workspace"); - } - for exclude in &args.exclude { - cmd.arg("--exclude"); - cmd.arg(exclude); - } for exclude in &args.exclude_from_test { cmd.arg("--exclude"); cmd.arg(exclude); @@ -284,56 +226,13 @@ pub(crate) fn test_args(cx: &Context, args: &Args, cmd: &mut ProcessBuilder) { cmd.arg("--target-dir"); cmd.arg(&cx.ws.target_dir); - cx.build.cargo_args(cmd); - cx.manifest.cargo_args(cmd); - - for unstable_flag in &args.unstable_flags { - cmd.arg("-Z"); - cmd.arg(unstable_flag); + for cargo_arg in &args.cargo_args { + cmd.arg(cargo_arg); } - if !args.args.is_empty() { + if !args.rest.is_empty() { cmd.arg("--"); - cmd.args(&args.args); - } -} - -// https://doc.rust-lang.org/nightly/cargo/commands/cargo-run.html -pub(crate) fn run_args(cx: &Context, args: &RunOptions, cmd: &mut ProcessBuilder) { - for name in &args.bin { - cmd.arg("--bin"); - cmd.arg(name); - } - for name in &args.example { - cmd.arg("--example"); - cmd.arg(name); - } - - if args.quiet { - cmd.arg("--quiet"); - } - if let Some(package) = &args.package { - cmd.arg("--package"); - cmd.arg(package); - } - - cmd.arg("--manifest-path"); - cmd.arg(&cx.ws.current_manifest); - - cmd.arg("--target-dir"); - cmd.arg(&cx.ws.target_dir); - - cx.build.cargo_args(cmd); - cx.manifest.cargo_args(cmd); - - for unstable_flag in &args.unstable_flags { - cmd.arg("-Z"); - cmd.arg(unstable_flag); - } - - if !args.args.is_empty() { - cmd.arg("--"); - cmd.args(&args.args); + cmd.args(&args.rest); } } @@ -361,8 +260,6 @@ pub(crate) fn clean_args(cx: &Context, cmd: &mut ProcessBuilder) { cmd.arg("--target-dir"); cmd.arg(&cx.ws.target_dir); - cx.manifest.cargo_args(cmd); - // If `-vv` is passed, propagate `-v` to cargo. if cx.build.verbose > 1 { cmd.arg(format!("-{}", "v".repeat(cx.build.verbose as usize - 1))); diff --git a/src/clean.rs b/src/clean.rs index e95c6f60..857a7abf 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -11,24 +11,24 @@ use walkdir::WalkDir; use crate::{ cargo::{self, Workspace}, - cli::{CleanOptions, ManifestOptions}, + cli::Args, context::Context, fs, term, }; -pub(crate) fn run(mut options: CleanOptions) -> Result<()> { +pub(crate) fn run(options: &mut Args) -> Result<()> { let ws = Workspace::new(&options.manifest, None, false, false)?; - ws.config.merge_to_args(&mut None, &mut options.verbose, &mut options.color); - term::set_coloring(&mut options.color); + ws.config.merge_to_args(&mut None, &mut options.build.verbose, &mut options.build.color); + term::set_coloring(&mut options.build.color); if !options.workspace { for dir in &[&ws.target_dir, &ws.output_dir] { - rm_rf(dir, options.verbose != 0)?; + rm_rf(dir, options.build.verbose != 0)?; } return Ok(()); } - clean_ws(&ws, &ws.metadata.workspace_members, &options.manifest, options.verbose)?; + clean_ws(&ws, &ws.metadata.workspace_members, options.build.verbose)?; Ok(()) } @@ -63,12 +63,7 @@ pub(crate) fn clean_partial(cx: &Context) -> Result<()> { Ok(()) } -fn clean_ws( - ws: &Workspace, - pkg_ids: &[PackageId], - manifest: &ManifestOptions, - verbose: u8, -) -> Result<()> { +fn clean_ws(ws: &Workspace, pkg_ids: &[PackageId], verbose: u8) -> Result<()> { clean_ws_inner(ws, pkg_ids, verbose != 0)?; let package_args: Vec<_> = @@ -90,7 +85,6 @@ fn clean_ws( if verbose > 0 { cmd.arg(format!("-{}", "v".repeat(verbose as usize))); } - manifest.cargo_args(&mut cmd); cmd.dir(&ws.metadata.workspace_root); if let Err(e) = if verbose > 0 { cmd.run() } else { cmd.run_with_output() } { warn!("{:#}", e); diff --git a/src/cli.rs b/src/cli.rs index 7e2f3310..bab1c230 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,173 +1,608 @@ -use std::mem; +use std::{ffi::OsString, mem, str::FromStr}; -use camino::Utf8PathBuf; -use clap::{AppSettings, Parser}; +use anyhow::{bail, format_err, Error, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use lexopt::{ + Arg::{Long, Short, Value}, + ValueExt, +}; -use crate::{process::ProcessBuilder, term::Coloring}; +use crate::{ + env, + term::{self, Coloring}, +}; -const ABOUT: &str = - "Cargo subcommand to easily use LLVM source-based code coverage (-C instrument-coverage). +// TODO: add --config option and passthrough to cargo-config: https://github.com/rust-lang/cargo/pull/10755/ -Use -h for short descriptions and --help for more details."; - -const MAX_TERM_WIDTH: usize = 100; - -#[derive(Debug, Parser)] -#[clap( - bin_name = "cargo", - version, - max_term_width(MAX_TERM_WIDTH), - setting(AppSettings::DeriveDisplayOrder) -)] -pub(crate) enum Opts { - #[clap(about(ABOUT), version)] - LlvmCov(Args), -} - -#[derive(Debug, Parser)] -#[clap( - bin_name = "cargo llvm-cov", - about(ABOUT), - version, - max_term_width(MAX_TERM_WIDTH), - setting(AppSettings::DeriveDisplayOrder) -)] +#[derive(Debug)] pub(crate) struct Args { - #[clap(subcommand)] - pub(crate) subcommand: Option, + pub(crate) subcommand: Subcommand, - #[clap(flatten)] cov: LlvmCovOptions, + pub(crate) show_env: ShowEnvOptions, // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/instrument-coverage.html#including-doc-tests /// Including doc tests (unstable) /// /// This flag is unstable. /// See for more. - #[clap(long)] pub(crate) doctests: bool, // ========================================================================= // `cargo test` options // https://doc.rust-lang.org/nightly/cargo/commands/cargo-test.html /// Generate coverage report without running tests - #[clap(long, conflicts_with = "no-report")] pub(crate) no_run: bool, - /// Run all tests regardless of failure - #[clap(long)] - pub(crate) no_fail_fast: bool, + // /// Run all tests regardless of failure + // pub(crate) no_fail_fast: bool, /// Run all tests regardless of failure and generate report /// /// If tests failed but report generation succeeded, exit with a status of 0. - #[clap( - long, - // --ignore-run-fail implicitly enable --no-fail-fast. - conflicts_with = "no-fail-fast", - )] pub(crate) ignore_run_fail: bool, - /// Display one character per test instead of one line - #[clap(short, long, conflicts_with = "verbose")] - pub(crate) quiet: bool, + // /// Display one character per test instead of one line + // pub(crate) quiet: bool, /// Test only this package's library unit tests - #[clap(long, conflicts_with = "doc", conflicts_with = "doctests")] pub(crate) lib: bool, /// Test only the specified binary - #[clap( - long, - multiple_occurrences = true, - value_name = "NAME", - conflicts_with = "doc", - conflicts_with = "doctests" - )] pub(crate) bin: Vec, /// Test all binaries - #[clap(long, conflicts_with = "doc", conflicts_with = "doctests")] pub(crate) bins: bool, /// Test only the specified example - #[clap( - long, - multiple_occurrences = true, - value_name = "NAME", - conflicts_with = "doc", - conflicts_with = "doctests" - )] pub(crate) example: Vec, /// Test all examples - #[clap(long, conflicts_with = "doc", conflicts_with = "doctests")] pub(crate) examples: bool, /// Test only the specified test target - #[clap( - long, - multiple_occurrences = true, - value_name = "NAME", - conflicts_with = "doc", - conflicts_with = "doctests" - )] pub(crate) test: Vec, /// Test all tests - #[clap(long, conflicts_with = "doc", conflicts_with = "doctests")] pub(crate) tests: bool, /// Test only the specified bench target - #[clap( - long, - multiple_occurrences = true, - value_name = "NAME", - conflicts_with = "doc", - conflicts_with = "doctests" - )] pub(crate) bench: Vec, /// Test all benches - #[clap(long, conflicts_with = "doc", conflicts_with = "doctests")] pub(crate) benches: bool, /// Test all targets - #[clap(long, conflicts_with = "doc", conflicts_with = "doctests")] pub(crate) all_targets: bool, /// Test only this library's documentation (unstable) /// /// This flag is unstable because it automatically enables --doctests flag. /// See for more. - #[clap(long)] pub(crate) doc: bool, - /// Package to run tests for - // cargo allows the combination of --package and --workspace, but we reject - // it because the situation where both flags are specified is odd. - #[clap( - short, - long, - multiple_occurrences = true, - value_name = "SPEC", - conflicts_with = "workspace" - )] - pub(crate) package: Vec, + // /// Package to run tests for + // pub(crate) package: Vec, /// Test all packages in the workspace - #[clap(long, visible_alias = "all")] pub(crate) workspace: bool, /// Exclude packages from both the test and report - #[clap(long, multiple_occurrences = true, value_name = "SPEC", requires = "workspace")] pub(crate) exclude: Vec, /// Exclude packages from the test (but not from the report) - #[clap(long, multiple_occurrences = true, value_name = "SPEC", requires = "workspace")] pub(crate) exclude_from_test: Vec, /// Exclude packages from the report (but not from the test) - #[clap(long, multiple_occurrences = true, value_name = "SPEC")] pub(crate) exclude_from_report: Vec, - #[clap(flatten)] - build: BuildOptions, - - #[clap(flatten)] - manifest: ManifestOptions, + pub(crate) build: BuildOptions, - /// Unstable (nightly-only) flags to Cargo - #[clap(short = 'Z', multiple_occurrences = true, value_name = "FLAG")] - pub(crate) unstable_flags: Vec, + pub(crate) manifest: ManifestOptions, + pub(crate) cargo_args: Vec, /// Arguments for the test binary - #[clap(last = true)] - pub(crate) args: Vec, + pub(crate) rest: Vec, } impl Args { + pub(crate) fn parse() -> Result { + const SUBCMD: &str = "llvm-cov"; + + // rustc/cargo args must be valid Unicode + // https://github.com/rust-lang/rust/blob/1.62.0/compiler/rustc_driver/src/lib.rs#L1325-L1335 + fn handle_args( + args: impl IntoIterator>, + ) -> impl Iterator> { + args.into_iter().enumerate().map(|(i, arg)| { + arg.into().into_string().map_err(|arg| { + format_err!("argument {} is not valid Unicode: {:?}", i + 1, arg) + }) + }) + } + + let mut raw_args = handle_args(env::args_os()); + raw_args.next(); // cargo + match raw_args.next().transpose()? { + Some(a) if a == SUBCMD => {} + Some(a) => bail!("expected subcommand '{}', found argument '{}'", SUBCMD, a), + None => bail!("expected subcommand '{}'", SUBCMD), + } + let mut args = vec![]; + for arg in &mut raw_args { + let arg = arg?; + if arg == "--" { + break; + } + args.push(arg); + } + let rest = raw_args.collect::>>()?; + + let mut cargo_args = vec![]; + let mut subcommand: Option = None; + + let mut manifest_path = None; + let mut color = None; + + let mut doctests = false; + let mut no_run = false; + let mut no_fail_fast = false; + let mut ignore_run_fail = false; + let mut lib = false; + let mut bin = vec![]; + let mut bins = false; + let mut example = vec![]; + let mut examples = false; + let mut test = vec![]; + let mut tests = false; + let mut bench = vec![]; + let mut benches = false; + let mut all_targets = false; + let mut doc = false; + + let mut package: Vec = vec![]; + let mut workspace = false; + let mut exclude = vec![]; + let mut exclude_from_test = vec![]; + let mut exclude_from_report = vec![]; + + // llvm-cov options + let mut json = false; + let mut lcov = false; + let mut text = false; + let mut html = false; + let mut open = false; + let mut summary_only = false; + let mut output_path = None; + let mut output_dir = None; + let mut failure_mode = None; + let mut ignore_filename_regex = None; + let mut disable_default_ignore_filename_regex = false; + let mut hide_instantiations = false; + let mut no_cfg_coverage = false; + let mut no_cfg_coverage_nightly = false; + let mut no_report = false; + let mut fail_under_lines = None; + let mut fail_uncovered_lines = None; + let mut fail_uncovered_regions = None; + let mut fail_uncovered_functions = None; + let mut show_missing_lines = false; + let mut include_build_script = false; + + // build options + let mut jobs = None; + let mut release = false; + let mut profile = None; + let mut target = None; + let mut coverage_target_only = false; + let mut remap_path_prefix = false; + let mut include_ffi = false; + let mut verbose: usize = 0; + + // show-env options + let mut export_prefix = false; + + let mut parser = lexopt::Parser::from_args(args); + while let Some(arg) = parser.next()? { + macro_rules! parse_opt { + ($opt:ident[] $(,)?) => {{ + $opt.push(parser.value()?.parse()?); + }}; + ($opt:ident $(,)?) => {{ + if $opt.is_some() { + multi_arg(&arg)?; + } + $opt = Some(parser.value()?.parse()?); + }}; + } + macro_rules! parse_opt_passthrough { + ($opt:ident[] $(,)?) => {{ + match arg { + Long(flag) => { + let flag = format!("--{}", flag); + if let Some(val) = parser.optional_value() { + $opt.push(val.parse()?); + cargo_args.push(format!("{}={}", flag, val.into_string().unwrap())); + } else { + let val = parser.value()?.into_string().unwrap(); + $opt.push(val.parse()?); + cargo_args.push(flag); + cargo_args.push(val); + } + } + Short(flag) => { + if let Some(val) = parser.optional_value() { + $opt.push(val.parse()?); + cargo_args.push(format!("-{}{}", flag, val.into_string().unwrap())); + } else { + let val = parser.value()?.into_string().unwrap(); + $opt.push(val.parse()?); + cargo_args.push(format!("-{}", flag)); + cargo_args.push(val); + } + } + Value(_) => unreachable!(), + } + }}; + ($opt:ident $(,)?) => {{ + if $opt.is_some() { + multi_arg(&arg)?; + } + match arg { + Long(flag) => { + let flag = format!("--{}", flag); + if let Some(val) = parser.optional_value() { + $opt = Some(val.parse()?); + cargo_args.push(format!("{}={}", flag, val.into_string().unwrap())); + } else { + let val = parser.value()?.into_string().unwrap(); + $opt = Some(val.parse()?); + cargo_args.push(flag); + cargo_args.push(val); + } + } + Short(flag) => { + if let Some(val) = parser.optional_value() { + $opt = Some(val.parse()?); + cargo_args.push(format!("-{}{}", flag, val.into_string().unwrap())); + } else { + let val = parser.value()?.into_string().unwrap(); + $opt = Some(val.parse()?); + cargo_args.push(format!("-{}", flag)); + cargo_args.push(val); + } + } + Value(_) => unreachable!(), + } + }}; + } + macro_rules! parse_flag { + ($flag:ident $(,)?) => { + if mem::replace(&mut $flag, true) { + multi_arg(&arg)?; + } + }; + } + macro_rules! parse_flag_passthrough { + ($flag:ident $(,)?) => {{ + parse_flag!($flag); + passthrough!(); + }}; + } + macro_rules! passthrough { + () => { + match arg { + Long(flag) => { + let flag = format!("--{}", flag); + if let Some(val) = parser.optional_value() { + cargo_args.push(format!("{}={}", flag, val.parse::()?)); + } else { + cargo_args.push(flag); + } + } + Short(flag) => { + if let Some(val) = parser.optional_value() { + cargo_args.push(format!("-{}{}", flag, val.parse::()?)); + } else { + cargo_args.push(format!("-{}", flag)); + } + } + Value(_) => unreachable!(), + } + }; + } + + match arg { + Long("color") => parse_opt_passthrough!(color), + Long("manifest-path") => parse_opt!(manifest_path), + + Long("doctests") => parse_flag!(doctests), + Long("ignore-run-fail") => parse_flag!(ignore_run_fail), + Long("no-run") => parse_flag!(no_run), + Long("no-fail-fast") => parse_flag_passthrough!(no_fail_fast), + + Long("lib") => parse_flag_passthrough!(lib), + Long("bin") => parse_opt_passthrough!(bin[]), + Long("bins") => parse_flag_passthrough!(bins), + Long("example") => parse_opt_passthrough!(example[]), + Long("examples") => parse_flag_passthrough!(examples), + Long("test") => parse_opt_passthrough!(test[]), + Long("tests") => parse_flag_passthrough!(tests), + Long("bench") => parse_opt_passthrough!(bench[]), + Long("benches") => parse_flag_passthrough!(benches), + Long("all-targets") => parse_flag_passthrough!(all_targets), + Long("doc") => parse_flag_passthrough!(doc), + + Short('p') | Long("package") => parse_opt_passthrough!(package[]), + Long("workspace" | "all") => parse_flag_passthrough!(workspace), + Long("exclude") => parse_opt_passthrough!(exclude[]), + Long("exclude-from-test") => parse_opt!(exclude_from_test[]), + Long("exclude-from-report") => parse_opt!(exclude_from_report[]), + + // build options + Short('j') | Long("jobs") => parse_opt_passthrough!(jobs), + Short('r') | Long("release") => parse_flag_passthrough!(release), + Long("profile") => parse_opt_passthrough!(profile), + Long("target") => parse_opt_passthrough!(target), + Long("coverage-target-only") => parse_flag!(coverage_target_only), + Long("remap-path-prefix") => parse_flag!(remap_path_prefix), + Long("include-ffi") => parse_flag!(include_ffi), + Short('v') | Long("verbose") => verbose += 1, + + // llvm-cov options + Long("json") => parse_flag!(json), + Long("lcov") => parse_flag!(lcov), + Long("text") => parse_flag!(text), + Long("html") => parse_flag!(html), + Long("open") => parse_flag!(open), + Long("summary-only") => parse_flag!(summary_only), + Long("output-path") => parse_opt!(output_path), + Long("output-dir") => parse_opt!(output_dir), + Long("failure-mode") => parse_opt!(failure_mode), + Long("ignore-filename-regex") => parse_opt!(ignore_filename_regex), + Long("disable-default-ignore-filename-regex") => { + parse_flag!(disable_default_ignore_filename_regex); + } + Long("hide-instantiations") => parse_flag!(hide_instantiations), + Long("no-cfg-coverage") => parse_flag!(no_cfg_coverage), + Long("no-cfg-coverage-nightly") => parse_flag!(no_cfg_coverage_nightly), + Long("no-report") => parse_flag!(no_report), + Long("fail-under-lines") => parse_opt!(fail_under_lines), + Long("fail-uncovered-lines") => parse_opt!(fail_uncovered_lines), + Long("fail-uncovered-regions") => parse_opt!(fail_uncovered_regions), + Long("fail-uncovered-functions") => parse_opt!(fail_uncovered_functions), + Long("show-missing-lines") => parse_flag!(show_missing_lines), + Long("include-build-script") => parse_flag!(include_build_script), + + // show-env options + Long("export-prefix") => parse_flag!(export_prefix), + + Short('h') | Long("help") => { + print!("{}", Subcommand::help_text(subcommand)); + std::process::exit(0); + } + Short('V') | Long("version") => { + if subcommand.is_none() { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } else { + unexpected("--version")?; + } + } + + // passthrough + Long(_) => passthrough!(), + Short(flag) => { + if matches!(flag, 'q' | 'r') { + // To handle combined short flags properly, handle known + // short flags without value as special cases. + cargo_args.push(format!("-{}", flag)); + } else { + passthrough!(); + } + } + Value(val) => { + let val = val.parse::()?; + if subcommand.is_none() { + if let Ok(v) = val.parse() { + subcommand = Some(v); + if subcommand == Some(Subcommand::Demangle) { + if let Some(arg) = parser.next()? { + return Err(arg.unexpected().into()); + } + } + } else { + cargo_args.push(val); + } + } else { + cargo_args.push(val); + } + } + } + } + + term::set_coloring(&mut color); + + let subcommand = subcommand.unwrap_or(Subcommand::Test); + + // unexpected options + if export_prefix && subcommand != Subcommand::ShowEnv { + unexpected("--export-prefix")?; + } + // TODO: check + // match subcommand { + // Subcommand::Test => {} + // Subcommand::Run => {} + // Subcommand::ShowEnv => {} + // Subcommand::Clean => {} + // Subcommand::Nextest => {} + // Subcommand::Demangle => {} + // } + + // requires + if !exclude.is_empty() && !workspace { + // TODO: This is the same behavior as cargo, but should we allow it to be used + // in the root of a virtual workspace as well? + requires("--exclude", &["--workspace"])?; + } + if coverage_target_only && target.is_none() { + requires("--coverage-target-only", &["--target"])?; + } + + // conflicts + if no_run && no_report { + conflicts("--no-run", "--no-report")?; + } + if ignore_run_fail && no_fail_fast { + // --ignore-run-fail implicitly enable --no-fail-fast. + conflicts("--ignore-run-fail", "--no-fail-fast")?; + } + if doc || doctests { + let flag = if doc { "--doc" } else { "--doctests" }; + if lib { + conflicts(flag, "--lib")?; + } + if !bin.is_empty() { + conflicts(flag, "--bin")?; + } + if bins { + conflicts(flag, "--bins")?; + } + if !example.is_empty() { + conflicts(flag, "--example")?; + } + if examples { + conflicts(flag, "--examples")?; + } + if !test.is_empty() { + conflicts(flag, "--test")?; + } + if tests { + conflicts(flag, "--tests")?; + } + if !bench.is_empty() { + conflicts(flag, "--bench")?; + } + if benches { + conflicts(flag, "--benches")?; + } + if all_targets { + conflicts(flag, "--all-targets")?; + } + } + if !package.is_empty() && workspace { + // cargo allows the combination of --package and --workspace, but + // we reject it because the situation where both flags are specified is odd. + conflicts("--package", "--workspace")?; + } + if lcov { + let flag = "--lcov"; + if json { + conflicts(flag, "--json")?; + } + } + if text { + let flag = "--text"; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + } + if html || open { + let flag = if html { "--html" } else { "--open" }; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if text { + conflicts(flag, "--text")?; + } + } + if summary_only || output_path.is_some() { + let flag = if summary_only { "--summary-only" } else { "--output-path" }; + if html { + conflicts(flag, "--html")?; + } + if open { + conflicts(flag, "--open")?; + } + } + if output_dir.is_some() { + let flag = "--output-dir"; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if output_path.is_some() { + conflicts(flag, "--output-path")?; + } + } + + // forbid_empty_values + if ignore_filename_regex.as_deref() == Some("") { + bail!("empty string is not allowed in --ignore-filename-regex") + } + if output_path.as_deref() == Some(Utf8Path::new("")) { + bail!("empty string is not allowed in --output-path") + } + if output_dir.as_deref() == Some(Utf8Path::new("")) { + bail!("empty string is not allowed in --output-dir") + } + + term::verbose::set(verbose != 0); + // If `-vv` is passed, propagate `-v` to cargo. + if verbose > 1 { + cargo_args.push(format!("-{}", "v".repeat(verbose - 1))); + } + + Ok(Self { + subcommand, + cov: LlvmCovOptions { + json, + lcov, + text, + html, + open, + summary_only, + output_path, + output_dir, + failure_mode, + ignore_filename_regex, + disable_default_ignore_filename_regex, + hide_instantiations, + no_cfg_coverage, + no_cfg_coverage_nightly, + no_report, + fail_under_lines, + fail_uncovered_lines, + fail_uncovered_regions, + fail_uncovered_functions, + show_missing_lines, + include_build_script, + }, + show_env: ShowEnvOptions { export_prefix }, + doctests, + no_run, + ignore_run_fail, + lib, + bin, + bins, + example, + examples, + test, + tests, + bench, + benches, + all_targets, + doc, + workspace, + exclude, + exclude_from_test, + exclude_from_report, + build: BuildOptions { + jobs, + release, + profile, + target, + coverage_target_only, + verbose: verbose.try_into().unwrap_or(u8::MAX), + color, + remap_path_prefix, + include_ffi, + }, + manifest: ManifestOptions { manifest_path }, + cargo_args, + rest, + }) + } + pub(crate) fn cov(&mut self) -> LlvmCovOptions { mem::take(&mut self.cov) } @@ -181,56 +616,62 @@ impl Args { } } -#[derive(Debug, Parser)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Subcommand { - /// Run a binary or example and generate coverage report. - #[clap( - bin_name = "cargo llvm-cov run", - max_term_width(MAX_TERM_WIDTH), - setting(AppSettings::DeriveDisplayOrder) - )] - Run(Box), + Test, - /// Output the environment set by cargo-llvm-cov to build Rust projects. - #[clap( - bin_name = "cargo llvm-cov show-env", - max_term_width(MAX_TERM_WIDTH), - setting(AppSettings::DeriveDisplayOrder) - )] - ShowEnv(ShowEnvOptions), + /// Run a binary or example and generate coverage report. + Run, /// Remove artifacts that cargo-llvm-cov has generated in the past - #[clap( - bin_name = "cargo llvm-cov clean", - max_term_width(MAX_TERM_WIDTH), - setting(AppSettings::DeriveDisplayOrder) - )] - Clean(CleanOptions), + Clean, + + /// Output the environment set by cargo-llvm-cov to build Rust projects. + ShowEnv, /// Run tests with cargo nextest - #[clap( - bin_name = "cargo llvm-cov nextest", - max_term_width(MAX_TERM_WIDTH), - setting(AppSettings::DeriveDisplayOrder), - trailing_var_arg = true, - allow_hyphen_values = true - )] - Nextest { - #[clap(multiple_values = true)] - passthrough_options: Vec, - }, + Nextest, // internal (unstable) - #[clap( - bin_name = "cargo llvm-cov demangle", - max_term_width(MAX_TERM_WIDTH), - hide = true, - setting(AppSettings::DeriveDisplayOrder) - )] Demangle, } -#[derive(Debug, Default, Parser)] +static CARGO_LLVM_COV_USAGE: &str = include_str!("../docs/cargo-llvm-cov.txt"); +static CARGO_LLVM_COV_RUN_USAGE: &str = include_str!("../docs/cargo-llvm-cov-run.txt"); +static CARGO_LLVM_COV_CLEAN_USAGE: &str = include_str!("../docs/cargo-llvm-cov-clean.txt"); +static CARGO_LLVM_COV_SHOW_ENV_USAGE: &str = include_str!("../docs/cargo-llvm-cov-show-env.txt"); +static CARGO_LLVM_COV_NEXTEST_USAGE: &str = include_str!("../docs/cargo-llvm-cov-nextest.txt"); + +impl Subcommand { + fn help_text(subcommand: Option) -> &'static str { + match subcommand { + None | Some(Self::Test) => CARGO_LLVM_COV_USAGE, + Some(Self::Run) => CARGO_LLVM_COV_RUN_USAGE, + Some(Self::Clean) => CARGO_LLVM_COV_CLEAN_USAGE, + Some(Self::ShowEnv) => CARGO_LLVM_COV_SHOW_ENV_USAGE, + Some(Self::Nextest) => CARGO_LLVM_COV_NEXTEST_USAGE, + Some(Self::Demangle) => "", // internal API + } + } +} + +impl FromStr for Subcommand { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + // "test" | "t" => Ok(Self::Test), + "run" | "r" => Ok(Self::Run), + "show-env" => Ok(Self::ShowEnv), + "clean" => Ok(Self::Clean), + "nextest" => Ok(Self::Nextest), + "demangle" => Ok(Self::Demangle), + _ => bail!("unrecognized subcommand {}", s), + } + } +} + +#[derive(Debug, Default)] pub(crate) struct LlvmCovOptions { /// Export coverage data in "json" format /// @@ -238,7 +679,6 @@ pub(crate) struct LlvmCovOptions { /// /// This internally calls `llvm-cov export -format=text`. /// See for more. - #[clap(long)] pub(crate) json: bool, /// Export coverage data in "lcov" format /// @@ -246,7 +686,6 @@ pub(crate) struct LlvmCovOptions { /// /// This internally calls `llvm-cov export -format=lcov`. /// See for more. - #[clap(long, conflicts_with = "json")] pub(crate) lcov: bool, /// Generate coverage report in “text” format @@ -255,7 +694,6 @@ pub(crate) struct LlvmCovOptions { /// /// This internally calls `llvm-cov show -format=text`. /// See for more. - #[clap(long, conflicts_with = "json", conflicts_with = "lcov")] pub(crate) text: bool, /// Generate coverage report in "html" format /// @@ -263,85 +701,54 @@ pub(crate) struct LlvmCovOptions { /// /// This internally calls `llvm-cov show -format=html`. /// See for more. - #[clap(long, conflicts_with = "json", conflicts_with = "lcov", conflicts_with = "text")] pub(crate) html: bool, /// Generate coverage reports in "html" format and open them in a browser after the operation. /// /// See --html for more. - #[clap(long, conflicts_with = "json", conflicts_with = "lcov", conflicts_with = "text")] pub(crate) open: bool, /// Export only summary information for each file in the coverage data /// /// This flag can only be used together with either --json or --lcov. // If the format flag is not specified, this flag is no-op because the only summary is displayed anyway. - #[clap(long, conflicts_with = "text", conflicts_with = "html", conflicts_with = "open")] pub(crate) summary_only: bool, /// Specify a file to write coverage data into. /// /// This flag can only be used together with --json, --lcov, or --text. /// See --output-dir for --html and --open. - #[clap( - long, - value_name = "PATH", - conflicts_with = "html", - conflicts_with = "open", - forbid_empty_values = true - )] pub(crate) output_path: Option, /// Specify a directory to write coverage report into (default to `target/llvm-cov`). /// /// This flag can only be used together with --text, --html, or --open. /// See also --output-path. // If the format flag is not specified, this flag is no-op. - #[clap( - long, - value_name = "DIRECTORY", - conflicts_with = "json", - conflicts_with = "lcov", - conflicts_with = "output-path", - forbid_empty_values = true - )] pub(crate) output_dir: Option, /// Fail if `any` or `all` profiles cannot be merged (default to `any`) - #[clap(long, value_name = "any|all", possible_values(&["any", "all"]), hide_possible_values = true)] pub(crate) failure_mode: Option, /// Skip source code files with file paths that match the given regular expression. - #[clap(long, value_name = "PATTERN", forbid_empty_values = true)] pub(crate) ignore_filename_regex: Option, // For debugging (unstable) - #[clap(long, hide = true)] pub(crate) disable_default_ignore_filename_regex: bool, /// Hide instantiations from report - #[clap(long)] pub(crate) hide_instantiations: bool, /// Unset cfg(coverage), which is enabled when code is built using cargo-llvm-cov. - #[clap(long)] pub(crate) no_cfg_coverage: bool, /// Unset cfg(coverage_nightly), which is enabled when code is built using cargo-llvm-cov and nightly compiler. - #[clap(long)] pub(crate) no_cfg_coverage_nightly: bool, /// Run tests, but don't generate coverage report - #[clap(long)] pub(crate) no_report: bool, /// Exit with a status of 1 if the total line coverage is less than MIN percent. - #[clap(long, value_name = "MIN")] pub(crate) fail_under_lines: Option, /// Exit with a status of 1 if the uncovered lines are greater than MAX. - #[clap(long, value_name = "MAX")] pub(crate) fail_uncovered_lines: Option, /// Exit with a status of 1 if the uncovered regions are greater than MAX. - #[clap(long, value_name = "MAX")] pub(crate) fail_uncovered_regions: Option, /// Exit with a status of 1 if the uncovered functions are greater than MAX. - #[clap(long, value_name = "MAX")] pub(crate) fail_uncovered_functions: Option, /// Show lines with no coverage. - #[clap(long)] pub(crate) show_missing_lines: bool, /// Include build script in coverage report. - #[clap(long)] pub(crate) include_build_script: bool, } @@ -351,32 +758,25 @@ impl LlvmCovOptions { } } -#[derive(Debug, Default, Parser)] +#[derive(Debug, Default)] pub(crate) struct BuildOptions { /// Number of parallel jobs, defaults to # of CPUs // Max value is u32::MAX: https://github.com/rust-lang/cargo/blob/0.62.0/src/cargo/util/command_prelude.rs#L356 - #[clap(short, long, value_name = "N")] pub(crate) jobs: Option, /// Build artifacts in release mode, with optimizations - #[clap(short, long)] pub(crate) release: bool, /// Build artifacts with the specified profile - #[clap(long, value_name = "PROFILE-NAME")] pub(crate) profile: Option, - /// Space or comma separated list of features to activate - #[clap(short = 'F', long, multiple_occurrences = true, value_name = "FEATURES")] - pub(crate) features: Vec, - /// Activate all available features - #[clap(long)] - pub(crate) all_features: bool, - /// Do not activate the `default` feature - #[clap(long)] - pub(crate) no_default_features: bool, + // /// Space or comma separated list of features to activate + // pub(crate) features: Vec, + // /// Activate all available features + // pub(crate) all_features: bool, + // /// Do not activate the `default` feature + // pub(crate) no_default_features: bool, /// Build for the target triple /// /// When this option is used, coverage for proc-macro and build script will /// not be displayed because cargo does not pass RUSTFLAGS to them. - #[clap(long, value_name = "TRIPLE")] pub(crate) target: Option, /// Activate coverage reporting only for the target triple /// @@ -384,183 +784,92 @@ pub(crate) struct BuildOptions { /// This is important, if the project uses multiple targets via the cargo /// bindeps feature, and not all targets can use `instrument-coverage`, /// e.g. a microkernel, or an embedded binary. - #[clap(long, requires = "target")] pub(crate) coverage_target_only: bool, // TODO: Currently, we are using a subdirectory of the target directory as // the actual target directory. What effect should this option have // on its behavior? // /// Directory for all generated artifacts - // #[clap(long, value_name = "DIRECTORY")] // target_dir: Option, /// Use verbose output /// /// Use -vv (-vvv) to propagate verbosity to cargo. - #[clap(short, long, parse(from_occurrences))] pub(crate) verbose: u8, /// Coloring // This flag will be propagated to both cargo and llvm-cov. - #[clap(long, arg_enum, value_name = "WHEN")] pub(crate) color: Option, /// Use --remap-path-prefix for workspace root /// /// Note that this does not fully compatible with doctest. - #[clap(long)] pub(crate) remap_path_prefix: bool, /// Include coverage of C/C++ code linked to Rust library/binary /// /// Note that `CC`/`CXX`/`LLVM_COV`/`LLVM_PROFDATA` environment variables /// must be set to Clang/LLVM compatible with the LLVM version used in rustc. // TODO: support specifying languages like: --include-ffi=c, --include-ffi=c,c++ - #[clap(long)] pub(crate) include_ffi: bool, } -impl BuildOptions { - pub(crate) fn cargo_args(&self, cmd: &mut ProcessBuilder) { - if let Some(jobs) = self.jobs { - cmd.arg("--jobs"); - cmd.arg(jobs.to_string()); - } - if self.release { - cmd.arg("--release"); - } - if let Some(profile) = &self.profile { - cmd.arg("--profile"); - cmd.arg(profile); - } - for features in &self.features { - cmd.arg("--features"); - cmd.arg(features); - } - if self.all_features { - cmd.arg("--all-features"); - } - if self.no_default_features { - cmd.arg("--no-default-features"); - } - if let Some(target) = &self.target { - cmd.arg("--target"); - cmd.arg(target); - } - - if let Some(color) = self.color { - cmd.arg("--color"); - cmd.arg(color.cargo_color()); - } - - // If `-vv` is passed, propagate `-v` to cargo. - if self.verbose > 1 { - cmd.arg(format!("-{}", "v".repeat(self.verbose as usize - 1))); - } - } +#[derive(Debug)] +pub(crate) struct ShowEnvOptions { + /// Prepend "export " to each line, so that the output is suitable to be sourced by bash. + pub(crate) export_prefix: bool, } -#[derive(Debug, Parser)] -pub(crate) struct RunOptions { - #[clap(flatten)] - cov: LlvmCovOptions, - - /// No output printed to stdout - #[clap(short, long, conflicts_with = "verbose")] - pub(crate) quiet: bool, - /// Name of the bin target to run - #[clap(long, multiple_occurrences = true, value_name = "NAME")] - pub(crate) bin: Vec, - /// Name of the example target to run - #[clap(long, multiple_occurrences = true, value_name = "NAME")] - pub(crate) example: Vec, - /// Package with the target to run - #[clap(short, long, value_name = "SPEC")] - pub(crate) package: Option, - - #[clap(flatten)] - build: BuildOptions, - - #[clap(flatten)] - manifest: ManifestOptions, - - /// Unstable (nightly-only) flags to Cargo - #[clap(short = 'Z', multiple_occurrences = true, value_name = "FLAG")] - pub(crate) unstable_flags: Vec, - - /// Arguments for the test binary - #[clap(last = true)] - pub(crate) args: Vec, +// https://doc.rust-lang.org/nightly/cargo/commands/cargo-test.html#manifest-options +#[derive(Debug, Default)] +pub(crate) struct ManifestOptions { + /// Path to Cargo.toml + pub(crate) manifest_path: Option, } -impl RunOptions { - pub(crate) fn cov(&mut self) -> LlvmCovOptions { - mem::take(&mut self.cov) - } - - pub(crate) fn build(&mut self) -> BuildOptions { - mem::take(&mut self.build) - } - - pub(crate) fn manifest(&mut self) -> ManifestOptions { - mem::take(&mut self.manifest) +fn format_flag(flag: &lexopt::Arg<'_>) -> String { + match flag { + Long(flag) => format!("--{}", flag), + Short(flag) => format!("-{}", flag), + Value(_) => unreachable!(), } } -#[derive(Debug, Parser)] -pub(crate) struct ShowEnvOptions { - /// Prepend "export " to each line, so that the output is suitable to be sourced by bash. - #[clap(long)] - pub(crate) export_prefix: bool, +#[cold] +#[inline(never)] +fn multi_arg(flag: &lexopt::Arg<'_>) -> Result<()> { + let flag = &format_flag(flag); + bail!("The argument '{}' was provided more than once, but cannot be used multiple times", flag); } -#[derive(Debug, Parser)] -pub(crate) struct CleanOptions { - /// Remove artifacts that may affect the coverage results of packages in the workspace. - #[clap(long)] - pub(crate) workspace: bool, - // TODO: Currently, we are using a subdirectory of the target directory as - // the actual target directory. What effect should this option have - // on its behavior? - // /// Directory for all generated artifacts - // #[clap(long, value_name = "DIRECTORY")] - // pub(crate) target_dir: Option, - /// Use verbose output - #[clap(short, long, parse(from_occurrences))] - pub(crate) verbose: u8, - /// Coloring - #[clap(long, arg_enum, value_name = "WHEN")] - pub(crate) color: Option, - #[clap(flatten)] - pub(crate) manifest: ManifestOptions, +// `flag` requires one of `requires`. +#[cold] +#[inline(never)] +fn requires(flag: &str, requires: &[&str]) -> Result<()> { + let with = match requires.len() { + 0 => unreachable!(), + 1 => requires[0].to_string(), + 2 => format!("either {} or {}", requires[0], requires[1]), + _ => { + let mut with = String::new(); + for f in requires.iter().take(requires.len() - 1) { + with += f; + with += ", "; + } + with += "or "; + with += requires.last().unwrap(); + with + } + }; + bail!("{} can only be used together with {}", flag, with); } -// https://doc.rust-lang.org/nightly/cargo/commands/cargo-test.html#manifest-options -#[derive(Debug, Default, Parser)] -pub(crate) struct ManifestOptions { - /// Path to Cargo.toml - #[clap(long, value_name = "PATH")] - pub(crate) manifest_path: Option, - /// Require Cargo.lock and cache are up to date - #[clap(long)] - pub(crate) frozen: bool, - /// Require Cargo.lock is up to date - #[clap(long)] - pub(crate) locked: bool, - /// Run without accessing the network - #[clap(long)] - pub(crate) offline: bool, +#[cold] +#[inline(never)] +fn conflicts(a: &str, b: &str) -> Result<()> { + bail!("{} may not be used together with {}", a, b); } -impl ManifestOptions { - pub(crate) fn cargo_args(&self, cmd: &mut ProcessBuilder) { - // Skip --manifest-path because it is set based on Workspace::current_manifest. - if self.frozen { - cmd.arg("--frozen"); - } - if self.locked { - cmd.arg("--locked"); - } - if self.offline { - cmd.arg("--offline"); - } - } +#[cold] +#[inline(never)] +fn unexpected(arg: &str) -> Result<()> { + bail!("found argument '{}' which wasn't expected, or isn't valid in this context", arg); } #[cfg(test)] @@ -568,135 +877,14 @@ mod tests { use std::{ env, io::Write, - panic, path::Path, process::{Command, Stdio}, }; use anyhow::Result; - use clap::{CommandFactory, Parser}; use fs_err as fs; - use super::{Args, Opts, MAX_TERM_WIDTH}; - - #[test] - fn assert_app() { - Args::command().debug_assert(); - } - - // https://github.com/clap-rs/clap/issues/751 - #[cfg(unix)] - #[test] - fn non_utf8_arg() { - use std::{ffi::OsStr, os::unix::prelude::OsStrExt}; - - // `cargo llvm-cov -- $'fo\x80o'` - Opts::try_parse_from([ - "cargo".as_ref(), - "llvm-cov".as_ref(), - "--".as_ref(), - OsStr::from_bytes(&[b'f', b'o', 0x80, b'o']), - ]) - .unwrap_err(); - } - - // https://github.com/clap-rs/clap/issues/1772 - #[test] - fn multiple_occurrences() { - let Opts::LlvmCov(args) = - Opts::try_parse_from(["cargo", "llvm-cov", "--features", "a", "--features", "b"]) - .unwrap(); - assert_eq!(args.build.features, ["a", "b"]); - - let Opts::LlvmCov(args) = - Opts::try_parse_from(["cargo", "llvm-cov", "--package", "a", "--package", "b"]) - .unwrap(); - assert_eq!(args.package, ["a", "b"]); - - let Opts::LlvmCov(args) = Opts::try_parse_from([ - "cargo", - "llvm-cov", - "--exclude", - "a", - "--exclude", - "b", - "--all", - ]) - .unwrap(); - assert_eq!(args.exclude, ["a", "b"]); - - let Opts::LlvmCov(args) = - Opts::try_parse_from(["cargo", "llvm-cov", "-Z", "a", "-Zb"]).unwrap(); - assert_eq!(args.unstable_flags, ["a", "b"]); - - let Opts::LlvmCov(args) = - Opts::try_parse_from(["cargo", "llvm-cov", "--", "a", "b"]).unwrap(); - assert_eq!(args.args, ["a", "b"]); - } - - // https://github.com/taiki-e/cargo-llvm-cov/pull/127#issuecomment-1018204521 - #[test] - fn multiple_values() { - Opts::try_parse_from(["cargo", "llvm-cov", "--features", "a", "b"]).unwrap_err(); - Opts::try_parse_from(["cargo", "llvm-cov", "--package", "a", "b"]).unwrap_err(); - Opts::try_parse_from(["cargo", "llvm-cov", "--exclude", "a", "b"]).unwrap_err(); - Opts::try_parse_from(["cargo", "llvm-cov", "-Z", "a", "b"]).unwrap_err(); - } - - // https://github.com/clap-rs/clap/issues/1740 - #[test] - fn empty_value() { - let forbidden = &[ - "--output-path", - "--output-dir", - "--ignore-filename-regex", - // "--target-dir", - ]; - let allowed = &[ - "--bin", - "--example", - "--test", - "--bench", - "--package", - "--exclude", - "--profile", - "--features", - "--target", - // "--target-dir", - "--manifest-path", - "-Z", - "--", - ]; - - for &flag in forbidden { - Opts::try_parse_from(["cargo", "llvm-cov", flag, ""]).unwrap_err(); - } - for &flag in allowed { - if flag == "--exclude" { - Opts::try_parse_from(["cargo", "llvm-cov", flag, "", "--workspace"]).unwrap(); - } else { - Opts::try_parse_from(["cargo", "llvm-cov", flag, ""]).unwrap(); - } - } - } - - fn get_help(long: bool) -> Result { - let mut buf = vec![]; - if long { - Args::command().term_width(MAX_TERM_WIDTH).write_long_help(&mut buf)?; - } else { - Args::command().term_width(MAX_TERM_WIDTH).write_help(&mut buf)?; - } - let mut out = String::new(); - for mut line in String::from_utf8(buf)?.lines() { - if let Some(new) = line.trim_end().strip_suffix(env!("CARGO_PKG_VERSION")) { - line = new; - } - out.push_str(line.trim_end()); - out.push('\n'); - } - Ok(out) - } + use super::*; #[track_caller] fn assert_diff(expected_path: impl AsRef, actual: impl AsRef) { @@ -728,21 +916,9 @@ mod tests { } } - #[test] - fn long_help() { - let actual = get_help(true).unwrap(); - assert_diff("tests/long-help.txt", actual); - } - - #[test] - fn short_help() { - let actual = get_help(false).unwrap(); - assert_diff("tests/short-help.txt", actual); - } - #[test] fn update_readme() -> Result<()> { - let new = get_help(true)?; + let new = CARGO_LLVM_COV_USAGE; let path = &Path::new(env!("CARGO_MANIFEST_DIR")).join("README.md"); let base = fs::read_to_string(path)?; let mut out = String::with_capacity(base.capacity()); @@ -756,7 +932,7 @@ mod tests { start = true; out.push_str("```console\n"); out.push_str("$ cargo llvm-cov --help\n"); - out.push_str(&new); + out.push_str(new); for line in &mut lines { if line == "" { out.push_str("```\n"); diff --git a/src/config.rs b/src/config.rs index a85313a5..5e226196 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, collections::BTreeMap, ffi::OsStr}; -use anyhow::{format_err, Context as _, Result}; +use anyhow::{Context as _, Result}; use serde::Deserialize; use crate::{env, term::Coloring}; @@ -151,8 +151,7 @@ impl Config { self.term.verbose = Some(verbose.parse()?); } if let Some(color) = env::var("CARGO_TERM_COLOR")? { - self.term.color = - Some(clap::ArgEnum::from_str(&color, false).map_err(|e| format_err!("{}", e))?); + self.term.color = Some(color.parse()?); } Ok(()) } diff --git a/src/context.rs b/src/context.rs index ae69b083..50f86ab1 100644 --- a/src/context.rs +++ b/src/context.rs @@ -17,7 +17,6 @@ pub(crate) struct Context { pub(crate) ws: Workspace, pub(crate) build: BuildOptions, - pub(crate) manifest: ManifestOptions, pub(crate) cov: LlvmCovOptions, pub(crate) doctests: bool, @@ -46,7 +45,7 @@ impl Context { #[allow(clippy::too_many_arguments)] pub(crate) fn new( mut build: BuildOptions, - manifest: ManifestOptions, + manifest: &ManifestOptions, mut cov: LlvmCovOptions, exclude: &[String], exclude_from_report: &[String], @@ -54,7 +53,7 @@ impl Context { no_run: bool, show_env: bool, ) -> Result { - let ws = Workspace::new(&manifest, build.target.as_deref(), doctests, show_env)?; + let ws = Workspace::new(manifest, build.target.as_deref(), doctests, show_env)?; ws.config.merge_to_args(&mut build.target, &mut build.verbose, &mut build.color); term::set_coloring(&mut build.color); term::verbose::set(build.verbose != 0); @@ -136,7 +135,6 @@ impl Context { Ok(Self { ws, build, - manifest, cov, doctests, no_run, diff --git a/src/main.rs b/src/main.rs index cc9e2f5b..e97316e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,13 @@ #![forbid(unsafe_code)] #![warn(rust_2018_idioms, single_use_lifetimes, unreachable_pub)] #![warn(clippy::pedantic)] -#![allow(clippy::single_match_else, clippy::struct_excessive_bools)] +#![allow( + clippy::match_same_arms, + clippy::similar_names, + clippy::single_match_else, + clippy::struct_excessive_bools, + clippy::too_many_lines +)] // Refs: // - https://doc.rust-lang.org/nightly/rustc/instrument-coverage.html @@ -35,12 +41,11 @@ use std::{ use anyhow::{Context as _, Result}; use camino::{Utf8Path, Utf8PathBuf}; use cargo_llvm_cov::json; -use clap::Parser; use regex::Regex; use walkdir::WalkDir; use crate::{ - cli::{Args, Opts, RunOptions, ShowEnvOptions, Subcommand}, + cli::{Args, ShowEnvOptions, Subcommand}, config::StringOrArray, context::Context, json::LlvmCovJsonExport, @@ -61,21 +66,21 @@ fn main() { } fn try_main() -> Result<()> { - let Opts::LlvmCov(mut args) = Opts::parse(); + let mut args = Args::parse()?; - match args.subcommand.take() { - Some(Subcommand::Demangle) => { + match args.subcommand { + Subcommand::Demangle => { demangler::run()?; } - Some(Subcommand::Clean(options)) => { - clean::run(options)?; + Subcommand::Clean => { + clean::run(&mut args)?; } - Some(Subcommand::Run(mut args)) => { + Subcommand::Run => { let cx = &Context::new( args.build(), - args.manifest(), + &args.manifest(), args.cov(), &[], &[], @@ -94,27 +99,16 @@ fn try_main() -> Result<()> { } } - Some(Subcommand::ShowEnv(options)) => { + Subcommand::ShowEnv => { let cx = &context_from_args(&mut args, true)?; let stdout = io::stdout(); - let writer = &mut ShowEnvWriter { target: stdout.lock(), options }; + let writer = &mut ShowEnvWriter { target: stdout.lock(), options: args.show_env }; set_env(cx, writer); writer.set("CARGO_LLVM_COV_TARGET_DIR", cx.ws.metadata.target_directory.as_str()); } - Some(Subcommand::Nextest { passthrough_options }) => { - let cx = &context_from_args( - &mut Args::try_parse_from( - [ - // fake argv[0] to help clap parse - "nextest".to_string(), - ] - .iter() - // real pass-through args - .chain(passthrough_options.iter()), - )?, - false, - )?; + Subcommand::Nextest => { + let cx = &context_from_args(&mut args, false)?; clean::clean_partial(cx)?; create_dirs(cx)?; @@ -133,7 +127,7 @@ fn try_main() -> Result<()> { } } - None => { + Subcommand::Test => { let cx = &context_from_args(&mut args, false)?; let tmp = term::warn(); // The following warnings should not be promoted to an error. if args.doctests { @@ -168,7 +162,7 @@ fn try_main() -> Result<()> { fn context_from_args(args: &mut Args, show_env: bool) -> Result { Context::new( args.build(), - args.manifest(), + &args.manifest(), args.cov(), &args.exclude, &args.exclude_from_report, @@ -331,8 +325,23 @@ fn set_env(cx: &Context, env: &mut impl EnvTarget) { env.set("RUST_TEST_THREADS", "1"); } -fn has_z_flag(args: &Args, name: &str) -> bool { - args.unstable_flags.iter().any(|f| f == name) +fn has_z_flag(args: &[String], name: &str) -> bool { + let mut iter = args.iter().map(String::as_str); + while let Some(mut arg) = iter.next() { + if arg == "-Z" { + arg = iter.next().unwrap(); + } else if let Some(a) = arg.strip_prefix("-Z") { + arg = a; + } else { + continue; + } + if let Some(rest) = arg.strip_prefix(name) { + if rest.is_empty() || rest.starts_with('=') { + return true; + } + } + } + false } fn run_test(cx: &Context, args: &Args) -> Result<()> { @@ -341,7 +350,7 @@ fn run_test(cx: &Context, args: &Args) -> Result<()> { set_env(cx, &mut cargo); cargo.arg("test"); - if cx.doctests && !has_z_flag(args, "doctest-in-workspace") { + if cx.doctests && !has_z_flag(&args.cargo_args, "doctest-in-workspace") { // https://github.com/rust-lang/cargo/issues/9427 cargo.arg("-Z"); cargo.arg("doctest-in-workspace"); @@ -352,7 +361,7 @@ fn run_test(cx: &Context, args: &Args) -> Result<()> { if !args.no_run { cargo_no_run.arg("--no-run"); } - cargo::test_args(cx, args, &mut cargo_no_run); + cargo::test_or_run_args(cx, args, &mut cargo_no_run); if term::verbose() { status!("Running", "{}", cargo_no_run); cargo_no_run.stdout_to_stderr().run()?; @@ -363,7 +372,7 @@ fn run_test(cx: &Context, args: &Args) -> Result<()> { drop(cargo_no_run); cargo.arg("--no-fail-fast"); - cargo::test_args(cx, args, &mut cargo); + cargo::test_or_run_args(cx, args, &mut cargo); if term::verbose() { status!("Running", "{}", cargo); } @@ -371,7 +380,7 @@ fn run_test(cx: &Context, args: &Args) -> Result<()> { warn!("{}", e); } } else { - cargo::test_args(cx, args, &mut cargo); + cargo::test_or_run_args(cx, args, &mut cargo); if term::verbose() { status!("Running", "{}", cargo); } @@ -392,7 +401,7 @@ fn run_nextest(cx: &Context, args: &Args) -> Result<()> { return Err(anyhow::anyhow!("doctest is not supported for nextest")); } - cargo::test_args(cx, args, &mut cargo); + cargo::test_or_run_args(cx, args, &mut cargo); if term::verbose() { status!("Running", "{}", cargo); @@ -402,13 +411,13 @@ fn run_nextest(cx: &Context, args: &Args) -> Result<()> { Ok(()) } -fn run_run(cx: &Context, args: &RunOptions) -> Result<()> { +fn run_run(cx: &Context, args: &Args) -> Result<()> { let mut cargo = cx.cargo(); set_env(cx, &mut cargo); cargo.arg("run"); - cargo::run_args(cx, args, &mut cargo); + cargo::test_or_run_args(cx, args, &mut cargo); if term::verbose() { status!("Running", "{}", cargo); diff --git a/src/term.rs b/src/term.rs index 34232a93..f841af02 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,12 +1,14 @@ use std::{ io::Write, + str::FromStr, sync::atomic::{AtomicBool, AtomicU8, Ordering}, }; +use anyhow::{bail, Error}; use serde::Deserialize; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, clap::ArgEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "kebab-case")] #[repr(u8)] pub(crate) enum Coloring { @@ -29,6 +31,19 @@ impl Coloring { } } +impl FromStr for Coloring { + type Err = Error; + + fn from_str(color: &str) -> Result { + match color { + "auto" => Ok(Self::Auto), + "always" => Ok(Self::Always), + "never" => Ok(Self::Never), + other => bail!("must be auto, always, or never, but found `{}`", other), + } + } +} + static COLORING: AtomicU8 = AtomicU8::new(Coloring::AUTO); pub(crate) fn set_coloring(coloring: &mut Option) { let mut color = coloring.unwrap_or(Coloring::Auto); diff --git a/tests/auxiliary/mod.rs b/tests/auxiliary/mod.rs index cabc3b6a..60238ef1 100644 --- a/tests/auxiliary/mod.rs +++ b/tests/auxiliary/mod.rs @@ -201,36 +201,50 @@ pub struct AssertOutput { status: ExitStatus, } -fn line_separated(lines: &str, f: impl FnMut(&str)) { - lines.split('\n').map(str::trim).filter(|line| !line.is_empty()).for_each(f); +fn line_separated(lines: &str) -> impl Iterator { + lines.split('\n').map(str::trim).filter(|line| !line.is_empty()) } impl AssertOutput { /// Receives a line(`\n`)-separated list of patterns and asserts whether stderr contains each pattern. #[track_caller] pub fn stderr_contains(&self, pats: &str) -> &Self { - line_separated(pats, |pat| { + for pat in line_separated(pats) { assert!( self.stderr.contains(pat), "assertion failed: `self.stderr.contains(..)`:\n\nEXPECTED:\n{0}\n{pat}\n{0}\n\nACTUAL:\n{0}\n{1}\n{0}\n", "-".repeat(60), self.stderr ); - }); + } self } /// Receives a line(`\n`)-separated list of patterns and asserts whether stdout contains each pattern. #[track_caller] pub fn stdout_contains(&self, pats: &str) -> &Self { - line_separated(pats, |pat| { + for pat in line_separated(pats) { assert!( self.stdout.contains(pat), "assertion failed: `self.stdout.contains(..)`:\n\nEXPECTED:\n{0}\n{pat}\n{0}\n\nACTUAL:\n{0}\n{1}\n{0}\n", "-".repeat(60), self.stdout ); - }); + } + self + } + + /// Receives a line(`\n`)-separated list of patterns and asserts whether stdout contains each pattern. + #[track_caller] + pub fn stdout_not_contains(&self, pats: &str) -> &Self { + for pat in line_separated(pats) { + assert!( + !self.stdout.contains(pat), + "assertion failed: `!self.stdout.contains(..)`:\n\nEXPECTED:\n{0}\n{pat}\n{0}\n\nACTUAL:\n{0}\n{1}\n{0}\n", + "-".repeat(60), + self.stdout + ); + } self } } diff --git a/tests/test.rs b/tests/test.rs index 1a4bc277..4f3ba8dc 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -259,10 +259,34 @@ fn open_report() { ); } +#[test] +fn show_env() { + cargo_llvm_cov().args(["show-env"]).assert_success().stdout_not_contains("export"); + cargo_llvm_cov() + .args(["show-env", "--export-prefix"]) + .assert_success() + .stdout_contains("export"); +} + +#[test] +fn help() { + for subcommand in ["", "run", "clean", "show-env", "nextest"] { + if subcommand.is_empty() { + cargo_llvm_cov().arg("--help").assert_success().stdout_contains("cargo llvm-cov"); + } else { + cargo_llvm_cov() + .arg(subcommand) + .arg("--help") + .assert_success() + .stdout_contains(&format!("cargo llvm-cov {subcommand}")); + } + } +} + #[test] fn version() { cargo_llvm_cov().arg("--version").assert_success().stdout_contains(env!("CARGO_PKG_VERSION")); cargo_llvm_cov().args(["clean", "--version"]).assert_failure().stderr_contains( - "Found argument '--version' which wasn't expected, or isn't valid in this context", + "found argument '--version' which wasn't expected, or isn't valid in this context", ); }