1use std::ffi::OsStr;
20use std::fs;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23
24use cargo_difftests_core::CoreTestDesc;
25use log::{debug, info, warn};
26
27use crate::analysis::AnalysisContext;
28use crate::index_data::{IndexDataCompilerConfig, TestIndex};
29use crate::{analysis_data, DifftestsError, DifftestsResult};
30
31#[derive(serde::Serialize, serde::Deserialize, Debug)]
36pub struct Difftest {
37 pub(crate) dir: PathBuf,
38 pub(crate) test_binary_path: PathBuf,
39 pub(crate) test_name_path: PathBuf,
40 pub(crate) profraws: Vec<PathBuf>,
41 pub(crate) self_json: Option<PathBuf>,
42 pub(crate) test_run_time: std::time::SystemTime,
43 pub(crate) profdata_file: Option<PathBuf>,
44 pub(crate) index_data: Option<PathBuf>,
45
46 pub(crate) cleaned: bool,
47}
48
49impl Difftest {
50 const CLEANED_FILE_NAME: &'static str = "cargo_difftests_cleaned";
51
52 pub fn dir(&self) -> &Path {
57 &self.dir
58 }
59
60 pub fn has_index(&self) -> bool {
63 self.index_data.is_some()
64 }
65
66 fn read_test_binary_path(&self) -> DifftestsResult<PathBuf> {
67 let s = fs::read_to_string(&self.test_binary_path)?;
68 Ok(PathBuf::from(s))
69 }
70
71 pub fn test_info(&self) -> DifftestsResult<TestInfo> {
72 let test_name = std::fs::read_to_string(&self.test_name_path)?;
73 let test_binary = self.read_test_binary_path()?;
74
75 let extra_desc = self.load_test_desc()?;
76
77 Ok(TestInfo {
78 test_name,
79 test_binary,
80 extra_desc,
81 })
82 }
83
84 pub fn read_index_data(&self) -> DifftestsResult<Option<TestIndex>> {
88 let Some(index_data) = &self.index_data else {
89 return Ok(None);
90 };
91
92 TestIndex::read_from_file(index_data).map(Some)
93 }
94
95 pub(crate) fn test_run_time(&self) -> std::time::SystemTime {
96 self.test_run_time
97 }
98
99 pub fn load_test_desc(&self) -> DifftestsResult<Option<CoreTestDesc>> {
102 if let Some(self_json) = self.self_json.as_ref() {
103 let s = fs::read_to_string(self_json)?;
104 let desc = serde_json::from_str(&s)
105 .map_err(|e| DifftestsError::Json(e, Some(self_json.clone())))?;
106 Ok(desc)
107 } else {
108 Ok(None)
109 }
110 }
111
112 pub fn merge_profraw_files_into_profdata(&mut self, force: bool) -> DifftestsResult<()> {
114 if self.cleaned {
115 return Err(DifftestsError::DifftestCleaned);
116 }
117
118 if self.profdata_file.is_some() && !force {
119 return Ok(());
120 }
121
122 merge_profraws(self)?;
123
124 self.profdata_file = Some(self.out_profdata_path());
125
126 Ok(())
127 }
128
129 pub fn clean(&mut self) -> DifftestsResult<()> {
136 fn clean_file(f: &mut Option<PathBuf>) -> DifftestsResult<()> {
137 if let Some(f) = f {
138 fs::remove_file(f)?;
139 }
140
141 *f = None;
142 Ok(())
143 }
144
145 clean_file(&mut self.profdata_file)?;
146
147 for profraw in self.profraws.drain(..) {
148 fs::remove_file(profraw)?;
149 }
150
151 fs::write(self.dir.join(Self::CLEANED_FILE_NAME), b"")?;
152
153 self.cleaned = true;
154
155 Ok(())
156 }
157
158 pub fn was_cleaned(&self) -> bool {
160 self.cleaned
161 }
162
163 pub fn has_profdata(&self) -> bool {
165 if self.cleaned {
166 return false;
167 }
168
169 self.profdata_file.is_some()
170 }
171
172 pub fn discover_from(
175 dir: PathBuf,
176 index_resolver: Option<&DiscoverIndexPathResolver>,
177 ) -> DifftestsResult<Self> {
178 let test_name_path = dir.join(cargo_difftests_core::CARGO_DIFFTESTS_TEST_NAME_FILENAME);
179
180 if !test_name_path.exists() {
181 return Err(DifftestsError::IO(std::io::Error::new(
182 std::io::ErrorKind::NotFound,
183 format!(
184 "test name file does not exist: {}",
185 test_name_path.display()
186 ),
187 )));
188 }
189
190 discover_difftest_from_tempdir(dir, test_name_path, index_resolver)
191 }
192
193 pub fn export_profdata(
199 &self,
200 config: ExportProfdataConfig,
201 ) -> DifftestsResult<analysis_data::CoverageData> {
202 assert!(self.has_profdata());
203
204 let ExportProfdataConfig {
205 ignore_registry_files,
206 mut other_binaries,
207 } = config;
208
209 for other_binary in &mut other_binaries {
210 if !other_binary.is_absolute() {
211 use path_absolutize::Absolutize;
212 *other_binary = other_binary.absolutize()?.into_owned();
213 }
214 }
215
216 let r = export_profdata_file(
217 &ProfdataExportableWrapper {
218 difftest: self,
219 other_bins: other_binaries,
220 },
221 ignore_registry_files,
222 ExportProfdataAction::Read,
223 )?;
224 let r = r.coverage_data();
225 Ok(r)
226 }
227
228 pub fn start_analysis(
236 &mut self,
237 config: ExportProfdataConfig,
238 ) -> DifftestsResult<AnalysisContext<'_>> {
239 info!("Starting analysis...");
240 let profdata = self.export_profdata(config)?;
241
242 Ok(AnalysisContext::new(self, profdata))
243 }
244
245 pub fn compile_test_index_data(
250 &self,
251 config: ExportProfdataConfig,
252 index_data_compiler_config: IndexDataCompilerConfig,
253 ) -> DifftestsResult<TestIndex> {
254 info!("Compiling test index data...");
255
256 let profdata = self.export_profdata(config)?;
257 let test_index_data = TestIndex::index(self, profdata, index_data_compiler_config)?;
258
259 info!("Done compiling test index data.");
260 Ok(test_index_data)
261 }
262}
263
264#[derive(Clone)]
267pub struct ExportProfdataConfig {
268 pub ignore_registry_files: bool,
270 pub other_binaries: Vec<PathBuf>,
278}
279
280pub enum DiscoverIndexPathResolver {
294 Remap {
296 from: PathBuf,
298 to: PathBuf,
300 },
301 Custom {
303 f: Box<dyn Fn(&Path) -> Option<PathBuf>>,
305 },
306}
307
308impl DiscoverIndexPathResolver {
309 pub fn resolve(&self, p: &Path) -> Option<PathBuf> {
311 match self {
312 DiscoverIndexPathResolver::Remap { from, to } => {
313 let p = p.strip_prefix(from).ok()?;
314 Some(to.join(p))
315 }
316 DiscoverIndexPathResolver::Custom { f } => f(p),
317 }
318 }
319}
320
321fn discover_difftest_from_tempdir(
322 dir: PathBuf,
323 test_name_path: PathBuf,
324 index_resolver: Option<&DiscoverIndexPathResolver>,
325) -> DifftestsResult<Difftest> {
326 let self_json = dir.join(cargo_difftests_core::CARGO_DIFFTESTS_SELF_JSON_FILENAME);
327
328 let self_json = self_json.exists().then_some(self_json);
329
330 if !test_name_path.exists() {
331 return Err(DifftestsError::IO(std::io::Error::new(
332 std::io::ErrorKind::NotFound,
333 format!(
334 "test name file does not exist: {}",
335 test_name_path.display()
336 ),
337 )));
338 }
339
340 let cargo_difftests_version = dir.join(cargo_difftests_core::CARGO_DIFFTESTS_VERSION_FILENAME);
341
342 if !cargo_difftests_version.exists() {
343 return Err(DifftestsError::CargoDifftestsVersionDoesNotExist(
344 cargo_difftests_version,
345 ));
346 }
347
348 let version = fs::read_to_string(&cargo_difftests_version)?;
349
350 if version != env!("CARGO_PKG_VERSION") {
351 return Err(DifftestsError::CargoDifftestsVersionMismatch(
352 version,
353 env!("CARGO_PKG_VERSION").to_owned(),
354 ));
355 }
356
357 let test_binary_path = dir.join(cargo_difftests_core::CARGO_DIFFTESTS_TEST_BINARY_FILENAME);
358
359 if !test_binary_path.exists() {
360 return Err(DifftestsError::IO(std::io::Error::new(
361 std::io::ErrorKind::NotFound,
362 format!(
363 "test binary file does not exist: {}",
364 test_binary_path.display()
365 ),
366 )));
367 }
368
369 let test_run = test_binary_path.metadata()?.modified()?;
370
371 let mut profraws = Vec::new();
372
373 let mut profdata_file = None;
374
375 let mut cleaned = false;
376
377 for e in dir.read_dir()? {
378 let e = e?;
379 let p = e.path();
380
381 if !p.is_file() {
382 continue;
383 }
384
385 let file_name = p.file_name();
386 let ext = p.extension();
387
388 if ext == Some(OsStr::new("profraw")) {
389 profraws.push(p);
390 continue;
391 }
392
393 if ext == Some(OsStr::new("profdata")) {
394 if profdata_file.is_none() {
395 profdata_file = Some(p);
396 } else {
397 warn!(
398 "multiple profdata files found in difftest directory: {}",
399 dir.display()
400 );
401 warn!("ignoring: {}", p.display());
402 }
403 continue;
404 }
405
406 if file_name == Some(OsStr::new(Difftest::CLEANED_FILE_NAME)) {
407 cleaned = true;
408 }
409 }
410
411 let index_data = 'index_data: {
412 let index_data = index_resolver.and_then(|resolver| resolver.resolve(&dir));
413
414 if let Some(ind) = &index_data {
415 if !ind.exists() {
416 debug!("index data file does not exist: {}", ind.display());
417 break 'index_data None;
418 } else if !ind.is_file() {
419 debug!("index data file is not a file: {}", ind.display());
420 break 'index_data None;
421 }
422
423 if ind.metadata()?.modified()? < test_run {
424 warn!("index data file is older than test run");
425 break 'index_data None;
426 }
427 }
428
429 index_data
430 };
431
432 Ok(Difftest {
433 dir,
434 test_binary_path,
435 test_name_path,
436 profraws,
437 self_json,
438 test_run_time: test_run,
439 profdata_file,
440 index_data,
441 cleaned,
442 })
443}
444
445fn discover_difftests_to_vec(
446 dir: &Path,
447 discovered: &mut Vec<Difftest>,
448 ignore_incompatible: bool,
449 index_resolver: Option<&DiscoverIndexPathResolver>,
450) -> DifftestsResult {
451 let test_name_path = dir.join(cargo_difftests_core::CARGO_DIFFTESTS_TEST_NAME_FILENAME);
452 if test_name_path.exists() && test_name_path.is_file() {
453 let r = discover_difftest_from_tempdir(dir.to_path_buf(), test_name_path, index_resolver);
454
455 if let Err(DifftestsError::CargoDifftestsVersionMismatch(_, _)) = r {
456 if ignore_incompatible {
457 return Ok(());
458 }
459 }
460
461 discovered.push(r?);
462 return Ok(());
463 }
464
465 for entry in fs::read_dir(dir)? {
466 let entry = entry?;
467 let path = entry.path();
468 if path.is_dir() {
469 discover_difftests_to_vec(&path, discovered, ignore_incompatible, index_resolver)?;
470 }
471 }
472
473 Ok(())
474}
475
476pub fn discover_difftests(
481 dir: &Path,
482 ignore_incompatible: bool,
483 index_resolver: Option<&DiscoverIndexPathResolver>,
484) -> DifftestsResult<Vec<Difftest>> {
485 let mut discovered = Vec::new();
486
487 discover_difftests_to_vec(dir, &mut discovered, ignore_incompatible, index_resolver)?;
488
489 Ok(discovered)
490}
491
492pub trait ProfrawsMergeable {
493 fn list_profraws(&self) -> impl Iterator<Item = &Path>;
494 fn out_profdata_path(&self) -> PathBuf;
495}
496
497impl ProfrawsMergeable for Difftest {
498 fn list_profraws(&self) -> impl Iterator<Item = &Path> {
499 self.profraws.iter().map(PathBuf::as_path)
500 }
501
502 fn out_profdata_path(&self) -> PathBuf {
503 const OUT_FILE_NAME: &str = "merged.profdata";
504
505 self.dir.join(OUT_FILE_NAME)
506 }
507}
508
509pub fn merge_profraws<T: ProfrawsMergeable>(d: &T) -> DifftestsResult<()> {
510 debug!(
511 "Merging profraw files into {}...",
512 d.out_profdata_path().display()
513 );
514
515 let p = d.out_profdata_path();
516
517 let mut cmd = Command::new("rust-profdata");
518
519 cmd.arg("merge")
520 .arg("-sparse")
521 .args(d.list_profraws())
522 .arg("-o")
523 .arg(&p);
524
525 let status = cmd.status()?;
526
527 if !status.success() {
528 return Err(DifftestsError::ProcessFailed {
529 name: "rust-profdata",
530 });
531 }
532
533 Ok(())
534}
535
536pub trait ProfDataExportable {
537 fn profdata_path(&self) -> &Path;
538 fn main_bin_path(&self) -> DifftestsResult<PathBuf>;
539 fn other_bins(&self) -> impl Iterator<Item = &Path>;
540}
541
542struct ProfdataExportableWrapper<'a> {
543 difftest: &'a Difftest,
544 other_bins: Vec<PathBuf>,
545}
546
547impl<'a> ProfDataExportable for ProfdataExportableWrapper<'a> {
548 fn profdata_path(&self) -> &Path {
549 self.difftest.profdata_file.as_ref().unwrap()
550 }
551
552 fn main_bin_path(&self) -> DifftestsResult<PathBuf> {
553 self.difftest.read_test_binary_path()
554 }
555
556 fn other_bins(&self) -> impl Iterator<Item = &Path> {
557 self.other_bins.iter().map(PathBuf::as_path)
558 }
559}
560
561#[derive(Debug)]
562pub enum ExportProfdataAction {
563 Store(PathBuf),
564 Read,
565}
566
567#[derive(Debug)]
568pub enum ExportProfdataActionResult {
569 Store,
570 Read(analysis_data::CoverageData),
571}
572
573impl ExportProfdataActionResult {
574 pub fn coverage_data(self) -> analysis_data::CoverageData {
575 match self {
576 ExportProfdataActionResult::Store => unreachable!(),
577 ExportProfdataActionResult::Read(r) => r,
578 }
579 }
580}
581
582pub fn export_profdata_file(
583 d: &impl ProfDataExportable,
584 ignore_registry_files: bool,
585 action: ExportProfdataAction,
586) -> DifftestsResult<ExportProfdataActionResult> {
587 debug!(
588 "Exporting profdata file from {}...",
589 d.profdata_path().display()
590 );
591
592 let mut cmd = Command::new("rust-cov");
593
594 cmd.arg("export")
595 .arg("-instr-profile")
596 .arg(d.profdata_path())
597 .arg(d.main_bin_path()?)
598 .args(
599 d.other_bins()
600 .flat_map(|it| [OsStr::new("--object"), it.as_os_str()]),
601 );
602
603 #[cfg(not(windows))]
604 const REGISTRY_FILES_REGEX: &str = r#"/\.cargo/registry"#;
605
606 #[cfg(windows)]
607 const REGISTRY_FILES_REGEX: &str = r#"\\\.cargo\\registry"#;
608
609 if ignore_registry_files {
610 cmd.arg("-ignore-filename-regex").arg(REGISTRY_FILES_REGEX);
611 }
612
613 match &action {
614 ExportProfdataAction::Store(p) => {
615 cmd.stdout(fs::File::create(p)?);
616 }
617 ExportProfdataAction::Read => {
618 cmd.stdout(std::process::Stdio::piped());
619 }
620 }
621
622 debug!("Running: {:?}", cmd);
623
624 let mut process = cmd.spawn()?;
625
626 let r = match &action {
627 ExportProfdataAction::Store(_p) => {
628 let status = process.wait()?;
629
630 if !status.success() {
631 return Err(DifftestsError::ProcessFailed { name: "rust-cov" });
632 }
633
634 ExportProfdataActionResult::Store
635 }
636 ExportProfdataAction::Read => {
637 let r = {
638 let stdout = process.stdout.as_mut().unwrap();
639 let stdout = std::io::BufReader::new(stdout);
640
641 serde_json::from_reader(stdout)
642 .map_err(|e| DifftestsError::Json(e, Some(d.profdata_path().to_path_buf())))?
643 };
644
645 let status = process.wait()?;
646
647 if !status.success() {
648 return Err(DifftestsError::ProcessFailed { name: "rust-cov" });
649 }
650
651 ExportProfdataActionResult::Read(r)
652 }
653 };
654
655 Ok(r)
656}
657
658#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
659pub struct TestInfo {
660 pub test_name: String,
661 pub test_binary: PathBuf,
662
663 pub extra_desc: Option<CoreTestDesc>,
664}