diff --git a/src/testium/api/termconsole.py b/src/testium/api/termconsole.py index 6b0a47e..8117cc7 100644 --- a/src/testium/api/termconsole.py +++ b/src/testium/api/termconsole.py @@ -81,9 +81,13 @@ class TermConsole(Console): bufsize=0) else: - self.term = pexpect.spawn( shell_cmd, - echo=False, - cwd=self.ppath) + # In Flatpak this returns a `flatpak-spawn --host` wrapper so the + # console behaves like a host shell (matching py_func / lua_func / + # run); elsewhere it's the chosen command unchanged. + from interpreter.utils import bins + argv = bins.host_console_command(shell_cmd, self.ppath) + self.term = pexpect.spawn(argv[0], args=argv[1:], + echo=False, cwd=self.ppath) self.q = BytesStore() self.t = threading.Thread(target=self.enqueue_output) diff --git a/src/testium/interpreter/test_items/dialog_choices_files/choices_dialog.py b/src/testium/interpreter/test_items/dialog_choices_files/choices_dialog.py index c37be62..69d9867 100644 --- a/src/testium/interpreter/test_items/dialog_choices_files/choices_dialog.py +++ b/src/testium/interpreter/test_items/dialog_choices_files/choices_dialog.py @@ -221,6 +221,11 @@ def main(args, conn=None): if conn: settings.setValue(SettingsLastChoices, result) + # Flush before sending: the parent terminates this subprocess as soon + # as it reads the result, so the QSettings destructor never runs and + # the write would race the kill (lost under Flatpak — see the + # tested-references dialog for the full rationale). + settings.sync() conn.send([result, success]) conn.close() else: diff --git a/src/testium/interpreter/test_items/tested_references_files/tested_refs_dialog.py b/src/testium/interpreter/test_items/tested_references_files/tested_refs_dialog.py index d1695e8..0dfabd8 100644 --- a/src/testium/interpreter/test_items/tested_references_files/tested_refs_dialog.py +++ b/src/testium/interpreter/test_items/tested_references_files/tested_refs_dialog.py @@ -76,6 +76,12 @@ def main(args, conn=None): if conn: settings.setValue(SettingsLastReference, result) + # Flush to disk *before* handing the result back: as soon as the parent + # receives it on the pipe it terminates this subprocess (SIGTERM, no + # handler), so the QSettings destructor never runs. Without sync() the + # write races the kill and is lost — reliably so under Flatpak, where + # the .conf is atomically renamed on the slower ~/.var/app overlay. + settings.sync() conn.send([result, success]) conn.close() else: diff --git a/src/testium/interpreter/utils/bins.py b/src/testium/interpreter/utils/bins.py index cdf4c6e..d358a9a 100644 --- a/src/testium/interpreter/utils/bins.py +++ b/src/testium/interpreter/utils/bins.py @@ -19,6 +19,7 @@ Public API import atexit import os +import shlex import shutil import subprocess import tempfile @@ -177,6 +178,27 @@ def flatpak_host_spawn(interp_bin, cmd_args, host_cwd, extra_env=None): return spawn +def host_console_command(shell_cmd, cwd): + """Build the argv to start *shell_cmd* as an ordinary interactive console. + + *shell_cmd* is the command the caller chose (a string — shell-split — or + an argv list); the choice is preserved verbatim. + + Outside Flatpak the command is returned unchanged. Inside Flatpak a bare + spawn would run in the sandbox under the runtime python3, so a host venv + (``/path/venv/bin/python3 -m mod``) can't see its pip deps. We simply run + it on the host with ``flatpak-spawn --host`` so it behaves like any other + terminal: flatpak-spawn passes the current environment through unchanged + and the shell (sourced venv, profile, …) sets things up as the user wants. + No env forwarding or scrubbing — the launcher's leaked PYTHONPATH points at + /app paths absent on the host, so it's inert there. + """ + argv = shlex.split(shell_cmd) if isinstance(shell_cmd, str) else list(shell_cmd) + if not _in_flatpak(): + return argv + return ["flatpak-spawn", "--host", f"--directory={cwd}", *argv] + + def _which_host_flatpak(name): """Resolve a binary name (or absolute path) on the host via flatpak-spawn. diff --git a/test/validation/items/console/test.tum b/test/validation/items/console/test.tum index b59a959..5e84ad7 100644 --- a/test/validation/items/console/test.tum +++ b/test/validation/items/console/test.tum @@ -94,6 +94,17 @@ {% endif %} - read_until: {expected: endOfCmd, timeout: 1, process_result: "'Hello' in r'''$(result)''' and 'PASS' in r'''$(result)''' "} +{% if os == "Linux" %} +- console: + name: Console runs on host (not the Flatpak sandbox) + doc: Regression guard for the 0.2.1 Flatpak bug (term console spawned inside the sandbox instead of on the host). /.flatpak-info exists only inside the sandbox, so the host-only marker is emitted (and matched by read_until) ONLY when the shell really runs on the host. On a broken Flatpak the marker never appears, read_until times out and the item FAILS. The marker is built at runtime ($M) so it is never present in the command line itself. Passes on every other channel. + console_name: term + key: $(test)_PASS + steps: + - writeln: 'test -e /.flatpak-info && M=SANDBOX || M=HOST; echo "console_host_check_$M"' + - read_until: {expected: console_host_check_HOST, timeout: 5} +{% endif %} + - console: name: Console closure execute_on_stop: true