Skip to content

Commit b6f573f

Browse files
committed
tests(_internal[file_lock]): Add async tests for FileLock
why: Test async file locking functionality. what: - Add TestAsyncFileLock with 8 async tests - Add TestAsyncAtomicInit with 4 async tests - Tests cover context manager, acquire/release, timeout, concurrent access
1 parent a5a2454 commit b6f573f

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Async tests for libvcs._internal.file_lock."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from pathlib import Path
7+
8+
import pytest
9+
10+
from libvcs._internal.file_lock import (
11+
AsyncAcquireReturnProxy,
12+
AsyncFileLock,
13+
FileLockTimeout,
14+
async_atomic_init,
15+
)
16+
17+
18+
class TestAsyncFileLock:
19+
"""Tests for AsyncFileLock asynchronous operations."""
20+
21+
@pytest.mark.asyncio
22+
async def test_async_context_manager(self, tmp_path: Path) -> None:
23+
"""Test AsyncFileLock as async context manager."""
24+
lock_path = tmp_path / "test.lock"
25+
lock = AsyncFileLock(lock_path)
26+
27+
assert not lock.is_locked
28+
async with lock:
29+
assert lock.is_locked
30+
assert lock_path.exists()
31+
assert not lock.is_locked
32+
33+
@pytest.mark.asyncio
34+
async def test_async_explicit_acquire_release(self, tmp_path: Path) -> None:
35+
"""Test explicit acquire() and release() for async lock."""
36+
lock_path = tmp_path / "test.lock"
37+
lock = AsyncFileLock(lock_path)
38+
39+
proxy = await lock.acquire()
40+
assert isinstance(proxy, AsyncAcquireReturnProxy)
41+
assert lock.is_locked
42+
43+
await lock.release()
44+
assert not lock.is_locked
45+
46+
@pytest.mark.asyncio
47+
async def test_async_reentrant(self, tmp_path: Path) -> None:
48+
"""Test async reentrant locking."""
49+
lock_path = tmp_path / "test.lock"
50+
lock = AsyncFileLock(lock_path)
51+
52+
await lock.acquire()
53+
assert lock.lock_counter == 1
54+
55+
await lock.acquire()
56+
assert lock.lock_counter == 2
57+
58+
await lock.release()
59+
assert lock.lock_counter == 1
60+
61+
await lock.release()
62+
assert lock.lock_counter == 0
63+
64+
@pytest.mark.asyncio
65+
async def test_async_timeout(self, tmp_path: Path) -> None:
66+
"""Test async lock timeout."""
67+
lock_path = tmp_path / "test.lock"
68+
69+
lock1 = AsyncFileLock(lock_path)
70+
await lock1.acquire()
71+
72+
lock2 = AsyncFileLock(lock_path, timeout=0.1)
73+
with pytest.raises(FileLockTimeout):
74+
await lock2.acquire()
75+
76+
await lock1.release()
77+
78+
@pytest.mark.asyncio
79+
async def test_async_non_blocking(self, tmp_path: Path) -> None:
80+
"""Test async non-blocking acquire."""
81+
lock_path = tmp_path / "test.lock"
82+
83+
lock1 = AsyncFileLock(lock_path)
84+
await lock1.acquire()
85+
86+
lock2 = AsyncFileLock(lock_path)
87+
with pytest.raises(FileLockTimeout):
88+
await lock2.acquire(blocking=False)
89+
90+
await lock1.release()
91+
92+
@pytest.mark.asyncio
93+
async def test_async_acquire_proxy_context(self, tmp_path: Path) -> None:
94+
"""Test AsyncAcquireReturnProxy as async context manager."""
95+
lock_path = tmp_path / "test.lock"
96+
lock = AsyncFileLock(lock_path)
97+
98+
proxy = await lock.acquire()
99+
async with proxy as acquired_lock:
100+
assert acquired_lock is lock
101+
assert lock.is_locked
102+
103+
assert not lock.is_locked
104+
105+
@pytest.mark.asyncio
106+
async def test_async_concurrent_acquisition(self, tmp_path: Path) -> None:
107+
"""Test concurrent async lock acquisition."""
108+
lock_path = tmp_path / "test.lock"
109+
results: list[int] = []
110+
111+
async def worker(lock: AsyncFileLock, worker_id: int) -> None:
112+
async with lock:
113+
results.append(worker_id)
114+
await asyncio.sleep(0.01)
115+
116+
lock = AsyncFileLock(lock_path)
117+
await asyncio.gather(*[worker(lock, i) for i in range(3)])
118+
119+
# All workers should have completed
120+
assert len(results) == 3
121+
# Results should be sequential (one at a time)
122+
assert sorted(results) == list(range(3))
123+
124+
@pytest.mark.asyncio
125+
async def test_async_repr(self, tmp_path: Path) -> None:
126+
"""Test __repr__ for async lock."""
127+
lock_path = tmp_path / "test.lock"
128+
lock = AsyncFileLock(lock_path)
129+
130+
assert "unlocked" in repr(lock)
131+
async with lock:
132+
assert "locked" in repr(lock)
133+
134+
135+
class TestAsyncAtomicInit:
136+
"""Tests for async_atomic_init function."""
137+
138+
@pytest.mark.asyncio
139+
async def test_async_atomic_init_first(self, tmp_path: Path) -> None:
140+
"""Test first async_atomic_init performs initialization."""
141+
resource_path = tmp_path / "resource"
142+
resource_path.mkdir()
143+
init_called: list[bool] = []
144+
145+
async def async_init_fn() -> None:
146+
init_called.append(True)
147+
await asyncio.sleep(0)
148+
149+
result = await async_atomic_init(resource_path, async_init_fn)
150+
151+
assert result is True
152+
assert len(init_called) == 1
153+
assert (resource_path / ".initialized").exists()
154+
155+
@pytest.mark.asyncio
156+
async def test_async_atomic_init_already_done(self, tmp_path: Path) -> None:
157+
"""Test async_atomic_init skips when already initialized."""
158+
resource_path = tmp_path / "resource"
159+
resource_path.mkdir()
160+
(resource_path / ".initialized").touch()
161+
162+
init_called: list[bool] = []
163+
164+
async def async_init_fn() -> None:
165+
init_called.append(True)
166+
167+
result = await async_atomic_init(resource_path, async_init_fn)
168+
169+
assert result is False
170+
assert len(init_called) == 0
171+
172+
@pytest.mark.asyncio
173+
async def test_async_atomic_init_sync_fn(self, tmp_path: Path) -> None:
174+
"""Test async_atomic_init works with sync init function."""
175+
resource_path = tmp_path / "resource"
176+
resource_path.mkdir()
177+
init_called: list[bool] = []
178+
179+
def sync_init_fn() -> None:
180+
init_called.append(True)
181+
182+
result = await async_atomic_init(resource_path, sync_init_fn)
183+
184+
assert result is True
185+
assert len(init_called) == 1
186+
187+
@pytest.mark.asyncio
188+
async def test_async_atomic_init_concurrent(self, tmp_path: Path) -> None:
189+
"""Test async_atomic_init handles concurrent calls."""
190+
resource_path = tmp_path / "resource"
191+
resource_path.mkdir()
192+
init_count = {"count": 0}
193+
194+
async def init_fn() -> None:
195+
init_count["count"] += 1
196+
await asyncio.sleep(0.1) # Simulate slow init
197+
198+
results = await asyncio.gather(
199+
*[async_atomic_init(resource_path, init_fn) for _ in range(5)]
200+
)
201+
202+
# Only one should have returned True
203+
assert sum(results) == 1
204+
# Only one init should have run
205+
assert init_count["count"] == 1

0 commit comments

Comments
 (0)