Skip to main content

dev_fixtures/
lib.rs

1//! # dev-fixtures
2//!
3//! Repeatable test environments, sample data, and controlled inputs for
4//! Rust. Part of the `dev-*` verification suite.
5//!
6//! ## Why
7//!
8//! Tests are only useful if they are repeatable. AI agents in particular
9//! need fixtures that:
10//!
11//! - Build the same way every time
12//! - Clean themselves up
13//! - Provide both happy-path and adversarial inputs
14//!
15//! `dev-fixtures` provides primitives for building those environments.
16//!
17//! ## Quick example
18//!
19//! ```no_run
20//! use dev_fixtures::TempProject;
21//!
22//! let project = TempProject::new()
23//!     .with_file("Cargo.toml", "[package]\nname = \"sample\"\n")
24//!     .with_file("src/lib.rs", "pub fn answer() -> u32 { 42 }")
25//!     .build()
26//!     .unwrap();
27//!
28//! // project.path() points at a temp directory.
29//! // It is deleted automatically when `project` is dropped.
30//! ```
31
32#![cfg_attr(docsrs, feature(doc_cfg))]
33#![warn(missing_docs)]
34#![warn(rust_2018_idioms)]
35
36use std::fs;
37use std::io;
38use std::path::{Path, PathBuf};
39
40/// A temporary project directory that auto-cleans on drop.
41///
42/// Holds an internal `tempfile::TempDir`. The temp directory is deleted
43/// when this value is dropped.
44pub struct TempProject {
45    _dir: tempfile::TempDir,
46    files: Vec<(PathBuf, Vec<u8>)>,
47}
48
49impl TempProject {
50    /// Begin building a temp project.
51    pub fn new() -> TempProjectBuilder {
52        TempProjectBuilder::default()
53    }
54
55    /// Path to the root of the temp project.
56    pub fn path(&self) -> &Path {
57        self._dir.path()
58    }
59
60    /// Files declared at build time. Useful for diagnostics.
61    pub fn declared_files(&self) -> impl Iterator<Item = (&Path, &[u8])> {
62        self.files.iter().map(|(p, b)| (p.as_path(), b.as_slice()))
63    }
64}
65
66/// Builder for [`TempProject`].
67#[derive(Default)]
68pub struct TempProjectBuilder {
69    files: Vec<(PathBuf, Vec<u8>)>,
70}
71
72impl TempProjectBuilder {
73    /// Stage a UTF-8 text file at `relative_path` inside the temp project.
74    pub fn with_file(mut self, relative_path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
75        self.files
76            .push((relative_path.into(), contents.into().into_bytes()));
77        self
78    }
79
80    /// Stage a binary file at `relative_path` inside the temp project.
81    pub fn with_bytes(
82        mut self,
83        relative_path: impl Into<PathBuf>,
84        contents: impl Into<Vec<u8>>,
85    ) -> Self {
86        self.files.push((relative_path.into(), contents.into()));
87        self
88    }
89
90    /// Build the temp project on disk.
91    pub fn build(self) -> io::Result<TempProject> {
92        let dir = tempfile::tempdir()?;
93        for (rel, bytes) in &self.files {
94            let target = dir.path().join(rel);
95            if let Some(parent) = target.parent() {
96                fs::create_dir_all(parent)?;
97            }
98            fs::write(&target, bytes)?;
99        }
100        Ok(TempProject {
101            _dir: dir,
102            files: self.files,
103        })
104    }
105}
106
107/// A trait for any fixture that can be set up and torn down.
108///
109/// Implementors should ensure that `tear_down` is idempotent and that
110/// `set_up` followed by `tear_down` always returns the system to a clean
111/// state.
112pub trait Fixture {
113    /// Output produced when the fixture is set up.
114    type Output;
115
116    /// Set the fixture up. Returns the output (e.g. a path, a handle).
117    fn set_up(&mut self) -> io::Result<Self::Output>;
118
119    /// Tear the fixture down. MUST be idempotent.
120    fn tear_down(&mut self) -> io::Result<()>;
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn temp_project_builds_and_writes_files() {
129        let project = TempProject::new()
130            .with_file("a.txt", "hello")
131            .with_file("nested/b.txt", "world")
132            .build()
133            .unwrap();
134
135        let a = project.path().join("a.txt");
136        let b = project.path().join("nested").join("b.txt");
137        assert!(a.exists());
138        assert!(b.exists());
139        assert_eq!(std::fs::read_to_string(&a).unwrap(), "hello");
140        assert_eq!(std::fs::read_to_string(&b).unwrap(), "world");
141    }
142
143    #[test]
144    fn temp_project_cleans_up_on_drop() {
145        let path = {
146            let project = TempProject::new()
147                .with_file("x.txt", "ephemeral")
148                .build()
149                .unwrap();
150            project.path().to_path_buf()
151        };
152        assert!(!path.exists());
153    }
154}