Release 1.1.3 with game window and Gamescope lifecycle fixes
This commit is contained in:
866
tests/test_ptlinux_packaging.py
Normal file
866
tests/test_ptlinux_packaging.py
Normal file
@@ -0,0 +1,866 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user