179 lines
7.4 KiB
Python
179 lines
7.4 KiB
Python
"""US09-02: the installation manual, checked against the thing it describes.
|
|
|
|
Documentation rots quietly. A setting is renamed, a command grows a flag, an exit
|
|
code changes meaning — and the manual keeps confidently saying the old thing until
|
|
somebody follows it into a bad afternoon. So every fact in it that the code also
|
|
knows is compared with the code, in both directions where both directions are
|
|
meaningful: a setting the manual invents fails, and a setting the manual forgot
|
|
fails too.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
from photo_pipeline.config import ENV_PREFIX, LEGACY_ALIASES, Config
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
MANUAL = REPO / "docs" / "installation.md"
|
|
MAIN = REPO / "photo_pipeline" / "__main__.py"
|
|
|
|
TEXT = MANUAL.read_text()
|
|
ENV_NAMES = re.compile(rf"{ENV_PREFIX}[A-Z0-9_]+")
|
|
|
|
# Read by docker-compose.yml, not by Config. They are configuration an installer sets,
|
|
# so the manual documents them; they are simply not application settings.
|
|
COMPOSITION_ONLY = {
|
|
f"{ENV_PREFIX}IMAGE",
|
|
f"{ENV_PREFIX}LIBRARY_HOST_PATH",
|
|
f"{ENV_PREFIX}PUBLISH_ADDRESS",
|
|
f"{ENV_PREFIX}UID",
|
|
f"{ENV_PREFIX}GID",
|
|
f"{ENV_PREFIX}ENV_FILE", # read before Config exists, so it is not a field
|
|
}
|
|
|
|
|
|
def documented_env_names() -> set[str]:
|
|
return set(ENV_NAMES.findall(TEXT))
|
|
|
|
|
|
@lru_cache
|
|
def cli_help(*args: str) -> str:
|
|
"""What the CLI says about itself, asked the way an operator asks."""
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "photo_pipeline", *args, "--help"],
|
|
cwd=REPO,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
return result.stdout
|
|
|
|
|
|
@lru_cache
|
|
def cli_commands() -> frozenset[str]:
|
|
listed = re.search(r"\{([a-z0-9,\-]+)\}", cli_help())
|
|
assert listed, f"the CLI listed no subcommands:\n{cli_help()}"
|
|
return frozenset(listed.group(1).split(","))
|
|
|
|
|
|
def cli_flags(command: str) -> frozenset[str]:
|
|
return frozenset(re.findall(r"--[a-z][a-z-]+", cli_help(command)))
|
|
|
|
|
|
# ── settings ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_every_setting_is_documented():
|
|
"""A setting nobody documents is a setting nobody configures deliberately."""
|
|
expected = {f"{ENV_PREFIX}{field.upper()}" for field in Config.model_fields}
|
|
missing = sorted(expected - documented_env_names())
|
|
assert missing == [], f"undocumented settings: {missing}"
|
|
|
|
|
|
def test_the_manual_invents_no_settings():
|
|
unknown = sorted(
|
|
name
|
|
for name in documented_env_names()
|
|
if name.removeprefix(ENV_PREFIX).lower() not in Config.model_fields
|
|
and name not in COMPOSITION_ONLY
|
|
)
|
|
assert unknown == [], f"documented but not a real setting: {unknown}"
|
|
|
|
|
|
def test_the_documented_defaults_are_the_real_defaults():
|
|
"""Spot-checked where a wrong default is actively dangerous: the bind address,
|
|
the trust boundary, and the gate that protects an irreplaceable library."""
|
|
assert Config.model_fields["host"].default == "127.0.0.1"
|
|
assert "`PHOTO_PIPELINE_HOST` | `127.0.0.1`" in TEXT
|
|
assert Config.model_fields["allowed_hosts"].default == ()
|
|
assert Config.model_fields["require_dry_run_approval"].default is False
|
|
assert "`PHOTO_PIPELINE_REQUIRE_DRY_RUN_APPROVAL` | `false`" in TEXT
|
|
|
|
|
|
def test_every_secret_is_marked_as_one():
|
|
for field in ("access_secret", "vision_api_key", "immich_api_key"):
|
|
name = f"{ENV_PREFIX}{field.upper()}"
|
|
row = next(line for line in TEXT.splitlines() if line.startswith(f"| `{name}`"))
|
|
assert "secret" in row.lower(), f"{name} is not marked as a secret"
|
|
|
|
|
|
def test_the_legacy_aliases_are_documented():
|
|
"""An operator with an existing photo_analyzer.env needs to know it still works."""
|
|
for alias in LEGACY_ALIASES:
|
|
assert alias in TEXT, f"legacy alias {alias} is undocumented"
|
|
|
|
|
|
# ── commands ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# The manual is an installation and operations manual, not a command reference: these
|
|
# are the commands an installer actually runs, and every one of them must exist.
|
|
OPERATIONAL = ("migrate", "serve", "worker", "backup", "verify-backup", "restore", "diagnostics")
|
|
|
|
|
|
def test_every_command_the_manual_tells_you_to_run_exists():
|
|
missing = [name for name in OPERATIONAL if name not in cli_commands()]
|
|
assert missing == [], f"the manual names commands the CLI does not have: {missing}"
|
|
undocumented = [name for name in OPERATIONAL if not re.search(rf"\b{re.escape(name)}\b", TEXT)]
|
|
assert undocumented == [], f"operational commands the manual omits: {undocumented}"
|
|
|
|
|
|
def test_every_documented_flag_exists_on_the_command_it_is_shown_with():
|
|
for command, flag in (
|
|
("backup", "--reason"),
|
|
("backup", "--keep"),
|
|
("restore", "--into"),
|
|
("worker", "--allow-legacy"),
|
|
("serve", "--allow-legacy"),
|
|
):
|
|
assert flag in cli_flags(command), f"{command} has no {flag}"
|
|
assert flag in TEXT, f"{flag} is undocumented"
|
|
|
|
|
|
# ── exit codes ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_every_documented_exit_code_is_one_the_cli_can_return():
|
|
documented = {int(code) for code in re.findall(r"^\| `(\d)` \|", TEXT, re.MULTILINE)}
|
|
returned = {int(code) for code in re.findall(r"^\s+return (\d)$", MAIN.read_text(), re.MULTILINE)}
|
|
assert documented, "no exit codes are documented"
|
|
assert documented <= returned | {0}, f"documented but unreachable: {sorted(documented - returned)}"
|
|
|
|
|
|
def test_every_refusal_exit_code_is_documented():
|
|
"""0 is success and 1 is 'it said why'; every other code is a specific refusal an
|
|
operator will meet at startup, and meeting an undocumented one is the worst case."""
|
|
returned = {int(code) for code in re.findall(r"^\s+return (\d)$", MAIN.read_text(), re.MULTILINE)}
|
|
documented = {int(code) for code in re.findall(r"^\| `(\d)` \|", TEXT, re.MULTILINE)}
|
|
missing = sorted(code for code in returned if code not in documented and code != 0)
|
|
assert missing == [], f"undocumented exit codes: {missing}"
|
|
|
|
|
|
# ── the promises the manual makes ────────────────────────────────────────────
|
|
|
|
|
|
def test_the_manual_carries_no_credential_shaped_example():
|
|
"""The repository's own scanner refuses these in a commit; a manual is exactly
|
|
where a real-looking one gets copied from."""
|
|
suspicious = re.findall(
|
|
r"(?i)(api[_-]?key|secret|token|password)\s*[=:]\s*[\"']?([A-Za-z0-9_\-]{12,})",
|
|
TEXT,
|
|
)
|
|
assert suspicious == [], f"credential-shaped example: {suspicious}"
|
|
|
|
|
|
def test_the_manual_states_the_invariants_an_installer_must_not_break():
|
|
for invariant in ("_IGNORE/", "One writer", "EXIF is verified before upload"):
|
|
assert invariant in TEXT, f"the manual does not state: {invariant}"
|
|
|
|
|
|
def test_the_manual_is_reachable_from_the_index():
|
|
assert "installation.md" in (REPO / "docs" / "index.md").read_text()
|