|
15 | 15 | from locklib import LockTraceWrapper |
16 | 16 | from suby.subprocess_result import SubprocessResult |
17 | 17 |
|
18 | | -from tests.helpers import make_tar_bytes |
| 18 | +from tests.helpers import hold_windows_path_open, make_tar_bytes |
19 | 19 | from throng import ( |
20 | 20 | InvalidBaseDirectoryError, |
21 | 21 | IsolateDeletedError, |
|
28 | 28 | TemporaryDirectoryThrong, |
29 | 29 | ) |
30 | 30 |
|
| 31 | +WINDOWS_DIRECTORY_HANDLE_FLAGS = 0x02000000 |
| 32 | +WINDOWS_FILE_SHARE_READ = 0x00000001 |
| 33 | +WINDOWS_FILE_SHARE_WRITE = 0x00000002 |
| 34 | +WINDOWS_SHARING_VIOLATION_REASON = 'The process cannot access the file because it is being used by another process' |
| 35 | + |
31 | 36 |
|
32 | 37 | def assert_isolate_deleted(operation): |
33 | 38 | with pytest.raises(IsolateDeletedError, match=match('Isolate has been deleted.')): |
@@ -136,6 +141,29 @@ def test_temp_base_not_writable(tmp_path, request): |
136 | 141 | ] |
137 | 142 |
|
138 | 143 |
|
| 144 | +@pytest.mark.skipif(os_name != 'nt', reason='Windows read-only directory attributes do not apply on POSIX') |
| 145 | +def test_temp_base_read_only_on_windows_is_rejected_and_logged(tmp_path, request): |
| 146 | + """ |
| 147 | + Verify that a Windows read-only temporary base directory is rejected and logged. |
| 148 | +
|
| 149 | + Setting the Windows read-only attribute makes the configured base fail the |
| 150 | + plugin's ``W_OK`` check, so no temporary isolate may be created inside it. |
| 151 | + """ |
| 152 | + base_directory = tmp_path / 'base' |
| 153 | + base_directory.mkdir() |
| 154 | + request.addfinalizer(lambda: base_directory.chmod(S_IREAD | S_IWRITE | S_IEXEC)) |
| 155 | + base_directory.chmod(S_IREAD) |
| 156 | + config = TemporaryDirectoryIsolationConfig(base_directory=str(base_directory)) |
| 157 | + logger = MemoryLogger() |
| 158 | + |
| 159 | + with pytest.raises(InvalidBaseDirectoryError, match=match(f'Temporary base directory is not writable: {base_directory}')): |
| 160 | + TemporaryDirectoryThrong(logger=logger, config=config).get_isolate() |
| 161 | + |
| 162 | + assert [str(call.message) for call in logger.data.error] == [ |
| 163 | + f'Temporary base directory is not writable: {base_directory}', |
| 164 | + ] |
| 165 | + |
| 166 | + |
139 | 167 | def test_temp_isolates_are_distinct(tmp_path): |
140 | 168 | """Verify that temporary isolates get separate directories and do not share files.""" |
141 | 169 | config = TemporaryDirectoryIsolationConfig(base_directory=str(tmp_path)) |
@@ -373,6 +401,38 @@ def test_temp_delete_failure_raises_and_does_not_log_success(tmp_path, request): |
373 | 401 | assert [str(call.message) for call in logger.data.info].count('Delete completed successfully.') == 1 |
374 | 402 |
|
375 | 403 |
|
| 404 | +@pytest.mark.skipif(os_name != 'nt', reason='Windows sharing violations are not available on POSIX') |
| 405 | +def test_temp_delete_locked_windows_directory_raises_and_can_be_retried(tmp_path): |
| 406 | + """ |
| 407 | + Verify that a native Windows delete failure is logged and leaves deletion retryable. |
| 408 | +
|
| 409 | + An open directory handle without delete sharing blocks the first cleanup; |
| 410 | + after the handle closes, the same isolate must be deletable successfully. |
| 411 | + """ |
| 412 | + logger = MemoryLogger() |
| 413 | + throng = TemporaryDirectoryThrong(logger=logger, config=TemporaryDirectoryIsolationConfig(base_directory=str(tmp_path))) |
| 414 | + isolate = throng.get_isolate() |
| 415 | + isolate_directory = isolate.directory |
| 416 | + denied_path = isolate_directory if version_info >= (3, 12) else str(isolate_directory) |
| 417 | + permission_error_message = f'[WinError 32] {WINDOWS_SHARING_VIOLATION_REASON}: {denied_path!r}' |
| 418 | + |
| 419 | + with hold_windows_path_open( |
| 420 | + isolate_directory, |
| 421 | + share_mode=WINDOWS_FILE_SHARE_READ | WINDOWS_FILE_SHARE_WRITE, |
| 422 | + flags=WINDOWS_DIRECTORY_HANDLE_FLAGS, |
| 423 | + ), pytest.raises(PermissionError, match=match(permission_error_message)): |
| 424 | + isolate.delete() |
| 425 | + |
| 426 | + assert isolate_directory.exists() |
| 427 | + assert any('Delete failed' in str(call.message) for call in logger.data.exception) |
| 428 | + assert all(str(call.message) != 'Delete completed successfully.' for call in logger.data.info) |
| 429 | + |
| 430 | + isolate.delete() |
| 431 | + |
| 432 | + assert not isolate_directory.exists() |
| 433 | + assert [str(call.message) for call in logger.data.info].count('Delete completed successfully.') == 1 |
| 434 | + |
| 435 | + |
376 | 436 | def test_temp_delete_after_delete_raises(tmp_path): |
377 | 437 | """Verify that a second delete call is rejected and logged as a delete operation.""" |
378 | 438 | logger = MemoryLogger() |
|
0 commit comments