From e78bcbaa006e8f1ea07462bc330cabe01e032b37 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Sun, 28 Apr 2024 16:46:55 -0700 Subject: [PATCH] First implementation --- README.md | 3 +++ lib/__init__.py | 0 lib/main.py | 5 +++++ tests/__init__.py | 0 tests/conftest.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_basic.py | 39 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 lib/__init__.py create mode 100644 lib/main.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_basic.py diff --git a/README.md b/README.md index e69de29..195c7e0 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +Demonstration of the use of "conditional cleanups" in `pytest`, as explored further in [this blog post](https://blog.scubbo.org/posts/conditional-cleanups-in-pytest). + +Run `pytest`, and observe that the tempfile is cleaned up for the passing test, but persisted for the failing test. diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/main.py b/lib/main.py new file mode 100644 index 0000000..b8d8379 --- /dev/null +++ b/lib/main.py @@ -0,0 +1,5 @@ +import pathlib + +# Very simple method as placeholder for logic being tested. +def write_content_to_file(content: str, path: pathlib.Path) -> None: + path.write_text(content) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c96fc73 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,45 @@ +import pytest + +from typing import Callable + +# https://stackoverflow.com/questions/69281822/how-to-only-run-a-pytest-fixture-cleanup-on-test-error-or-failure, +# Though syntax appears to have changed +# https://docs.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + # execute all other hooks to obtain the report object + outcome = yield + + # TODO - we may care about more than just a binary result! + # (i.e. a skipped test is neither passed nor failed...probably?) + setattr( + item, + "rep_" + outcome.get_result().when + "_passed", + outcome.get_result().passed, + ) + + +class Cleanups(object): + def __init__(self): + self.success_cleanups = [] + self.failure_cleanups = [] + + def add_success(self, success_cleanup: Callable[[], None]): + self.success_cleanups.append(success_cleanup) + + def add_failure(self, failure_cleanup: Callable[[], None]): + self.failure_cleanups.append(failure_cleanup) + + +@pytest.fixture +def cleanups(request): + cleanups = Cleanups() + yield cleanups + + if request.node.rep_call_passed: + cleanups = cleanups.success_cleanups + else: + cleanups = cleanups.failure_cleanups + if cleanups: + for cleanup in cleanups[::-1]: # Apply in reverse order + cleanup() diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..19029c1 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,39 @@ +import os +import tempfile +import pytest + +import pathlib + +from lib.main import write_content_to_file + + +@pytest.mark.parametrize("test_input", ["Expected content", "Incorrect content"]) +def test(test_input, temp_file_path, cleanups): + + write_content_to_file(test_input, temp_file_path) + + def in_test_print_out_on_test_failure(): + print(f'(In-test cleanup) Test using tempfile {temp_file_path} failed') + cleanups.add_failure(in_test_print_out_on_test_failure) + + assert temp_file_path.read_text() == "Expected content" + + + +@pytest.fixture +def temp_file_path(cleanups) -> pathlib.Path: + # In real production use, you'd be more likely to use methods from the `tempfile` library + # (https://docs.python.org/3/library/tempfile.html) directly - but I'm implementing this fixture by-hand to + # demonstrate that the `cleanups` object can handle cleanup-methods being attached from within a fixture. + f = tempfile.NamedTemporaryFile(delete=False, dir='.') + path = pathlib.Path(f.name) + + def delete_temp_file_on_test_success(): + path.unlink() + cleanups.add_success(delete_temp_file_on_test_success) + + def print_out_temp_file_path_on_test_failure(): + print(f'Test failed involving tempfile {path}') + cleanups.add_failure(print_out_temp_file_path_on_test_failure) + + return path \ No newline at end of file