Skip to main content

tests: switch to using pytester to test the test suite

ID
6c1cb05
date
2026-02-28 09:14:03+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
eecb946
message
tests: switch to using pytester to test the test suite

This doesn't change the behaviour of anything, but it's a more idiomatic
way to test my test suite, and lays the groundwork for some other changes
I'd like to make.
changed files
2 files, 117 additions, 86 deletions

Changed files

tests/conftest.py (144) → tests/conftest.py (173)

diff --git a/tests/conftest.py b/tests/conftest.py
index 302fe1c..9a07b84 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,4 +2,6 @@
 
 from nitrate.cassettes import cassette_name, vcr_cassette
 
+pytest_plugins = "pytester"
+
 __all__ = ["cassette_name", "vcr_cassette"]

tests/test_static_site_tests.py (7599) → tests/test_static_site_tests.py (8753)

diff --git a/tests/test_static_site_tests.py b/tests/test_static_site_tests.py
index 157ea9a..3a9c8f2 100644
--- a/tests/test_static_site_tests.py
+++ b/tests/test_static_site_tests.py
@@ -2,20 +2,23 @@
 Tests for `chives.static_site_tests`.
 """
 
-from collections.abc import Iterator
 from pathlib import Path
 import shutil
 import subprocess
 from typing import Any, TypeVar
 
 import pytest
+from pytest import Pytester
 
 from chives import dates
-from chives.static_site_tests import StaticSiteTestSuite
 
 
 M = TypeVar("M")
 
+GIT_ROOT = Path(
+    subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
+)
+
 
 @pytest.fixture
 def site_root(tmp_path: Path) -> Path:
@@ -25,35 +28,61 @@ def site_root(tmp_path: Path) -> Path:
     return tmp_path
 
 
-def create_test_suite[M](
-    site_root: Path,
-    metadata: M,
+def create_pyfile(
+    pytester: Pytester,
+    site_root: Path | None = None,
+    metadata: Any = None,
     *,
     paths_in_metadata: set[Path] | None = None,
     tags_in_metadata: set[str] | None = None,
-) -> StaticSiteTestSuite[M]:
+    date_formats: list[str] | None = None,
+    known_similar_tags: set[tuple[str, str]] | None = None,
+) -> None:
     """
-    Create a new instance of StaticSiteTestSuite with the hard-coded data
-    provided.
+    Create a new instance of `pytest.Pytester` which is ready to run
+    a test suite based on StaticSiteTestSuite.
     """
-
-    class TestSuite(StaticSiteTestSuite[M]):
-        def site_root(self) -> Path:  # pragma: no cover
-            return site_root
-
-        def metadata(self, site_root: Path) -> M:  # pragma: no cover
-            return metadata
-
-        def list_paths_in_metadata(self, metadata: M) -> set[Path]:
-            return paths_in_metadata or set()
-
-        def list_tags_in_metadata(self, metadata: M) -> Iterator[str]:
-            yield from (tags_in_metadata or set())
-
-    return TestSuite()
+    default_date_formats = [
+        "%Y-%m-%dT%H:%M:%SZ",
+        "%Y-%m-%d",
+    ]
+
+    pytester.makepyfile(
+        f"""
+        from collections.abc import Iterator
+        from pathlib import Path, PosixPath
+        from typing import Any
+        
+        import pytest
+        
+        from chives.static_site_tests import StaticSiteTestSuite
+        
+        
+        class TestSuite(StaticSiteTestSuite[Any]):
+            @pytest.fixture
+            def site_root(self) -> Path:
+                return Path({str(site_root or pytester.path)!r})
+
+            @pytest.fixture
+            def metadata(self, site_root: Path) -> Any:
+                return {repr(metadata)}
+
+            def list_paths_in_metadata(self, metadata: Any) -> set[Path]:
+                return {repr(paths_in_metadata or set())}
+
+            def list_tags_in_metadata(self, metadata: Any) -> Iterator[str]:
+                yield from {repr(tags_in_metadata or set())}
+            
+            date_formats = {repr(date_formats or default_date_formats)}
+            
+            known_similar_tags = {repr(known_similar_tags or set())}
+        """
+    )
 
 
-def test_paths_saved_locally_match_metadata(site_root: Path) -> None:
+def test_paths_saved_locally_match_metadata(
+    pytester: Pytester, site_root: Path
+) -> None:
     """
     The tests check that the set of paths saved locally match the metadata.
     """
@@ -73,35 +102,35 @@ def test_paths_saved_locally_match_metadata(site_root: Path) -> None:
 
     metadata = [Path("media/cat.jpg"), Path("media/dog.png"), Path("media/emu.gif")]
 
-    t = create_test_suite(site_root, metadata, paths_in_metadata=set(metadata))
-    t.test_every_file_in_metadata_is_saved_locally(metadata, site_root)
-    t.test_every_local_file_is_in_metadata(metadata, site_root)
+    create_pyfile(pytester, site_root, metadata, paths_in_metadata=set(metadata))
+
+    keyword = (
+        "test_every_file_in_metadata_is_saved_locally or "
+        "test_every_local_file_is_in_metadata"
+    )
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=2)
 
     # Add a new file locally, and check the test starts failing.
     (site_root / "media/fish.tiff").write_text("test")
-
-    with pytest.raises(AssertionError):
-        t.test_every_local_file_is_in_metadata(metadata, site_root)
-
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1, failed=1)
     (site_root / "media/fish.tiff").unlink()
 
     # Delete one of the local files, and check the test starts failing.
     (site_root / "media/cat.jpg").unlink()
-
-    with pytest.raises(AssertionError):
-        t.test_every_file_in_metadata_is_saved_locally(metadata, site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1, failed=1)
 
 
-def test_checks_for_git_changes(site_root: Path) -> None:
+def test_checks_for_git_changes(pytester: Pytester, site_root: Path) -> None:
     """
     The tests check that there are no uncommitted Git changes.
     """
-    t = create_test_suite(site_root, metadata=[1, 2, 3])
+    create_pyfile(pytester, site_root)
+
+    keyword = "test_no_uncommitted_git_changes"
 
     # Initially this should fail, because there isn't a Git repo in
     # the folder.
-    with pytest.raises(AssertionError):
-        t.test_no_uncommitted_git_changes(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
     # Create a Git repo, add a file, and commit it.
     (site_root / "README.md").write_text("hello world")
@@ -110,23 +139,23 @@ def test_checks_for_git_changes(site_root: Path) -> None:
     subprocess.check_call(["git", "commit", "-m", "initial commit"], cwd=site_root)
 
     # Check there are no uncommitted Git changes
-    t.test_no_uncommitted_git_changes(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     # Make a new change, and check it's spotted
     (site_root / "README.md").write_text("a different hello world")
-
-    with pytest.raises(AssertionError):
-        t.test_no_uncommitted_git_changes(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
 
-def test_checks_for_url_safe_paths(site_root: Path) -> None:
+def test_checks_for_url_safe_paths(pytester: Pytester, site_root: Path) -> None:
     """
     The tests check for URL-safe paths.
     """
-    t = create_test_suite(site_root, metadata=[1, 2, 3])
+    create_pyfile(pytester, site_root)
+
+    keyword = "test_every_path_is_url_safe"
 
     # This should pass trivially when the site is empty.
-    t.test_every_path_is_url_safe(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     # Now write some files with URL-safe names, and check it's still okay.
     for filename in [
@@ -136,39 +165,38 @@ def test_checks_for_url_safe_paths(site_root: Path) -> None:
     ]:
         (site_root / filename).write_text("test")
 
-    t.test_every_path_is_url_safe(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     # Write another file with a URL-unsafe name, and check it's caught
     # by the test.
     (site_root / "a#b#c").write_text("test")
+    pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
-    with pytest.raises(AssertionError):
-        t.test_every_path_is_url_safe(site_root)
 
-
-def test_checks_for_av1_videos(site_root: Path) -> None:
+def test_checks_for_av1_videos(pytester: Pytester, site_root: Path) -> None:
     """
     The tests check for AV1-encoded videos.
     """
-    t = create_test_suite(site_root, metadata=[1, 2, 3])
+    create_pyfile(pytester, site_root)
+
+    keyword = "test_no_videos_are_av1"
 
     # This should pass trivially when the site is empty.
-    t.test_no_videos_are_av1(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     # Copy in an H.264-encoded video, and check it's not flagged.
     shutil.copyfile(
-        "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
+        GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
         site_root / "Sintel_360_10s_1MB_H264.mp4",
     )
-    t.test_no_videos_are_av1(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     # Copy in an AV1-encoded video, and check it's caught by the test
     shutil.copyfile(
-        "tests/fixtures/media/Sintel_360_10s_1MB_AV1.mp4",
+        GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_AV1.mp4",
         site_root / "Sintel_360_10s_1MB_AV1.mp4",
     )
-    with pytest.raises(AssertionError):
-        t.test_no_videos_are_av1(site_root)
+    pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
 
 class TestAllTimestampsAreConsistent:
@@ -183,65 +211,66 @@ class TestAllTimestampsAreConsistent:
             {"date_saved": dates.now()},
         ],
     )
-    def test_allows_correct_date_formats(self, site_root: Path, metadata: Any) -> None:
+    def test_allows_correct_date_formats(
+        self, pytester: Pytester, metadata: Any
+    ) -> None:
         """
         The tests pass if all the dates are in the correct format.
         """
-        t = create_test_suite(site_root, metadata)
-        t.test_all_timestamps_are_consistent(metadata)
+        create_pyfile(pytester, metadata=metadata)
+
+        keyword = "test_all_timestamps_are_consistent"
+
+        pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     @pytest.mark.parametrize("metadata", [{"date_saved": "AAAA-BB-CC"}])
     def test_rejects_incorrect_date_formats(
-        self, site_root: Path, metadata: Any
+        self, pytester: Pytester, site_root: Path, metadata: Any
     ) -> None:
         """
         The tests fail if the metadata has inconsistent date formats.
         """
-        t = create_test_suite(site_root, metadata)
-        with pytest.raises(AssertionError):
-            t.test_all_timestamps_are_consistent(metadata)
+        create_pyfile(pytester, metadata=metadata)
+
+        keyword = "test_all_timestamps_are_consistent"
 
-    def test_can_override_date_formats(self, site_root: Path) -> None:
+        pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
+
+    def test_can_override_date_formats(self, pytester: Pytester) -> None:
         """
         A previously-blocked date format is allowed if you add it to
         the `date_formats` list.
         """
         metadata = {"date_saved": "2025"}
-        t = create_test_suite(site_root, metadata)
+        keyword = "test_all_timestamps_are_consistent"
 
         # It fails with the default settings
-        with pytest.raises(AssertionError):
-            t.test_all_timestamps_are_consistent(metadata)
+        create_pyfile(pytester, metadata=metadata)
+        pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
         # It passes if we add the format to `date_formats`
-        t.date_formats.append("%Y")
-        t.test_all_timestamps_are_consistent(metadata)
+        create_pyfile(pytester, metadata=metadata, date_formats=["%Y"])
+        pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
 
-def test_checks_for_similar_tags(site_root: Path) -> None:
+def test_checks_for_similar_tags(pytester: Pytester) -> None:
     """
     The tests check for similar and misspelt tags.
     """
-    metadata = [1, 2, 3]
+    keyword = "test_no_similar_tags"
 
     # Check a site with distinct tags.
-    t1 = create_test_suite(
-        site_root, metadata, tags_in_metadata={"red", "green", "blue"}
-    )
-    t1.test_no_similar_tags(metadata)
+    create_pyfile(pytester, tags_in_metadata={"red", "green", "blue"})
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
     # Check a site with similar tags.
-    t2 = create_test_suite(
-        site_root, metadata, tags_in_metadata={"red robot", "rod robot", "rid robot"}
-    )
-    with pytest.raises(AssertionError):
-        t2.test_no_similar_tags(metadata)
+    create_pyfile(pytester, tags_in_metadata={"red robot", "rod robot", "rid robot"})
+    pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
     # Check a site with similar tags, but marked as known-similar.
-    t3 = create_test_suite(
-        site_root,
-        metadata,
+    create_pyfile(
+        pytester,
         tags_in_metadata={"red robot", "rod robot", "green", "blue"},
+        known_similar_tags={("red robot", "rod robot")},
     )
-    t3.known_similar_tags = {("red robot", "rod robot")}
-    t3.test_no_similar_tags(metadata)
+    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)