1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//! Tests for [`super`] — the cross-process advisory lock primitive (#5344).
use super::{lock_path, with_exclusive_lock};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::TempDir;
/// The sidecar sits next to the guarded file and is named after it.
#[test]
fn lock_path_is_a_sidecar() {
let p = Path::new("/tmp/some/dir/indexes.toml");
assert_eq!(
lock_path(p),
Path::new("/tmp/some/dir/indexes.toml.lock"),
"the lock must be a sibling sidecar, never the guarded file itself"
);
}
/// Why: the whole point of the module — two independently-opened descriptors on
/// the same sidecar must not both be inside the critical section. Each thread
/// opens its OWN descriptor, which is exactly the conflict a separate process
/// produces; no in-process mutex is involved.
/// What: 8 threads × 5 rounds each increment a shared counter while asserting
/// that no other thread is inside the section.
/// Test: this IS the test.
#[test]
fn with_exclusive_lock_serialises_separate_descriptors() {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("guarded.toml");
let inside = AtomicUsize::new(0);
let total = AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..8 {
let path = path.clone();
let inside = &inside;
let total = &total;
scope.spawn(move || {
for _ in 0..5 {
with_exclusive_lock(&path, || {
assert_eq!(
inside.fetch_add(1, Ordering::SeqCst),
0,
"two holders inside the critical section at once"
);
std::thread::yield_now();
total.fetch_add(1, Ordering::SeqCst);
inside.fetch_sub(1, Ordering::SeqCst);
})
.expect("lock acquisition");
}
});
}
});
assert_eq!(total.load(Ordering::SeqCst), 40);
}
/// Why: RAII release must survive a panicking closure, or one bad write would
/// wedge every later writer of that file for the process's lifetime.
/// What: panics inside the section, then proves a later acquisition succeeds.
/// Test: this IS the test.
#[test]
fn with_exclusive_lock_releases_on_panic() {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("guarded.toml");
let panicked = std::panic::catch_unwind(|| {
let _ = with_exclusive_lock(&path, || panic!("boom"));
});
assert!(panicked.is_err(), "the closure's panic must propagate");
let ran = with_exclusive_lock(&path, || 42).expect("lock must be free again");
assert_eq!(ran, 42);
}
/// Why: fail-closed. An unusable lock path must be an error and the closure
/// must never run — running it unlocked is the lost-update bug itself.
/// What: points the sidecar's parent at a regular FILE so `create_dir_all`
/// cannot succeed, and asserts the closure was not invoked.
/// Test: this IS the test.
#[test]
fn with_exclusive_lock_unopenable_errors() {
let dir = TempDir::new().expect("tempdir");
let blocker = dir.path().join("not-a-dir");
std::fs::write(&blocker, b"x").expect("write blocker");
let path = blocker.join("guarded.toml");
let ran = AtomicUsize::new(0);
let result = with_exclusive_lock(&path, || {
ran.fetch_add(1, Ordering::SeqCst);
});
assert!(result.is_err(), "an unusable lock path must be an error");
assert_eq!(
ran.load(Ordering::SeqCst),
0,
"the closure must never run unlocked"
);
}