-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Reliable version of os.replace for Windows (wait/retry loop) #3239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ff27b6e
b089a79
42c6987
2d369c0
8e61b79
beb453f
2f3e380
4950918
a608f0e
0cc6337
f05647c
f9de4c9
199a103
a21f6b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
from typing import Iterator, List, Tuple, IO | ||
import time | ||
import os | ||
import sys | ||
import tempfile | ||
from contextlib import contextmanager | ||
from threading import Thread | ||
from unittest import TestCase, main, skipUnless | ||
from mypy import util | ||
from mypy.build import random_string | ||
|
||
|
||
WIN32 = sys.platform.startswith("win") | ||
|
||
|
||
@skipUnless(WIN32, "only relevant for Windows") | ||
class WindowsReplace(TestCase): | ||
tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-', | ||
dir=os.path.abspath('tmp-test-dirs')) | ||
# Choose timeout value that would ensure actual wait inside util.replace is close to timeout | ||
timeout = 0.0009 * 2 ** 10 | ||
short_lock = timeout / 4 | ||
long_lock = timeout * 2 | ||
|
||
threads = [] # type: List[Thread] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should become an instance variable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My idea was that the entire file locking arrangement is shared across all the Note: I temporarily reduced the number of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went back to having 4 tests, since otherwise it's too messy. There's no semantic problem with that, and I'll deal with the extra few seconds in test runtime later. But with this, I'd like to keep the class-level attributes if you don't see an issue with them; both semantically and performance-wise, I don't want to force each individual test to wait for the threads to expire. |
||
|
||
@classmethod | ||
def close_file_after(cls, file: IO, delay: float) -> Thread: | ||
"""Start a background thread to close file after delay sec.""" | ||
def _close_file_after() -> None: | ||
time.sleep(delay) | ||
file.close() | ||
|
||
t = Thread(target=_close_file_after, daemon=True) | ||
cls.threads.append(t) | ||
t.start() | ||
return t | ||
|
||
@classmethod | ||
def tearDownClass(cls) -> None: | ||
# Need to wait for threads to complete, otherwise we'll get PermissionError | ||
# at the end (whether tmpdir goes out of scope or we explicitly call cleanup). | ||
for t in cls.threads: | ||
t.join() | ||
cls.tmpdir.cleanup() | ||
|
||
def prepare_src_dest(self, src_lock_duration: float, dest_lock_duration: float | ||
) -> Tuple[str, str]: | ||
"""Create two files in self.tmpdir random names (src, dest) and unique contents; | ||
then spawn two threads that lock each of them for a specified duration. | ||
|
||
Return a tuple (src, dest). | ||
""" | ||
src = os.path.join(self.tmpdir.name, random_string()) | ||
dest = os.path.join(self.tmpdir.name, random_string()) | ||
|
||
for fname, delay in zip((src, dest), (src_lock_duration, dest_lock_duration)): | ||
f = open(fname, 'w') | ||
f.write(fname) | ||
if delay: | ||
self.close_file_after(f, delay) | ||
else: | ||
f.close() | ||
|
||
return src, dest | ||
|
||
def replace_ok(self, src_lock_duration: float, dest_lock_duration: float, | ||
timeout: float) -> None: | ||
"""Check whether util._replace, called with a specified timeout, | ||
worked successfully on two newly created files locked for specified | ||
durations. | ||
|
||
Return True if the replacement succeeded. | ||
""" | ||
src, dest = self.prepare_src_dest(src_lock_duration, dest_lock_duration) | ||
util._replace(src, dest, timeout=timeout) | ||
# Note that dest handle may still be open but reading from it is ok. | ||
with open(dest) as f: | ||
self.assertEqual(f.read(), src, 'replace failed') | ||
|
||
def test_no_locks(self) -> None: | ||
# No files locked. | ||
self.replace_ok(0, 0, self.timeout) | ||
|
||
def test_original_problem(self) -> None: | ||
# Make sure we can reproduce https://github.com/python/mypy/issues/3215 with our setup. | ||
src, dest = self.prepare_src_dest(self.short_lock, 0) | ||
with self.assertRaises(PermissionError): | ||
os.replace(src, dest) | ||
|
||
def test_short_locks(self) -> None: | ||
# Lock files for a time short enough that util.replace won't timeout. | ||
self.replace_ok(self.short_lock, 0, self.timeout) | ||
self.replace_ok(0, self.short_lock, self.timeout) | ||
|
||
def test_long_locks(self) -> None: | ||
# Lock files for a time long enough that util.replace times out. | ||
with self.assertRaises(PermissionError): | ||
self.replace_ok(self.long_lock, 0, self.timeout) | ||
with self.assertRaises(PermissionError): | ||
self.replace_ok(0, self.long_lock, self.timeout) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, turns out
mypy.test
,unittest
, and pytest are currently being used...ouch...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but Max didn't start that. IIUC we want to kill mypy.test, but it's low priority (not sure if there's even an issue) and pytest works well enough with unittest (also, personally, I'm more used to the unittest way of writing tests -- let pytest just be the test runner).