124 lines
5.0 KiB
Python
124 lines
5.0 KiB
Python
"""Regression test: ``hermes dump`` reports a real git SHA inside the container.
|
|
|
|
``.dockerignore`` excludes ``.git``, so ``git rev-parse HEAD`` fails inside
|
|
the published image. CI writes ``install-stamp.json`` before ``docker build``
|
|
(scripts/write_install_stamp.py) and it is COPY'd to the canonical
|
|
``/opt/hermes/install-stamp.json``. ``hermes dump`` reads the
|
|
commit from that stamp through ``hermes_cli.version_info``.
|
|
|
|
A local ``docker build`` (the ``built_image`` fixture in
|
|
``tests/docker/conftest.py``) has no CI stamp — only the Dockerfile's
|
|
distribution-only fallback, whose all-zero commit version_info skips. In
|
|
that case ``hermes dump`` falls back to ``(unknown)``.
|
|
|
|
This test asserts both cases:
|
|
|
|
* When the stamp exists in the image, ``hermes dump`` must show the first 8
|
|
characters of its commit, not ``(unknown)``.
|
|
* When the stamp is absent, ``hermes dump`` must show ``(unknown)`` — a guard
|
|
against the helper inventing a SHA from another source.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
|
|
_VERSION_LINE = re.compile(r"^version:\s+(?P<rest>.+)$", re.MULTILINE)
|
|
# The stamp-first version line is "X.Y.Z [<sha>] (YYYY-MM-DD)" — the sha
|
|
# bracket may be followed by the build-date suffix.
|
|
_SHA_BRACKET = re.compile(r"\[(?P<sha>[^\]]+)\]")
|
|
|
|
|
|
def _run_dump(image: str) -> str:
|
|
"""Return the stdout of ``docker run <image> dump``.
|
|
|
|
Relies on Docker's anonymous VOLUME for ``/opt/data`` (declared by the
|
|
Dockerfile) so the container's hermes user (UID 10000) can bootstrap
|
|
its config. Anonymous volumes are auto-cleaned by ``--rm``, so unlike
|
|
a host bind-mount we don't have to chown anything to UID 10000 (which
|
|
would break cleanup on non-root hosts).
|
|
"""
|
|
r = subprocess.run(
|
|
["docker", "run", "--rm", image, "dump"],
|
|
capture_output=True, text=True, timeout=120,
|
|
)
|
|
assert r.returncode == 0, (
|
|
f"hermes dump exited {r.returncode}: "
|
|
f"stderr={r.stderr[-1000:]!r}\nstdout={r.stdout[-1000:]!r}"
|
|
)
|
|
return r.stdout
|
|
|
|
|
|
def _read_stamp_commit_from_image(image: str) -> str | None:
|
|
"""Return the stamp commit from the image, or None when absent/unusable."""
|
|
r = subprocess.run(
|
|
[
|
|
"docker", "run", "--rm", "--entrypoint", "cat", image,
|
|
"/opt/hermes/install-stamp.json",
|
|
],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
if r.returncode != 0:
|
|
return None
|
|
try:
|
|
commit = json.loads(r.stdout).get("commit") or ""
|
|
except ValueError:
|
|
return None
|
|
# An all-zero commit is the writer's fallback placeholder, and
|
|
# version_info skips it the same way.
|
|
if not commit or set(commit) == {"0"}:
|
|
return None
|
|
return commit
|
|
|
|
|
|
def test_dump_reports_stamp_commit_when_present(built_image: str) -> None:
|
|
"""When the image carries an install stamp, dump must surface its commit.
|
|
|
|
Together with the smoke-test action (which exercises ``--help``), this
|
|
closes the regression loop for the missing-sha bug: any future change
|
|
that breaks the stamp -> dump pipeline will fail CI here.
|
|
"""
|
|
stamped = _read_stamp_commit_from_image(built_image)
|
|
# docker.yml writes this build input before building and testing the image.
|
|
# Read the independent input: the canonical runner scrubs CI/GITHUB_SHA.
|
|
source_stamp = Path(__file__).resolve().parents[2] / "install-stamp.json"
|
|
if os.environ.get("HERMES_TEST_IMAGE"):
|
|
assert source_stamp.is_file(), "prebuilt image requires its checkout build stamp"
|
|
if source_stamp.is_file():
|
|
expected = json.loads(source_stamp.read_text(encoding="utf-8-sig"))["commit"]
|
|
assert re.fullmatch(r"[0-9a-f]{40}", expected), "invalid expected build commit"
|
|
assert stamped == expected, "image lost or changed its build provenance"
|
|
if stamped is not None:
|
|
assert re.fullmatch(r"[0-9a-f]{40}", stamped), "malformed image stamp commit"
|
|
stdout = _run_dump(built_image)
|
|
|
|
match = _VERSION_LINE.search(stdout)
|
|
assert match, f"no `version:` line in dump output:\n{stdout[:2000]}"
|
|
sha_match = _SHA_BRACKET.search(match.group("rest"))
|
|
assert sha_match, (
|
|
f"`version:` line missing [<sha>] bracket: {match.group('rest')!r}"
|
|
)
|
|
reported = sha_match.group("sha")
|
|
|
|
if stamped is None:
|
|
# Local-build path: no stamp in the image. The fallback must stay
|
|
# '(unknown)' — a guard against the helper inventing a SHA.
|
|
assert reported == "(unknown)", (
|
|
f"expected '(unknown)' when no stamp is baked, got {reported!r}"
|
|
)
|
|
return
|
|
|
|
# CI path: the stamp exists. ``hermes dump`` shows the first 8 chars.
|
|
assert reported != "(unknown)", (
|
|
"install stamp present in image but dump still reported "
|
|
f"'(unknown)' — the stamp fallback is broken. Stamp commit: {stamped!r}"
|
|
)
|
|
assert reported == stamped[:8], (
|
|
f"dump reported {reported!r} but the stamp commit is {stamped!r} "
|
|
f"(expected first 8 chars: {stamped[:8]!r})"
|
|
)
|