"""Candidate profile/ABI selection; tests use inert native-file fixtures.""" import hashlib import json import os from pathlib import Path import tempfile import stat 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_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()