use std::mem;
use camino::Utf8PathBuf;
use clap::{AppSettings, ArgSettings, Parser};
use serde::Deserialize;
use crate::process::ProcessBuilder;
const ABOUT: &str =
"Cargo subcommand to easily use LLVM source-based code coverage (-Z instrument-coverage).
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)
)]
pub(crate) struct Args {
#[clap(subcommand)]
pub(crate) subcommand: Option<Subcommand>,
#[clap(flatten)]
cov: LlvmCovOptions,
#[clap(long)]
pub(crate) doctests: bool,
#[clap(long, conflicts_with = "no-report")]
pub(crate) no_run: bool,
#[clap(long)]
pub(crate) no_fail_fast: bool,
#[clap(short, long, conflicts_with = "verbose")]
pub(crate) quiet: bool,
#[clap(long, conflicts_with = "doc", conflicts_with = "doctests")]
pub(crate) lib: bool,
#[clap(
long,
multiple_occurrences = true,
value_name = "NAME",
conflicts_with = "doc",
conflicts_with = "doctests"
)]
pub(crate) bin: Vec<String>,
#[clap(long, conflicts_with = "doc", conflicts_with = "doctests")]
pub(crate) bins: bool,
#[clap(
long,
multiple_occurrences = true,
value_name = "NAME",
conflicts_with = "doc",
conflicts_with = "doctests"
)]
pub(crate) example: Vec<String>,
#[clap(long, conflicts_with = "doc", conflicts_with = "doctests")]
pub(crate) examples: bool,
#[clap(
long,
multiple_occurrences = true,
value_name = "NAME",
conflicts_with = "doc",
conflicts_with = "doctests"
)]
pub(crate) test: Vec<String>,
#[clap(long, conflicts_with = "doc", conflicts_with = "doctests")]
pub(crate) tests: bool,
#[clap(
long,
multiple_occurrences = true,
value_name = "NAME",
conflicts_with = "doc",
conflicts_with = "doctests"
)]
pub(crate) bench: Vec<String>,
#[clap(long, conflicts_with = "doc", conflicts_with = "doctests")]
pub(crate) benches: bool,
#[clap(long, conflicts_with = "doc", conflicts_with = "doctests")]
pub(crate) all_targets: bool,
#[clap(long)]
pub(crate) doc: bool,
#[clap(
short,
long,
multiple_occurrences = true,
value_name = "SPEC",
conflicts_with = "workspace"
)]
pub(crate) package: Vec<String>,
#[clap(long, visible_alias = "all")]
pub(crate) workspace: bool,
#[clap(long, multiple_occurrences = true, value_name = "SPEC", requires = "workspace")]
pub(crate) exclude: Vec<String>,
#[clap(flatten)]
build: BuildOptions,
#[clap(flatten)]
manifest: ManifestOptions,
#[clap(short = 'Z', multiple_occurrences = true, value_name = "FLAG")]
pub(crate) unstable_flags: Vec<String>,
#[clap(last = true)]
pub(crate) args: Vec<String>,
}
impl Args {
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)
}
}
#[derive(Debug, Parser)]
pub(crate) enum Subcommand {
#[clap(
bin_name = "cargo llvm-cov run",
max_term_width = MAX_TERM_WIDTH,
setting = AppSettings::DeriveDisplayOrder,
)]
Run(Box<RunOptions>),
#[clap(
bin_name = "cargo llvm-cov clean",
max_term_width = MAX_TERM_WIDTH,
setting = AppSettings::DeriveDisplayOrder,
)]
Clean(CleanOptions),
#[clap(
bin_name = "cargo llvm-cov demangle",
max_term_width = MAX_TERM_WIDTH,
setting = AppSettings::DeriveDisplayOrder,
setting = AppSettings::Hidden,
)]
Demangle,
}
#[derive(Debug, Default, Parser)]
pub(crate) struct LlvmCovOptions {
#[clap(long)]
pub(crate) json: bool,
#[clap(long, conflicts_with = "json")]
pub(crate) lcov: bool,
#[clap(long, conflicts_with = "json", conflicts_with = "lcov")]
pub(crate) text: bool,
#[clap(long, conflicts_with = "json", conflicts_with = "lcov", conflicts_with = "text")]
pub(crate) html: bool,
#[clap(long, conflicts_with = "json", conflicts_with = "lcov", conflicts_with = "text")]
pub(crate) open: bool,
#[clap(long, conflicts_with = "text", conflicts_with = "html", conflicts_with = "open")]
pub(crate) summary_only: bool,
#[clap(
long,
value_name = "PATH",
conflicts_with = "html",
conflicts_with = "open",
setting(ArgSettings::ForbidEmptyValues)
)]
pub(crate) output_path: Option<Utf8PathBuf>,
#[clap(
long,
value_name = "DIRECTORY",
conflicts_with = "json",
conflicts_with = "lcov",
conflicts_with = "output-path",
setting(ArgSettings::ForbidEmptyValues)
)]
pub(crate) output_dir: Option<Utf8PathBuf>,
#[clap(long, value_name = "any|all", possible_values(&["any", "all"]), hide_possible_values = true)]
pub(crate) failure_mode: Option<String>,
#[clap(long, value_name = "PATTERN", setting(ArgSettings::ForbidEmptyValues))]
pub(crate) ignore_filename_regex: Option<String>,
#[clap(long, hidden = true)]
pub(crate) disable_default_ignore_filename_regex: bool,
#[clap(long, hidden = true)]
pub(crate) hide_instantiations: bool,
#[clap(long, hidden = true)]
pub(crate) no_cfg_coverage: bool,
#[clap(long)]
pub(crate) no_report: bool,
}
impl LlvmCovOptions {
pub(crate) fn show(&self) -> bool {
self.text || self.html
}
}
#[derive(Debug, Default, Parser)]
pub(crate) struct BuildOptions {
#[clap(short, long, value_name = "N")]
pub(crate) jobs: Option<u32>,
#[clap(long)]
pub(crate) release: bool,
#[clap(long, value_name = "PROFILE-NAME")]
pub(crate) profile: Option<String>,
#[clap(long, multiple_occurrences = true, value_name = "FEATURES")]
pub(crate) features: Vec<String>,
#[clap(long)]
pub(crate) all_features: bool,
#[clap(long)]
pub(crate) no_default_features: bool,
#[clap(long, value_name = "TRIPLE")]
pub(crate) target: Option<String>,
#[clap(short, long, parse(from_occurrences))]
pub(crate) verbose: u8,
#[clap(long, arg_enum, value_name = "WHEN")]
pub(crate) color: Option<Coloring>,
}
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 self.verbose > 1 {
cmd.arg(format!("-{}", "v".repeat(self.verbose as usize - 1)));
}
}
}
#[derive(Debug, Parser)]
pub(crate) struct RunOptions {
#[clap(flatten)]
cov: LlvmCovOptions,
#[clap(short, long, conflicts_with = "verbose")]
pub(crate) quiet: bool,
#[clap(long, multiple_occurrences = true, value_name = "NAME")]
pub(crate) bin: Vec<String>,
#[clap(long, multiple_occurrences = true, value_name = "NAME")]
pub(crate) example: Vec<String>,
#[clap(short, long, value_name = "SPEC")]
pub(crate) package: Option<String>,
#[clap(flatten)]
build: BuildOptions,
#[clap(flatten)]
manifest: ManifestOptions,
#[clap(short = 'Z', multiple_occurrences = true, value_name = "FLAG")]
pub(crate) unstable_flags: Vec<String>,
#[clap(last = true)]
pub(crate) args: Vec<String>,
}
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)
}
}
#[derive(Debug, Parser)]
pub(crate) struct CleanOptions {
#[clap(long)]
pub(crate) workspace: bool,
#[clap(short, long, parse(from_occurrences))]
pub(crate) verbose: u8,
#[clap(long, arg_enum, value_name = "WHEN")]
pub(crate) color: Option<Coloring>,
#[clap(flatten)]
pub(crate) manifest: ManifestOptions,
}
#[derive(Debug, Default, Parser)]
pub(crate) struct ManifestOptions {
#[clap(long, value_name = "PATH")]
pub(crate) manifest_path: Option<Utf8PathBuf>,
#[clap(long)]
pub(crate) frozen: bool,
#[clap(long)]
pub(crate) locked: bool,
#[clap(long)]
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");
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, clap::ArgEnum)]
#[serde(rename_all = "kebab-case")]
#[repr(u8)]
pub(crate) enum Coloring {
Auto = 0,
Always,
Never,
}
impl Coloring {
pub(crate) fn cargo_color(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Always => "always",
Self::Never => "never",
}
}
}
#[cfg(test)]
mod tests {
use std::{env, panic, path::Path, process::Command};
use anyhow::Result;
use clap::{IntoApp, Parser};
use fs_err as fs;
use tempfile::Builder;
use super::{Args, Opts, MAX_TERM_WIDTH};
#[cfg(unix)]
#[test]
fn non_utf8_arg() {
use std::{ffi::OsStr, os::unix::prelude::OsStrExt};
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();
}
#[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"]);
}
#[test]
fn empty_value() {
let forbidden = &[
"--output-path",
"--output-dir",
"--ignore-filename-regex",
];
let allowed = &[
"--bin",
"--example",
"--test",
"--bench",
"--package",
"--exclude",
"--profile",
"--features",
"--target",
"--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<String> {
let mut buf = vec![];
if long {
Args::into_app().term_width(MAX_TERM_WIDTH).write_long_help(&mut buf)?;
} else {
Args::into_app().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)
}
#[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 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 outdir = Builder::new().prefix("assert_diff").tempdir().unwrap();
let actual_path = &outdir.path().join(expected_path.file_name().unwrap());
fs::write(actual_path, actual).unwrap();
let status = Command::new("git")
.args(["--no-pager", "diff", "--no-index", "--"])
.args([expected_path, actual_path])
.status()
.unwrap();
assert!(!status.success());
panic!("assertion failed");
} else {
fs::write(expected_path, actual).unwrap();
}
}
}
#[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 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 {
fs::write(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(())
}
}