Add New Basic Tests

This commit is contained in:
David Sangrey committed 2026-01-25 16:03:13 -05:00
1 parent ccf7fbc164
commit 9b95ca5ca2
19 files changed
+1710 -147

No files matched your search

+79 -56
View File
@@ -1,4 +1,7 @@
# flake8: noqa
# mypy: ignore-errors
"""Tests for journal_lock.py code."""
from __future__ import annotations
import multiprocessing as mp
@@ -25,22 +28,22 @@ def other_process_lock(continue_q: mp.Queue, exit_q: mp.Queue, lockfile: pathlib
:param exit_q: When there's an item in this, exit.
:param lockfile: Path where the lockfile should be.
"""
with open(lockfile / 'edmc-journal-lock.txt', mode='w+') as lf:
print(f'sub-process: Opened {lockfile} for read...')
with open(lockfile / "edmc-journal-lock.txt", mode="w+") as lf:
print(f"sub-process: Opened {lockfile} for read...")
# This needs to be kept in sync with journal_lock.py:_obtain_lock()
if not _obtain_lock('sub-process', lf):
print('sub-process: Failed to get lock, so returning')
if not _obtain_lock("sub-process", lf):
print("sub-process: Failed to get lock, so returning")
return
print('sub-process: Got lock, telling main process to go...')
continue_q.put('go', timeout=5)
print("sub-process: Got lock, telling main process to go...")
continue_q.put("go", timeout=5)
# Wait for signal to exit
print('sub-process: Waiting for exit signal...')
print("sub-process: Waiting for exit signal...")
exit_q.get(block=True, timeout=None)
# And clean up
_release_lock('sub-process', lf)
os.unlink(lockfile / 'edmc-journal-lock.txt')
_release_lock("sub-process", lf)
os.unlink(lockfile / "edmc-journal-lock.txt")
def _obtain_lock(prefix: str, filehandle) -> bool:
@@ -51,26 +54,27 @@ def _obtain_lock(prefix: str, filehandle) -> bool:
:param filehandle: File handle already open on the lockfile.
:return: bool - True if we obtained the lock.
"""
if sys.platform == 'win32':
print(f'{prefix}: On win32')
if sys.platform == "win32":
print(f"{prefix}: On win32")
import msvcrt
try:
print(f'{prefix}: Trying msvcrt.locking() ...')
print(f"{prefix}: Trying msvcrt.locking() ...")
msvcrt.locking(filehandle.fileno(), msvcrt.LK_NBLCK, 4096)
except Exception as e:
print(f'{prefix}: Unable to lock file: {e!r}')
print(f"{prefix}: Unable to lock file: {e!r}")
return False
else:
import fcntl
print(f'{prefix}: Not win32, using fcntl')
print(f"{prefix}: Not win32, using fcntl")
try:
fcntl.flock(filehandle, fcntl.LOCK_EX | fcntl.LOCK_NB)
except Exception as e:
print(f'{prefix}: Unable to lock file: {e!r}')
print(f"{prefix}: Unable to lock file: {e!r}")
return False
return True
@@ -84,30 +88,33 @@ def _release_lock(prefix: str, filehandle) -> bool:
:param filehandle: File handle already open on the lockfile.
:return: bool - True if we released the lock.
"""
if sys.platform == 'win32':
print(f'{prefix}: On win32')
if sys.platform == "win32":
print(f"{prefix}: On win32")
import msvcrt
try:
print(f'{prefix}: Trying msvcrt.locking() ...')
print(f"{prefix}: Trying msvcrt.locking() ...")
filehandle.seek(0)
msvcrt.locking(filehandle.fileno(), msvcrt.LK_UNLCK, 4096)
except Exception as e:
print(f'{prefix}: Unable to unlock file: {e!r}')
print(f"{prefix}: Unable to unlock file: {e!r}")
return False
else:
import fcntl
print(f'{prefix}: Not win32, using fcntl')
print(f"{prefix}: Not win32, using fcntl")
try:
fcntl.flock(filehandle, fcntl.LOCK_UN)
except Exception as e:
print(f'{prefix}: Unable to unlock file: {e!r}')
print(f"{prefix}: Unable to unlock file: {e!r}")
return False
return True
###########################################################################
@@ -116,16 +123,16 @@ class TestJournalLock:
@pytest.fixture
def mock_journaldir(
self, monkeypatch: MonkeyPatch,
tmp_path_factory: TempdirFactory
self, monkeypatch: MonkeyPatch, tmp_path_factory: TempdirFactory
) -> Generator:
"""Fixture for mocking config.get_str('journaldir')."""
def get_str(key: str, *, default: str | None = None) -> str:
"""Mock config.*Config get_str to provide fake journaldir."""
if key == 'journaldir':
if key == "journaldir":
return str(tmp_path_factory.getbasetemp())
print('Other key, calling up ...')
print("Other key, calling up ...")
return config.get_str(key) # Call the non-mocked
with monkeypatch.context() as m:
@@ -134,17 +141,16 @@ class TestJournalLock:
@pytest.fixture
def mock_journaldir_changing(
self,
monkeypatch: MonkeyPatch,
tmp_path_factory: TempdirFactory
self, monkeypatch: MonkeyPatch, tmp_path_factory: TempdirFactory
) -> Generator:
"""Fixture for mocking config.get_str('journaldir')."""
def get_str(key: str, *, default: str | None = None) -> str:
"""Mock config.*Config get_str to provide fake journaldir."""
if key == 'journaldir':
if key == "journaldir":
return tmp_path_factory.mktemp("changing") # type: ignore
print('Other key, calling up ...')
print("Other key, calling up ...")
return config.get_str(key) # Call the non-mocked
with monkeypatch.context() as m:
@@ -155,7 +161,7 @@ class TestJournalLock:
# Tests against JournalLock.__init__()
def test_journal_lock_init(self, mock_journaldir: TempPathFactory):
"""Test JournalLock instantiation."""
print(f'{type(mock_journaldir)=}')
print(f"{type(mock_journaldir)=}")
tmpdir = str(mock_journaldir.getbasetemp())
jlock = JournalLock()
@@ -215,32 +221,43 @@ class TestJournalLock:
def test_obtain_lock_with_tmpdir_ro(self, mock_journaldir: TempPathFactory):
"""Test JournalLock.obtain_lock() with read-only tmpdir."""
tmpdir = str(mock_journaldir.getbasetemp())
print(f'{tmpdir=}')
print(f"{tmpdir=}")
# Make tmpdir read-only ?
if sys.platform == 'win32':
if sys.platform == "win32":
# Ref: <https://stackoverflow.com/a/12168268>
import ntsecuritycon as con
import win32security
# Fetch user details
winuser, domain, type = win32security.LookupAccountName("", os.environ.get('USERNAME'))
winuser, domain, type = win32security.LookupAccountName(
"", os.environ.get("USERNAME")
)
# Fetch the current security of tmpdir for that user.
sd = win32security.GetFileSecurity(tmpdir, win32security.DACL_SECURITY_INFORMATION)
dacl = sd.GetSecurityDescriptorDacl() # instead of dacl = win32security.ACL()
sd = win32security.GetFileSecurity(
tmpdir, win32security.DACL_SECURITY_INFORMATION
)
dacl = (
sd.GetSecurityDescriptorDacl()
) # instead of dacl = win32security.ACL()
# Add Write to Denied list
# con.FILE_WRITE_DATA results in a 'Special permissions' being
# listed on Properties > Security for the user in the 'Deny' column.
# Clicking through to 'Advanced' shows a 'Deny' for
# 'Create files / write data'.
dacl.AddAccessDeniedAce(win32security.ACL_REVISION, con.FILE_WRITE_DATA, winuser)
dacl.AddAccessDeniedAce(
win32security.ACL_REVISION, con.FILE_WRITE_DATA, winuser
)
# Apply that change.
sd.SetSecurityDescriptorDacl(1, dacl, 0) # may not be necessary
win32security.SetFileSecurity(tmpdir, win32security.DACL_SECURITY_INFORMATION, sd)
win32security.SetFileSecurity(
tmpdir, win32security.DACL_SECURITY_INFORMATION, sd
)
else:
import stat
os.chmod(tmpdir, stat.S_IRUSR | stat.S_IXUSR)
jlock = JournalLock()
@@ -249,7 +266,7 @@ class TestJournalLock:
locked = jlock.obtain_lock()
# Revert permissions for test cleanup
if sys.platform == 'win32':
if sys.platform == "win32":
# We can reuse winuser etc from before
import pywintypes
@@ -258,12 +275,17 @@ class TestJournalLock:
i = 0
ace = dacl.GetAce(i)
while ace:
if ace[0] == (con.ACCESS_DENIED_ACE_TYPE, 0) and ace[1] == con.FILE_WRITE_DATA:
if (
ace[0] == (con.ACCESS_DENIED_ACE_TYPE, 0)
and ace[1] == con.FILE_WRITE_DATA
):
# Delete the Ace that we added
dacl.DeleteAce(i)
# Apply that change.
sd.SetSecurityDescriptorDacl(1, dacl, 0) # may not be necessary
win32security.SetFileSecurity(tmpdir, win32security.DACL_SECURITY_INFORMATION, sd)
win32security.SetFileSecurity(
tmpdir, win32security.DACL_SECURITY_INFORMATION, sd
)
break
i += 1
@@ -283,16 +305,17 @@ class TestJournalLock:
"""Test JournalLock.obtain_lock() with tmpdir."""
continue_q: mp.Queue = mp.Queue()
exit_q: mp.Queue = mp.Queue()
locker = mp.Process(target=other_process_lock,
args=(continue_q, exit_q, mock_journaldir.getbasetemp())
)
print('Starting sub-process other_process_lock()...')
locker = mp.Process(
target=other_process_lock,
args=(continue_q, exit_q, mock_journaldir.getbasetemp()),
)
print("Starting sub-process other_process_lock()...")
locker.start()
# Wait for the sub-process to have locked
print('Waiting for "go" signal from sub-process...')
continue_q.get(block=True, timeout=5)
print('Attempt actual lock test...')
print("Attempt actual lock test...")
# Now attempt to lock with to-test code
jlock = JournalLock()
second_attempt = jlock.obtain_lock()
@@ -303,11 +326,11 @@ class TestJournalLock:
# on later tests.
jlock.journal_dir_lockfile.close() # type: ignore
print('Telling sub-process to quit...')
exit_q.put('quit')
print('Waiting for sub-process...')
print("Telling sub-process to quit...")
exit_q.put("quit")
print("Waiting for sub-process...")
locker.join()
print('Done.')
print("Done.")
###########################################################################
# Tests against JournalLock.release_lock()
@@ -322,9 +345,11 @@ class TestJournalLock:
assert jlock.release_lock()
# And finally check it actually IS unlocked.
with open(mock_journaldir.getbasetemp() / 'edmc-journal-lock.txt', mode='w+') as lf:
assert _obtain_lock('release-lock', lf)
assert _release_lock('release-lock', lf)
with open(
mock_journaldir.getbasetemp() / "edmc-journal-lock.txt", mode="w+"
) as lf:
assert _obtain_lock("release-lock", lf)
assert _release_lock("release-lock", lf)
# Cleanup, to avoid side-effect on other tests
os.unlink(str(jlock.journal_dir_lockfile_name))
@@ -342,9 +367,7 @@ class TestJournalLock:
###########################################################################
# Tests against JournalLock.update_lock()
def test_update_lock(
self,
mock_journaldir_changing: TempPathFactory):
def test_update_lock(self, mock_journaldir_changing: TempPathFactory):
"""
Test JournalLock.update_lock().