1#![allow(missing_docs)]
16
17mod fallback;
18#[cfg(unix)]
19mod unix;
20
21use std::io;
22use std::path::PathBuf;
23
24use thiserror::Error;
25
26#[cfg(not(unix))]
27pub use self::fallback::FileLock;
28#[cfg(unix)]
29pub use self::unix::FileLock;
30
31#[derive(Debug, Error)]
32#[error("{message}: {path}")]
33pub struct FileLockError {
34 pub message: &'static str,
35 pub path: PathBuf,
36 #[source]
37 pub err: io::Error,
38}
39
40#[cfg(test)]
41mod tests {
42 use std::cmp::max;
43 use std::fs;
44 use std::thread;
45 use std::time::Duration;
46
47 use test_case::test_case;
48
49 use super::*;
50 use crate::tests::new_temp_dir;
51
52 #[test_case(FileLock::lock)]
53 #[cfg_attr(unix, test_case(fallback::FileLock::lock))]
54 fn lock_basic<T>(lock_fn: fn(PathBuf) -> Result<T, FileLockError>) {
55 let temp_dir = new_temp_dir();
56 let lock_path = temp_dir.path().join("test.lock");
57 assert!(!lock_path.exists());
58 {
59 let _lock = lock_fn(lock_path.clone()).unwrap();
60 assert!(lock_path.exists());
61 }
62 assert!(!lock_path.exists());
63 }
64
65 #[test_case(FileLock::lock)]
66 #[cfg_attr(unix, test_case(fallback::FileLock::lock))]
67 fn lock_concurrent<T>(lock_fn: fn(PathBuf) -> Result<T, FileLockError>) {
68 let temp_dir = new_temp_dir();
69 let data_path = temp_dir.path().join("test");
70 let lock_path = temp_dir.path().join("test.lock");
71 fs::write(&data_path, 0_u32.to_le_bytes()).unwrap();
72 let num_threads = max(num_cpus::get(), 4);
73 thread::scope(|s| {
74 for _ in 0..num_threads {
75 s.spawn(|| {
76 let _lock = lock_fn(lock_path.clone()).unwrap();
77 let data = fs::read(&data_path).unwrap();
78 let value = u32::from_le_bytes(data.try_into().unwrap());
79 thread::sleep(Duration::from_millis(1));
80 fs::write(&data_path, (value + 1).to_le_bytes()).unwrap();
81 });
82 }
83 });
84 let data = fs::read(&data_path).unwrap();
85 let value = u32::from_le_bytes(data.try_into().unwrap());
86 assert_eq!(value, num_threads as u32);
87 }
88}