rskit_testutil/
workspace.rs1use std::path::{Path, PathBuf};
4
5use rskit_errors::{AppError, AppResult};
6use rskit_fs::{TempDir, safe_join, sync_io::file};
7
8#[derive(Debug)]
14pub struct TestWorkspace {
15 inner: TempDir,
16 fixture_dir: Option<PathBuf>,
17}
18
19impl TestWorkspace {
20 pub fn new(label: &str) -> Self {
22 Self::try_new().unwrap_or_else(|error| panic!("create test workspace for {label}: {error}"))
23 }
24
25 pub fn try_new() -> AppResult<Self> {
27 Ok(Self {
28 inner: TempDir::new()?,
29 fixture_dir: None,
30 })
31 }
32
33 #[must_use]
35 pub fn with_fixture_dir(mut self, fixture_dir: impl Into<PathBuf>) -> Self {
36 self.fixture_dir = Some(fixture_dir.into());
37 self
38 }
39
40 #[must_use]
42 pub fn path(&self) -> &Path {
43 self.inner.path()
44 }
45
46 pub fn child(&self, rel_path: impl AsRef<Path>) -> AppResult<PathBuf> {
48 self.inner.child(rel_path)
49 }
50
51 pub fn write_file(&self, rel_path: impl AsRef<Path>, content: &[u8]) -> AppResult<PathBuf> {
53 self.inner.write_file(rel_path, content)
54 }
55
56 pub fn fixture_path(&self, rel_path: impl AsRef<Path>) -> AppResult<PathBuf> {
58 let Some(fixture_dir) = &self.fixture_dir else {
59 return Err(AppError::invalid_input(
60 "fixture_dir",
61 "fixture directory is not configured",
62 ));
63 };
64 safe_join(fixture_dir, rel_path.as_ref())
65 .map_err(|error| AppError::invalid_input("fixture_path", error.to_string()))
66 }
67
68 pub fn read_fixture(&self, rel_path: impl AsRef<Path>) -> AppResult<Vec<u8>> {
70 file::read(&self.fixture_path(rel_path)?)
71 }
72
73 pub fn read_fixture_string(&self, rel_path: impl AsRef<Path>) -> AppResult<String> {
75 file::read_string(&self.fixture_path(rel_path)?)
76 }
77
78 pub fn copy_fixture(
80 &self,
81 fixture_rel_path: impl AsRef<Path>,
82 dest_rel_path: impl AsRef<Path>,
83 ) -> AppResult<PathBuf> {
84 let source = self.fixture_path(fixture_rel_path)?;
85 let dest = self.child(dest_rel_path)?;
86 file::copy(&source, &dest)?;
87 Ok(dest)
88 }
89}
90
91#[macro_export]
96macro_rules! test_workspace {
97 ($label:expr) => {
98 $crate::TestWorkspace::new($label).with_fixture_dir(
99 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
100 .join("tests")
101 .join("fixtures"),
102 )
103 };
104}
105
106#[cfg(test)]
107mod tests {
108 use std::path::Path;
109
110 use rskit_errors::ErrorCode;
111
112 use super::TestWorkspace;
113
114 #[test]
115 fn writes_files_under_workspace() {
116 let workspace = TestWorkspace::new("write");
117
118 let path = workspace.write_file("nested/file.txt", b"hello").unwrap();
119
120 assert!(path.starts_with(workspace.path()));
121 assert_eq!(
122 rskit_fs::sync_io::file::read_string(&path).unwrap(),
123 "hello"
124 );
125 }
126
127 #[test]
128 fn copies_fixture_into_workspace() {
129 let fixtures = TestWorkspace::new("fixtures");
130 fixtures
131 .write_file("config/app.toml", b"name = 'demo'")
132 .unwrap();
133 let workspace = TestWorkspace::new("workspace").with_fixture_dir(fixtures.path());
134
135 let copied = workspace
136 .copy_fixture("config/app.toml", "copied/app.toml")
137 .unwrap();
138
139 assert!(copied.starts_with(workspace.path()));
140 assert_eq!(
141 rskit_fs::sync_io::file::read_string(&copied).unwrap(),
142 "name = 'demo'"
143 );
144 }
145
146 #[test]
147 fn reads_fixture_string() {
148 let fixtures = TestWorkspace::new("fixtures");
149 fixtures.write_file("message.txt", b"hello").unwrap();
150 let workspace = TestWorkspace::new("workspace").with_fixture_dir(fixtures.path());
151
152 let content = workspace.read_fixture_string("message.txt").unwrap();
153
154 assert_eq!(content, "hello");
155 }
156
157 #[test]
158 fn rejects_fixture_access_without_fixture_dir() {
159 let workspace = TestWorkspace::new("workspace");
160
161 let error = workspace.fixture_path("message.txt").unwrap_err();
162
163 assert_eq!(error.code(), ErrorCode::InvalidInput);
164 assert!(
165 error
166 .message()
167 .contains("fixture directory is not configured")
168 );
169 }
170
171 #[test]
172 fn rejects_escaping_fixture_paths() {
173 let fixtures = TestWorkspace::new("fixtures");
174 let workspace = TestWorkspace::new("workspace").with_fixture_dir(fixtures.path());
175
176 let error = workspace
177 .fixture_path(Path::new("../escape.txt"))
178 .unwrap_err();
179
180 assert_eq!(error.code(), ErrorCode::InvalidInput);
181 }
182}