1use crate::error::Result;
2use once_cell::sync::OnceCell;
3use std::fs::{self, File, OpenOptions};
4use std::io;
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
8use std::thread;
9use std::time::{Duration, SystemTime};
10
11static LOCK: OnceCell<Mutex<()>> = OnceCell::new();
12
13pub struct Lock {
14 intraprocess_guard: Guard,
15 lockfile: FileLock,
16}
17
18// High-quality lock to coordinate different #[test] functions within the *same*
19// integration test crate.
20enum Guard {
21 NotLocked,
22 Locked(MutexGuard<'static, ()>),
23}
24
25// Best-effort filesystem lock to coordinate different #[test] functions across
26// *different* integration tests.
27enum FileLock {
28 NotLocked,
29 Locked {
30 path: PathBuf,
31 done: Arc<AtomicBool>,
32 },
33}
34
35impl Lock {
36 pub fn acquire(path: impl AsRef<Path>) -> Result<Self> {
37 Ok(Lock {
38 intraprocess_guard: Guard::acquire(),
39 lockfile: FileLock::acquire(path)?,
40 })
41 }
42}
43
44impl Guard {
45 fn acquire() -> Self {
46 Guard::Locked(
47 LOCK.get_or_init(|| Mutex::new(()))
48 .lock()
49 .unwrap_or_else(PoisonError::into_inner),
50 )
51 }
52}
53
54impl FileLock {
55 fn acquire(path: impl AsRef<Path>) -> Result<Self> {
56 let path = path.as_ref().to_owned();
57 let lockfile = match create(&path) {
58 None => return Ok(FileLock::NotLocked),
59 Some(lockfile) => lockfile,
60 };
61 let done = Arc::new(AtomicBool::new(false));
62 let thread = thread::Builder::new().name("trybuild-flock".to_owned());
63 thread.spawn({
64 let done = Arc::clone(&done);
65 move || poll(lockfile, done)
66 })?;
67 Ok(FileLock::Locked { path, done })
68 }
69}
70
71impl Drop for Lock {
72 fn drop(&mut self) {
73 let Lock {
74 intraprocess_guard,
75 lockfile,
76 } = self;
77 // Unlock file lock first.
78 *lockfile = FileLock::NotLocked;
79 *intraprocess_guard = Guard::NotLocked;
80 }
81}
82
83impl Drop for FileLock {
84 fn drop(&mut self) {
85 match self {
86 FileLock::NotLocked => {}
87 FileLock::Locked { path, done } => {
88 done.store(true, Ordering::Release);
89 let _ = fs::remove_file(path);
90 }
91 }
92 }
93}
94
95fn create(path: &Path) -> Option<File> {
96 loop {
97 match OpenOptions::new().write(true).create_new(true).open(path) {
98 // Acquired lock by creating lockfile.
99 Ok(lockfile) => return Some(lockfile),
100 Err(io_error) => match io_error.kind() {
101 // Lock is already held by another test.
102 io::ErrorKind::AlreadyExists => {}
103 // File based locking isn't going to work for some reason.
104 _ => return None,
105 },
106 }
107
108 // Check whether it's okay to bust the lock.
109 let metadata = match fs::metadata(path) {
110 Ok(metadata) => metadata,
111 Err(io_error) => match io_error.kind() {
112 // Other holder of the lock finished. Retry.
113 io::ErrorKind::NotFound => continue,
114 _ => return None,
115 },
116 };
117
118 let modified = match metadata.modified() {
119 Ok(modified) => modified,
120 Err(_) => return None,
121 };
122
123 let now = SystemTime::now();
124 let considered_stale = now - Duration::from_millis(1500);
125 let considered_future = now + Duration::from_millis(1500);
126 if modified < considered_stale || considered_future < modified {
127 return File::create(path).ok();
128 }
129
130 // Try again shortly.
131 thread::sleep(Duration::from_millis(500));
132 }
133}
134
135// Bump mtime periodically while test directory is in use.
136fn poll(lockfile: File, done: Arc<AtomicBool>) {
137 loop {
138 thread::sleep(Duration::from_millis(500));
139 if done.load(Ordering::Acquire) || lockfile.set_len(0).is_err() {
140 return;
141 }
142 }
143}
144