Add parallel test item with thread-aware stdout routing

The parallel item runs branches concurrently with sync:all or sync:any
policy and optional per-branch wait_for synchronization. Each branch
runs in its own daemon thread and produces a clean per-item entry in
the SQLite report; the live output is prefixed [<branch_name>] so
concurrent branches stay readable.

Supporting changes:
- StdoutProxy (lib/stdout_redirect.py): thread-aware sys.stdout/stderr
  with per-thread capture buffers and per-branch live-output prefix.
  Adds writeln() for Python 3.14 unittest compatibility.
- TestItemContainer: shared base extracted from Group/Cycle for the
  sequential children execution pattern.
- TestItemSleep: interruptible loop polling _is_stopped so sync:any
  can cancel slow branches quickly.
- TestReport: thread-safe SQLite (check_same_thread=False + lock).

Also drops the unused -m/--terminal mode and its module.

Validation: 11 scenarios in test/validation/items/parallel covering
sync:all/any, no_fail, wait_for + timeout, conditions, multi-branch,
nested parallel, parallel inside loop, real branch failure.

Documentation: new parallel_test_item.rst added to the manual.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 23:23:31 +02:00
parent be540cd304
commit 1b2d427ced
22 changed files with 1258 additions and 558 deletions

View File

@@ -2,26 +2,25 @@
## What is testium
Testium is a test sequencer/runner written in Python. It executes YAML-based test scripts ("`.tum`" files) and supports three execution modes:
Testium is a test sequencer/runner written in Python. It executes YAML-based test scripts ("`.tum`" files) and supports two execution modes:
- **GUI mode** (default, no flag): PySide6 Qt application (`src/testium/main_win/`)
- **Batch mode** (`-b` / `--batch-execution`): headless, non-interactive, runs tests and exits
- **Terminal mode** (`-m` / `--terminal`): interactive REPL in the terminal (partially implemented / work-in-progress)
Run from repo root: `./run.sh` (Linux) or `run.bat` / `run.ps1` (Windows).
Direct invocation: `python3 -m src/testium [-b|-m] <test_file.tum>`
Direct invocation: `python3 -m src/testium [-b] <test_file.tum>`
## Architecture
### Entry point
`src/testium/__init__.py` — parses CLI args, dispatches to the three modes.
`src/testium/__init__.py` — parses CLI args, dispatches to the two modes.
`multiprocessing.set_start_method('spawn')` is called early (required for Linux dialog subprocesses).
### Core execution
- `src/testium/interpreter/process.py``TestProcess(multiprocessing.Process)`: runs the test in a child process. Stdout is redirected via a `StringQueue` → pipe → parent thread (`capture_stdout`) that writes to real stdout.
- `src/testium/interpreter/batch.py``Batch`: parent-side orchestrator for `-b` mode. Creates the `msg_queue`, starts `TestProcess`, waits for the "finished" signal.
- `src/testium/interpreter/test_set.py``TestSet`: builds and executes the tree of test items.
- `src/testium/interpreter/test_items/test_item*.py` — one file per test item type (check, cycle, group, let, unittest, py_func, lua_func, console, git, dialogs, report, …).
- `src/testium/interpreter/test_items/test_item*.py` — one file per test item type (check, cycle, group, let, unittest, py_func, lua_func, console, git, dialogs, report, parallel, …).
### Communication channels (parent ↔ child process)
- `msg_queue` (`multiprocessing.Queue`): carries status messages from child to parent.
@@ -44,7 +43,7 @@ test item print()
`src/testium/interpreter/utils/globdict.py` — shared state accessible from test scripts via `tm.gd()` / `tm.setgd()`. When `set_update_queue()` is active (during test execution), every `setgd`/`delgd` on a non-`_`-prefixed key pushes a message to `msg_queue`.
### Coloring (`-o` disables it)
`src/testium/interpreter/utils/termlog.py``TermLog` wraps stdout with colorama-based line coloring (PASS=green, FAIL=red, WARN=yellow, …). Applied in parent process for batch/terminal modes. Auto-detects light/dark terminal background via (in order): `COLORFGBG` env var, OSC 11 query, default dark.
`src/testium/interpreter/utils/termlog.py``TermLog` wraps stdout with colorama-based line coloring (PASS=green, FAIL=red, WARN=yellow, …). Applied in parent process for batch mode. Auto-detects light/dark terminal background via (in order): `COLORFGBG` env var, OSC 11 query, default dark.
### Dialog items in batch mode
All dialog items (`dialog_image`, `dialog_question`, `dialog_references`, `dialog_value`, `dialog_message`, `dialog_choices`, `dialog_note`) follow this rule in non-interactive text mode (`-b`):
@@ -54,6 +53,50 @@ All dialog items (`dialog_image`, `dialog_question`, `dialog_references`, `dialo
`auto_result` (and `auto_value` for value/note dialogs) is intended for the validation test suite (`test/validation/`) only.
### `parallel` item
`src/testium/interpreter/test_items/test_item_parallel.py` — runs multiple branches concurrently.
```yaml
- parallel:
name: My parallel block
sync: all # all: wait for all; any: stop as soon as one finishes
no_fail: true # (optional) don't propagate branch failures to parent
branches:
- name: Branch A
wait_for: # (optional) poll condition before starting
condition: <| expr |>
timeout: 10
steps:
- ...
- name: Branch B
steps:
- ...
```
- `TestItemParallel(TestItemContainer)`: mutates `dict_item["steps"]` to inject synthetic `parallel_branch` items so `load_test_recursively` loads branches normally as children.
- `TestItemParallelBranch(TestItemContainer)`: container for one branch. `wait_for` polls every 0.1s up to `timeout` seconds before running steps.
- `sync: any` calls `_stop_branch_recursively()` on all other branches when one *actually runs* (SUCCESS/FAILURE). A `NORUN` branch (disabled, condition not met) never wins the race.
- Each branch runs in a daemon thread; the parent waits with `.join()`.
- Branches stopped late (e.g. user disabled them in the GUI, or another sync:any branch already won) go through the normal `branch.stop() + branch.execute()` path so they always produce a clean DB entry via `addTest()`.
- Exceptions raised in a branch's `execute()` are caught by `run_branch`, logged to stdout, and converted to a `FAILURE` result so they never disappear silently.
- `sync: all` ignores `NORUN` branches when computing success (matches Group/Cycle semantics): only an actual `FAILURE` fails the parallel.
- `TestItemSleep` is interruptible (polls `self._is_stopped` in a loop) so `sync: any` can stop slow branches quickly. `py_func` and `console` items are not interruptible; their full duration is observed before the branch returns.
### `TestItemContainer` base class
`src/testium/interpreter/test_items/test_item_container.py` — shared base for Group, Cycle, Parallel, and ParallelBranch. Provides `_run_children_sequentially()` which handles stop-on-failure, `executedOnStop` items, and returns `(TestResult, stopped_bool)`.
### Report threading
`src/testium/interpreter/test_report/test_report.py` — SQLite report with thread-safe writes:
- `sqlite3.connect(..., check_same_thread=False)`
- `self._lock = threading.Lock()` guards the SQLite `INSERT` only.
- Per-item log capture (`stdio_redir.read()`) is naturally race-free thanks to per-thread buffers (see `StdoutProxy`).
### Thread-aware stdout (`StdoutProxy`)
`src/lib/stdout_redirect.py` — when `log_stored: True`, `intercept()` installs a `StdoutProxy` as `sys.stdout`/`sys.stderr` instead of a single shared `StringQueue`. The proxy:
- Holds one `StringQueue` per thread (registered via `register_thread(buffer=...)`). The main thread uses a default buffer; each parallel branch's thread registers its own at start and unregisters at end. `stdio_redir.read()` reads the calling thread's buffer → `addTest()` of an item running in branch X reads X's clean, non-interleaved output.
- For the live stream (terminal in batch / GUI panel), prefixes every line emitted from a branch's thread with `[<branch_name>] ` so concurrent branches stay readable.
- Exposes `write` / `writeln` / `flush` (Python 3.14's `unittest` calls `stream.writeln()` directly without `_WritelnDecorator`).
## Key files
| Path | Role |
@@ -61,8 +104,9 @@ All dialog items (`dialog_image`, `dialog_question`, `dialog_references`, `dialo
| `src/testium/__init__.py` | CLI entry, mode dispatch |
| `src/testium/interpreter/batch.py` | `-b` mode orchestrator |
| `src/testium/interpreter/process.py` | Child test process |
| `src/testium/interpreter/terminal.py` | `-m` mode (WIP) |
| `src/testium/interpreter/test_set.py` | Test tree builder/executor |
| `src/testium/interpreter/test_items/test_item_container.py` | Base class for container items |
| `src/testium/interpreter/test_items/test_item_parallel.py` | `parallel` and `parallel_branch` items |
| `src/testium/interpreter/utils/globdict.py` | Global variable dict |
| `src/testium/interpreter/utils/termlog.py` | Terminal color output |
| `src/lib/stdout_redirect.py` | `StdioRedirect` singleton (`stdio_redir`) |
@@ -91,9 +135,6 @@ 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).
## Known issues / WIP
- `-m` (terminal mode) is not functional yet.
### `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:
- **SUCCESS** if the sub-instance launched and ran to completion (exit code is ignored)
@@ -101,6 +142,15 @@ Icons are assigned once when the test file is loaded (not updated live on theme
The sub-test's own pass/fail result is intentionally not propagated.
## Recent fixes (branch `parallel_execution`)
- `test_item_parallel.py`: new `parallel` item with `sync: all|any`, `wait_for`, daemon threads, `_stop_branch_recursively()`. Each branch thread registers a per-thread stdout buffer with `stdio_redir.register_thread(...)` so its log capture and live-output prefix work in isolation.
- `test_item_container.py`: new `TestItemContainer` base class extracted from Group/Cycle patterns
- `test_item_sleep.py`: interruptible loop (checks `self._is_stopped`) instead of blocking `time.sleep()` so `sync: any` can stop slow branches quickly
- `stdout_redirect.py`: rewrote `intercept()` to install a `StdoutProxy` (thread-aware: per-thread capture buffers + branch-prefixed live output). Adds `writeln()` for Python 3.14 unittest compatibility.
- `test_report.py`: `check_same_thread=False` + lock around the SQLite `INSERT` for parallel branch concurrency. Log capture itself is race-free thanks to per-thread buffers.
- `__init__.py`: removed `-m`/`--terminal` mode
- `terminal.py`: deleted
## Recent fixes (branch `text_no_pyside`)
- `batch.py`: premature loop exit when `gd_update` messages (no `"id"` key) were mistaken for the "finished" signal — fix: `"id" in m and m["id"] is None`
- `batch.py`: `control("loaded")` deadlock if `TestProcess` crashed before `cmd_th` started — fix: daemon thread + `threading.Event` + `is_alive()` polling
@@ -109,7 +159,11 @@ The sub-test's own pass/fail result is intentionally not propagated.
- `run` item: removed `stdout=PIPE` (caused deadlock with `multiprocessing` spawn); simplified result to SUCCESS on any completed subprocess.
## Validation tests
Located in `test/validation/`. Run with `-b` flag against each `.tum` file.
Located in `test/validation/`. Run with `-b` flag:
```
./run.sh -b -l mon_log.log -- test/validation/main.tum
```
Parallel item tests: `test/validation/items/parallel/test.tum`
## Dependencies
See `src/requirements.txt`. Key ones: `pyside6`, `pyyaml`, `jinja2`, `colorama`, `gitpython`, `pexpect`, `matplotlib`.

View File

@@ -0,0 +1,96 @@
.. _sec_parallel_item:
**parallel** test item
============================================================
This element is of the following form:
.. code-block:: yaml
:caption: ``parallel`` test item usage example
- parallel:
name: My parallel block
sync: all
branches:
- name: Branch A
steps:
- py_func:
name: Long operation
file: long_op.py
func_name: do_work
- name: Branch B
wait_for:
condition: <| "$(ready_flag)" == "True" |>
timeout: 30
steps:
- let:
name: Mark done
values:
- branch_b_done: true
The ``parallel`` element runs several sequences of items concurrently. Each
inner sequence is called a *branch* and runs in its own thread. The parent
test item waits for branches to finish according to the ``sync`` policy.
Attributes
--------------------
* ``branches``: required. A list of branches to execute concurrently. Each
branch has a ``name`` and a ``steps`` list (same structure as a ``group``
item). It can also declare a ``wait_for`` precondition (see below).
* ``sync``: optional, defaults to ``all``.
* ``all``: the parallel item completes when *every* branch has finished.
The result is ``PASS`` if no branch returned ``FAIL`` (skipped or
disabled branches are ignored, like in ``group``); otherwise ``FAIL``.
* ``any``: the parallel item completes as soon as the *first* branch
finishes. The remaining branches are stopped (their next test items
are not executed). The result is ``PASS`` if at least one branch
succeeded.
* ``no_fail``: optional. When ``true``, a ``FAIL`` result is forced to
``PASS`` for the parallel item itself (same semantics as for any test
item). Branches keep their own result.
Branch attributes
--------------------
Each entry of ``branches`` is a dict with the following attributes:
* ``name``: required. The branch name. Used in reports and as a prefix
in the live log output (each line printed by the branch is prefixed
with ``[<name>] `` so concurrent branches stay readable).
* ``steps``: required. The list of test items executed sequentially
inside the branch.
* ``wait_for``: optional. Forces the branch to wait until a condition is
met before running its steps. If the timeout elapses, the branch
returns ``FAIL`` (the steps are not run). Sub-attributes:
* ``condition``: a testium expression evaluated repeatedly (every
100 ms) until it returns ``True``.
* ``timeout``: maximum wait, in seconds. Defaults to 30.
Reporting
--------------------
Each branch produces its own row in the SQLite report (with type
``Parallel branch``), in addition to the parent ``Parallel`` row. The
``log`` column of each row contains only the output emitted from that
branch's thread, so logs are never mixed between concurrent branches.
In the live (terminal / GUI) output, lines emitted from a branch are
prefixed with ``[<branch name>] ``. The prefix is not stored in the
SQLite log column.
Notes
--------------------
* A ``sleep`` item inside a branch is interruptible: if another
``sync: any`` branch wins the race, slow ``sleep`` items are aborted
within ~50 ms.
* A ``py_func`` or ``console`` item inside a branch is **not**
interruptible: a ``sync: any`` stop will only take effect after the
current item returns. The branch will then skip its remaining steps.
* When a user disables a branch in the GUI tree, the branch returns
``SKIP`` instantly without affecting the others (it does *not* win a
``sync: any`` race).

View File

@@ -255,6 +255,7 @@ step list attributes.
test_items/let_test_item.rst
test_items/loop_test_item.rst
test_items/lua_func_test_item.rst
test_items/parallel_test_item.rst
test_items/plot_test_item.rst
test_items/report_test_item.rst
test_items/run_test_item.rst

Binary file not shown.

View File

@@ -1,8 +1,88 @@
import sys
import threading
from threading import (Thread, Event)
from lib.string_queue import StringQueue
from time import (sleep)
class StdoutProxy:
"""Thread-aware stdout proxy.
Each writing thread can be associated with:
- a per-thread buffer (StringQueue) where its writes are captured for the
per-item SQLite log column;
- a 'branch' label, used to prefix each line in the live (parent-visible)
output stream so concurrent branches are easy to read.
Threads with no association fall back to the default buffer (the "main"
thread's buffer) and write to live output without prefix.
"""
def __init__(self, live_stream, default_buffer):
self.live_stream = live_stream
self.default_buffer = default_buffer
self._buffers = {}
self._branches = {}
self._lock = threading.Lock()
def register(self, tid=None, buffer=None, branch=None):
if tid is None:
tid = threading.get_ident()
with self._lock:
if buffer is not None:
self._buffers[tid] = buffer
if branch is not None:
self._branches[tid] = branch
def unregister(self, tid=None):
if tid is None:
tid = threading.get_ident()
with self._lock:
self._buffers.pop(tid, None)
self._branches.pop(tid, None)
def get_buffer(self, tid=None):
if tid is None:
tid = threading.get_ident()
with self._lock:
return self._buffers.get(tid, self.default_buffer)
def write(self, s):
if not s:
return
tid = threading.get_ident()
with self._lock:
buf = self._buffers.get(tid, self.default_buffer)
branch = self._branches.get(tid)
# Per-thread capture: clean, no prefix
buf.write(s)
# Live stream: prefix each line with the branch label
if branch:
self.live_stream.write(self._prefix(s, f'[{branch}] '))
else:
self.live_stream.write(s)
@staticmethod
def _prefix(s, prefix):
ends_nl = s.endswith('\n')
body = s[:-1] if ends_nl else s
if body == '':
return s
prefixed = '\n'.join(prefix + line for line in body.split('\n'))
if ends_nl:
prefixed += '\n'
return prefixed
def writeln(self, s=''):
self.write(s + '\n')
def flush(self):
try:
self.live_stream.flush()
except AttributeError:
pass
class StdioRedirect:
def __init__(self):
@@ -28,48 +108,38 @@ class StdioRedirect:
def intercept(self):
if not self.spy_enabled:
self.thr_started = Event()
self.log_buf = StringQueue()
self.in_stream = StringQueue()
self.stop_output = Event()
self.thrd_out = Thread(target=self.interceptStdOut)
self.thrd_out.daemon = True
sys.stdout = self.in_stream
sys.stderr = self.in_stream
self.stream = self.in_stream
self.thrd_out.start()
self.thr_started.wait()
self.log_buf = StringQueue() # default buffer (main thread)
self.proxy = StdoutProxy(self.out_stream, self.log_buf)
sys.stdout = self.proxy
sys.stderr = self.proxy
self.stream = self.proxy
self.spy_enabled = True
def stop(self):
if self.spy_enabled:
sys.stdout = self.out_stream
sys.stderr = self.out_stream
self.stream = self.out_stream
self.stop_output.set()
self.thrd_out.join()
del self.log_buf
del self.in_stream
del self.stop_output
del self.thrd_out
del self.thr_started
del self.proxy
self.spy_enabled = False
def interceptStdOut(self):
self.thr_started.set()
while not self.stop_output.is_set():
data = self.in_stream.read()
self.log_buf.write(data)
self.out_stream.write(data)
if data == '':
sleep(0.1)
def read(self):
ret = ''
"""Read accumulated content from the calling thread's buffer."""
if not self.spy_enabled:
return ''
return self.proxy.get_buffer().read()
def register_thread(self, buffer=None, branch=None):
"""Register the calling thread's per-thread buffer and/or branch label."""
if self.spy_enabled:
ret = self.log_buf.read()
return ret
self.proxy.register(buffer=buffer, branch=branch)
def unregister_thread(self):
"""Drop the calling thread's registration."""
if self.spy_enabled:
self.proxy.unregister()
stdio_redir = StdioRedirect()

View File

@@ -21,10 +21,8 @@ def main():
help="Returns the version of testium", action='store_true')
parser.add_argument("-b", "--batch-execution",
help="Executes the test in batch mode", action='store_true')
parser.add_argument("-m", "--terminal",
help="Starts terminal mode", action='store_true')
parser.add_argument("-o", "--no-color",
help="Deactivates stdout colors in batch and terminal mode", action='store_true')
help="Deactivates stdout colors in batch mode", action='store_true')
parser.add_argument("-c", "--config-file", help="Configuration file",
nargs='+',
default=[])
@@ -95,30 +93,6 @@ def main():
from interpreter.utils.version import get_testium_version
print(get_testium_version())
elif args.terminal:
import select
from interpreter.terminal import Terminal
if (lf != '') or (rf != '') or (tf != '') or (pn != []):
print('"-l", "-p", "-t", "-n" options are not supported in this mode.')
t = Terminal(os.getcwd(), cf, defines, args.no_color, text_mode=True)
loop = 1
while loop:
try:
loop = 0
t.cmdloop()
except KeyboardInterrupt:
print("\n<ctrl-c>")
loop = 1
except Exception as exc:
if str(exc) == 'quit':
break
print(exc)
loop = 1
elif args.batch_execution:
if (lf != ''):
print('"-l" option is not supported in this mode.')

View File

@@ -1,246 +0,0 @@
try:
import readline
except:
pass
from cmd import Cmd
import os
import sys
from yaml import load, Loader
import functools
import platform
import types
import inspect
# test modules
from interpreter.utils.test_init import (
env_init, prepare_global, set_standard_gd_keys,
update_global, test_run_init, test_run_header, load_test)
from interpreter.utils.globdict import (global_dict)
import libs.testium as tm
from interpreter.utils.constants import TestItemType as cst
from interpreter.test_report.test_report import TestReport
class FakeQueue:
def put(self, arg):
pass
def func(self, args):
if not args.startswith("{"):
args = "{"+args+"}"
y = load(args, Loader)
obj = self.current_item(y, status_queue=FakeQueue())
obj.report = self.report
res = obj.execute()
if not (res.value is None):
print('result : {}'.format(res.value))
print(res.test_result)
class Terminal(Cmd):
SUPPORTED_TESTS = [
cst.TYPE_SLEEP,
cst.TYPE_LET,
cst.TYPE_PY_FUNCTION,
cst.TYPE_LUA_FUNCTION,
cst.TYPE_CONSOLE,
cst.TYPE_IMAGE_DLG,
cst.TYPE_MESSAGE_DLG,
cst.TYPE_QUESTION_DLG,
cst.TYPE_VALUE_DLG,
]
SUPPORTED_GROUPS = [
cst.TYPE_GROUP,
cst.TYPE_CYCLE
]
def __init__(self, working_dir, config_files, defines, no_color, text_mode=False):
super().__init__()
self.working_dir = working_dir
self.config_files = config_files
self.current_item = None
report = TestReport(None)
self.report = report
env_init()
prepare_global()
# Define the builtin variables
set_standard_gd_keys("Unnamed", self.working_dir, '', config_files)
update_global([], defines)
if text_mode:
tm.setgd("_text_mode", True)
# creation of the functions
for tst in self.SUPPORTED_TESTS:
meth_name = "do_" + tst.item_cmd
# copy of the function
f = types.FunctionType(func.__code__, func.__globals__, name=meth_name,
argdefs=func.__defaults__,
closure=func.__closure__)
f = functools.update_wrapper(f, func)
f.__kwdefaults__ = func.__kwdefaults__
f.__doc__ = tst.item_class.__doc__
setattr(self, meth_name, types.MethodType(f, self))
test_run_init()
self.prompt = "(testium)~ "
# display header
print(test_run_header())
# redirect output
if 'Linux' in platform.system() and not no_color:
from lib.stdout_redirect import stdio_redir
try:
from interpreter.utils.termlog import TermLog
stdio_redir.redirect(TermLog(sys.stdout))
except ModuleNotFoundError:
tm.print_info('Colored console not supported by the system.' +
' If you want it, please install colorama module')
def precmd(self, line: str) -> str:
c = line.split(" ", 1)[0].strip()
self.current_item = None
for tst in self.SUPPORTED_TESTS:
if c == tst.item_cmd:
self.current_item = tst.item_class
break
return line
def load_test_recursively(self, tree_parent, parent_seq, status_queue):
try:
parent_seq_name = parent_seq['name']
except KeyError:
parent_seq['name'] = "sequence"
except TypeError:
raise Exception("Syntax error in an item of type {} which is a child of {}".format(
tree_parent.type(), tree_parent.parent().name()))
try:
parent_seq_actions = parent_seq['steps']
except KeyError:
raise Exception(' No action list found for "%s" sequence'
% (parent_seq_name))
# if action is a dictionary , we assume it is a single action
# that has not been nested in a list, so do it
if isinstance(parent_seq_actions, (dict)):
parent_seq_actions = [parent_seq_actions]
if not isinstance(parent_seq_actions, (list, tuple)):
raise Exception('Actions list not valid.')
# first we merged to the same level 'sequence dict entries and list within the list
counter = 0
test_dir = tm.gd('test_directory')
while (counter < len(parent_seq_actions)):
action = parent_seq_actions[counter]
# if action is a list raise up to the the same level,
# ie insert action element into the parent_seq_actions
if isinstance(action, (list, tuple)):
parent_seq_actions[counter:counter+1] = action
continue
# if action is a NoneType skip and continue
# (when pointing to an unused alias for instance)
if action is None:
counter += 1
continue
# if action is a sequence we insert its entry into the action list
if 'sequence' in action:
parent_seq_actions[counter:counter+1] = action['sequence']
continue
else:
executed = False
for it in [*self.SUPPORTED_TESTS, *self.SUPPORTED_GROUPS]:
if it.item_cmd in action:
executed = True
item = (it.item_class)(action[it.item_cmd],
tree_parent,
status_queue)
# check for sequence type:
if it.item_cmd == cst.TYPE_UNITTEST.item_cmd:
item.setTestDir(test_dir)
item.load()
elif ((it.item_cmd == cst.TYPE_CYCLE.item_cmd) or
(it.item_cmd == cst.TYPE_GROUP.item_cmd)):
self.load_test_recursively(
item, action[it.item_cmd], status_queue)
if not executed:
raise Exception('action type is not known "{}"'.format(
list(action.keys())[0]))
counter += 1
def __setReportRecursively(self, parent):
for i in range(parent.childCount()):
parent.child(i).report = self.report
self.__setReportRecursively(parent.child(i))
def setReport(self, root_item):
root_item.report = self.report
self.__setReportRecursively(root_item)
def get_names(self):
memb = inspect.getmembers(self)
return [n[0] for n in memb if (inspect.ismethod(n[1]) and n[0].startswith("do_"))]
def do_load(self, args):
"""load function.
This function loads and executes a testium sub-script.
The loaded sequence can't be a main testium script ("testium -b" option is
defined for such a usage).
Accepted files are with extension "*.tum".
usage:
load path/to/my/sequence.tum
"""
file = args.strip()
suff = file[-4:]
if not suff in ['.tum']:
raise Exception('Wrong input file extension')
if not (os.path.exists(file) and os.path.isfile(file)):
raise Exception(
'"{}" does not exist or is not a file.'.format(file))
d, _ = load_test(file)
if not isinstance(d, list):
raise Exception(
"The file root object must be a list. A \"main\" tum can't be loaded from here (use batch mode instead).")
if (len(d) == 1) and isinstance(d[0], dict) and (not d[0].get('sequence', None) is None):
d = d[0]['sequence']
sq = FakeQueue()
root_item = (cst.TYPE_ROOT.item_class)(
dict_item={'steps': d}, status_queue=sq)
self.load_test_recursively(root_item, {'steps': d}, sq)
self.setReport(root_item)
res = root_item.execute()
if not (res.value is None):
print('"{}" execution overall result: {}'.format(file, res.value))
print(res.test_result)
def do_gd(self, args):
"""Variables lists and values.
usage:
gd
gd home
"""
if args != '':
res = tm.gd(args, None)
if res is None:
raise Exception(
'the variable: "{}" has not been found.'.format(args))
print(res)
return
for k in global_dict.keys():
print('{}: {}'.format(str(k), str(global_dict[k])))
def do_quit(self, args):
'''Quit the application.'''
raise Exception('quit')

View File

@@ -0,0 +1,37 @@
from interpreter.test_items.test_item import TestItem, test_run
from interpreter.test_items.test_result import TestResult, TestValue
class TestItemContainer(TestItem):
"""Base class for items that run a sequence of children sequentially."""
def __init__(self, item_type, dict_item, parent=None, status_queue=None, filename=""):
self._name = item_type.item_name
super().__init__(dict_item, parent, status_queue, filename=filename)
self._type = item_type
self.is_container = True
def _run_children_sequentially(self):
"""Execute all children in order, respecting stop_on_failure and stop requests.
Returns a TestResult aggregating all children outcomes."""
i = 0
to_be_stopped = False
while not self.isStopped() and i < self.childCount() and not to_be_stopped:
result = self.child(i).execute()
if result.test_result == TestValue.FAILURE and self._stop_on_failure:
to_be_stopped = True
i += 1
if self.isStopped() or to_be_stopped:
for j in range(self.childCount()):
if self.child(j).executedOnStop() and j >= i:
self.child(j).execute()
success = TestValue.SUCCESS
for j in range(i):
if self.child(j).result.test_result == TestValue.FAILURE:
success = TestValue.FAILURE
break
stopped = self.isStopped() or to_be_stopped
return TestResult(None, success, ""), stopped

View File

@@ -0,0 +1,175 @@
import threading
from time import sleep, time
from interpreter.test_items.test_item_container import TestItemContainer
from interpreter.test_items.test_item import test_run
from interpreter.test_items.test_result import TestResult, TestValue
from interpreter.utils.constants import TestItemType as cst
from interpreter.utils.eval import eval_to_boolean
from lib.tum_except import ETUMSyntaxError
from lib.string_queue import StringQueue
from lib.stdout_redirect import stdio_redir
class TestItemParallelBranch(TestItemContainer):
"""One branch of a parallel item. Runs its children sequentially,
optionally waiting for a condition before starting."""
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
super().__init__(cst.TYPE_PARALLEL_BRANCH, dict_item, parent, status_queue, filename=filename)
self._wait_condition = None
self._wait_timeout = 30
if "wait_for" in dict_item:
wf = dict_item["wait_for"]
if not isinstance(wf, dict):
raise ETUMSyntaxError(
f"'wait_for' in branch '{self.name()}' must be a dict with 'condition' and optional 'timeout'",
self.seqFilename(),
)
self._wait_condition = wf.get("condition", None)
self._wait_timeout = float(wf.get("timeout", 30))
def _wait_start(self):
"""Block until wait_for condition is True, or timeout. Returns False on timeout."""
if self._wait_condition is None:
return True
deadline = time() + self._wait_timeout
while time() < deadline:
if self.isStopped():
return False
try:
c = self._prms.expanse(self._wait_condition)
if eval_to_boolean(c):
return True
except Exception:
pass
sleep(0.1)
return False
@test_run
def execute(self):
if not self._wait_start():
self.result.set(
TestValue.FAILURE,
f"wait_for timeout ({self._wait_timeout}s): condition '{self._wait_condition}' not met",
)
return
result, stopped = self._run_children_sequentially()
if stopped:
if result.test_result == TestValue.FAILURE:
self.result.set(TestValue.FAILURE, "Branch aborted on failure")
else:
self.result.set(TestValue.NORUN, "Branch aborted on user request")
else:
self.result.set(result.test_result, "")
class TestItemParallel(TestItemContainer):
"""Runs multiple branches concurrently.
YAML:
parallel:
name: ...
sync: all # all (default): wait for every branch
# any: stop as soon as one branch finishes
stop_on_failure: false
branches:
- name: Branch A
wait_for:
condition: "'$(ready)' == 'True'"
timeout: 30
steps:
- ...
- name: Branch B
steps:
- ...
"""
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
branches = dict_item.get("branches", [])
if not branches:
raise ETUMSyntaxError(
f"'parallel' item requires at least one branch in 'branches'",
dict_item.get("seq_filename", ""),
)
# Inject a synthetic 'steps' key so load_test_recursively can load branches
# as TestItemParallelBranch children. The original 'branches' key is kept
# for display in the GUI (it's included by _filter_dict_item, 'steps' is not).
dict_item["steps"] = [{"parallel_branch": b} for b in branches]
super().__init__(cst.TYPE_PARALLEL, dict_item, parent, status_queue, filename=filename)
self._sync = str(dict_item.get("sync", "all")).lower()
if self._sync not in ("all", "any"):
raise ETUMSyntaxError(
f"'sync' must be 'all' or 'any', got '{self._sync}'",
self.seqFilename(),
)
def _stop_branch_recursively(self, item):
item.stop()
for i in range(item.childCount()):
self._stop_branch_recursively(item.child(i))
@test_run
def execute(self):
branch_results = [None] * self.childCount()
any_done = threading.Event()
def run_branch(idx):
branch = self.child(idx)
stdio_redir.register_thread(buffer=StringQueue(), branch=branch.name())
try:
# sync:any: if another branch already won the race, mark this
# branch as stopped so its execute() skips children but still
# goes through the normal addTest path (clean DB entry).
if self._sync == "any" and any_done.is_set():
branch.stop()
try:
result = branch.execute()
except Exception as e:
import traceback
print(f"[parallel] Branch '{branch.name()}' crashed: {e}")
traceback.print_exc()
branch.result.set(TestValue.FAILURE, f"Branch crashed: {e}")
result = branch.result
branch_results[idx] = result
# Only a branch that actually ran (SUCCESS or FAILURE) wins the
# sync:any race. A disabled or skipped branch returns NORUN
# almost instantly and must not stop legitimate branches.
if self._sync == "any" and result.test_result != TestValue.NORUN:
any_done.set()
for j in range(self.childCount()):
if j != idx:
self._stop_branch_recursively(self.child(j))
finally:
stdio_redir.unregister_thread()
threads = [
threading.Thread(target=run_branch, args=(i,), daemon=True)
for i in range(self.childCount())
]
for t in threads:
t.start()
for t in threads:
t.join()
if self._sync == "all":
# Pass if no branch failed; disabled/skipped branches (NORUN) are
# ignored, matching how Group/Cycle treat disabled children.
success = all(
r is not None and r.test_result != TestValue.FAILURE
for r in branch_results
)
else:
# Pass if at least one branch ran and succeeded.
success = any(
r is not None and r.test_result == TestValue.SUCCESS
for r in branch_results
)
self.result.set(
TestValue.SUCCESS if success else TestValue.FAILURE,
f"parallel sync={self._sync}",
)

View File

@@ -76,5 +76,8 @@ class TestItemSleep(TestItem):
else:
if not isinstance(timeout, (int, float)):
raise ETUMRuntimeError(f"Timeout value of sleep test item \"{self.name}\" is not valid: \"{timeout}\".")
sleep(timeout)
import time as _time
end_time = _time.time() + float(timeout)
while _time.time() < end_time and not self._is_stopped:
sleep(min(0.05, end_time - _time.time()))
self.result.set(TestValue.SUCCESS, 'Sleep %s sec' % (str(timeout)))

View File

@@ -1,4 +1,5 @@
import os
import threading
from functools import wraps
import sqlite3
from time import (time, sleep)
@@ -143,6 +144,7 @@ class TestReport:
self._level = 0
self._log_stored = False
self._con = None
self._lock = threading.Lock()
if dict_report is None:
self._active = False
@@ -231,7 +233,7 @@ class TestReport:
prepare_file_to_save(rep_path)
if not os.path.exists(os.path.dirname(rep_path)):
raise ETUMRuntimeError("Report path does not exist: " + rep_path)
self._con = sqlite3.connect(rep_path)
self._con = sqlite3.connect(rep_path, check_same_thread=False)
self.createHeader(header)
self.createTestTable()
self._con.commit()
@@ -334,7 +336,8 @@ class TestReport:
req = req + '?,'
req = req[:-1] + ')'
self._con.execute(req, param)
with self._lock:
self._con.execute(req, param)
def incLevel(self):
self._level = self._level + 1

View File

@@ -33,6 +33,8 @@ class TestItemType(Enum):
TYPE_RUN = TestItemEnum("run", "Run tum")
TYPE_JSON_RPC = TestItemEnum("json_rpc", "JSON-RPC")
TYPE_JSON_RPC_ACTION = TestItemEnum("json_rpc_action", "JSON-RPC action")
TYPE_PARALLEL = TestItemEnum("parallel", "Parallel")
TYPE_PARALLEL_BRANCH = TestItemEnum("parallel_branch", "Parallel branch")
TYPE_ROOT = TestItemEnum("default", "default")
@staticmethod

View File

@@ -44,6 +44,7 @@ from interpreter.test_items.test_item_choices_dialog import TestItemChoicesDialo
from interpreter.test_items.test_item_console import TestItemConsole
from interpreter.test_items.test_item_run import TestItemRun
from interpreter.test_items.test_item_report import TestItemReport
from interpreter.test_items.test_item_parallel import TestItemParallel, TestItemParallelBranch
def _constants_init():
@@ -69,6 +70,8 @@ def _constants_init():
cst.TYPE_SLEEP.item_class = TestItemSleep
cst.TYPE_UNITTEST.item_class = TestItemUnittestFile
cst.TYPE_VALUE_DLG.item_class = TestItemValueDialog
cst.TYPE_PARALLEL.item_class = TestItemParallel
cst.TYPE_PARALLEL_BRANCH.item_class = TestItemParallelBranch
def locate_report_file(rep_file):

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -45,6 +45,7 @@
<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>
<file alias="parallel.png">black/parallel.png</file>
</qresource>
<qresource prefix="/white">
<file alias="testium_logo.png">testium_logo.png</file>
@@ -92,6 +93,7 @@
<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>
<file alias="parallel.png">white/parallel.png</file>
</qresource>
<qresource prefix="/color">
<file alias="testium_logo.png">testium_logo.png</file>
@@ -139,5 +141,6 @@
<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>
<file alias="parallel.png">color/parallel.png</file>
</qresource>
</RCC>

View File

@@ -4261,6 +4261,38 @@ tEXtdate:timesta\
mp\x002026-01-05T17\
:43:41+00:00l1\xd2\xbe\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x01\xe0\
\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\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x01\x95IDATx\x9c\xed\xd91N\
\xc30\x14\x06\xe0\xff\xbd\xe6\x08\x15b`\xa6\xdd\xb8B\
'\x06\x0e\xd0\x93p\x0cn\xc0V\xd1\xa1\x82\x89\x09\xb1\
\xc0\x05:\xb0\x01\xdd\x10\x13g@\xfa\x91\xd5T\x02\x11\
\x1a'5q\xe0\xbdO\x8a\x22\xd5\xfec\xe7\xd9\x89\x22\
\x15p\xce9\xe7\xcc\x92]\xc2$O\x00\xec\x01xm\
y\x89\x83\xf2\xdc6\xbf\x0f\xe0MDn;/\x00\xc9\
G\x00#\xf4\xc3\x93\x88\x8c\xdb\x04u\x87\x95\xef\xcb\xcd\
\x07\xa3rN\xdd\x14\x00\xc0\x10\xfd3\xec\xb2\x00/\xe8\
\x9fVs*\x12N\xe0\x14\xc0Cd\xdf#\x00g\x89\
\xf3\xc8]\x80\xa5\x88\xdc\xc7t$\xf9\xfe\x0byt\xf9\
\x08\xfc\x1b\x0a\xe3\x14\xc6)\x8cS\x18\xa70Na\x9c\
\xc28\x85q\x0a\xe3\x14\xc6)\x8cS\x18\xa70Na\
\x9c\xc28\x85q\x0a\xe3\x14\xc6)\x8cS\x18\xa70N\
a\x9c\xc2\xb8\xa2\xae\x03\xc9\xc3p\x16\x91\xe7&\x17\xde\
\xe46r\xe5\xebrZs\x91\xf3\xf0\xd7s8H\xce\
H\xc6\x14\xac\x08}7\xb9\xdc\xf9\xf2\x1e\x9a#9\xe6\
w\x8br\x80IE\xdb\xa4l\x0b}~\x92+?N\
\xf5\x0e\x98\x02\x98\x03\x18T\xb4\x0d\xca\xb6i\x8f\xf3\xcd\
p\xbd\xed\xaa\xdcE\xfe\xc6\x1e\xe4g\xbb\x14`@\xf2\
\x82\xed]\x91\x9cg\xcc_\xc6\xbc7\xb6\x8ax.\xeb\
\x9e\xd7\xacy\xa4\xc0\xe6;\xe1K\xe5s\xe7S\x15\xa1\
\x88\x5c\x89\xca\xca\xe7\xcewU\x84\xc5\xb6\xc1s\xe7\x93\
\xe0z;^W\x0c~\x13\xf9\xa1\x925\x9f\x04\xd7+\
\xb1\xfa4\xf8\xaa\xc9\xe0\xb9\xf3\xc9\x90<\x0e\xc7_\xcd\
;\xe7\x9cs\xce9\xe7\x1c\xac\xfa\x00r^\xb7Q\xcd\
\x1e\x12J\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03\xda\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -9314,6 +9346,49 @@ D\x90\xfcw&\xd3\xf3\xb6\xb0\x1ex\x19!\xf2\x03\xc0\
\xdf\x00\x0e\xb8\xdcf\x8b\x11#F\x8c\x181bD\x1c\
\x7f\x01K7\xae\xf7\x8d\x18\xc1Z\x00\x00\x00\x00IE\
ND\xaeB`\x82\
\x00\x00\x02\x8e\
\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\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x02CIDATx\x9c\xed\xdaM\x8b\
\xd3@\x18\x07\xf0\xff\xf3\xb4~\x82.R:\x22\x82d\
6\xb2\x14\xbf\x80\x87z\xf1\xe0\xcb\xc5\xc3\x82\xee\xc5\x0f\
\xe1\xc7\xf0\x1b\xecI\xc5\x8b(z\xd5\x8b=x\xf5\xe0\
\xc9l\xd8C\x11\x09E\xec\xa9'a\x93\x91YZ\x08\
\xddm\xf3\xd2\xd0\x89<\xf3\x83PH\xe6?3y\x9a\
&\x94\x0c\xe0y\x9e\xe7yb\xd16a\xad\xf5}c\
\xccU\x22\xfaU'o\x8c\xb9v>\x89\x9ay\x22\xea\
\x13\xd1\xef(\x8a>\xd7\xc9oU\x00\xadu\x04`\x1f\
\xedp\x12\xc7qX'\xc8u\xbf\xf9\x16\x9d\xbc\xb5\xbf\
\x98\xd3n\x0a\x00`\x0f\xed\xb3\xb7\xb3\x020\xf3O\xb4\
L\xdd9u\x1b\x9c\xc3s\x00\xdfK\xb6\xbd\x0d\xe0E\
\xc3y8-\x003\x7f\x8b\xa2h\x5c\xa6m\x18\x86g\
Y\x965\x9a\xaf\x8b!\x1cC8\x86p\x0c\xe1\x18\xc2\
1\x84c\x08\xc7\x10\x8e!\x1cC8\x86p\x0c\xe1\x18\
\xc21\x84c\x08\xc7\x10\x8e!\x1cC8\x86p\x0c\xe1\
\x18\xc21\x84c\x08\xc7\x10\xae[\xd4 \x0cCm?\
\xa3(\x8a\xabt\xbc\xcc-\xb9\xca\x17\xe5x\xd3A\xad\
\xf5q\x96e'v\xd3Z\xbf\x1a\x8dF\x85\x05\xb3m\
l\xdbe\xceu\xde\x9eC\xad\xf5\x01Zk\xfb\xbe\xfd\
\xc7\xca\xee\xb7\x83\xc1\xe0h:\x9d\xde\xc9\xb2\xecK\xfe\
\x003\xdf\xed\xf7\xfb_\x93$y\x03\xe0pM\xb7\xae\
\xf2\xb7\xe28\xb6\xeb\x19\xb6\xbe\x07\x1c\xda\x01\x98\xb9s\
\xa1#\xe6N\xc1\xe4\xdb\x90\xbf\xa0\xb3\xee\xc0l6\xfb\
\xd3\xeb\xf5n.\xde\xc4\xe6\x1d\xa4iz\x9d\x88n\xe4\
w.\xf6=(1\xe6\xae\xf3\xaf\xe38>\xae\xbbD\
\xa6\x13\x04\xc1K\x22:B\x0d\xc6\x98\xf7\x00\xfe\x12\xd1\
SG\xf9wJ\xa9'\xe3\xf1\xf8l]\x9bnA\x1f\
\xa9R\xeaY\x92$W\xaa^Z\xf6\xf7\xaa\x94:/\
\x5c\x92$]W\xf9M'_e\x91T\xa5+\xe1\x92\
\xca\xbb\xce\xaf\xc5e:\x5c^\x09\xb6\xaa%+\xbf:\
\xb8\xeb|\xf5\x9b\xe0\xaa\xc9d\x92\x0d\x87\xc3\x8f\xf3\xf9\
\xdc>\x1e\x0f\xb0\xe11u\xd9\xe0\xae\xf3M\xae\x13\xec\
\x04A\xf0\x81\x88\x1e\xe5w\x1ac>)\xa5\x1e\x96\x18\
\xdcu~\xeb\xff\x02\xa9R\xea\xb11\xe647\xf8i\
\x85\xc1]\xe7\x9b\x13\x86\xe1=\xbb\xfd\xafy\xcf\xf3<\
\xcf\xf3<\xcf\xf3<H\xf5\x0f(\x04\x96V\xf2\xca\xe5\
\xb8\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03c\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -14847,6 +14922,84 @@ T\xc3\xaf\x11t\xff \xa2=\x80P\x96\xc0\x89\xa4\x89\
\x06\xa0\xaa\xf9\x86\x00\xa8l\xfe\xaf\x00T7\x1f\xa8\xf4\
-;\x01\x91Hy\xfd\x02\xd0L\xcf\xbd_\xc5\x18J\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04\xb8\
\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\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x04mIDATx\x9c\xed\x9a]H\
[W\x00\xc7\xff\xe7\xdch\xd2\x99f\xadM4jg\
\x92\xf5\xc3\x86\xd2\x06\xf60\x18\xf8\xd0\xbc\xf8`\x1d{\
2Te\xe8kI\xcb\x1ef\x1f6\x18\xcb\xd3(\xcc\
\x15\xfa\xd0'\x19C\x87\x8cE\x8b-c\x1f\xae\x0f\xf1\
\xc1\xa7\xb1\x96\x15:jl\xa7\x89\xab\x1f\xab\xae\xab&\
\x99QL\xce8\xd7Er\xcd\x92\xdc{\x03\x22;\xf7\
\x07\x87p\xcf=\xbfs\xee\xff\xdes\xce\x0d$\x80\x81\
\x81\x81\x81\x81\x81\xb0\x90J\xe4w\xaf}\xdd^S\xb5\
V\xe7\xac^|\xa6\xc7_xu\xeb8\xfflZ\xab\
\xd6\xe5\x9f\xac\x8a;k\xe9_\xcf\xdb?\xb8\xf3#\xf6\
\xfb\x06\x5c\xfepp\xba\xbez\xa1\x05\x07\x00\x8f\xf4{\
\xb4\xf7\xa3\xcf\xcf\xe8q\xa9\x1e\xa9\xfb\xdaX\xfbA\x09\
\xcf\x99\xcb\xbc\xd6\xf2\xcd'\xef\xb4\xef\xdb\x0d\xd8\xc0+\
v\x1c0l4e\xdf\xb7\x1b0\xb7\xe1\x9d\x8f\xa5O\
\xe1\xa0p\xaej\x1aoH\x8f\xe6\xf5\xb8&\xbd\x83>\
H\xbc\x85'\x7f{q\x88n\xc8\xc7M\x96x\xbf\xc7\
2\xf3P\x8d\x1b\xadO\xfb\xe2\xb5\xe9\x81\xfc:\xd7\x0b\
K\x7f\xcb\x1f\x16U\xbe\xaf\xea\x91\xef\x94)6@\xc1\
`%)\x1c\xa5k\x00\xd3\x97\xc3\x84\x0aHd\x8e\xc8\
\x85\xb3\xba\xddx\x7f\xe8z\xf7\xa4\x1a\xcf\xfd\xfd\x95m\
B\x94W\xbch\xdb\xbe\xff\xd5\xe5\xeb\xaa\xfc\xc4\xa7\xce\
m\x90\x8a^`\x95-\x81\xff\x13\x14\x82C!8\x14\
\x82C!8\x14\x82C!8\x14\x82C!8\x14\x82\
C!8\x14\x82C!8\x14\x82C!8\x14\x82C\
!8\x14\x82C!8\x14\x82C!8\x14\x82C!\
8\x14\x82c*\xd7\xc0\x17\xfc\xed4\xff|x\xeb\xc4\
\x8c\x96\x8es^\xa2\xfd\x86|<{\xf1\x96&\xff\xf5\
o\x83\xb2\xff\xcb\xf4\x1d\xf9\xd8\xf6\xfe\x82&\x7f\xfd\xb3\
\xa6\xd3j<Z\xea\xe4\xf9\xe0\xec \x03\x89\xf2r>\
8;|!\x14){\xc3x\x1b\xde\x96;\x1b\xbe{\
Q&A.\x9e\x1f\x82\xc3\x17\x22\xa1\xf2~$d\xe2\
m\xb9\xf3\xf1\xf2\x83(aY\xb9\xac\x0f4\x0c\xb3P\
\xf9\x07\xc6\xdb\xf0\xb69/1\xd00X\xaa=)v\
\xe2\xdc\x95\xb93\x84\xb1\xc7\x8a\xce\x81\xf01G\xbc\xe7\
\xe5\xaa\xbb5\xcbX$\xff\x1c%\xc4\x7f\xc4\x1e\x9b\xfa\
s\xc55B\x80@\xd6\xfa\x02)\xff\x17{\xbb\x0d\xbb\
\xcc\x8e\x9eXz\xb5\x95\x10\xa5\xcf\x18\xf1\xbb-\xf6\xa9\
\xf8\xe6\xca\x08\x80\x80g3\x81\xbbO\xee\xed\xbd\xdc\xb0\
5\xb9\xd8\x93\xacq\xb6\x82\x10\x85\x0f\xc6\xfc\xd6\xd4\xf2\
T\xd2\xda8\x02\xb0\x80\xf2\xba\x89\xd7\xd6\xbf8]\xf1\
\x1e@\x80\x80\x1c\x90f\xa5\x82s4+\xe5\xc2\x97\xe8\
\x22\xc0\x03R\xd0\x02\x9f\xd7\xe5\xc2\x17\xd7Y\x80\x07$\
YV8~\x96I\xff\x15\xbe\x1cR\xb1\x13\xcf\x7f\xba\
\xb9Z\xff\xe6{'\xf8rV\x0c\x04\x9c\xcd2\xd2L\
\x00w~\xfd\xbfu\x17w\xdbm\x1d\x02\xabYC\xd6\
\xb6\xb2\xb7\xeb\xb3 \xac\x99\xffF\xaaL \xd7\xed\xfa\
/Mf\x1c\xdfJ\xa1%\xbdV\xe8SR\xe8\xef\xd4\
\xed\xfa9\x18\xf0\xa5\xad\x7f\xa9\xe82 (Ag'\
\x93f\xea\xe6\x86\x18\xd0\x03=\x90\xcc\xed\xf5\x8e\x9b\x9b\
\x04\xac[\x8fnB\xf6\xf6\xcf\xbf\xde\xdd\xa4L\x9f\x0f\
`\xcc\x9a\x5c\xea\x22!l\x17k@K\xd9\xa3\xa3$\
s\xd4\x11\xef\xe3k_\xeb\xc8\xdc\xa9\xb5?\xbb\xe46\
\xdb{\xa1\xc3\xe7N\x93\xb9\xfe\x92-\xb1\xd8\xcb\xd7\xbe\
v\x9d\xef\x17\xa5\xc3sT\xfd\xc8\xaec&\x8c\xd5:\
\xe2]\x93!\xff\xce\xe0\xe1N\xc9s\xb8n\x08D\xa5\
\xcf0\xe6\xb28\xba&\xfd!\xd9gaH\xc9\xf9\x86\
!h\x18_Mx\xd5\x9b\xa0\x96\x99 ?\xf9\xfc\xf0\
\x9c\xc0h\xc6eq\xf4\xa9\x9c\x09\xe1\xfc\xf0\x1c\x12@\
\xc6\x9a\x5c\xeaS7\x13\xd4=\xf9\xdd\xd6\xd0\x00\x7f\xc7\
\x97\xda\xe9s\xafIE\xf8|?\x122\x95\xd9\xe9\xe5\
\xd7d~xE\xff!\x98J\xef\xf4;\xafI\xb5\xe1\
9\x9a\xffg\xc2\x97C\xb4nn\x1c\xc0\xdb\x8a\x13\x84\
L\xd4\xdac\x1d\xc5\xc2\xef\xb2\xb3\x1c\xc6A\xf6\xf8 \
\x13.\xb3\xbd\xa3X\xf8\x1c|9\xa4\xe6\x9d\xe3\x0cD\
\xe1\x13`\xa2&\xb9\xd4\xa1%<G\xd7\x1fmvf\
B\xf3c\x02r\x92\x1f3\xb0\xa7\xc7\x1c\xf3\xde\xb2\xe1\
\x953\x81\x7f\xc9\x92}\x00O]f\x87\xb7\x5c\xf8\xfc\
\x99\x90\xb0:\x15\xe3\x1fN.{\xb5\x86\xaf\x18\xdf\xd5\
\xd96^\xf4\xfa\x9e\xef\xae\xb6\xf1\xa2\xd7O\xdehl\
\xe3E\xafo```````````\x00\
Q\xf9\x07\x0c\xad\xc2\xa6\x1c\xa8D\x90\x00\x00\x00\x00I\
END\xaeB`\x82\
\x00\x00\x06S\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -16214,6 +16367,10 @@ qt_resource_name = b"\
\x06\xc7\x98g\
\x00a\
\x00b\x00o\x00u\x00t\x00.\x00p\x00n\x00g\
\x00\x0c\
\x0cnG'\
\x00p\
\x00a\x00r\x00a\x00l\x00l\x00e\x00l\x00.\x00p\x00n\x00g\
\x00\x0e\
\x0d\x8b9\xe7\
\x00e\
@@ -16272,282 +16429,288 @@ 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\x00^\
\x00\x00\x00\x10\x00\x02\x00\x00\x00.\x00\x00\x00`\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00-\x00\x00\x001\
\x00\x00\x00\x00\x00\x02\x00\x00\x00.\x00\x00\x002\
\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\x00.\x00\x00\x00\x04\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02\xa8\x00\x00\x00\x00\x00\x01\x00\x00}\x1e\
\x00\x00\x01\x9b\x8fA\xee\x1e\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x00\xfa\xe1\
\x00\x00\x01\x9b\x8fA\xee\x95\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x09\xb0\
\x00\x00\x01\x9b\x8fA\xee\xd5\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x1ds\
\x00\x00\x01\x9b\x8fA\xee\xb9\
\x00\x00\x04\x22\x00\x00\x00\x00\x00\x01\x00\x01\x1d-\
\x00\x00\x01\x9b\x8fA\xee\x10\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x04@\x00\x00\x00\x00\x00\x01\x00\x01\x1f\x11\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x00O\x0d\
\x00\x00\x01\x9b\x8fA\xee\xc7\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00`\x90\
\x00\x00\x01\x9b\x8fA\xee\x9c\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x83\xfa\
\x00\x00\x01\x9b\x8f'M\xbb\
\x00\x00\x04Z\x00\x00\x00\x00\x00\x01\x00\x01#\x9e\
\x00\x00\x01\x9b\x8fA\xeeD\
\x00\x00\x01\x9b\x97*\xf4\x02\
\x00\x00\x04x\x00\x00\x00\x00\x00\x01\x00\x01%\x82\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00w]\
\x00\x00\x01\x9b\x8fA\xee\x7f\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x00:\x88\
\x00\x00\x01\x9b\x8fA\xee\xce\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x80i\
\x00\x00\x01\x9b\x8fA\xee\xa3\
\x00\x00\x04\xc6\x00\x00\x00\x00\x00\x01\x00\x018\xab\
\x00\x00\x01\x9b\x8fA\xeei\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x04\xe4\x00\x00\x00\x00\x00\x01\x00\x01:\x8f\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x03X\x00\x00\x00\x00\x00\x01\x00\x01\x00\x89\
\x00\x00\x01\x9b\x8fA\xee\x09\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x002\xd8\
\x00\x00\x01\x9b\x8fA\xeeb\
\x00\x00\x04\xae\x00\x00\x00\x00\x00\x01\x00\x013\xc6\
\x00\x00\x01\x9b\x8fA\xeex\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x04\xcc\x00\x00\x00\x00\x00\x01\x00\x015\xaa\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x00\x5c\x04\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x00T\x8d\
\x00\x00\x01\x9b\x8fA\xef\x07\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x04\x96\x00\x00\x00\x00\x00\x01\x00\x01-P\
\x00\x00\x01\x9b\x8fA\xeeS\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xb4\x00\x00\x00\x00\x00\x01\x00\x01/4\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x00\x07&\
\x00\x00\x01\x9b\x8fA\xee\xeb\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x15\xcd\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x03\xe2\x00\x00\x00\x00\x00\x01\x00\x01\x16\xae\
\x00\x00\x01\x9b\x8fA\xee\x8d\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x01\x18\x92\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x00o\x9a\
\x00\x00\x01\x9b\x8fA\xef\x00\
\x00\x00\x03\xaa\x00\x00\x00\x00\x00\x01\x00\x01\x0eL\
\x00\x00\x01\x9b\x8fA\xee&\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x01\x100\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x03\x0c\x00\x00\x00\x00\x00\x01\x00\x00\xf4\xc5\
\x00\x00\x01\x9b\x8fA\xee\xab\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x00>\x99\
\x00\x00\x01\x9d\xce^\x88S\
\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x01\x09h\
\x00\x00\x01\x9b\x8fA\xee\x17\
\x00\x00\x01\x9d\xcf\xc3\xa3\x15\
\x00\x00\x03\xb0\x00\x00\x00\x00\x00\x01\x00\x01\x0bL\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x00\x0fl\
\x00\x00\x01\x9b\xbd8B\xc4\
\x00\x00\x01\x9b\xc5\xbd\x83\x1b\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00+s\
\x00\x00\x01\x9b\x8fA\xeeK\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x00\xf7Q\
\x00\x00\x01\x9b\x8fA\xee\xf9\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x00!6\
\x00\x00\x01\x9b\x8fA\xee\xf2\
\x00\x00\x04B\x00\x00\x00\x00\x00\x01\x00\x01!\x0a\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x04`\x00\x00\x00\x00\x00\x01\x00\x01\x22\xee\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00#\xa7\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x04~\x00\x00\x00\x00\x00\x01\x00\x01'\xaa\
\x00\x00\x01\x9b\x8fA\xee\xe4\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x9c\x00\x00\x00\x00\x00\x01\x00\x01)\x8e\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x022\x00\x00\x00\x00\x00\x01\x00\x00k\xfc\
\x00\x00\x01\x9b\x8fA\xee\xdd\
\x00\x00\x01\x9b\x97*\xf4\x06\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x00H\xe3\
\x00\x00\x01\x9b\x8fA\xeeq\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x00e#\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x00Y~\
\x00\x00\x01\x9b\x8fA\xee\xb2\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x01\x05\x8a\
\x00\x00\x01\x9b\x8fA\xee4\
\x00\x00\x01\x9d\xd7\xdb\x89<\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x00Y~\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x03\x8e\x00\x00\x00\x00\x00\x01\x00\x01\x07n\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x00.\x86\
\x00\x00\x01\x9b\x8fA\xeeZ\
\x00\x00\x01\x9b\x97*\xf4\x04\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x006\xc0\
\x00\x00\x01\x9b\x8fA\xee;\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x00r\x15\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x01\x11w\
\x00\x00\x01\x9b\x8fA\xee\xc0\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x01\x19\xbb\
\x00\x00\x01\x9b\x8fA\xee-\
\x00\x00\x02\xa8\x00\x00\x00\x00\x00\x01\x00\x03\x06\x16\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x03\x88)\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x02x\xfe\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x8fq\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x04\x22\x00\x00\x00\x00\x00\x01\x00\x03\xb2\x94\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x02\xd0\x09\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x02\xe1K\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x03\x107\
\x00\x00\x01\x9b\x8f'M\xbb\
\x00\x00\x04Z\x00\x00\x00\x00\x00\x01\x00\x03\xba\xf2\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x03\x00\x09\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x02\xacC\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x03\x0bx\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x04\xc6\x00\x00\x00\x00\x00\x01\x00\x03\xd5o\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03X\x00\x00\x00\x00\x00\x01\x00\x03\x8bd\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x02\xa5\xb6\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x04\xae\x00\x00\x00\x00\x00\x01\x00\x03\xd0\xb4\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x02\xdc\xbf\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xdd\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x02m\xe0\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x04\x96\x00\x00\x00\x00\x00\x01\x00\x03\xc7G\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x02u\x06\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x02\x87\xcb\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x03\xe2\x00\x00\x00\x00\x00\x01\x00\x03\xaa\x10\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x02\xf5\xf9\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03\xaa\x00\x00\x00\x00\x00\x01\x00\x03\x9de\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x81\x02\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x02\xb0\xd1\
\x00\x00\x01\x9d\xce\x5c\x0ci\
\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x03\x94\xec\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x02}\xbf\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xa15\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x03\x83x\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x02\x97\x0e\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x04B\x00\x00\x00\x00\x00\x01\x00\x03\xb8^\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x99i\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x04~\x00\x00\x00\x00\x00\x01\x00\x03\xbek\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x022\x00\x00\x00\x00\x00\x01\x00\x02\xf2\x0c\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x02\xc6\xca\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x02\xeb3\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x02\xdaL\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x03\x8e\x95\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x02\xa3\xf8\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x02\xa8\xe4\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x02\xfa\xc1\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x9f\x9e\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x03\xac\xfe\
\x00\x00\x01\x9b\x8f'M\xb3\
\x00\x00\x02\xa8\x00\x00\x00\x00\x00\x01\x00\x01\xb4\xe1\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x021;\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x01Fu\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x01Y\xc5\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x04\x22\x00\x00\x00\x00\x00\x01\x00\x02P\x00\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x01\x89s\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x01\x99\xe5\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x01\xba\xeb\
\x00\x00\x01\x9b\x8f'M\xbb\
\x00\x00\x04Z\x00\x00\x00\x00\x00\x01\x00\x02U\xfc\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x01\xaf\x80\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x01s\xf3\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x01\xb7\xcf\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x04\xc6\x00\x00\x00\x00\x00\x01\x00\x02i\xa2\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03X\x00\x00\x00\x00\x00\x01\x00\x026d\
\x00\x00\x01\x9b\x8f'M\xb1\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x01m5\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x04\xae\x00\x00\x00\x00\x00\x01\x00\x02e\x0c\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x01\x95Y\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x01\x8e\x90\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x01=G\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x04\x96\x00\x00\x00\x00\x00\x01\x00\x02^\xf1\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x01Dm\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x01R\x1f\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x03\xe2\x00\x00\x00\x00\x00\x01\x00\x02J\xa3\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x01\xa88\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03\xaa\x00\x00\x00\x00\x00\x01\x00\x02C\x13\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03\x0c\x00\x00\x00\x00\x00\x01\x00\x02+\xb6\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x01w\xaf\
\x00\x00\x01\x9d\xce^\x88R\
\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x02>f\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x01K\xdd\
\x00\x00\x01\x9b\xbd8B\xba\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01f\xb9\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x02-\xfd\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x01\x5c\xf7\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x04B\x00\x00\x00\x00\x00\x01\x00\x02Sh\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01^\xed\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x04~\x00\x00\x00\x00\x00\x01\x00\x02Y\xad\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x022\x00\x00\x00\x00\x00\x01\x00\x01\xa4\xce\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x83\x92\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x01\x9d\xf5\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x01\x93\x1f\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x02:\xff\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x01iW\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x01p\xad\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x01\xaa8\
\x00\x00\x01\x9b\x8f'M\xba\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x02E\xd1\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x02M\x0a\
\x00\x00\x01\x9b\x8f'M\xb2\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x01\x13[\
\x00\x00\x01\x9b\x97*\xf4\x05\
\x00\x00\x04\x18\x00\x00\x00\x00\x00\x01\x00\x01\x1b\x9f\
\x00\x00\x01\x9b\x97*\xf4\x03\
\x00\x00\x02\xa8\x00\x00\x00\x00\x00\x01\x00\x03\x0a\x8c\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x03\x8c\x9f\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x02}t\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x93\xe7\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x04@\x00\x00\x00\x00\x00\x01\x00\x03\xbb\xc6\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x02\xd4\x7f\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x02\xe5\xc1\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x03\x14\xad\
\x00\x00\x01\x9b\x97*\xf4\x02\
\x00\x00\x04x\x00\x00\x00\x00\x00\x01\x00\x03\xc4$\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x03\x04\x7f\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x02\xb0\xb9\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x03\x0f\xee\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x04\xe4\x00\x00\x00\x00\x00\x01\x00\x03\xde\xa1\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03X\x00\x00\x00\x00\x00\x01\x00\x03\x8f\xda\
\x00\x00\x01\x9b\x97*\xf3\xe3\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x02\xaa,\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x04\xcc\x00\x00\x00\x00\x00\x01\x00\x03\xd9\xe6\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x02\xe15\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x02\xd8S\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x02rV\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\xb4\x00\x00\x00\x00\x00\x01\x00\x03\xd0y\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x02y|\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x02\x8cA\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x03\xb3B\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x02\xfao\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x03\xa6\x97\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x85x\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x02\xb5G\
\x00\x00\x01\x9d\xcf\xc3\xa3\x0d\
\x00\x00\x03\xb0\x00\x00\x00\x00\x00\x01\x00\x03\x9e\x1e\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x02\x825\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xa5\xab\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x03\x87\xee\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x84\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x04`\x00\x00\x00\x00\x00\x01\x00\x03\xc1\x90\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x9d\xdf\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x9c\x00\x00\x00\x00\x00\x01\x00\x03\xc7\x9d\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x022\x00\x00\x00\x00\x00\x01\x00\x02\xf6\x82\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x02\xcb@\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x02\xef\xa9\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0b\
\x00\x00\x01\x9d\xd7\xdb\x89=\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x02\xde\xc2\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x03\x8e\x00\x00\x00\x00\x00\x01\x00\x03\x97\xc7\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x02\xa8n\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x02\xadZ\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x02\xff7\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x03\xa8\xd0\
\x00\x00\x01\x9b\x97*\xf3\xe5\
\x00\x00\x04\x18\x00\x00\x00\x00\x00\x01\x00\x03\xb60\
\x00\x00\x01\x9b\x97*\xf3\xe4\
\x00\x00\x02\xa8\x00\x00\x00\x00\x00\x01\x00\x01\xb6\xc5\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x023\x1f\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x01HY\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x01[\xa9\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04@\x00\x00\x00\x00\x00\x01\x00\x02Tv\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x01\x8bW\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x01\x9b\xc9\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x01\xbc\xcf\
\x00\x00\x01\x9b\x97*\xf4\x02\
\x00\x00\x04x\x00\x00\x00\x00\x00\x01\x00\x02Zr\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x01\xb1d\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x01u\xd7\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x01\xb9\xb3\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04\xe4\x00\x00\x00\x00\x00\x01\x00\x02n\x18\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03X\x00\x00\x00\x00\x00\x01\x00\x028H\
\x00\x00\x01\x9b\x97*\xf3\xe0\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x01o\x19\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04\xcc\x00\x00\x00\x00\x00\x01\x00\x02i\x82\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x01\x97=\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x01\x90t\
\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\xb4\x00\x00\x00\x00\x00\x01\x00\x02cg\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x01FQ\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x01T\x03\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x02O\x19\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x01\xaa\x1c\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x02G\x89\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03\x0c\x00\x00\x00\x00\x00\x01\x00\x02-\x9a\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x01y\x93\
\x00\x00\x01\x9d\xcf\xc3\xa3\x0d\
\x00\x00\x03\xb0\x00\x00\x00\x00\x00\x01\x00\x02B\xdc\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x00z\x00\x00\x00\x00\x00\x01\x00\x01M\xc1\
\x00\x00\x01\x9b\xc5\xbd\x82\xf5\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01h\x9d\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x02/\xe1\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x01^\xdb\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x04`\x00\x00\x00\x00\x00\x01\x00\x02W\xde\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01`\xd1\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x04\x9c\x00\x00\x00\x00\x00\x01\x00\x02^#\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x022\x00\x00\x00\x00\x00\x01\x00\x01\xa6\xb2\
\x00\x00\x01\x9b\x97*\xf3\xe2\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x85v\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x01\x9f\xd9\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03p\x00\x00\x00\x00\x00\x01\x00\x02<\xe3\
\x00\x00\x01\x9d\xd7\xdb\x89;\
\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x01\x95\x03\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x03\x8e\x00\x00\x00\x00\x00\x01\x00\x02?u\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x01k;\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x016\x00\x00\x00\x00\x00\x01\x00\x01r\x91\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x01\xac\x1c\
\x00\x00\x01\x9b\x97*\xf3\xfe\
\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x02JG\
\x00\x00\x01\x9b\x97*\xf3\xe1\
\x00\x00\x04\x18\x00\x00\x00\x00\x00\x01\x00\x02Q\x80\
\x00\x00\x01\x9b\x97*\xf3\xe1\
"
def qInitResources():

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -35,6 +35,8 @@ _ITEM_CONFIG = {
"Run tum": {"icon": "run.png"},
"JSON-RPC": {"icon": "json.png", "unfoldable": False},
"JSON-RPC action": {"icon": "json.png"},
"Parallel": {"icon": "parallel.png", "expanded": True},
"Parallel branch": {"icon": "parallel.png", "expanded": True},
}

View File

@@ -0,0 +1,16 @@
import time
import libs.testium as tm
def sleep_func(duration):
time.sleep(float(duration))
return 0
def check_duration(item_name, max_duration):
t0 = tm.gd(f"ts_start_{item_name}")
t1 = tm.gd(f"ts_end_{item_name}")
duration = tm.timestamp_as_sec(t1 - t0)
if duration < float(max_duration):
return 0
return 1

View File

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

View File

@@ -0,0 +1,343 @@
# --- Test 1: both branches succeed, sync:all ---
- parallel:
name: Both branches pass
key: $(test)_PASS
sync: all
branches:
- name: Branch A
steps:
- let:
name: Set A done
values:
- branch_a_done: true
- name: Branch B
steps:
- let:
name: Set B done
values:
- branch_b_done: true
- check:
name: Both branches ran
key: $(test)_PASS
values:
- <| $(branch_a_done) == True |>
- <| $(branch_b_done) == True |>
# --- Test 2: one branch fails, sync:all + no_fail → parallel forced to PASS ---
- parallel:
name: One branch fails
key: $(test)_PASS
sync: all
no_fail: true
branches:
- name: Pass branch
steps:
- let:
name: Set pass flag
values:
- pass_branch_ran: true
- name: Fail branch
steps:
- py_func:
name: Raise exception
file: $(test_path)$(psep)parallel.py
func_name: sleep_func
param: [0]
expected_result: fail
- check:
name: Pass branch still ran
key: $(test)_PASS
values:
- <| $(pass_branch_ran) == True |>
# --- Test 3: sync:any — first branch done stops the rest ---
- let:
name: Reset slow flag
values:
- slow_done: false
- parallel:
name: sync any - first wins
key: $(test)_PASS
sync: any
branches:
- name: Fast branch
steps:
- let:
name: Fast done
values:
- fast_done: true
- name: Slow branch
steps:
- py_func:
name: Sleep 2s
file: $(test_path)$(psep)parallel.py
func_name: sleep_func
param: [2]
- let:
name: Slow done
values:
- slow_done: true
- check:
name: Fast branch ran, slow branch was stopped
key: $(test)_PASS
values:
- <| $(fast_done) == True |>
- <| $(slow_done) == False |>
# --- Test 4: wait_for — branch B waits for A to set a flag ---
- let:
name: Reset sync flag
values:
- sync_flag: ""
- waiter_ran: false
- parallel:
name: wait_for synchronization
key: $(test)_PASS
sync: all
branches:
- name: Setter branch
steps:
- py_func:
name: Sleep 0.3s then set flag
file: $(test_path)$(psep)parallel.py
func_name: sleep_func
param: [0.3]
- let:
name: Set sync flag
values:
- sync_flag: ready
- name: Waiter branch
wait_for:
condition: <| "$(sync_flag)" == "ready" |>
timeout: 10
steps:
- let:
name: Got flag
values:
- waiter_ran: true
- check:
name: Waiter branch ran after flag was set
key: $(test)_PASS
values:
- <| $(waiter_ran) == True |>
# --- Test 5: parallel is faster than sequential (timing) ---
# Two 1s sleeps in parallel → ~1s total, not ~2s sequential
- parallel:
name: Timing test
key: $(test)_PASS
sync: all
branches:
- name: Sleep A
steps:
- sleep:
name: Sleep 1s A
timeout: 1
- name: Sleep B
steps:
- sleep:
name: Sleep 1s B
timeout: 1
- let:
name: Capture parallel duration
values:
- parallel_duration: $(ts_duration_Timing test)
- check:
name: Duration < 1.8s (would be 2s if sequential)
key: $(test)_PASS
values:
- <| float("$(parallel_duration)") < 1.8 |>
# --- Test 6: more than two branches ---
- let:
name: Reset N flags
values:
- n_a: false
- n_b: false
- n_c: false
- n_d: false
- parallel:
name: Four branches
key: $(test)_PASS
sync: all
branches:
- name: NA
steps:
- let: {name: set n_a, values: [{n_a: true}]}
- name: NB
steps:
- let: {name: set n_b, values: [{n_b: true}]}
- name: NC
steps:
- let: {name: set n_c, values: [{n_c: true}]}
- name: ND
steps:
- let: {name: set n_d, values: [{n_d: true}]}
- check:
name: Four branches all set their flag
key: $(test)_PASS
values:
- <| $(n_a) == True |>
- <| $(n_b) == True |>
- <| $(n_c) == True |>
- <| $(n_d) == True |>
# --- Test 7: nested parallel ---
- let:
name: Reset nested flags
values:
- outer_x: false
- inner_x_1: false
- inner_x_2: false
- parallel:
name: Outer parallel
key: $(test)_PASS
sync: all
branches:
- name: Outer X
steps:
- let: {name: set outer_x, values: [{outer_x: true}]}
- parallel:
name: Inner parallel
sync: all
branches:
- name: Inner X1
steps:
- let: {name: set inner_x_1, values: [{inner_x_1: true}]}
- name: Inner X2
steps:
- let: {name: set inner_x_2, values: [{inner_x_2: true}]}
- name: Outer Y
steps:
- sleep:
name: brief sleep
timeout: 0
- check:
name: Nested parallel set all flags
key: $(test)_PASS
values:
- <| $(outer_x) == True |>
- <| $(inner_x_1) == True |>
- <| $(inner_x_2) == True |>
# --- Test 9: wait_for timeout ---
- let:
name: Reset waiter timeout flag
values:
- waiter_timeout_ran: false
- parallel:
name: wait_for timeout
key: $(test)_PASS
sync: all
no_fail: true
branches:
- name: Quick branch
steps:
- sleep:
name: brief sleep
timeout: 0
- name: Doomed waiter
wait_for:
condition: <| "never" == "ready" |>
timeout: 1
steps:
- let: {name: should not run, values: [{waiter_timeout_ran: true}]}
- check:
name: Doomed waiter never ran its steps
key: $(test)_PASS
values:
- <| $(waiter_timeout_ran) == False |>
# --- Test 10: sync:all with a real branch failure (parallel must FAIL) ---
- parallel:
name: One branch really fails
key: $(test)_FAIL
sync: all
branches:
- name: ok branch
steps:
- let: {name: noop, values: [{noop_var: 1}]}
- name: broken branch
steps:
- py_func:
name: Forced fail
file: $(test_path)$(psep)parallel.py
func_name: sleep_func
param: [0]
expected_result: fail
# --- Test 11: branch with unmet condition is skipped, not failing the parallel ---
- let:
name: Reset branch condition flag
values:
- cond_branch_ran: false
- other_branch_ran: false
- parallel:
name: Condition-skipped branch
key: $(test)_PASS
sync: all
branches:
- name: Skipped branch
condition: <| "always" == "false" |>
steps:
- let: {name: should not run, values: [{cond_branch_ran: true}]}
- name: Other branch
steps:
- let: {name: ran, values: [{other_branch_ran: true}]}
- check:
name: Skipped condition branch did not run
key: $(test)_PASS
values:
- <| $(cond_branch_ran) == False |>
- <| $(other_branch_ran) == True |>
# --- Test 8: parallel inside loop (re-execution) ---
- let:
name: Reset loop counters
values:
- loop_count_a: 0
- loop_count_b: 0
- loop:
name: Loop wrapping parallel
iterator: 3
steps:
- parallel:
name: Per-iteration parallel
sync: all
branches:
- name: LA
steps:
- let:
name: bump A
values:
- loop_count_a: <| int("$(loop_count_a)") + 1 |>
- name: LB
steps:
- let:
name: bump B
values:
- loop_count_b: <| int("$(loop_count_b)") + 1 |>
- check:
name: Both branches ran 3 times
key: $(test)_PASS
values:
- <| int("$(loop_count_a)") == 3 |>
- <| int("$(loop_count_b)") == 3 |>