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}