precious_testhelper/
lib.rs1use anyhow::{Context, Result};
2use log::debug;
3use mitsein::prelude::*;
4use precious_helpers::exec::Exec;
5use pushd::Pushd;
6use regex::Regex;
7use std::{
8 env,
9 ffi::OsString,
10 fs,
11 io::prelude::*,
12 path::{Path, PathBuf},
13 sync::{LazyLock, OnceLock},
14};
15use tempfile::TempDir;
16
17pub struct TestHelper {
18 _tempdir: TempDir,
21 git_root: PathBuf,
22 precious_root: PathBuf,
23 paths: Vec<PathBuf>,
24 root_gitignore_file: PathBuf,
25 tests_data_gitignore_file: PathBuf,
26}
27
28static RERERE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new("Recorded preimage").unwrap());
29
30impl TestHelper {
31 const PATHS: &'static [&'static str] = &[
32 "README.md",
33 "can_ignore.x",
34 "merge-conflict-file",
35 "src/bar.rs",
36 "src/can_ignore.rs",
37 "src/main.rs",
38 "src/module.rs",
39 "src/sub/mod.rs",
40 "tests/data/bar.txt",
41 "tests/data/foo.txt",
42 "tests/data/generated.txt",
43 ];
44
45 pub fn new() -> Result<Self> {
46 static LOGGER_INIT: OnceLock<bool> = OnceLock::new();
47 LOGGER_INIT.get_or_init(|| {
48 env_logger::builder().is_test(true).init();
49 true
50 });
51
52 let mut td = tempfile::Builder::new()
53 .prefix("precious-testhelper-")
54 .tempdir()?;
55 if let Ok(p) = env::var("PRECIOUS_TESTS_PRESERVE_TEMPDIR") {
56 if !(p.is_empty() || p == "0") {
57 td.disable_cleanup(true);
58 }
59 }
60
61 let root = maybe_canonicalize(td.path())?;
62
63 let helper = TestHelper {
64 _tempdir: td,
65 git_root: root.clone(),
66 precious_root: root,
67 paths: Self::PATHS.iter().map(PathBuf::from).collect(),
68 root_gitignore_file: PathBuf::from(".gitignore"),
69 tests_data_gitignore_file: PathBuf::from("tests/data/.gitignore"),
70 };
71 Ok(helper)
72 }
73
74 pub fn with_precious_root_in_subdir<P: AsRef<Path>>(mut self, subdir: P) -> Self {
75 self.precious_root.push(subdir);
76 self
77 }
78
79 pub fn with_git_repo(self) -> Result<Self> {
80 self.create_git_repo()?;
81 Ok(self)
82 }
83
84 fn create_git_repo(&self) -> Result<()> {
85 debug!("Creating git repo in {}", self.git_root.display());
86 for p in self.paths.iter() {
87 let content = if is_rust_file(p) {
88 "fn foo() {}\n"
89 } else {
90 "some text"
91 };
92 self.write_file(p, content)?;
93 }
94
95 self.run_git(vec!["init", "--initial-branch", "master"])?;
96
97 self.run_git(vec!["config", "user.email", "precious@example.com"])?;
101 self.run_git(vec!["config", "core.autocrlf", "false"])?;
105
106 self.stage_all()?;
107 self.run_git(vec!["commit", "-m", "initial commit"])?;
108
109 Ok(())
110 }
111
112 pub fn with_config_file(self, file_name: &str, content: &str) -> Result<Self> {
113 if cfg!(windows) {
114 self.write_file(self.config_file(file_name), &content.replace('\n', "\r\n"))?;
115 } else {
116 self.write_file(self.config_file(file_name), content)?;
117 }
118 Ok(self)
119 }
120
121 pub fn pushd_to_git_root(&self) -> Result<Pushd> {
122 Ok(Pushd::new(self.git_root.clone())?)
123 }
124
125 pub fn pushd_to_subdir(&self) -> Result<Pushd> {
126 let mut subdir = self.git_root.clone();
127 subdir.push("src");
128 Ok(Pushd::new(subdir)?)
129 }
130
131 pub fn git_root(&self) -> PathBuf {
132 self.git_root.clone()
133 }
134
135 pub fn precious_root(&self) -> PathBuf {
136 self.precious_root.clone()
137 }
138
139 pub fn config_file(&self, file_name: &str) -> PathBuf {
140 let mut path = self.precious_root.clone();
141 path.push(file_name);
142 path
143 }
144
145 pub fn all_files(&self) -> Vec<PathBuf> {
146 let mut files = self.paths.clone();
147 files.sort();
148 files
149 }
150
151 pub fn all_files1(&self) -> Vec1<PathBuf> {
152 let mut files = self.paths.clone();
153 files.sort();
154 files.try_into().unwrap()
155 }
156
157 pub fn stage_all(&self) -> Result<()> {
158 self.run_git(vec!["add", "."])
159 }
160
161 pub fn stage_some(&self, files: &[&Path]) -> Result<()> {
162 let mut cmd = vec!["add"];
163 cmd.append(&mut files.iter().map(|f| f.to_str().unwrap()).collect());
164 self.run_git(cmd)
165 }
166
167 pub fn commit_all(&self) -> Result<()> {
168 self.run_git(vec!["commit", "-a", "-m", "committed"])
169 }
170
171 const ROOT_GITIGNORE: &'static str = "
172/**/bar.*
173can_ignore.*
174";
175
176 const TESTS_DATA_GITIGNORE: &'static str = "
177generated.*
178";
179
180 pub fn non_ignored_files() -> Vec<PathBuf> {
181 Self::PATHS
182 .iter()
183 .filter_map(|&p| {
184 if p.contains("can_ignore") || p.contains("bar.") || p.contains("generated.txt") {
185 None
186 } else {
187 Some(PathBuf::from(p))
188 }
189 })
190 .collect()
191 }
192
193 pub fn switch_to_branch(&self, branch: &str, exists: bool) -> Result<()> {
194 let mut args: Vec<&str> = vec!["checkout", "--quiet"];
195 if !exists {
196 args.push("-b");
197 }
198 args.push(branch);
199
200 Exec::builder()
201 .exe("git")
202 .args(args)
203 .ok_exit_codes(&[0])
204 .in_dir(&self.git_root)
205 .build()
206 .run()?;
207 Ok(())
208 }
209
210 pub fn merge_master(&self, expect_fail: bool) -> Result<()> {
211 let mut expect_codes = [0].to_vec();
212 if expect_fail {
213 expect_codes.push(1);
214 }
215
216 Exec::builder()
217 .exe("git")
218 .args(vec!["merge", "--quiet", "--no-ff", "--no-commit", "master"])
219 .ok_exit_codes(&expect_codes)
220 .in_dir(&self.git_root)
221 .ignore_stderr(vec![RERERE_RE.clone()])
222 .build()
223 .run()?;
224 Ok(())
225 }
226
227 pub fn add_gitignore_files(&self) -> Result<Vec<PathBuf>> {
228 self.write_file(&self.root_gitignore_file, Self::ROOT_GITIGNORE)?;
229 self.write_file(&self.tests_data_gitignore_file, Self::TESTS_DATA_GITIGNORE)?;
230
231 Ok(vec![
232 self.root_gitignore_file.clone(),
233 self.tests_data_gitignore_file.clone(),
234 ])
235 }
236
237 fn run_git(&self, args: Vec<&str>) -> Result<()> {
238 Exec::builder()
239 .exe("git")
240 .args(args)
241 .ok_exit_codes(&[0])
242 .in_dir(&self.git_root)
243 .build()
244 .run()?;
245 Ok(())
246 }
247
248 const TO_MODIFY: &'static [&'static str] = &["src/module.rs", "tests/data/foo.txt"];
249
250 pub fn modify_files(&self) -> Result<Vec<PathBuf>> {
251 let mut paths: Vec<PathBuf> = vec![];
252 for p in Self::TO_MODIFY.iter().map(PathBuf::from) {
253 let content = if is_rust_file(&p) {
254 "fn bar() {}\n"
255 } else {
256 "new text"
257 };
258 self.write_file(&p, content)?;
259 paths.push(p.clone());
260 }
261 paths.sort();
262 Ok(paths)
263 }
264
265 pub fn write_file<P: AsRef<Path>>(&self, rel: P, content: &str) -> Result<()> {
266 let mut full = self.precious_root.clone();
267 full.push(rel.as_ref());
268 let parent = full.parent().unwrap();
269 debug!("creating dir at {}", parent.display());
270 fs::create_dir_all(parent)
271 .with_context(|| format!("Creating dir at {}", parent.display()))?;
272 debug!("writing file at {}", full.display());
273 let mut file = fs::File::create(full.clone())
274 .context(format!("Creating file at {}", full.display()))?;
275 file.write_all(content.as_bytes())
276 .context(format!("Writing to file at {}", full.display()))?;
277
278 Ok(())
279 }
280
281 pub fn delete_file<P: AsRef<Path>>(&self, rel: P) -> Result<()> {
282 let mut full = self.precious_root.clone();
283 full.push(rel.as_ref());
284 debug!("deleting path at {}", full.display());
285 if full.is_file() {
286 return Ok(fs::remove_file(full)?);
287 }
288
289 Ok(fs::remove_dir_all(full)?)
290 }
291
292 #[cfg(not(target_os = "windows"))]
293 pub fn read_file(&self, rel: &Path) -> Result<String> {
294 let mut full = self.precious_root.clone();
295 full.push(rel);
296 let content = fs::read_to_string(full.clone())
297 .context(format!("Reading file at {}", full.display()))?;
298
299 Ok(content)
300 }
301}
302
303fn is_rust_file(p: &Path) -> bool {
304 if let Some(e) = p.extension() {
305 let rs = OsString::from("rs");
306 return *e == rs;
307 }
308 false
309}
310
311pub fn maybe_canonicalize(path: &Path) -> Result<PathBuf> {
314 if cfg!(windows) {
315 return Ok(path.to_owned());
316 }
317 Ok(fs::canonicalize(path)?)
318}