Spaces:
Sleeping
Sleeping
| # Copyright (c) 2025-2026, RTE (https://www.rte-france.com) | |
| # This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. | |
| # If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, | |
| # you can obtain one at http://mozilla.org/MPL/2.0/. | |
| # SPDX-License-Identifier: MPL-2.0 | |
| """Unit tests for the D9 docs-as-checked-artifact gate (scripts/check_docs_tree.py). | |
| Two layers: | |
| 1. Fixture-driven: build a throwaway doc + tree under tmp_path and assert | |
| scan_docs() classifies each reference (exists / missing / removed-context / | |
| generated / line-anchor) correctly. Hermetic. | |
| 2. Self-guard: the REAL repo's inventory docs must pass the gate (main() == 0), | |
| so any future drift fails this test too — not just the CI step. | |
| """ | |
| import importlib.util | |
| import os | |
| import sys | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| def _load(): | |
| spec = importlib.util.spec_from_file_location( | |
| "check_docs_tree", os.path.join(HERE, "check_docs_tree.py") | |
| ) | |
| mod = importlib.util.module_from_spec(spec) | |
| # Register before exec so the dataclass's `from __future__ import annotations` | |
| # field resolution can find the module in sys.modules. | |
| sys.modules[spec.name] = mod | |
| spec.loader.exec_module(mod) | |
| return mod | |
| mod = _load() | |
| def _write(root, rel, text): | |
| p = root / rel | |
| p.parent.mkdir(parents=True, exist_ok=True) | |
| p.write_text(text, encoding="utf-8") | |
| return p | |
| def test_existing_reference_passes(tmp_path): | |
| _write(tmp_path, "expert_backend/services/foo.py", "x = 1\n") | |
| _write(tmp_path, "doc.md", "See `services/foo.py` for details.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert f.total == 0 | |
| def test_missing_reference_is_flagged(tmp_path): | |
| _write(tmp_path, "doc.md", "See `services/ghost.py` for details.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert [r for _, _, r in f.missing_paths] == ["services/ghost.py"] | |
| def test_removed_context_same_line_is_exempt(tmp_path): | |
| _write(tmp_path, "doc.md", "The `recommenders/_service_integration.py` module was removed.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert f.missing_paths == [] | |
| def test_removed_context_wrapped_line_is_exempt(tmp_path): | |
| # The "former … removed" qualifier wraps onto neighbouring lines. | |
| _write(tmp_path, "doc.md", | |
| "the former\n`recommenders/_service_integration.py` module\nwas removed in D1.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert f.missing_paths == [] | |
| def test_generated_artifact_is_exempt(tmp_path): | |
| _write(tmp_path, "doc.md", | |
| "Build produces `frontend/dist-standalone/standalone.html` and `Overflow_Graph/x.pdf`.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| # standalone.html (generated suffix) is exempt; Overflow_Graph/x.pdf has no | |
| # tracked extension so it's not a candidate at all — either way, no finding. | |
| assert f.missing_paths == [] | |
| def test_line_number_anchor_is_flagged(tmp_path): | |
| _write(tmp_path, "services/foo.py", "x = 1\n") | |
| _write(tmp_path, "doc.md", "See the singleton at `services/foo.py:352` for details.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert [a for _, _, a in f.line_anchors] == ["services/foo.py:352"] | |
| def test_line_range_anchor_is_flagged(tmp_path): | |
| _write(tmp_path, "doc.md", "`App.tsx:310-324` resets state.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert [a for _, _, a in f.line_anchors] == ["App.tsx:310-324"] | |
| def test_bare_filename_without_slash_is_ignored(tmp_path): | |
| # Bare names (`main.py`, `App.tsx`) are illustrative and too ambiguous to | |
| # resolve — only directory-qualified paths are existence-checked. | |
| _write(tmp_path, "doc.md", "Edit `main.py` and `types.ts` as needed.\n") | |
| f = mod.scan_docs(tmp_path, doc_files=["doc.md"]) | |
| assert f.missing_paths == [] | |
| def test_gate_returns_nonzero_only_without_warn_only(tmp_path, monkeypatch, capsys): | |
| _write(tmp_path, "doc.md", "Broken `services/ghost.py`.\n") | |
| monkeypatch.setattr(mod, "REPO_ROOT", tmp_path) | |
| monkeypatch.setattr(mod, "DOC_FILES", ["doc.md"]) | |
| assert mod.main([]) == 1 | |
| assert mod.main(["--warn-only"]) == 0 | |
| out = capsys.readouterr().out | |
| assert "services/ghost.py" in out | |
| def test_real_repo_inventory_docs_are_clean(): | |
| # Self-guard: the committed CLAUDE.md tree must stay drift-free, so a future | |
| # rename that forgets the docs fails HERE, not only in the CI gate step. | |
| assert mod.main([]) == 0 | |