Repository: taiki-e/cargo-llvm-cov (rust)

Issue:
`-failure-mode` option?
Would you consider a PR to pass a `-failure-mode` option to `llvm-profdata merge`?
https://llvm.org/docs/CommandGuide/llvm-profdata.html#cmdoption-llvm-profdata-merge-failure-mode

Pull request description:
Implement `--failure-mode` option
Fixes #90.
I modified the existing merge test. I hope that was okay.
I am open to changing anything in this PR, no matter how small.

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 03302cc..f531043 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,6 +43,7 @@ clap_derive = "=3.0.0-beta.4"
 
 [dev-dependencies]
 easy-ext = "1"
+itertools = "0.10.1"
 once_cell = "1"
 rand = "0.8"
 tempfile = "3"
diff --git a/README.md b/README.md
index dec9f3d..1b6f024 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,9 @@ OPTIONS:
             This flag can only be used together with --text, --html, or --open. See also --output-
             path.
 
+        --failure-mode <any|all>
+            Fail if `any` or `all` profiles cannot be merged (default to `any`)
+
         --ignore-filename-regex <PATTERN>
             Skip source code files with file paths that match the given regular expression
 
diff --git a/src/cli.rs b/src/cli.rs
index 853af21..2ade104 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -279,6 +279,9 @@ pub(crate) struct LlvmCovOptions {
     )]
     pub(crate) output_dir: Option<Utf8PathBuf>,
 
+    /// Fail if `any` or `all` profiles cannot be merged (default to `any`)
+    #[clap(long, value_name = "any|all", possible_values(&["any", "all"]), hide_possible_values = true)]
+    pub(crate) failure_mode: Option<String>,
     /// Skip source code files with file paths that match the given regular expression.
     #[clap(long, value_name = "PATTERN", setting(ArgSettings::ForbidEmptyValues))]
     pub(crate) ignore_filename_regex: Option<String>,
diff --git a/src/main.rs b/src/main.rs
index 92ecd6d..fc7126c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -264,6 +264,9 @@ fn merge_profraw(cx: &Context) -> Result<()> {
         )
         .arg("-o")
         .arg(&cx.ws.profdata_file);
+    if let Some(mode) = &cx.cov.failure_mode {
+        cmd.arg(format!("-failure-mode={}", mode));
+    }
     if let Some(jobs) = cx.build.jobs {
         cmd.arg(format!("-num-threads={}", jobs));
     }
diff --git a/src/process.rs b/src/process.rs
index 567500d..65c7cac 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -210,6 +210,14 @@ impl fmt::Display for ProcessBuilder {
 struct ProcessError {
     /// A detailed description to show to the user why the process failed.
     desc: String,
+    /// The exit status of the process.
+    ///
+    /// This can be `None` if the process failed to launch (like process not found).
+    status: Option<ExitStatus>,
+    /// The output from the process.
+    ///
+    /// This can be `None` if the process failed to launch, or the output was not captured.
+    output: Option<Output>,
 }
 
 impl ProcessError {
@@ -241,7 +249,7 @@ impl ProcessError {
             }
         }
 
-        Self { desc }
+        Self { desc, status, output: output.cloned() }
     }
 }
 


Test patch:
diff --git a/tests/auxiliary/mod.rs b/tests/auxiliary/mod.rs
index 5d7f86e..f4885d4 100644
--- a/tests/auxiliary/mod.rs
+++ b/tests/auxiliary/mod.rs
@@ -1,7 +1,12 @@
 mod json;
 
 use std::{
+    convert::TryInto,
     env,
+    ffi::OsStr,
+    io::{Read, Seek, Write},
+    mem::size_of,
+    path::{Path, PathBuf},
     process::{Command, ExitStatus},
     sync::atomic::{AtomicUsize, Ordering::Relaxed},
 };
@@ -114,6 +119,46 @@ pub fn test_project(model: &str, name: &str) -> Result<TempDir> {
     Ok(tmpdir)
 }
 
+pub fn perturb_one_header(workspace_root: &Path) -> Result<Option<PathBuf>> {
+    let target_dir = workspace_root.join("target").join("llvm-cov-target");
+    let read_dir = fs::read_dir(target_dir)?;
+    let path = itertools::process_results(read_dir, |mut iter| {
+        iter.find_map(|entry| {
+            let path = entry.path();
+            if path.extension() == Some(OsStr::new("profraw")) {
+                Some(path)
+            } else {
+                None
+            }
+        })
+    })?;
+    path.as_ref().map(perturb_header).transpose()?;
+    Ok(path)
+}
+
+const INSTR_PROF_RAW_MAGIC_64: u64 = (255_u64) << 56
+    | ('l' as u64) << 48
+    | ('p' as u64) << 40
+    | ('r' as u64) << 32
+    | ('o' as u64) << 24
+    | ('f' as u64) << 16
+    | ('r' as u64) << 8
+    | (129_u64);
+
+fn perturb_header<P: AsRef<Path>>(path: P) -> Result<()> {
+    let mut file = fs::OpenOptions::new().read(true).write(true).open(path.as_ref())?;
+    let mut magic = {
+        let mut buf = vec![0_u8; size_of::<u64>()];
+        file.read_exact(&mut buf)?;
+        u64::from_ne_bytes(buf.try_into().unwrap())
+    };
+    assert_eq!(magic, INSTR_PROF_RAW_MAGIC_64);
+    magic += 1;
+    file.rewind()?;
+    file.write_all(&magic.to_ne_bytes())?;
+    Ok(())
+}
+
 #[ext(CommandExt)]
 impl Command {
     #[track_caller]
diff --git a/tests/long-help.txt b/tests/long-help.txt
index 5d434bd..6c26845 100644
--- a/tests/long-help.txt
+++ b/tests/long-help.txt
@@ -74,6 +74,9 @@ OPTIONS:
             This flag can only be used together with --text, --html, or --open. See also --output-
             path.
 
+        --failure-mode <any|all>
+            Fail if `any` or `all` profiles cannot be merged (default to `any`)
+
         --ignore-filename-regex <PATTERN>
             Skip source code files with file paths that match the given regular expression
 
diff --git a/tests/short-help.txt b/tests/short-help.txt
index 2490741..c0f9fcf 100644
--- a/tests/short-help.txt
+++ b/tests/short-help.txt
@@ -42,6 +42,9 @@ OPTIONS:
         --output-dir <DIRECTORY>
             Specify a directory to write coverage report into (default to `target/llvm-cov`)
 
+        --failure-mode <any|all>
+            Fail if `any` or `all` profiles cannot be merged (default to `any`)
+
         --ignore-filename-regex <PATTERN>
             Skip source code files with file paths that match the given regular expression
 
diff --git a/tests/test.rs b/tests/test.rs
index deee9ae..048dbfb 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -5,8 +5,10 @@
 mod auxiliary;
 
 use anyhow::Context as _;
-use auxiliary::{cargo_llvm_cov, test_project, test_report, CommandExt};
+use auxiliary::{cargo_llvm_cov, perturb_one_header, test_project, test_report, CommandExt};
+use camino::Utf8Path;
 use fs_err as fs;
+use tempfile::tempdir;
 
 fn test_set() -> Vec<(&'static str, &'static [&'static str])> {
     vec![
@@ -114,8 +116,19 @@ fn no_coverage() {
 
 #[test]
 fn merge() {
+    let output_dir = auxiliary::FIXTURES_PATH.join("coverage-reports").join("merge");
+    merge_with_failure_mode(&output_dir, false);
+}
+
+#[test]
+fn merge_failure_mode_all() {
+    let tempdir = tempdir().unwrap();
+    let output_dir = Utf8Path::from_path(tempdir.path()).unwrap();
+    merge_with_failure_mode(output_dir, true);
+}
+
+fn merge_with_failure_mode(output_dir: &Utf8Path, failure_mode_all: bool) {
     let model = "merge";
-    let output_dir = auxiliary::FIXTURES_PATH.join("coverage-reports").join(model);
     fs::create_dir_all(&output_dir).unwrap();
     for (extension, args) in test_set() {
         let workspace_root = test_project(model, model).unwrap();
@@ -128,15 +141,23 @@ fn merge() {
             .args(["--color", "never", "--no-report", "--features", "b"])
             .current_dir(workspace_root.path())
             .assert_success();
-        cargo_llvm_cov()
-            .args(["--color", "never", "--no-run", "--output-path"])
+        let mut cmd = cargo_llvm_cov();
+        cmd.args(["--color", "never", "--no-run", "--output-path"])
             .arg(output_path)
             .args(args)
-            .current_dir(workspace_root.path())
-            .assert_success();
-
-        auxiliary::normalize_output(output_path, args).unwrap();
-        auxiliary::assert_output(output_path).unwrap();
+            .current_dir(workspace_root.path());
+        cmd.assert_success();
+
+        if failure_mode_all {
+            perturb_one_header(workspace_root.path()).unwrap().unwrap();
+            cmd.assert_failure()
+                .stderr_contains("unrecognized instrumentation profile encoding format");
+            cmd.args(&["--failure-mode", "all"]);
+            cmd.assert_success();
+        } else {
+            auxiliary::normalize_output(output_path, args).unwrap();
+            auxiliary::assert_output(output_path).unwrap();
+        }
     }
 }