From c256876f33a72033073d08c9d60a856535a74944 Mon Sep 17 00:00:00 2001 From: Taiki Endo Date: Sun, 10 Mar 2024 19:44:01 +0900 Subject: [PATCH] Add --nextest-archive-file option to report subcommand --- .github/workflows/ci.yml | 4 + docs/cargo-llvm-cov-report.txt | 3 + src/cargo.rs | 6 +- src/cli.rs | 1991 ++++++++++++++++---------------- src/context.rs | 6 +- src/main.rs | 10 +- 6 files changed, 1032 insertions(+), 988 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eac4c8d0..0c6dd257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,19 +85,23 @@ jobs: cd ../real1 cargo llvm-cov nextest-archive --archive-file a.tar.zst cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 + cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 rm a.tar.zst cargo clean cargo llvm-cov nextest-archive --archive-file a.tar.zst --release cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 + cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 rm a.tar.zst cargo clean cargo llvm-cov nextest-archive --archive-file a.tar.zst --cargo-profile custom cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 + cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 rm a.tar.zst cargo clean host=$(rustc -Vv | grep host | sed 's/host: //') cargo llvm-cov nextest-archive --archive-file a.tar.zst --target "${host}" cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 + cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 working-directory: tests/fixtures/crates/bin_crate - run: | set -eEuxo pipefail diff --git a/docs/cargo-llvm-cov-report.txt b/docs/cargo-llvm-cov-report.txt index 7098a605..cac09a4a 100644 --- a/docs/cargo-llvm-cov-report.txt +++ b/docs/cargo-llvm-cov-report.txt @@ -77,6 +77,9 @@ OPTIONS: This flag can only be used together with --text, --html, or --open. See also --output-path. + --nextest-archive-file + Path to nextest archive + --failure-mode Fail if `any` or `all` profiles cannot be merged (default to `any`) diff --git a/src/cargo.rs b/src/cargo.rs index ec4b9cc9..c16bfe9e 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -223,8 +223,8 @@ pub(crate) fn test_or_run_args(cx: &Context, cmd: &mut ProcessBuilder) { if cx.args.release { cmd.arg("--release"); } - if let Some(profile) = &cx.args.profile { - if cx.args.subcommand.is_nextest_based() { + if let Some(profile) = &cx.args.cargo_profile { + if cx.args.subcommand.call_cargo_nextest() { cmd.arg("--cargo-profile"); } else { cmd.arg("--profile"); @@ -253,7 +253,7 @@ pub(crate) fn clean_args(cx: &Context, cmd: &mut ProcessBuilder) { if cx.args.release { cmd.arg("--release"); } - if let Some(profile) = &cx.args.profile { + if let Some(profile) = &cx.args.cargo_profile { cmd.arg("--profile"); cmd.arg(profile); } diff --git a/src/cli.rs b/src/cli.rs index 8fc7e7c6..3908fae3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -87,9 +87,9 @@ pub(crate) struct Args { pub(crate) release: bool, /// Build artifacts with the specified profile /// - /// On `cargo llvm-cov nextest` this is the value of `--cargo-profile` option, - /// otherwise this is the value of `--profile` option. - pub(crate) profile: Option, + /// On `cargo llvm-cov nextest`/`cargo llvm-cov nextest-archive` this is the + /// value of `--cargo-profile` option, otherwise this is the value of `--profile` option. + pub(crate) cargo_profile: Option, // /// Space or comma separated list of features to activate // pub(crate) features: Vec, // /// Activate all available features @@ -138,1049 +138,1086 @@ pub(crate) struct Args { pub(crate) manifest: ManifestOptions, - pub(crate) archive_file: Option, + pub(crate) nextest_archive_file: Option, pub(crate) cargo_args: Vec, /// Arguments for the test binary 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.70.0/compiler/rustc_driver_impl/src/lib.rs#L1366-L1376 - 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: {arg:?}", i + 1)) - }) - } +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum Subcommand { + /// Run tests and generate coverage report. + None, - let mut raw_args = handle_args(env::args_os()); - raw_args.next(); // cargo - match raw_args.next().transpose()? { - Some(arg) if arg == SUBCMD => {} - Some(arg) => bail!("expected subcommand '{SUBCMD}', found argument '{arg}'"), - 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::>>()?; + /// Run tests and generate coverage report. + Test, - let mut cargo_args = vec![]; - let mut subcommand = Subcommand::None; - let mut after_subcommand = false; + /// Run a binary or example and generate coverage report. + Run, - let mut manifest_path = None; - let mut frozen = false; - let mut locked = false; - let mut offline = false; - let mut color = None; + /// Generate coverage report. + Report { + nextest_archive_file: bool, + }, - 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; + /// Remove artifacts that cargo-llvm-cov has generated in the past + Clean, - 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![]; + /// Output the environment set by cargo-llvm-cov to build Rust projects. + ShowEnv, - // llvm-cov options - let mut json = false; - let mut lcov = false; - let mut cobertura = false; - let mut codecov = 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 show_instantiations = false; - let mut no_cfg_coverage = false; - let mut no_cfg_coverage_nightly = false; - let mut no_report = false; - let mut fail_under_functions = None; - let mut fail_under_lines = None; - let mut fail_under_regions = 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; - let mut dep_coverage = None; - let mut skip_functions = false; + /// Run tests with cargo nextest + Nextest { + archive_file: bool, + }, - // build options - 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; - let mut no_clean = false; + /// Build and archive tests with cargo nextest + NextestArchive, - // show-env options - let mut export_prefix = false; + // internal (unstable) + Demangle, +} - // nextest options - let mut archive_file = None; +static CARGO_LLVM_COV_USAGE: &str = include_str!("../docs/cargo-llvm-cov.txt"); +static CARGO_LLVM_COV_TEST_USAGE: &str = include_str!("../docs/cargo-llvm-cov-test.txt"); +static CARGO_LLVM_COV_RUN_USAGE: &str = include_str!("../docs/cargo-llvm-cov-run.txt"); +static CARGO_LLVM_COV_REPORT_USAGE: &str = include_str!("../docs/cargo-llvm-cov-report.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"); +static CARGO_LLVM_COV_NEXTEST_ARCHIVE_USAGE: &str = + include_str!("../docs/cargo-llvm-cov-nextest-archive.txt"); - let mut parser = lexopt::Parser::from_args(args.clone()); - while let Some(arg) = parser.next()? { - macro_rules! parse_opt { - ($opt:tt $(,)?) => {{ - if Store::is_full(&$opt) { - multi_arg(&arg)?; - } - Store::push(&mut $opt, &parser.value()?.into_string().unwrap())?; - after_subcommand = false; - }}; - } - macro_rules! parse_opt_passthrough { - ($opt:tt $(,)?) => {{ - if Store::is_full(&$opt) { - multi_arg(&arg)?; - } - match arg { - Long(flag) => { - let flag = format!("--{}", flag); - if let Some(val) = parser.optional_value() { - let val = val.into_string().unwrap(); - Store::push(&mut $opt, &val)?; - cargo_args.push(format!("{}={}", flag, val)); - } else { - let val = parser.value()?.into_string().unwrap(); - Store::push(&mut $opt, &val)?; - cargo_args.push(flag); - cargo_args.push(val); - } - } - Short(flag) => { - if let Some(val) = parser.optional_value() { - let val = val.into_string().unwrap(); - Store::push(&mut $opt, &val)?; - cargo_args.push(format!("-{}{}", flag, val)); - } else { - let val = parser.value()?.into_string().unwrap(); - Store::push(&mut $opt, &val)?; - cargo_args.push(format!("-{}", flag)); - cargo_args.push(val); - } - } - Value(_) => unreachable!(), - } - after_subcommand = false; - }}; - } - macro_rules! parse_flag { - ($flag:ident $(,)?) => {{ - if mem::replace(&mut $flag, true) { - multi_arg(&arg)?; - } - #[allow(unused_assignments)] - { - after_subcommand = false; - } - }}; - } - 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.string()?)); - } else { - cargo_args.push(flag); - } - } - Short(flag) => { - if let Some(val) = parser.optional_value() { - cargo_args.push(format!("-{}{}", flag, val.string()?)); - } else { - cargo_args.push(format!("-{}", flag)); - } - } - Value(_) => unreachable!(), - } - after_subcommand = false; - }}; - } +impl Subcommand { + fn can_passthrough(subcommand: Self) -> bool { + matches!(subcommand, Self::Test | Self::Run | Self::Nextest { .. } | Self::NextestArchive) + } - match arg { - Long("color") => parse_opt_passthrough!(color), - Long("manifest-path") => parse_opt!(manifest_path), - Long("frozen") => parse_flag_passthrough!(frozen), - Long("locked") => parse_flag_passthrough!(locked), - Long("offline") => parse_flag_passthrough!(offline), + fn help_text(subcommand: Self) -> &'static str { + match subcommand { + Self::None => CARGO_LLVM_COV_USAGE, + Self::Test => CARGO_LLVM_COV_TEST_USAGE, + Self::Run => CARGO_LLVM_COV_RUN_USAGE, + Self::Report { .. } => CARGO_LLVM_COV_REPORT_USAGE, + Self::Clean => CARGO_LLVM_COV_CLEAN_USAGE, + Self::ShowEnv => CARGO_LLVM_COV_SHOW_ENV_USAGE, + Self::Nextest { .. } => CARGO_LLVM_COV_NEXTEST_USAGE, + Self::NextestArchive => CARGO_LLVM_COV_NEXTEST_ARCHIVE_USAGE, + Self::Demangle => "", // internal API + } + } - 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), + fn as_str(self) -> &'static str { + match self { + Self::None => "", + Self::Test => "test", + Self::Run => "run", + Self::Report { .. } => "report", + Self::Clean => "clean", + Self::ShowEnv => "show-env", + Self::Nextest { .. } => "nextest", + Self::NextestArchive => "nextest-archive", + Self::Demangle => "demangle", + } + } - 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('r') | Long("release") => parse_flag!(release), - Long("profile") if !subcommand.is_nextest_based() => { - parse_opt!(profile); - } - Long("cargo-profile") if subcommand.is_nextest_based() => { - parse_opt!(profile); - } - Long("target") => parse_opt!(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), - Long("no-clean") => parse_flag!(no_clean), - - // report options - Long("json") => parse_flag!(json), - Long("lcov") => parse_flag!(lcov), - Long("cobertura") => parse_flag!(cobertura), - Long("codecov") => parse_flag!(codecov), - Long("text") => parse_flag!(text), - Long("html") => parse_flag!(html), - Long("open") => parse_flag!(open), - Long("summary-only") => parse_flag!(summary_only), - Long("skip-functions") => parse_flag!(skip_functions), - 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("show-instantiations") => parse_flag!(show_instantiations), - Long("hide-instantiations") => { - // The following warning is a hint, so it should not be promoted to an error. - let _guard = term::warn::ignore(); - warn!("--hide-instantiations is now enabled by default"); - } - 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-functions") => parse_opt!(fail_under_functions), - Long("fail-under-lines") => parse_opt!(fail_under_lines), - Long("fail-under-regions") => parse_opt!(fail_under_regions), - 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), - Long("dep-coverage") => parse_opt!(dep_coverage), + pub(crate) fn call_cargo_nextest(self) -> bool { + matches!(self, Self::Nextest { .. } | Self::NextestArchive) + } + pub(crate) fn read_nextest_archive(self) -> bool { + matches!( + self, + Self::Nextest { archive_file: true } | Self::Report { nextest_archive_file: true } + ) + } +} - // show-env options - Long("export-prefix") => parse_flag!(export_prefix), +impl FromStr for Subcommand { + type Err = Error; - Long("archive-file") if matches!(subcommand, Subcommand::Nextest { .. }) => { - parse_opt_passthrough!(archive_file); - } + fn from_str(s: &str) -> Result { + match s { + "test" | "t" => Ok(Self::Test), + "run" | "r" => Ok(Self::Run), + "report" => Ok(Self::Report { nextest_archive_file: false }), + "clean" => Ok(Self::Clean), + "show-env" => Ok(Self::ShowEnv), + "nextest" => Ok(Self::Nextest { archive_file: false }), + "nextest-archive" => Ok(Self::NextestArchive), + "demangle" => Ok(Self::Demangle), + _ => bail!("unrecognized subcommand {s}"), + } + } +} - Short('v') | Long("verbose") => { - verbose += 1; - after_subcommand = false; - } - Short('h') | Long("help") => { - print!("{}", Subcommand::help_text(subcommand)); - std::process::exit(0); - } - Short('V') | Long("version") => { - if subcommand == Subcommand::None { - println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - std::process::exit(0); - } else { - unexpected("--version", subcommand)?; - } - } +#[derive(Debug, Default)] +pub(crate) struct LlvmCovOptions { + /// 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. + pub(crate) json: bool, + /// 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. + pub(crate) lcov: 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? - Long("target-dir") => unexpected(&format_arg(&arg), subcommand)?, + /// Export coverage data in "cobertura" XML format + /// + /// If --output-path is not specified, the report will be printed to stdout. + /// + /// This internally calls `llvm-cov export -format=lcov` and then converts to cobertura.xml. + /// See for more. + pub(crate) cobertura: bool, - // Handle known options for can_passthrough=false subcommands - Short('Z') => parse_opt_passthrough!(()), - Short('F' | 'j') | Long("features" | "jobs") - if matches!( - subcommand, - Subcommand::None - | Subcommand::Test - | Subcommand::Run - | Subcommand::Nextest { .. } - | Subcommand::NextestArchive - ) => - { - parse_opt_passthrough!(()); - } - Short('q') | Long("quiet") => passthrough!(), - Long( - "all-features" - | "no-default-features" - | "--keep-going" - | "--ignore-rust-version", - ) if matches!( - subcommand, - Subcommand::None - | Subcommand::Test - | Subcommand::Run - | Subcommand::Nextest { .. } - | Subcommand::NextestArchive - ) => - { - passthrough!(); - } + /// Export coverage data in "Codecov Custom Coverage" format + /// + /// If --output-path is not specified, the report will be printed to stdout. + /// + /// This internally calls `llvm-cov export -format=json` and then converts to codecov.json. + /// See for more. + pub(crate) codecov: bool, - // passthrough - Long(_) | Short(_) if Subcommand::can_passthrough(subcommand) => passthrough!(), - Value(val) - if subcommand == Subcommand::None - || Subcommand::can_passthrough(subcommand) => - { - let val = val.into_string().unwrap(); - if subcommand == Subcommand::None { - subcommand = val.parse::()?; - if subcommand == Subcommand::Demangle && args.len() != 1 { - unexpected( - args.iter().find(|&arg| arg != "demangle").unwrap(), - subcommand, - )?; - } - after_subcommand = true; - } else { - if after_subcommand - && matches!(subcommand, Subcommand::Nextest { .. }) - && matches!( - val.as_str(), - // from `cargo nextest --help` - "list" | "run" | "archive" | "show-config" | "self" | "help" - ) - { - // The following warning is a hint, so it should not be promoted to an error. - let _guard = term::warn::ignore(); - warn!( - "note that `{val}` is treated as test filter instead of subcommand \ - because `cargo llvm-cov nextest` internally calls `cargo nextest \ - run`; if you want to use `nextest archive`, please use `cargo llvm-cov nextest-archive`" - ); - } - cargo_args.push(val); - after_subcommand = false; - } - } - _ => unexpected(&format_arg(&arg), subcommand)?, - } - } + /// 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. + pub(crate) text: bool, + /// 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. + pub(crate) html: bool, + /// Generate coverage reports in "html" format and open them in a browser after the operation. + /// + /// See --html for more. + pub(crate) open: bool, - term::set_coloring(&mut color); + /// Export only summary information for each file in the coverage data + /// + /// This flag can only be used together with --json, --lcov, --cobertura, or --codecov. + // If the format flag is not specified, this flag is no-op because the only summary is displayed anyway. + pub(crate) summary_only: bool, - if matches!(subcommand, Subcommand::Nextest { .. }) { - subcommand = Subcommand::Nextest { archive_file: archive_file.is_some() }; - } + /// Specify a file to write coverage data into. + /// + /// This flag can only be used together with --json, --lcov, --cobertura, --codecov, or --text. + /// See --output-dir for --html and --open. + 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. + pub(crate) output_dir: Option, - // unexpected options - match subcommand { - Subcommand::ShowEnv => {} - _ => { - if export_prefix { - unexpected("--export-prefix", subcommand)?; - } - } - } - if doc || doctests { - let flag = if doc { "--doc" } else { "--doctests" }; - match subcommand { - Subcommand::None | Subcommand::Test => {} - Subcommand::ShowEnv | Subcommand::Report if doctests => {} - Subcommand::Nextest { .. } | Subcommand::NextestArchive => { - bail!("doctest is not supported for nextest") - } - _ => unexpected(flag, subcommand)?, - } - } - match subcommand { - Subcommand::None | Subcommand::Nextest { .. } | Subcommand::NextestArchive => {} - Subcommand::Test => { - if no_run { - unexpected("--no-run", subcommand)?; - } - } - _ => { - if lib { - unexpected("--lib", subcommand)?; - } - if bins { - unexpected("--bins", subcommand)?; - } - if examples { - unexpected("--examples", subcommand)?; - } - if !test.is_empty() { - unexpected("--test", subcommand)?; - } - if tests { - unexpected("--tests", subcommand)?; - } - if !bench.is_empty() { - unexpected("--bench", subcommand)?; - } - if benches { - unexpected("--benches", subcommand)?; - } - if all_targets { - unexpected("--all-targets", subcommand)?; - } - if no_run { - unexpected("--no-run", subcommand)?; - } - if no_fail_fast { - unexpected("--no-fail-fast", subcommand)?; - } - if !exclude.is_empty() { - unexpected("--exclude", subcommand)?; - } - if !exclude_from_test.is_empty() { - unexpected("--exclude-from-test", subcommand)?; - } - } - } - match subcommand { - Subcommand::None - | Subcommand::Test - | Subcommand::Run - | Subcommand::Nextest { .. } - | Subcommand::NextestArchive => {} - _ => { - if !bin.is_empty() { - unexpected("--bin", subcommand)?; - } - if !example.is_empty() { - unexpected("--example", subcommand)?; - } - if !exclude_from_report.is_empty() { - unexpected("--exclude-from-report", subcommand)?; - } - if no_report { - unexpected("--no-report", subcommand)?; - } - if no_clean { - unexpected("--no-clean", subcommand)?; - } - if ignore_run_fail { - unexpected("--ignore-run-fail", subcommand)?; - } - } + /// Fail if `any` or `all` profiles cannot be merged (default to `any`) + pub(crate) failure_mode: Option, + /// Skip source code files with file paths that match the given regular expression. + pub(crate) ignore_filename_regex: Option, + // For debugging (unstable) + pub(crate) disable_default_ignore_filename_regex: bool, + /// Show instantiations in report + pub(crate) show_instantiations: bool, + /// Unset cfg(coverage), which is enabled when code is built using cargo-llvm-cov. + pub(crate) no_cfg_coverage: bool, + /// Unset cfg(coverage_nightly), which is enabled when code is built using cargo-llvm-cov and nightly compiler. + pub(crate) no_cfg_coverage_nightly: bool, + /// Run tests, but don't generate coverage report + pub(crate) no_report: bool, + /// Exit with a status of 1 if the total function coverage is less than MIN percent. + pub(crate) fail_under_functions: Option, + /// Exit with a status of 1 if the total line coverage is less than MIN percent. + pub(crate) fail_under_lines: Option, + /// Exit with a status of 1 if the total region coverage is less than MIN percent. + pub(crate) fail_under_regions: Option, + /// Exit with a status of 1 if the uncovered lines are greater than MAX. + pub(crate) fail_uncovered_lines: Option, + /// Exit with a status of 1 if the uncovered regions are greater than MAX. + pub(crate) fail_uncovered_regions: Option, + /// Exit with a status of 1 if the uncovered functions are greater than MAX. + pub(crate) fail_uncovered_functions: Option, + /// Show lines with no coverage. + pub(crate) show_missing_lines: bool, + /// Include build script in coverage report. + pub(crate) include_build_script: bool, + /// Show coverage of th specified dependency instead of the crates in the current workspace. (unstable) + pub(crate) dep_coverage: Option, + /// Skip functions in coverage report. + pub(crate) skip_functions: bool, +} + +impl LlvmCovOptions { + pub(crate) const fn show(&self) -> bool { + self.text || self.html + } +} + +#[derive(Debug, Clone)] +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, +} + +// 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, + /// Require Cargo.lock and cache are up to date + pub(crate) frozen: bool, + /// Require Cargo.lock is up to date + pub(crate) locked: bool, + /// Run without accessing the network + pub(crate) offline: bool, +} + +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"); } - match subcommand { - Subcommand::None - | Subcommand::Test - | Subcommand::Run - | Subcommand::Nextest { .. } - | Subcommand::NextestArchive - | Subcommand::ShowEnv => {} - _ => { - if no_cfg_coverage { - unexpected("--no-cfg-coverage", subcommand)?; - } - if no_cfg_coverage_nightly { - unexpected("--no-cfg-coverage-nightly", subcommand)?; - } - } + if self.locked { + cmd.arg("--locked"); } - match subcommand { - Subcommand::None - | Subcommand::Test - | Subcommand::Nextest { .. } - | Subcommand::NextestArchive - | Subcommand::Clean => {} - _ => { - if workspace { - unexpected("--workspace", subcommand)?; - } - } + if self.offline { + cmd.arg("--offline"); } - // TODO: check more + } +} - // requires - if !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? - if !exclude.is_empty() { - requires("--exclude", &["--workspace"])?; - } - if !exclude_from_test.is_empty() { - requires("--exclude-from-test", &["--workspace"])?; - } - } - if coverage_target_only && target.is_none() { - requires("--coverage-target-only", &["--target"])?; +pub(crate) fn merge_config_to_args( + ws: &crate::cargo::Workspace, + target: &mut Option, + verbose: &mut u8, + color: &mut Option, +) { + // CLI flags are prefer over config values. + if target.is_none() { + target.clone_from(&ws.target_for_cli); + } + if *verbose == 0 { + *verbose = u8::from(ws.config.term.verbose.unwrap_or(false)); + } + if color.is_none() { + *color = ws.config.term.color.map(Into::into); + } +} + +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.70.0/compiler/rustc_driver_impl/src/lib.rs#L1366-L1376 + 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: {arg:?}", i + 1)) + }) } - // conflicts - if no_report && no_run { - conflicts("--no-report", "--no-run")?; + let mut raw_args = handle_args(env::args_os()); + raw_args.next(); // cargo + match raw_args.next().transpose()? { + Some(arg) if arg == SUBCMD => {} + Some(arg) => bail!("expected subcommand '{SUBCMD}', found argument '{arg}'"), + None => bail!("expected subcommand '{SUBCMD}'"), } - if no_report || no_run { - let flag = if no_report { "--no-report" } else { "--no-run" }; - if no_clean { - // --no-report/--no-run implicitly enable --no-clean. - conflicts(flag, "--no-clean")?; + let mut args = vec![]; + for arg in &mut raw_args { + let arg = arg?; + if arg == "--" { + break; } + args.push(arg); } - 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")?; - } - // TODO: handle these mutual exclusions elegantly. - if lcov { - let flag = "--lcov"; - if json { - conflicts(flag, "--json")?; - } - } - if cobertura { - let flag = "--cobertura"; - if json { - conflicts(flag, "--json")?; - } - if lcov { - conflicts(flag, "--lcov")?; - } - if codecov { - conflicts(flag, "--codecov")?; - } - } - if codecov { - let flag = "--codecov"; - if json { - conflicts(flag, "--json")?; - } - if lcov { - conflicts(flag, "--lcov")?; - } - if cobertura { - conflicts(flag, "--cobertura")?; - } - } - if text { - let flag = "--text"; - if json { - conflicts(flag, "--json")?; - } - if lcov { - conflicts(flag, "--lcov")?; - } - if cobertura { - conflicts(flag, "--cobertura")?; - } - if codecov { - conflicts(flag, "--codecov")?; - } - } - if html || open { - let flag = if html { "--html" } else { "--open" }; - if json { - conflicts(flag, "--json")?; - } - if lcov { - conflicts(flag, "--lcov")?; - } - if cobertura { - conflicts(flag, "--cobertura")?; - } - if codecov { - conflicts(flag, "--codecov")?; - } - 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 skip_functions { - let flag = "--skip-functions"; - if html { - conflicts(flag, "--html")?; - } - } - if output_dir.is_some() { - let flag = "--output-dir"; - if json { - conflicts(flag, "--json")?; + let rest = raw_args.collect::>>()?; + + let mut cargo_args = vec![]; + let mut subcommand = Subcommand::None; + let mut after_subcommand = false; + + let mut manifest_path = None; + let mut frozen = false; + let mut locked = false; + let mut offline = false; + 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 cobertura = false; + let mut codecov = 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 show_instantiations = false; + let mut no_cfg_coverage = false; + let mut no_cfg_coverage_nightly = false; + let mut no_report = false; + let mut fail_under_functions = None; + let mut fail_under_lines = None; + let mut fail_under_regions = 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; + let mut dep_coverage = None; + let mut skip_functions = false; + + // build options + let mut release = false; + 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; + let mut no_clean = false; + + // show-env options + let mut export_prefix = false; + + // options ambiguous between nextest-related and others + let mut profile = None; + let mut cargo_profile = None; + let mut archive_file = None; + let mut nextest_archive_file = None; + + let mut parser = lexopt::Parser::from_args(args.clone()); + while let Some(arg) = parser.next()? { + macro_rules! parse_opt { + ($opt:tt $(,)?) => {{ + if Store::is_full(&$opt) { + multi_arg(&arg)?; + } + Store::push(&mut $opt, &parser.value()?.into_string().unwrap())?; + after_subcommand = false; + }}; } - if lcov { - conflicts(flag, "--lcov")?; + macro_rules! parse_opt_passthrough { + ($opt:tt $(,)?) => {{ + if Store::is_full(&$opt) { + multi_arg(&arg)?; + } + match arg { + Long(flag) => { + let flag = format!("--{}", flag); + if let Some(val) = parser.optional_value() { + let val = val.into_string().unwrap(); + Store::push(&mut $opt, &val)?; + cargo_args.push(format!("{}={}", flag, val)); + } else { + let val = parser.value()?.into_string().unwrap(); + Store::push(&mut $opt, &val)?; + cargo_args.push(flag); + cargo_args.push(val); + } + } + Short(flag) => { + if let Some(val) = parser.optional_value() { + let val = val.into_string().unwrap(); + Store::push(&mut $opt, &val)?; + cargo_args.push(format!("-{}{}", flag, val)); + } else { + let val = parser.value()?.into_string().unwrap(); + Store::push(&mut $opt, &val)?; + cargo_args.push(format!("-{}", flag)); + cargo_args.push(val); + } + } + Value(_) => unreachable!(), + } + after_subcommand = false; + }}; } - if cobertura { - conflicts(flag, "--cobertura")?; + macro_rules! parse_flag { + ($flag:ident $(,)?) => {{ + if mem::replace(&mut $flag, true) { + multi_arg(&arg)?; + } + #[allow(unused_assignments)] + { + after_subcommand = false; + } + }}; } - if codecov { - conflicts(flag, "--codecov")?; + macro_rules! parse_flag_passthrough { + ($flag:ident $(,)?) => {{ + parse_flag!($flag); + passthrough!(); + }}; } - if output_path.is_some() { - conflicts(flag, "--output-path")?; + macro_rules! passthrough { + () => {{ + match arg { + Long(flag) => { + let flag = format!("--{}", flag); + if let Some(val) = parser.optional_value() { + cargo_args.push(format!("{}={}", flag, val.string()?)); + } else { + cargo_args.push(flag); + } + } + Short(flag) => { + if let Some(val) = parser.optional_value() { + cargo_args.push(format!("-{}{}", flag, val.string()?)); + } else { + cargo_args.push(format!("-{}", flag)); + } + } + Value(_) => unreachable!(), + } + after_subcommand = false; + }}; } - } - // 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") - } - - if no_run { - // The following warnings should not be promoted to an error. - let _guard = term::warn::ignore(); - warn!("--no-run is deprecated, use `cargo llvm-cov report` subcommand instead"); - } - - // If `-vv` is passed, propagate `-v` to cargo. - if verbose > 1 { - cargo_args.push(format!("-{}", "v".repeat(verbose - 1))); - } - - // For subsequent processing - if no_report || no_run { - // --no-report and --no-run implies --no-clean - no_clean = true; - } - if doc { - // --doc implies --doctests - doctests = true; - } - if no_run { - // --no-run is deprecated alias for report - subcommand = Subcommand::Report; - } - - Ok(Self { - subcommand, - cov: LlvmCovOptions { - json, - lcov, - cobertura, - codecov, - text, - html, - open, - summary_only, - output_path, - output_dir, - failure_mode, - ignore_filename_regex, - disable_default_ignore_filename_regex, - show_instantiations, - no_cfg_coverage, - no_cfg_coverage_nightly, - no_report, - fail_under_functions, - fail_under_lines, - fail_under_regions, - fail_uncovered_lines, - fail_uncovered_regions, - fail_uncovered_functions, - show_missing_lines, - include_build_script, - dep_coverage, - skip_functions, - }, - show_env: ShowEnvOptions { export_prefix }, - doctests, - ignore_run_fail, - lib, - bin, - bins, - example, - examples, - test, - tests, - bench, - benches, - all_targets, - doc, - workspace, - exclude, - exclude_from_test, - exclude_from_report, - release, - profile, - target, - coverage_target_only, - verbose: verbose.try_into().unwrap_or(u8::MAX), - color, - remap_path_prefix, - include_ffi, - no_clean, - manifest: ManifestOptions { manifest_path, frozen, locked, offline }, - archive_file, - cargo_args, - rest, - }) - } -} + match arg { + Long("color") => parse_opt_passthrough!(color), + Long("manifest-path") => parse_opt!(manifest_path), + Long("frozen") => parse_flag_passthrough!(frozen), + Long("locked") => parse_flag_passthrough!(locked), + Long("offline") => parse_flag_passthrough!(offline), -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum Subcommand { - /// Run tests and generate coverage report. - None, + 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), - /// Run tests and generate coverage report. - Test, + 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), - /// Run a binary or example and generate coverage report. - Run, + 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), - /// Generate coverage report. - Report, + // build options + Short('r') | Long("release") => parse_flag!(release), + // ambiguous between nextest-related and others will be handled later + Long("profile") => parse_opt!(profile), + Long("cargo-profile") => parse_opt!(cargo_profile), + Long("target") => parse_opt!(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), + Long("no-clean") => parse_flag!(no_clean), - /// Remove artifacts that cargo-llvm-cov has generated in the past - Clean, + // report options + Long("json") => parse_flag!(json), + Long("lcov") => parse_flag!(lcov), + Long("cobertura") => parse_flag!(cobertura), + Long("codecov") => parse_flag!(codecov), + Long("text") => parse_flag!(text), + Long("html") => parse_flag!(html), + Long("open") => parse_flag!(open), + Long("summary-only") => parse_flag!(summary_only), + Long("skip-functions") => parse_flag!(skip_functions), + 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("show-instantiations") => parse_flag!(show_instantiations), + Long("hide-instantiations") => { + // The following warning is a hint, so it should not be promoted to an error. + let _guard = term::warn::ignore(); + warn!("--hide-instantiations is now enabled by default"); + } + 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-functions") => parse_opt!(fail_under_functions), + Long("fail-under-lines") => parse_opt!(fail_under_lines), + Long("fail-under-regions") => parse_opt!(fail_under_regions), + 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), + Long("dep-coverage") => parse_opt!(dep_coverage), - /// Output the environment set by cargo-llvm-cov to build Rust projects. - ShowEnv, + // show-env options + Long("export-prefix") => parse_flag!(export_prefix), - /// Run tests with cargo nextest - Nextest { - archive_file: bool, - }, + // ambiguous between nextest-related and others will be handled later + Long("archive-file") => parse_opt_passthrough!(archive_file), + Long("nextest-archive-file") => parse_opt!(nextest_archive_file), - /// Build and archive tests with cargo nextest - NextestArchive, + Short('v') | Long("verbose") => { + verbose += 1; + after_subcommand = false; + } + Short('h') | Long("help") => { + print!("{}", Subcommand::help_text(subcommand)); + std::process::exit(0); + } + Short('V') | Long("version") => { + if subcommand == Subcommand::None { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } else { + unexpected("--version", subcommand)?; + } + } - // internal (unstable) - Demangle, -} + // 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? + Long("target-dir") => unexpected(&format_arg(&arg), subcommand)?, -static CARGO_LLVM_COV_USAGE: &str = include_str!("../docs/cargo-llvm-cov.txt"); -static CARGO_LLVM_COV_TEST_USAGE: &str = include_str!("../docs/cargo-llvm-cov-test.txt"); -static CARGO_LLVM_COV_RUN_USAGE: &str = include_str!("../docs/cargo-llvm-cov-run.txt"); -static CARGO_LLVM_COV_REPORT_USAGE: &str = include_str!("../docs/cargo-llvm-cov-report.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"); -static CARGO_LLVM_COV_NEXTEST_ARCHIVE_USAGE: &str = - include_str!("../docs/cargo-llvm-cov-nextest-archive.txt"); + // Handle known options for can_passthrough=false subcommands + Short('Z') => parse_opt_passthrough!(()), + Short('F' | 'j') | Long("features" | "jobs") + if matches!( + subcommand, + Subcommand::None + | Subcommand::Test + | Subcommand::Run + | Subcommand::Nextest { .. } + | Subcommand::NextestArchive + ) => + { + parse_opt_passthrough!(()); + } + Short('q') | Long("quiet") => passthrough!(), + Long( + "all-features" + | "no-default-features" + | "--keep-going" + | "--ignore-rust-version", + ) if matches!( + subcommand, + Subcommand::None + | Subcommand::Test + | Subcommand::Run + | Subcommand::Nextest { .. } + | Subcommand::NextestArchive + ) => + { + passthrough!(); + } -impl Subcommand { - fn can_passthrough(subcommand: Self) -> bool { - matches!(subcommand, Self::Test | Self::Run | Self::Nextest { .. } | Self::NextestArchive) - } + // passthrough + Long(_) | Short(_) if Subcommand::can_passthrough(subcommand) => passthrough!(), + Value(val) + if subcommand == Subcommand::None + || Subcommand::can_passthrough(subcommand) => + { + let val = val.into_string().unwrap(); + if subcommand == Subcommand::None { + subcommand = val.parse::()?; + if subcommand == Subcommand::Demangle && args.len() != 1 { + unexpected( + args.iter().find(|&arg| arg != "demangle").unwrap(), + subcommand, + )?; + } + after_subcommand = true; + } else { + if after_subcommand + && matches!(subcommand, Subcommand::Nextest { .. }) + && matches!( + val.as_str(), + // from `cargo nextest --help` + "list" | "run" | "archive" | "show-config" | "self" | "help" + ) + { + // The following warning is a hint, so it should not be promoted to an error. + let _guard = term::warn::ignore(); + warn!( + "note that `{val}` is treated as test filter instead of subcommand \ + because `cargo llvm-cov nextest` internally calls `cargo nextest \ + run`; if you want to use `nextest archive`, please use `cargo llvm-cov nextest-archive`" + ); + } + cargo_args.push(val); + after_subcommand = false; + } + } + _ => unexpected(&format_arg(&arg), subcommand)?, + } + } - fn help_text(subcommand: Self) -> &'static str { + term::set_coloring(&mut color); + + // unexpected options + match subcommand { + Subcommand::ShowEnv => {} + _ => { + if export_prefix { + unexpected("--export-prefix", subcommand)?; + } + } + } + if doc || doctests { + let flag = if doc { "--doc" } else { "--doctests" }; + match subcommand { + Subcommand::None | Subcommand::Test => {} + Subcommand::ShowEnv | Subcommand::Report { .. } if doctests => {} + Subcommand::Nextest { .. } | Subcommand::NextestArchive => { + bail!("doctest is not supported for nextest") + } + _ => unexpected(flag, subcommand)?, + } + } + match subcommand { + Subcommand::None | Subcommand::Nextest { .. } | Subcommand::NextestArchive => {} + Subcommand::Test => { + if no_run { + unexpected("--no-run", subcommand)?; + } + } + _ => { + if lib { + unexpected("--lib", subcommand)?; + } + if bins { + unexpected("--bins", subcommand)?; + } + if examples { + unexpected("--examples", subcommand)?; + } + if !test.is_empty() { + unexpected("--test", subcommand)?; + } + if tests { + unexpected("--tests", subcommand)?; + } + if !bench.is_empty() { + unexpected("--bench", subcommand)?; + } + if benches { + unexpected("--benches", subcommand)?; + } + if all_targets { + unexpected("--all-targets", subcommand)?; + } + if no_run { + unexpected("--no-run", subcommand)?; + } + if no_fail_fast { + unexpected("--no-fail-fast", subcommand)?; + } + if !exclude.is_empty() { + unexpected("--exclude", subcommand)?; + } + if !exclude_from_test.is_empty() { + unexpected("--exclude-from-test", subcommand)?; + } + } + } match subcommand { - Self::None => CARGO_LLVM_COV_USAGE, - Self::Test => CARGO_LLVM_COV_TEST_USAGE, - Self::Run => CARGO_LLVM_COV_RUN_USAGE, - Self::Report => CARGO_LLVM_COV_REPORT_USAGE, - Self::Clean => CARGO_LLVM_COV_CLEAN_USAGE, - Self::ShowEnv => CARGO_LLVM_COV_SHOW_ENV_USAGE, - Self::Nextest { .. } => CARGO_LLVM_COV_NEXTEST_USAGE, - Self::NextestArchive => CARGO_LLVM_COV_NEXTEST_ARCHIVE_USAGE, - Self::Demangle => "", // internal API + Subcommand::None + | Subcommand::Test + | Subcommand::Run + | Subcommand::Nextest { .. } + | Subcommand::NextestArchive => {} + _ => { + if !bin.is_empty() { + unexpected("--bin", subcommand)?; + } + if !example.is_empty() { + unexpected("--example", subcommand)?; + } + if !exclude_from_report.is_empty() { + unexpected("--exclude-from-report", subcommand)?; + } + if no_report { + unexpected("--no-report", subcommand)?; + } + if no_clean { + unexpected("--no-clean", subcommand)?; + } + if ignore_run_fail { + unexpected("--ignore-run-fail", subcommand)?; + } + } } - } - - fn as_str(self) -> &'static str { - match self { - Self::None => "", - Self::Test => "test", - Self::Run => "run", - Self::Report => "report", - Self::Clean => "clean", - Self::ShowEnv => "show-env", - Self::Nextest { .. } => "nextest", - Self::NextestArchive => "nextest-archive", - Self::Demangle => "demangle", + match subcommand { + Subcommand::None + | Subcommand::Test + | Subcommand::Run + | Subcommand::Nextest { .. } + | Subcommand::NextestArchive + | Subcommand::ShowEnv => {} + _ => { + if no_cfg_coverage { + unexpected("--no-cfg-coverage", subcommand)?; + } + if no_cfg_coverage_nightly { + unexpected("--no-cfg-coverage-nightly", subcommand)?; + } + } } - } - - pub(crate) fn is_nextest_based(self) -> bool { - matches!(self, Self::Nextest { .. } | Self::NextestArchive) - } -} - -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), - "report" => Ok(Self::Report), - "clean" => Ok(Self::Clean), - "show-env" => Ok(Self::ShowEnv), - "nextest" => Ok(Self::Nextest { archive_file: false }), - "nextest-archive" => Ok(Self::NextestArchive), - "demangle" => Ok(Self::Demangle), - _ => bail!("unrecognized subcommand {s}"), + match subcommand { + Subcommand::None + | Subcommand::Test + | Subcommand::Nextest { .. } + | Subcommand::NextestArchive + | Subcommand::Clean => {} + _ => { + if workspace { + unexpected("--workspace", subcommand)?; + } + } } - } -} - -#[derive(Debug, Default)] -pub(crate) struct LlvmCovOptions { - /// 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. - pub(crate) json: bool, - /// 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. - pub(crate) lcov: bool, - - /// Export coverage data in "cobertura" XML format - /// - /// If --output-path is not specified, the report will be printed to stdout. - /// - /// This internally calls `llvm-cov export -format=lcov` and then converts to cobertura.xml. - /// See for more. - pub(crate) cobertura: bool, - - /// Export coverage data in "Codecov Custom Coverage" format - /// - /// If --output-path is not specified, the report will be printed to stdout. - /// - /// This internally calls `llvm-cov export -format=json` and then converts to codecov.json. - /// See for more. - pub(crate) codecov: bool, - - /// 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. - pub(crate) text: bool, - /// 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. - pub(crate) html: bool, - /// Generate coverage reports in "html" format and open them in a browser after the operation. - /// - /// See --html for more. - pub(crate) open: bool, - - /// Export only summary information for each file in the coverage data - /// - /// This flag can only be used together with --json, --lcov, --cobertura, or --codecov. - // If the format flag is not specified, this flag is no-op because the only summary is displayed anyway. - pub(crate) summary_only: bool, + // TODO: check more - /// Specify a file to write coverage data into. - /// - /// This flag can only be used together with --json, --lcov, --cobertura, --codecov, or --text. - /// See --output-dir for --html and --open. - 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. - pub(crate) output_dir: Option, + // requires + if !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? + if !exclude.is_empty() { + requires("--exclude", &["--workspace"])?; + } + if !exclude_from_test.is_empty() { + requires("--exclude-from-test", &["--workspace"])?; + } + } + if coverage_target_only && target.is_none() { + requires("--coverage-target-only", &["--target"])?; + } - /// Fail if `any` or `all` profiles cannot be merged (default to `any`) - pub(crate) failure_mode: Option, - /// Skip source code files with file paths that match the given regular expression. - pub(crate) ignore_filename_regex: Option, - // For debugging (unstable) - pub(crate) disable_default_ignore_filename_regex: bool, - /// Show instantiations in report - pub(crate) show_instantiations: bool, - /// Unset cfg(coverage), which is enabled when code is built using cargo-llvm-cov. - pub(crate) no_cfg_coverage: bool, - /// Unset cfg(coverage_nightly), which is enabled when code is built using cargo-llvm-cov and nightly compiler. - pub(crate) no_cfg_coverage_nightly: bool, - /// Run tests, but don't generate coverage report - pub(crate) no_report: bool, - /// Exit with a status of 1 if the total function coverage is less than MIN percent. - pub(crate) fail_under_functions: Option, - /// Exit with a status of 1 if the total line coverage is less than MIN percent. - pub(crate) fail_under_lines: Option, - /// Exit with a status of 1 if the total region coverage is less than MIN percent. - pub(crate) fail_under_regions: Option, - /// Exit with a status of 1 if the uncovered lines are greater than MAX. - pub(crate) fail_uncovered_lines: Option, - /// Exit with a status of 1 if the uncovered regions are greater than MAX. - pub(crate) fail_uncovered_regions: Option, - /// Exit with a status of 1 if the uncovered functions are greater than MAX. - pub(crate) fail_uncovered_functions: Option, - /// Show lines with no coverage. - pub(crate) show_missing_lines: bool, - /// Include build script in coverage report. - pub(crate) include_build_script: bool, - /// Show coverage of th specified dependency instead of the crates in the current workspace. (unstable) - pub(crate) dep_coverage: Option, - /// Skip functions in coverage report. - pub(crate) skip_functions: bool, -} + // conflicts + if no_report && no_run { + conflicts("--no-report", "--no-run")?; + } + if no_report || no_run { + let flag = if no_report { "--no-report" } else { "--no-run" }; + if no_clean { + // --no-report/--no-run implicitly enable --no-clean. + conflicts(flag, "--no-clean")?; + } + } + 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")?; + } + // TODO: handle these mutual exclusions elegantly. + if lcov { + let flag = "--lcov"; + if json { + conflicts(flag, "--json")?; + } + } + if cobertura { + let flag = "--cobertura"; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if codecov { + conflicts(flag, "--codecov")?; + } + } + if codecov { + let flag = "--codecov"; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if cobertura { + conflicts(flag, "--cobertura")?; + } + } + if text { + let flag = "--text"; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if cobertura { + conflicts(flag, "--cobertura")?; + } + if codecov { + conflicts(flag, "--codecov")?; + } + } + if html || open { + let flag = if html { "--html" } else { "--open" }; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if cobertura { + conflicts(flag, "--cobertura")?; + } + if codecov { + conflicts(flag, "--codecov")?; + } + 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 skip_functions { + let flag = "--skip-functions"; + if html { + conflicts(flag, "--html")?; + } + } + if output_dir.is_some() { + let flag = "--output-dir"; + if json { + conflicts(flag, "--json")?; + } + if lcov { + conflicts(flag, "--lcov")?; + } + if cobertura { + conflicts(flag, "--cobertura")?; + } + if codecov { + conflicts(flag, "--codecov")?; + } + if output_path.is_some() { + conflicts(flag, "--output-path")?; + } + } -impl LlvmCovOptions { - pub(crate) const fn show(&self) -> bool { - self.text || self.html - } -} + // 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") + } -#[derive(Debug, Clone)] -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, -} + if no_run { + // The following warnings should not be promoted to an error. + let _guard = term::warn::ignore(); + warn!("--no-run is deprecated, use `cargo llvm-cov report` subcommand instead"); + } -// 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, - /// Require Cargo.lock and cache are up to date - pub(crate) frozen: bool, - /// Require Cargo.lock is up to date - pub(crate) locked: bool, - /// Run without accessing the network - pub(crate) offline: bool, -} + // If `-vv` is passed, propagate `-v` to cargo. + if verbose > 1 { + cargo_args.push(format!("-{}", "v".repeat(verbose - 1))); + } -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"); + // For subsequent processing + if no_report || no_run { + // --no-report and --no-run implies --no-clean + no_clean = true; } - if self.locked { - cmd.arg("--locked"); + if doc { + // --doc implies --doctests + doctests = true; } - if self.offline { - cmd.arg("--offline"); + if no_run { + // --no-run is deprecated alias for report + subcommand = Subcommand::Report { nextest_archive_file: false }; } - } -} -pub(crate) fn merge_config_to_args( - ws: &crate::cargo::Workspace, - target: &mut Option, - verbose: &mut u8, - color: &mut Option, -) { - // CLI flags are prefer over config values. - if target.is_none() { - target.clone_from(&ws.target_for_cli); - } - if *verbose == 0 { - *verbose = u8::from(ws.config.term.verbose.unwrap_or(false)); - } - if color.is_none() { - *color = ws.config.term.color.map(Into::into); + // nextest-related + if subcommand.call_cargo_nextest() { + if let Some(profile) = profile { + // nextest profile will be propagated + cargo_args.push("--profile".to_owned()); + cargo_args.push(profile); + } + if nextest_archive_file.is_some() { + bail!("'--nextest-archive-file' is report-specific option; did you mean '--archive-file'?"); + } + nextest_archive_file = archive_file; + if let Subcommand::Nextest { archive_file: f } = &mut subcommand { + *f = nextest_archive_file.is_some(); + } + } else { + if cargo_profile.is_some() { + bail!("'--cargo-profile' is nextest-specific option; did you mean '--profile'?"); + } + cargo_profile = profile; + if let Subcommand::Report { nextest_archive_file: f } = &mut subcommand { + if archive_file.is_some() { + bail!("'--archive-file' is nextest-specific option; did you mean '--nextest-archive-file'?"); + } + *f = nextest_archive_file.is_some(); + } else { + if archive_file.is_some() { + bail!("'--archive-file' is nextest-specific option and not supported for this subcommand"); + } + if nextest_archive_file.is_some() { + bail!("'--nextest-archive-file' is report-specific option and not supported for this subcommand"); + } + } + } + + Ok(Self { + subcommand, + cov: LlvmCovOptions { + json, + lcov, + cobertura, + codecov, + text, + html, + open, + summary_only, + output_path, + output_dir, + failure_mode, + ignore_filename_regex, + disable_default_ignore_filename_regex, + show_instantiations, + no_cfg_coverage, + no_cfg_coverage_nightly, + no_report, + fail_under_functions, + fail_under_lines, + fail_under_regions, + fail_uncovered_lines, + fail_uncovered_regions, + fail_uncovered_functions, + show_missing_lines, + include_build_script, + dep_coverage, + skip_functions, + }, + show_env: ShowEnvOptions { export_prefix }, + doctests, + ignore_run_fail, + lib, + bin, + bins, + example, + examples, + test, + tests, + bench, + benches, + all_targets, + doc, + workspace, + exclude, + exclude_from_test, + exclude_from_report, + release, + cargo_profile, + target, + coverage_target_only, + verbose: verbose.try_into().unwrap_or(u8::MAX), + color, + remap_path_prefix, + include_ffi, + no_clean, + manifest: ManifestOptions { manifest_path, frozen, locked, offline }, + nextest_archive_file, + cargo_args, + rest, + }) } } diff --git a/src/context.rs b/src/context.rs index 519ea95c..9ee6ebdc 100644 --- a/src/context.rs +++ b/src/context.rs @@ -77,7 +77,7 @@ impl Context { not be displayed because cargo does not pass RUSTFLAGS to them" ); } - if !matches!(args.subcommand, Subcommand::Report | Subcommand::Clean) + if !matches!(args.subcommand, Subcommand::Report { .. } | Subcommand::Clean) && (!args.cov.no_cfg_coverage || ws.rustc_version.nightly && !args.cov.no_cfg_coverage_nightly) { @@ -101,14 +101,14 @@ impl Context { if args.cov.output_dir.is_none() && args.cov.html { args.cov.output_dir = Some(ws.output_dir.clone()); } - if !matches!(args.subcommand, Subcommand::Report | Subcommand::Clean) + if !matches!(args.subcommand, Subcommand::Report { .. } | Subcommand::Clean) && env::var_os("CARGO_LLVM_COV_SHOW_ENV").is_some() { warn!( "cargo-llvm-cov subcommands other than report and clean may not work correctly \ in context where environment variables are set by show-env; consider using \ normal {} commands", - if args.subcommand.is_nextest_based() { "cargo-nextest" } else { "cargo" } + if args.subcommand.call_cargo_nextest() { "cargo-nextest" } else { "cargo" } ); } diff --git a/src/main.rs b/src/main.rs index 996c65ac..eef98e3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,7 @@ fn try_main() -> Result<()> { set_env(cx, writer, IsNextest(true))?; // Include envs for nextest. writer.set("CARGO_LLVM_COV_TARGET_DIR", cx.ws.metadata.target_directory.as_str())?; } - Subcommand::Report => { + Subcommand::Report { .. } => { let cx = &Context::new(args)?; create_dirs(cx)?; generate_report(cx)?; @@ -733,7 +733,7 @@ fn object_files(cx: &Context) -> Result> { // https://doc.rust-lang.org/nightly/rustc/instrument-coverage.html#tips-for-listing-the-binaries-automatically let mut target_dir = cx.ws.target_dir.clone(); let mut auto_detect_profile = false; - if matches!(cx.args.subcommand, Subcommand::Nextest { archive_file: true }) { + if cx.args.subcommand.read_nextest_archive() { #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct BinariesMetadata { @@ -745,7 +745,7 @@ fn object_files(cx: &Context) -> Result> { base_output_directories: Vec, } target_dir.push("target"); - let archive_file = cx.args.archive_file.as_ref().unwrap(); + let archive_file = cx.args.nextest_archive_file.as_ref().unwrap(); let decoder = ruzstd::StreamingDecoder::new(fs::File::open(archive_file)?)?; let mut archive = Archive::new(decoder); let mut binaries_metadata = vec![]; @@ -771,7 +771,7 @@ fn object_files(cx: &Context) -> Result> { if cx.args.release { info!("--release flag is no longer needed because detection from nextest archive is now supported"); } - if cx.args.profile.is_some() { + if cx.args.cargo_profile.is_some() { info!("--cargo-profile flag is no longer needed because detection from nextest archive is now supported"); } target_dir.push(&binaries_metadata.rust_build_meta.base_output_directories[0]); @@ -789,7 +789,7 @@ fn object_files(cx: &Context) -> Result> { target_dir.push(target); } // https://doc.rust-lang.org/nightly/cargo/reference/profiles.html#custom-profiles - let profile = match cx.args.profile.as_deref() { + let profile = match cx.args.cargo_profile.as_deref() { None if cx.args.release => "release", Some("release" | "bench") => "release", None | Some("dev" | "test") => "debug",