aboutsummaryrefslogtreecommitdiff
path: root/tests/tools
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-16 14:40:39 +0530
committernatsuoto <[email protected]>2026-07-16 14:40:39 +0530
commitb7ae2f3885d5ff84237c9da8bc0c64fe1a509465 (patch)
tree27d8e35406931aad0f26c89bb4679e7c38c0e24c /tests/tools
parent9d308c262788252cac26f5821cade327e18da7aa (diff)
downloadedify-b7ae2f3885d5ff84237c9da8bc0c64fe1a509465.tar.xz
edify-b7ae2f3885d5ff84237c9da8bc0c64fe1a509465.zip
feat(release): changes/ fragment system + generator, .public-surface snapshot + CI migration gate, deprecation-warning + anchor harnesses, Trusted Publishing, relicense CHANGELOG entry
Diffstat (limited to 'tests/tools')
-rw-r--r--tests/tools/changes.test.py116
-rw-r--r--tests/tools/surface.test.py89
2 files changed, 205 insertions, 0 deletions
diff --git a/tests/tools/changes.test.py b/tests/tools/changes.test.py
new file mode 100644
index 0000000..7eeb777
--- /dev/null
+++ b/tests/tools/changes.test.py
@@ -0,0 +1,116 @@
+"""Tests for the ``changes/`` fragment generator in ``tools/changes.py``."""
+
+import importlib.util
+from pathlib import Path
+
+_REPO_ROOT = Path(__file__).parent.parent.parent
+_GENERATOR_PATH = _REPO_ROOT / "tools" / "changes.py"
+
+
+def _load_generator():
+ spec = importlib.util.spec_from_file_location("edify_changes_tool", _GENERATOR_PATH)
+ assert spec is not None
+ assert spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+_GENERATOR = _load_generator()
+
+
+def _write_fragment(directory: Path, name: str, body: str) -> Path:
+ fragment_path = directory / name
+ fragment_path.write_text(body, encoding="utf-8")
+ return fragment_path
+
+
+def test_collect_fragments_returns_files_in_fragment_id_order(tmp_path):
+ _write_fragment(tmp_path, "0020-second.rst", "[change]\nanchor=b\nheading=B\ncontext=b")
+ _write_fragment(tmp_path, "0010-first.rst", "[change]\nanchor=a\nheading=A\ncontext=a")
+ collected = _GENERATOR.collect_fragments(tmp_path)
+ assert [path.name for path in collected] == ["0010-first.rst", "0020-second.rst"]
+
+
+def test_collect_fragments_skips_the_readme(tmp_path):
+ _write_fragment(tmp_path, "README.rst", "not a fragment")
+ _write_fragment(tmp_path, "0010-only.rst", "[change]\nanchor=a\nheading=A\ncontext=a")
+ collected = _GENERATOR.collect_fragments(tmp_path)
+ assert [path.name for path in collected] == ["0010-only.rst"]
+
+
+def test_render_fragment_emits_anchor_heading_and_context(tmp_path):
+ fragment_path = _write_fragment(
+ tmp_path,
+ "0010-example.rst",
+ "[change]\nanchor = my-anchor\nheading = My Heading\ncontext = A single sentence.",
+ )
+ rendered = _GENERATOR.render_fragment(fragment_path)
+ assert ".. _my-anchor:" in rendered
+ assert "My Heading" in rendered
+ assert "-" * len("My Heading") in rendered
+ assert "A single sentence." in rendered
+
+
+def test_render_fragment_collapses_multiline_context_into_one_paragraph(tmp_path):
+ fragment_path = _write_fragment(
+ tmp_path,
+ "0010-multiline.rst",
+ "[change]\nanchor = a\nheading = H\ncontext = first line\n second line",
+ )
+ rendered = _GENERATOR.render_fragment(fragment_path)
+ assert "first line second line" in rendered
+
+
+def test_render_fragment_includes_before_after_block_when_both_present(tmp_path):
+ fragment_path = _write_fragment(
+ tmp_path,
+ "0010-beforeafter.rst",
+ "[change]\nanchor = a\nheading = H\nbefore = old\nafter = new\ncontext = changed",
+ )
+ rendered = _GENERATOR.render_fragment(fragment_path)
+ assert ".. code-block:: text" in rendered
+ assert " old" in rendered
+ assert " new" in rendered
+
+
+def test_render_fragment_omits_before_after_block_when_absent(tmp_path):
+ fragment_path = _write_fragment(
+ tmp_path,
+ "0010-nofix.rst",
+ "[change]\nanchor = a\nheading = H\ncontext = no before/after here",
+ )
+ rendered = _GENERATOR.render_fragment(fragment_path)
+ assert ".. code-block:: text" not in rendered
+
+
+def test_render_all_concatenates_every_fragment_in_order(tmp_path):
+ _write_fragment(tmp_path, "0020-b.rst", "[change]\nanchor=b\nheading=Bravo\ncontext=b")
+ _write_fragment(tmp_path, "0010-a.rst", "[change]\nanchor=a\nheading=Alpha\ncontext=a")
+ rendered = _GENERATOR.render_all(tmp_path)
+ assert rendered.index("Alpha") < rendered.index("Bravo")
+
+
+def test_main_release_deletes_consumed_fragments(tmp_path, monkeypatch):
+ _write_fragment(tmp_path, "0010-a.rst", "[change]\nanchor=a\nheading=A\ncontext=a")
+ monkeypatch.setattr(_GENERATOR, "_CHANGES_DIR", tmp_path)
+ exit_code = _GENERATOR.main(["--release"])
+ assert exit_code == 0
+ assert _GENERATOR.collect_fragments(tmp_path) == []
+
+
+def test_main_without_release_leaves_fragments_in_place(tmp_path, monkeypatch, capsys):
+ _write_fragment(tmp_path, "0010-a.rst", "[change]\nanchor=a\nheading=A\ncontext=a")
+ monkeypatch.setattr(_GENERATOR, "_CHANGES_DIR", tmp_path)
+ exit_code = _GENERATOR.main([])
+ captured = capsys.readouterr()
+ assert exit_code == 0
+ assert "A" in captured.out
+ assert len(_GENERATOR.collect_fragments(tmp_path)) == 1
+
+
+def test_committed_changes_directory_fragments_all_parse():
+ changes_dir = _REPO_ROOT / "changes"
+ for fragment_path in _GENERATOR.collect_fragments(changes_dir):
+ rendered = _GENERATOR.render_fragment(fragment_path)
+ assert rendered.strip() != ""
diff --git a/tests/tools/surface.test.py b/tests/tools/surface.test.py
new file mode 100644
index 0000000..0b8daf0
--- /dev/null
+++ b/tests/tools/surface.test.py
@@ -0,0 +1,89 @@
+"""Tests for the public-surface snapshot tool in ``tools/surface.py``."""
+
+import importlib.util
+from pathlib import Path
+
+_REPO_ROOT = Path(__file__).parent.parent.parent
+_TOOL_PATH = _REPO_ROOT / "tools" / "surface.py"
+_SURFACE_PATH = _REPO_ROOT / ".public-surface"
+
+
+def _load_tool():
+ spec = importlib.util.spec_from_file_location("edify_surface_tool", _TOOL_PATH)
+ assert spec is not None
+ assert spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+_TOOL = _load_tool()
+
+
+def test_committed_surface_file_exists_and_is_non_empty():
+ assert _SURFACE_PATH.exists()
+ assert _SURFACE_PATH.read_text(encoding="utf-8").strip() != ""
+
+
+def test_computed_surface_matches_the_committed_snapshot():
+ computed = _TOOL.compute_surface()
+ committed = _SURFACE_PATH.read_text(encoding="utf-8")
+ assert computed == committed, (
+ "public surface drift: run `python tools/surface.py --write` and commit "
+ ".public-surface with a changes/ fragment describing the change."
+ )
+
+
+def test_surface_lists_the_core_public_symbols():
+ computed = _TOOL.compute_surface()
+ assert "edify.Pattern" in computed
+ assert "edify.RegexBuilder" in computed
+ assert "edify.Regex" in computed
+ assert "edify.EdifyError" in computed
+
+
+def test_surface_excludes_private_module_paths():
+ computed = _TOOL.compute_surface()
+ for line in computed.splitlines():
+ dotted = line.split("(")[0]
+ parts = dotted.split(".")
+ assert not any(part.startswith("_") for part in parts[:-1]), (
+ f"surface line leaks a private module path: {line!r}"
+ )
+
+
+def test_surface_is_sorted_and_deduplicated():
+ computed = _TOOL.compute_surface()
+ lines = computed.splitlines()
+ assert lines == sorted(lines)
+ assert len(lines) == len(set(lines))
+
+
+def test_check_returns_zero_when_surface_matches(capsys):
+ exit_code = _TOOL.main(["--check"])
+ assert exit_code == 0
+
+
+def test_check_returns_one_when_surface_drifts(tmp_path, monkeypatch, capsys):
+ drifted_snapshot = tmp_path / ".public-surface"
+ drifted_snapshot.write_text("edify.SomethingRemoved\n", encoding="utf-8")
+ monkeypatch.setattr(_TOOL, "_SURFACE_PATH", drifted_snapshot)
+ exit_code = _TOOL.main(["--check"])
+ captured = capsys.readouterr()
+ assert exit_code == 1
+ assert "public surface drift" in captured.err
+
+
+def test_write_overwrites_the_snapshot_file(tmp_path, monkeypatch):
+ target = tmp_path / ".public-surface"
+ monkeypatch.setattr(_TOOL, "_SURFACE_PATH", target)
+ exit_code = _TOOL.main(["--write"])
+ assert exit_code == 0
+ assert target.read_text(encoding="utf-8") == _TOOL.compute_surface()
+
+
+def test_bare_invocation_prints_the_surface_to_stdout(capsys):
+ exit_code = _TOOL.main([])
+ captured = capsys.readouterr()
+ assert exit_code == 0
+ assert "edify.Pattern" in captured.out