US09-03: Write the Architecture Overview (#109)
This commit was merged in pull request #109.
This commit is contained in:
164
tests/integration/test_architecture_overview.py
Normal file
164
tests/integration/test_architecture_overview.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""US09-03: the architecture overview, checked against the architecture.
|
||||
|
||||
An architecture document is the one that rots most quietly: nothing breaks when it
|
||||
describes a module that was renamed two epics ago, it just quietly misleads the next
|
||||
person. So the parts of it that the code also knows — module names, state machines,
|
||||
table names, the modules an invariant is claimed to live in — are compared with the
|
||||
code, and a missing one fails the suite.
|
||||
|
||||
The diagrams' rendering is proven in ``tests/e2e/test_docs_ui.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import photo_pipeline.models # noqa: F401 (registers every table on Base.metadata)
|
||||
from photo_pipeline.db import Base
|
||||
from photo_pipeline.services import jobs, rename_journal, upload_batches
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
PACKAGE = REPO / "photo_pipeline"
|
||||
OVERVIEW = REPO / "docs" / "architecture.md"
|
||||
TEXT = OVERVIEW.read_text()
|
||||
|
||||
# Names the map does not owe the reader individually: the package markers, the CLI
|
||||
# entry point, and the two modules that exist to be small and obvious.
|
||||
UNMAPPED = {"__init__", "__main__", "logging"}
|
||||
|
||||
|
||||
def modules() -> set[str]:
|
||||
"""Every module and package under photo_pipeline, by the name it is imported as."""
|
||||
names = set()
|
||||
for path in PACKAGE.rglob("*.py"):
|
||||
relative = path.relative_to(PACKAGE)
|
||||
names.add(relative.parts[0] if len(relative.parts) > 1 else relative.stem)
|
||||
return {name.removesuffix(".py") for name in names} - UNMAPPED
|
||||
|
||||
|
||||
def states(container: type) -> set[str]:
|
||||
return {
|
||||
value
|
||||
for name, value in vars(container).items()
|
||||
if name.isupper() and isinstance(value, str)
|
||||
}
|
||||
|
||||
|
||||
# ── the module map ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_every_module_appears_in_the_map():
|
||||
"""A service nobody documented is a service the next person re-implements."""
|
||||
missing = sorted(name for name in modules() if name not in TEXT)
|
||||
assert missing == [], f"absent from the architecture overview: {missing}"
|
||||
|
||||
|
||||
def test_every_service_is_named_individually():
|
||||
"""The package table says what `services/` is for; this is the list that actually
|
||||
drifts, because a new service is added roughly every story."""
|
||||
services = {path.stem for path in (PACKAGE / "services").glob("*.py")} - UNMAPPED
|
||||
missing = sorted(name for name in services if not re.search(rf"\b{name}\b", TEXT))
|
||||
assert missing == [], f"services missing from the overview: {missing}"
|
||||
|
||||
|
||||
def test_the_map_names_no_module_that_does_not_exist():
|
||||
"""Every `services/<name>` or backticked module path in the document resolves."""
|
||||
referenced = set(re.findall(r"`(?:photo_pipeline/)?([a-z_]+)/([a-z_]+)\.py`", TEXT))
|
||||
referenced |= {("", name) for name in re.findall(r"`([a-z_]+)\.py`", TEXT)}
|
||||
missing = [
|
||||
f"{package}/{module}.py" if package else f"{module}.py"
|
||||
for package, module in referenced
|
||||
if not (PACKAGE / package / f"{module}.py").is_file()
|
||||
]
|
||||
assert missing == [], f"documented but absent from the source: {missing}"
|
||||
|
||||
|
||||
# ── the state machines ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_every_job_state_is_documented():
|
||||
missing = sorted(state for state in states(jobs.JobState) if state not in TEXT)
|
||||
assert missing == [], f"job states missing from the overview: {missing}"
|
||||
|
||||
|
||||
def test_every_rename_journal_state_is_documented():
|
||||
missing = sorted(state for state in states(rename_journal.JournalState) if state not in TEXT)
|
||||
assert missing == [], f"journal states missing from the overview: {missing}"
|
||||
|
||||
|
||||
def test_every_upload_batch_state_is_documented():
|
||||
missing = sorted(state for state in states(upload_batches.BatchState) if state not in TEXT)
|
||||
assert missing == [], f"batch states missing from the overview: {missing}"
|
||||
|
||||
|
||||
def test_the_unsafe_journal_states_are_named_as_the_ones_that_block():
|
||||
"""The reason an unrelated mutation is refused has to be findable."""
|
||||
for state in rename_journal.UNSAFE_STATES:
|
||||
assert state in TEXT
|
||||
assert "rename_recovery_required" in TEXT
|
||||
|
||||
|
||||
def test_an_uncertain_upload_is_documented_as_not_retryable():
|
||||
assert upload_batches.BatchState.UNKNOWN not in upload_batches.RUNNABLE_STATES
|
||||
assert "not** restartable" in TEXT or "not restartable" in TEXT
|
||||
|
||||
|
||||
# ── the data model ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_every_table_is_listed():
|
||||
missing = sorted(name for name in Base.metadata.tables if name not in TEXT)
|
||||
assert missing == [], f"tables missing from the overview: {missing}"
|
||||
|
||||
|
||||
def test_the_document_lists_no_table_that_does_not_exist():
|
||||
listed = set(re.findall(r"`([a-z_]+)`", TEXT))
|
||||
plausible = {name for name in listed if name.endswith("s") and "_" in name}
|
||||
invented = sorted(
|
||||
name
|
||||
for name in plausible
|
||||
if name not in Base.metadata.tables
|
||||
and not (PACKAGE / "services" / f"{name}.py").is_file()
|
||||
and name not in {"library_roots", "trusted_proxies", "allowed_hosts", "asset_paths"}
|
||||
)
|
||||
assert invented == [], f"looks like a table but is not one: {invented}"
|
||||
|
||||
|
||||
# ── the invariants ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_each_invariant_points_at_a_module_that_enforces_it():
|
||||
"""The point of the table is to answer 'where do I look'. A wrong answer there
|
||||
costs more than no answer."""
|
||||
claims = {
|
||||
"path_policy.is_excluded": PACKAGE / "path_policy.py",
|
||||
"path_policy.resolve_in_roots": PACKAGE / "path_policy.py",
|
||||
"services/app_lock.py": PACKAGE / "services" / "app_lock.py",
|
||||
"services/exif_checkpoint.py": PACKAGE / "services" / "exif_checkpoint.py",
|
||||
"services/archive_transfer.py": PACKAGE / "services" / "archive_transfer.py",
|
||||
"api/security.py": PACKAGE / "api" / "security.py",
|
||||
"imaging.py": PACKAGE / "imaging.py",
|
||||
}
|
||||
for claim, path in claims.items():
|
||||
assert claim in TEXT, f"the invariant table does not mention {claim}"
|
||||
assert path.is_file(), f"{claim} does not exist"
|
||||
function = claim.rpartition(".")[2] if "/" not in claim else ""
|
||||
if function and function != "py":
|
||||
assert f"def {function}" in path.read_text(), f"{claim} is not defined there"
|
||||
|
||||
|
||||
def test_the_lock_order_matches_the_ranks_in_the_code():
|
||||
from photo_pipeline.jobs import locks
|
||||
|
||||
order = [name for name, _ in sorted(locks.LOCK_RANK.items(), key=lambda item: item[1])]
|
||||
assert order[0].startswith("library"), "the broadest lock is no longer the library"
|
||||
assert "library → stage/job → album/folder → asset" in TEXT
|
||||
|
||||
|
||||
def test_the_frozen_donor_archive_is_described_as_provenance_not_a_dependency():
|
||||
"""That production never imports the frozen sources is proven by
|
||||
``tests/unit/test_legacy_archive.py`` — which also forbids any other test from
|
||||
naming that directory, so this one checks the claim by its consequence."""
|
||||
assert "must not import" in TEXT
|
||||
assert "provenance and rollback evidence" in TEXT
|
||||
Reference in New Issue
Block a user