2Tests for `chives.static_site_tests`.
5from pathlib import Path
8from typing import Any, TypeVar
11from pytest import Pytester
13from chives import dates
19 subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
24def site_root(tmp_path: Path) -> Path:
26 Return a temp directory to use as a site root.
33 site_root: Path | None = None,
36 paths_in_metadata: set[Path] | None = None,
37 tags_in_metadata: set[str] | None = None,
38 date_formats: list[str] | None = None,
39 known_similar_tags: set[tuple[str, str]] | None = None,
42 Create a new instance of `pytest.Pytester` which is ready to run
43 a test suite based on StaticSiteTestSuite.
45 default_date_formats = [
52 from collections.abc import Iterator
53 from pathlib import Path, PosixPath
54 from typing import Any
58 from chives.static_site_tests import StaticSiteTestSuite
61 class TestSuite(StaticSiteTestSuite[Any]):
63 def get_site_root(self) -> Path:
64 return Path({str(site_root or pytester.path)!r})
67 def metadata(self, site_root: Path) -> Any:
68 return {repr(metadata)}
70 def list_paths_in_metadata(self, metadata: Any) -> set[Path]:
71 return {repr(paths_in_metadata or set())}
73 def list_tags_in_metadata(self, metadata: Any) -> Iterator[str]:
74 yield from {repr(tags_in_metadata or set())}
76 date_formats = {repr(date_formats or default_date_formats)}
78 known_similar_tags = {repr(known_similar_tags or set())}
83def test_paths_saved_locally_match_metadata(
84 pytester: Pytester, site_root: Path
87 The tests check that the set of paths saved locally match the metadata.
89 # Create a series of paths in tmp_path.
99 p = site_root / filename
100 p.parent.mkdir(exist_ok=True)
103 metadata = [Path("media/cat.jpg"), Path("media/dog.png"), Path("media/emu.gif")]
105 create_pyfile(pytester, site_root, metadata, paths_in_metadata=set(metadata))
108 "test_every_file_in_metadata_is_saved_locally or "
109 "test_every_local_file_is_in_metadata"
111 pytester.runpytest("-k", keyword).assert_outcomes(passed=2)
113 # Add a new file locally, and check the test starts failing.
114 (site_root / "media/fish.tiff").write_text("test")
115 pytester.runpytest("-k", keyword).assert_outcomes(passed=1, failed=1)
116 (site_root / "media/fish.tiff").unlink()
118 # Delete one of the local files, and check the test starts failing.
119 (site_root / "media/cat.jpg").unlink()
120 pytester.runpytest("-k", keyword).assert_outcomes(passed=1, failed=1)
123def test_checks_for_git_changes(pytester: Pytester, site_root: Path) -> None:
125 The tests check that there are no uncommitted Git changes.
127 create_pyfile(pytester, site_root)
129 keyword = "test_no_uncommitted_git_changes"
131 # Initially this should fail, because there isn't a Git repo in
133 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
135 # Create a Git repo, add a file, and commit it.
136 (site_root / "README.md").write_text("hello world")
137 subprocess.check_call(["git", "init"], cwd=site_root)
138 subprocess.check_call(["git", "add", "README.md"], cwd=site_root)
139 subprocess.check_call(["git", "commit", "-m", "initial commit"], cwd=site_root)
141 # Check there are no uncommitted Git changes
142 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
144 # Make a new change, and check it's spotted
145 (site_root / "README.md").write_text("a different hello world")
146 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
149def test_checks_for_url_safe_paths(pytester: Pytester, site_root: Path) -> None:
151 The tests check for URL-safe paths.
153 create_pyfile(pytester, site_root)
155 keyword = "test_every_path_is_url_safe"
157 # This should pass trivially when the site is empty.
158 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
160 # Now write some files with URL-safe names, and check it's still okay.
166 (site_root / filename).write_text("test")
168 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
170 # Write another file with a URL-unsafe name, and check it's caught
172 (site_root / "a#b#c").write_text("test")
173 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
176def test_checks_for_av1_videos(pytester: Pytester, site_root: Path) -> None:
178 The tests check for AV1-encoded videos.
180 create_pyfile(pytester, site_root)
182 keyword = "test_no_videos_are_av1"
184 # This should pass trivially when the site is empty.
185 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
187 # Copy in an H.264-encoded video, and check it's not flagged.
189 GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
190 site_root / "Sintel_360_10s_1MB_H264.mp4",
192 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
194 # Copy in an AV1-encoded video, and check it's caught by the test
196 GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_AV1.mp4",
197 site_root / "Sintel_360_10s_1MB_AV1.mp4",
199 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
202def test_checks_for_srt_subtitles(pytester: Pytester, site_root: Path) -> None:
204 The tests check for SRT-encoded subtitles, and fail if they're detected.
206 create_pyfile(pytester, site_root)
208 keyword = "test_no_subtitles_are_srt"
210 # This should pass trivially when the site is empty.
211 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
213 # Add a VTT-encoded subtitle, and check the test still passes.
214 (site_root / "my-great-show").mkdir()
215 (site_root / "my-great-show/e01 - Episode the First.en.vtt").write_text("")
216 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
218 # Add an SRT-encoded subtitle, and check the test fails
219 (site_root / "my-great-show/e02 - Episode the Second.en.srt").write_text("")
220 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
223class TestAllTimestampsAreConsistent:
225 Tests for the `test_all_timestamps_are_consistent` method.
228 @pytest.mark.parametrize(
231 {"date_saved": "2025-12-06"},
232 {"date_saved": dates.now()},
235 def test_allows_correct_date_formats(
236 self, pytester: Pytester, metadata: Any
239 The tests pass if all the dates are in the correct format.
241 create_pyfile(pytester, metadata=metadata)
243 keyword = "test_all_timestamps_are_consistent"
245 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
247 @pytest.mark.parametrize("metadata", [{"date_saved": "AAAA-BB-CC"}])
248 def test_rejects_incorrect_date_formats(
249 self, pytester: Pytester, site_root: Path, metadata: Any
252 The tests fail if the metadata has inconsistent date formats.
254 create_pyfile(pytester, metadata=metadata)
256 keyword = "test_all_timestamps_are_consistent"
258 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
260 def test_can_override_date_formats(self, pytester: Pytester) -> None:
262 A previously-blocked date format is allowed if you add it to
263 the `date_formats` list.
265 metadata = {"date_saved": "2025"}
266 keyword = "test_all_timestamps_are_consistent"
268 # It fails with the default settings
269 create_pyfile(pytester, metadata=metadata)
270 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
272 # It passes if we add the format to `date_formats`
273 create_pyfile(pytester, metadata=metadata, date_formats=["%Y"])
274 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
277@pytest.mark.parametrize(
278 "tags_in_metadata, known_similar_tags, expected_outcome",
280 pytest.param({"red", "green", "blue"}, {}, {"passed": 1}, id="distinct_tags"),
281 pytest.param({"red robot", "rod robot"}, {}, {"failed": 1}, id="similar_tags"),
283 {"red robot", "rod robot"},
284 {("red robot", "rod robot")},
286 id="similar_tags_marked_known",
290def test_checks_for_similar_tags(
292 tags_in_metadata: set[str],
293 known_similar_tags: set[tuple[str, str]],
294 expected_outcome: dict[str, int],
297 The tests check for similar and misspelt tags.
299 keyword = "test_no_similar_tags"
303 tags_in_metadata=tags_in_metadata,
304 known_similar_tags=known_similar_tags,
306 pytester.runpytest("-k", keyword).assert_outcomes(**expected_outcome)
309class TestChivesJSisUpToDate:
311 Tests for the `test_chives_js_is_up_to_date` method.
314 def test_no_js_is_passing(self, pytester: Pytester) -> None:
316 If the site doesn't have any JavaScript files, it's trivially okay.
318 create_pyfile(pytester)
320 keyword = "test_chives_js_is_up_to_date"
322 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
324 def test_correct_js_is_passing(self, pytester: Pytester, site_root: Path) -> None:
326 If the site has up-to-date JavaScript files, it's okay.
329 GIT_ROOT / "javascript/chives-image.js", site_root / "chives-image.js"
332 (site_root / "static").mkdir()
334 GIT_ROOT / "javascript/chives-image.js",
335 site_root / "static/chives-image.js",
338 create_pyfile(pytester, site_root=site_root)
340 keyword = "test_chives_js_is_up_to_date"
342 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
344 @pytest.mark.parametrize(
350 "// javascript/chives-wrongname.js\n// chives version: 48",
353 # Missing javascript/ prefixes
355 "// chives-example.js\n// chives version: 48",
358 # Missing version line
360 "// javascript/chives-example.js\n",
365 "// javascript/chives-example.js\n// chives version: 1",
369 def test_incorrect_js_is_error(
370 self, pytester: Pytester, site_root: Path, filename: str, header: str
373 If the site has an old chives-js file or a file with an incorrect
374 header, it fails this test.
376 # Create a chives-*.js file in the site root with a bad header.
377 (site_root / filename).write_text(
378 header + "\n\n" + "function greet() { console.log('hello world'); }"
381 create_pyfile(pytester, site_root=site_root)
383 keyword = "test_chives_js_is_up_to_date"
385 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)