import contextlib import os from pathlib import Path import runpy import shutil import struct import tempfile import threading import time import unittest from unittest import mock import zlib BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin" OPEN = runpy.run_path(str(BIN / "droiddeck-open"))["main"].__globals__ RUN = runpy.run_path(str(BIN / "droiddeck-proton-run"))["main"].__globals__ def elf(machine, extra=b""): head = bytearray(64) head[0:4], head[4], head[5], head[18] = b"\x7fELF", 2, 1, machine head[8:8 + len(extra)] = extra return bytes(head) class Temp(unittest.TestCase): def setUp(self): self.tmp = Path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, self.tmp, True) def write(self, relative, data, mode=0o644): path = self.tmp / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(data if isinstance(data, bytes) else data.encode()) path.chmod(mode) return path class KindTest(Temp): def test_each_file_is_told_by_its_content_then_its_name(self): cases = { "game.x86_64": (elf(62), "elf"), "Tool.AppImage": (elf(183, b"AI\x02"), "appimage"), "renamed": (elf(62, b"AI\x02"), "appimage"), "setup.exe": (b"MZ" + bytes(62), "windows"), "launcher.bat": (b"@echo off\r\n", "windows"), "no-extension": (b"MZ" + bytes(62), "windows"), "d3d11.dll": (b"MZ" + bytes(62), None), "start.sh": (b"echo hi\n", "script"), "run": (b"#!/usr/bin/env python3\n", "script"), "notes.txt": (b"hello\n", None), } for name, (data, expected) in cases.items(): with self.subTest(name): self.assertEqual(OPEN["kind"](str(self.write(name, data))), expected) def test_a_script_names_its_interpreter_or_gets_bash(self): self.assertEqual(OPEN["interpreter"](str(self.write("a", b"#!/usr/bin/env python3 -u\r\nx\n"))), ["/usr/bin/env", "python3", "-u"]) self.assertEqual(OPEN["interpreter"](str(self.write("b", b"echo\n"))), ["/bin/bash"]) class PlacementTest(Temp): def setUp(self): super().setUp() self.gpu = self.write("droiddeck-gpu", b"#!/bin/sh\n", 0o755) patcher = mock.patch.dict(OPEN, {"GPU": str(self.gpu), "unattended": lambda env: False}) patcher.start() self.addCleanup(patcher.stop) self.desktop = {"BL_DESKTOP": "1"} def placed(self, what, mode, env=None): return OPEN["placed"](["cmd"], what, mode, self.desktop if env is None else env) def test_programs_from_the_desktop_go_to_the_gpu(self): for what in ("windows", "elf", "appimage", "x86 script", "folder"): with self.subTest(what): self.assertEqual(self.placed(what, "run"), [str(self.gpu), "cmd"]) def test_a_plain_script_and_a_window_stay_on_the_desktop(self): self.assertEqual(self.placed("script", "run"), ["cmd"]) self.assertEqual(self.placed("windows", "window"), ["cmd"]) def test_away_from_the_desktop_nothing_moves(self): self.assertEqual(self.placed("windows", "run", {}), ["cmd"]) self.assertEqual(self.placed("windows", "run", dict(self.desktop, GAMESCOPE_WAYLAND_DISPLAY="x")), ["cmd"]) def test_konsole_takes_what_asks_for_a_terminal(self): with mock.patch.object(OPEN["shutil"], "which", return_value="/usr/bin/konsole"): self.assertEqual(self.placed("script", "terminal"), ["konsole", "--hold", "-e", "cmd"]) self.assertEqual(self.placed("windows", "terminal"), [str(self.gpu), "cmd"]) def test_an_exec_stays_where_its_output_is_read(self): self.assertEqual(self.placed("elf", "exec"), ["cmd"]) self.assertEqual(self.placed("windows", "exec"), [str(self.gpu), "cmd"]) with mock.patch.dict(OPEN, {"unattended": lambda env: True}): self.assertEqual(self.placed("elf", "exec"), [str(self.gpu), "cmd"]) class UnattendedTest(Temp): def with_stdout(self, path, env): saved = os.dup(1) try: with open(path, "w") as target: os.dup2(target.fileno(), 1) return OPEN["unattended"](env) finally: os.dup2(saved, 1) os.close(saved) def test_only_output_nobody_reads_counts(self): log = self.write("session/desktop.log", b"") env = {"BL_DEBUG_DIR": str(log.parent)} self.assertTrue(self.with_stdout(os.devnull, env)) self.assertTrue(self.with_stdout(str(log), env)) self.assertFalse(self.with_stdout(str(self.tmp / "out.txt"), env)) self.assertFalse(self.with_stdout(str(log), {})) read, write = os.pipe() saved = os.dup(1) try: os.dup2(write, 1) self.assertFalse(OPEN["unattended"](env)) finally: os.dup2(saved, 1) for fd in (saved, read, write): os.close(fd) class CommandTest(Temp): def test_each_kind_gets_its_runner(self): exe, script = str(self.tmp / "a.exe"), str(self.tmp / "go.sh") self.write("go.sh", b"#!/bin/sh\n") self.assertEqual(OPEN["command"](exe, ["-x"], "windows"), ([OPEN["PROTON_RUN"], exe, "-x"], str(self.tmp))) self.assertEqual(OPEN["command"](exe, [], "windows", True)[0], [OPEN["PROTON_RUN"], "--window", exe]) self.assertEqual(OPEN["command"](script, ["1"], "script")[0], ["/bin/sh", script, "1"]) self.assertEqual(OPEN["command"](script, [], "x86 script")[0], [OPEN["FEX"], "run", "--mode", "on", "--for", str(self.tmp), "--", "/bin/sh", script]) def test_a_native_program_on_noexec_storage_runs_from_a_copy(self): program = self.write("storage/tool", elf(183)) x86 = self.write("storage/game.x86_64", elf(62)) with mock.patch.dict(OPEN, {"CACHE": str(self.tmp / "cache"), "noexec": lambda path: True}): copy = OPEN["runnable"](str(program)) self.assertNotEqual(copy, str(program)) self.assertEqual(Path(copy).read_bytes(), elf(183)) self.assertTrue(os.access(copy, os.X_OK)) self.assertEqual(OPEN["runnable"](str(program)), copy) self.assertEqual(OPEN["runnable"](str(x86)), str(x86)) with mock.patch.dict(OPEN, {"noexec": lambda path: False}): self.assertEqual(OPEN["runnable"](str(program)), str(program)) class InstallTest(Temp): def test_the_run_entries_and_defaults_are_written_once(self): OPEN["install"](str(self.tmp)) entry = (self.tmp / "droiddeck-open.desktop").read_text() self.assertIn("Exec=/usr/local/bin/droiddeck-open %f", entry) self.assertIn("application/x-ms-dos-executable", entry) self.assertIn("Exec=/usr/local/bin/droiddeck-open --terminal %f", (self.tmp / "droiddeck-open-terminal.desktop").read_text()) defaults = (self.tmp / "mimeapps.list").read_text() self.assertIn("application/x-shellscript=droiddeck-open.desktop\n", defaults) self.assertIn("application/x-msi=droiddeck-open.desktop;droiddeck-open-window.desktop;\n", defaults) stamp = (self.tmp / "mimeapps.list").stat().st_mtime_ns time.sleep(0.01) OPEN["install"](str(self.tmp)) self.assertEqual((self.tmp / "mimeapps.list").stat().st_mtime_ns, stamp) class ImportTest(Temp): def test_the_app_unpacks_what_the_desktop_cannot(self): launch = self.tmp / "launch" launch.mkdir() folder = self.tmp / "imported/eden" (folder / "app").mkdir(parents=True) def app(): request = launch / "desktop-import-0123456789abcdef.request" while not request.exists(): time.sleep(0.05) text = request.read_text() request.unlink() self.assertEqual(text, "id=0123456789abcdef\nkind=appimage\npath=/root/Storage/Eden.AppImage\n") (launch / "desktop-import-0123456789abcdef.result").write_text("ok %s\n" % folder) worker = threading.Thread(target=app) worker.start() with mock.patch.dict(os.environ, {"BL_LAUNCH_DIR": str(launch)}), mock.patch.dict(OPEN, {"notify": lambda *a: None}): self.assertEqual(OPEN["imported"]("appimage", "/root/Storage/Eden.AppImage", "0123456789abcdef", "Eden"), str(folder)) worker.join() self.assertEqual(list(launch.iterdir()), []) def test_the_apps_answer_is_the_error(self): launch = self.tmp / "launch" launch.mkdir() (launch / "desktop-import-0123456789abcdef.result").write_text("error Not enough space\n") with mock.patch.dict(os.environ, {"BL_LAUNCH_DIR": str(launch)}), mock.patch.dict(OPEN, {"notify": lambda *a: None}): with self.assertRaisesRegex(RuntimeError, "^Not enough space$"): OPEN["imported"]("appimage", "/x.AppImage", "0123456789abcdef", "x") def test_outside_a_session_it_says_how_to_add_one(self): with mock.patch.dict(os.environ, {"BL_LAUNCH_DIR": ""}): with self.assertRaisesRegex(RuntimeError, "Add an app"): OPEN["imported"]("folder", "/x/start.sh", "0123456789abcdef", "x") def test_starts_of_one_image_unpack_it_once(self): image = self.write("Tool.AppImage", elf(183, b"AI\x02")) unpacks = [] def extract(cmd, cwd, stdout): unpacks.append(cwd) time.sleep(0.2) os.makedirs(os.path.join(cwd, "squashfs-root")) open(os.path.join(cwd, "squashfs-root", "AppRun"), "w").close() return 0 found = [] none = {what: (str(self.tmp / "none"), made) for what, (_, made) in OPEN["IMPORTS"].items()} with mock.patch.dict(OPEN, {"CACHE": str(self.tmp / "cache"), "IMPORTS": none, "notify": lambda *a: None}), \ mock.patch.object(OPEN["subprocess"], "call", extract): starts = [threading.Thread(target=lambda: found.append(OPEN["appimage"](str(image)))) for _ in range(3)] for start in starts: start.start() for start in starts: start.join() self.assertEqual(len(unpacks), 1) self.assertEqual(len(set(found)), 1) self.assertTrue(os.path.isfile(os.path.join(found[0], "app", "AppRun"))) self.assertEqual(Path(found[0], "arch").read_text(), "arm64\n") def test_an_earlier_import_is_found_by_its_key(self): self.write("user/eden/desktop-import", "0123456789abcdef\n/root/Storage/Eden.AppImage") (self.tmp / "user/eden/app").mkdir() self.write("user/other/desktop-import", "fedcba9876543210\n/root/Storage/Other.AppImage") self.write("apps/game/desktop-import", "fedcba9876543210\n/root/Storage/Game/start.sh") self.write("apps/game/entry", "start.sh") imports = {"appimage": (str(self.tmp / "user"), "app"), "folder": (str(self.tmp / "apps"), "entry")} with mock.patch.dict(OPEN, {"IMPORTS": imports}): self.assertEqual(OPEN["import_of"]("0123456789abcdef", "appimage"), str(self.tmp / "user/eden")) self.assertIsNone(OPEN["import_of"]("fedcba9876543210", "appimage")) self.assertEqual(OPEN["import_of"]("fedcba9876543210", "folder"), str(self.tmp / "apps/game")) class GameFolderTest(Temp): def setUp(self): super().setUp() self.storage = self.tmp / "storage" patcher = mock.patch.dict(OPEN, {"noexec": lambda path: True, "storage_root": lambda folder: str(self.storage)}) patcher.start() self.addCleanup(patcher.stop) def folder(self, path): return OPEN["game_folder"](str(self.storage / path)) def test_a_native_games_folder_on_noexec_storage_is_copied_in(self): self.write("storage/Download/Game/start.sh", b"#!/bin/sh\n./bin/game.aarch64\n") self.write("storage/Download/Game/bin/game.aarch64", elf(183)) self.write("storage/Download/Godot/Game.arm64", elf(183)) self.write("storage/Download/Godot/libgodot.so", elf(183)) self.write("storage/MyGame/start.sh", b"./game\n") self.write("storage/MyGame/game", elf(183)) self.assertEqual(self.folder("Download/Game/start.sh"), str(self.storage / "Download/Game")) self.assertEqual(self.folder("Download/Godot/Game.arm64"), str(self.storage / "Download/Godot")) self.assertEqual(self.folder("MyGame/start.sh"), str(self.storage / "MyGame")) def test_x86_games_run_where_they_lie(self): self.write("storage/Download/Game/start.sh", b"#!/bin/sh\n./bin/game.x86_64\n") self.write("storage/Download/Game/bin/game.x86_64", elf(62)) self.write("storage/Download/Unity/Game.x86_64", elf(62)) self.write("storage/Download/Unity/UnityPlayer.so", elf(62)) self.write("storage/Download/Old/game", elf(3)) for path in ("Download/Game/start.sh", "Download/Unity/Game.x86_64", "Download/Old/game"): with self.subTest(path): self.assertIsNone(self.folder(path)) def test_what_runs_where_it_lies_is_left_there(self): self.write("storage/Download/Scripts/hello.sh", b"echo hi\n") self.write("storage/Download/Tool/tool", elf(183)) self.write("storage/Download/start.sh", b"./Tool/tool\n") self.write("storage/start.sh", b"./Download/Tool/tool\n") self.write("storage/Download/Deep/a/b/c/d/lib.so", elf(183)) self.write("storage/Download/Deep/start.sh", b"echo\n") for path in ("Download/Scripts/hello.sh", "Download/Tool/tool", "Download/start.sh", "start.sh", "Download/Deep/start.sh"): with self.subTest(path): self.assertIsNone(self.folder(path)) self.write("internal/Game/start.sh", b"./game\n") self.write("internal/Game/game", elf(183)) with mock.patch.dict(OPEN, {"noexec": lambda path: False}): self.assertIsNone(OPEN["game_folder"](str(self.tmp / "internal/Game/start.sh"))) def test_the_app_copies_the_folder_in_once(self): script = self.write("storage/Download/Game/start.sh", b"./game\n") launch = self.tmp / "launch" launch.mkdir() copy = self.tmp / "apps/game" copy.mkdir(parents=True) (copy / "entry").write_text("start.sh") requests = [] def app(): while not requests: for request in launch.glob("desktop-import-*.request"): requests.append(request.read_text()) request.unlink() (launch / request.name.replace(".request", ".result")).write_text("ok %s\n" % copy) time.sleep(0.05) worker = threading.Thread(target=app) worker.start() imports = {what: (str(self.tmp / "none"), made) for what, (_, made) in OPEN["IMPORTS"].items()} with mock.patch.dict(os.environ, {"BL_LAUNCH_DIR": str(launch)}), \ mock.patch.dict(OPEN, {"CACHE": str(self.tmp / "cache"), "IMPORTS": imports, "notify": lambda *a: None}): self.assertEqual(OPEN["command"](str(script), ["-x"], "folder"), ([OPEN["SCRIPT_RUN"], str(copy), "-x"], str(script.parent))) worker.join() self.assertRegex(requests[0], "^id=[0-9a-f]{16}\nkind=folder\npath=%s\n$" % script) class ExecTest(Temp): def setUp(self): super().setUp() self.storage = self.tmp / "storage" self.write("storage/Download/Game/start.sh", b"#!/bin/sh\n./game.x86_64\n", 0o755) self.write("storage/Download/Game/game.x86_64", elf(62), 0o755) self.write("storage/Download/Game/helper.bin", elf(183), 0o755) self.write("storage/Download/Game/libhelper.so", elf(183)) self.copied = [] env = {k: v for k, v in os.environ.items() if k not in ("BL_DESKTOP", "BL_BINFMT", "GAMESCOPE_WAYLAND_DISPLAY")} for patcher in (mock.patch.dict(OPEN, {"noexec": lambda path: True, "storage_root": lambda folder: str(self.storage), "copied_in": lambda path: self.copied.append(path) or "/opt/droiddeck-apps/game", "steam_title": lambda path: None, "notify": lambda *a: None, "CACHE": str(self.tmp / "cache")}), mock.patch.dict(os.environ, env, clear=True), mock.patch.object(OPEN["os"], "chdir"), mock.patch.object(OPEN["os"], "execv", side_effect=self.execv), mock.patch.object(OPEN["subprocess"], "call", side_effect=self.call)): patcher.start() self.addCleanup(patcher.stop) self.ran = [] def execv(self, program, cmd): self.ran.append(cmd) raise SystemExit(0) def call(self, cmd, *args, **kwargs): self.ran.append(cmd) return 0 def open(self, *argv, env=None): os.environ.update(env or {}) with contextlib.suppress(SystemExit): OPEN["main"](list(argv)) return self.ran[-1] def test_an_x86_program_a_running_program_starts_runs_under_fex_where_it_lies(self): program = str(self.storage / "Download/Game/game.x86_64") for env in ({}, {"BL_BINFMT": "1"}): with self.subTest(env=env): self.assertEqual(self.open("--exec", program, "-x", env=env), [OPEN["FEX"], "run", "--mode", "auto", "--", program, "-x"]) self.assertEqual(self.copied, []) def test_an_in_place_hand_off_keeps_the_callers_working_folder(self): program = str(self.storage / "Download/Game/game.x86_64") with mock.patch("os.chdir") as chdir: self.open("--exec", program, env={"BL_BINFMT": "1"}) chdir.assert_not_called() self.open("--exec", str(self.storage / "Download/Game/helper.bin")) chdir.assert_called_once() def test_a_native_program_a_running_program_starts_still_gets_its_folder_copied_in(self): program = str(self.storage / "Download/Game/helper.bin") self.assertEqual(self.open("--exec", program, "-y"), [OPEN["SCRIPT_RUN"], "/opt/droiddeck-apps/game", "-y"]) self.assertEqual(self.copied, [program]) def test_opening_an_x86_program_from_the_desktop_still_copies_its_folder_in(self): program = str(self.storage / "Download/Game/game.x86_64") self.assertEqual(self.open(program, env={"BL_DESKTOP": "1"}), [OPEN["SCRIPT_RUN"], "/opt/droiddeck-apps/game"]) self.assertEqual(self.copied, [program]) def appinfo(apps): strings = [] def key(name): if name not in strings: strings.append(name) return struct.pack("