csusage_test_support/
lib.rs1use std::{
2 ffi::{OsStr, OsString},
3 fs,
4 path::{Path, PathBuf},
5 sync::{Mutex, MutexGuard},
6};
7
8use assert_fs::{
9 TempDir,
10 fixture::{ChildPath, FileWriteStr, PathChild, PathCreateDir},
11};
12
13pub mod claude_science;
14pub mod zcode;
15
16static ENV_LOCK: Mutex<()> = Mutex::new(());
17
18fn env_lock() -> MutexGuard<'static, ()> {
19 ENV_LOCK.lock().unwrap_or_else(|error| error.into_inner())
20}
21
22pub struct EnvVarGuard {
23 key: &'static str,
24 previous: Option<OsString>,
25 _guard: MutexGuard<'static, ()>,
26}
27
28impl EnvVarGuard {
29 pub fn set(key: &'static str, value: impl AsRef<OsStr>) -> Self {
30 let guard = env_lock();
31 let previous = std::env::var_os(key);
32 unsafe { std::env::set_var(key, value) };
33 Self {
34 key,
35 previous,
36 _guard: guard,
37 }
38 }
39}
40
41impl Drop for EnvVarGuard {
42 fn drop(&mut self) {
43 match self.previous.take() {
44 Some(value) => unsafe { std::env::set_var(self.key, value) },
45 None => unsafe { std::env::remove_var(self.key) },
46 }
47 }
48}
49
50pub struct EnvVarsGuard {
51 previous: Vec<(&'static str, Option<OsString>)>,
52 _guard: MutexGuard<'static, ()>,
53}
54
55impl EnvVarsGuard {
56 pub fn set_many(vars: impl IntoIterator<Item = (&'static str, Option<OsString>)>) -> Self {
57 let guard = env_lock();
58 let mut previous = Vec::new();
59 for (key, value) in vars {
60 previous.push((key, std::env::var_os(key)));
61 match value {
62 Some(value) => unsafe { std::env::set_var(key, value) },
63 None => unsafe { std::env::remove_var(key) },
64 }
65 }
66 Self {
67 previous,
68 _guard: guard,
69 }
70 }
71}
72
73impl Drop for EnvVarsGuard {
74 fn drop(&mut self) {
75 for (key, value) in self.previous.drain(..).rev() {
76 match value {
77 Some(value) => unsafe { std::env::set_var(key, value) },
78 None => unsafe { std::env::remove_var(key) },
79 }
80 }
81 }
82}
83
84pub struct Fixture {
85 dir: TempDir,
86}
87
88impl Fixture {
89 pub fn new() -> Self {
90 Self {
91 dir: TempDir::new().expect("failed to create temporary fixture directory"),
92 }
93 }
94
95 pub fn root(&self) -> &Path {
96 self.dir.path()
97 }
98
99 #[must_use]
100 pub fn path(&self, path: impl AsRef<Path>) -> PathBuf {
101 self.dir.path().join(path)
102 }
103
104 fn child(&self, path: impl AsRef<Path>) -> ChildPath {
105 self.dir.child(path)
106 }
107
108 #[must_use]
109 pub fn write_file(&self, path: impl AsRef<Path>, contents: impl AsRef<str>) -> PathBuf {
110 let child = self.child(path);
111 if let Some(parent) = child.path().parent() {
112 fs::create_dir_all(parent).expect("failed to create fixture file parent directory");
113 }
114 child
115 .write_str(contents.as_ref())
116 .expect("failed to write fixture file");
117 child.path().to_path_buf()
118 }
119
120 #[must_use]
121 pub fn create_dir_all(&self, path: impl AsRef<Path>) -> PathBuf {
122 let child = self.child(path);
123 child
124 .create_dir_all()
125 .expect("failed to create fixture directory");
126 child.path().to_path_buf()
127 }
128}
129
130impl Default for Fixture {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136#[macro_export]
137macro_rules! fs_fixture {
138 ({ $($path:literal : $contents:expr_2021),* $(,)? }) => {{
139 let fixture = $crate::Fixture::new();
140 $(
141 let _ = fixture.write_file($path, $contents);
142 )*
143 fixture
144 }};
145}
146
147#[cfg(test)]
148mod tests {
149 #[test]
150 fn creates_inline_fixture_tree() {
151 let fixture = fs_fixture!({
152 "projects/example/session.jsonl": "{}\n",
153 });
154
155 assert_eq!(
156 std::fs::read_to_string(fixture.path("projects/example/session.jsonl")).unwrap(),
157 "{}\n"
158 );
159 }
160
161 #[test]
162 fn creates_incremental_fixture_tree() {
163 let fixture = fs_fixture!({});
164 let _ = fixture.write_file("projects/example/session/chat.jsonl", "{}\n");
165
166 assert!(
167 fixture
168 .path("projects/example/session/chat.jsonl")
169 .is_file()
170 );
171 }
172}