nextest_runner/reuse_build/
mod.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Reuse builds performed earlier.
5//!
6//! Nextest allows users to reuse builds done on one machine. This module contains support for that.
7//!
8//! The main data structures here are [`ReuseBuildInfo`] and [`PathMapper`].
9
10use crate::{
11    errors::{
12        ArchiveExtractError, ArchiveReadError, MetadataMaterializeError, PathMapperConstructError,
13        PathMapperConstructKind,
14    },
15    list::BinaryList,
16    platform::PlatformLibdir,
17};
18use camino::{Utf8Path, Utf8PathBuf};
19use camino_tempfile::Utf8TempDir;
20use guppy::graph::PackageGraph;
21use nextest_metadata::{BinaryListSummary, PlatformLibdirUnavailable};
22use std::{fmt, fs, io, sync::Arc};
23use tracing::debug;
24
25mod archive_reporter;
26mod archiver;
27mod unarchiver;
28
29pub use archive_reporter::*;
30pub use archiver::*;
31pub use unarchiver::*;
32
33/// The name of the file in which Cargo metadata is stored.
34pub const CARGO_METADATA_FILE_NAME: &str = "target/nextest/cargo-metadata.json";
35
36/// The name of the file in which binaries metadata is stored.
37pub const BINARIES_METADATA_FILE_NAME: &str = "target/nextest/binaries-metadata.json";
38
39/// The name of the directory in which libdirs are stored.
40pub const LIBDIRS_BASE_DIR: &str = "target/nextest/libdirs";
41
42/// Reuse build information.
43#[derive(Debug, Default)]
44pub struct ReuseBuildInfo {
45    /// Cargo metadata and remapping for the target directory.
46    pub cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
47
48    /// Binaries metadata JSON and remapping for the target directory.
49    pub binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
50
51    /// Remapping for the build directory.
52    ///
53    /// When `build.build-dir` is set, the build directory differs from the
54    /// target directory. This remap handles that case. When not set, the build
55    /// directory remap falls back to the target directory remap.
56    pub build_dir_remap: Option<Utf8PathBuf>,
57
58    /// A remapper for libdirs.
59    pub libdir_mapper: LibdirMapper,
60
61    /// Optional temporary directory used for cleanup.
62    _temp_dir: Option<Utf8TempDir>,
63}
64
65impl ReuseBuildInfo {
66    /// Creates a new [`ReuseBuildInfo`] from the given cargo and binaries metadata information.
67    pub fn new(
68        cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
69        binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
70        build_dir_remap: Option<Utf8PathBuf>,
71        // TODO: accept libdir_mapper as an argument, as well
72    ) -> Self {
73        Self {
74            cargo_metadata,
75            binaries_metadata,
76            build_dir_remap,
77            libdir_mapper: LibdirMapper::default(),
78            _temp_dir: None,
79        }
80    }
81
82    /// Extracts an archive and constructs a [`ReuseBuildInfo`] from it.
83    pub fn extract_archive<F>(
84        archive_file: &Utf8Path,
85        format: ArchiveFormat,
86        dest: ExtractDestination,
87        callback: F,
88        workspace_remap: Option<&Utf8Path>,
89    ) -> Result<Self, ArchiveExtractError>
90    where
91        F: for<'e> FnMut(ArchiveEvent<'e>) -> io::Result<()>,
92    {
93        let mut file = fs::File::open(archive_file)
94            .map_err(|err| ArchiveExtractError::Read(ArchiveReadError::Io(err)))?;
95
96        let mut unarchiver = Unarchiver::new(&mut file, format);
97        let ExtractInfo {
98            dest_dir,
99            temp_dir,
100            binary_list,
101            cargo_metadata_json,
102            graph,
103            libdir_mapper,
104        } = unarchiver.extract(dest, callback)?;
105
106        let cargo_metadata = MetadataWithRemap {
107            metadata: ReusedCargoMetadata::new((cargo_metadata_json, graph)),
108            remap: workspace_remap.map(|p| p.to_owned()),
109        };
110        let binaries_metadata = MetadataWithRemap {
111            metadata: ReusedBinaryList::new(binary_list),
112            remap: Some(dest_dir.join("target")),
113        };
114
115        Ok(Self {
116            cargo_metadata: Some(cargo_metadata),
117            binaries_metadata: Some(binaries_metadata),
118            // Archives normalize build_directory == target_directory, so no
119            // separate build dir remap is needed.
120            build_dir_remap: None,
121            libdir_mapper,
122            _temp_dir: temp_dir,
123        })
124    }
125
126    /// Returns the Cargo metadata.
127    pub fn cargo_metadata(&self) -> Option<&ReusedCargoMetadata> {
128        self.cargo_metadata.as_ref().map(|m| &m.metadata)
129    }
130
131    /// Returns the binaries metadata, reading it from disk if necessary.
132    pub fn binaries_metadata(&self) -> Option<&ReusedBinaryList> {
133        self.binaries_metadata.as_ref().map(|m| &m.metadata)
134    }
135
136    /// Returns true if any component of the build is being reused.
137    #[inline]
138    pub fn is_active(&self) -> bool {
139        self.cargo_metadata.is_some() || self.binaries_metadata.is_some()
140    }
141
142    /// Returns the new workspace directory.
143    pub fn workspace_remap(&self) -> Option<&Utf8Path> {
144        self.cargo_metadata
145            .as_ref()
146            .and_then(|m| m.remap.as_deref())
147    }
148
149    /// Returns the new target directory.
150    pub fn target_dir_remap(&self) -> Option<&Utf8Path> {
151        self.binaries_metadata
152            .as_ref()
153            .and_then(|m| m.remap.as_deref())
154    }
155
156    /// Returns the new build directory.
157    pub fn build_dir_remap(&self) -> Option<&Utf8Path> {
158        self.build_dir_remap.as_deref()
159    }
160}
161
162/// Metadata as either deserialized contents or a path, along with a possible directory remap.
163#[derive(Clone, Debug)]
164pub struct MetadataWithRemap<T> {
165    /// The metadata.
166    pub metadata: T,
167
168    /// The remapped directory.
169    pub remap: Option<Utf8PathBuf>,
170}
171
172/// Type parameter for [`MetadataWithRemap`].
173pub trait MetadataKind: Clone + fmt::Debug {
174    /// The type of metadata stored.
175    type MetadataType: Sized;
176
177    /// Constructs a new [`MetadataKind`] from the given metadata.
178    fn new(metadata: Self::MetadataType) -> Self;
179
180    /// Reads a path, resolving it into this data type.
181    fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError>;
182}
183
184/// [`MetadataKind`] for a [`BinaryList`].
185#[derive(Clone, Debug)]
186pub struct ReusedBinaryList {
187    /// The binary list.
188    pub binary_list: Arc<BinaryList>,
189}
190
191impl MetadataKind for ReusedBinaryList {
192    type MetadataType = BinaryList;
193
194    fn new(binary_list: Self::MetadataType) -> Self {
195        Self {
196            binary_list: Arc::new(binary_list),
197        }
198    }
199
200    fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
201        // Three steps: read the contents, turn it into a summary, and then turn it into a
202        // BinaryList.
203        //
204        // Buffering the contents in memory is generally much faster than trying to read it
205        // using a BufReader.
206        let contents =
207            fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
208                path: path.to_owned(),
209                error,
210            })?;
211
212        let summary: BinaryListSummary = serde_json::from_str(&contents).map_err(|error| {
213            MetadataMaterializeError::Deserialize {
214                path: path.to_owned(),
215                error,
216            }
217        })?;
218
219        let binary_list = BinaryList::from_summary(summary).map_err(|error| {
220            MetadataMaterializeError::RustBuildMeta {
221                path: path.to_owned(),
222                error,
223            }
224        })?;
225
226        Ok(Self::new(binary_list))
227    }
228}
229
230/// [`MetadataKind`] for Cargo metadata.
231#[derive(Clone, Debug)]
232pub struct ReusedCargoMetadata {
233    /// Cargo metadata JSON.
234    pub json: Arc<String>,
235
236    /// The package graph.
237    pub graph: Arc<PackageGraph>,
238}
239
240impl MetadataKind for ReusedCargoMetadata {
241    type MetadataType = (String, PackageGraph);
242
243    fn new((json, graph): Self::MetadataType) -> Self {
244        Self {
245            json: Arc::new(json),
246            graph: Arc::new(graph),
247        }
248    }
249
250    fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
251        // Read the contents into memory, then parse them as a `PackageGraph`.
252        let json =
253            std::fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
254                path: path.to_owned(),
255                error,
256            })?;
257        let graph = PackageGraph::from_json(&json).map_err(|error| {
258            MetadataMaterializeError::PackageGraphConstruct {
259                path: path.to_owned(),
260                error,
261            }
262        })?;
263
264        Ok(Self::new((json, graph)))
265    }
266}
267
268/// A helper for path remapping.
269///
270/// This is useful when running tests in a different directory, or a different computer, from
271/// building them.
272#[derive(Clone, Debug, Default)]
273pub struct PathMapper {
274    workspace: Option<(Utf8PathBuf, Utf8PathBuf)>,
275    target_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
276    build_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
277    libdir_mapper: LibdirMapper,
278}
279
280impl PathMapper {
281    /// Constructs the path mapper.
282    pub fn new(
283        orig_workspace_root: impl Into<Utf8PathBuf>,
284        workspace_remap: Option<&Utf8Path>,
285        orig_target_dir: impl Into<Utf8PathBuf>,
286        target_dir_remap: Option<&Utf8Path>,
287        orig_build_dir: impl Into<Utf8PathBuf>,
288        build_dir_remap: Option<&Utf8Path>,
289        libdir_mapper: LibdirMapper,
290    ) -> Result<Self, PathMapperConstructError> {
291        let workspace_root = workspace_remap
292            .map(|root| Self::canonicalize_dir(root, PathMapperConstructKind::WorkspaceRoot))
293            .transpose()?;
294        let target_dir = target_dir_remap
295            .map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::TargetDir))
296            .transpose()?;
297        let build_dir = build_dir_remap
298            .map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::BuildDir))
299            .transpose()?;
300
301        Ok(Self {
302            workspace: workspace_root.map(|w| (orig_workspace_root.into(), w)),
303            target_dir: target_dir.map(|d| (orig_target_dir.into(), d)),
304            build_dir: build_dir.map(|d| (orig_build_dir.into(), d)),
305            libdir_mapper,
306        })
307    }
308
309    /// Constructs a no-op path mapper.
310    pub fn noop() -> Self {
311        Self {
312            workspace: None,
313            target_dir: None,
314            build_dir: None,
315            libdir_mapper: LibdirMapper::default(),
316        }
317    }
318
319    /// Returns the libdir mapper.
320    pub fn libdir_mapper(&self) -> &LibdirMapper {
321        &self.libdir_mapper
322    }
323
324    fn canonicalize_dir(
325        input: &Utf8Path,
326        kind: PathMapperConstructKind,
327    ) -> Result<Utf8PathBuf, PathMapperConstructError> {
328        let canonicalized_path =
329            input
330                .canonicalize()
331                .map_err(|err| PathMapperConstructError::Canonicalization {
332                    kind,
333                    input: input.into(),
334                    err,
335                })?;
336        let canonicalized_path: Utf8PathBuf =
337            canonicalized_path
338                .try_into()
339                .map_err(|err| PathMapperConstructError::NonUtf8Path {
340                    kind,
341                    input: input.into(),
342                    err,
343                })?;
344        if !canonicalized_path.is_dir() {
345            return Err(PathMapperConstructError::NotADirectory {
346                kind,
347                input: input.into(),
348                canonicalized_path,
349            });
350        }
351
352        Ok(os_imp::strip_verbatim(canonicalized_path))
353    }
354
355    /// Returns the canonicalized workspace root, if a workspace remap was
356    /// specified.
357    pub fn new_workspace_root(&self) -> Option<&Utf8Path> {
358        self.workspace.as_ref().map(|(_, new)| new.as_path())
359    }
360
361    pub(super) fn new_target_dir(&self) -> Option<&Utf8Path> {
362        self.target_dir.as_ref().map(|(_, new)| new.as_path())
363    }
364
365    pub(super) fn new_build_dir(&self) -> Option<&Utf8Path> {
366        self.build_dir.as_ref().map(|(_, new)| new.as_path())
367    }
368
369    pub(crate) fn map_cwd(&self, path: Utf8PathBuf) -> Utf8PathBuf {
370        Self::remap_path(self.workspace.as_ref(), path)
371    }
372
373    /// Remaps a path under the target directory.
374    ///
375    /// Use this for paths that are relative to `target_directory`, such as
376    /// non-test binaries and archive include paths.
377    pub(crate) fn map_target_path(&self, path: Utf8PathBuf) -> Utf8PathBuf {
378        Self::remap_path(self.target_dir.as_ref(), path)
379    }
380
381    /// Remaps a path under the build directory.
382    ///
383    /// Use this for paths that are relative to `build_directory`, such as test
384    /// binaries, build script out dirs, and linked paths.
385    pub(crate) fn map_build_path(&self, path: Utf8PathBuf) -> Utf8PathBuf {
386        Self::remap_path(self.build_dir.as_ref(), path)
387    }
388
389    fn remap_path(mapping: Option<&(Utf8PathBuf, Utf8PathBuf)>, path: Utf8PathBuf) -> Utf8PathBuf {
390        match mapping {
391            Some((from, to)) => match path.strip_prefix(from) {
392                Ok(p) if !p.as_str().is_empty() => to.join(p),
393                Ok(_) => to.clone(),
394                Err(_) => {
395                    debug!(
396                        target: "nextest-runner::reuse_build",
397                        "path `{path}` does not start with remap prefix `{from}`, \
398                         returning unchanged",
399                    );
400                    path
401                }
402            },
403            None => path,
404        }
405    }
406}
407
408/// A mapper for lib dirs.
409///
410/// Archives store parts of lib dirs, which must be remapped to the new target directory.
411#[derive(Clone, Debug, Default)]
412pub struct LibdirMapper {
413    /// The host libdir mapper.
414    pub(crate) host: PlatformLibdirMapper,
415
416    /// The target libdir mapper.
417    pub(crate) target: PlatformLibdirMapper,
418}
419
420/// A mapper for an individual platform libdir.
421///
422/// Part of [`LibdirMapper`].
423#[derive(Clone, Debug, Default)]
424pub(crate) enum PlatformLibdirMapper {
425    Path(Utf8PathBuf),
426    Unavailable,
427    #[default]
428    NotRequested,
429}
430
431impl PlatformLibdirMapper {
432    pub(crate) fn map(&self, original: &PlatformLibdir) -> PlatformLibdir {
433        match self {
434            PlatformLibdirMapper::Path(new) => {
435                // Just use the new path. (We may have to check original in the future, but it
436                // doesn't seem necessary for now -- if a libdir has been provided to the remapper,
437                // that's that.)
438                PlatformLibdir::Available(new.clone())
439            }
440            PlatformLibdirMapper::Unavailable => {
441                // In this case, the original value is ignored -- we expected a libdir to be
442                // present, but it wasn't.
443                PlatformLibdir::Unavailable(PlatformLibdirUnavailable::NOT_IN_ARCHIVE)
444            }
445            PlatformLibdirMapper::NotRequested => original.clone(),
446        }
447    }
448}
449
450#[cfg(windows)]
451mod os_imp {
452    use camino::Utf8PathBuf;
453    use std::ptr;
454    use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
455
456    /// Strips verbatim prefix from a path if possible.
457    pub(super) fn strip_verbatim(path: Utf8PathBuf) -> Utf8PathBuf {
458        let path_str = String::from(path);
459        if path_str.starts_with(r"\\?\UNC") {
460            // In general we don't expect UNC paths, so just return the path as is.
461            path_str.into()
462        } else if path_str.starts_with(r"\\?\") {
463            const START_LEN: usize = r"\\?\".len();
464
465            let is_absolute_exact = {
466                let mut v = path_str[START_LEN..].encode_utf16().collect::<Vec<u16>>();
467                // Ensure null termination.
468                v.push(0);
469                is_absolute_exact(&v)
470            };
471
472            if is_absolute_exact {
473                path_str[START_LEN..].into()
474            } else {
475                path_str.into()
476            }
477        } else {
478            // Not a verbatim path, so return it as is.
479            path_str.into()
480        }
481    }
482
483    /// Test that the path is absolute, fully qualified and unchanged when processed by the Windows API.Add commentMore actions
484    ///
485    /// For example:
486    ///
487    /// - `C:\path\to\file` will return true.
488    /// - `C:\path\to\nul` returns false because the Windows API will convert it to \\.\NUL
489    /// - `C:\path\to\..\file` returns false because it will be resolved to `C:\path\file`.
490    ///
491    /// This is a useful property because it means the path can be converted from and to and verbatim
492    /// path just by changing the prefix.
493    fn is_absolute_exact(path: &[u16]) -> bool {
494        // Adapted from the Rust project: https://github.com/rust-lang/rust/commit/edfc74722556c659de6fa03b23af3b9c8ceb8ac2
495
496        // This is implemented by checking that passing the path through
497        // GetFullPathNameW does not change the path in any way.
498
499        // Windows paths are limited to i16::MAX length
500        // though the API here accepts a u32 for the length.
501        if path.is_empty() || path.len() > u32::MAX as usize || path.last() != Some(&0) {
502            return false;
503        }
504        // The path returned by `GetFullPathNameW` must be the same length as the
505        // given path, otherwise they're not equal.
506        let buffer_len = path.len();
507        let mut new_path = Vec::with_capacity(buffer_len);
508        let result = unsafe {
509            GetFullPathNameW(
510                path.as_ptr(),
511                new_path.capacity() as u32,
512                new_path.as_mut_ptr(),
513                ptr::null_mut(),
514            )
515        };
516        // Note: if non-zero, the returned result is the length of the buffer without the null termination
517        if result == 0 || result as usize != buffer_len - 1 {
518            false
519        } else {
520            // SAFETY: `GetFullPathNameW` initialized `result` bytes and does not exceed `nBufferLength - 1` (capacity).
521            unsafe {
522                new_path.set_len((result as usize) + 1);
523            }
524            path == new_path
525        }
526    }
527}
528
529#[cfg(unix)]
530mod os_imp {
531    use camino::Utf8PathBuf;
532
533    pub(super) fn strip_verbatim(path: Utf8PathBuf) -> Utf8PathBuf {
534        // On Unix, there aren't any verbatin paths, so just return the path as
535        // is.
536        path
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::{list::RustBuildMeta, platform::BuildPlatforms};
544
545    /// Ensure that PathMapper turns relative paths into absolute ones.
546    #[test]
547    fn test_path_mapper_relative() {
548        let current_dir: Utf8PathBuf = std::env::current_dir()
549            .expect("current dir obtained")
550            .try_into()
551            .expect("current dir is valid UTF-8");
552
553        let temp_workspace_root = Utf8TempDir::new().expect("new temp dir created");
554        let workspace_root_path: Utf8PathBuf = os_imp::strip_verbatim(
555            temp_workspace_root
556                .path()
557                // On Mac, the temp dir is a symlink, so canonicalize it.
558                .canonicalize()
559                .expect("workspace root canonicalized correctly")
560                .try_into()
561                .expect("workspace root is valid UTF-8"),
562        );
563        let rel_workspace_root = pathdiff::diff_utf8_paths(&workspace_root_path, &current_dir)
564            .expect("abs to abs diff is non-None");
565
566        let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
567        let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
568            temp_target_dir
569                .path()
570                .canonicalize()
571                .expect("target dir canonicalized correctly")
572                .try_into()
573                .expect("target dir is valid UTF-8"),
574        );
575        let rel_target_dir = pathdiff::diff_utf8_paths(&target_dir_path, &current_dir)
576            .expect("abs to abs diff is non-None");
577
578        let temp_build_dir = Utf8TempDir::new().expect("new temp dir created");
579        let build_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
580            temp_build_dir
581                .path()
582                .canonicalize()
583                .expect("build dir canonicalized correctly")
584                .try_into()
585                .expect("build dir is valid UTF-8"),
586        );
587        let rel_build_dir = pathdiff::diff_utf8_paths(&build_dir_path, &current_dir)
588            .expect("abs to abs diff is non-None");
589
590        // These aren't really used other than to do mapping against.
591        let orig_workspace_root = Utf8PathBuf::from(
592            std::env::var("NEXTEST_WORKSPACE_ROOT")
593                .expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
594        );
595        let orig_target_dir = orig_workspace_root.join("target");
596        let orig_build_dir = orig_workspace_root.join("build");
597
598        let path_mapper = PathMapper::new(
599            orig_workspace_root.as_path(),
600            Some(&rel_workspace_root),
601            &orig_target_dir,
602            Some(&rel_target_dir),
603            &orig_build_dir,
604            Some(&rel_build_dir),
605            LibdirMapper::default(),
606        )
607        .expect("remapped paths exist");
608
609        assert_eq!(
610            path_mapper.map_cwd(orig_workspace_root.join("foobar")),
611            workspace_root_path.join("foobar")
612        );
613        assert_eq!(
614            path_mapper.map_target_path(orig_target_dir.join("foobar")),
615            target_dir_path.join("foobar")
616        );
617        assert_eq!(
618            path_mapper.map_build_path(orig_build_dir.join("foobar")),
619            build_dir_path.join("foobar")
620        );
621    }
622
623    /// Test that when build_dir and target_dir are separate, each maps
624    /// independently.
625    #[test]
626    fn test_path_mapper_separate_build_dir() {
627        let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
628        let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
629            temp_target_dir
630                .path()
631                .canonicalize()
632                .expect("target dir canonicalized correctly")
633                .try_into()
634                .expect("target dir is valid UTF-8"),
635        );
636
637        let temp_build_dir = Utf8TempDir::new().expect("new temp dir created");
638        let build_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
639            temp_build_dir
640                .path()
641                .canonicalize()
642                .expect("build dir canonicalized correctly")
643                .try_into()
644                .expect("build dir is valid UTF-8"),
645        );
646
647        let orig_workspace_root = Utf8PathBuf::from(
648            std::env::var("NEXTEST_WORKSPACE_ROOT")
649                .expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
650        );
651        let orig_target_dir = orig_workspace_root.join("target");
652        let orig_build_dir = orig_workspace_root.join("build");
653
654        let path_mapper = PathMapper::new(
655            orig_workspace_root.as_path(),
656            None,
657            &orig_target_dir,
658            Some(target_dir_path.as_path()),
659            &orig_build_dir,
660            Some(build_dir_path.as_path()),
661            LibdirMapper::default(),
662        )
663        .expect("remapped paths exist");
664
665        // Target paths remap under target_dir.
666        assert_eq!(
667            path_mapper.map_target_path(orig_target_dir.join("debug/mybin")),
668            target_dir_path.join("debug/mybin"),
669        );
670
671        // Build paths remap under build_dir.
672        assert_eq!(
673            path_mapper.map_build_path(orig_build_dir.join("debug/deps/test-abc123")),
674            build_dir_path.join("debug/deps/test-abc123"),
675        );
676
677        // Paths not matching either prefix are returned unchanged.
678        let unrelated = Utf8PathBuf::from("/some/other/path");
679        assert_eq!(path_mapper.map_target_path(unrelated.clone()), unrelated,);
680        assert_eq!(path_mapper.map_build_path(unrelated.clone()), unrelated,);
681    }
682
683    /// Test that `map_paths` falls back to target_dir remap when no explicit
684    /// build_dir remap is set.
685    #[test]
686    fn test_map_paths_build_dir_fallback() {
687        let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
688        let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
689            temp_target_dir
690                .path()
691                .canonicalize()
692                .expect("target dir canonicalized correctly")
693                .try_into()
694                .expect("target dir is valid UTF-8"),
695        );
696
697        let orig_workspace_root = Utf8PathBuf::from(
698            std::env::var("NEXTEST_WORKSPACE_ROOT")
699                .expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
700        );
701        let orig_target_dir = orig_workspace_root.join("target");
702        let orig_build_dir = orig_workspace_root.join("build");
703
704        // Only target_dir remap is set, no build_dir remap.
705        let path_mapper = PathMapper::new(
706            orig_workspace_root.as_path(),
707            None,
708            &orig_target_dir,
709            Some(target_dir_path.as_path()),
710            &orig_build_dir,
711            None,
712            LibdirMapper::default(),
713        )
714        .expect("remapped paths exist");
715
716        // new_build_dir() returns None since no explicit build dir remap was
717        // passed.
718        assert_eq!(path_mapper.new_build_dir(), None);
719        assert_eq!(
720            path_mapper.new_target_dir(),
721            Some(target_dir_path.as_path()),
722        );
723
724        // Verify the fallback by actually calling map_paths: since no
725        // build_dir remap is set, map_paths should fall back to the
726        // target_dir remap for build_directory.
727        let build_platforms = BuildPlatforms::new_with_no_target()
728            .expect("creating BuildPlatforms without target triple succeeds");
729        let meta = RustBuildMeta::new(&orig_target_dir, &orig_build_dir, build_platforms);
730        let mapped = meta.map_paths(&path_mapper);
731
732        assert_eq!(mapped.target_directory, target_dir_path);
733        assert_eq!(
734            mapped.build_directory, target_dir_path,
735            "build_directory should fall back to the target_dir remap",
736        );
737    }
738}