cargo_difftests/
difftest.rs

1/*
2 *        Copyright (c) 2023-2024 Dinu Blanovschi
3 *
4 *    Licensed under the Apache License, Version 2.0 (the "License");
5 *    you may not use this file except in compliance with the License.
6 *    You may obtain a copy of the License at
7 *
8 *        https://www.apache.org/licenses/LICENSE-2.0
9 *
10 *    Unless required by applicable law or agreed to in writing, software
11 *    distributed under the License is distributed on an "AS IS" BASIS,
12 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *    See the License for the specific language governing permissions and
14 *    limitations under the License.
15 */
16
17//! Holds the [`Difftest`] struct and related functions.
18
19use 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/// A single difftest.
32///
33/// Points to the files that were generated by the call to
34/// `cargo_difftests_testclient::init`.
35#[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    /// Get the directory to the difftest.
53    ///
54    /// Note that you should not modify the contents
55    /// of this directory in any way.
56    pub fn dir(&self) -> &Path {
57        &self.dir
58    }
59
60    /// Tests whether the difftest has a test index
61    /// file attached.
62    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    /// Reads the [`TestIndex`] data associated with the [`Difftest`].
85    ///
86    /// Returns `Ok(None)` if the [`Difftest`] does not have an associated index file.
87    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    /// Loads the `self.json` file from the test directory, and parses it into
100    /// a [`CoreTestDesc`] value.
101    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    /// Merges the `.profraw` files into a `.profdata` file, via `llvm-profdata merge`.
113    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    /// Cleans up the profiling data from the [`Difftest`], reducing
130    /// the size on-disk substantially, but all the profiling data
131    /// gets lost in the process.
132    ///
133    /// This is used by the `--index-strategy=always-and-clean` flag, which keeps
134    /// the test index data around, even if the profiling data is deleted.
135    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    /// Checks whether the [`Difftest`] has been cleaned.
159    pub fn was_cleaned(&self) -> bool {
160        self.cleaned
161    }
162
163    /// Checks whether the [`Difftest`] has the `.profdata` file.
164    pub fn has_profdata(&self) -> bool {
165        if self.cleaned {
166            return false;
167        }
168
169        self.profdata_file.is_some()
170    }
171
172    /// Tries to discover a [`Difftest`] from the given directory, with the
173    /// given [`DiscoverIndexPathResolver`] to resolve the index path.
174    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    /// Reads the `.profdata` profiling data file, to be able to use
194    /// it for analysis.
195    ///
196    /// This function should be ran after
197    /// [`Difftest::merge_profraw_files_into_profdata`].
198    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    /// Starts the analysis of the exported `.json` profiling data file.
229    ///
230    /// See the [`AnalysisContext`] type and the [`analysis`](crate::analysis)
231    /// module for how to perform the analysis.
232    ///
233    /// This function should be ran after
234    /// [`Difftest::merge_profraw_files_into_profdata`].
235    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    /// Compiles the exported `.json` profiling data file into a
246    /// [`TestIndex`] object, but does not save it on-disk.
247    ///
248    /// To save it on-disk, use [`TestIndex::write_to_file`].
249    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/// The configuration to use when exporting a `.profdata` file
265/// into a `.json` file.
266#[derive(Clone)]
267pub struct ExportProfdataConfig {
268    /// Whether to ignore files from the cargo registry.
269    pub ignore_registry_files: bool,
270    /// Other binaries to include in the export.
271    ///
272    /// By default, only the test binary is included (via the [`CoreTestDesc`]'s
273    /// `bin_path` field), but if the test has spawned some other child process
274    /// (as often happens when testing binaries), and they
275    /// were profiled, the paths to those binaries should
276    /// be passed here.
277    pub other_binaries: Vec<PathBuf>,
278}
279
280/// A resolver for test index data file paths.
281///
282/// # Examples
283///
284/// ```
285/// # use std::path::{Path, PathBuf};
286/// # use cargo_difftests::difftest::DiscoverIndexPathResolver;
287///
288/// let resolver = DiscoverIndexPathResolver::Remap {from: "foo".into(), to: "bar".into()};
289///
290/// assert_eq!(resolver.resolve(Path::new("foo/bar/baz")), Some(PathBuf::from("bar/bar/baz")));
291/// assert_eq!(resolver.resolve(Path::new("bar/baz")), None);
292/// ```
293pub enum DiscoverIndexPathResolver {
294    /// Remaps the index path from the given `from` path to the given `to` path.
295    Remap {
296        /// The path to strip from the index path.
297        from: PathBuf,
298        /// The path to append to the stripped index path.
299        to: PathBuf,
300    },
301    /// A custom remapping function.
302    Custom {
303        /// The remapping function.
304        f: Box<dyn Fn(&Path) -> Option<PathBuf>>,
305    },
306}
307
308impl DiscoverIndexPathResolver {
309    /// Resolves the index path from the given [`Difftest`] directory.
310    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
476/// Discovers all the [`Difftest`]s in the given directory,
477/// optionally using the `index_resolver` to resolve the index paths,
478/// and ignoring incompatible [`Difftest`] directories if `ignore_incompatible`
479/// is true.
480pub 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}