testdir/
macros.rs

1//! The `testdir!()` macro and friends.
2//!
3//! Tests for this are almost exclusively in the integration tests at `tests/macro.rs`.
4
5/// Creates a test directory at the requested scope.
6///
7/// This macro creates a new or re-uses an existing [`NumberedDir`] in the cargo target
8/// directory.  It than creates the requested sub-directory within this [`NumberedDir`].
9/// The path for this directory is returned as a [`PathBuf`].
10///
11/// For the typical `testdir!()` invocation in a test function this would result in
12/// `target/testdir-$N/$CARGO_CRATE_NAME/module/path/to/test_function_name1.  A symbolic
13/// link to the most recent [`NumberedDir`] is also created as `target/testdir-current ->
14/// testdir-$N`.
15///
16/// **Reuse** of the [`NumberedDir`] is triggered when this process is being run as a
17/// subprocess of Cargo, as is typical when running `cargo test`.  In this case the same
18/// [`NumberedDir`] is re-used between all Cargo sub-processes, which means it is shared
19/// between unittests, integration tests and doctests of the same test run.
20///
21/// The path within the numbered directory is created based on the context and how it is
22/// invoked.  There are several ways to specify this:
23///
24/// * Use the scope of the current test function to create a unique and predictable
25///   directory: `testdir!(TestScope)`.  This is the default when invoked as without any
26///   arguments as well: `testdir!()`.  In this case the directory path will follow the crate
27///   name and module path, ending with the test function name.  This also works in
28///   integration and doctests.
29///
30/// * Use the scope of the current module: `testdir!(ModuleScope)`.  In this case the crate
31///   name and module path is used, but with an additional final `mod` component.
32///
33/// * Directly provide the path using an expression, e.g. `testdir!("sub/dir").  This
34///   expression will be passed to [`NumberedDir::create_subdir`] and thus must evaluate to
35///   something which implements ``AsRef<Path>``, e.g. a simple `"sub/dir"` can be used or
36///   something more advanced evaluating to a path, usually [`Path`] or [`PathBuf`].
37///
38/// # Panics
39///
40/// If there is any problem with creating the directories or cleaning up old ones this will
41/// panic.
42///
43/// # Examples
44///
45/// Inside a test function you can use the shorthand:
46/// ```
47/// use std::path::PathBuf;
48/// use testdir::testdir;
49///
50/// let path0: PathBuf = testdir!();
51/// ```
52///
53/// This is the same as invoking:
54/// ```
55/// # use testdir::testdir;
56/// let path1 = testdir!(TestScope);
57/// ```
58/// These constructs can also be used in a doctest.
59///
60/// The module path is valid in any scope, so can be used together with [once_cell] (or
61/// [lazy_static]) to share a common directory between different tests.
62/// ```no_run
63/// use std::path::PathBuf;
64/// use once_cell::sync::Lazy;
65/// use testdir::testdir;
66///
67/// static TDIR: Lazy<PathBuf> = Lazy::new(|| testdir!(ModuleScope));
68///
69/// #[test]
70/// fn test_module_scope() {
71///     assert!(TDIR.ends_with("mod"));
72/// }
73/// ```
74///
75/// [lazy_static]: https://docs.rs/lazy_static
76/// [`NumberedDir`]: crate::NumberedDir
77/// [`PathBuf`]: std::path::PathBuf
78#[macro_export]
79macro_rules! testdir {
80    () => {
81        $crate::testdir!(TestScope)
82    };
83    ( TestScope ) => {{
84        $crate::init_testdir!();
85        let module_path = ::std::module_path!();
86        let test_name = $crate::private::extract_test_name(&module_path);
87        let subdir_path = ::std::path::Path::new(&module_path.replace("::", "/")).join(&test_name);
88        $crate::with_testdir(move |tdir| {
89            tdir.create_subdir(subdir_path)
90                .expect("Failed to create test-scoped sub-directory")
91        })
92    }};
93    ( ModuleScope ) => {{
94        $crate::init_testdir!();
95        let module_path = ::std::module_path!();
96        let subdir_path = ::std::path::Path::new(&module_path.replace("::", "/")).join("mod");
97        $crate::with_testdir(move |tdir| {
98            tdir.create_subdir(subdir_path)
99                .expect("Failed to create module-scoped sub-directory")
100        })
101    }};
102    ( $e:expr ) => {{
103        $crate::init_testdir!();
104        $crate::with_testdir(move |tdir| {
105            tdir.create_subdir($e)
106                .expect("Failed to create sub-directory")
107        })
108    }};
109}
110
111/// Initialises the global [`NumberedDir`] used by the [`testdir`] macro.
112///
113/// This macro is implicitly called by the [`testdir`] macro to initialise the global
114/// [`NumberedDir`] instance inside the cargo target directory.  It must be called before
115/// any call to [`with_testdir`](crate::with_testdir) to ensure this is initialised.
116///
117/// # Examples
118///
119/// ```
120/// use testdir::{init_testdir, with_testdir};
121///
122/// init_testdir!();
123/// let path = with_testdir(|dir| dir.create_subdir("some/subdir").unwrap());
124/// assert!(path.is_dir());
125/// assert!(path.ends_with("some/subdir"));
126/// ```
127///
128/// [`NumberedDir`]: crate::NumberedDir
129#[macro_export]
130macro_rules! init_testdir {
131    () => {{
132        $crate::TESTDIR.get_or_init(move || {
133            let parent = match $crate::private::cargo_metadata::MetadataCommand::new().exec() {
134                Ok(metadata) => metadata.target_directory.into(),
135                Err(_) => {
136                    // In some environments cargo-metadata is not available,
137                    // e.g. cargo-dinghy.  Use the directory of test executable.
138                    let current_exe = ::std::env::current_exe().expect("no current exe");
139                    current_exe
140                        .parent()
141                        .expect("no parent dir for current exe")
142                        .into()
143                }
144            };
145            let pkg_name = "testdir";
146            let mut builder = $crate::NumberedDirBuilder::new(pkg_name.to_string());
147            builder.set_parent(parent);
148            builder.reusefn($crate::private::reuse_cargo);
149            let testdir = builder.create().expect("Failed to create testdir");
150            $crate::private::create_cargo_pid_file(testdir.path());
151            testdir
152        })
153    }};
154}