main.rs
  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  }