Files
voculinux/tests/test_reproducible_wheel.py
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

376 lines
15 KiB
Python

"""Exercise byte comparison and ensure failed verification exposes no release files."""
from __future__ import annotations
import importlib.util
import subprocess
import sys
from pathlib import Path
from types import ModuleType
import pytest
# Other legacy tests replace zipfile in sys.modules during collection.
ZIPFILE = getattr(sys, "_voculinux_real_zipfile", __import__("zipfile"))
SCRIPT = Path(__file__).resolve().parents[1] / "scripts/build_reproducible_wheel.py"
@pytest.fixture
def gate(monkeypatch: pytest.MonkeyPatch) -> ModuleType:
"""Load the standalone build tool without importing Voculinux/GTK."""
spec = importlib.util.spec_from_file_location("reproducible_wheel", SCRIPT)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
monkeypatch.setattr(module, "zipfile", ZIPFILE)
return module
def wheel(path: Path, content: str = "payload", year: int = 2020) -> Path:
"""Make a tiny real archive, including independently controllable ZIP metadata."""
path.parent.mkdir(parents=True, exist_ok=True)
info = ZIPFILE.ZipInfo("voculinux/__init__.py", date_time=(year, 1, 1, 0, 0, 0))
with ZIPFILE.ZipFile(path, "w") as archive:
archive.writestr(info, content)
return path
def test_identical_wheels_pass(gate: ModuleType, tmp_path: Path) -> None:
first = wheel(tmp_path / "a/package.whl")
second = wheel(tmp_path / "b/package.whl")
report = tmp_path / "comparison.txt"
gate.compare_wheels(first, second, report)
assert "PASS" in report.read_text()
assert gate.digest(first) in report.read_text()
@pytest.mark.parametrize("difference", ["payload", "metadata", "filename"])
def test_mismatches_fail_without_normalizing_archives(
gate: ModuleType, tmp_path: Path, difference: str
) -> None:
first = wheel(tmp_path / "a/package.whl")
second = wheel(
tmp_path / "b" / ("other.whl" if difference == "filename" else "package.whl"),
content="changed" if difference == "payload" else "payload",
year=2021 if difference == "metadata" else 2020,
)
report = tmp_path / "comparison.txt"
with pytest.raises(RuntimeError, match="reproducibility failed"):
gate.compare_wheels(first, second, report)
assert "FAIL" in report.read_text()
if difference == "payload":
assert "member content differs: voculinux/__init__.py" in report.read_text()
def test_snapshot_uses_working_source_without_ignored_build_state(
gate: ModuleType, tmp_path: Path
) -> None:
source = tmp_path / "repo"
source.mkdir()
subprocess.run(["git", "init", "-q", str(source)], check=True)
(source / ".gitignore").write_text(
"build/\n*.egg-info/\n.venv/\n.reproducible-wheel-stage-*/\n.reproducible-wheel.lock\n"
)
(source / "tracked.py").write_text("old")
(source / "deleted.py").write_text("deleted")
subprocess.run(["git", "add", "."], cwd=source, check=True)
(source / "tracked.py").write_text("working-tree edit")
(source / "deleted.py").unlink()
(source / "new.py").write_text("new file")
(source / ".reproducible-wheel.lock").touch()
for name in ("build", "stale.egg-info", ".venv", ".reproducible-wheel-stage-interrupted"):
(source / name).mkdir()
(source / name / "stale").write_text("must not be copied")
target = tmp_path / "snapshot"
gate.snapshot(source, target)
assert (target / "tracked.py").read_text() == "working-tree edit"
assert (target / "new.py").read_text() == "new file"
assert {path.name for path in target.iterdir()} == {"tracked.py", "new.py", ".gitignore"}
@pytest.mark.parametrize("mismatch", [False, True])
def test_staging_waits_for_comparison_and_uses_only_canonical_outputs(
gate: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mismatch: bool
) -> None:
"""Stand in for external tools; real comparison controls the publication boundary."""
source = tmp_path / "repo"
source.mkdir()
subprocess.run(["git", "init", "-q", str(source)], check=True)
subprocess.run(
[
"git",
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"commit",
"--allow-empty",
"-qm",
"fixture",
],
cwd=source,
check=True,
)
monkeypatch.setattr(gate, "ROOT", source)
monkeypatch.setenv("SOURCE_DATE_EPOCH", "1")
epoch = subprocess.check_output(
["git", "log", "-1", "--format=%ct"], cwd=source, text=True
).strip()
calls = []
def fake_run(argv: list[str], cwd: Path, env: dict[str, str], log: Path) -> None:
assert env["SOURCE_DATE_EPOCH"] == epoch
assert env["UV_PYTHON"] == sys.executable
calls.append((argv, cwd))
if "--outdir" in argv:
dist = Path(argv[argv.index("--outdir") + 1])
wheel(
dist / "package.whl",
"different" if mismatch and dist.name == "independent" else "payload",
)
(dist / "package.tar.gz").write_text(dist.name)
monkeypatch.setattr(gate, "run", fake_run)
output = tmp_path / "dist"
diagnostics = tmp_path / "diagnostics"
if mismatch:
with pytest.raises(RuntimeError, match="reproducibility failed"):
gate.build(sys.executable, output, diagnostics)
assert not output.exists()
else:
gate.build(sys.executable, output, diagnostics)
assert sorted(path.name for path in output.iterdir()) == ["package.tar.gz", "package.whl"]
assert (output / "package.tar.gz").read_text() == "canonical"
# A second invocation cleans both successful and partial failed builds.
calls.clear()
if mismatch:
with pytest.raises(RuntimeError, match="reproducibility failed"):
gate.build(sys.executable, output, diagnostics)
assert not output.exists()
else:
gate.build(sys.executable, output, diagnostics)
assert (output / "package.tar.gz").read_text() == "canonical"
builds = [(argv, cwd) for argv, cwd in calls if "--outdir" in argv]
assert len(builds) == 2
assert builds[0][0][0] != builds[1][0][0] # Separate venv interpreters.
assert builds[0][1] != builds[1][1] # Separate source/build trees.
assert all("--no-isolation" in argv for argv, _ in builds)
venvs = [argv for argv, _ in calls if argv[:2] == ["uv", "venv"]]
assert len(venvs) == 2
assert all("--system-site-packages" not in argv for argv in venvs)
installs = [argv for argv, _ in calls if argv[:3] == ["uv", "pip", "install"]]
assert len(installs) == 2
for argv in installs:
assert {"--require-hashes", "--no-deps", "--only-binary=:all:", "--reinstall"} <= set(argv)
def test_existing_output_is_preserved(gate: ModuleType, tmp_path: Path) -> None:
output = tmp_path / "dist"
output.mkdir()
existing = output / "user-file"
existing.write_text("keep")
with pytest.raises(RuntimeError, match="unrecognized or modified output"):
gate.build(sys.executable, output, tmp_path / "diagnostics")
assert existing.read_text() == "keep"
@pytest.mark.parametrize("unexpected", ["dist-file", "modified", "diagnostics-file", "symlink"])
def test_cleanup_validates_everything_before_removing_anything(
gate: ModuleType, tmp_path: Path, unexpected: str
) -> None:
output = tmp_path / "dist"
diagnostics = tmp_path / "build/reproducible-wheel"
canonical = wheel(diagnostics / "canonical/package.whl")
staged = wheel(output / "package.whl")
log = diagnostics / "inputs.log"
log.write_text("previous build")
unrelated_build = tmp_path / "build/user-file"
unrelated_build.write_text("keep")
if unexpected == "dist-file":
(output / "user-file").write_text("keep")
elif unexpected == "modified":
staged.write_text("user modified archive")
elif unexpected == "diagnostics-file":
(diagnostics / "user-file").write_text("keep")
else:
(diagnostics / "independent").symlink_to(output, target_is_directory=True)
with pytest.raises(RuntimeError):
gate.prepare_outputs(output, diagnostics)
assert canonical.exists()
assert staged.exists()
assert log.read_text() == "previous build"
assert unrelated_build.read_text() == "keep"
def test_cleanup_only_removes_gate_files(gate: ModuleType, tmp_path: Path) -> None:
output = tmp_path / "dist"
diagnostics = tmp_path / "build/reproducible-wheel"
wheel(diagnostics / "canonical/package.whl")
wheel(diagnostics / "independent/package.whl", content="failed build")
wheel(output / "package.whl")
(diagnostics / "inputs.log").write_text("previous build")
unrelated = tmp_path / "build/user-file"
unrelated.write_text("keep")
gate.prepare_outputs(output, diagnostics)
assert not list(output.iterdir())
assert not list(diagnostics.iterdir())
assert unrelated.read_text() == "keep"
def test_cleanup_rejects_symlinked_parent(gate: ModuleType, tmp_path: Path) -> None:
real_build = tmp_path / "real-build"
real_build.mkdir()
(tmp_path / "build").symlink_to(real_build, target_is_directory=True)
diagnostics = tmp_path / "build/reproducible-wheel"
wheel(diagnostics / "canonical/package.whl")
output = tmp_path / "dist"
staged = wheel(output / "package.whl")
with pytest.raises(RuntimeError, match="through symlink"):
gate.prepare_outputs(output, diagnostics)
assert staged.exists()
assert (real_build / "reproducible-wheel/canonical/package.whl").exists()
def test_cleanup_rejects_aliased_overlap(gate: ModuleType, tmp_path: Path) -> None:
output = tmp_path / "dist"
output.mkdir()
diagnostics = output / ".." / "dist"
with pytest.raises(RuntimeError, match="must not overlap"):
gate.prepare_outputs(output, diagnostics)
@pytest.mark.parametrize("interrupt_after", range(1, 7))
def test_interrupted_cleanup_can_be_retried(
gate: ModuleType,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
interrupt_after: int,
) -> None:
"""Canonical ownership evidence must survive until every staged copy is gone."""
output = tmp_path / "dist"
diagnostics = tmp_path / "build/reproducible-wheel"
for name in ("package.whl", "package.tar.gz"):
wheel(diagnostics / "canonical" / name)
wheel(diagnostics / "independent" / name)
wheel(output / name)
unrelated = tmp_path / "build/user-file"
unrelated.write_text("keep")
unlink = Path.unlink
deleted = 0
def interrupted_unlink(path: Path, missing_ok: bool = False) -> None:
nonlocal deleted
unlink(path, missing_ok=missing_ok)
deleted += 1
if deleted == interrupt_after:
raise OSError("simulated interruption")
with monkeypatch.context() as patch:
patch.setattr(Path, "unlink", interrupted_unlink)
with pytest.raises(OSError, match="simulated interruption"):
gate.prepare_outputs(output, diagnostics)
gate.prepare_outputs(output, diagnostics)
assert not list(output.iterdir())
assert not list(diagnostics.iterdir())
assert unrelated.read_text() == "keep"
@pytest.mark.parametrize("operation", ["copy", "rename"])
@pytest.mark.parametrize("interrupt_after", [1, 2])
def test_interrupted_staging_can_be_retried(
gate: ModuleType,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
operation: str,
interrupt_after: int,
) -> None:
"""An interrupted copy must never expose partial bytes as a release archive."""
output = tmp_path / "dist"
diagnostics = tmp_path / "diagnostics"
artifacts = tuple(
wheel(diagnostics / "canonical" / name) for name in ("package.whl", "package.tar.gz")
)
copy = gate.shutil.copy2
replace = Path.replace
calls = 0
def interrupted_copy(source: Path, target: Path) -> None:
nonlocal calls
calls += 1
if calls == interrupt_after:
target.write_bytes(b"partial archive")
raise KeyboardInterrupt("simulated interruption")
copy(source, target)
def interrupted_rename(source: Path, target: Path) -> Path:
nonlocal calls
result = replace(source, target)
calls += 1
if calls == interrupt_after:
raise KeyboardInterrupt("simulated interruption")
return result
with monkeypatch.context() as patch:
if operation == "copy":
patch.setattr(gate.shutil, "copy2", interrupted_copy)
else:
patch.setattr(Path, "replace", interrupted_rename)
with pytest.raises(KeyboardInterrupt, match="simulated interruption"):
gate.stage_distributions(artifacts, output)
for staged in output.iterdir():
assert staged.read_bytes() == (diagnostics / "canonical" / staged.name).read_bytes()
assert not list(tmp_path.glob(".reproducible-wheel-stage-*"))
# Exercise the next run's ownership validation, then build/stage again.
gate.prepare_outputs(output, diagnostics)
for artifact in artifacts:
wheel(artifact)
gate.stage_distributions(artifacts, output)
assert {path.name for path in output.iterdir()} == {path.name for path in artifacts}
def test_competing_process_does_not_touch_outputs(
gate: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The public build entry point must hold its lock before cleanup starts."""
monkeypatch.setattr(gate, "ROOT", tmp_path)
output = tmp_path / "dist"
diagnostics = tmp_path / "diagnostics"
staged = wheel(output / "package.whl")
canonical = wheel(diagnostics / "canonical/package.whl")
log = diagnostics / "inputs.log"
log.write_text("active build")
competitor = (
"import importlib.util, pathlib, sys; "
"spec = importlib.util.spec_from_file_location('gate', sys.argv[1]); "
"gate = importlib.util.module_from_spec(spec); spec.loader.exec_module(gate); "
"gate.ROOT = pathlib.Path(sys.argv[2]); "
"gate.build(sys.executable, gate.ROOT / 'dist', gate.ROOT / 'diagnostics')"
)
with gate.checkout_lock():
result = subprocess.run(
[sys.executable, "-c", competitor, str(SCRIPT), str(tmp_path)],
capture_output=True,
text=True,
timeout=5,
)
assert result.returncode != 0
assert "another reproducible-wheel build is active" in result.stderr
assert staged.exists() and canonical.exists()
assert log.read_text() == "active build"
# Reusing the persistent lock file must work after its owner exits.
with gate.checkout_lock():
pass
def test_build_failure_releases_checkout_lock(
gate: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(gate, "ROOT", tmp_path)
output = tmp_path / "dist"
output.mkdir()
(output / "user-file").write_text("keep")
with pytest.raises(RuntimeError, match="unrecognized or modified output"):
gate.build(sys.executable, output, tmp_path / "diagnostics")
with gate.checkout_lock():
pass