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 trait CargoPathExt {
124 fn to_url(&self) -> url::Url;
125
126 fn rm_rf(&self);
127 fn mkdir_p(&self);
128
129 fn ls_r(&self) -> Vec<PathBuf>;
132
133 fn move_into_the_past(&self) {
134 self.move_in_time(|sec, nsec| (sec - 3600, nsec))
135 }
136
137 fn move_into_the_future(&self) {
138 self.move_in_time(|sec, nsec| (sec + 3600, nsec))
139 }
140
141 fn move_in_time<F>(&self, travel_amount: F)
142 where
143 F: Fn(i64, u32) -> (i64, u32);
144
145 fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData);
146
147 fn assert_dir_layout(&self, expected: impl snapbox::IntoData, ignored_path_patterns: &[String]);
148}
149
150impl CargoPathExt for Path {
151 fn to_url(&self) -> url::Url {
152 url::Url::from_file_path(self).ok().unwrap()
153 }
154
155 fn rm_rf(&self) {
156 let meta = match self.symlink_metadata() {
157 Ok(meta) => meta,
158 Err(e) => {
159 if e.kind() == ErrorKind::NotFound {
160 return;
161 }
162 panic!("failed to remove {:?}, could not read: {:?}", self, e);
163 }
164 };
165 if meta.is_dir() {
169 if let Err(e) = fs::remove_dir_all(self) {
170 panic!("failed to remove {:?}: {:?}", self, e)
171 }
172 } else if let Err(e) = fs::remove_file(self) {
173 panic!("failed to remove {:?}: {:?}", self, e)
174 }
175 }
176
177 fn mkdir_p(&self) {
178 fs::create_dir_all(self)
179 .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
180 }
181
182 fn ls_r(&self) -> Vec<PathBuf> {
183 walkdir::WalkDir::new(self)
184 .sort_by_file_name()
185 .into_iter()
186 .filter_map(|e| e.map(|e| e.path().to_owned()).ok())
187 .collect()
188 }
189
190 fn move_in_time<F>(&self, travel_amount: F)
191 where
192 F: Fn(i64, u32) -> (i64, u32),
193 {
194 if self.is_file() {
195 time_travel(self, &travel_amount);
196 } else {
197 recurse(self, &self.join("target"), &travel_amount);
198 }
199
200 fn recurse<F>(p: &Path, bad: &Path, travel_amount: &F)
201 where
202 F: Fn(i64, u32) -> (i64, u32),
203 {
204 if p.is_file() {
205 time_travel(p, travel_amount)
206 } else if !p.starts_with(bad) {
207 for f in t!(fs::read_dir(p)) {
208 let f = t!(f).path();
209 recurse(&f, bad, travel_amount);
210 }
211 }
212 }
213
214 fn time_travel<F>(path: &Path, travel_amount: &F)
215 where
216 F: Fn(i64, u32) -> (i64, u32),
217 {
218 let stat = t!(path.symlink_metadata());
219
220 let mtime = FileTime::from_last_modification_time(&stat);
221
222 let (sec, nsec) = travel_amount(mtime.unix_seconds(), mtime.nanoseconds());
223 let newtime = FileTime::from_unix_time(sec, nsec);
224
225 do_op(path, "set file times", |path| {
228 filetime::set_file_times(path, newtime, newtime)
229 });
230 }
231 }
232
233 #[track_caller]
234 fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
235 self.assert_dir_layout(expected.unordered(), &build_dir_ignored_path_patterns());
242 }
243
244 #[track_caller]
245 fn assert_dir_layout(
246 &self,
247 expected: impl snapbox::IntoData,
248 ignored_path_patterns: &[String],
249 ) {
250 let assert = assert_e2e();
251 let actual = WalkDir::new(self)
252 .sort_by_file_name()
253 .into_iter()
254 .filter_map(|e| e.ok())
255 .filter(|e| e.file_type().is_file())
256 .map(|e| e.path().to_string_lossy().into_owned())
257 .filter(|file| {
258 for ignored in ignored_path_patterns {
259 if match_contains(&ignored, file, &assert.redactions()).is_ok() {
260 return false;
261 }
262 }
263 return true;
264 })
265 .join("\n");
266
267 assert.eq(format!("{actual}\n"), expected);
268 }
269}
270
271impl CargoPathExt for PathBuf {
272 fn to_url(&self) -> url::Url {
273 self.as_path().to_url()
274 }
275
276 fn rm_rf(&self) {
277 self.as_path().rm_rf()
278 }
279 fn mkdir_p(&self) {
280 self.as_path().mkdir_p()
281 }
282
283 fn ls_r(&self) -> Vec<PathBuf> {
284 self.as_path().ls_r()
285 }
286
287 fn move_in_time<F>(&self, travel_amount: F)
288 where
289 F: Fn(i64, u32) -> (i64, u32),
290 {
291 self.as_path().move_in_time(travel_amount)
292 }
293
294 #[track_caller]
295 fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
296 self.as_path().assert_build_dir_layout(expected);
297 }
298
299 #[track_caller]
300 fn assert_dir_layout(
301 &self,
302 expected: impl snapbox::IntoData,
303 ignored_path_patterns: &[String],
304 ) {
305 self.as_path()
306 .assert_dir_layout(expected, ignored_path_patterns);
307 }
308}
309
310fn do_op<F>(path: &Path, desc: &str, mut f: F)
311where
312 F: FnMut(&Path) -> io::Result<()>,
313{
314 match f(path) {
315 Ok(()) => {}
316 Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
317 let mut p = t!(path.metadata()).permissions();
318 p.set_readonly(false);
319 t!(fs::set_permissions(path, p));
320
321 let parent = path.parent().unwrap();
324 let mut p = t!(parent.metadata()).permissions();
325 p.set_readonly(false);
326 t!(fs::set_permissions(parent, p));
327
328 f(path).unwrap_or_else(|e| {
329 panic!("failed to {} {}: {}", desc, path.display(), e);
330 })
331 }
332 Err(e) => {
333 panic!("failed to {} {}: {}", desc, path.display(), e);
334 }
335 }
336}
337
338fn build_dir_ignored_path_patterns() -> Vec<String> {
340 vec![
341 "[..].dSYM/[..]",
344 "[..].pdb",
346 ]
347 .into_iter()
348 .map(ToString::to_string)
349 .collect()
350}
351
352pub fn get_lib_filename(name: &str, kind: &str) -> String {
371 let prefix = get_lib_prefix(kind);
372 let extension = get_lib_extension(kind);
373 format!("{}{}.{}", prefix, name, extension)
374}
375
376pub fn get_lib_prefix(kind: &str) -> &str {
378 match kind {
379 "lib" | "rlib" => "lib",
380 "staticlib" | "dylib" | "proc-macro" => {
381 if cfg!(windows) {
382 ""
383 } else {
384 "lib"
385 }
386 }
387 _ => unreachable!(),
388 }
389}
390
391pub fn get_lib_extension(kind: &str) -> &str {
393 match kind {
394 "lib" | "rlib" => "rlib",
395 "staticlib" => {
396 if cfg!(windows) {
397 "lib"
398 } else {
399 "a"
400 }
401 }
402 "dylib" | "proc-macro" => {
403 if cfg!(windows) {
404 "dll"
405 } else if cfg!(target_os = "macos") {
406 "dylib"
407 } else {
408 "so"
409 }
410 }
411 _ => unreachable!(),
412 }
413}
414
415pub fn sysroot() -> String {
417 let output = Command::new("rustc")
418 .arg("--print=sysroot")
419 .output()
420 .expect("rustc to run");
421 assert!(output.status.success());
422 let sysroot = String::from_utf8(output.stdout).unwrap();
423 sysroot.trim().to_string()
424}
425
426#[cfg(windows)]
432pub fn windows_reserved_names_are_allowed() -> bool {
433 use std::ffi::OsStr;
434 use std::os::windows::ffi::OsStrExt;
435 use std::ptr;
436 use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
437
438 let test_file_name: Vec<_> = OsStr::new("aux.rs").encode_wide().chain([0]).collect();
439
440 let buffer_length =
441 unsafe { GetFullPathNameW(test_file_name.as_ptr(), 0, ptr::null_mut(), ptr::null_mut()) };
442
443 if buffer_length == 0 {
444 return false;
446 }
447
448 let mut buffer = vec![0u16; buffer_length as usize];
449
450 let result = unsafe {
451 GetFullPathNameW(
452 test_file_name.as_ptr(),
453 buffer_length,
454 buffer.as_mut_ptr(),
455 ptr::null_mut(),
456 )
457 };
458
459 if result == 0 {
460 return false;
463 }
464
465 let prefix: Vec<_> = OsStr::new("\\\\.\\").encode_wide().collect();
470 if buffer.starts_with(&prefix) {
471 false
472 } else {
473 true
474 }
475}