Skip to content

Commit ea8e37a

Browse files
committed
Adding FileLock Tests
1 parent 689434a commit ea8e37a

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import Foundation
2+
import SystemPackage
3+
import Testing
4+
5+
@testable import SwiftlyCore
6+
7+
@Suite struct FileLockTests {
8+
@Test("FileLock creation writes process ID to lock file")
9+
func testFileLockCreation() async throws {
10+
try await SwiftlyTests.withTestHome {
11+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "test.lock"
12+
13+
let lock = try FileLock(at: lockPath)
14+
15+
// Verify lock file exists
16+
#expect(try await fs.exists(atPath: lockPath))
17+
18+
// Verify lock file contains process ID
19+
let lockData = try Data(contentsOf: URL(fileURLWithPath: lockPath.string))
20+
let lockContent = String(data: lockData, encoding: .utf8)
21+
let expectedPID = Foundation.ProcessInfo.processInfo.processIdentifier.description
22+
#expect(lockContent == expectedPID)
23+
24+
try await lock.unlock()
25+
}
26+
}
27+
28+
@Test("FileLock fails when lock file already exists")
29+
func testFileLockConflict() async throws {
30+
try await SwiftlyTests.withTestHome {
31+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "conflict.lock"
32+
33+
// Create first lock
34+
let firstLock = try FileLock(at: lockPath)
35+
36+
// Attempt to create second lock should fail
37+
#expect(throws: FileLockError.cannotAcquireLock) {
38+
try FileLock(at: lockPath)
39+
}
40+
41+
try await firstLock.unlock()
42+
}
43+
}
44+
45+
@Test("FileLock unlock removes lock file")
46+
func testFileLockUnlock() async throws {
47+
try await SwiftlyTests.withTestHome {
48+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "unlock.lock"
49+
50+
let lock = try FileLock(at: lockPath)
51+
#expect(try await fs.exists(atPath: lockPath))
52+
53+
try await lock.unlock()
54+
#expect(!(try await fs.exists(atPath: lockPath)))
55+
}
56+
}
57+
58+
@Test("FileLock can be reacquired after unlock")
59+
func testFileLockReacquisition() async throws {
60+
try await SwiftlyTests.withTestHome {
61+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "reacquire.lock"
62+
63+
// First acquisition
64+
let firstLock = try FileLock(at: lockPath)
65+
try await firstLock.unlock()
66+
67+
// Second acquisition should succeed
68+
let secondLock = try FileLock(at: lockPath)
69+
try await secondLock.unlock()
70+
}
71+
}
72+
73+
@Test("waitForLock succeeds immediately when no lock exists")
74+
func testWaitForLockImmediate() async throws {
75+
try await SwiftlyTests.withTestHome {
76+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "immediate.lock"
77+
let time = Date()
78+
let lock = try await FileLock.waitForLock(lockPath, timeout: 1.0, pollingInterval: 0.1)
79+
let duration = Date().timeIntervalSince(time)
80+
#expect(duration < 1.0)
81+
#expect(try await fs.exists(atPath: lockPath))
82+
try await lock.unlock()
83+
}
84+
}
85+
86+
@Test("waitForLock times out when lock cannot be acquired")
87+
func testWaitForLockTimeout() async throws {
88+
try await SwiftlyTests.withTestHome {
89+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "timeout.lock"
90+
91+
// Create existing lock
92+
let existingLock = try FileLock(at: lockPath)
93+
94+
// Attempt to wait for lock should timeout
95+
await #expect(throws: FileLockError.timeoutExceeded) {
96+
try await FileLock.waitForLock(lockPath, timeout: 0.5, pollingInterval: 0.1)
97+
}
98+
99+
try await existingLock.unlock()
100+
}
101+
}
102+
103+
@Test("waitForLock succeeds when lock becomes available")
104+
func testWaitForLockEventualSuccess() async throws {
105+
try await SwiftlyTests.withTestHome {
106+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "eventual.lock"
107+
108+
// Create initial lock
109+
let initialLock = try FileLock(at: lockPath)
110+
111+
// Task to release lock after delay
112+
let releaseTask = Task {
113+
try await Task.sleep(for: .seconds(0.3))
114+
try await initialLock.unlock()
115+
}
116+
117+
// Wait for lock - should succeed after release
118+
let waitingLock = try await FileLock.waitForLock(lockPath, timeout: 1.0, pollingInterval: 0.1)
119+
try await waitingLock.unlock()
120+
try await releaseTask.value
121+
}
122+
}
123+
124+
@Test("withLock executes action and automatically unlocks")
125+
func testWithLockSuccess() async throws {
126+
try await SwiftlyTests.withTestHome {
127+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "withlock.lock"
128+
var actionExecuted = false
129+
130+
let result = try await withLock(lockPath, timeout: 1.0, pollingInterval: 0.1) {
131+
actionExecuted = true
132+
return "success"
133+
}
134+
135+
#expect(actionExecuted)
136+
#expect(result == "success")
137+
#expect(!(try await fs.exists(atPath: lockPath)))
138+
}
139+
}
140+
141+
@Test("withLock unlocks even when action throws")
142+
func testWithLockErrorHandling() async throws {
143+
try await SwiftlyTests.withTestHome {
144+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "withlockError.lock"
145+
146+
struct TestError: Error {}
147+
148+
await #expect(throws: TestError.self) {
149+
try await withLock(lockPath, timeout: 1.0, pollingInterval: 0.1) {
150+
throw TestError()
151+
}
152+
}
153+
154+
// Lock should be released even after error
155+
let exists = try await fs.exists(atPath: lockPath)
156+
#expect(!exists)
157+
}
158+
}
159+
160+
@Test("withLock fails when lock cannot be acquired within timeout")
161+
func testWithLockTimeout() async throws {
162+
try await SwiftlyTests.withTestHome {
163+
let lockPath = SwiftlyTests.ctx.mockedHomeDir! / "withlockTimeout.lock"
164+
165+
// Create existing lock
166+
let existingLock = try FileLock(at: lockPath)
167+
168+
await #expect(throws: SwiftlyError.self) {
169+
try await withLock(lockPath, timeout: 0.5, pollingInterval: 0.1) {
170+
"should not execute"
171+
}
172+
}
173+
174+
try await existingLock.unlock()
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)