Skip to main content

cranpose_core/
test_scratch.rs

1//! Where a test writes real files.
2//!
3//! Enabled by the `test-helpers` feature. Every crate in the workspace that
4//! needs a file on disk during a test asks here, so there is one answer to
5//! where those files go rather than one per crate that drifts from the rest.
6
7use std::{
8    path::{Path, PathBuf},
9    sync::atomic::{AtomicU32, Ordering},
10};
11
12/// A unique, empty directory under the workspace `target/test-output`.
13///
14/// Never the system temporary directory: on Linux that is tmpfs, so a test
15/// writing a payload there writes it to RAM, and a failure leaves nothing
16/// under `target` to look at afterwards.
17/// `apps/desktop-demo/tests/source_hygiene_aliases.rs` enforces that across
18/// the workspace.
19///
20/// `manifest_dir` is the caller's own `env!("CARGO_MANIFEST_DIR")`. Its last
21/// component names the subdirectory, so two crates asking for the same `tag`
22/// get different directories; the workspace root is found by walking up to
23/// the directory holding `Cargo.lock` rather than by counting `..` hops,
24/// which differ per crate and are wrong the moment one moves.
25pub fn test_scratch_dir(manifest_dir: &str, tag: &str) -> PathBuf {
26    static COUNTER: AtomicU32 = AtomicU32::new(0);
27    let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
28    let manifest = PathBuf::from(manifest_dir);
29    let owner = manifest
30        .file_name()
31        .map(|name| name.to_string_lossy().into_owned())
32        .unwrap_or_else(|| "workspace".to_string());
33    let path = workspace_root(&manifest)
34        .join("target/test-output")
35        .join(owner)
36        .join(format!("{tag}-{}-{unique}", std::process::id()));
37    let _ = std::fs::remove_dir_all(&path);
38    std::fs::create_dir_all(&path).expect("a scratch directory under target/test-output");
39    path
40}
41
42fn workspace_root(manifest: &Path) -> PathBuf {
43    manifest
44        .ancestors()
45        .find(|directory| directory.join("Cargo.lock").is_file())
46        .unwrap_or(manifest)
47        .to_path_buf()
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn a_scratch_directory_is_empty_under_the_workspace_target() {
56        let path = test_scratch_dir(env!("CARGO_MANIFEST_DIR"), "scratch");
57        assert!(path.is_dir());
58        assert_eq!(std::fs::read_dir(&path).expect("read").count(), 0);
59        assert!(
60            path.components().any(|part| part.as_os_str() == "target"),
61            "the scratch directory belongs under the workspace target: {}",
62            path.display()
63        );
64        let _ = std::fs::remove_dir_all(&path);
65    }
66
67    #[test]
68    fn two_calls_do_not_share_a_directory() {
69        let first = test_scratch_dir(env!("CARGO_MANIFEST_DIR"), "scratch");
70        let second = test_scratch_dir(env!("CARGO_MANIFEST_DIR"), "scratch");
71        assert_ne!(first, second);
72        let _ = std::fs::remove_dir_all(&first);
73        let _ = std::fs::remove_dir_all(&second);
74    }
75}