Skip to content

Commit e528fdc

Browse files
Restore logging colors, refactor metatests, fix error (#72)
Refactors and fixes for the metatests: - Uses a consistent temporary directory, allowing for the daemon to detect changes - VS Code config uses watchman - Daemon VS Code config watches `/tmp/_metatests` - Removed code that wrote a `.gitignore` file as it is obsolete - Added `file_changes.py` which contains non-test changes - Added another test file, `test_file_changes.py` - Added additional test cases including one that brings out the bug that was fixed - Fixed async tests being skipped. They for sure worked before but seemed to have broke when a temporary directory was used and pytest was no longer using the same config. Refactors and fixes for the client / daemon: - The default jurigged logging is used again. Before a placeholder was used to avoid duplicated output, but it no longer appears to be needed. The behavior seems correct without any changes. - Reduced retries for waiting on the daemon to come up, from 1000 to 100. The 1000 value seemed excessive - Pytest arguments are passed from the client to the server when the server is automatically started. Note that includes tests to run, but the nature of the daemon results in it having no effect. This was needed because the server wasn't using the plugin and possibly other plugins or relevant plugin arguments that were provided to the client. - The server now returns an explicit stacktrace back to the client rather than using the built-in error handling of the XML RPC server. The XML RPC server was only giving the error and the error message, which can be insufficient. - The server no longer restores the current working directory. In the case of using temporary directories, the cwd would get deleted resulting in a cryptic file not found error after the tests successfully ran. Storing and restoring the cwd before and after test runs didn't seem to provide value. - Fixed bug where the hot reloading was attempting to access a global function that didn't exist, if the updated function was actually the method or static method of a class. This also resulted in a refactor of how function signatures are checked. It now pulls `self.node` rather than attempting to pull the old function from the global scope. The signatures are pulled from the AST, and then just the names of the arguments are copied for the purposes of detecting signature changes. - Added comments so its easier to trace args to where they get plugged in Notes: - Resolves #71
1 parent 3129704 commit e528fdc

File tree

11 files changed

+417
-108
lines changed

11 files changed

+417
-108
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash
22
poetry run pytest --daemon &
3+
poetry run dmypy start
34

45
ptyme-track --standalone

.ptyme_track/JamesHutchison

Lines changed: 199 additions & 0 deletions
Large diffs are not rendered by default.

.vscode/launch.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"args": [
1414
"--daemon",
1515
"--daemon-watch-globs",
16-
"./pytest_hot_reloading/daemon*.py:./pytest_hot_reloading/plugin.py",
16+
"./pytest_hot_reloading/daemon*.py:./pytest_hot_reloading/plugin.py:/tmp/_metatests/*.py",
1717
"--daemon-ignore-watch-globs",
1818
"./.venv/*" // this is the default value
1919
]
@@ -38,6 +38,9 @@
3838
"request": "launch",
3939
"program": "${workspaceFolder}/metatests/metatest_runner.py",
4040
"justMyCode": false,
41+
"args": [
42+
"--use-watchman"
43+
]
4144
// "args": [
4245
// "--do-not-reset-daemon"
4346
// ]

metatests/metatest_runner.py

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
2+
import os
23
import shutil
3-
import tempfile
44
import time
55
from os import system
66
from pathlib import Path
@@ -27,36 +27,37 @@ def __init__(
2727
self.modified_conftest_file = self.temp_dir / "conftest.py"
2828
self.modified_test_file = self.temp_dir / "test_fixture_changes.py"
2929
self.modified_used_by_conftest_file = self.temp_dir / "used_by_conftest.py"
30+
self.modified_code_file = self.temp_dir / "file_changes.py"
3031

3132
def make_fresh_copy(self):
3233
# delete the directory contents if it is not empty
3334
if self.temp_dir.exists():
3435
shutil.rmtree(self.temp_dir)
3536
shutil.copytree(TEMPLATE_DIR, self.temp_dir)
36-
with (self.temp_dir / ".gitignore").open("w") as gitignore:
37-
gitignore.write("*")
3837

3938
def run_test(
4039
self,
4140
test_name: str,
4241
*file_mod_funcs: Callable,
4342
expect_fail: bool = False,
4443
use_watchman: bool = False,
45-
change_delay: float = 0.01,
4644
retries: int = 0,
45+
test_file: str = "test_fixture_changes.py",
4746
):
4847
for retry_num in range(retries + 1):
4948
self.make_fresh_copy()
49+
os.chdir(self.temp_dir)
5050
if system(
5151
f"pytest -p pytest_hot_reloading.plugin --daemon-start-if-needed {'--daemon-use-watchman' if use_watchman else ''} "
52+
f"--daemon-watch-globs '{self.temp_dir}/*.py' "
5253
f"{self.temp_dir}/test_fixture_changes.py::test_always_ran"
5354
):
5455
raise Exception("Failed to prep daemon")
5556
for func in file_mod_funcs:
5657
func()
57-
time.sleep(change_delay + retry_num * 0.25)
58+
time.sleep(self.change_delay + retry_num * 0.25)
5859
try:
59-
if system(f"pytest {self.temp_dir}/test_fixture_changes.py::{test_name}"):
60+
if system(f"pytest {self.temp_dir}/{test_file}::{test_name}"):
6061
if not expect_fail:
6162
raise Exception(f"Failed to run test {test_name}")
6263
elif expect_fail:
@@ -231,6 +232,37 @@ def remove_autouse_fixture_outside_of_conftest(self) -> None:
231232
with self.modified_used_by_conftest_file.open("w") as f:
232233
f.writelines(new_lines)
233234

235+
def modify_function_return_value(self) -> None:
236+
print(self.modified_code_file)
237+
# modify the function in file_changes.py
238+
with self.modified_code_file.open() as f:
239+
lines = f.readlines()
240+
241+
# write new version of file_changes.py
242+
with self.modified_code_file.open("w") as f:
243+
for line in lines:
244+
f.write(line.replace('return "foo"', 'return "foo modified"'))
245+
246+
def modify_method_return_value(self) -> None:
247+
# modify the method in file_changes.py
248+
with self.modified_code_file.open() as f:
249+
lines = f.readlines()
250+
251+
# write new version of file_changes.py
252+
with self.modified_code_file.open("w") as f:
253+
for line in lines:
254+
f.write(line.replace('return "bar"', 'return "bar modified"'))
255+
256+
def modify_staticmethod_return_value(self) -> None:
257+
# modify the method in file_changes.py
258+
with self.modified_code_file.open() as f:
259+
lines = f.readlines()
260+
261+
# write new version of file_changes.py
262+
with self.modified_code_file.open("w") as f:
263+
for line in lines:
264+
f.write(line.replace('return "moo"', 'return "moo modified"'))
265+
234266
def main(self) -> None:
235267
if not self.do_not_reset_daemon:
236268
system("pytest --stop-daemon")
@@ -265,6 +297,11 @@ def main(self) -> None:
265297
self.rename_fixture,
266298
self.rename_use_of_fixture,
267299
)
300+
self.run_test(
301+
"TestClass::test_method_fixture_change",
302+
self.rename_fixture,
303+
self.rename_use_of_fixture,
304+
)
268305
self.run_test(
269306
"test_renaming_should_fail",
270307
self.rename_fixture,
@@ -297,6 +334,21 @@ def main(self) -> None:
297334
"test_autouse_fixture_outside_of_conftest_is_removed",
298335
self.remove_autouse_fixture_outside_of_conftest,
299336
)
337+
self.run_test(
338+
"test_file_function_change",
339+
self.modify_function_return_value,
340+
test_file="test_file_changes.py",
341+
)
342+
self.run_test(
343+
"test_class_method_change",
344+
self.modify_method_return_value,
345+
test_file="test_file_changes.py",
346+
)
347+
self.run_test(
348+
"test_staticmethod_change",
349+
self.modify_staticmethod_return_value,
350+
test_file="test_file_changes.py",
351+
)
300352

301353

302354
if __name__ == "__main__":
@@ -305,14 +357,17 @@ def main(self) -> None:
305357
argparser.add_argument("--use-watchman", action="store_true")
306358
argparser.add_argument("--change-delay", default=0.01, type=float)
307359
argparser.add_argument("--retry", default=0, type=int)
360+
argparser.add_argument("--temp-dir", default="/tmp/_metatests")
308361
args = argparser.parse_args()
309362

310-
with tempfile.TemporaryDirectory() as temp_dir:
311-
runner = MetaTestRunner(
312-
args.do_not_reset_daemon,
313-
args.use_watchman,
314-
args.change_delay,
315-
args.retry,
316-
Path(temp_dir),
317-
)
318-
runner.main()
363+
temp_dir = Path(args.temp_dir)
364+
if not temp_dir.exists():
365+
temp_dir.mkdir()
366+
runner = MetaTestRunner(
367+
args.do_not_reset_daemon,
368+
args.use_watchman,
369+
args.change_delay,
370+
args.retry,
371+
Path(temp_dir),
372+
)
373+
runner.main()

metatests/template/file_changes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
def some_func():
2+
return "foo"
3+
4+
5+
class SomeClass:
6+
def some_method(self):
7+
return "bar"
8+
9+
@staticmethod
10+
def some_static_method():
11+
return "moo"

metatests/template/pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
asyncio_mode: auto
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from .file_changes import SomeClass, some_func
2+
3+
4+
def test_file_function_change():
5+
"""
6+
This test uses a function that has the return value changed.
7+
8+
Should pass
9+
"""
10+
assert some_func() == "foo modified"
11+
12+
13+
def test_class_method_change():
14+
"""
15+
This test uses a method that has the return value changed.
16+
17+
Should pass
18+
"""
19+
assert SomeClass().some_method() == "bar modified"
20+
21+
22+
def test_staticmethod_change():
23+
"""
24+
This test uses a static method that has the return value changed.
25+
26+
Should pass
27+
"""
28+
assert SomeClass.some_static_method() == "moo modified"

metatests/template/test_fixture_changes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,12 @@ def test_autouse_fixture_outside_of_conftest_is_removed(fixture_outside_of_conft
108108
Should pass
109109
"""
110110
assert fixture_outside_of_conftest == "modified by autouse value"
111+
112+
113+
class TestClass:
114+
def test_method_fixture_change(self, renamed_fixture):
115+
"""
116+
This test uses a fixture that is renamed
117+
118+
Should pass
119+
"""

pytest_hot_reloading/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import time
66
import xmlrpc.client
77
from pathlib import Path
8-
from typing import cast
8+
from typing import Sequence, cast
99

1010

1111
class PytestClient:
@@ -25,6 +25,7 @@ def __init__(
2525
start_daemon_if_needed: bool = False,
2626
do_not_autowatch_fixtures: bool = False,
2727
use_watchman: bool = False,
28+
additional_args: Sequence[str] = [],
2829
) -> None:
2930
self._socket = None
3031
self._daemon_host = daemon_host
@@ -33,6 +34,7 @@ def __init__(
3334
self._will_start_daemon_if_needed = start_daemon_if_needed
3435
self._do_not_autowatch_fixtures = do_not_autowatch_fixtures
3536
self._use_watchman = use_watchman
37+
self._additional_args = additional_args
3638

3739
def _get_server(self) -> xmlrpc.client.ServerProxy:
3840
server_url = f"http://{self._daemon_host}:{self._daemon_port}"
@@ -112,4 +114,5 @@ def _start_daemon(self) -> None:
112114
pytest_name=self._pytest_name,
113115
do_not_autowatch_fixtures=self._do_not_autowatch_fixtures,
114116
use_watchman=self._use_watchman,
117+
additional_args=self._additional_args,
115118
)

0 commit comments

Comments
 (0)