Files
framebridge/source/tests/test_linux_compatibility.py
FrameBridge release automation cdd980e1eb FrameBridge 0.3.4 corresponding-source release
Snapshot assembled only from the signed corresponding-source archive.
Source archive SHA-256: db91a270cdefc4be79056ed38a0b4a16747e1ab130592645eac40fa3809809df
2026-10-29 11:53:40 -04:00

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()