from __future__ import annotations import hashlib import json import os from pathlib import Path import shutil import shlex import subprocess import sys import tarfile import tempfile import unittest from ptlinux import __version__ from ptlinux.desktop_entry import escape_icon_path, quote_exec_argument from ptlinux.launcher_wrapper import quote_shell_word from scripts.repository_paths import PROJECT_ROOT, SOURCE_ROOT from tools.sign_release_artifact import sign_release_artifact class AioPackagingTests(unittest.TestCase): def test_bootstrap_release_remains_reproducible_with_a_fixed_epoch(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) outputs = (root / "first", root / "second") for output in outputs: environment = os.environ.copy() environment.pop("PTLINUX_VERSION", None) environment.update( { "PTLINUX_OUTPUT_DIRECTORY": str(output), "PTLINUX_PYTHON": sys.executable, "SOURCE_DATE_EPOCH": "0", } ) built = subprocess.run( ["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(built.returncode, 0, built.stderr) first = { path.name: path.read_bytes() for path in outputs[0].iterdir() if path.is_file() } second = { path.name: path.read_bytes() for path in outputs[1].iterdir() if path.is_file() } self.assertEqual(first, second) def test_packaged_installer_uses_the_compatible_top_level_source_layout(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) output = root / "dist" build_environment = os.environ.copy() build_environment.pop("PTLINUX_VERSION", None) build_environment.update( { "PTLINUX_OUTPUT_DIRECTORY": str(output), "PTLINUX_PYTHON": sys.executable, "SOURCE_DATE_EPOCH": "0", } ) built = subprocess.run( ["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")], cwd=PROJECT_ROOT, env=build_environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(built.returncode, 0, built.stderr) release_name = ( f"pristontale-eu-linux-installer-v{__version__.replace('.', '-')}" ) archive = output / f"{release_name}.tar.gz" extracted = root / "extracted" with tarfile.open(archive, "r:gz") as bundle: bundle.extractall(extracted, filter="data") packaged_root = extracted / release_name self.assertTrue((packaged_root / "ptlinux").is_dir()) self.assertFalse((packaged_root / "src").exists()) home = root / "home" data_home = root / "desktop-data" install_directory = root / "managed-install" install_environment = os.environ.copy() install_environment.pop("PTLINUX_INSTALL_DIR", None) install_environment.update( { "HOME": str(home), "XDG_DATA_HOME": str(data_home), "PATH": "/usr/bin:/bin", "PTLINUX_PYTHON": sys.executable, } ) installed = subprocess.run( [ "bash", str(packaged_root / "install.sh"), "--install-dir", str(install_directory), "--skip-deps", "--skip-doctor", "--skip-runtime-bootstrap", ], cwd=packaged_root, env=install_environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(installed.returncode, 0, installed.stderr) current = install_directory / "app/current" self.assertTrue((current / "ptlinux/cli.py").is_file()) self.assertTrue((current / "ptdiag/logs.py").is_file()) def test_installer_dry_run_reports_destinations_without_writing(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) home = root / "home" data_home = root / "desktop-data" install_directory = root / "managed-install" environment = os.environ.copy() environment.pop("PTLINUX_INSTALL_DIR", None) environment.update( { "HOME": str(home), "XDG_DATA_HOME": str(data_home), "PATH": "/usr/bin:/bin", "PTLINUX_PYTHON": sys.executable, } ) result = subprocess.run( [ "bash", str(PROJECT_ROOT / "install.sh"), "--install-dir", str(install_directory), "--dry-run", "--skip-deps", "--skip-doctor", "--skip-runtime-bootstrap", ], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn(f"Install data: {install_directory}", result.stdout) self.assertIn("Skipping host dependency installation.", result.stdout) self.assertFalse(install_directory.exists()) self.assertFalse(home.exists()) self.assertFalse(data_home.exists()) def test_installer_writes_path_independent_desktop_entry(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) home = root / "home with spaces" data_home = root / "data with spaces" home.mkdir() fake_bin = root / "bin" fake_bin.mkdir() fake_umu = fake_bin / "umu-run" fake_umu.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") fake_umu.chmod(0o755) environment = os.environ.copy() environment.pop("PTLINUX_INSTALL_DIR", None) environment.update( { "HOME": str(home), "XDG_DATA_HOME": str(data_home), "PATH": f"{fake_bin}:/usr/bin:/bin", "PTLINUX_PYTHON": sys.executable, } ) result = subprocess.run( [ "bash", str(PROJECT_ROOT / "install.sh"), "--skip-deps", "--skip-doctor", ], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn(str(fake_umu.resolve()), result.stdout) self.assertIn( f"Note: {home / '.local/bin'} is not in PATH for this shell.", result.stdout, ) output_lines = result.stdout.splitlines() start_line = output_lines[output_lines.index("Start PristonTale EU with:") + 1] self.assertEqual( shlex.split(start_line), [str(home / ".local/bin/pristontale-eu"), "gui"], ) self.assertIn("export PATH=", result.stdout) default_install = home / "Games/PristontaleEU" self.assertTrue((default_install / "app/current/ptlinux").is_dir()) self.assertTrue((default_install / "app/current/ptlinux/umu.py").is_file()) self.assertTrue( (default_install / "app/current/ptlinux/ui/dashboard.py").is_file() ) self.assertTrue( ( default_install / "app/current/ptlinux/ui/viewmodels/downloader.py" ).is_file() ) self.assertTrue( (default_install / "app/current/ptlinux/native/pt-identity-probe.exe").is_file() ) self.assertTrue( (default_install / "app/current/ptlinux/native/ptr-bcrypt-proxy.dll").is_file() ) gamescope = ( default_install / "app/current/ptlinux/native/gamescope-3.16.20-ubuntu26.04-amd64" ) self.assertTrue(gamescope.is_file()) self.assertEqual(gamescope.stat().st_mode & 0o777, 0o755) self.assertFalse( (default_install / "app/current/ptlinux/desktop_entry.py").exists() ) self.assertFalse( (default_install / "app/current/ptlinux/launcher_wrapper.py").exists() ) self.assertFalse( (default_install / "app/current/ptlinux/world_fix.py").exists() ) self.assertTrue((default_install / "app/current").is_symlink()) self.assertEqual( (default_install / "app/current").resolve().stat().st_mode & 0o777, 0o755, ) launcher = home / ".local/bin/pristontale-eu" desktop = data_home / "applications/eu.pristontale.PTLinux.desktop" icon = data_home / "pixmaps/eu.pristontale.PTLinux.png" self.assertTrue(launcher.is_file()) self.assertEqual( icon.read_bytes(), (PROJECT_ROOT / "packaging/eu.pristontale.PTLinux.png").read_bytes(), ) entry = desktop.read_text(encoding="utf-8") exec_line = next(line for line in entry.splitlines() if line.startswith("Exec=")) icon_line = next(line for line in entry.splitlines() if line.startswith("Icon=")) self.assertEqual( exec_line, f"Exec={quote_exec_argument(str(launcher))} gui", ) self.assertEqual(icon_line, f"Icon={escape_icon_path(str(icon))}") self.assertIn("StartupWMClass=PristonTaleEU", entry) self.assertNotIn("TryExec=", entry) self.assertIn( f"install_directory={quote_shell_word(str(default_install))}", launcher.read_text(encoding="utf-8"), ) self.assertIn( 'exec "$python_bin" -B -P -m ptlinux', launcher.read_text(encoding="utf-8"), ) arguments = shlex.split(exec_line.removeprefix("Exec=")) self.assertEqual(arguments, [str(launcher), "gui"]) arguments[1] = "--help" launched = subprocess.run( arguments, cwd=root, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(launched.returncode, 0, launched.stderr) self.assertIn("PristonTale EU all-in-one", launched.stdout) stale_directory = root / "unpacked-old-installer" stale_package = stale_directory / "ptlinux" stale_package.mkdir(parents=True) (stale_package / "__init__.py").write_text("", encoding="utf-8") (stale_package / "__main__.py").write_text( 'print("STALE LAUNCHER WAS IMPORTED")\n', encoding="utf-8", ) isolated = subprocess.run( [launcher, "--version"], cwd=stale_directory, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(isolated.returncode, 0, isolated.stderr) self.assertEqual(isolated.stdout.strip(), f"pristontale-eu {__version__}") self.assertNotIn("STALE LAUNCHER", isolated.stdout) def test_installer_persists_custom_install_directory(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) home = root / "home" data_home = root / "desktop data" install_directory = root / "External $(touch INJECTED)'s Game" home.mkdir() environment = os.environ.copy() environment.pop("PTLINUX_INSTALL_DIR", None) environment.update( { "HOME": str(home), "XDG_DATA_HOME": str(data_home), "PATH": "/usr/bin:/bin", "PTLINUX_PYTHON": sys.executable, } ) result = subprocess.run( [ "bash", str(PROJECT_ROOT / "install.sh"), "--install-dir", str(install_directory), "--skip-deps", "--skip-doctor", "--skip-runtime-bootstrap", ], cwd=root, env=environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue((install_directory / "app/current/ptlinux").is_dir()) self.assertTrue((install_directory / "app/current").is_symlink()) launcher = home / ".local/bin/pristontale-eu" wrapper = launcher.read_text(encoding="utf-8") self.assertIn( f"install_directory={quote_shell_word(str(install_directory))}", wrapper, ) runtime_environment = environment.copy() runtime_environment.pop("PTLINUX_INSTALL_DIR", None) runtime_environment["XDG_DATA_HOME"] = str(root / "different data home") launched = subprocess.run( [str(launcher), "--help"], cwd=root, env=runtime_environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(launched.returncode, 0, launched.stderr) self.assertIn("PristonTale EU all-in-one", launched.stdout) self.assertFalse((root / "INJECTED").exists()) def test_installer_preserves_the_previous_default_on_upgrade(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) home = root / "home" data_home = root / "data" legacy_install = data_home / "pristontale-eu" legacy_game = ( legacy_install / "launcher-prefix/drive_c/Program Files (x86)/Pristontale EU/Game.exe" ) legacy_game.parent.mkdir(parents=True) legacy_game.write_bytes(b"existing managed game") legacy_package = legacy_install / "app/ptlinux" legacy_package.mkdir(parents=True) (legacy_package / "__init__.py").write_text( '__version__ = "0.4.7"\n', encoding="utf-8", ) settings_file = home / ".config/pristontale-eu/settings.json" settings_file.parent.mkdir(parents=True) settings_file.write_text( json.dumps( { "borderless": False, "runtime": "proton7", "proton_sync": "esync", } ), encoding="utf-8", ) environment = os.environ.copy() environment.pop("PTLINUX_INSTALL_DIR", None) environment.update( { "HOME": str(home), "XDG_DATA_HOME": str(data_home), "PATH": "/usr/bin:/bin", "PTLINUX_PYTHON": sys.executable, } ) result = subprocess.run( [ "bash", str(PROJECT_ROOT / "install.sh"), "--skip-deps", "--skip-doctor", "--skip-runtime-bootstrap", ], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("preserving its previous data location", result.stdout) launcher = home / ".local/bin/pristontale-eu" self.assertIn( f"install_directory={quote_shell_word(str(legacy_install))}", launcher.read_text(encoding="utf-8"), ) self.assertFalse((home / "Games/PristontaleEU/app").exists()) self.assertTrue((legacy_install / "app/current").is_symlink()) settings_result = subprocess.run( [launcher, "--json", "settings", "--init"], cwd=root, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(settings_result.returncode, 0, settings_result.stderr) migrated = json.loads(settings_result.stdout)["settings"] self.assertFalse(migrated["borderless"]) self.assertEqual(migrated["runtime"], "ge-proton") self.assertEqual(migrated["proton_sync"], "esync") persisted = json.loads(settings_file.read_text(encoding="utf-8")) self.assertEqual(persisted["runtime"], "ge-proton") self.assertIs(persisted["ge_proton_default_migrated"], True) def test_production_release_requires_a_signing_key(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: output = Path(temporary_directory) / "output" environment = os.environ.copy() environment.pop("PTLINUX_SIGNING_KEY", None) environment["PTLINUX_RELEASE_MODE"] = "production" environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output) result = subprocess.run( ["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(result.returncode, 2) self.assertIn("PTLINUX_SIGNING_KEY is required", result.stderr) self.assertFalse(output.exists()) def test_production_release_rejects_a_key_path_inside_the_checkout(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: output = Path(temporary_directory) / "output" environment = os.environ.copy() environment["PTLINUX_RELEASE_MODE"] = "production" environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output) environment["PTLINUX_PYTHON"] = sys.executable environment["PTLINUX_SIGNING_KEY"] = str( PROJECT_ROOT / "release-keys/fixture-private.pem" ) result = subprocess.run( ["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(result.returncode, 2) self.assertIn("outside the repository", result.stderr) self.assertFalse(output.exists()) def test_release_artifact_signature_is_generated_and_verified(self) -> None: openssl = shutil.which("openssl") if openssl is None: self.skipTest("OpenSSL is unavailable") with tempfile.TemporaryDirectory() as temporary_directory: root = Path(temporary_directory) private_key = root / "private.pem" public_key = root / "public.pem" artifact = root / "installer.tar.gz" artifact.write_bytes(b"release bytes") commands = ( [ openssl, "genpkey", "-quiet", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", "-out", str(private_key), ], [ openssl, "pkey", "-in", str(private_key), "-pubout", "-out", str(public_key), ], ) for command in commands: generated = subprocess.run( command, capture_output=True, text=True, timeout=20, check=False, ) self.assertEqual(generated.returncode, 0, generated.stderr) private_key.chmod(0o600) signature = sign_release_artifact( artifact, signing_key=private_key, public_key=public_key, ) self.assertTrue(signature.is_file()) self.assertEqual(signature.stat().st_mode & 0o777, 0o644) artifact.write_bytes(b"tampered release bytes") verification = subprocess.run( [ openssl, "dgst", "-sha256", "-verify", str(public_key), "-signature", str(signature), str(artifact), ], capture_output=True, text=True, timeout=20, check=False, ) self.assertNotEqual(verification.returncode, 0) def test_release_builder_packages_current_updater_sources(self) -> None: self.assertIn( f'version = "{__version__}"', (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"), ) with tempfile.TemporaryDirectory() as temporary_directory: output = Path(temporary_directory) environment = os.environ.copy() environment.pop("PTLINUX_VERSION", None) environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output) environment["SOURCE_DATE_EPOCH"] = "0" result = subprocess.run( ["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=30, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) release_name = ( f"pristontale-eu-linux-installer-v{__version__.replace('.', '-')}" ) archive = output / f"{release_name}.tar.gz" checksum = archive.with_suffix(archive.suffix + ".sha256") self.assertTrue(archive.is_file()) self.assertTrue(checksum.is_file()) self.assertFalse(Path(f"{archive}.sig").exists()) self.assertTrue((output / "launcher-update-public.pem").is_file()) zip_archive = output / f"{release_name}.zip" self.assertTrue(zip_archive.is_file()) self.assertFalse(Path(f"{zip_archive}.sig").exists()) update_archive = output / f"pristontale-eu-linux-update-v{__version__}.tar.gz" self.assertTrue(update_archive.is_file()) update_checksum = update_archive.with_suffix(update_archive.suffix + ".sha256") self.assertTrue(update_checksum.is_file()) manifest = output / "stable.json" self.assertTrue(manifest.is_file()) for artifact in ( archive, checksum, update_archive, update_checksum, manifest, ): self.assertEqual(artifact.stat().st_mode & 0o777, 0o644) expected_sha256 = checksum.read_text(encoding="utf-8").split()[0] self.assertEqual( hashlib.sha256(archive.read_bytes()).hexdigest(), expected_sha256, ) release_root = release_name with tarfile.open(archive, "r:gz") as release: members = release.getmembers() names = {member.name for member in members} self.assertTrue( all(member.mode & 0o022 == 0 for member in members), "release entries must not be group- or world-writable", ) self.assertEqual( next(member.mode for member in members if member.name == release_root), 0o755, ) self.assertEqual( {name for name in names if name.endswith(".md")}, {f"{release_root}/README.md"}, ) self.assertIn(f"{release_root}/ptlinux/desktop_entry.py", names) self.assertIn(f"{release_root}/ptlinux/install_status.py", names) self.assertIn(f"{release_root}/ptlinux/ui/dashboard.py", names) self.assertIn( f"{release_root}/ptlinux/ui/components/progress_panel.py", names, ) self.assertIn(f"{release_root}/ptlinux/launcher_wrapper.py", names) self.assertIn(f"{release_root}/ptlinux/self_update.py", names) self.assertIn(f"{release_root}/ptlinux/container_host.py", names) self.assertIn(f"{release_root}/release/config.json", names) self.assertIn(f"{release_root}/scripts/__init__.py", names) self.assertIn(f"{release_root}/scripts/repository_paths.py", names) self.assertIn(f"{release_root}/scripts/release_lib.py", names) self.assertFalse(any(name.startswith(f"{release_root}/devtools/") for name in names)) self.assertIn( f"{release_root}/ptlinux/launcher-update-public.pem", names, ) self.assertIn( f"{release_root}/ptlinux/native/pt-identity-probe.exe", names, ) self.assertIn( f"{release_root}/ptlinux/native/ptr-bcrypt-proxy.dll", names, ) gamescope_name = ( f"{release_root}/ptlinux/native/" "gamescope-3.16.20-ubuntu26.04-amd64" ) self.assertIn(gamescope_name, names) self.assertEqual( next(member.mode for member in members if member.name == gamescope_name), 0o755, ) for name in ( "gamescope-compat.README", "gamescope-compat.copyright", "gamescope-lazy-output-images.patch", "gamescope-wayland-live-resize.patch", "gamescope-wayland-letterbox-backing.patch", "gamescope-wayland-libdecor-input-scope.patch", "gamescope-keep-nested-windows-visible.patch", ): self.assertIn(f"{release_root}/ptlinux/native/{name}", names) self.assertNotIn( f"{release_root}/ptlinux/native/pt-world-fix.exe", names, ) self.assertNotIn(f"{release_root}/ptlinux/world_fix.py", names) archived_readme = release.extractfile(f"{release_root}/README.md") self.assertIsNotNone(archived_readme) assert archived_readme is not None readme = archived_readme.read().decode("utf-8") self.assertIn(release_name, readme) self.assertIn("./install.sh", readme) self.assertIn("pristontale-eu update --repair", readme) self.assertIn("pristontale-eu --help", readme) for command in ( "gui", "doctor", "container-host", "fetch", "setup", "update", "sync", "wine-update", "import-game", "cache-compatibility", "prepare", "play", "settings", "launcher-update", ): self.assertIn(f"pristontale-eu {command}", readme) archived_depot = release.extractfile(f"{release_root}/ptlinux/depot.py") self.assertIsNotNone(archived_depot) assert archived_depot is not None self.assertEqual( archived_depot.read(), (SOURCE_ROOT / "ptlinux/depot.py").read_bytes(), ) archived_login_trace = release.extractfile( f"{release_root}/ptlinux/login_trace.py" ) self.assertIsNotNone(archived_login_trace) assert archived_login_trace is not None login_trace_source = archived_login_trace.read().decode("utf-8") self.assertIn("LOGIN_TRACE_ENABLED = False", login_trace_source) self.assertNotIn("+relay", login_trace_source) self.assertNotIn("RelayInclude", login_trace_source) with tarfile.open(update_archive, "r:gz") as update: update_root = f"pristontale-eu-linux-update-v{__version__}" update_members = update.getmembers() update_names = {member.name for member in update_members} self.assertIn(f"{update_root}/release.json", update_names) self.assertIn(f"{update_root}/ptlinux/self_update.py", update_names) self.assertIn(f"{update_root}/ptlinux/container_host.py", update_names) self.assertIn(f"{update_root}/ptlinux/import_state.py", update_names) self.assertIn(f"{update_root}/ptlinux/ui/dashboard.py", update_names) self.assertIn( f"{update_root}/ptlinux/ui/dialogs/settings_dialog.py", update_names, ) self.assertNotIn( f"{update_root}/ptlinux/desktop_entry.py", update_names, ) self.assertNotIn( f"{update_root}/ptlinux/launcher_wrapper.py", update_names, ) self.assertIn( f"{update_root}/ptlinux/launcher-update-public.pem", update_names, ) self.assertIn( f"{update_root}/ptlinux/native/pt-identity-probe.exe", update_names, ) self.assertIn( f"{update_root}/ptlinux/native/ptr-bcrypt-proxy.dll", update_names, ) gamescope_name = ( f"{update_root}/ptlinux/native/" "gamescope-3.16.20-ubuntu26.04-amd64" ) self.assertIn(gamescope_name, update_names) self.assertEqual( next( member.mode for member in update_members if member.name == gamescope_name ), 0o755, ) for name in ( "gamescope-compat.README", "gamescope-compat.copyright", "gamescope-lazy-output-images.patch", "gamescope-wayland-live-resize.patch", "gamescope-wayland-letterbox-backing.patch", "gamescope-wayland-libdecor-input-scope.patch", "gamescope-keep-nested-windows-visible.patch", ): self.assertIn( f"{update_root}/ptlinux/native/{name}", update_names, ) self.assertNotIn( f"{update_root}/ptlinux/native/pt-world-fix.exe", update_names, ) self.assertNotIn(f"{update_root}/ptlinux/world_fix.py", update_names) def test_release_builder_rejects_unsafe_version_text(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: output = Path(temporary_directory) / "output" environment = os.environ.copy() environment["PTLINUX_VERSION"] = "../unsafe" environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output) result = subprocess.run( ["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")], cwd=PROJECT_ROOT, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(result.returncode, 2) self.assertIn("expected X.Y.Z", result.stderr) self.assertFalse(output.exists()) def test_compact_release_can_import_overlay_in_clean_interpreter(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: release_root = Path(temporary_directory) shutil.copytree(SOURCE_ROOT / "ptlinux", release_root / "ptlinux") (release_root / "ptdiag").mkdir() shutil.copy2( SOURCE_ROOT / "ptdiag" / "__init__.py", release_root / "ptdiag" / "__init__.py", ) shutil.copy2( SOURCE_ROOT / "ptdiag" / "assbin.py", release_root / "ptdiag" / "assbin.py", ) shutil.copy2( SOURCE_ROOT / "ptdiag" / "logs.py", release_root / "ptdiag" / "logs.py", ) environment = os.environ.copy() environment["PYTHONPATH"] = str(release_root) result = subprocess.run( [sys.executable, "-c", "import ptlinux.overlay"], cwd=release_root, env=environment, capture_output=True, text=True, timeout=10, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) if __name__ == "__main__": unittest.main()