Snapshot assembled only from the signed corresponding-source archive. Source archive SHA-256: db91a270cdefc4be79056ed38a0b4a16747e1ab130592645eac40fa3809809df
270 lines
15 KiB
Python
270 lines
15 KiB
Python
"""Candidate profile/ABI selection; tests use inert native-file fixtures."""
|
|
import hashlib
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import stat
|
|
import sys
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from framebridge_expanded import compatibility as compat
|
|
from framebridge_expanded import runtime, uninstall
|
|
from framebridge_expanded import display_context, network
|
|
|
|
|
|
class LinuxCompatibilityTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.prefix = Path(self.temp.name)
|
|
|
|
def artifact(self, profile_id):
|
|
_distro, _release, minor, abi = compat.PROFILES[profile_id]
|
|
vendor = self.prefix / compat.VENDOR_RELATIVE
|
|
vendor.mkdir(parents=True, exist_ok=True)
|
|
for old in vendor.rglob("*.so"):
|
|
old.unlink()
|
|
# These inert files test selection only; package tests inspect real ELF.
|
|
suffix = ".cpython-" + abi[2:] + "-x86_64-linux-gnu.so"
|
|
for rel in ["_cffi_backend", "pydantic_core/_pydantic_core", "rpds/rpds"]:
|
|
path = vendor / (rel + suffix)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(b"synthetic ABI filename fixture, never imported")
|
|
manifest = dict(schema=1, profile_id=profile_id, architecture="amd64",
|
|
python_minor=minor, python_abi=abi, validation_status="candidate")
|
|
(self.prefix / "release-profile.json").write_text(json.dumps(manifest))
|
|
return manifest
|
|
|
|
def check(self, distro, version, python, **options):
|
|
return compat.assess(self.prefix, os_release={"ID": distro, "VERSION_ID": version},
|
|
machine=options.pop("machine", "x86_64"), python_version=python,
|
|
libc=options.pop("libc", ("glibc", "2.39")), **options)
|
|
|
|
def test_explicit_candidate_profiles_require_matching_artifacts(self):
|
|
targets = [("debian13-amd64-py313", "debian", "13", (3, 13)),
|
|
("ubuntu24.04-amd64-py312", "ubuntu", "24.04", (3, 12)),
|
|
("ubuntu26.04-amd64-py314", "ubuntu", "26.04", (3, 14)),
|
|
("mint22-amd64-py312", "linuxmint", "22.3", (3, 12)),
|
|
("lmde7-amd64-py313", "linuxmint", "7", (3, 13))]
|
|
for name, distro, version, python in targets:
|
|
with self.subTest(name=name):
|
|
self.artifact(name)
|
|
result = self.check(distro, version, python)
|
|
self.assertTrue(result["ok"], result)
|
|
self.assertEqual(name, result["profile_id"])
|
|
self.assertEqual("candidate", result["validation_status"])
|
|
self.assertTrue(result["warnings"])
|
|
|
|
def test_version_and_derivative_identity_do_not_silently_opt_in(self):
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
for distro, version in [("ubuntu", "22.04"), ("ubuntu", "24.10"),
|
|
("linuxmint", "21.3"), ("debian", "12"), ("pop", "24.04")]:
|
|
with self.subTest(distro=distro, version=version):
|
|
self.assertFalse(self.check(distro, version, (3, 12))["ok"])
|
|
self.assertIsNone(compat.profile_for({"ID": "other", "ID_LIKE": "ubuntu", "VERSION_ID": "24.04"}))
|
|
|
|
def test_public_builder_profiles_are_accepted_by_the_runtime_without_claiming_validation(self):
|
|
# Use the real public builder's profile output, rather than duplicating
|
|
# its validation_status value in a fixture. Archive/vendor verification
|
|
# has separate tests; --check-vendor does not stage/install a package.
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
|
|
import archive_sources
|
|
import build_expanded
|
|
targets = [("debian13-amd64-py313", "debian", "13", (3, 13)),
|
|
("ubuntu24.04-amd64-py312", "ubuntu", "24.04", (3, 12)),
|
|
("mint22-amd64-py312", "linuxmint", "22.3", (3, 12))]
|
|
for profile_id, distro, version, python in targets:
|
|
with self.subTest(profile=profile_id):
|
|
self.artifact(profile_id)
|
|
output = io.StringIO()
|
|
globals_before = {name: getattr(build_expanded, name) for name in
|
|
("VERSION", "PUBLIC_RELEASE", "SOURCE_ARCHIVE", "PUBLIC_FILENAME", "RELEASE", "STAGE", "PREFIX")}
|
|
args = ["--target", profile_id, "--public-release", "--source-manifest", str(self.prefix / "source.json"), "--check-vendor"]
|
|
if profile_id != "debian13-amd64-py313":
|
|
args += ["--vendor-dir", str(self.prefix / "reviewed-cp312")]
|
|
with patch.multiple(build_expanded, **globals_before), \
|
|
patch.object(build_expanded.subprocess, "run"), \
|
|
patch.object(build_expanded, "vendor_versions", return_value={}), \
|
|
patch.object(build_expanded, "validate_vendor", return_value={"vendor_sha256": "a" * 64}), \
|
|
patch.object(build_expanded, "release_source", return_value={"filename": "FrameBridge-0.3.4-source.tar.gz", "sha256": "b" * 64}), \
|
|
patch.object(build_expanded, "legal_preflight"), \
|
|
patch.object(archive_sources, "source_files", return_value={}), \
|
|
contextlib.redirect_stdout(output):
|
|
build_expanded.main(args)
|
|
manifest = json.loads(output.getvalue())
|
|
self.assertEqual(manifest["validation_status"], "release_build")
|
|
(self.prefix / "release-profile.json").write_text(json.dumps(manifest))
|
|
result = self.check(distro, version, python)
|
|
self.assertTrue(result["ok"], result)
|
|
self.assertEqual(result["validation_status"], manifest["validation_status"])
|
|
self.assertIn("Release assembly does not establish a tested installation", result["warnings"][0])
|
|
self.assertNotEqual(result["validation_status"], "validated")
|
|
|
|
def test_interpreter_artifact_and_cpu_must_all_match(self):
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 13))["ok"])
|
|
self.assertFalse(self.check("linuxmint", "22.1", (3, 12))["ok"])
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), machine="aarch64")["ok"])
|
|
|
|
def test_nonstandard_interpreter_abi_is_rejected(self):
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), implementation="PyPy")["ok"])
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), abi_flags="t")["ok"])
|
|
|
|
def test_mixed_native_abi_and_missing_extensions_are_rejected(self):
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
vendor = self.prefix / compat.VENDOR_RELATIVE
|
|
path = next(vendor.glob("_cffi_backend*.so"))
|
|
path.rename(path.with_name(path.name.replace("cpython-312", "cpython-313")))
|
|
result = self.check("ubuntu", "24.04", (3, 12))
|
|
self.assertFalse(result["ok"])
|
|
self.assertTrue(any("native extension does not match" in issue for issue in result["issues"]))
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
path = next(vendor.glob("_cffi_backend*.so"))
|
|
path.rename(path.with_name(path.name.replace("cpython-312", "cpython-312t")))
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 12))["ok"])
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
next((vendor / "rpds").glob("*.so")).unlink()
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 12))["ok"])
|
|
|
|
def test_libc_floor_and_libc_family_are_checked(self):
|
|
self.artifact("ubuntu24.04-amd64-py312")
|
|
for libc in [("glibc", "2.33"), ("musl", "1.2.5"), ("", "")]:
|
|
with self.subTest(libc=libc):
|
|
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), libc=libc)["ok"])
|
|
self.assertTrue(self.check("ubuntu", "24.04", (3, 12), libc=("glibc", "2.34"))["ok"])
|
|
|
|
def test_invalid_or_inconsistent_manifest_fails_closed(self):
|
|
original = self.artifact("ubuntu24.04-amd64-py312")
|
|
path = self.prefix / "release-profile.json"
|
|
for value in ["{", "[]", json.dumps({**original, "schema": True}),
|
|
json.dumps({**original, "python_abi": "cp313"}),
|
|
json.dumps({**original, "validation_status": "unknown"})]:
|
|
with self.subTest(value=value):
|
|
path.write_text(value)
|
|
result = self.check("ubuntu", "24.04", (3, 12))
|
|
self.assertFalse(result["ok"])
|
|
self.assertTrue(any("Invalid release-profile" in issue for issue in result["issues"]))
|
|
|
|
def test_missing_manifest_cannot_silently_relabel_legacy_vendor(self):
|
|
self.artifact("debian13-amd64-py313")
|
|
(self.prefix / "release-profile.json").unlink()
|
|
self.assertFalse(self.check("linuxmint", "22.1", (3, 13))["ok"])
|
|
self.assertTrue(self.check("debian", "13", (3, 13))["ok"])
|
|
with patch.object(compat, "INSTALLED_PREFIX", self.prefix):
|
|
self.assertFalse(self.check("debian", "13", (3, 13))["ok"])
|
|
|
|
def test_wayland_diagnostic_never_claims_native_desktop_control(self):
|
|
result = compat.display_support({"XDG_SESSION_TYPE": "wayland", "DISPLAY": ":0"}, {"kind": "text"})
|
|
self.assertFalse(result["native_wayland_cua"])
|
|
self.assertFalse(result["physical_x11_verified"])
|
|
self.assertIn("XWayland", result["notes"][0])
|
|
virtual = compat.display_support({"WAYLAND_DISPLAY": "wayland-0"}, {"kind": "virtual-x11"})
|
|
self.assertTrue(virtual["private_virtual_x11"])
|
|
self.assertFalse(virtual["native_wayland_cua"])
|
|
|
|
def test_doctor_uses_profile_result_and_start_stops_before_creating_files(self):
|
|
rejected = dict(ok=False, profile_id="ubuntu24.04-amd64-py312", issues=["ABI mismatch"], warnings=[])
|
|
with patch.object(runtime, "state_path", return_value=self.prefix), \
|
|
patch.object(runtime, "load_config", return_value={"role": "node", "headless": "auto"}), \
|
|
patch.object(runtime, "environment", return_value={"XDG_SESSION_TYPE": "wayland"}), \
|
|
patch.object(runtime.shutil, "which", return_value="/fixture/command"), \
|
|
patch.object(compat, "assess", return_value=rejected), \
|
|
patch.object(display_context, "missing_graphical_commands", return_value=[]), \
|
|
patch.object(display_context, "describe", return_value={"kind": "text"}), \
|
|
patch.object(network, "inspect", return_value={}), \
|
|
patch.object(runtime, "status", return_value={"running": False}):
|
|
doctor = runtime.doctor(self.prefix)
|
|
self.assertFalse(doctor["ok"])
|
|
self.assertIn("ABI mismatch", doctor["issues"])
|
|
self.assertFalse(doctor["compatible_debian_13_amd64"])
|
|
self.assertEqual("wayland", doctor["display_support"]["session_type"])
|
|
self.assertFalse(runtime.start(self.prefix)["ok"])
|
|
self.assertFalse((self.prefix / "logs").exists())
|
|
|
|
|
|
class ManagedInterpreterValidationTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.state = "/home/fixture/profile"
|
|
self.unit = self.root / ("framebridge-autostart-" + hashlib.sha256(self.state.encode()).hexdigest()[:16] + ".service")
|
|
|
|
def unit_text(self, interpreter):
|
|
return '\n'.join(["# FrameBridge managed autostart v1", 'Environment="PYTHONPATH=' + uninstall.MODULE_ROOT + '"',
|
|
"ExecStart=" + interpreter + " -B -m framebridge_expanded.autostart _run --state " + self.state])
|
|
|
|
def test_matching_minor_system_interpreters_preserve_cleanup(self):
|
|
self.assertEqual(compat.SYSTEM_PYTHONS, uninstall.SYSTEM_PYTHONS)
|
|
for interpreter in sorted(compat.SYSTEM_PYTHONS):
|
|
with self.subTest(interpreter=interpreter):
|
|
self.unit.write_text(self.unit_text(interpreter))
|
|
self.assertTrue(uninstall.managed_unit(self.unit, os.getuid()))
|
|
|
|
def test_arbitrary_interpreters_duplicate_commands_and_links_remain_rejected(self):
|
|
for interpreter in ["/tmp/python3.12", "/usr/bin/python3.11", "/usr/local/bin/python3.14"]:
|
|
self.unit.write_text(self.unit_text(interpreter))
|
|
self.assertFalse(uninstall.managed_unit(self.unit, os.getuid()))
|
|
self.unit.write_text(self.unit_text("/usr/bin/python3.12") + "\nExecStart=/bin/true")
|
|
self.assertFalse(uninstall.managed_unit(self.unit, os.getuid()))
|
|
real = self.root / "real"
|
|
real.write_text(self.unit_text("/usr/bin/python3.12"))
|
|
self.unit.unlink()
|
|
self.unit.symlink_to(real)
|
|
self.assertFalse(uninstall.managed_unit(self.unit, os.getuid()))
|
|
|
|
|
|
class RootHelperCleanupTests(unittest.TestCase):
|
|
def test_root_purge_uses_only_fixed_helper_and_bounded_environment(self):
|
|
calls = []
|
|
def run(args, **options):
|
|
calls.append((args, options))
|
|
return SimpleNamespace(returncode=0)
|
|
with patch.object(uninstall.os, "getuid", return_value=0), \
|
|
patch.object(uninstall.os.path, "lexists", return_value=True), \
|
|
patch.object(uninstall, "_trusted_root_file", return_value=True):
|
|
uninstall.purge_credential_helpers(run=run)
|
|
self.assertEqual([str(uninstall.CREDENTIAL_HELPER), "purge-all"], calls[0][0])
|
|
self.assertEqual({"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, calls[0][1]["env"])
|
|
self.assertEqual(30, calls[0][1]["timeout"])
|
|
|
|
def test_nonroot_or_unsafe_helper_never_executes(self):
|
|
calls = []
|
|
with patch.object(uninstall.os, "getuid", return_value=1000):
|
|
with self.assertRaises(RuntimeError):
|
|
uninstall.purge_credential_helpers(run=lambda *a, **kw: calls.append(a))
|
|
with patch.object(uninstall.os, "getuid", return_value=0), \
|
|
patch.object(uninstall.os.path, "lexists", return_value=True), \
|
|
patch.object(uninstall, "_trusted_root_file", return_value=False):
|
|
with self.assertRaises(RuntimeError):
|
|
uninstall.purge_credential_helpers(run=lambda *a, **kw: calls.append(a))
|
|
self.assertEqual([], calls)
|
|
|
|
def test_failure_to_purge_pauses_removal(self):
|
|
with patch.object(uninstall.os, "getuid", return_value=0), \
|
|
patch.object(uninstall.os.path, "lexists", return_value=True), \
|
|
patch.object(uninstall, "_trusted_root_file", return_value=True):
|
|
with self.assertRaises(RuntimeError):
|
|
uninstall.purge_credential_helpers(run=lambda *a, **kw: SimpleNamespace(returncode=1))
|
|
|
|
def test_fixed_helper_trust_rejects_links_nonroot_and_writable_parents(self):
|
|
target = uninstall.CREDENTIAL_HELPER
|
|
for mode, owner in [(stat.S_IFLNK | 0o777, 0), (stat.S_IFREG | 0o755, 1000),
|
|
(stat.S_IFREG | 0o777, 0)]:
|
|
with self.subTest(mode=mode, owner=owner), \
|
|
patch.object(Path, "lstat", return_value=SimpleNamespace(st_mode=mode, st_uid=owner)):
|
|
self.assertFalse(uninstall._trusted_root_file(target))
|
|
def info(path):
|
|
return SimpleNamespace(st_uid=0, st_mode=(stat.S_IFREG | 0o755) if path == target else (stat.S_IFDIR | 0o775))
|
|
with patch.object(Path, "lstat", autospec=True, side_effect=info):
|
|
self.assertFalse(uninstall._trusted_root_file(target))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|