1use filetime::FileTime;
4use itertools::Itertools;
5use walkdir::WalkDir;
6
7use std::cell::RefCell;
8use std::fs;
9use std::io::{self, ErrorKind};
10use std::path::{Path, PathBuf};
11use std::process::Command;
12use std::sync::Mutex;
13use std::sync::OnceLock;
14use std::sync::atomic::{AtomicUsize, Ordering};
15
16use crate::compare::assert_e2e;
17use crate::compare::match_contains;
18
19static CARGO_INTEGRATION_TEST_DIR: &str = "cit";
20
21static GLOBAL_ROOT: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
22
23fn set_global_root(tmp_dir: &'static str) {
24 let mut lock = GLOBAL_ROOT
25 .get_or_init(|| Default::default())
26 .lock()
27 .unwrap();
28 if lock.is_none() {
29 let mut root = PathBuf::from(tmp_dir);
30 root.push(CARGO_INTEGRATION_TEST_DIR);
31 *lock = Some(root);
32 }
33}
34
35pub fn global_root() -> PathBuf {
39 let lock = GLOBAL_ROOT
40 .get_or_init(|| Default::default())
41 .lock()
42 .unwrap();
43 match lock.as_ref() {
44 Some(p) => p.clone(),
45 None => unreachable!("GLOBAL_ROOT not set yet"),
46 }
47}
48
49thread_local! {
58 static TEST_ID: RefCell<Option<usize>> = const { RefCell::new(None) };
59}
60
61pub struct TestIdGuard {
63 _private: (),
64}
65
66pub fn init_root(tmp_dir: &'static str) -> TestIdGuard {
68 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
69
70 let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
71 TEST_ID.with(|n| *n.borrow_mut() = Some(id));
72
73 let guard = TestIdGuard { _private: () };
74
75 set_global_root(tmp_dir);
76 let r = root();
77 r.rm_rf();
78 r.mkdir_p();
79
80 guard
81}
82
83impl Drop for TestIdGuard {
84 fn drop(&mut self) {
85 TEST_ID.with(|n| *n.borrow_mut() = None);
86 }
87}
88
89pub fn root() -> PathBuf {
93 let id = TEST_ID.with(|n| {
94 n.borrow().expect(
95 "Tests must use the `#[cargo_test]` attribute in \
96 order to be able to use the crate root.",
97 )
98 });
99
100 let mut root = global_root();
101 root.push(&format!("t{}", id));
102 root
103}
104
105pub fn home() -> PathBuf {
109 let mut path = root();
110 path.push("home");
111 path.mkdir_p();
112 path
113}
114
115pub fn cargo_home() -> PathBuf {
119 home().join(".cargo")
120}
121
122pub fn log_dir() -> PathBuf {
126 cargo_home().join("log")
127}
128
129pub fn log_file(idx: usize) -> PathBuf {
135 let log_dir = log_dir();
136
137 let entries = std::fs::read_dir(&log_dir).unwrap();
138 let mut log_files: Vec<_> = entries
139 .filter_map(Result::ok)
140 .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("jsonl"))
141 .collect();
142
143 log_files.sort_unstable_by(|a, b| a.file_name().to_str().cmp(&b.file_name().to_str()));
145
146 assert_eq!(
147 idx + 1,
148 log_files.len(),
149 "unexpected number of log files: {}, expected {}",
150 log_files.len(),
151 idx + 1
152 );
153
154 log_files[idx].path()
155}
156
157pub trait CargoPathExt {
159 fn to_url(&self) -> url::Url;
160
161 fn rm_rf(&self);
162 fn mkdir_p(&self);
163
164 fn ls_r(&self) -> Vec<PathBuf>;
167
168 fn move_into_the_past(&self) {
169 self.move_in_time(|sec, nsec| (sec - 3600, nsec))
170 }
171
172 fn move_into_the_future(&self) {
173 self.move_in_time(|sec, nsec| (sec + 3600, nsec))
174 }
175
176 fn move_in_time<F>(&self, travel_amount: F)
177 where
178 F: Fn(i64, u32) -> (i64, u32);
179
180 fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData);
181
182 fn assert_dir_layout(&self, expected: impl snapbox::IntoData, ignored_path_patterns: &[String]);
183}
184
185impl CargoPathExt for Path {
186 fn to_url(&self) -> url::Url {
187 url::Url::from_file_path(self).ok().unwrap()
188 }
189
190 fn rm_rf(&self) {
191 let meta = match self.symlink_metadata() {
192 Ok(meta) => meta,
193 Err(e) => {
194 if e.kind() == ErrorKind::NotFound {
195 return;
196 }
197 panic!("failed to remove {:?}, could not read: {:?}", self, e);
198 }
199 };
200 if meta.is_dir() {
204 if let Err(e) = fs::remove_dir_all(self) {
205 panic!("failed to remove {:?}: {:?}", self, e)
206 }
207 } else if let Err(e) = fs::remove_file(self) {
208 panic!("failed to remove {:?}: {:?}", self, e)
209 }
210 }
211
212 fn mkdir_p(&self) {
213 fs::create_dir_all(self)
214 .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
215 }
216
217 fn ls_r(&self) -> Vec<PathBuf> {
218 walkdir::WalkDir::new(self)
219 .sort_by_file_name()
220 .into_iter()
221 .filter_map(|e| e.map(|e| e.path().to_owned()).ok())
222 .collect()
223 }
224
225 fn move_in_time<F>(&self, travel_amount: F)
226 where
227 F: Fn(i64, u32) -> (i64, u32),
228 {
229 if self.is_file() {
230 time_travel(self, &travel_amount);
231 } else {
232 recurse(self, &self.join("target"), &travel_amount);
233 }
234
235 fn recurse<F>(p: &Path, bad: &Path, travel_amount: &F)
236 where
237 F: Fn(i64, u32) -> (i64, u32),
238 {
239 if p.is_file() {
240 time_travel(p, travel_amount)
241 } else if !p.starts_with(bad) {
242 for f in t!(fs::read_dir(p)) {
243 let f = t!(f).path();
244 recurse(&f, bad, travel_amount);
245 }
246 }
247 }
248
249 fn time_travel<F>(path: &Path, travel_amount: &F)
250 where
251 F: Fn(i64, u32) -> (i64, u32),
252 {
253 let stat = t!(path.symlink_metadata());
254
255 let mtime = FileTime::from_last_modification_time(&stat);
256
257 let (sec, nsec) = travel_amount(mtime.unix_seconds(), mtime.nanoseconds());
258 let newtime = FileTime::from_unix_time(sec, nsec);
259
260 do_op(path, "set file times", |path| {
263 filetime::set_file_times(path, newtime, newtime)
264 });
265 }
266 }
267
268 #[track_caller]
269 fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
270 self.assert_dir_layout(expected.unordered(), &build_dir_ignored_path_patterns());
277 }
278
279 #[track_caller]
280 fn assert_dir_layout(
281 &self,
282 expected: impl snapbox::IntoData,
283 ignored_path_patterns: &[String],
284 ) {
285 let assert = assert_e2e();
286 let actual = WalkDir::new(self)
287 .sort_by_file_name()
288 .into_iter()
289 .filter_map(|e| e.ok())
290 .filter(|e| e.file_type().is_file())
291 .map(|e| e.path().to_string_lossy().into_owned())
292 .filter(|file| {
293 for ignored in ignored_path_patterns {
294 if match_contains(&ignored, file, &assert.redactions()).is_ok() {
295 return false;
296 }
297 }
298 return true;
299 })
300 .join("\n");
301
302 assert.eq(format!("{actual}\n"), expected);
303 }
304}
305
306impl CargoPathExt for PathBuf {
307 fn to_url(&self) -> url::Url {
308 self.as_path().to_url()
309 }
310
311 fn rm_rf(&self) {
312 self.as_path().rm_rf()
313 }
314 fn mkdir_p(&self) {
315 self.as_path().mkdir_p()
316 }
317
318 fn ls_r(&self) -> Vec<PathBuf> {
319 self.as_path().ls_r()
320 }
321
322 fn move_in_time<F>(&self, travel_amount: F)
323 where
324 F: Fn(i64, u32) -> (i64, u32),
325 {
326 self.as_path().move_in_time(travel_amount)
327 }
328
329 #[track_caller]
330 fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
331 self.as_path().assert_build_dir_layout(expected);
332 }
333
334 #[track_caller]
335 fn assert_dir_layout(
336 &self,
337 expected: impl snapbox::IntoData,
338 ignored_path_patterns: &[String],
339 ) {
340 self.as_path()
341 .assert_dir_layout(expected, ignored_path_patterns);
342 }
343}
344
345fn do_op<F>(path: &Path, desc: &str, mut f: F)
346where
347 F: FnMut(&Path) -> io::Result<()>,
348{
349 match f(path) {
350 Ok(()) => {}
351 Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
352 let mut p = t!(path.metadata()).permissions();
353 p.set_readonly(false);
354 t!(fs::set_permissions(path, p));
355
356 let parent = path.parent().unwrap();
359 let mut p = t!(parent.metadata()).permissions();
360 p.set_readonly(false);
361 t!(fs::set_permissions(parent, p));
362
363 f(path).unwrap_or_else(|e| {
364 panic!("failed to {} {}: {}", desc, path.display(), e);
365 })
366 }
367 Err(e) => {
368 panic!("failed to {} {}: {}", desc, path.display(), e);
369 }
370 }
371}
372
373fn build_dir_ignored_path_patterns() -> Vec<String> {
375 vec![
376 "[..].dSYM/[..]",
379 "[..].pdb",
381 ]
382 .into_iter()
383 .map(ToString::to_string)
384 .collect()
385}
386
387pub fn get_lib_filename(name: &str, kind: &str) -> String {
406 let prefix = get_lib_prefix(kind);
407 let extension = get_lib_extension(kind);
408 format!("{}{}.{}", prefix, name, extension)
409}
410
411pub fn get_lib_prefix(kind: &str) -> &str {
413 match kind {
414 "lib" | "rlib" => "lib",
415 "staticlib" | "dylib" | "proc-macro" => {
416 if cfg!(windows) {
417 ""
418 } else {
419 "lib"
420 }
421 }
422 _ => unreachable!(),
423 }
424}
425
426pub fn get_lib_extension(kind: &str) -> &str {
428 match kind {
429 "lib" | "rlib" => "rlib",
430 "staticlib" => {
431 if cfg!(windows) {
432 "lib"
433 } else {
434 "a"
435 }
436 }
437 "dylib" | "proc-macro" => {
438 if cfg!(windows) {
439 "dll"
440 } else if cfg!(target_os = "macos") {
441 "dylib"
442 } else {
443 "so"
444 }
445 }
446 _ => unreachable!(),
447 }
448}
449
450pub fn sysroot() -> String {
452 let output = Command::new("rustc")
453 .arg("--print=sysroot")
454 .output()
455 .expect("rustc to run");
456 assert!(output.status.success());
457 let sysroot = String::from_utf8(output.stdout).unwrap();
458 sysroot.trim().to_string()
459}
460
461#[cfg(windows)]
467pub fn windows_reserved_names_are_allowed() -> bool {
468 use std::ffi::OsStr;
469 use std::os::windows::ffi::OsStrExt;
470 use std::ptr;
471 use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
472
473 let test_file_name: Vec<_> = OsStr::new("aux.rs").encode_wide().chain([0]).collect();
474
475 let buffer_length =
476 unsafe { GetFullPathNameW(test_file_name.as_ptr(), 0, ptr::null_mut(), ptr::null_mut()) };
477
478 if buffer_length == 0 {
479 return false;
481 }
482
483 let mut buffer = vec![0u16; buffer_length as usize];
484
485 let result = unsafe {
486 GetFullPathNameW(
487 test_file_name.as_ptr(),
488 buffer_length,
489 buffer.as_mut_ptr(),
490 ptr::null_mut(),
491 )
492 };
493
494 if result == 0 {
495 return false;
498 }
499
500 let prefix: Vec<_> = OsStr::new("\\\\.\\").encode_wide().collect();
505 if buffer.starts_with(&prefix) {
506 false
507 } else {
508 true
509 }
510}