19 Commits

Author SHA1 Message Date
cf5db9e112 docs: release note for the GUI test-tree search
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:18:38 +02:00
f56125ced3 docs: test-tree search (GUI)
Document the Ctrl+F find bar: field checkboxes, the blockSignals pass that
avoids the on_testChecked controller storm, and the flag-driven
_refresh_highlight (run > search > default) that keeps the run and search
highlight layers from clobbering each other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:15:11 +02:00
a4377d691f feat(gui): search the test tree (Ctrl+F)
Find bar over the test tree: highlight matches and navigate them
(Enter / ◂ ▸), with Name/Type/Doc checkboxes to choose the searched
fields. Ctrl+F toggles the bar (clearing the highlight); Esc / ✕ close.

- QTestTreeItem: matches_search(needle, fields) + a search highlight that
  shares one _refresh_highlight() with the green run highlight, recomputed
  from state flags (run > search > default) so the two layers never leave a
  stale/permanent colour. Amber bg + forced black text → readable in any
  theme.
- QTestTree.search()/clear_search(): single signal-blocked pass (setBackground
  fires itemChanged → on_testChecked, a controller storm otherwise); expands
  ancestors of matches; returns matches in visual order.
- MainWindow: the find bar widget + Ctrl+F shortcut + navigation; search is
  reset when a new test file is loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:57:02 +02:00
72b207aab6 feat(gui): pytest sub-items use the pytest icon
"pytest step" children now show the pytest logo (pytest.png), matching the
parent "pytest" item, instead of the generic document icon. Icon already
embedded in the compiled resources — no rcc regen needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:24:15 +02:00
f579599f1d release 0.3 preparation
- version 0.3 (the pytest item release)
- release note: 0.3 entry (user-facing)
- regenerated user manual (0.3 stamp + pytest item section)
- DESIGN: "Graceful item load" section

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:21:34 +02:00
1c598a1eae fix(pytest): robust plugin injection via pytest.main(plugins=[...])
The plugin was delivered by writing it to a temp dir, putting that dir on
PYTHONPATH and loading it with `python -m pytest -p _testium_pytest_plugin`.
That import-by-name failed in the AppImage runtime (ModuleNotFoundError:
_testium_pytest_plugin) so collection returned nothing and the item FAILed
— while wheel/pyinstaller/flatpak worked. Local sims forcing the AppImage
env path (apply_host_libs) passed, ruling out the env scrubbing.

Ship the plugin as a self-contained launcher run directly
(`python launcher.py ...`) that registers it as a plugin object via
pytest.main(plugins=[sys.modules[__name__]]): no PYTHONPATH, no `-p`, no
import-by-name. apply_host_libs is untouched. Verified on source, wheel,
pyinstaller, flatpak and AppImage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:19:30 +02:00
e167da97d0 feat(pytest): clear "pytest not installed" message
When collection finds nothing because pytest is missing on the host
interpreter, load() raises a dedicated message ("pytest is not installed
... pip install pytest") instead of the raw pytest output. The graceful
load mechanism surfaces it as a WARN at load + a clean FAIL at run, the
rest of the campaign keeps running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:24:27 +02:00
b4bfe72239 Merge branch 'feat/graceful-item-load' into feat/pytest-item
# Conflicts:
#	DESIGN.md
2026-06-14 20:23:08 +02:00
5cc795ebb3 feat: graceful load failure for module-loading items
A self-loading item that can't load its module/file (unittest test file
with a missing import, pytest not installed on the host, ...) no longer
aborts the whole test load. TestSet._load_item() wraps load(), warns at
load time and records item._load_error; @test_run turns it into a clean
run-time FAILURE. The rest of the campaign loads and runs.

Scoped to module-loading items (unittest; pytest once merged). Structural
action loading stays fail-fast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:19:43 +02:00
ea481b5889 feat(gui): dedicated pytest item icon
Use the official pytest logo (devicon) for the pytest item instead of
reusing python.png — visually distinct from the py_func (Python logo)
item. Three 64x64 theme variants (color RGBA, black RGBA silhouette,
white LA), declared in the QRC and compiled into testium_core_win_rc.py;
_ITEM_CONFIG "pytest" now points to pytest.png.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:57:42 +02:00
c9daaffea8 docs(pytest): add pytest test item manual section
New test_items/pytest_test_item.rst (params, host-subprocess execution,
pytest-must-be-installed note), wired into the toctree. Regenerated PDF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:44:47 +02:00
06ae210e02 Merge branch 'main' into feat/pytest-item
# Conflicts:
#	DESIGN.md
2026-06-14 19:42:02 +02:00
a875828de0 chore: bump version back to 0.2.4
0.3 is reserved for the upcoming pytest item release. Regenerated the
manual PDF so its version stamp matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:33:33 +02:00
8a498dd6ac fix: expand parameters at run time, not at load
Variable substitution ($(...)) must use the runtime global dict, so it
must happen at run time (execute), never at load (__init__).

- console telnet_port: was never expanded — `telnet_port: $(port)` stayed
  literal. Now expanded at run (processed=True in execute, like the other
  host/port params).
- test_item base: stop_on_failure / execute_on_stop are now stored raw and
  resolved at run time via properties (so a $(...) flag reflects the
  runtime value, not the load-time one).
- cycle iterator and git repo: drop the redundant load-time expansion
  (execute() already re-expands them).
- tested_references: fetch 'reference' raw, expand each value in execute().

Justified load-time exceptions kept: name, doc, skipped (static/GUI at
load) and unittest test_method (drives child loading at load).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:19:07 +02:00
3661a71145 docs(console): read_until list match + regex
Document that read_until's 'expected' accepts a list (match any) and the
new 'regex' flag, with examples and the bounded-window limitation note.
Regenerated manual PDF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:00:46 +02:00
e4300ecf7b feat(console): list/regex read_until, serial error clarity; v0.3
read_until:
- 'expected' now accepts a list of values (succeeds on any match).
- new 'regex: true' flag: each pattern is a Python regex (re.search over a
  bounded tail, Console.REGEX_WINDOW). Reports which pattern matched.

Serial console robustness & clarity:
- failed open() raises a clear ETUMRuntimeError ("Serial device '…' does not
  exist." / permission hint) instead of a raw pyserial traceback.
- a console whose open failed is safely "not open" (init _thd=None +
  isOpened guards in readchar/read_nowait/close) — no more cascading
  AttributeError: '_thd' on subsequent read steps.
- action handlers: one-liner for expected (ETUMRuntimeError) errors, full
  traceback kept for unexpected ones. All console errors use testium
  exceptions (ETUMRuntimeError).

Flatpak: grant --device=all so serial adapters (/dev/ttyUSB*, /dev/ttyACM*)
are visible in the sandbox.

Validation: new read_until list/regex (match + no-match) cases in
items/console/test.tum.

Version: 0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:54:00 +02:00
c77f56f2fb feat(items): add pytest test item
Run a user pytest file as a testium item, surfacing each collected test
as a child with its own PASS/FAIL/SKIP, duration and failure message.

Mirrors the unittest item but runs pytest in a subprocess on the host
interpreter (bins.python_bin(), like py_func/lua_func) so it works across
every packaging channel. A stdlib-only pytest plugin streams collected
node-ids and per-test results over stdout via sentinels; the parent parses
them live. Params: test_file, test_method. stop_on_failure maps to -x;
disabled children are reported NORUN without running.

Wiring: TYPE_PYTEST / TYPE_PYTEST_STEP constants, test_init registration,
self-loading branch in test_set, GUI tree icon. Schema/LSP pick it up
automatically from the declarative PARAMS.

Validation: test/validation/items/pytest/ (validation venv now installs
pytest).

WIP: paused mid-feature (DESIGN.md documented; manual section pending).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:09:09 +02:00
8c4e1b56b5 feat(windows): icon, windowed exe, no-admin installer
- PyInstaller exe built windowed (console=False) with package/testium.ico
  as the embedded icon (BMP entries for shell compatibility).
- Suppress stray subprocess console windows in the frozen Windows build via
  paths.no_window_kwargs() (CREATE_NO_WINDOW); wheel/source unchanged.
  Applied to py_process, lua_process, bins probes, sys_app_path_win.
- New per-user Inno Setup installer (package/innosetup/): no admin,
  version-scoped AppId/dir so versions install side-by-side, one Start
  Menu entry per version, .ico shipped for shortcut/uninstall icons.
- DESIGN.md + release_note.txt updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:57:48 +02:00
e0802a9a72 release 0.2.3 preparation 2026-06-10 12:30:18 +02:00
41 changed files with 1583 additions and 337 deletions

View File

@@ -194,6 +194,13 @@ pyside6-rcc testium_core_win.qrc -o testium_core_win_rc.py
Icons are assigned once when the test file is loaded (not updated live on theme change — a file reload is required).
## Test-tree search (GUI)
A find bar (Ctrl+F) over the `QTestTree` (`src/testium/main_win/test_tree.py`) highlights matching items and navigates them (Enter / ◂ ▸), with **Name / Type / Doc** checkboxes choosing which fields are searched. Ctrl+F toggles the bar (clearing the highlight); Esc / ✕ close it. The bar (`MainWindow._build_search_bar`, `testium_win.py`) is persistent and reset on each file load (`_reset_search`, called from `test_file_manager`).
- `QTestTree.search(text, fields)` / `clear_search()` run a **single pass wrapped in `blockSignals(True)`**: `setBackground` emits `itemChanged`, wired to `on_testChecked` (a per-item controller round-trip) — without blocking, searching storms the controller (100 % CPU / freeze) and corrupts the check-state. It expands the ancestors of each match and returns matches in **visual (pre-order)** order for navigation.
- `QTestTreeItem._refresh_highlight()` is the single source of truth for the name-column colours: the **search** highlight (pastel amber bg + forced black text, readable in light *and* dark themes) and the green **run** highlight (`setHighlighted`) are recomputed from state flags with precedence **run > search > default**. No brush is saved/restored, so the two layers never leave a stale/permanent colour when they overlap (e.g. searching while a test runs).
### `run` item
`src/testium/interpreter/test_items/test_item_run.py` — launches a `.tum` file in a new testium instance (`-b` in batch mode, `-r` in GUI mode). Result:
- **PASS** if the sub-instance launched and ran to completion (exit code is ignored)
@@ -203,6 +210,20 @@ The sub-test's own pass/fail result is intentionally not propagated.
The interpreter and entry point used to spawn the sub-instance are picked automatically by `_testium_launch_cmd()` based on how the parent was started (AppImage → `$APPIMAGE`; Flatpak → `flatpak run`; PyInstaller → the frozen binary; source/wheel → `[sys.executable, abspath(sys.argv[0])]`). The user cannot override either via the YAML — selecting a different testium binary or Python from a sub-test was removed because it was either ill-defined (bundle modes have no separable Python) or could mismatch the parent's environment in surprising ways.
### `pytest` item
`src/testium/interpreter/test_items/test_item_pytest.py` — the pytest analogue of the `unittest` item: runs a user pytest file and surfaces every collected test as a child item (one PASS/FAIL/SKIP per test, with duration + failure message in the report).
Unlike `unittest` (which runs in-process), pytest runs in a **subprocess on the host interpreter** (`bins.python_bin()`), like `py_func`/`lua_func` — so the user's pytest install and test dependencies live on the host and the item works across every packaging channel (incl. Flatpak via the same staging used by `py_func`).
- A stdlib-only pytest plugin (`_PLUGIN_SOURCE`, written to a temp dir and loaded with `-p`) streams sentinel-prefixed lines back over the subprocess stdout: `__TESTIUM_PYTEST_COLLECTED__` (node-id list, at collection), `__TESTIUM_PYTEST_START__` / `__TESTIUM_PYTEST_RESULT__` (per test). The parent parses them live; non-sentinel lines are forwarded to the log.
- `load()` runs `pytest --collect-only` once to build the child tree; `execute()` runs the enabled node-ids once and maps results back by node-id.
- pytest is invoked with `--capture=no` (so plugin sentinels + test prints reach our pipe), `-o addopts=` (neutralise user addopts — xdist/cov would break the per-test hook parsing), `-p no:cacheprovider`. `stop_on_failure``-x`; disabled children → NORUN without running.
- Params: `test_file` (required), `test_method` (optional list of function names, matched against the node-id function segment with the parametrisation suffix stripped). Registered as `cst.TYPE_PYTEST` / `TYPE_PYTEST_STEP`, loaded via the same self-loading branch as `unittest` in `test_set.load_test_recursively`.
- `load()` raises on a collection problem (pytest not installed → a dedicated "pip install pytest" message; bad file / unknown `test_method`). That raise is handled by the **Graceful item load** path below — a warning at load and a clean FAIL at run, never a crash.
### Graceful item load
A self-loading item whose `load()` fails (a `unittest` test file importing a missing module, `pytest` not installed on the host, …) must not abort the **whole** test load. `TestSet._load_item()` wraps the `load()` call: on any exception it emits `tm.print_warn(...)` and stores the reason in `item._load_error` instead of propagating. The `@test_run` wrapper (`test_item.py`) turns a non-None `_load_error` into a clean run-time `FAILURE` (the message is printed once by `write_footer`), so the rest of the campaign still loads and runs. Scoped to the self-loading, module-loading items (`unittest`, `pytest`); structural action loading (`console`/`plot`/`json_rpc`) stays fail-fast at load.
### Report exporters & plugins
`src/testium/interpreter/test_report/test_report.py``_EXPORTER_REGISTRY` dict maps a format name (cmd key in the YAML `report.export`) to a lazy loader. Built-ins: `text`, `json`, `junit` (needs `junit_xml`), `html` (needs `lxml`). `sqlite` is the storage layer, no-op as an export.
@@ -226,7 +247,8 @@ Four distribution channels coexist, all sharing the single `src/testium/` packag
| Channel | Where | Build | Notes |
|---------|-------|-------|-------|
| Wheel (`pip install`) | `src/pyproject.toml` | `python -m build` | Vanilla Python package; entry point `testium = "testium:main"`. |
| PyInstaller binary | `package/pyinstaller/` | `build.sh` | Single ~130 MB binary. `py_func`, `runtime`, `lua_func` bundled at `_MEIPASS` root so the **host** Python can find them when launched as `python3 py_func`. `api`/`interpreter` are **not** exposed (subprocess isolation). |
| PyInstaller binary | `package/pyinstaller/` | `build.sh` | Single ~130 MB binary. `py_func`, `runtime`, `lua_func` bundled at `_MEIPASS` root so the **host** Python can find them when launched as `python3 py_func`. `api`/`interpreter` are **not** exposed (subprocess isolation). Built windowed (`console=False`) with `package/testium.ico` as the exe icon — see "Windows frozen build". |
| Windows installer | `package/innosetup/` | `build.ps1` (Inno Setup 6) | Wraps the PyInstaller exe. Per-user, **no admin** (`PrivilegesRequired=lowest`, installs under `%LOCALAPPDATA%`). Version-scoped `AppId` + install dir so versions coexist side-by-side; one Start Menu entry per version. |
| Flatpak | `package/flatpak/` | `build.sh` (uses `flatpak-builder`) | KDE 6.10 runtime. The bundled Python runs only the main process; `py_func` / `lua_func` MUST run under the **host** interpreter (no Python/Lua bundled). Produces a distributable `.flatpak` bundle. |
| AppImage | `package/appimage/` | `build.sh` (Debian Bookworm container via Podman/Docker) | Bundles Python 3.11 for the main process; `py_func` / `lua_func` MUST run under the **host** interpreter. Build runs in a container so it works on Arch / any non-Debian host. |
@@ -257,6 +279,19 @@ The bundled Python (Flatpak's runtime python, AppImage's `python3.11`) is reserv
- If the host has no python3/lua, `ensure()` raises `ETUMRuntimeError` at test load with the candidate list — no silent fallback to a bundled interpreter.
- `py_process.py` additionally pops `PYTHONUSERBASE` (set to `/var/data/python` by the Flatpak runtime, which would hide `~/.local/lib/...`).
### Windows frozen build: no console, hidden subprocess windows
The PyInstaller exe is built **windowed** (`console=False` in `testium.spec`) so a
double-click doesn't open a console. The catch: a windowed process has **no console
to inherit**, so every console subprocess it spawns (the `py_func`/`lua_func` host
Python bridges — the otherwise-permanent window — plus the `where`/`which`/`--version`
probes) opens its **own** console window. `paths.no_window_kwargs()` returns
`{"creationflags": CREATE_NO_WINDOW}` on a frozen Windows build (and `{}` everywhere
else, so the **wheel/source** keeps its console and child output stays visible). It is
applied at every spawn site: `py_process.py`, `lua_process.py`, `bins._run_probe`,
`paths.sys_app_path_win`. `termconsole.py` is intentionally exempt (it already hides
`cmd.exe` via `STARTUPINFO`).
### Declarative test item parameters
Each `TestItem` subclass declares its accepted parameters as a class attribute `PARAMS = ParamSet(Param(...), ...)` (`interpreter/utils/param_decl.py`). The descriptor carries the parameter name, *kind* (`SCALAR` — the default and may be omitted; `LIST`; `BLOCK`; `Enum("a", "b", ...)`), `required` flag, `default`, and free-form `doc`. There is **no Python type** in the descriptor on purpose: most parameter values are expressions (`$(...)` / `<| ... |>`) whose effective type is only known after expansion, so a static type would be misleading. Post-expansion `validate=lambda v: ...` callbacks are available as an opt-in for the rare cases where a runtime check is warranted (e.g. a specific format).
@@ -288,6 +323,11 @@ The `testium_assist` editor extension is a thin LSP client that spawns `testium
Both Flatpak and AppImage export `TESTIUM_VERSION` from a launcher (Flatpak: launcher script in `org.testium.Testium.yaml`; AppImage: `runtime.env` in `AppImageBuilder.yml`). `get_testium_version()` checks `/.flatpak-info` / `APPIMAGE` and reads `TESTIUM_VERSION` rather than relying on package metadata or repo introspection.
## Recent fixes / notable changes
- Test-tree search (GUI): a Ctrl+F find bar highlights + navigates matching items, with Name/Type/Doc field checkboxes. Search modifications run under `blockSignals` (else `setBackground``itemChanged``on_testChecked` storms the controller), and the search/run highlights share one flag-driven `_refresh_highlight()` (run > search > default) so overlapping layers never leave a stale colour. See "## Test-tree search (GUI)".
- `pytest` item: pytest analogue of `unittest`, but runs on the **host interpreter in a subprocess** (`bins.python_bin()`, like `py_func`) so it works across every packaging channel. A stdlib-only pytest plugin streams collected node-ids + per-test results back over stdout via sentinels; each test becomes a child item with its own PASS/FAIL/SKIP, duration and failure message. Params: `test_file`, `test_method`. Validation item: `test/validation/items/pytest/` (the validation venv now pip-installs `pytest`). See "### `pytest` item".
- Graceful item load: a self-loading item that fails to load its module/file (e.g. a `unittest` test file importing a missing module, or `pytest` not installed on the host) no longer aborts the **whole** test load. `TestSet._load_item()` wraps the item's `load()`, emits a `tm.print_warn(...)` at load time and records the reason in `item._load_error`; the `@test_run` wrapper turns a non-None `_load_error` into a clean run-time `FAILURE` (message printed once via `write_footer`). The rest of the campaign loads and runs normally. Applies to module-loading items (`unittest`, `pytest`); structural action loading stays fail-fast.
- `console` item — serial robustness + richer `read_until`: (1) a failed serial `open()` now raises a clear `ETUMRuntimeError` ("Serial device '…' does not exist." / permission hint) instead of dumping a pyserial traceback, and a console whose open failed is safely "not open" (init `_thd=None` + `isOpened` guards in `readchar`/`read_nowait`/`close`) so later reads no longer crash with `AttributeError: '_thd'`; the action handlers show a one-liner for expected (`ETUMRuntimeError`) errors and keep the full traceback for unexpected ones. (2) `read_until`'s `expected` now accepts a **list of values** (match any) and a new `regex: true` flag treats each pattern as a Python regex (`re.search` over a bounded tail — `Console.REGEX_WINDOW`; limitation: cost/memory bounded, so a match only after a very long stream or beyond the window won't fire). Flatpak manifest now grants `--device=all` so serial adapters (`/dev/ttyUSB*`, `/dev/ttyACM*`) are visible in the sandbox. Validation: new `read_until` list/regex cases in `test/validation/items/console/test.tum`.
- Parameters are expanded at **run time**, never at load: control-flow flags (`stop_on_failure`/`execute_on_stop`) resolve via properties at run, `cycle` iterator / `git` repo / `tested_references` references and `console` `telnet_port` are no longer (incorrectly) expanded or left unexpanded at load. Justified load-time exceptions: `name`, `doc`, `skipped`, and `unittest`/`pytest` `test_method`.
- Subprocess RPC startup handshake: the `py_func`/`lua_func`/`eval_proc` worker now picks its own port (`bind 0`), announces it on stdout (`__TESTIUM_RPC_PORT__=`), and the parent connects only after reading it. Fixes intermittent Windows `failed to connect : timeout` and the matching `wait_ready()` hang; removes the reserve/close/rebind race and `SO_REUSEADDR`. See "Subprocess RPC startup handshake".
- `build_all.sh`: builds the four heavy channels in parallel (serial prep for the shared venv + wheel), results in completion order, Ctrl+C kills the whole job tree; `--ram` puts the build scratch on tmpfs (`/dev/shm`) + skips UPX for fast builds on USB/SD storage (Flatpak excluded — rofiles-fuse can't mount tmpfs). See the "Building all channels" section.
- LSP across packaging channels: `testium lsp` (and the `testium_assist` editor extension that spawns it) now works from source, wheel, PyInstaller, Flatpak and AppImage. Two enablers — (1) action items declare a class-level `ACTIONS = {key: class}` registry (like `PARAMS`), so `lsp/schema.py` builds the full schema from class attributes with no `inspect.getsource`/AST (which broke under frozen PyInstaller); (2) the `[lsp]` extra (pygls) is wired into every full-app channel. `test/validation/lsp_check.py`, run by `run.sh` before the suite, asserts per-channel that `schema` keeps its actions and `lsp` answers `initialize`. See the matching architecture sections.

View File

@@ -121,15 +121,44 @@ writeln function is similar to the write function except that a '\n' (newline) c
The ``read_until`` action is waiting for a string pattern from the console,
its parameter are listed below
* ``expected``: Character string to wait for
* ``expected``: the pattern(s) to wait for. It accepts either a **single
value** or a **list of values**; when a list is given the action succeeds
as soon as **any** of the values is seen.
* ``regex``: Boolean value (``True`` or ``False``, default ``False``). When
``True`` every ``expected`` entry is interpreted as a Python regular
expression (searched in the incoming stream, not anchored) instead of a
literal string.
* ``timeout``: Timeout setting for the action (in seconds)
* ``no_fail``: Boolean value (``True`` or ``False``) leading to no error reported
if the expected input is not read
* ``mute``: Boolean value (``True`` or ``False``) does not log any readen data
.. code-block:: yaml
:caption: matching several values, and with a regular expression
# succeeds as soon as one of the three strings is received
- read_until:
expected: [login:, "Password:", "$ "]
timeout: 10
# regex: wait for "version X.Y.Z" with any numbers
- read_until:
expected: 'version \d+\.\d+\.\d+'
regex: True
timeout: 5
The text read by the ``read_until`` action is stored in the global
variable named ``cn_<test_name>`` (See :ref:`global variables<sec_global_variables>`
for more detail on accessing global variables from test items and scripts).
When a list of values is given, the report also records, under the
``matched`` key, which pattern actually matched.
.. note::
``regex`` matching scans a bounded tail of the received stream
(``Console.REGEX_WINDOW`` characters), so a pattern that could only match
after a very large amount of output — or across more than that window —
may not be detected. Literal matching (the default) has no such limit.
In the example above, the global variable ``$(cn_test name in GUI)``
would be created at the end of the step. It would contain the resulting

View File

@@ -0,0 +1,50 @@
**pytest** test item
============================================================
The ``pytest`` test item runs a `pytest <https://docs.pytest.org>`_ test
file and turns every collected test into a child item, each with its own
``PASS`` / ``FAIL`` / ``SKIP`` result, duration and failure message. It is the
pytest counterpart of the ``unittest`` test item.
The tum file prototype is as followed:
.. code-block:: yaml
:caption: ``pytest`` test item usage example
- pytest:
name: pytest test item
test_file: test_device.py
test_method:
- test_boot
- test_version
Attributes
------------------
Beside common test items attributes, the pytest test item has specific
attributes:
* ``test_file``: the name (and eventually path) of the pytest file to run.
* ``test_method``: optional list of test function names. When present, only
the matching tests are included in the test tree (the name is matched
against the function part of each pytest *node id*, the parametrisation
suffix being ignored). Otherwise every collected test in the file is run.
Host execution
------------------
Unlike ``unittest`` (which runs in *testium*'s own interpreter), the
``pytest`` item runs pytest in a **subprocess on the host interpreter** — the
same one used by ``py_func`` / ``lua_func`` (overridable with the
``python_bin`` global variable). As a consequence:
* ``pytest`` (and the test's own dependencies) must be installed on that host
interpreter — e.g. ``pip install pytest``. It is not bundled with *testium*.
* the tests run isolated from *testium*'s in-process API; they are meant to be
self-contained (they exercise the device/software under test, not the
*testium* tree).
The item is invoked with a controlled pytest configuration
(``--capture=no``, user ``addopts`` neutralised, cache disabled); a small
built-in pytest plugin reports the collected tests and their results back to
*testium*.

View File

@@ -270,6 +270,7 @@ step list attributes.
test_items/run_test_item.rst
test_items/sleep_test_item.rst
test_items/unittest_test_item.rst
test_items/pytest_test_item.rst

Binary file not shown.

View File

@@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=Testium
Exec=testium
Icon=testium
Terminal=false
Categories=Utility;Automated test

View File

@@ -11,7 +11,11 @@ finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
# Expose all host devices to the sandbox. testium is a hardware-in-the-loop
# test tool: the console item must reach serial adapters (/dev/ttyUSB*,
# /dev/ttyACM*, …) which are otherwise invisible in the sandbox. --device=all
# also covers the GPU (supersedes --device=dri).
- --device=all
- --share=network
- --filesystem=home
- --filesystem=/tmp

View File

@@ -0,0 +1,31 @@
# Build the Testium installer from testium.iss (needs Inno Setup 6 / ISCC.exe).
# Install ISCC without admin: winget install --id JRSoftware.InnoSetup -e
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# The PyInstaller exe must exist first.
$exe = Join-Path $scriptDir '..\pyinstaller\dist\testium.exe'
if (-not (Test-Path $exe)) {
throw "PyInstaller build not found: $exe`nRun package\pyinstaller\build first."
}
# Locate ISCC.exe: PATH, then the usual install dirs.
$iscc = (Get-Command ISCC.exe -ErrorAction SilentlyContinue).Source
if (-not $iscc) {
foreach ($p in @(
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe")) {
if (Test-Path $p) { $iscc = $p; break }
}
}
if (-not $iscc) {
throw "ISCC.exe not found. Install Inno Setup 6:`n winget install --id JRSoftware.InnoSetup -e"
}
Write-Host "Using ISCC: $iscc"
& $iscc (Join-Path $scriptDir 'testium.iss')
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE" }
Write-Host "`nInstaller built in: $(Join-Path $scriptDir 'dist')"

View File

@@ -0,0 +1,127 @@
; Inno Setup script: wraps the PyInstaller testium.exe into a per-user installer.
; Build with Inno Setup 6: ISCC.exe testium.iss (or ./build.ps1).
#define MyAppName "Testium"
#define MyAppExeName "testium.exe"
#define MyAppPublisher "Testium"
#define MyAppURL "https://github.com/"
; Read version from src/VERSION so the installer never drifts from the build.
#define VerFile FileOpen("..\..\src\VERSION")
#define MyAppVersion Trim(FileRead(VerFile))
#expr FileClose(VerFile)
#if MyAppVersion == ""
#error Could not read version from ..\..\src\VERSION
#endif
[Setup]
; Version-scoped AppId: each version is a distinct app, installable side-by-side.
AppId={{B7E6F1C2-9A4D-4E3B-8F71-7C2D5A6E0B14}_{#MyAppVersion}
AppName={#MyAppName} {#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
UninstallDisplayName={#MyAppName} {#MyAppVersion}
WizardStyle=modern
; Per-version install dir so versions never overwrite each other.
DefaultDirName={autopf}\{#MyAppName}\{#MyAppVersion}
; Shared "Testium" Start Menu folder; shortcuts below are named per version.
DefaultGroupName={#MyAppName}
UninstallDisplayIcon={app}\testium.ico
DisableProgramGroupPage=yes
; Per-user install, no admin ever: installs under %LOCALAPPDATA%, no UAC prompt.
PrivilegesRequired=lowest
ArchitecturesInstallIn64BitMode=x64compatible
OutputDir=dist
OutputBaseFilename=testium-{#MyAppVersion}-setup
SetupIconFile=..\testium.ico
Compression=lzma2/max
SolidCompression=yes
; Tell Explorer to refresh the environment after a PATH change.
ChangesEnvironment=yes
[Languages]
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
; PATH off by default: the exe is windowed (console=False), so CLI shows no output.
Name: "addtopath"; Description: "Ajouter Testium au PATH (usage en ligne de commande)"; Flags: unchecked
[Files]
Source: "..\pyinstaller\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
; Ship the .ico so shortcuts/uninstall reference it directly, not the embedded one.
Source: "..\testium.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; Per-version names so each install shows separately in the Start Menu.
Name: "{group}\{#MyAppName} {#MyAppVersion}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\testium.ico"
Name: "{group}\{cm:UninstallProgram,{#MyAppName} {#MyAppVersion}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName} {#MyAppVersion}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\testium.ico"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
[Code]
const
EnvKey = 'Environment';
// True if Param is not already a full segment of the per-user PATH.
function NeedsAddPath(Param: string): Boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', OrigPath) then
begin
Result := True;
exit;
end;
Result := Pos(';' + Uppercase(Param) + ';', ';' + Uppercase(OrigPath) + ';') = 0;
end;
// On install: append {app} to the per-user PATH if the task is selected.
procedure CurStepChanged(CurStep: TSetupStep);
var
Path: string;
begin
if CurStep = ssPostInstall then
begin
if WizardIsTaskSelected('addtopath') and NeedsAddPath(ExpandConstant('{app}')) then
begin
if not RegQueryStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path) then
Path := '';
if (Path <> '') and (Copy(Path, Length(Path), 1) <> ';') then
Path := Path + ';';
Path := Path + ExpandConstant('{app}');
RegWriteStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path);
end;
end;
end;
// On uninstall: strip {app} back out of the per-user PATH.
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
Path, AppDir, Segment: string;
P: Integer;
begin
if CurUninstallStep = usUninstall then
begin
if RegQueryStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path) then
begin
AppDir := ExpandConstant('{app}');
Segment := ';' + AppDir;
P := Pos(Uppercase(Segment), Uppercase(Path));
if P > 0 then
Delete(Path, P, Length(Segment))
else
begin
P := Pos(Uppercase(AppDir) + ';', Uppercase(Path));
if P = 1 then
Delete(Path, 1, Length(AppDir) + 1);
end;
RegWriteStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path);
end;
end;
end;

View File

@@ -94,11 +94,11 @@ exe = EXE(
upx=not os.environ.get("TESTIUM_NO_UPX"),
upx_exclude=[],
runtime_tmpdir=None,
console=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
ico='../testium.png'
ico='../testium.ico'
)

BIN
package/testium.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

View File

@@ -1,7 +1,30 @@
version 0.3
==============
- New ``pytest`` test item: run your pytest files as a test step; each
test shows up with its own PASS / FAIL / SKIP. Requires pytest to be
installed (``pip install pytest``).
- Search the test tree in the GUI (Ctrl+F): find items by name, type or
doc; matches are highlighted and you can step through them.
- console ``read_until`` can now wait for several possible texts at once
(it matches the first one seen), and a ``regex: true`` option lets you
match with a regular expression.
- Serial console: a clear message when the device is missing, and serial
ports now work in the Flatpak version.
- If a test file cannot be loaded (for example pytest is not installed),
only that step fails — the rest of the tests still run.
- Fix: a variable (``$(...)``) used in a console ``telnet_port`` is now
correctly substituted.
version 0.2.3
=============
- Windows version now working reliably. Fix of a problem of jrpc ports
handshakes between the py and lua processes and testium
handshakes between the py and lua processes and testium.
Beneficial to linux version too.
- Windows: UTF-8 console output and a self-sufficient validation
wrapper (run.bat).
- Resolved python_bin / lua_bin are now published into the global dict,
so test scripts can read them via $(python_bin) / $(lua_bin).
- Windows: new per-user installer (no admin).
version 0.2.2
==============

View File

@@ -1 +1 @@
0.2.3
0.3

View File

@@ -2,6 +2,7 @@ from datetime import datetime
import sys
import os
import re
import errno
from queue import Queue, Empty
from time import sleep
import collections
@@ -10,6 +11,8 @@ import threading
from telnetlib3 import Telnet, DO, WILL, WONT, TTYPE, IAC, SB, SE, theNULL
from runtime.tum_except import ETUMRuntimeError
TIMEOUT_NULL = 0.000001
STOP_POLL_INTERVAL = 0.2
@@ -124,7 +127,29 @@ A {classname}.close() is missing somewhere in your code !'.format(classname=type
# c = ''
return c
def read_until(self, match, timeout=None, return_data=False, mute=False, should_stop=None):
# Max chars of the buffer tail scanned in regex mode (bounds cost/memory).
REGEX_WINDOW = 65536
def _feed_match(self, data, search_deques, match_deques, matches):
"""Append *data* to each window; return the first matched pattern or None."""
matched = None
for sd, md, m in zip(search_deques, match_deques, matches):
sd.append(data)
if matched is None and sd == md:
matched = m
return matched
def _search_regex(self, read_data, compiled):
"""Search the buffer tail with each regex; return the first hit's text or None."""
tail = read_data[-self.REGEX_WINDOW:]
for p in compiled:
m = p.search(tail)
if m is not None:
return m.group(0)
return None
def read_until(self, match, timeout=None, return_data=False, mute=False,
should_stop=None, regex=False):
"""
read until the string 'match is found
If timeout is not set (None), this function runs indefinitely
@@ -141,15 +166,35 @@ A {classname}.close() is missing somewhere in your code !'.format(classname=type
read_data = ''
status = -1
if not match:
raise ValueError('match parameter can not be empty')
raise ETUMRuntimeError("'expected' pattern can not be empty")
# match: a string or list of strings; succeed as soon as any is seen.
if isinstance(match, (list, tuple)):
matches = [str(m) for m in match]
else:
matches = [str(match)]
if (not matches) or any(len(m) == 0 for m in matches):
raise ETUMRuntimeError("'expected' pattern can not be empty")
if timeout is None:
timeout = 1000000
# Fixed-length queue that will contain the readout characters
search_deque = collections.deque(maxlen=len(match))
# convert match string into a deque for faster comparisons
match_deque = collections.deque(match)
compiled = None
search_deques = match_deques = None
if regex:
# 'matches' are regular expressions; succeed on the first hit.
compiled = []
for m in matches:
try:
compiled.append(re.compile(m))
except re.error as e:
raise ETUMRuntimeError(
"Invalid regular expression {!r}: {}".format(m, e)) from None
else:
# One fixed-length rolling window per literal pattern.
search_deques = [collections.deque(maxlen=len(m)) for m in matches]
match_deques = [collections.deque(m) for m in matches]
self._matched = None
# In case of a timeout equal to zero, it must be looped until the
# buffer is empty
@@ -167,9 +212,13 @@ A {classname}.close() is missing somewhere in your code !'.format(classname=type
self.string_buffer += data
read_data += data
search_deque.append(data)
if search_deque == match_deque:
if regex:
matched = self._search_regex(read_data, compiled)
else:
matched = self._feed_match(data, search_deques, match_deques, matches)
if matched is not None:
status = 0
self._matched = matched
if (not mute) and (data != '\n'):
self.string_buffer += '\n'
@@ -210,9 +259,13 @@ A {classname}.close() is missing somewhere in your code !'.format(classname=type
self.string_buffer += data
read_data += data
search_deque.append(data)
if search_deque == match_deque:
if regex:
matched = self._search_regex(read_data, compiled)
else:
matched = self._feed_match(data, search_deques, match_deques, matches)
if matched is not None:
status = 0
self._matched = matched
if (not mute) and (data != '\n'):
self.string_buffer += '\n'
@@ -407,20 +460,35 @@ class SerialConsole(Console):
self.stop = threading.Event()
self.port = None
self.port_id = port
self._thd = None
def open(self):
try:
self.port = serial.Serial(port=self.port_id,
baudrate=self.baudrate,
stopbits=self.stopbits,
parity=self.parity,
xonxoff=self.xonxoff,
timeout=None)
except (serial.SerialException, OSError) as e:
raise ETUMRuntimeError(self._open_error_message(e)) from None
self.isOpened = True
if self.bufferize:
self.port.timeout = 2
self._thd = threading.Thread(target=self.read_thread)
self._thd.start()
def _open_error_message(self, exc):
"""Build a short, direct message for a failed serial open."""
errno_ = getattr(exc, "errno", None)
if errno_ == errno.ENOENT:
return "Serial device '{}' does not exist.".format(self.port_id)
if errno_ == errno.EACCES:
return ("Permission denied opening serial device '{}' "
"(is your user allowed to access it, e.g. 'dialout' group?)."
.format(self.port_id))
return "Could not open serial device '{}': {}".format(self.port_id, exc)
def read_thread(self):
while not self.stop.is_set():
c = self.port.read(1)
@@ -428,7 +496,7 @@ class SerialConsole(Console):
self.rx_queue.put(c)
def close(self):
if self.bufferize:
if self.bufferize and self._thd is not None:
self.stop.set()
self._thd.join()
if self.port is not None:
@@ -440,10 +508,12 @@ class SerialConsole(Console):
self.port.timeout = timeout
def readchar(self, timeout):
if not self.isOpened:
raise ETUMRuntimeError("Serial console '{}' is not open".format(self.name))
if self.bufferize:
if not self._thd.is_alive() and not self.stop.isSet():
raise RuntimeError(
"Impossible to read the serial console, it may be already openned")
raise ETUMRuntimeError(
"Impossible to read the serial console, it may be already opened")
if timeout < TIMEOUT_NULL:
return self.rx_queue.get(block=False)
else:
@@ -455,10 +525,12 @@ class SerialConsole(Console):
self.port.flush()
def read_nowait(self, mute=False):
if not self.isOpened:
raise ETUMRuntimeError("Serial console '{}' is not open".format(self.name))
if self.bufferize:
if not self._thd.is_alive() and not self.stop.isSet():
raise RuntimeError(
"Impossible to read the serial console, it may be already openned")
raise ETUMRuntimeError(
"Impossible to read the serial console, it may be already opened")
st = self.rx_queue.getAll().decode(self.encoding, errors='replace')
if not mute:
date_str = str(datetime.now()).split('.')[0].split(' ')[1]

View File

@@ -61,6 +61,13 @@ def test_run(f):
self.run_test_init()
# The item could not be loaded (e.g. a missing module): FAIL at run.
# run_test_end -> write_footer prints the message.
if self._load_error is not None:
self.result.set(TestValue.FAILURE, self._load_error)
self.run_test_end()
return self.result
while self._is_paused:
sleep(0.2)
if self.isStopped() :
@@ -145,16 +152,17 @@ class TestItem:
self._report_key = None
self._reported = None
self.status_queue = status_queue
self._execute_on_stop = False
self._execute_on_stop_raw = False
self._post_eval = None
self._store_result = None
self._expected_result = None
self._no_fail = None
self._is_stopped = False
self._load_error = None
self._is_running = False
self._is_breakpoint = False
self._is_paused = False
self._stop_on_failure = False
self._stop_on_failure_raw = False
self._doc = ""
self._name = ""
self.report = None
@@ -197,13 +205,14 @@ class TestItem:
self.skipped = False
self._report_key = self._prms.getParam("key", default=None)
self._stop_on_failure = self._prms.getParam(
"stop_on_failure", default=False, processed=True
# Kept raw: expanded at run time by the matching properties.
self._stop_on_failure_raw = self._prms.getParam(
"stop_on_failure", default=False
)
self._doc = self._prms.getParam("doc", default="", processed=True)
#
self._execute_on_stop = self._prms.getParam(
"execute_on_stop", default=False, processed=True
self._execute_on_stop_raw = self._prms.getParam(
"execute_on_stop", default=False
)
if "process_result" in dict_item:
@@ -570,6 +579,20 @@ class TestItem:
def setEnabled(self):
self.enabled = True
def _eval_flag(self, raw):
"""Run-time flag: bool as-is, otherwise expanded and coerced to bool."""
if isinstance(raw, bool):
return raw
return eval_to_boolean(self._prms.expanse(raw))
@property
def _stop_on_failure(self):
return self._eval_flag(self._stop_on_failure_raw)
@property
def _execute_on_stop(self):
return self._eval_flag(self._execute_on_stop_raw)
def executedOnStop(self):
return self._execute_on_stop

View File

@@ -4,7 +4,7 @@ import importlib
import traceback
import api.testium as tm
from runtime.tum_except import ETUMSyntaxError
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError
from runtime.stdout_redirect import stdio_redir
from interpreter.test_items.test_item import test_run
from interpreter.test_items.item_actions import TestItemActions
@@ -88,7 +88,7 @@ class TestItemConsoleOpen(TestItemConsoleAction):
telnet_host = self._prms.getParam(
"telnet_host", required=True, processed=True
)
telnet_port = self._prms.getParam("telnet_port", default=69)
telnet_port = self._prms.getParam("telnet_port", default=69, processed=True)
elif self._protocol == "ssh":
if tm.OS() == "Windows":
@@ -225,12 +225,16 @@ class TestItemConsoleOpen(TestItemConsoleAction):
tm.add_console(cons)
cons.open()
self.result.set(TestValue.SUCCESS)
except ETUMRuntimeError as e:
# Expected console error (device missing, no permission…): one line.
msg = "Impossible to open the console '{}': {}".format(cname, e._message)
self.result.set(result=TestValue.FAILURE, message=msg)
print(msg)
except Exception as e:
# Unexpected error: keep the full traceback for diagnosis.
self.result.set(
result=TestValue.FAILURE,
message="Impossible to open the console ({}) (exception: {})".format(
cname, e
),
message="Impossible to open the console '{}': {}".format(cname, e),
)
traceback.print_exception(*sys.exc_info())
@@ -319,12 +323,17 @@ class TestItemConsoleReadUntil(TestItemConsoleAction):
PARAMS = ParamSet(
Param("expected", required=True,
doc="Regex matched against incoming console output until found "
"or until timeout."),
doc="Literal string — or a list of strings — matched against the "
"incoming console output. The read succeeds as soon as one of "
"them is seen, or fails on timeout."),
Param("timeout", default=-1,
doc="Seconds before giving up. Negative means infinite."),
Param("mute", default=False,
doc="If true, don't echo received bytes to testium's stdout/log."),
Param("regex", default=False,
doc="If true, each 'expected' entry is treated as a Python "
"regular expression (searched, not anchored) instead of a "
"literal string."),
)
def __init__(
@@ -343,16 +352,21 @@ class TestItemConsoleReadUntil(TestItemConsoleAction):
@test_run
def execute(self):
cons = self.get_console()
# 'expected' may be a single value or a list of values (match any).
if isinstance(self._read_until, (list, tuple)):
ru = [self._prms.expanse(m) for m in self._read_until]
else:
ru = self._prms.expanse(self._read_until)
read_timeout = int(self._prms.getParam("timeout", default=-1, processed=True))
mute = self._prms.getParam("mute", default=False, processed=True)
use_regex = self._prms.getParam("regex", default=False, processed=True)
if read_timeout < 0:
read_timeout = None
try:
status, data = cons.read_until(
ru, timeout=read_timeout, return_data=True, mute=mute,
should_stop=self.isStopped,
should_stop=self.isStopped, regex=bool(use_regex),
)
if status == 0:
self.result.set(TestValue.SUCCESS)
@@ -364,14 +378,21 @@ class TestItemConsoleReadUntil(TestItemConsoleAction):
)
else:
self.result.set(result=TestValue.FAILURE, message="No matching text")
if mute:
self.result.reported = {"data": ""}
else:
self.result.reported = {"data": data}
reported = {"data": "" if mute else data}
# When several patterns were given, expose which one matched.
if status == 0 and isinstance(ru, (list, tuple)):
reported["matched"] = getattr(cons, "_matched", None)
self.result.reported = reported
# The result is put in global dir
tm.setgd("cn_" + self.parent()._name, data)
except:
except ETUMRuntimeError as e:
# Expected console error (e.g. console not open): clear one-liner.
msg = f"Console '{self.token['console_name']}': impossible to read ({e._message})"
self.result.set(result=TestValue.FAILURE, message=msg)
print(msg)
except Exception:
# Unexpected error: keep the full traceback for diagnosis.
print(traceback.format_exc())
self.result.set(
result=TestValue.FAILURE,

View File

@@ -51,11 +51,8 @@ class TestItemCycle(TestItem):
self._niter = None
if "iterator" in dict_cycle:
# Kept raw: expanded at run time in execute().
self._iter = dict_cycle["iterator"]
if isinstance(self._iter, str):
self._iter = self._prms.expanse(self._iter)
else:
self._iter = None

View File

@@ -21,7 +21,8 @@ class TestItemGit(TestItem):
super().__init__(dict_item, parent, status_queue, filename=filename)
self._type = cst.TYPE_GIT
self.is_container = False
self.repo = self._prms.getParamAll('repo', processed=True, required=True)
# Kept raw: each repo entry is expanded at run time in execute().
self.repo = self._prms.getParamAll('repo', required=True)
@test_run
def execute(self):

View File

@@ -0,0 +1,397 @@
"""``pytest`` test item.
Runs a user pytest file and surfaces every collected test as a child item
(one PASS / FAIL / SKIP per test, with duration and failure message in the
report) — the pytest analogue of the ``unittest`` item.
Unlike ``unittest`` (which runs in-process), pytest runs in a **subprocess on
the host interpreter** (``bins.python_bin()``), exactly like ``py_func`` /
``lua_func``. This keeps the user's pytest install and test dependencies on
the host (visible across every packaging channel — source, wheel, PyInstaller,
Flatpak, AppImage) instead of requiring them inside the bundled interpreter.
A tiny stdlib-only pytest plugin is shipped as a self-contained launcher
script (run directly and registered via ``pytest.main(plugins=[...])`` — no
PYTHONPATH / ``-p`` / import-by-name). It streams the collected node ids and
per-test results back over the subprocess stdout as sentinel-prefixed lines,
which the parent parses live.
"""
import os
import json
import shutil
import atexit
import tempfile
import threading
import queue
import subprocess
import api.testium as tm
from runtime.tum_except import ETUMFileError, ETUMRuntimeError
from interpreter.test_items.test_item import TestItem, test_run, test_data
from interpreter.test_items.test_result import TestResult, TestValue
from interpreter.utils.constants import TestItemType as cst
from interpreter.utils.param_decl import Param, ParamSet, LIST
from interpreter.utils.paths import no_window_kwargs
from interpreter.utils import bins
# Sentinels streamed by the in-subprocess plugin (see _PLUGIN_SOURCE). Kept in
# sync with the plugin source below.
_SENT_COLLECTED = "__TESTIUM_PYTEST_COLLECTED__"
_SENT_START = "__TESTIUM_PYTEST_START__"
_SENT_RESULT = "__TESTIUM_PYTEST_RESULT__"
# stdlib-only pytest plugin executed inside the host subprocess. It must not
# import anything from testium. It emits one sentinel line per event so the
# parent can rebuild the test tree (collection) and per-test results (run)
# without parsing pytest's human output or a JUnit XML.
_PLUGIN_SOURCE = '''\
import sys
import json
_SENT_COLLECTED = "__TESTIUM_PYTEST_COLLECTED__"
_SENT_START = "__TESTIUM_PYTEST_START__"
_SENT_RESULT = "__TESTIUM_PYTEST_RESULT__"
_reports = {}
def _emit(payload):
# Leading newline guarantees the sentinel starts its own line even if a
# test printed without a trailing newline (pytest runs with --capture=no).
sys.stdout.write("\\n" + payload + "\\n")
sys.stdout.flush()
def pytest_collection_modifyitems(session, config, items):
_emit(_SENT_COLLECTED + json.dumps([it.nodeid for it in items]))
def pytest_runtest_logstart(nodeid, location):
_emit(_SENT_START + nodeid)
def pytest_runtest_logreport(report):
_reports.setdefault(report.nodeid, {})[report.when] = report
def _skip_reason(report):
lr = report.longrepr
if isinstance(lr, tuple) and len(lr) == 3:
return str(lr[2])
return report.longreprtext or ""
def pytest_runtest_logfinish(nodeid, location):
phases = _reports.pop(nodeid, {})
setup = phases.get("setup")
call = phases.get("call")
teardown = phases.get("teardown")
duration = 0.0
for rep in (setup, call, teardown):
if rep is not None:
duration += getattr(rep, "duration", 0.0) or 0.0
outcome = "pass"
message = ""
if setup is not None and setup.failed:
outcome, message = "fail", setup.longreprtext
elif setup is not None and setup.skipped:
outcome, message = "skip", _skip_reason(setup)
elif call is not None:
if call.failed:
outcome, message = "fail", call.longreprtext
elif call.skipped:
outcome, message = "skip", _skip_reason(call)
else:
outcome = "pass"
if teardown is not None and teardown.failed and outcome == "pass":
outcome, message = "fail", teardown.longreprtext
_emit(_SENT_RESULT + json.dumps({
"nodeid": nodeid,
"outcome": outcome,
"message": message,
"duration": duration,
}))
'''
# Self-contained pytest runner: the plugin source above + a main that hands the
# module to pytest as a *plugin object* (pytest.main(plugins=[...])). Run
# directly (``python launcher.py …``), so the plugin needs no PYTHONPATH, no
# ``-p`` and no import-by-name — robust on every channel, AppImage included.
_LAUNCHER_SOURCE = _PLUGIN_SOURCE + '''
if __name__ == "__main__":
import pytest
sys.exit(pytest.main(plugins=[sys.modules[__name__]]))
'''
class TestItemPytestElement(TestItem):
"""One collected pytest test (leaf child of a pytest file item)."""
def __init__(self, name, parent=None, status_queue=None, filename=""):
super().__init__(None, parent, status_queue, filename=filename)
self.is_container = False
self._name = name
self._type = cst.TYPE_PYTEST_STEP
self.banner = ""
self.footer = ""
self._nodeid = ""
self._reported_done = False
class TestItemPytestFile(TestItem):
PARAMS = ParamSet(
Param("test_file", required=True,
doc="Path to the pytest test file."),
Param("test_method", kind=LIST,
doc="Optional list of test function names to restrict the run "
"to (matched against the function part of each node id, "
"parametrisation suffix stripped). When empty, every "
"collected test in the file is run."),
)
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
self._name = cst.TYPE_PYTEST.item_name
super().__init__(dict_item, parent, status_queue, filename=filename)
self.is_container = True
self._type = cst.TYPE_PYTEST
self._fileName = self._prms.getParam('test_file', required=True, processed=True)
self._testDir = ''
self._test_methods = self._prms.getParamAll('test_method', processed=True)
self._cwd = ""
self._launcher = ""
def setTestDir(self, dir):
self._testDir = dir
# ---- subprocess plumbing -------------------------------------------------
def _write_launcher(self):
# In Flatpak the host process can only read /tmp (shared), so stage the
# launcher there; outside Flatpak the default temp dir is fine.
d = tempfile.mkdtemp(prefix="testium_pytest_",
dir="/tmp" if bins._in_flatpak() else None)
path = os.path.join(d, "launcher.py")
with open(path, "w") as f:
f.write(_LAUNCHER_SOURCE)
atexit.register(shutil.rmtree, d, ignore_errors=True)
return path
def _pytest_popen(self, args):
pbin = bins.python_bin()
if not pbin:
raise ETUMRuntimeError("No valid Python 3 interpreter found")
env = os.environ.copy()
bins.apply_host_libs(env)
env.pop("PYTHONUSERBASE", None)
# Run the self-contained launcher directly: it registers our plugin via
# pytest.main(plugins=[...]), so no PYTHONPATH / -p / import-by-name.
cmd_args = [
self._launcher,
"--capture=no", # let plugin sentinels + test prints reach our pipe
"-o", "addopts=", # neutralise user addopts (xdist/cov break parsing)
"-p", "no:cacheprovider",
*args,
]
if bins._in_flatpak():
host_env = {"PATH": env["PATH"]} if env.get("PATH") else {}
params = bins.flatpak_host_spawn(
pbin, cmd_args, host_cwd=self._cwd, extra_env=host_env)
popen_kwargs = {}
else:
params = [pbin, *cmd_args]
popen_kwargs = {"env": env, "cwd": self._cwd}
return subprocess.Popen(
params,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
encoding="utf-8",
errors="replace",
restore_signals=False,
**no_window_kwargs(),
**popen_kwargs,
)
# ---- loading (collection) ------------------------------------------------
def _collect(self):
proc = self._pytest_popen(["--collect-only", "-q", self._fileName])
nodeids = []
output = []
for line in proc.stdout:
line = line.rstrip("\n")
if line.startswith(_SENT_COLLECTED):
try:
nodeids = json.loads(line[len(_SENT_COLLECTED):])
except ValueError:
pass
elif line != "":
output.append(line)
proc.wait()
return nodeids, "\n".join(output)
def _collection_error(self, output):
"""Clear reason why collection produced no test."""
if "No module named pytest" in output or "No module named 'pytest'" in output:
return ("pytest is not installed on the host interpreter used by "
"testium (python_bin). Install it, e.g. 'pip install pytest'.")
return 'No pytest test collected from "%s".\n%s' % (self._fileName, output)
def load(self):
ret = {}
if self._fileName == '':
raise ETUMFileError('A file name is expected but got "None"')
if not os.path.isabs(self._fileName):
self._fileName = os.path.normpath(os.path.join(self._testDir, self._fileName))
if not os.path.isfile(self._fileName):
raise ETUMFileError('File "%s" is not found' % (self._fileName))
self._cwd = os.path.dirname(self._fileName) or "."
self._launcher = self._write_launcher()
nodeids, output = self._collect()
if not nodeids:
raise ETUMFileError(self._collection_error(output))
if self._test_methods:
present = {nid.split("::")[-1].split("[")[0] for nid in nodeids}
for m in self._test_methods:
if m not in present:
raise ETUMFileError(
'Test function "%s" is not found in "%s"' % (m, self._fileName))
wanted = set(self._test_methods)
nodeids = [nid for nid in nodeids
if nid.split("::")[-1].split("[")[0] in wanted]
for nid in nodeids:
disp = nid.split("::", 1)[1] if "::" in nid else nid
item = TestItemPytestElement(disp, self)
item._nodeid = nid
ret.update(test_data(item, {}))
return ret
# ---- execution (run) -----------------------------------------------------
def _finish_child(self, child, value, message=""):
if child._reported_done:
return
if getattr(child, "t0", None) is None:
child.t0 = tm.timestamp()
self.status_queue.put(
{'id': child.id(), 'status': 'started', 'timestamp': child.t0})
child.duration = tm.timestamp() - child.t0
res = TestResult(child, value, message)
res.test_id = child.id()
res.sendStatus(self.status_queue)
self.status_queue.put(
{'id': child.id(), 'status': 'finished', 'duration': child.duration})
self.report.addTest(child, res)
child._reported_done = True
def _stream_results(self, proc, by_nodeid):
overall = TestValue.SUCCESS
outq = queue.Queue()
def reader():
for line in proc.stdout:
outq.put(line)
outq.put(None)
t = threading.Thread(target=reader, daemon=True)
t.start()
while True:
try:
line = outq.get(timeout=0.1)
except queue.Empty:
if self.isStopped():
try:
proc.terminate()
except Exception:
pass
break
continue
if line is None:
break
line = line.rstrip("\n")
if line.startswith(_SENT_COLLECTED):
# pytest re-collects at the start of the run; the node list was
# already consumed at load time, so drop it here.
continue
elif line.startswith(_SENT_START):
child = by_nodeid.get(line[len(_SENT_START):])
if child is not None and getattr(child, "t0", None) is None:
child.t0 = tm.timestamp()
self.status_queue.put(
{'id': child.id(), 'status': 'started', 'timestamp': child.t0})
elif line.startswith(_SENT_RESULT):
try:
rec = json.loads(line[len(_SENT_RESULT):])
except ValueError:
continue
child = by_nodeid.get(rec.get("nodeid"))
if child is None:
continue
value = {
"pass": TestValue.SUCCESS,
"fail": TestValue.FAILURE,
"skip": TestValue.NORUN,
}.get(rec.get("outcome"), TestValue.FAILURE)
self._finish_child(child, value, rec.get("message", ""))
if value == TestValue.FAILURE:
overall = TestValue.FAILURE
elif line != "":
print(line)
proc.wait()
return overall
@test_run
def execute(self):
by_nodeid = {}
enabled_nodeids = []
for i in range(self.childCount()):
c = self.child(i)
c.t0 = None
c._reported_done = False
by_nodeid[c._nodeid] = c
if c.enabled:
enabled_nodeids.append(c._nodeid)
else:
self._finish_child(c, TestValue.NORUN, "test disabled")
overall = TestValue.SUCCESS
if enabled_nodeids and not self.isStopped():
args = list(enabled_nodeids)
if self._stop_on_failure:
args.append("-x")
proc = self._pytest_popen(args)
overall = self._stream_results(proc, by_nodeid)
# Any enabled test that produced no result (crash, -x stop, user stop)
# is reported as NORUN so the tree stays consistent.
for i in range(self.childCount()):
c = self.child(i)
if c.enabled and not c._reported_done:
self._finish_child(c, TestValue.NORUN, "not executed")
if self.isStopped():
self.result.set(TestValue.NORUN, 'pytest execution aborted on user request')
else:
self.result.set(overall, 'pytest ' + str(overall))

View File

@@ -26,13 +26,14 @@ class TestItemTestedRefsDialog(TestItemDialogBase):
self.is_container = False
with item_load_context(self.cmd(), self.name(), self.seqFilename()):
self._question = self._prms.getParam('question', required=True)
self._init_values = self._prms.getParamAll('reference', required=False, processed=True)
# Kept raw: expanded at run time in execute().
self._init_values = self._prms.getParamAll('reference', required=False)
self._auto_result = self._prms.getParam('auto_result', required=False, default=None)
@test_run
def execute(self):
q = self._prms.expanse(self._question)
init_values = ','.join(self._init_values)
init_values = ','.join(self._prms.expanse(v) for v in self._init_values)
if _is_text_mode():
print(f"References: {q}")
rows = init_values.split(',') if init_values else ['']

View File

@@ -451,6 +451,20 @@ class TestSet:
def rootItem(self):
return self._rootItem
def _load_item(self, item):
"""Run an item's self-load, deferring a failure (e.g. a missing module)
to a run-time FAILURE instead of aborting the whole test load."""
try:
return item.load()
except Exception as e:
msg = getattr(e, "_message", None) or str(e)
item._load_error = msg
tm.print_warn(
f"'{item.cmd()}' item '{item.name()}' could not be loaded: "
f"{msg} (it will FAIL at run)."
)
return {}
def load_test_recursively(self, tree_parent, parent_seq, file_name):
ret = {}
try:
@@ -532,9 +546,9 @@ class TestSet:
item.is_folded = is_folded
child = {}
# case where the test item loads itself its descendants
if it == cst_type.TYPE_UNITTEST:
if it in (cst_type.TYPE_UNITTEST, cst_type.TYPE_PYTEST):
item.setTestDir(test_dir)
child = item.load()
child = self._load_item(item)
elif issubclass(it.item_class, TestItemActions):
child = item.load()
# case where the test item is an items container

View File

@@ -25,7 +25,7 @@ import subprocess
import tempfile
import api.testium as tm
from interpreter.utils.paths import sys_app_path_lin, sys_app_path_win
from interpreter.utils.paths import sys_app_path_lin, sys_app_path_win, no_window_kwargs
from runtime.tum_except import ETUMRuntimeError
@@ -272,6 +272,7 @@ def _run_probe(cmd):
r = subprocess.run(
cmd, capture_output=True, text=True,
encoding=tm.sys_encoding(), timeout=10, env=_probe_env(),
**no_window_kwargs(),
)
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
return None

View File

@@ -10,6 +10,8 @@ class TestItemEnum():
class TestItemType(Enum):
TYPE_UNITTEST = TestItemEnum("unittest", "unittest")
TYPE_UNITTEST_STEP = TestItemEnum("unittest_step", "unittest step")
TYPE_PYTEST = TestItemEnum("pytest", "pytest")
TYPE_PYTEST_STEP = TestItemEnum("pytest_step", "pytest step")
TYPE_CONSOLE = TestItemEnum("console", "Console")
TYPE_CONSOLE_ACTION = TestItemEnum("console_action", "Console action")
TYPE_CYCLE = TestItemEnum("loop", "Cycle")

View File

@@ -4,7 +4,7 @@ import subprocess
import api.testium as tm
from runtime.jrpc import JsonRpcClient
from interpreter.utils.paths import subproc_path
from interpreter.utils.paths import subproc_path, no_window_kwargs
from runtime.tum_except import ETUMRuntimeError
from interpreter.utils import bins
from interpreter.utils.proc_drain import drain_and_read_port, wait_for_port
@@ -114,6 +114,7 @@ class LuaProcessBase:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
restore_signals=False,
**no_window_kwargs(),
**popen_kwargs,
)
# Forward subprocess output to the log and read the startup port sentinel.

View File

@@ -8,6 +8,14 @@ import subprocess
import api.testium as tm
def no_window_kwargs():
# Hide stray child consoles in the frozen Windows GUI exe (console=False has
# no console to inherit). The wheel/source keeps its console, so leave it.
if sys.platform == "win32" and getattr(sys, "frozen", False):
return {"creationflags": subprocess.CREATE_NO_WINDOW}
return {}
def testium_path():
if getattr(sys, 'frozen', False):
@@ -54,6 +62,7 @@ def sys_app_path_win(app_name):
text=True,
encoding="oem",
timeout=10,
**no_window_kwargs(),
)
data = result.stdout
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):

View File

@@ -4,7 +4,7 @@ import subprocess
from runtime.jrpc import JsonRpcClient
import api.testium as tm
from runtime.tum_except import ETUMRuntimeError
from interpreter.utils.paths import testium_path, subproc_path
from interpreter.utils.paths import testium_path, subproc_path, no_window_kwargs
from interpreter.utils import bins
from interpreter.utils.proc_drain import drain_and_read_port, wait_for_port
@@ -97,6 +97,7 @@ class PyProcessBase:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
restore_signals=False,
**no_window_kwargs(),
**popen_kwargs,
)
# Forward subprocess output to the log and read the startup port sentinel.

View File

@@ -25,6 +25,7 @@ from interpreter.utils.version import (
from interpreter.test_items.test_item import TestItem
from interpreter.test_items.test_item_sleep import TestItemSleep
from interpreter.test_items.test_item_unittest import TestItemUnittestFile
from interpreter.test_items.test_item_pytest import TestItemPytestFile
from interpreter.test_items.test_item_cycle import TestItemCycle
from interpreter.test_items.test_item_runtime_plot import TestItemPlot
from interpreter.test_items.test_item_group import TestItemGroup
@@ -69,6 +70,7 @@ def _constants_init():
cst.TYPE_RUN.item_class = TestItemRun
cst.TYPE_SLEEP.item_class = TestItemSleep
cst.TYPE_UNITTEST.item_class = TestItemUnittestFile
cst.TYPE_PYTEST.item_class = TestItemPytestFile
cst.TYPE_VALUE_DLG.item_class = TestItemValueDialog
cst.TYPE_PARALLEL.item_class = TestItemParallel
cst.TYPE_PARALLEL_BRANCH.item_class = TestItemParallelBranch

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

View File

@@ -42,6 +42,7 @@
<file alias="exit.png">black/exit.png</file>
<file alias="terminal.png">black/terminal.png</file>
<file alias="python.png">black/python.png</file>
<file alias="pytest.png">black/pytest.png</file>
<file alias="lua.png">black/lua.png</file>
<file alias="verif.png">black/verif.png</file>
<file alias="view-refresh.png">black/view-refresh.png</file>
@@ -91,6 +92,7 @@
<file alias="exit.png">white/exit.png</file>
<file alias="terminal.png">white/terminal.png</file>
<file alias="python.png">white/python.png</file>
<file alias="pytest.png">white/pytest.png</file>
<file alias="lua.png">white/lua.png</file>
<file alias="verif.png">white/verif.png</file>
<file alias="view-refresh.png">white/view-refresh.png</file>
@@ -140,6 +142,7 @@
<file alias="exit.png">color/exit.png</file>
<file alias="terminal.png">color/terminal.png</file>
<file alias="python.png">color/python.png</file>
<file alias="pytest.png">color/pytest.png</file>
<file alias="lua.png">color/lua.png</file>
<file alias="verif.png">color/verif.png</file>
<file alias="view-refresh.png">color/view-refresh.png</file>

View File

@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.11.0
# Created by: The Resource Compiler for Qt version 6.11.1
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
@@ -1987,6 +1987,33 @@ h\xaa\x18\xc7\xe6\xc4!\x90#au{\x90\x9b\xfc\xed\
\xc1\x83\xbb\xe4\xff\xbbn\xdf\xb3\x9e\xf5\xac\xab\xf6\x17\xad\
\x1c\x1e\xee0\x17\x1bh\x00\x00\x00\x00IEND\xae\
B`\x82\
\x00\x00\x01\x8a\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00@\x00\x00\x00@\x08\x04\x00\x00\x00\x00`\xb9U\
\x00\x00\x01QIDATx\x9c\xed\x98\xb1J\x03A\
\x10\x86\xbf[\x17\xb5P\x14\xecm|\x00M\xa1\x98J\
\xf0\x09\x04\xb1\xf4u\xec|\x13!\x8dX\x8aH:\x95\
\x18,\xd4\xceF\x826\x8a\xca%\x04qE\x8bul\
\xb2\xca\x06\x87x\xf3os\xc3\xce\xfc\xfb\xdf\xfe\xc3\x1e\
\xb7E@\x17Ny}L\x803\x0b\xb0&T\x863\
\x01(\xc3'3\xda\x8c\xc7\xe7\x0d\xaei2\x17\xe3m\
Ni1\x19\xe3Mn8\x13\xb5\xebt\xf2\x05\x1c\x89\
\x9c\x17\xa0\xc9t\x8c\x1f?\xe7\xbf\x04>\xf3\xca\xa1\xa8\
\xed%\xd9)\xb4\xbf\x86~\xe0\xec\x12\xbb\xccd\xb0\xf7\
\xd8\xe1 \x91\x13\x06\x8dF\xc8\xc5\xd5@~B\xe2\x1c\
8\xcex\xfb\x1f2\x14\x89\x1eX`6c\xf9.\x97\
\x84\xdc&\xac\x8b]jQ\xb2,\xba\xfe\x82'V\x19\
\x8bq\x9b\x92\xba\xa8=\xa1\x9f\x14\x19R\xa3/\x1c]\
\x0c\x84{\x11\xaf\x05BW\xc4+a\xea[\x07\xcc'\
\xd9Q?\x09\x9d\x09\xc0,P\x863\x01(\xc3\x0f\x99\
\xaf\xa4&\xa2\xce\xdf\x0bx\xe3|\xc4,p&\x00e\
\xf8l\x86}&\xe2\xf3\x83\x86\x80\xad\x11\xb7\xc0\x99\x00\
\xcc\x82\xaa[\xe0\x7f]Q\x13\x7fBw\x1a\x02n\xf9\
_\x16\xb8\xca\x0b\xf0\xc9\x1d\xd8\x139\x1f\x97RCF\
\xa1}I\xe5\x94\xd7\xc7\x048\xb3\x00kBe8\x13\
@\xd5-x\x07\x18V\xe7\xe7\xb7ki\xfd\x00\x00\x00\
\x00IEND\xaeB`\x82\
\x00\x00\x05\xbd\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -7132,6 +7159,35 @@ h\xaa\x18\xc7\xe6\xc4!\x90#au{\x90\x9b\xfc\xed\
\xc1\x83\xbb\xe4\xff\xbbn\xdf\xb3\x9e\xf5\xac\xab\xf6\x17\xad\
\x1c\x1e\xee0\x17\x1bh\x00\x00\x00\x00IEND\xae\
B`\x82\
\x00\x00\x01\xa7\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\
\x00\x00\x01nIDATx\x9c\xed\xda1k\x14Q\
\x10\x00\xe0\xcfqQ\x0b\x83\x82\xbd\x8d? \xa60\xc4\
J\xf0\x17\x08b\xe9\xdf\xb1\xf3\x9f\x08iB\xca \xc1\
N\x83\x86\x14jg#\xa2\x8d\xa2\x92\x84 *\x07'\
\xa4\xd8g\xe1\x9e\xbe\x93\x99\x0f\x8e\x83\x1dv\xe6\xed\xdc\
{\xf7\x96\xbb\xa5\x94RJ)\xa5\x94\x92\xd1\x99\x05\xe4\
\xd8\xc7\xb9\x91\xe3w\xf0\x1aOpe$~\x1f\xcf\xf0\
\x1c\x17F\xe2w\xf1\x06{\x8d\xba\xb7\xf1n\xe2\xd8\x0d\
S\x13\xe0q#\xcf\xd7\xf9\xfb\xac\x01+#\xf1O\xa7\
\xce\x1fk\xe0\x17|\xc3N\xa3\xee\xf1\x1f\x8e\xb7,j\
\x09\x5c\xc7C\x5c\xd2\xc7l\x06<\xc0v\xa7\xfa6\xf1\
\xa3\xf3\xeb\xd5\xd4\x8b\x88\x09\xe7\xee\xeao\xb7\xf7.p\
\x0d\x97\xf5q\x84\x97\xf3\x99\xd0u\x1b\xbc\xd9\x98I\xb3\
\xed\xed\x107\x1a\xdf\xf2\x07\xf8\x8c\x0d\x9cml\xaf\x87\
\xf3\xfcc\x9e\xe2\xc4\x128i\xac\xcf\xd5y\xfcC#\
~\xeb\xd4'9\x16_\xc7\xc5\xdf\xac\xff\xab\x8b\x18|\
H.$\x17\x92\x0b\xc9\x85\xe4Br!\xb9\x90\x5cH\
n\xb0\xdcf\xb7\xc2k\x8d\xd8\xe4_\x83\xfe\x87\x06|\
\xc7\x8b\xbfY $\x17\x92\x0b\xc9\x85\xe4\x86\xde\x03\xc0\
\x16\xce\x8f\x1c\xff\x98\xa5\x01\xf7z\x16\x0f\xc9\x85\xe4B\
r!\xb9\x90\x5cH.$7\xfc\x83\x1ak\x8d\x7f~\
\xdeK\xd2\x80\xb7\x96XH.$\x17\x92\x1b\x16\x90\xe3\
Q#\xcf\xaf\x87\xa0J)\xa5\x94RJ)\xa5X2\
?\x01\x1cBc5g\xeb\xf6\x81\x00\x00\x00\x00IE\
ND\xaeB`\x82\
\x00\x00\x05]\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -12682,6 +12738,45 @@ h\xaa\x18\xc7\xe6\xc4!\x90#au{\x90\x9b\xfc\xed\
\xc1\x83\xbb\xe4\xff\xbbn\xdf\xb3\x9e\xf5\xac\xab\xf6\x17\xad\
\x1c\x1e\xee0\x17\x1bh\x00\x00\x00\x00IEND\xae\
B`\x82\
\x00\x00\x02L\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\
\x00\x00\x02\x13IDATx\x9c\xed\xdaMK\x15a\
\x14\xc0\xf1\xffsf|)\x88\x8c\xb8\x06\xb5(\x88\x08\
\x92@\x17\xa1\xb6\x0a?A\x10-[\x05\x06~\x88\x12\
\x82v\xb5\xbf\xabh\xdf\xae\x97e\xa9\x18-*LL\
\xa5\x9dX\x11itMM\xaf^\xefs\x82\xbb\xbc3\
+\x07\x99\x0b\xe7\xfc6\x03s\x98s\x0e\x87\x99y\x86\
\x99\x01\xe7\x9cs\xce9\xe7\x9cE\xa1p\x86\xa7\xdf\xe6\
\x81\xee\xcc\xfe(7\xb9{\xee\xeb\xfb\x85d\x16\xf4t\
{X\x88w\x86\xaf\xf2q\xe3~\xff\x1c\x84\xde\xf6x\
\x22\xe1\xd6\xd6\x0bY\xd9\xfb\xd7\xf8\x94\xdbx\xd0\xb1\x0b\
\xcb\xbf\x7f\x16m?-\x9a\x00e\x8a\x10\xb2y\xd2\xb8\
\xdd\xda\x06\x9dE9\xd1\x1en\x0a\x1b\xadp\xd0)\xcd\
\x19\xe0~\xa3\xb1U\xab\xff98\xa6\x957ye\xa5\
\xabQ/\xdc\xbb\xe3\xf0\x97\xc0\xc4\xc4\xc4\xa0\xaa>\x06\
N\x964\xc8z\x8c\xf1Q\xb5Z}]\xca%\xa0\xaa\
\x0f\x801J$\x22O\x80B\x03\x90\xc3\x1e\x18B\x98\
\xa1d\xaa:S\xea*0>>~1I\x92>J\
\x10c\xdc\xadV\xab\xcb\xad\xdbp\xa9\xcb\xe0\xb3\xef\xa3\
h\xc8\x9eI{a\x8e{gw\xde}\xe1\x9a\xe4\xdc\
\xe5C\x0f\x0b#\x97\xd8\xfc;yf$45i\x8f\
\xd7\x13\x9d\xafL\xae\xef\xac^\xee\x1f\xcd+\xbb\x9d\xae\
}\x18Xd\xbf\xfce0\xea4hW6s\x18\x04\
\xe6E\xe5\x15P\xc9\x1c\xb6\x1bo\x00\xd3\x1a\xf5\xad\x06\
2\xcf\x01=$\xc3K\x03\x95\xa5\xe3M\x9d\xcd+\xdb\
\xcb\xa9\xf3P[-\xda\xbe`\x9c`\x9c`\x9c`\x9c\
`\x9c`\x9c`\x9c`\x9c`\x5cJ\x07\xbb\xb2\xb8\xbe\
\xb32P\x19\xca\x8b\xd5zk\x85\xdf\x06u\xfc\x00\x02\
D\x16\xd7?\x1fe\x0d\xc18\xc18\xc18\xc1\xb8\xb4\
\xec\x06P^\x86\xa0=\x99\xfd\x07\xcd\x9a\x89\x01\xf4=\
\x5c\xbb]f}\xc18\xc18\xc18\xc18\xc18\xc1\
8\xc1\xb8\xf4\xa8\x0bh3\x0e\xc5.2_~6\xbb\
\xf9\x85\x85\x01\x5c\x1f\xe4\x07\x1dL0N0N0.\
-\x9c!\xf0\x1c\xcd\xc9\xa3\xd2\xfa\x09\xca9\xe7\x9cs\
\xce9\xe7\x1c\x9d\xe7?.\xa1\x9a\x02A^,~\x00\
\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x06\x09\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -16397,6 +16492,10 @@ qt_resource_name = b"\
\x00s\
\x00u\x00c\x00c\x00e\x00s\x00s\x00_\x00o\x00r\x00a\x00n\x00g\x00e\x00.\x00p\x00n\
\x00g\
\x00\x0a\
\x0c\xa8V\x07\
\x00p\
\x00y\x00t\x00e\x00s\x00t\x00.\x00p\x00n\x00g\
\x00\x08\
\x04\xd2YG\
\x00i\
@@ -16492,294 +16591,300 @@ qt_resource_name = b"\
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x10\x00\x02\x00\x00\x00/\x00\x00\x00b\
\x00\x00\x00\x10\x00\x02\x00\x00\x000\x00\x00\x00d\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00/\x00\x00\x003\
\x00\x00\x00\x00\x00\x02\x00\x00\x000\x00\x00\x004\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00 \x00\x02\x00\x00\x00/\x00\x00\x00\x04\
\x00\x00\x00 \x00\x02\x00\x00\x000\x00\x00\x00\x04\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x00~\x17\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x00\xfb\xda\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x00\x7f\xa5\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x03\x8a\x00\x00\x00\x00\x00\x01\x00\x00\xfdh\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x09\xb0\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x1ds\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x04l\x00\x00\x00\x00\x00\x01\x00\x01 \x0a\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x04\x86\x00\x00\x00\x00\x00\x01\x00\x01!\x98\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x00O\x0d\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00`\x90\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x84\xf3\
\x00\x00\x01\x9b\x97*\xf4\x02\
\x00\x00\x04\xa4\x00\x00\x00\x00\x00\x01\x00\x01&{\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x00xV\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x00\x86\x81\
\x00\x00\x01\x9bi\x96\x0e\x1c\
\x00\x00\x04\xbe\x00\x00\x00\x00\x00\x01\x00\x01(\x09\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x02\xd8\x00\x00\x00\x00\x00\x01\x00\x00y\xe4\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x00:\x88\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x02\xfc\x00\x00\x00\x00\x00\x01\x00\x00\x81b\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x05\x10\x00\x00\x00\x00\x00\x01\x00\x01;\x88\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x03\x84\x00\x00\x00\x00\x00\x01\x00\x01\x01\x82\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x00\x82\xf0\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x05*\x00\x00\x00\x00\x00\x01\x00\x01=\x16\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x03\x9e\x00\x00\x00\x00\x00\x01\x00\x01\x03\x10\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x00e#\
\x00\x00\x01\x9d\xe0StG\
\x00\x00\x01\x9d\xed\x19\x07\x82\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x002\xd8\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x016\xa3\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x05\x12\x00\x00\x00\x00\x00\x01\x00\x0181\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x00\x5c\x04\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x01\x9b\x8ac\x97y\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x00T\x8d\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xe0\x00\x00\x00\x00\x00\x01\x00\x010-\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04\xfa\x00\x00\x00\x00\x00\x01\x00\x011\xbb\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x00\x07&\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x15\xcd\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04,\x00\x00\x00\x00\x00\x01\x00\x01\x19\x8b\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04F\x00\x00\x00\x00\x00\x01\x00\x01\x1b\x19\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x00p\x93\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x01\x11)\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x038\x00\x00\x00\x00\x00\x01\x00\x00\xf5\xbe\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x01\x12\xb7\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x03R\x00\x00\x00\x00\x00\x01\x00\x00\xf7L\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x00>\x99\
\x00\x00\x01\x9d\xcf\xc3\xa3\x15\
\x00\x00\x03\xdc\x00\x00\x00\x00\x00\x01\x00\x01\x0cE\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x9d\xed\x19\x07\x82\
\x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x01\x0d\xd3\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x00\x0fl\
\x00\x00\x01\x9b\xc5\xbd\x83\x1b\
\x00\x00\x01\x9b\xa3\xe0\x22\xcf\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00+s\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x03R\x00\x00\x00\x00\x00\x01\x00\x00\xf8J\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x03l\x00\x00\x00\x00\x00\x01\x00\x00\xf9\xd8\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x00!6\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x04\x8c\x00\x00\x00\x00\x00\x01\x00\x01#\xe7\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x04\xa6\x00\x00\x00\x00\x00\x01\x00\x01%u\
\x00\x00\x01\x9b\x8ab\x88n\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00#\xa7\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x01*\x87\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04\xe2\x00\x00\x00\x00\x00\x01\x00\x01,\x15\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x02^\x00\x00\x00\x00\x00\x01\x00\x00l\xf5\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x00H\xe3\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x00f\x1c\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03\x9c\x00\x00\x00\x00\x00\x01\x00\x01\x06\x83\
\x00\x00\x01\x9d\xe0G\xedW\
\x00\x00\x01\x9b\x8acl\xeb\
\x00\x00\x03\xb6\x00\x00\x00\x00\x00\x01\x00\x01\x08\x11\
\x00\x00\x01\x9d\xed\x19\x07\x82\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x00Y~\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x03\xba\x00\x00\x00\x00\x00\x01\x00\x01\x08g\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x00xV\
\x00\x00\x01\x9e\xc7D\xd9\x84\
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x01\x09\xf5\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x00.\x86\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x006\xc0\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x00s\x0e\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x12\x00\x00\x00\x00\x00\x01\x00\x01\x14T\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x04D\x00\x00\x00\x00\x00\x01\x00\x01\x1c\x98\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x03\x0d\xbb\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x03\x8f\xce\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x02\x7ff\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x95\xd9\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x04l\x00\x00\x00\x00\x00\x01\x00\x03\xbe\xf5\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x02\xd6q\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x02\xe7\xb3\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x03\x17\xdc\
\x00\x00\x01\x9b\x97*\xf4\x02\
\x00\x00\x04\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xc7S\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x07\xae\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x02\xb2\xab\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x02\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x13\x1d\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x05\x10\x00\x00\x00\x00\x00\x01\x00\x03\xe1\xd0\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03\x84\x00\x00\x00\x00\x00\x01\x00\x03\x93\x09\
\x00\x00\x01\x9b\x97*\xf3\xe3\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x02\xf1\x9b\
\x00\x00\x01\x9d\xe0StF\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x02\xac\x1e\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x03\xdd\x15\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x02\xe3'\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x02\xdaE\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x02tH\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xe0\x00\x00\x00\x00\x00\x01\x00\x03\xd3\xa8\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x02{n\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x02\x8e3\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04,\x00\x00\x00\x00\x00\x01\x00\x03\xb6q\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x02\xfd\x9e\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x03\xa9\xc6\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x038\x00\x00\x00\x00\x00\x01\x00\x03\x88\xa7\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x02\xb79\
\x00\x00\x01\x9d\xcf\xc3\xa3\x0d\
\x00\x00\x03\xdc\x00\x00\x00\x00\x00\x01\x00\x03\xa1M\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x02\x84'\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xa7\x9d\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03R\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x1d\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x02\x9dv\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x04\x8c\x00\x00\x00\x00\x00\x01\x00\x03\xc4\xbf\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x9f\xd1\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x03\xca\xcc\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x02^\x00\x00\x00\x00\x00\x01\x00\x02\xf9\xb1\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x02\xcd2\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xf2\xd8\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03\x9c\x00\x00\x00\x00\x00\x01\x00\x03\x96:\
\x00\x00\x01\x9d\xe0G\xedD\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x02\xe0\xb4\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03\xba\x00\x00\x00\x00\x00\x01\x00\x03\x9a\xf6\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x02\xaa`\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x02\xafL\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x03\x02f\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x12\x00\x00\x00\x00\x00\x01\x00\x03\xab\xff\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x04D\x00\x00\x00\x00\x00\x01\x00\x03\xb9_\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x01\xb8\xb7\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x025\x11\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x01IR\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x01\x5c\xa2\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04l\x00\x00\x00\x00\x00\x01\x00\x02Vh\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x01\x8cP\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x01\x9c\xc2\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x01\xbe\xc1\
\x00\x00\x01\x9b\x97*\xf4\x02\
\x00\x00\x04\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x5cd\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x01\xb3V\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x01v\xd0\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x02\xfc\x00\x00\x00\x00\x00\x01\x00\x01\xbb\xa5\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x05\x10\x00\x00\x00\x00\x00\x01\x00\x02p\x0a\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03\x84\x00\x00\x00\x00\x00\x01\x00\x02::\
\x00\x00\x01\x9b\x97*\xf3\xe0\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x01\xa0\xd2\
\x00\x00\x01\x9d\xe0StF\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x01p\x12\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x02kt\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x01\x986\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x01\x91m\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x01@$\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xe0\x00\x00\x00\x00\x00\x01\x00\x02eY\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x01GJ\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x01T\xfc\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04,\x00\x00\x00\x00\x00\x01\x00\x02Q\x0b\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x01\xac\x0e\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x02I{\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x038\x00\x00\x00\x00\x00\x01\x00\x02/\x8c\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x01z\x8c\
\x00\x00\x01\x9d\xcf\xc3\xa3\x0d\
\x00\x00\x03\xdc\x00\x00\x00\x00\x00\x01\x00\x02D\xce\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x01N\xba\
\x00\x00\x01\x9b\xc5\xbd\x82\xf5\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01i\x96\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03R\x00\x00\x00\x00\x00\x01\x00\x021\xd3\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x01_\xd4\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x04\x8c\x00\x00\x00\x00\x00\x01\x00\x02Y\xd0\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01a\xca\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x02`\x15\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x02^\x00\x00\x00\x00\x00\x01\x00\x01\xa8\xa4\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x86o\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x01\xa1\xcb\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03\x9c\x00\x00\x00\x00\x00\x01\x00\x02>\xd5\
\x00\x00\x01\x9d\xe0G\xedD\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x01\x95\xfc\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03\xba\x00\x00\x00\x00\x00\x01\x00\x02Ag\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x01l4\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x01s\x8a\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x01\xae\x0e\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x12\x00\x00\x00\x00\x00\x01\x00\x02L9\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04D\x00\x00\x00\x00\x00\x01\x00\x02Sr\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\x9b\x8a`\x8cv\
\x00\x00\x04,\x00\x00\x00\x00\x00\x01\x00\x01\x15\xe2\
\x00\x00\x01\x9b\xa3\xda\x0d$\
\x00\x00\x04^\x00\x00\x00\x00\x00\x01\x00\x01\x1e&\
\x00\x00\x01\x9b\xa3\xda\x0d#\
\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x03\x13D\
\x00\x00\x01\x9b\x8a\x14\x90]\
\x00\x00\x03\x8a\x00\x00\x00\x00\x00\x01\x00\x03\x95W\
\x00\x00\x01\x9b\x8a\x15\xa3\xbc\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x02\x82\x9f\
\x00\x00\x01\x9b\x8a\x1e\x13:\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x99\x12\
\x00\x00\x01\x9b\x8a\x15\xeby\
\x00\x00\x04\x86\x00\x00\x00\x00\x00\x01\x00\x03\xc4~\
\x00\x00\x01\x9b\x8a\x15\x11\xa3\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x02\xd9\xaa\
\x00\x00\x01\x9b\x8a\x16/\xbc\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x02\xea\xec\
\x00\x00\x01\x9b\x8a$\x11*\
\x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x03\x1de\
\x00\x00\x01\x9bi\x96\x0e\x1c\
\x00\x00\x04\xbe\x00\x00\x00\x00\x00\x01\x00\x03\xcc\xdc\
\x00\x00\x01\x9b\x8a\x13\xa4\xaa\
\x00\x00\x02\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x0d7\
\x00\x00\x01\x9b\x8a\x12g\xa0\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x02\xb5\xe4\
\x00\x00\x01\x9b\x8a\x16Qr\
\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x03\x18\xa6\
\x00\x00\x01\x9bi\x96\x0e\x12\
\x00\x00\x05*\x00\x00\x00\x00\x00\x01\x00\x03\xe7Y\
\x00\x00\x01\x9b\x8a\x12\xf3 \
\x00\x00\x03\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x98\x92\
\x00\x00\x01\x9b\x8a\x153f\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x02\xf4\xd4\
\x00\x00\x01\x9d\xed\x19\x07|\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x02\xafW\
\x00\x00\x01\x9b\x8a\x13\x1e\xe7\
\x00\x00\x05\x12\x00\x00\x00\x00\x00\x01\x00\x03\xe2\x9e\
\x00\x00\x01\x9b\x8a\x12\x93}\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x02\xe6`\
\x00\x00\x01\x9b\x8ac\x97y\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x02\xdd~\
\x00\x00\x01\x9b\x8a\x16\xf8\x01\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x02w\x81\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04\xfa\x00\x00\x00\x00\x00\x01\x00\x03\xd91\
\x00\x00\x01\x9b\x8a\x13\x7f\x8d\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x02~\xa7\
\x00\x00\x01\x9bi\x96\x0e\x12\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x02\x91l\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04F\x00\x00\x00\x00\x00\x01\x00\x03\xbb\xfa\
\x00\x00\x01\x9b\x8a\x15|\xe4\
\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x03\x00\xd7\
\x00\x00\x01\x9b\x8a\x16\xd8\x95\
\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xafO\
\x00\x00\x01\x9b\x8a\x14\xb4l\
\x00\x00\x03R\x00\x00\x00\x00\x00\x01\x00\x03\x8e0\
\x00\x00\x01\x9bi\x96\x0e\x12\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x02\xbar\
\x00\x00\x01\x9d\xed\x19\x07|\
\x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x03\xa6\xd6\
\x00\x00\x01\x9b\x8a\x14\xdb\xaa\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x02\x87`\
\x00\x00\x01\x9b\x8a#\x9d\xb9\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xaa\xd6\
\x00\x00\x01\x9b\x8a\x13\xef\xba\
\x00\x00\x03l\x00\x00\x00\x00\x00\x01\x00\x03\x90\xa6\
\x00\x00\x01\x9b\x8a\x16\xb6\x00\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x02\xa0\xaf\
\x00\x00\x01\x9bi\x96\x0e\x12\
\x00\x00\x04\xa6\x00\x00\x00\x00\x00\x01\x00\x03\xcaH\
\x00\x00\x01\x9b\x8ab\x88n\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x02\xa3\x0a\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04\xe2\x00\x00\x00\x00\x00\x01\x00\x03\xd0U\
\x00\x00\x01\x9b\x8a\x16\x95H\
\x00\x00\x02^\x00\x00\x00\x00\x00\x01\x00\x02\xfc\xea\
\x00\x00\x01\x9b\x8a\x16s\xd0\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x02\xd0k\
\x00\x00\x01\x9b\x8a\x12\xc8}\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xf6\x11\
\x00\x00\x01\x9b\x8acl\xeb\
\x00\x00\x03\xb6\x00\x00\x00\x00\x00\x01\x00\x03\x9b\xc3\
\x00\x00\x01\x9d\xed\x19\x07|\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x02\xe3\xed\
\x00\x00\x01\x9bi\x96\x0e\x12\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x0a\xe7\
\x00\x00\x01\x9e\xc7D\xd9~\
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x03\xa0\x7f\
\x00\x00\x01\x9b\x8a\x14@&\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x02\xad\x99\
\x00\x00\x01\x9b\x8a\x13M\xe4\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x02\xb2\x85\
\x00\x00\x01\x9b\x8a\x14\x14\xa7\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x03\x05\x9f\
\x00\x00\x01\x9b\x8a`\x8cv\
\x00\x00\x04,\x00\x00\x00\x00\x00\x01\x00\x03\xb1\x88\
\x00\x00\x01\x9b\x8a#\xd7z\
\x00\x00\x04^\x00\x00\x00\x00\x00\x01\x00\x03\xbe\xe8\
\x00\x00\x01\x9b\x8a\x14h\xdc\
\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x01\xbb\xf0\
\x00\x00\x01\x9b\x8aR\xb8\x17\
\x00\x00\x03\x8a\x00\x00\x00\x00\x00\x01\x00\x028J\
\x00\x00\x01\x9b\x8aT\xb7\x8e\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x01J\xe0\
\x00\x00\x01\x9b\x8aU\xdfw\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x01^0\
\x00\x00\x01\x9b\x8aUX\xd5\
\x00\x00\x04\x86\x00\x00\x00\x00\x00\x01\x00\x02Y\xa1\
\x00\x00\x01\x9b\x8aREh\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x01\x8d\xde\
\x00\x00\x01\x9b\x8aU\x93\xcd\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x01\x9eP\
\x00\x00\x01\x9b\x8aT\xdc/\
\x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x01\xc1\xfa\
\x00\x00\x01\x9bi\x96\x0e\x1c\
\x00\x00\x04\xbe\x00\x00\x00\x00\x00\x01\x00\x02_\x9d\
\x00\x00\x01\x9b\x8aS^\xe7\
\x00\x00\x02\xd8\x00\x00\x00\x00\x00\x01\x00\x01\xb6\x8f\
\x00\x00\x01\x9b\x8aTJ\xe5\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x01x^\
\x00\x00\x01\x9b\x8aU\xc2\xd1\
\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x01\xbe\xde\
\x00\x00\x01\x9b\x8aU\x00b\
\x00\x00\x05*\x00\x00\x00\x00\x00\x01\x00\x02sC\
\x00\x00\x01\x9b\x8aS\xebC\
\x00\x00\x03\x9e\x00\x00\x00\x00\x00\x01\x00\x02=s\
\x00\x00\x01\x9b\x8aR\x1a\xcf\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x01\xa2`\
\x00\x00\x01\x9d\xed\x19\x07|\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x01q\xa0\
\x00\x00\x01\x9b\x8aS\xc7\x80\
\x00\x00\x05\x12\x00\x00\x00\x00\x00\x01\x00\x02n\xad\
\x00\x00\x01\x9b\x8aT/\x12\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x01\x99\xc4\
\x00\x00\x01\x9b\x8ac\x97y\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x01\x92\xfb\
\x00\x00\x01\x9b\x8aV\xbbi\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x01A\xb2\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04\xfa\x00\x00\x00\x00\x00\x01\x00\x02h\x92\
\x00\x00\x01\x9b\x8aS}\xfc\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x01H\xd8\
\x00\x00\x01\x9b\x8aV:\x86\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x01V\x8a\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04F\x00\x00\x00\x00\x00\x01\x00\x02TD\
\x00\x00\x01\x9b\x8aT\x92U\
\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x01\xad\x9c\
\x00\x00\x01\x9b\x8aV\x99B\
\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x02L\xb4\
\x00\x00\x01\x9b\x8aR\x903\
\x00\x00\x03R\x00\x00\x00\x00\x00\x01\x00\x022\xc5\
\x00\x00\x01\x9b\x8aU<\xbc\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x01|\x1a\
\x00\x00\x01\x9d\xed\x19\x07|\
\x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x02H\x07\
\x00\x00\x01\x9b\x8aRh\x0f\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x01PH\
\x00\x00\x01\x9b\xa3\xdek\xc5\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01k$\
\x00\x00\x01\x9b\x8aS@S\
\x00\x00\x03l\x00\x00\x00\x00\x00\x01\x00\x025\x0c\
\x00\x00\x01\x9b\x8aVw{\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x01ab\
\x00\x00\x01\x9b\x8aVZ\xb0\
\x00\x00\x04\xa6\x00\x00\x00\x00\x00\x01\x00\x02]\x09\
\x00\x00\x01\x9b\x8ab\x88n\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01cX\
\x00\x00\x01\x9bi\x96\x0e\x13\
\x00\x00\x04\xe2\x00\x00\x00\x00\x00\x01\x00\x02cN\
\x00\x00\x01\x9b\x8aV\x1c\x17\
\x00\x00\x02^\x00\x00\x00\x00\x00\x01\x00\x01\xaa2\
\x00\x00\x01\x9b\x8aU\xfd\x14\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x87\xfd\
\x00\x00\x01\x9b\x8aT\x10\x07\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x01\xa3Y\
\x00\x00\x01\x9b\x8acl\xeb\
\x00\x00\x03\xb6\x00\x00\x00\x00\x00\x01\x00\x02B\x0e\
\x00\x00\x01\x9d\xed\x19\x07|\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x01\x97\x8a\
\x00\x00\x01\x9b\x8aU\x1c\xe9\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x01\xb4\xe4\
\x00\x00\x01\x9e\xc7D\xd9\x83\
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x02D\xa0\
\x00\x00\x01\x9b\x8aR\xfd@\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x01m\xc2\
\x00\x00\x01\x9b\x8aS\x9cE\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x01u\x18\
\x00\x00\x01\x9b\x8aS\x1fV\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x01\xaf\x9c\
\x00\x00\x01\x9b\x8a`\x8cv\
\x00\x00\x04,\x00\x00\x00\x00\x00\x01\x00\x02Or\
\x00\x00\x01\x9b\x8aUv\xf5\
\x00\x00\x04^\x00\x00\x00\x00\x00\x01\x00\x02V\xab\
\x00\x00\x01\x9b\x8aR\xd9\xb9\
"
def qInitResources():

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -134,6 +134,7 @@ class TestFileManager:
QApplication.processEvents()
test_data = w.test_service.tree()
w.treeTests.clear()
w._reset_search()
QApplication.processEvents()
w.treeTests.loadTestRecursively(w.treeTests.invisibleRootItem(), test_data)
self._close_progress(progress)

View File

@@ -163,6 +163,46 @@ class QTestTree(QTreeWidget):
def clearGlobalSuccess(self):
self._global_success = True
def _all_items(self):
"""Pre-order (visual, top-to-bottom) iteration over every tree item."""
def walk(parent):
for i in range(parent.childCount()):
child = parent.child(i)
yield child
yield from walk(child)
yield from walk(self.invisibleRootItem())
def clear_search(self):
# Block signals: setBackground -> itemChanged -> on_testChecked storm.
self.blockSignals(True)
try:
for it in self._all_items():
it.setSearchMatch(False)
finally:
self.blockSignals(False)
def search(self, text, fields):
"""Highlight items matching *text* in *fields*, expand ancestors, return matches."""
matches = []
text = (text or "").strip()
needle = text.lower()
active = bool(text and fields)
# One blocked pass: clear stale + set new matches without firing signals.
self.blockSignals(True)
try:
for it in self._all_items():
matched = active and it.matches_search(needle, fields)
it.setSearchMatch(matched)
if matched:
matches.append(it)
p = it.parent()
while p is not None:
self.expandItem(p)
p = p.parent()
finally:
self.blockSignals(False)
return matches
def __findItemByIdRecursively(self, item_id, parent):
res = None
i = 0

View File

@@ -12,6 +12,8 @@ from api.testium import print_warn
_ITEM_CONFIG = {
"unittest": {"icon": "folder.png", "icon_on": "folder-open.png", "expanded": True, "no_breakpoint": True},
"unittest step": {"icon": "document.png", "no_breakpoint": True},
"pytest": {"icon": "pytest.png", "expanded": True, "no_breakpoint": True},
"pytest step": {"icon": "pytest.png", "no_breakpoint": True},
"Console": {"icon": "terminal.png", "unfoldable": False},
"Console action": {"icon": "terminal.png"},
"Cycle": {"icon": "cycle.png", "expanded": True},
@@ -101,7 +103,7 @@ class QTestTreeItem(QTreeWidgetItem):
self.setFlags(self.flags() | Qt.ItemIsUserCheckable)
self.setCheckState(self._cols["name"]["index"], Qt.Checked)
self._is_highlighted = False
self._initial_brush = None
self._is_search_match = False
self._failure_list = None
self._no_breakpoint = False
parent.addChild(self)
@@ -178,17 +180,44 @@ class QTestTreeItem(QTreeWidgetItem):
def isBreakpoint(self):
return self._display_pause
def _refresh_highlight(self):
"""Recompute name-column colours from flags: run (green) > search (amber) > none."""
col = self._cols["name"]["index"]
if self._is_highlighted:
self.setBackground(col, QBrush(QColor(153, 255, 153)))
self.setForeground(col, QBrush())
elif self._is_search_match:
self.setBackground(col, QBrush(QColor(255, 224, 130)))
self.setForeground(col, QBrush(QColor(0, 0, 0)))
else:
self.setBackground(col, QBrush())
self.setForeground(col, QBrush())
def setHighlighted(self):
if not self._is_highlighted:
self._initial_brush = self.background(self._cols["name"]["index"])
color = QBrush(QColor(153, 255, 153))
self.setBackground(self._cols["name"]["index"], color)
self._is_highlighted = True
self._refresh_highlight()
def resetHighlighted(self):
if self._is_highlighted:
self.setBackground(self._cols["name"]["index"], self._initial_brush)
self._is_highlighted = False
self._refresh_highlight()
def matches_search(self, needle, fields):
"""True if *needle* (lowercase) is in any enabled field (name/type/doc)."""
if "name" in fields and needle in (self.name or "").lower():
return True
if "type" in fields and needle in (self.test_type or "").lower():
return True
if "doc" in fields and needle in str(self.doc or "").lower():
return True
return False
def setSearchMatch(self, on):
"""Search highlight (amber bg + black text), readable in any theme."""
if on != self._is_search_match:
self._is_search_match = on
self._refresh_highlight()
def setRowIcon(self, resource_off, resource_on=""):

View File

@@ -7,7 +7,7 @@ import shutil
# Qt
from PySide6 import QtGui
from PySide6.QtGui import QAction, QShortcut, QIcon, QPixmap, QTextCursor, QDesktopServices, QTextCursor
from PySide6.QtGui import QAction, QShortcut, QIcon, QPixmap, QTextCursor, QDesktopServices, QTextCursor, QKeySequence
from PySide6.QtCore import Slot, QUrl, Qt, QTimer
from PySide6.QtWidgets import (
@@ -16,6 +16,12 @@ from PySide6.QtWidgets import (
QDialog,
QFileDialog,
QSizePolicy,
QWidget,
QHBoxLayout,
QLineEdit,
QCheckBox,
QLabel,
QToolButton,
)
ourPath = os.path.dirname(__file__)
@@ -169,6 +175,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
activated=self.on_F1Pressed,
)
self._search_matches = []
self._search_idx = 0
self._build_search_bar()
self.shortcut_find = QShortcut(
QKeySequence.Find, self, activated=self._toggle_search
)
self.actionRefresh_test.setDisabled(True)
# Signal connections
@@ -295,6 +308,135 @@ class MainWindow(QMainWindow, Ui_MainWindow):
del self.treeTests
self.treeTests = None
# ---- test-tree search ---------------------------------------------------
def _build_search_bar(self):
"""Find bar (Ctrl+F): highlight + navigate matches; Name/Type/Doc pick fields."""
self.searchBar = QWidget(self.widget)
lay = QHBoxLayout(self.searchBar)
lay.setContentsMargins(2, 2, 2, 2)
lay.setSpacing(4)
self.searchEdit = QLineEdit(self.searchBar)
self.searchEdit.setPlaceholderText("Search the test tree…")
self.searchEdit.setClearButtonEnabled(True)
lay.addWidget(self.searchEdit, 1)
self.cbSearchName = QCheckBox("Name", self.searchBar)
self.cbSearchType = QCheckBox("Type", self.searchBar)
self.cbSearchDoc = QCheckBox("Doc", self.searchBar)
for cb in (self.cbSearchName, self.cbSearchType, self.cbSearchDoc):
cb.setChecked(True)
cb.toggled.connect(self._do_search)
lay.addWidget(cb)
self.searchCount = QLabel("", self.searchBar)
lay.addWidget(self.searchCount)
self.searchPrev = QToolButton(self.searchBar)
self.searchPrev.setArrowType(Qt.UpArrow)
self.searchPrev.setToolTip("Previous match")
self.searchPrev.clicked.connect(self._search_prev)
lay.addWidget(self.searchPrev)
self.searchNext = QToolButton(self.searchBar)
self.searchNext.setArrowType(Qt.DownArrow)
self.searchNext.setToolTip("Next match (Enter)")
self.searchNext.clicked.connect(self._search_next)
lay.addWidget(self.searchNext)
self.searchClose = QToolButton(self.searchBar)
self.searchClose.setText("")
self.searchClose.setToolTip("Close (Esc)")
self.searchClose.clicked.connect(self._close_search)
lay.addWidget(self.searchClose)
self.searchEdit.textChanged.connect(self._do_search)
self.searchEdit.returnPressed.connect(self._search_next)
QShortcut(Qt.Key_Escape, self.searchEdit,
context=Qt.WidgetShortcut, activated=self._close_search)
# Insert above the tree (index 0 is the control row from setupUi).
self.verticalLayout.insertWidget(1, self.searchBar)
self.searchBar.setVisible(False)
def _search_fields(self):
fields = set()
if self.cbSearchName.isChecked():
fields.add("name")
if self.cbSearchType.isChecked():
fields.add("type")
if self.cbSearchDoc.isChecked():
fields.add("doc")
return fields
def _toggle_search(self):
"""Ctrl+F: open the find bar, or close it (clearing the highlight)."""
if self.searchBar.isVisible():
self._close_search()
else:
self._open_search()
def _open_search(self):
self.searchBar.setVisible(True)
self.searchEdit.setFocus()
self.searchEdit.selectAll()
if self.searchEdit.text():
self._do_search()
def _do_search(self):
if self.treeTests is None:
return
self._search_matches = self.treeTests.search(
self.searchEdit.text(), self._search_fields()
)
self._search_idx = 0
if self._search_matches:
self._goto_match(0)
else:
self._update_search_count()
def _update_search_count(self):
n = len(self._search_matches)
if n == 0:
self.searchCount.setText(
"0/0" if self.searchEdit.text().strip() else ""
)
else:
self.searchCount.setText("{}/{}".format(self._search_idx + 1, n))
def _goto_match(self, idx):
if not self._search_matches:
return
self._search_idx = idx % len(self._search_matches)
it = self._search_matches[self._search_idx]
self.treeTests.scrollToItem(it)
self.treeTests.setCurrentItem(it)
self._update_search_count()
def _search_next(self):
if self._search_matches:
self._goto_match(self._search_idx + 1)
def _search_prev(self):
if self._search_matches:
self._goto_match(self._search_idx - 1)
def _close_search(self):
if self.treeTests is not None:
self.treeTests.clear_search()
self.treeTests.setFocus()
self.searchBar.setVisible(False)
self._search_matches = []
def _reset_search(self):
"""New test file loaded: drop stale matches and hide the bar."""
self._search_matches = []
self._search_idx = 0
if hasattr(self, "searchBar"):
self.searchBar.setVisible(False)
self.searchCount.setText("")
def file_loaded_at_startup(self):
modeSlider_value = prefs.settings.show_checkboxes
if modeSlider_value:

View File

@@ -105,6 +105,46 @@
- read_until: {expected: console_host_check_HOST, timeout: 5}
{% endif %}
# --- read_until matching a list of values (succeeds on any) ---
- console:
name: Console read_until list match any
console_name: term
key: $(test)_PASS
steps:
- writeln: echo "list_marker_B"
- read_until: {expected: [list_marker_A, list_marker_B, list_marker_C], timeout: 5}
- console:
name: Console read_until list no match
console_name: term
key: $(test)_FAIL
steps:
- read_until: {expected: [never_marker_A, never_marker_B], timeout: 1}
# --- read_until with regular expressions ---
- console:
name: Console read_until regex
console_name: term
key: $(test)_PASS
steps:
- writeln: echo "regex_val_4242_end"
- read_until: {expected: 'regex_val_\d+_end', regex: true, timeout: 5}
- console:
name: Console read_until regex list any
console_name: term
key: $(test)_PASS
steps:
- writeln: echo "STATUS=ready"
- read_until: {expected: ['ERR:.*', 'STATUS=(ready|busy)'], regex: true, timeout: 5}
- console:
name: Console read_until regex no match
console_name: term
key: $(test)_FAIL
steps:
- read_until: {expected: 'never_\d{4}', regex: true, timeout: 1}
- console:
name: Console closure
execute_on_stop: true

View File

@@ -0,0 +1 @@
no_param: Null

View File

@@ -0,0 +1,17 @@
- pytest:
name: Pytest item
test_file: {{include_directory}}/test_cases.py
key: $(test)_PASS
test_method:
- test_01_pass
- test_02_pass
- test_05_param
- pytest:
name: Pytest item
test_file: {{include_directory}}/test_cases.py
key: $(test)_FAIL
test_method:
- test_01_pass
- test_03_fail
- test_04_skip

View File

@@ -0,0 +1,28 @@
import pytest
def test_01_pass():
''' Test 01 passes '''
assert 1 + 1 == 2
def test_02_pass():
''' Test 02 passes '''
assert "a" in "abc"
def test_03_fail():
''' Test 03 fails on purpose '''
assert 1 == 2, "deliberate failure"
@pytest.mark.skip(reason="skipped on purpose")
def test_04_skip():
''' Test 04 is skipped '''
assert False
@pytest.mark.parametrize("n", [1, 2])
def test_05_param(n):
''' Test 05 is parametrised, both cases pass '''
assert n < 3

View File

@@ -21,7 +21,7 @@
# even Flatpak reaches it via flatpak-spawn --host. The validation venv
# is created with --system-site-packages so existing system packages
# (PySide6, lxml, ...) stay visible, then junit-xml is pip-installed
# for post_execution.py.
# for post_execution.py and pytest for the `pytest` item.
#
# The report file is suffixed with the mode (e.g. validation-flatpak.sqlite)
# so consecutive runs in different modes don't overwrite each other.
@@ -73,7 +73,7 @@ if [ ! -d "$VENV_DIR" ]; then
echo "Creating validation venv at $VENV_DIR"
python3 -m venv --system-site-packages "$VENV_DIR"
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
"$VENV_DIR/bin/pip" install --quiet junit-xml
"$VENV_DIR/bin/pip" install --quiet junit-xml pytest
fi
VENV_PYTHON="$VENV_DIR/bin/python3"