Compare commits

...
2 Commits
Author SHA1 Message Date
xavierkandCommandCodeBot b2243a85f7 fix(packaging): add RPM ownership assertions, ghost group fix, and conffile check for #46
- Fix nfpm.yaml ghost directory to include `group: fenris` so RPM metadata
  matches the tmpfiles.d-created ownership (root:fenris 2750)
- Add RPM-native ownership assertions: store dir reported as package-owned
  via `rpm -qf`, store contents verified as never owned by the package
- Add RPM conffile assertion: `rpm -qc` verifies fenris.conf is listed
- Unify store dir stat assertion across both formats (deb and rpm both
  assert mode 2750 root:fenris)
- Remove unused `distro` parameter from `_find_package()`

All 16 packaging tests pass across the full matrix (3 deb + 1 rpm × 4 scenarios).
All 289 non-packaging tests pass.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-03 10:18:04 +05:30
xavierkandCommandCodeBot f1e1c0eebc fix(testing): fix containerized packaging tests for #45
- Copy packages to /pkg/ instead of /tmp/ to avoid tmpfs masking in
  docker run --tmpfs /tmp, which hid packages needed at runtime by the
  migration guard and upgrade tests
- Add version faking for deb upgrade test: sed the dpkg status to show
  version 0.2.0 so dpkg -i treats the reinstall as an upgrade and
  postinst receives the old-version argument
- For RPM upgrade test: extract and manually invoke the post scriptlet
  with $1=2 (upgrade arguments), since faking a different version in
  the binary RPM database is not practical
- Parameterize migration guard and upgrade assertions with pkg_name
  (and version) instead of hardcoding filenames

All 16 packaging tests now pass across debian:bookworm, ubuntu:22.04,
ubuntu:24.04, and fedora:40.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-03 02:59:55 +05:30
2 changed files with 62 additions and 27 deletions
+1
View File
@@ -39,6 +39,7 @@ contents:
type: ghost
file_info:
mode: 2750
group: fenris
scripts:
preinstall: packaging/preinst.sh
+61 -27
View File
@@ -55,7 +55,7 @@ def _container_exec(container: str, cmd: str) -> tuple[int, str]:
return r.returncode, r.stdout + r.stderr
def _find_package(fmt: str, distro: str | None = None) -> Path:
def _find_package(fmt: str) -> Path:
"""Locate the built package artifact."""
version = _get_version()
if fmt == "deb":
@@ -97,8 +97,8 @@ def _build_deb_dockerfile(image: str, pkg_name: str) -> str:
RUN apt-get update && apt-get install -y --no-install-recommends \\
python3 smartmontools systemd systemd-sysv dbus && \\
rm -rf /var/lib/apt/lists/*
COPY dist/{pkg_name} /tmp/{pkg_name}
RUN dpkg -i /tmp/{pkg_name} || apt-get install -f -y
COPY dist/{pkg_name} /pkg/{pkg_name}
RUN dpkg -i /pkg/{pkg_name} || apt-get install -f -y
""")
@@ -109,8 +109,8 @@ def _build_rpm_dockerfile(image: str, pkg_name: str) -> str:
RUN dnf install -y --setopt=install_weak_deps=False \
python3 smartmontools systemd dbus && \
dnf clean all
COPY dist/{pkg_name} /tmp/{pkg_name}
RUN rpm -ivh /tmp/{pkg_name}
COPY dist/{pkg_name} /pkg/{pkg_name}
RUN rpm -ivh /pkg/{pkg_name}
""")
@@ -184,22 +184,46 @@ def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
assert "device" in out.lower() or "devices" in out.lower() or "#" in out, \
"Config does not appear to be placeholder-commented"
if fmt == "rpm":
# rpm-native: config marked noreplace (not replaced on upgrade)
rc, out = _container_exec(
container,
"rpm -qc fenris 2>/dev/null | grep fenris.conf || true",
)
assert "fenris.conf" in out, "fenris.conf not listed as conffile by RPM"
# fenris group exists
rc, out = _container_exec(container, "getent group fenris")
assert rc == 0, "fenris group not created"
# Observation store directory
if fmt == "deb":
rc, out = _container_exec(container, "stat -c '%a %U %G' /var/lib/fenris")
assert rc == 0, "Observation store directory not created"
parts = out.strip().split()
assert parts[0] == "2750", f"Store dir mode: expected 2750, got {parts[0]}"
assert parts[1] == "root", f"Store dir owner: expected root, got {parts[1]}"
assert parts[2] == "fenris", f"Store dir group: expected fenris, got {parts[2]}"
else:
# rpm: directory owned by package (ghost)
rc, _ = _container_exec(container, "test -d /var/lib/fenris")
assert rc == 0, "Observation store directory not found"
rc, out = _container_exec(container, "stat -c '%a %U %G' /var/lib/fenris")
assert rc == 0, "Observation store directory not created"
parts = out.strip().split()
assert parts[0] == "2750", f"Store dir mode: expected 2750, got {parts[0]}"
assert parts[1] == "root", f"Store dir owner: expected root, got {parts[1]}"
assert parts[2] == "fenris", f"Store dir group: expected fenris, got {parts[2]}"
if fmt == "rpm":
# rpm-native: store dir reported as package-owned (ghost),
# but no contents are owned by the package
rc, out = _container_exec(container, "rpm -qf /var/lib/fenris 2>/dev/null")
assert rc == 0, "Store dir not reported as package-owned by RPM"
assert "fenris" in out.lower(), f"Store dir not owned by fenris package: {out}"
# No files inside the store should be package-owned
rc, out = _container_exec(
container,
"find /var/lib/fenris -mindepth 1 -type f -exec rpm -qf {} \\; 2>&1",
)
# rpm -qf exits 1 for unowned files; we expect all to be unowned
owned = [
line for line in out.strip().splitlines()
if not line.startswith("not owned by any package")
and "is not owned by any package" not in line
and rc == 0
]
assert not owned, f"Store contents owned by RPM (should not be): {owned}"
# Timer is disabled and inactive (dormant)
rc, out = _container_exec(
@@ -219,7 +243,7 @@ def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
assert rc != 0, "Legacy manifest.txt should not exist in package install"
def _assert_migration_guard(container: str, fmt: str) -> None:
def _assert_migration_guard(container: str, fmt: str, pkg_name: str) -> None:
"""Assert that install aborts on make-install remnants (spec §7, §9)."""
if fmt == "deb":
# Plant the legacy manifest marker
@@ -228,7 +252,7 @@ def _assert_migration_guard(container: str, fmt: str) -> None:
# Attempt install — should fail with migration pointer
rc, out = _container_exec(
container,
"dpkg -i /tmp/fenris_0.3.0_amd64.deb 2>&1 || true",
f"dpkg -i /pkg/{pkg_name} 2>&1 || true",
)
assert "remnants" in out.lower() or "migrat" in out.lower() or rc != 0, \
f"Migration guard did not trigger: {out}"
@@ -243,7 +267,7 @@ def _assert_migration_guard(container: str, fmt: str) -> None:
)
rc, out = _container_exec(
container,
"rpm -ivh /tmp/fenris-0.3.0-1.x86_64.rpm 2>&1 || true",
f"rpm -ivh /pkg/{pkg_name} 2>&1 || true",
)
assert "remnants" in out.lower() or "migrat" in out.lower() or rc != 0, \
f"Migration guard did not trigger: {out}"
@@ -253,7 +277,7 @@ def _assert_migration_guard(container: str, fmt: str) -> None:
)
def _assert_upgrade_semantics(container: str, fmt: str) -> None:
def _assert_upgrade_semantics(container: str, fmt: str, pkg_name: str, version: str) -> None:
"""Assert upgrade behavior (spec §7, ADR 0007 §6)."""
# Create a fake observation store using system Python
# (venv Python may not execute if host shared libs differ)
@@ -268,16 +292,26 @@ def _assert_upgrade_semantics(container: str, fmt: str) -> None:
)
if fmt == "deb":
# Fake older version so dpkg treats reinstall as upgrade
_container_exec(
container,
f"sed -i 's/^Version: {version}$/Version: 0.2.0/' /var/lib/dpkg/status",
)
# Re-install triggers upgrade path
rc, out = _container_exec(
container,
"dpkg --force-confnew -i /tmp/fenris_*.deb 2>&1 || "
f"dpkg --force-confnew -i /pkg/{pkg_name} 2>&1 || "
"apt-get install -f -y 2>&1 || true",
)
else:
rc, out = _container_exec(
# RPM: manually invoke the post scriptlet with upgrade arguments ($1=2)
# because faking a different version in the binary RPM database is not
# practical — we only need to verify the scriptlet's upgrade behaviour.
_container_exec(
container,
"rpm -Uvh /tmp/fenris-*.rpm 2>&1 || true",
f"rpm -q --scripts -p /pkg/{pkg_name} "
"| sed -n '/^postinstall scriptlet/,/^preuninstall/"
"{/^preuninstall/d;/^postinstall scriptlet/d;p}' | sh -s 2 2",
)
# Snapshot should exist
@@ -408,7 +442,7 @@ def test_migration_guard(skip_no_docker, image, fmt, version):
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-minimal smartmontools systemd systemd-sysv dbus && \
rm -rf /var/lib/apt/lists/*
COPY dist/{pkg_name} /tmp/{pkg_name}
COPY dist/{pkg_name} /pkg/{pkg_name}
# Plant make-install remnant BEFORE installing
RUN mkdir -p /var/lib/fenris && echo '# manifest' > /var/lib/fenris/manifest.txt
""")
@@ -418,7 +452,7 @@ def test_migration_guard(skip_no_docker, image, fmt, version):
RUN dnf install -y --setopt=install_weak_deps=False \
python3 smartmontools systemd dbus && \
dnf clean all
COPY dist/{pkg_name} /tmp/{pkg_name}
COPY dist/{pkg_name} /pkg/{pkg_name}
RUN mkdir -p /etc/systemd/system && \\
echo '[Unit]' > /etc/systemd/system/fenris-collect.timer
""")
@@ -444,7 +478,7 @@ def test_migration_guard(skip_no_docker, image, fmt, version):
)
try:
_assert_migration_guard(container, fmt)
_assert_migration_guard(container, fmt, pkg_name)
finally:
subprocess.run(
["docker", "rm", "-f", container],
@@ -497,7 +531,7 @@ def test_upgrade_semantics(skip_no_docker, image, fmt, version):
)
try:
_assert_upgrade_semantics(container, fmt)
_assert_upgrade_semantics(container, fmt, pkg_name, version)
finally:
subprocess.run(
["docker", "rm", "-f", container],