use std::{ffi::OsString, mem, str::FromStr};
use anyhow::{bail, format_err, Error, Result};
use camino::{Utf8Path, Utf8PathBuf};
use lexopt::{
Arg::{Long, Short, Value},
ValueExt,
};
use crate::{
env,
process::ProcessBuilder,
term::{self, Coloring},
};
#[derive(Debug)]
pub(crate) struct Args {
pub(crate) subcommand: Subcommand,
pub(crate) cov: LlvmCovOptions,
pub(crate) show_env: ShowEnvOptions,
pub(crate) doctests: bool,
pub(crate) ignore_run_fail: bool,
pub(crate) lib: bool,
pub(crate) bin: Vec<String>,
pub(crate) bins: bool,
pub(crate) example: Vec<String>,
pub(crate) examples: bool,
pub(crate) test: Vec<String>,
pub(crate) tests: bool,
pub(crate) bench: Vec<String>,
pub(crate) benches: bool,
pub(crate) all_targets: bool,
pub(crate) doc: bool,
pub(crate) workspace: bool,
pub(crate) exclude: Vec<String>,
pub(crate) exclude_from_test: Vec<String>,
pub(crate) exclude_from_report: Vec<String>,
pub(crate) release: bool,
pub(crate) profile: Option<String>,
pub(crate) target: Option<String>,
pub(crate) coverage_target_only: bool,
pub(crate) verbose: u8,
pub(crate) color: Option<Coloring>,
pub(crate) remap_path_prefix: bool,
pub(crate) include_ffi: bool,
pub(crate) no_clean: bool,
pub(crate) manifest: ManifestOptions,
pub(crate) cargo_args: Vec<String>,
pub(crate) rest: Vec<String>,
}
impl Args {
#[allow(clippy::collapsible_if)]
pub(crate) fn parse() -> Result<Self> {
const SUBCMD: &str = "llvm-cov";
fn handle_args(
args: impl IntoIterator<Item = impl Into<OsString>>,
) -> impl Iterator<Item = Result<String>> {
args.into_iter().enumerate().map(|(i, arg)| {
arg.into()
.into_string()
.map_err(|arg| format_err!("argument {} is not valid Unicode: {arg:?}", i + 1))
})
}
let mut raw_args = handle_args(env::args_os());
raw_args.next(); 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::<Result<Vec<_>>>()?;
let mut cargo_args = vec![];
let mut subcommand = Subcommand::None;
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<String> = vec![];
let mut workspace = false;
let mut exclude = vec![];
let mut exclude_from_test = vec![];
let mut exclude_from_report = vec![];
let mut json = false;
let mut lcov = false;
let mut cobertura = 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;
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;
let mut export_prefix = false;
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())?;
}};
}
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!(),
}
}};
}
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::<String>()?));
} else {
cargo_args.push(flag);
}
}
Short(flag) => {
if let Some(val) = parser.optional_value() {
cargo_args.push(format!("-{}{}", flag, val.parse::<String>()?));
} else {
cargo_args.push(format!("-{}", flag));
}
}
Value(_) => unreachable!(),
}
};
}
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),
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),
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),
Long("no-clean") => parse_flag!(no_clean),
Long("json") => parse_flag!(json),
Long("lcov") => parse_flag!(lcov),
Long("cobertura") => parse_flag!(cobertura),
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),
Long("export-prefix") => parse_flag!(export_prefix),
Short('v') | Long("verbose") => verbose += 1,
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)?;
}
}
Long("target-dir") => unexpected(&format_arg(&arg), subcommand)?,
Short('Z') => {
parse_opt_passthrough!(());
}
Short('F' | 'j') | Long("features" | "jobs")
if matches!(
subcommand,
Subcommand::None | Subcommand::Test | Subcommand::Run | Subcommand::Nextest
) =>
{
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
) =>
{
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::<Subcommand>()?;
if subcommand == Subcommand::Demangle {
if args.len() != 1 {
unexpected(
args.iter().find(|&arg| arg != "demangle").unwrap(),
subcommand,
)?;
}
}
} else {
cargo_args.push(val);
}
}
_ => unexpected(&format_arg(&arg), subcommand)?,
}
}
term::set_coloring(&mut color);
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::Nextest => bail!("doctest is not supported for nextest"),
_ => unexpected(flag, subcommand)?,
}
}
match subcommand {
Subcommand::None | Subcommand::Nextest => {}
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 => {
if ignore_run_fail {
bail!("--ignore-run-fail is not supported for nextest");
}
}
_ => {
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_cfg_coverage {
unexpected("--no-cfg-coverage", subcommand)?;
}
if no_cfg_coverage_nightly {
unexpected("--no-cfg-coverage-nightly", subcommand)?;
}
if no_report {
unexpected("--no-report", subcommand)?;
}
if no_clean {
unexpected("--no-clean", subcommand)?;
}
if ignore_run_fail {
unexpected("--ignore-run-fail", subcommand)?;
}
}
}
match subcommand {
Subcommand::None | Subcommand::Test | Subcommand::Nextest | Subcommand::Clean => {}
_ => {
if workspace {
unexpected("--workspace", subcommand)?;
}
}
}
if !workspace {
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"])?;
}
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 {
conflicts(flag, "--no-clean")?;
}
}
if ignore_run_fail && 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 {
conflicts("--package", "--workspace")?;
}
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 text {
let flag = "--text";
if json {
conflicts(flag, "--json")?;
}
if lcov {
conflicts(flag, "--lcov")?;
}
if cobertura {
conflicts(flag, "--cobertura")?;
}
}
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 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 cobertura {
conflicts(flag, "--cobertura")?;
}
if output_path.is_some() {
conflicts(flag, "--output-path")?;
}
}
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 {
let _guard = term::warn::ignore();
warn!("--no-run is deprecated, use `cargo llvm-cov report` subcommand instead");
}
if verbose > 1 {
cargo_args.push(format!("-{}", "v".repeat(verbose - 1)));
}
if no_report || no_run {
no_clean = true;
}
if doc {
doctests = true;
}
if no_run {
subcommand = Subcommand::Report;
}
Ok(Self {
subcommand,
cov: LlvmCovOptions {
json,
lcov,
cobertura,
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,
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 },
cargo_args,
rest,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Subcommand {
None,
Test,
Run,
Report,
Clean,
ShowEnv,
Nextest,
Demangle,
}
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");
impl Subcommand {
fn can_passthrough(subcommand: Self) -> bool {
matches!(subcommand, Self::Test | Self::Run | Self::Nextest)
}
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::Demangle => "", }
}
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::Demangle => "demangle",
}
}
}
impl FromStr for Subcommand {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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),
"demangle" => Ok(Self::Demangle),
_ => bail!("unrecognized subcommand {s}"),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct LlvmCovOptions {
pub(crate) json: bool,
pub(crate) lcov: bool,
pub(crate) cobertura: bool,
pub(crate) text: bool,
pub(crate) html: bool,
pub(crate) open: bool,
pub(crate) summary_only: bool,
pub(crate) output_path: Option<Utf8PathBuf>,
pub(crate) output_dir: Option<Utf8PathBuf>,
pub(crate) failure_mode: Option<String>,
pub(crate) ignore_filename_regex: Option<String>,
pub(crate) disable_default_ignore_filename_regex: bool,
pub(crate) hide_instantiations: bool,
pub(crate) no_cfg_coverage: bool,
pub(crate) no_cfg_coverage_nightly: bool,
pub(crate) no_report: bool,
pub(crate) fail_under_lines: Option<f64>,
pub(crate) fail_uncovered_lines: Option<u64>,
pub(crate) fail_uncovered_regions: Option<u64>,
pub(crate) fail_uncovered_functions: Option<u64>,
pub(crate) show_missing_lines: bool,
pub(crate) include_build_script: bool,
}
impl LlvmCovOptions {
pub(crate) const fn show(&self) -> bool {
self.text || self.html
}
}
#[derive(Debug, Clone)]
pub(crate) struct ShowEnvOptions {
pub(crate) export_prefix: bool,
}
#[derive(Debug, Default)]
pub(crate) struct ManifestOptions {
pub(crate) manifest_path: Option<Utf8PathBuf>,
pub(crate) frozen: bool,
pub(crate) locked: bool,
pub(crate) offline: bool,
}
impl ManifestOptions {
pub(crate) fn cargo_args(&self, cmd: &mut ProcessBuilder) {
if self.frozen {
cmd.arg("--frozen");
}
if self.locked {
cmd.arg("--locked");
}
if self.offline {
cmd.arg("--offline");
}
}
}
pub(crate) fn merge_config_to_args(
ws: &crate::cargo::Workspace,
target: &mut Option<String>,
verbose: &mut u8,
color: &mut Option<Coloring>,
) {
if target.is_none() {
*target = ws.target_for_cli.clone();
}
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);
}
}
trait Store<T> {
fn is_full(&self) -> bool {
false
}
fn push(&mut self, val: &str) -> Result<()>;
}
impl Store<OsString> for () {
fn push(&mut self, _: &str) -> Result<()> {
Ok(())
}
}
impl<T: FromStr> Store<T> for Option<T>
where
Error: From<T::Err>,
{
fn is_full(&self) -> bool {
self.is_some()
}
fn push(&mut self, val: &str) -> Result<()> {
*self = Some(val.parse()?);
Ok(())
}
}
impl<T: FromStr> Store<T> for Vec<T>
where
Error: From<T::Err>,
{
fn push(&mut self, val: &str) -> Result<()> {
self.push(val.parse()?);
Ok(())
}
}
fn format_arg(arg: &lexopt::Arg<'_>) -> String {
match arg {
Long(flag) => format!("--{flag}"),
Short(flag) => format!("-{flag}"),
Value(val) => val.parse().unwrap(),
}
}
#[cold]
#[inline(never)]
fn multi_arg(flag: &lexopt::Arg<'_>) -> Result<()> {
let flag = &format_arg(flag);
bail!("the argument '{flag}' was provided more than once, but cannot be used multiple times");
}
#[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!("{flag} can only be used together with {with}");
}
#[cold]
#[inline(never)]
fn conflicts(a: &str, b: &str) -> Result<()> {
bail!("{a} may not be used together with {b}");
}
#[cold]
#[inline(never)]
fn unexpected(arg: &str, subcommand: Subcommand) -> Result<()> {
if arg.starts_with('-') && !arg.starts_with("---") && arg != "--" {
if subcommand == Subcommand::None {
bail!("invalid option '{arg}'");
}
bail!("invalid option '{arg}' for subcommand '{}'", subcommand.as_str());
}
Err(lexopt::Error::UnexpectedArgument(arg.into()).into())
}
#[cfg(test)]
mod tests {
use std::{
env,
io::Write,
path::Path,
process::{Command, Stdio},
};
use anyhow::Result;
use fs_err as fs;
use super::*;
#[track_caller]
fn assert_diff(expected_path: impl AsRef<Path>, actual: impl AsRef<str>) {
let actual = actual.as_ref();
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let manifest_dir =
manifest_dir.strip_prefix(env::current_dir().unwrap()).unwrap_or(manifest_dir);
let expected_path = &manifest_dir.join(expected_path);
if !expected_path.is_file() {
fs::write(expected_path, "").unwrap();
}
let expected = fs::read_to_string(expected_path).unwrap();
if expected != actual {
if env::var_os("CI").is_some() {
let mut child = Command::new("git")
.args(["--no-pager", "diff", "--no-index", "--"])
.arg(expected_path)
.arg("-")
.stdin(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(actual.as_bytes()).unwrap();
assert!(!child.wait().unwrap().success());
panic!("assertion failed; please run test locally and commit resulting changes, or apply above diff as patch");
} else {
fs::write(expected_path, actual).unwrap();
}
}
}
#[test]
fn update_readme() -> Result<()> {
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());
let mut lines = base.lines();
let mut start = false;
let mut end = false;
while let Some(line) = lines.next() {
out.push_str(line);
out.push('\n');
if line == "<!-- readme-long-help:start -->" {
start = true;
out.push_str("```console\n");
out.push_str("$ cargo llvm-cov --help\n");
out.push_str(new);
for line in &mut lines {
if line == "<!-- readme-long-help:end -->" {
out.push_str("```\n");
out.push_str(line);
out.push('\n');
end = true;
break;
}
}
}
}
if start && end {
assert_diff(path, out);
} else if start {
panic!("missing `<!-- readme-long-help:end -->` comment in README.md");
} else {
panic!("missing `<!-- readme-long-help:start -->` comment in README.md");
}
Ok(())
}
}