9 Commits

Author SHA1 Message Date
3fb982b057 Solving refresh bug which disables every tests 2026-06-04 22:13:54 +02:00
53553dc1fa Allow floating number for console timeout 2026-06-04 22:11:23 +02:00
717727bf5a Unified let syntax to be a list of objects 2026-06-04 22:11:23 +02:00
d97d00c593 removed test logs 2026-06-02 00:00:40 +02:00
2b0c4b5ee0 release 2026-06-01 23:48:56 +02:00
59e63e1338 fix(flatpak): console on host + dialog persistence
- term console via flatpak-spawn --host so host venvs resolve (bins.host_console_command)
- QSettings sync() before subprocess kill in choices/tested-refs dialogs
- console regression test: fails on the in-sandbox 0.2.1 console

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 23:42:48 +02:00
de32a524da docs: testium_assist install instructions (Open VSX / VSCode)
Manual (modes.rst) and README: install the extension from Open VSX in
VSCodium/Cursor/etc., and as a .vsix by hand in Microsoft VSCode; note
that testium must be on PATH or set via testium.serverPath.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:56:34 +02:00
2515213b14 release preparation 2026-05-31 16:15:52 +02:00
0376b77494 fix(gui): show the testium icon in the GNOME task bar (Wayland)
Set the app id via setDesktopFileName so the window stops inheriting the
launcher's class ("python3" under the AppImage), which is what GNOME was
keying the wrong icon off. On native Wayland the task-bar icon comes from
an installed desktop file matched to the app id (setWindowIcon is
ignored there), so on Linux drop an idempotent desktop entry + 256px icon
under ~/.local/share. Flatpak keeps its own id/desktop; Windows / macOS
use the window icon. No-op off Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:14:04 +02:00
22 changed files with 254 additions and 33 deletions

View File

@@ -129,6 +129,22 @@ A VSCode / VSCodium client extension (`testium_assist`) wraps `testium lsp`;
the schema is built from testium itself, so new item types and parameters
appear in the editor on the next testium upgrade with no client change.
It is published on [Open VSX](https://open-vsx.org/extension/testium/testium-assist),
so in **VSCodium, Cursor, Windsurf, Theia and code-server** it installs from the
Extensions view (search `testium-assist`) or with
`codium --install-extension testium.testium-assist`.
**Microsoft VSCode** does not list Open VSX extensions, so install the `.vsix`
by hand — download it from the Open VSX page above, then *Extensions → ⋯ →
Install from VSIX…* or:
```sh
code --install-extension testium-assist-0.1.0.vsix
```
The extension runs `testium lsp`, so `testium` must be on the `PATH` (otherwise
point the `testium.serverPath` setting at the binary/AppImage).
## Troubleshooting
### `wl_proxy_marshal_flags` symbol error

View File

@@ -67,3 +67,36 @@ dependencies:
:caption: enable the language server for a wheel / source install
pip install 'testium[lsp]'
Installing the VSCode / VSCodium extension
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The *testium_assist* client extension is published on `Open VSX
<https://open-vsx.org/extension/testium/testium-assist>`_, the registry used by
VSCodium, Cursor, Windsurf, Eclipse Theia and code-server. In those editors,
open the Extensions view and search ``testium-assist``, or install it from the
command line:
.. code-block:: text
:caption: install in VSCodium and other Open VSX editors
codium --install-extension testium.testium-assist
Microsoft *VSCode* uses a different marketplace that does not list Open VSX
extensions, so install the packaged ``.vsix`` by hand. Download it from the
Open VSX page linked above, then either choose *Extensions**⋯*
*Install from VSIX…* in the UI, or run:
.. code-block:: text
:caption: install the .vsix in Microsoft VSCode
code --install-extension testium-assist-0.1.0.vsix
The extension launches ``testium lsp``, so the ``testium`` command must be on
the ``PATH``. If *testium* is installed elsewhere — a specific binary or an
AppImage — point the ``testium.serverPath`` setting at it instead.
Once installed, open a ``.tum`` file: completion of item types, hover
documentation and the outline view become available. If nothing happens, check
that no ``files.associations`` entry forces ``*.tum`` to another language (it
must stay the ``tum`` language the extension provides).

View File

@@ -9,9 +9,9 @@ This element is of the following form:
- let:
name: Let Item
values:
key1: value1
key2: value2
key3: <| $(variable)[$(loop_index)] |>
- key1: value1
- key2: value2
- key3: <| $(variable)[$(loop_index)] |>
The ``let`` element is used to set values in the global directory.

View File

@@ -51,8 +51,8 @@ The parameter file can be specified in the `.tum` file root:
:caption: configuration files definition in the main `.tum` test file
config_file:
config1.yaml
config2.yaml
- config1.yaml
- config2.yaml
main:
name: Test example

Binary file not shown.

View File

@@ -1,3 +1,9 @@
version 0.2.2
==============
- Flatpak sandbox issue fixed for term console. Now a term console is
exactly like a host console.
- Persistence fix of dialogs in case of flatpak.
version 0.2.1
==============
- Faster test loading, especially for large tests built from jinja

View File

@@ -1 +1 @@
0.2
0.2.2

View File

@@ -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)

View File

@@ -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:

View File

@@ -344,7 +344,7 @@ class TestItemConsoleReadUntil(TestItemConsoleAction):
def execute(self):
cons = self.get_console()
ru = self._prms.expanse(self._read_until)
read_timeout = int(self._prms.getParam("timeout", default=-1, processed=True))
read_timeout = float(self._prms.getParam("timeout", default=-1, processed=True))
mute = self._prms.getParam("mute", default=False, processed=True)
if read_timeout < 0:
read_timeout = None

View File

@@ -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:

View File

@@ -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.

View File

@@ -0,0 +1,95 @@
"""Install a desktop entry + icon under the user's data dir so desktop shells
show the testium icon in the task bar / dock.
On a native Wayland session GNOME takes a window's task-bar icon from the
``.desktop`` file whose name (or ``StartupWMClass``) matches the window
``app_id`` — ``QGuiApplication.setWindowIcon`` is ignored there. The portable
channels (source checkout, PyInstaller binary, AppImage) install no system
desktop file, so we drop an idempotent one in ``~/.local/share``. The window
``app_id`` is set to ``testium`` (see ``QApplication.setDesktopFileName`` in
``testium_win``), which is exactly this file's base name.
Flatpak ships its own ``org.testium.Testium.desktop`` and keeps its own app id,
so the caller skips this integration there.
"""
import os
import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap
# Must match QApplication.setDesktopFileName(...) for the GUI, and is used as
# both the desktop-file base name and the StartupWMClass.
APP_ID = "testium"
def _launch_command():
"""Best-effort Exec= for the menu entry. Not needed for icon matching, but
makes the entry actually launchable when possible."""
appimage = os.environ.get("APPIMAGE")
if appimage:
return f'"{appimage}"'
if getattr(sys, "frozen", False):
return f'"{os.path.abspath(sys.executable)}"'
argv0 = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else ""
if argv0 and os.path.exists(argv0):
return f'"{os.path.abspath(sys.executable)}" "{argv0}"'
return f'"{os.path.abspath(sys.executable)}" -m testium'
def ensure_desktop_entry():
"""Create (or refresh) ~/.local/share icon + desktop entry. Best-effort:
any failure is swallowed so it can never take the GUI down.
Freedesktop-only: a no-op off Linux (Windows / macOS use the window icon)."""
if not sys.platform.startswith("linux"):
return
try:
data_home = os.environ.get("XDG_DATA_HOME") or os.path.join(
os.path.expanduser("~"), ".local", "share"
)
icon_dir = os.path.join(data_home, "icons", "hicolor", "256x256", "apps")
app_dir = os.path.join(data_home, "applications")
icon_path = os.path.join(icon_dir, f"{APP_ID}.png")
desktop_path = os.path.join(app_dir, f"{APP_ID}.desktop")
os.makedirs(icon_dir, exist_ok=True)
os.makedirs(app_dir, exist_ok=True)
# Icon: render the bundled Qt resource to a PNG once. Requires a live
# QGuiApplication (the caller creates it before calling us).
if not os.path.isfile(icon_path):
pixmap = QPixmap(u":/black/testium_logo.png")
if not pixmap.isNull():
pixmap = pixmap.scaled(
256, 256,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
pixmap.save(icon_path, "PNG")
# Absolute Icon= path so the shell resolves it without an icon-cache
# refresh; StartupWMClass lets X11 / XWayland match too.
desktop = (
"[Desktop Entry]\n"
"Type=Application\n"
"Name=Testium\n"
"Comment=Test sequencer\n"
f"Icon={icon_path}\n"
f"Exec={_launch_command()} %f\n"
"Terminal=false\n"
f"StartupWMClass={APP_ID}\n"
"Categories=Utility;Development;\n"
)
# Write only when missing or changed, to avoid needless menu churn.
current = None
if os.path.isfile(desktop_path):
with open(desktop_path, "r") as fh:
current = fh.read()
if current != desktop:
with open(desktop_path, "w") as fh:
fh.write(desktop)
except Exception:
# Desktop integration is a nicety, never a hard requirement.
pass

View File

@@ -51,14 +51,18 @@ class TestFileManager:
w.disconnect_signals()
# Snapshot user-selected checkboxes and fold state so they survive a
# reload of the same file (same logic as session-restore through prefs).
# checkList works only if show_checkboxes is True
previous_check_list = w.treeTests.getCheckList()
previous_fold_list = w.treeTests.getFoldList()
previous_count = w.treeTests.getItemCount()
self.clear_process()
if self.load(file_name) and w.test_service is not None:
if w.treeTests.getItemCount() == previous_count:
w.treeTests.restoreCheckList(previous_check_list, w.test_service)
if self.load(file_name) and \
w.test_service is not None and \
w.treeTests.getItemCount() == previous_count:
if prefs.settings.show_checkboxes :
w.treeTests.restoreCheckList(previous_check_list, w.test_service)
w.treeTests.restoreFoldList(previous_fold_list)
w.reconnect_signals()
def _make_progress(self, w):

View File

@@ -678,6 +678,24 @@ def MainWin(
debug=False,
):
app = QApplication(sys.argv)
# Application identity so desktop shells (GNOME, ...) show the testium
# icon in the task bar / dock instead of a generic one. On Wayland this
# sets the surface app_id; on X11/XWayland it sets WM_CLASS, so the window
# stops inheriting the launcher's class (e.g. "python3" under the AppImage,
# which is what GNOME was keying the wrong icon off) and the window icon
# below is used as the fallback. In Flatpak the id must be the Flatpak app
# id so it matches the installed desktop file.
app.setApplicationName("Testium")
app.setApplicationDisplayName("Testium")
app.setDesktopFileName(os.environ.get("FLATPAK_ID", "testium"))
app.setWindowIcon(QIcon(u":/black/testium_logo.png"))
# On native Wayland the task-bar icon comes from an installed desktop file
# matched to the app_id, not from setWindowIcon(). Flatpak ships its own;
# for the other Linux channels drop an idempotent one under ~/.local/share.
# Windows / macOS use the window icon set above, so this is Linux-only.
if sys.platform.startswith("linux") and not os.environ.get("FLATPAK_ID"):
from main_win.desktop_integration import ensure_desktop_entry
ensure_desktop_entry()
ui = MainWindow(
test_file,
config_files,

View File

@@ -84,7 +84,18 @@
- read_until: {expected: HelloConsole, timeout: 1, mute: true}
- console:
name: Console read_until muted
name: Console read_until float timeout
console_name: term
key: $(test)_PASS
steps:
- writeln: echo "HelloConsole"
{% if os == "Windows" %}
- read_until: {expected: echo "HelloConsole", timeout: 0.2}
{% endif %}
- read_until: {expected: HelloConsole, timeout: 0.2}
- console:
name: Console read_until process result
console_name: term
key: $(test)_PASS
steps:
@@ -94,6 +105,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

View File

@@ -11,8 +11,8 @@
- let:
name: Let it be
values:
it: $(loop_param)
be: <| $(loop_param) == $(it) |>
- it: $(loop_param)
- be: <| $(loop_param) == $(it) |>
- loop:
name: Cycle iterating on list

View File

@@ -1,7 +1,7 @@
- let:
name: lua_func test constants,
values:
lua_func test parameter: test parameter lua_func
- lua_func test parameter: test parameter lua_func
- lua_func:
name: fail lua_func

View File

@@ -1,7 +1,7 @@
- let:
name: py_func test constants,
values:
py_func test parameter: test parameter
- py_func test parameter: test parameter
- py_func:
name: pass py_func

View File

@@ -1,7 +0,0 @@
main:
name: run sub-test (always fail)
steps:
- check:
name: fail
values:
- false

View File

@@ -1,7 +0,0 @@
main:
name: run sub-test (always pass)
steps:
- check:
name: pass
values:
- true

View File

@@ -31,7 +31,11 @@ main:
{% for item in items %}
# item test
- let: {name: {{ item }} test constants, values: {test: {{ item }}, test_path: items/$(test)}}
- let:
name: {{ item }} test constants
values:
- test: {{ item }}
- test_path: items/$(test)
- group:
name: {{ item }} test
steps: