From 3c6363718cd16178d1b559b15b745aa4829362a2 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Tue, 27 Jan 2026 12:35:03 -0800 Subject: [PATCH] Revert "Add failing tests for #403: File Handle Resource Leak in SyncLock.acquire()" --- pdd/sync_determine_operation.py | 32 +- tests/test_e2e_issue_403_file_handle_leak.py | 519 ------------------- tests/test_sync_determine_operation.py | 193 ------- 3 files changed, 12 insertions(+), 732 deletions(-) delete mode 100644 tests/test_e2e_issue_403_file_handle_leak.py diff --git a/pdd/sync_determine_operation.py b/pdd/sync_determine_operation.py index 8798ad17..4793020c 100644 --- a/pdd/sync_determine_operation.py +++ b/pdd/sync_determine_operation.py @@ -181,26 +181,18 @@ def acquire(self): # Create lock file and acquire file descriptor lock self.lock_file.touch() self.fd = open(self.lock_file, 'w') - - try: - # Critical section - must close file if anything fails - if HAS_FCNTL: - # POSIX systems - fcntl.flock(self.fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) - elif HAS_MSVCRT: - # Windows systems - msvcrt.locking(self.fd.fileno(), msvcrt.LK_NBLCK, 1) - - # Write current PID to lock file - self.fd.write(str(self.current_pid)) - self.fd.flush() - except: - # Close file on ANY exception (not just IOError/OSError) - if self.fd: - self.fd.close() - self.fd = None - raise - + + if HAS_FCNTL: + # POSIX systems + fcntl.flock(self.fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + elif HAS_MSVCRT: + # Windows systems + msvcrt.locking(self.fd.fileno(), msvcrt.LK_NBLCK, 1) + + # Write current PID to lock file + self.fd.write(str(self.current_pid)) + self.fd.flush() + except (IOError, OSError) as e: if self.fd: self.fd.close() diff --git a/tests/test_e2e_issue_403_file_handle_leak.py b/tests/test_e2e_issue_403_file_handle_leak.py deleted file mode 100644 index 3b79d9d1..00000000 --- a/tests/test_e2e_issue_403_file_handle_leak.py +++ /dev/null @@ -1,519 +0,0 @@ -""" -E2E Test for Issue #403: File Handle Resource Leak in SyncLock.acquire() - -This test verifies the bug at a system level by simulating what happens when -a user runs a sync operation and interrupts it with Ctrl+C (KeyboardInterrupt). - -The Bug: -- SyncLock.acquire() opens a file at line 183 without guaranteed cleanup -- The exception handler only catches (IOError, OSError) at line 196 -- When KeyboardInterrupt or other non-IO exceptions occur, the file handle leaks -- In long-running processes, this accumulates and causes "Too many open files" - -E2E Test Strategy: -- Use subprocess to run a Python script that: - 1. Acquires a SyncLock (opens file descriptor) - 2. Gets interrupted by KeyboardInterrupt during lock acquisition - 3. Checks if the file descriptor leaked using lsof or psutil -- This isolates the test and simulates real-world user behavior -- The test should FAIL on buggy code (leak detected) and PASS once fixed - -User-Facing Impact: -- Users running `pdd sync` and pressing Ctrl+C -- Long-running processes that repeatedly acquire locks -- CI/CD pipelines with frequent interruptions -- Eventually leads to "OSError: [Errno 24] Too many open files" -""" - -import json -import os -import subprocess -import sys -import tempfile -from pathlib import Path -from typing import List, Dict, Any - -import pytest - - -def get_project_root() -> Path: - """Get the project root directory.""" - current = Path(__file__).parent.parent - return current - - -def get_open_lock_files(pid: int) -> List[Dict[str, Any]]: - """ - Get list of open .lock files for a given process using psutil. - - Returns a list of dicts with file info: {'path': str, 'fd': int} - """ - try: - import psutil - process = psutil.Process(pid) - lock_files = [] - - # Get all open files - for file in process.open_files(): - if file.path.endswith('.lock'): - lock_files.append({ - 'path': file.path, - 'fd': file.fd, - }) - - return lock_files - except (psutil.NoSuchProcess, psutil.AccessDenied): - return [] - - -class TestFileHandleLeakE2E: - """ - E2E tests for Issue #403: File handle resource leak in SyncLock.acquire() - when interrupted by KeyboardInterrupt or other non-IO exceptions. - """ - - def test_synclock_keyboard_interrupt_leaks_file_handle(self, tmp_path: Path): - """ - E2E Test: SyncLock interrupted by KeyboardInterrupt leaves file handle open. - - This test simulates the real-world scenario: - 1. User runs `pdd sync` which acquires a SyncLock - 2. User presses Ctrl+C during lock acquisition - 3. File handle should be closed, but due to the bug, it leaks - - Expected behavior (after fix): - - File descriptor is closed even when KeyboardInterrupt occurs - - No .lock files remain open after the exception - - Bug behavior (Issue #403): - - File descriptor remains open after KeyboardInterrupt - - The .lock file is listed in lsof output for the process - - Multiple interruptions accumulate leaked file descriptors - """ - project_root = get_project_root() - - # Create a test script that simulates the bug scenario - test_script = f''' -import sys -import json -import os -import fcntl - -# Add project root to path -sys.path.insert(0, "{project_root}") - -from pdd.sync_determine_operation import SyncLock -from pathlib import Path - -def check_open_lock_files(): - """Check for open .lock files using psutil.""" - try: - import psutil - process = psutil.Process(os.getpid()) - lock_files = [] - - for file in process.open_files(): - if file.path.endswith('.lock'): - lock_files.append({{'path': file.path, 'fd': file.fd}}) - - return lock_files - except Exception as e: - return [] - -def simulate_interrupt_during_lock_acquisition(): - """Simulate KeyboardInterrupt during lock acquisition.""" - # Patch fcntl.flock to raise KeyboardInterrupt - original_flock = fcntl.flock - - def interrupt_flock(fd, operation): - # Raise KeyboardInterrupt to simulate Ctrl+C - raise KeyboardInterrupt("User pressed Ctrl+C") - - fcntl.flock = interrupt_flock - - try: - lock = SyncLock("test_interrupt", "python") - try: - lock.acquire() # This should raise KeyboardInterrupt - except KeyboardInterrupt: - pass # Expected - user pressed Ctrl+C - finally: - fcntl.flock = original_flock - - # Return the list of open .lock files - return check_open_lock_files() - -if __name__ == "__main__": - # Check initial state - initial_locks = check_open_lock_files() - - # Simulate interrupt during lock acquisition - leaked_locks = simulate_interrupt_during_lock_acquisition() - - # Output results as JSON - result = {{ - "initial_lock_count": len(initial_locks), - "leaked_lock_count": len(leaked_locks), - "leaked_files": leaked_locks, - "bug_detected": len(leaked_locks) > 0 - }} - - print(json.dumps(result)) -''' - - # Write the test script to a temp file - script_file = tmp_path / "test_interrupt_leak.py" - script_file.write_text(test_script) - - # Run the test script in a subprocess with proper environment - env = os.environ.copy() - env['PYTHONPATH'] = str(project_root) - env['PDD_FORCE_LOCAL'] = '1' # Prevent any real API calls - - result = subprocess.run( - [sys.executable, str(script_file)], - capture_output=True, - text=True, - cwd=str(tmp_path), - env=env, - timeout=30 - ) - - # Parse the output - if result.returncode != 0: - pytest.fail( - f"Test script failed to run:\n" - f"Return code: {result.returncode}\n" - f"STDOUT:\n{result.stdout}\n" - f"STDERR:\n{result.stderr}" - ) - - try: - test_result = json.loads(result.stdout.strip()) - except json.JSONDecodeError as e: - pytest.fail( - f"Failed to parse test output as JSON:\n" - f"Error: {e}\n" - f"STDOUT:\n{result.stdout}\n" - f"STDERR:\n{result.stderr}" - ) - - # THE BUG CHECK: File handles should NOT leak after KeyboardInterrupt - if test_result['bug_detected']: - leaked_files = test_result['leaked_files'] - leaked_paths = [f['path'] for f in leaked_files] - - pytest.fail( - f"BUG DETECTED (Issue #403): File handle leaked after KeyboardInterrupt!\n\n" - f"Scenario:\n" - f" 1. SyncLock.acquire() opens file at line 183: self.fd = open(self.lock_file, 'w')\n" - f" 2. KeyboardInterrupt raised during fcntl.flock() at line 187\n" - f" 3. Exception handler at line 196 only catches (IOError, OSError)\n" - f" 4. File handle never closed - LEAKED!\n\n" - f"Results:\n" - f" - Initial open .lock files: {test_result['initial_lock_count']}\n" - f" - Leaked .lock files: {test_result['leaked_lock_count']}\n" - f" - Leaked file paths:\n " + "\n ".join(leaked_paths) + "\n\n" - f"Impact:\n" - f" - Users pressing Ctrl+C during 'pdd sync' leak file descriptors\n" - f" - Long-running processes accumulate leaked FDs\n" - f" - Eventually causes: OSError: [Errno 24] Too many open files\n\n" - f"Root Cause:\n" - f" - The except (IOError, OSError) handler is too narrow\n" - f" - KeyboardInterrupt inherits from BaseException, not Exception\n" - f" - Need try-finally block to guarantee cleanup\n\n" - f"Fix:\n" - f" Add nested try-finally block after line 183:\n" - f" self.fd = open(self.lock_file, 'w')\n" - f" try:\n" - f" # locking operations\n" - f" finally:\n" - f" # cleanup on ANY exception" - ) - - def test_synclock_runtime_error_leaks_file_handle(self, tmp_path: Path): - """ - E2E Test: SyncLock interrupted by RuntimeError also leaks file handle. - - This test verifies that not just KeyboardInterrupt, but ANY exception - that's not IOError/OSError will cause the leak. - - User-facing scenario: - - Unexpected errors during lock operations (memory issues, system errors) - - Third-party library exceptions during file operations - - System resource exhaustion - """ - project_root = get_project_root() - - test_script = f''' -import sys -import json -import os -import fcntl - -sys.path.insert(0, "{project_root}") - -from pdd.sync_determine_operation import SyncLock - -def check_open_lock_files(): - try: - import psutil - process = psutil.Process(os.getpid()) - lock_files = [] - for file in process.open_files(): - if file.path.endswith('.lock'): - lock_files.append({{'path': file.path, 'fd': file.fd}}) - return lock_files - except Exception: - return [] - -def simulate_runtime_error_during_lock(): - """Simulate RuntimeError during lock acquisition.""" - original_flock = fcntl.flock - - def error_flock(fd, operation): - raise RuntimeError("Unexpected system error") - - fcntl.flock = error_flock - - try: - lock = SyncLock("test_error", "python") - try: - lock.acquire() - except RuntimeError: - pass # Expected - finally: - fcntl.flock = original_flock - - return check_open_lock_files() - -if __name__ == "__main__": - leaked_locks = simulate_runtime_error_during_lock() - result = {{ - "leaked_lock_count": len(leaked_locks), - "leaked_files": leaked_locks, - "bug_detected": len(leaked_locks) > 0 - }} - print(json.dumps(result)) -''' - - script_file = tmp_path / "test_error_leak.py" - script_file.write_text(test_script) - - env = os.environ.copy() - env['PYTHONPATH'] = str(project_root) - env['PDD_FORCE_LOCAL'] = '1' - - result = subprocess.run( - [sys.executable, str(script_file)], - capture_output=True, - text=True, - cwd=str(tmp_path), - env=env, - timeout=30 - ) - - if result.returncode != 0: - pytest.fail(f"Test script failed:\n{result.stderr}") - - try: - test_result = json.loads(result.stdout.strip()) - except json.JSONDecodeError: - pytest.fail(f"Failed to parse output:\n{result.stdout}") - - if test_result['bug_detected']: - pytest.fail( - f"BUG DETECTED (Issue #403): File handle leaked after RuntimeError!\n\n" - f"This confirms the bug affects ALL non-IOError/OSError exceptions,\n" - f"not just KeyboardInterrupt.\n\n" - f"Leaked files: {test_result['leaked_lock_count']}\n" - f"Paths: {[f['path'] for f in test_result['leaked_files']]}\n\n" - f"The exception handler must catch ALL exceptions to prevent leaks." - ) - - def test_synclock_normal_operation_no_leak(self, tmp_path: Path): - """ - E2E Test: Normal SyncLock operation should NOT leak file handles. - - This is a regression test to ensure: - 1. Normal lock acquisition and release works correctly - 2. No file handles remain open after release - 3. The fix doesn't break existing functionality - """ - project_root = get_project_root() - - test_script = f''' -import sys -import json -import os - -sys.path.insert(0, "{project_root}") - -from pdd.sync_determine_operation import SyncLock - -def check_open_lock_files(): - try: - import psutil - process = psutil.Process(os.getpid()) - lock_files = [] - for file in process.open_files(): - if file.path.endswith('.lock'): - lock_files.append({{'path': file.path, 'fd': file.fd}}) - return lock_files - except Exception: - return [] - -if __name__ == "__main__": - # Normal operation: acquire and release - lock = SyncLock("test_normal", "python") - - # Check before acquire - before = check_open_lock_files() - - # Acquire lock - lock.acquire() - during = check_open_lock_files() - - # Release lock - lock.release() - after = check_open_lock_files() - - result = {{ - "before_count": len(before), - "during_count": len(during), - "after_count": len(after), - "leak_detected": len(after) > 0 - }} - print(json.dumps(result)) -''' - - script_file = tmp_path / "test_normal.py" - script_file.write_text(test_script) - - env = os.environ.copy() - env['PYTHONPATH'] = str(project_root) - env['PDD_FORCE_LOCAL'] = '1' - - result = subprocess.run( - [sys.executable, str(script_file)], - capture_output=True, - text=True, - cwd=str(tmp_path), - env=env, - timeout=30 - ) - - if result.returncode != 0: - pytest.fail(f"Test script failed:\n{result.stderr}") - - try: - test_result = json.loads(result.stdout.strip()) - except json.JSONDecodeError: - pytest.fail(f"Failed to parse output:\n{result.stdout}") - - # Normal operation should NOT leak - assert not test_result['leak_detected'], ( - f"File handle leaked during normal operation!\n" - f"Before acquire: {test_result['before_count']} files\n" - f"During lock: {test_result['during_count']} files\n" - f"After release: {test_result['after_count']} files\n\n" - f"Expected: after_count = 0\n" - f"This is a regression - normal operation should not leak." - ) - - # Verify lock was actually held during acquisition - assert test_result['during_count'] >= 1, ( - f"Lock file was not opened during acquisition!\n" - f"Expected at least 1 .lock file open during lock hold,\n" - f"but found {test_result['during_count']}" - ) - - def test_synclock_context_manager_interrupted_leaks(self, tmp_path: Path): - """ - E2E Test: SyncLock context manager interrupted also leaks. - - This tests the __enter__/__exit__ path which calls acquire()/release(). - Even with context manager (which should guarantee cleanup), if acquire() - is interrupted before completion, the file handle leaks. - """ - project_root = get_project_root() - - test_script = f''' -import sys -import json -import os -import fcntl - -sys.path.insert(0, "{project_root}") - -from pdd.sync_determine_operation import SyncLock - -def check_open_lock_files(): - try: - import psutil - process = psutil.Process(os.getpid()) - lock_files = [] - for file in process.open_files(): - if file.path.endswith('.lock'): - lock_files.append({{'path': file.path, 'fd': file.fd}}) - return lock_files - except Exception: - return [] - -if __name__ == "__main__": - # Patch fcntl.flock to raise KeyboardInterrupt - original_flock = fcntl.flock - fcntl.flock = lambda fd, op: (_ for _ in ()).throw(KeyboardInterrupt()) - - try: - # Use context manager (with statement) - with SyncLock("test_ctx", "python"): - pass # Should never reach here - except KeyboardInterrupt: - pass # Expected - finally: - fcntl.flock = original_flock - - # Check for leaked files - leaked = check_open_lock_files() - result = {{ - "leaked_count": len(leaked), - "bug_detected": len(leaked) > 0 - }} - print(json.dumps(result)) -''' - - script_file = tmp_path / "test_ctx_leak.py" - script_file.write_text(test_script) - - env = os.environ.copy() - env['PYTHONPATH'] = str(project_root) - env['PDD_FORCE_LOCAL'] = '1' - - result = subprocess.run( - [sys.executable, str(script_file)], - capture_output=True, - text=True, - cwd=str(tmp_path), - env=env, - timeout=30 - ) - - if result.returncode != 0: - pytest.fail(f"Test script failed:\n{result.stderr}") - - try: - test_result = json.loads(result.stdout.strip()) - except json.JSONDecodeError: - pytest.fail(f"Failed to parse output:\n{result.stdout}") - - if test_result['bug_detected']: - pytest.fail( - f"BUG DETECTED (Issue #403): Context manager doesn't prevent leak!\n\n" - f"Even when using 'with SyncLock(...):' context manager,\n" - f"KeyboardInterrupt during acquire() leaves file handle open.\n\n" - f"Leaked files: {test_result['leaked_count']}\n\n" - f"The bug is in acquire() itself - it must use try-finally\n" - f"for cleanup, not rely on __exit__ which never gets called\n" - f"if __enter__ (acquire) raises." - ) diff --git a/tests/test_sync_determine_operation.py b/tests/test_sync_determine_operation.py index b1cd749b..5f2fa01f 100644 --- a/tests/test_sync_determine_operation.py +++ b/tests/test_sync_determine_operation.py @@ -196,199 +196,6 @@ def test_lock_reentrancy(self, pdd_test_environment): lock.acquire() # Should not raise an error assert (get_locks_dir() / f"{BASENAME}_{LANGUAGE}.lock").exists() - def test_file_handle_cleanup_on_keyboard_interrupt(self, pdd_test_environment, monkeypatch): - """Test that file descriptor is closed when KeyboardInterrupt occurs during lock acquisition. - - This is the primary bug scenario - when user presses Ctrl+C during lock acquisition, - the file descriptor should be properly closed to prevent resource leaks. - - Bug: The current code only catches (IOError, OSError), so KeyboardInterrupt bypasses - the cleanup code, leaving the file descriptor open. - """ - lock = SyncLock(BASENAME, LANGUAGE) - - # Mock fcntl.flock to raise KeyboardInterrupt after file is opened - import fcntl - original_flock = fcntl.flock - def mock_flock(fd, op): - raise KeyboardInterrupt("Simulating Ctrl+C") - - monkeypatch.setattr("fcntl.flock", mock_flock) - - # Attempt to acquire lock - should raise KeyboardInterrupt - with pytest.raises(KeyboardInterrupt): - lock.acquire() - - # BUG: File descriptor should be closed but currently remains open - # After fix: lock.fd should be None - # Before fix: lock.fd is still an open file object - assert lock.fd is None, "File descriptor should be closed after KeyboardInterrupt" - - # Restore original flock before trying to acquire lock2 - monkeypatch.setattr("fcntl.flock", original_flock) - - # Verify lock file cleanup - lock_file = get_locks_dir() / f"{BASENAME}_{LANGUAGE}.lock" - # Lock file may still exist, but should not be locked - # Verify by attempting to acquire with a new lock instance - lock2 = SyncLock(BASENAME, LANGUAGE) - lock2.acquire() # Should succeed without TimeoutError - lock2.release() - - def test_file_handle_cleanup_on_runtime_error(self, pdd_test_environment, monkeypatch): - """Test that file descriptor is closed when RuntimeError occurs during lock acquisition. - - This tests that non-IOError/OSError exceptions also trigger proper cleanup. - - Bug: The current code only catches (IOError, OSError), so RuntimeError bypasses - the cleanup code, leaving the file descriptor open. - """ - lock = SyncLock(BASENAME, LANGUAGE) - - # Mock fcntl.flock to raise RuntimeError after file is opened - import fcntl - original_flock = fcntl.flock - def mock_flock(fd, op): - raise RuntimeError("Unexpected error during lock acquisition") - - monkeypatch.setattr("fcntl.flock", mock_flock) - - # Attempt to acquire lock - should raise RuntimeError (not TimeoutError) - with pytest.raises(RuntimeError, match="Unexpected error"): - lock.acquire() - - # BUG: File descriptor should be closed but currently remains open - assert lock.fd is None, "File descriptor should be closed after RuntimeError" - - # Restore original flock before trying to acquire lock2 - monkeypatch.setattr("fcntl.flock", original_flock) - - # Verify lock file is not held - lock2 = SyncLock(BASENAME, LANGUAGE) - lock2.acquire() # Should succeed - lock2.release() - - def test_file_handle_cleanup_on_exception_during_write(self, pdd_test_environment, monkeypatch): - """Test that file descriptor is closed when Exception occurs during file write operations. - - This tests cleanup for exceptions during later file operations (write/flush). - - Bug: If an exception occurs during fd.write() or fd.flush(), and it's not an - IOError/OSError, the file descriptor remains open. - """ - lock = SyncLock(BASENAME, LANGUAGE) - - # Mock the file descriptor's write method to raise Exception - original_open = open - class MockFileWithFailingWrite: - def __init__(self, *args, **kwargs): - self._real_file = original_open(*args, **kwargs) - - def fileno(self): - return self._real_file.fileno() - - def write(self, data): - raise ValueError("Simulated write failure") - - def flush(self): - return self._real_file.flush() - - def close(self): - return self._real_file.close() - - monkeypatch.setattr("builtins.open", lambda *args, **kwargs: MockFileWithFailingWrite(*args, **kwargs)) - - # Attempt to acquire lock - should raise ValueError (not TimeoutError) - with pytest.raises(ValueError, match="Simulated write failure"): - lock.acquire() - - # BUG: File descriptor should be closed but currently remains open - assert lock.fd is None, "File descriptor should be closed after write failure" - - def test_ioerror_still_converts_to_timeout_error(self, pdd_test_environment, monkeypatch): - """Test that IOError/OSError are still converted to TimeoutError (regression test). - - This ensures the fix doesn't break existing error handling logic. IOError and OSError - should still be caught and converted to TimeoutError with appropriate message. - """ - lock = SyncLock(BASENAME, LANGUAGE) - - # Mock fcntl.flock to raise IOError - import fcntl - def mock_flock(fd, op): - raise IOError("Lock file busy") - - monkeypatch.setattr("fcntl.flock", mock_flock) - - # Should raise TimeoutError (not IOError) - with pytest.raises(TimeoutError, match="Failed to acquire lock"): - lock.acquire() - - # File descriptor should be closed - assert lock.fd is None, "File descriptor should be closed after IOError" - - def test_normal_operation_regression(self, pdd_test_environment): - """Test that normal lock acquisition and release still works correctly (regression test). - - This ensures the fix doesn't break the happy path. Lock should be acquired, - file descriptor should be open during lock, and properly closed on release. - """ - lock = SyncLock(BASENAME, LANGUAGE) - lock_file = get_locks_dir() / f"{BASENAME}_{LANGUAGE}.lock" - - # Before acquisition - assert not lock_file.exists() - assert lock.fd is None - - # Acquire lock - lock.acquire() - assert lock_file.exists() - assert lock.fd is not None, "File descriptor should be open during lock" - assert lock_file.read_text().strip() == str(os.getpid()) - - # Release lock - lock.release() - assert not lock_file.exists() - assert lock.fd is None, "File descriptor should be closed after release" - - def test_context_manager_cleanup_on_keyboard_interrupt(self, pdd_test_environment, monkeypatch): - """Test that context manager properly handles exceptions during acquisition. - - When using SyncLock as a context manager, if an exception occurs during acquire() - before entering the context, the exception should propagate and resources should be cleaned up. - - Bug: If KeyboardInterrupt occurs during acquire(), the file descriptor may leak - because __exit__ is never called (context was never entered). - """ - # Mock fcntl.flock to raise RuntimeError for first call only - import fcntl - call_count = [0] - original_flock = fcntl.flock - - def mock_flock(fd, op): - call_count[0] += 1 - if call_count[0] == 1: - raise RuntimeError("Simulating unexpected error during context manager") - # For subsequent calls, use the original function - return original_flock(fd, op) - - monkeypatch.setattr("fcntl.flock", mock_flock) - - # Attempt to use lock as context manager - with pytest.raises(RuntimeError): - with SyncLock(BASENAME, LANGUAGE) as lock: - # This block should never be reached - pytest.fail("Should not reach this point") - - # After exception, create a new lock to verify cleanup - # The file descriptor from the failed attempt should not interfere - lock2 = SyncLock(BASENAME, LANGUAGE) - # If the first attempt leaked a file descriptor, this might fail or cause issues - lock_file = get_locks_dir() / f"{BASENAME}_{LANGUAGE}.lock" - # Lock file might exist, but should be acquirable (will call flock again with call_count[0] == 2) - lock2.acquire() - lock2.release() - class TestFileUtilities: def test_calculate_sha256(self, pdd_test_environment):