main.rs raw

   1  // Copyright (c) The Bitcoin Core developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or https://opensource.org/license/mit/.
   4  
   5  use std::env;
   6  use std::fs::File;
   7  use std::path::{Path, PathBuf};
   8  use std::process::{Command, ExitCode};
   9  use std::str;
  10  
  11  /// A type for a complete and readable error message.
  12  type AppError = String;
  13  type AppResult = Result<(), AppError>;
  14  
  15  const LLVM_PROFDATA: &str = "llvm-profdata";
  16  const LLVM_COV: &str = "llvm-cov";
  17  const GIT: &str = "git";
  18  
  19  fn exit_help(err: &str) -> AppError {
  20      format!(
  21          r#"
  22  Error: {err}
  23  
  24  Usage: program ./build_dir boost_unittest_filter
  25  
  26  Refer to the devtools/README.md for more details."#
  27      )
  28  }
  29  
  30  fn sanity_check(test_exe: &Path) -> AppResult {
  31      for tool in [LLVM_PROFDATA, LLVM_COV, GIT] {
  32          let output = Command::new(tool).arg("--help").output();
  33          match output {
  34              Ok(output) if output.status.success() => {}
  35              _ => Err(exit_help(&format!("The tool {tool} is not installed")))?,
  36          }
  37      }
  38      if !test_exe.exists() {
  39          Err(exit_help(&format!(
  40              "Test executable ({}) not found",
  41              test_exe.display()
  42          )))?;
  43      }
  44      Ok(())
  45  }
  46  
  47  fn app() -> AppResult {
  48      // Parse args
  49      let args = env::args().collect::<Vec<_>>();
  50      let build_dir = args.get(1).ok_or(exit_help("Must set build dir"))?;
  51      if build_dir == "--help" {
  52          Err(exit_help("--help requested"))?;
  53      }
  54      let filter = args
  55          .get(2)
  56          // Require filter for now. In the future it could be optional and the tool could provide a
  57          // default filter.
  58          .ok_or(exit_help("Must set boost test filter"))?;
  59      if args.get(3).is_some() {
  60          Err(exit_help("Too many args"))?;
  61      }
  62  
  63      let build_dir = Path::new(build_dir);
  64      let test_exe = build_dir.join("bin/test_bitcoin");
  65  
  66      sanity_check(&test_exe)?;
  67  
  68      deterministic_coverage(build_dir, &test_exe, filter)
  69  }
  70  
  71  fn deterministic_coverage(build_dir: &Path, test_exe: &Path, filter: &str) -> AppResult {
  72      let profraw_file = build_dir.join("test_det_cov.profraw");
  73      let profdata_file = build_dir.join("test_det_cov.profdata");
  74      let run_single = |run_id: char| -> Result<PathBuf, AppError> {
  75          println!("Run with id {run_id}");
  76          let cov_txt_path = build_dir.join(format!("test_det_cov.show.{run_id}.txt"));
  77          if !Command::new(test_exe)
  78              .env("LLVM_PROFILE_FILE", &profraw_file)
  79              .env("BOOST_TEST_RUN_FILTERS", filter)
  80              .env("RANDOM_CTX_SEED", "21")
  81              .status()
  82              .map_err(|e| format!("test failed with {e}"))?
  83              .success()
  84          {
  85              Err("test failed".to_string())?;
  86          }
  87          if !Command::new(LLVM_PROFDATA)
  88              .arg("merge")
  89              .arg("--sparse")
  90              .arg(&profraw_file)
  91              .arg("-o")
  92              .arg(&profdata_file)
  93              .status()
  94              .map_err(|e| format!("{LLVM_PROFDATA} merge failed with {e}"))?
  95              .success()
  96          {
  97              Err(format!("{LLVM_PROFDATA} merge failed. This can be a sign of compiling without code coverage support."))?;
  98          }
  99          let cov_file = File::create(&cov_txt_path)
 100              .map_err(|e| format!("Failed to create coverage txt file ({e})"))?;
 101          if !Command::new(LLVM_COV)
 102              .args([
 103                  "show",
 104                  "--show-line-counts-or-regions",
 105                  "--show-branches=count",
 106                  "--show-expansions",
 107                  "--show-instantiation-summary",
 108                  "-Xdemangler=llvm-cxxfilt",
 109                  &format!("--instr-profile={}", profdata_file.display()),
 110              ])
 111              .arg(test_exe)
 112              .stdout(cov_file)
 113              .status()
 114              .map_err(|e| format!("{LLVM_COV} show failed with {e}"))?
 115              .success()
 116          {
 117              Err(format!("{LLVM_COV} show failed"))?;
 118          }
 119          Ok(cov_txt_path)
 120      };
 121      let check_diff = |a: &Path, b: &Path| -> AppResult {
 122          let same = Command::new(GIT)
 123              .args(["--no-pager", "diff", "--no-index"])
 124              .arg(a)
 125              .arg(b)
 126              .status()
 127              .map_err(|e| format!("{GIT} diff failed with {e}"))?
 128              .success();
 129          if !same {
 130              Err("The coverage was not deterministic between runs.".to_string())?;
 131          }
 132          Ok(())
 133      };
 134      let r0 = run_single('a')?;
 135      let r1 = run_single('b')?;
 136      check_diff(&r0, &r1)?;
 137      println!("✨ The coverage was deterministic across two runs. ✨");
 138      Ok(())
 139  }
 140  
 141  fn main() -> ExitCode {
 142      match app() {
 143          Ok(()) => ExitCode::SUCCESS,
 144          Err(err) => {
 145              eprintln!("⚠️\n{err}");
 146              ExitCode::FAILURE
 147          }
 148      }
 149  }
 150