Skip to content

Commit 32e630e

Browse files
authored
Merge pull request #202 from Integration-Automation/dev
Merge dev: QA/test framework layer + USB passthrough subsystem
2 parents 32e09ad + 5c11485 commit 32e630e

104 files changed

Lines changed: 11731 additions & 365 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 46 additions & 6 deletions
Large diffs are not rendered by default.

README/README_zh-CN.md

Lines changed: 44 additions & 6 deletions
Large diffs are not rendered by default.

README/README_zh-TW.md

Lines changed: 44 additions & 6 deletions
Large diffs are not rendered by default.

docs/source/Eng/doc/new_features/new_features_doc.rst

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,11 +363,42 @@ over a list while the body sees the current item.
363363
}],
364364
])
365365

366-
Comparison operators for ``AC_if_var``: ``eq``, ``ne``, ``lt``, ``le``,
367-
``gt``, ``ge``, ``contains``, ``startswith``, ``endswith``.
366+
Comparison operators for ``AC_if_var`` (and ``AC_while_var``): ``eq``,
367+
``ne``, ``lt``, ``le``, ``gt``, ``ge``, ``contains``, ``startswith``,
368+
``endswith``.
369+
370+
``AC_while_var`` loops a ``body`` while a variable comparison holds. The
371+
condition is re-evaluated against the live scope before every iteration,
372+
so a body that mutates the variable (e.g. ``AC_inc_var``) terminates the
373+
loop; ``max_iter`` (default 1000) caps a condition that never turns
374+
false. ``AC_break`` / ``AC_continue`` work as in any loop::
375+
376+
executor.execute_action([
377+
["AC_set_var", {"name": "i", "value": 0}],
378+
["AC_while_var", {
379+
"name": "i", "op": "lt", "value": 5,
380+
"body": [["AC_inc_var", {"name": "i"}]],
381+
}],
382+
])
383+
384+
``AC_try`` adds try / catch / finally. When ``body`` raises, the
385+
``catch`` branch runs instead of aborting the script; ``finally`` always
386+
runs (on success, on a caught error, or while a ``reraise`` / loop
387+
break/continue propagates). The error text is exposed to ``error_var``
388+
for the ``catch`` branch to inspect, and ``reraise=true`` re-raises after
389+
cleanup::
390+
391+
executor.execute_action([
392+
["AC_try", {
393+
"body": [["AC_click_image", {"image": "dialog_ok.png"}]],
394+
"catch": [["AC_set_var", {"name": "dismissed", "value": False}]],
395+
"finally": [["AC_screenshot", {"file_path": "after.png"}]],
396+
"error_var": "err",
397+
}],
398+
])
368399

369400
Action-JSON commands: ``AC_set_var``, ``AC_get_var``, ``AC_inc_var``,
370-
``AC_if_var``, ``AC_for_each``.
401+
``AC_if_var``, ``AC_for_each``, ``AC_while_var``, ``AC_try``.
371402

372403
GUI: **Variables** tab — live view of ``executor.variables`` with
373404
single-set, JSON seed, and clear-all controls; reflects what
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
==================================
2+
New Features (2026-06) — QA Layer
3+
==================================
4+
5+
Nine additions that turn AutoControl's automation primitives into a
6+
full **test framework**: assert screen state, drive scripts from data,
7+
detect and quarantine flaky tests, run a scored suite, emit CI-native
8+
reports, audit accessibility / i18n, fan a script across a device
9+
matrix, and assert on audio / video. Every feature ships with a
10+
headless Python API, an ``AC_*`` executor command, an ``ac_*`` MCP
11+
tool, and a Qt GUI tab — same pattern as the rest of the framework.
12+
13+
.. contents::
14+
:local:
15+
:depth: 2
16+
17+
18+
Assertions
19+
==========
20+
21+
Assertion DSL
22+
-------------
23+
24+
Verify the screen state instead of only driving it. Each ``assert_*``
25+
observes the current state, returns an :class:`AssertionResult`, and
26+
(by default) raises ``AutoControlAssertionException`` on mismatch so a
27+
script / test / scheduled run fails loudly at the broken assumption::
28+
29+
from je_auto_control import (
30+
assert_text, assert_image, assert_pixel, assert_window,
31+
)
32+
33+
assert_text("Login successful", region=[0, 0, 800, 200])
34+
assert_image("checkmark.png", threshold=0.9)
35+
assert_pixel(100, 200, [0, 200, 0], tolerance=10)
36+
assert_window("Settings", exists=True)
37+
38+
``assert_text`` accepts ``regex=True`` and ``present=False`` (assert
39+
*absence*); every helper takes ``raise_on_fail`` and ``capture_on_fail``
40+
(saves a screenshot of the failing screen under
41+
``~/.je_auto_control/assertions/``).
42+
43+
Executor: ``AC_assert_text / _image / _pixel / _window``.
44+
MCP: ``ac_assert_*``. GUI: **Assertions** tab.
45+
46+
47+
Off-screen and system assertions
48+
---------------------------------
49+
50+
The DSL also verifies state that is not on the screen::
51+
52+
from je_auto_control import (
53+
assert_clipboard, assert_process, assert_file, assert_http,
54+
)
55+
56+
assert_clipboard("ORDER-12345", mode="contains")
57+
assert_process("chrome", running=True)
58+
assert_file("export.csv", min_size=1, contains="total")
59+
assert_http("https://localhost:8080/health", status=200)
60+
61+
* ``assert_clipboard`` — clipboard text by ``equals`` / ``contains`` /
62+
``regex``; ``present=False`` confirms a secret was *cleared*.
63+
* ``assert_process`` — a process whose name contains the argument is (or
64+
is not) running, via ``psutil``.
65+
* ``assert_file`` — existence / substring / SHA-256 / minimum size of a
66+
file; the path is ``realpath``-normalised before any I/O. Verifies a
67+
download or export.
68+
* ``assert_http`` — an ``http``/``https`` endpoint returns a status code
69+
(and optional body substring), always with an explicit ``timeout``.
70+
Only ``http``/``https`` schemes are accepted; an unreachable host is a
71+
failed assertion, not a crash.
72+
73+
Executor: ``AC_assert_clipboard / _process / _file / _http``.
74+
MCP: ``ac_assert_clipboard / ac_assert_process / ac_assert_file /
75+
ac_assert_http``.
76+
77+
78+
Assertion combinators (group / OR / poll)
79+
-----------------------------------------
80+
81+
Compose the eight assertion kinds with declarative *specs* — plain dicts
82+
like ``{"kind": "text", "text": "Saved"}`` — so the same checks are
83+
reachable from Python, JSON, and MCP without passing callables::
84+
85+
from je_auto_control import assert_all, assert_any, assert_eventually
86+
87+
# soft assertions: run the whole batch, collect every failure
88+
assert_all([
89+
{"kind": "window", "title": "Dashboard"},
90+
{"kind": "text", "text": "Welcome"},
91+
])
92+
93+
# OR: pass when at least one spec passes (short-circuits)
94+
assert_any([
95+
{"kind": "text", "text": "Success"},
96+
{"kind": "window", "title": "Redirecting"},
97+
])
98+
99+
# poll any spec until it passes or times out
100+
assert_eventually({"kind": "http", "url": "http://localhost:8080/health"},
101+
timeout=30, interval=0.5)
102+
103+
``assert_all`` (AND) never short-circuits and returns a
104+
:class:`GroupAssertionResult` summarising every sub-result;
105+
``assert_any`` (OR) stops at the first pass; ``assert_eventually``
106+
re-checks one spec on an interval until it holds — ideal for waiting on a
107+
service to come up or a download file to appear.
108+
109+
Executor: ``AC_assert_all / AC_assert_any / AC_assert_eventually``.
110+
MCP: ``ac_assert_all / ac_assert_any / ac_assert_eventually``.
111+
112+
113+
Media assertions (audio / video)
114+
--------------------------------
115+
116+
Assert that something actually *played* or *animated*::
117+
118+
from je_auto_control import assert_audio_activity, assert_video_changes
119+
120+
assert_audio_activity(duration_s=1.0, threshold=0.01, expect_sound=True)
121+
assert_video_changes("clip.mp4", start_s=0, end_s=3, expect_motion=True)
122+
123+
``assert_audio_activity`` records from an input device and compares the
124+
RMS level to a threshold (sound vs silence). ``assert_video_changes``
125+
measures mean frame-to-frame difference over a video segment (motion vs
126+
static), with an optional ``region`` crop. The numeric cores
127+
(``rms``, ``mean_frame_diff``, ``measure_audio_rms``,
128+
``video_segment_motion``) are public and pure. ``sounddevice`` /
129+
OpenCV are lazy dependencies.
130+
131+
Executor: ``AC_assert_audio / AC_assert_video_changes``.
132+
MCP: ``ac_assert_audio / ac_assert_video_changes``. GUI: **Media
133+
Checks** tab.
134+
135+
136+
Data-driven execution
137+
=====================
138+
139+
Feed rows from CSV / JSON / SQLite / Excel / inline literals into a
140+
``${var}`` script, then run the same body once per row::
141+
142+
from je_auto_control import load_rows
143+
144+
rows = load_rows({"kind": "csv", "path": "users.csv"})
145+
146+
In a JSON action file the new ``AC_for_each_row`` block command loads a
147+
data source and binds each row to a variable whose columns are
148+
addressable as ``${row.column}``::
149+
150+
["AC_for_each_row", {
151+
"source": {"kind": "csv", "path": "users.csv"},
152+
"as": "row",
153+
"body": [
154+
["AC_type_keyboard", {"keys": "${row.username}"}],
155+
["AC_assert_text", {"text": "${row.expected}"}]
156+
]
157+
}]
158+
159+
The SQLite connector accepts a **single read-only** ``SELECT`` / ``WITH``
160+
statement only (multi-statement / write queries are rejected); all file
161+
paths are ``realpath``-validated. ``${var}`` interpolation now resolves
162+
dotted paths into dict keys and list indices (``${row.user}``,
163+
``${results.0}``) while preserving value types.
164+
165+
Executor: ``AC_load_data`` + ``AC_for_each_row``.
166+
MCP: ``ac_load_data``. GUI: **Data Sources** tab.
167+
168+
169+
Flaky-test detection & quarantine
170+
==================================
171+
172+
Flaky report
173+
------------
174+
175+
Score intermittent failures from the SQLite run-history store. Runs are
176+
grouped by ``script_path`` (or ``source_id``); the report counts
177+
pass/fail outcomes and pass↔fail *flips* in chronological order so a
178+
flaky script ranks above one that is consistently green or red::
179+
180+
from je_auto_control import analyze_flakiness
181+
182+
report = analyze_flakiness(min_runs=3)
183+
for entry in report.entries:
184+
print(entry.key, entry.flip_rate, entry.flaky)
185+
186+
Executor: ``AC_flaky_report``. MCP: ``ac_flaky_report``.
187+
GUI: **Flaky Tests** tab.
188+
189+
190+
Quarantine (closing the loop)
191+
-----------------------------
192+
193+
A quarantined case name is *skipped* by the suite runner (recorded as
194+
``skipped`` with reason ``quarantined``) so a known-flaky case stops
195+
poisoning the suite's red/green status until it is fixed. The store is a
196+
small JSON file (mode 0600 on POSIX) that persists across restarts::
197+
198+
from je_auto_control import (
199+
default_quarantine_store, auto_quarantine_from_flakiness,
200+
)
201+
202+
default_quarantine_store().add("login_suite", reason="under triage")
203+
auto_quarantine_from_flakiness(flip_rate_threshold=0.5)
204+
205+
``auto_quarantine_from_flakiness`` reads the flakiness report and
206+
quarantines every group above the flip-rate threshold.
207+
208+
Executor: ``AC_quarantine_add / _remove / _list / _clear / _auto``.
209+
MCP: ``ac_quarantine_*``. GUI: quarantine panel on the **Test Suites**
210+
tab.
211+
212+
213+
QA suite runner + CI reports
214+
============================
215+
216+
Suite orchestration
217+
-------------------
218+
219+
Turn flat action lists into scored test cases with setup / teardown,
220+
tags, and per-case pass/fail. A case carrying a ``data`` source expands
221+
to one scored case per row::
222+
223+
from je_auto_control import run_suite
224+
225+
spec = {
226+
"name": "Login",
227+
"setup": [["AC_focus_window", {"title": "MyApp"}]],
228+
"teardown": [["AC_close_window", {"title": "MyApp"}]],
229+
"cases": [
230+
{"name": "valid login", "tags": ["smoke"],
231+
"actions": [["AC_assert_text", {"text": "Welcome"}]]},
232+
{"name": "each user", "as": "row",
233+
"data": {"kind": "csv", "path": "users.csv"},
234+
"actions": [["AC_assert_text", {"text": "${row.expected}"}]]},
235+
],
236+
}
237+
result = run_suite(spec, tags=["smoke"])
238+
print(result.passed, result.failed, result.errored, result.skipped)
239+
240+
An ``AutoControlAssertionException`` marks a case **failed**; any other
241+
exception marks it **error**; a clean run is **passed**. Quarantined
242+
case names are recorded as **skipped**.
243+
244+
Executor: ``AC_run_suite``. MCP: ``ac_run_suite``.
245+
GUI: **Test Suites** tab.
246+
247+
248+
CI-native reports (JUnit / Allure)
249+
----------------------------------
250+
251+
Emit reports that Jenkins, GitHub Actions, GitLab CI, and Allure parse
252+
natively::
253+
254+
from je_auto_control import write_junit_xml, write_allure_results
255+
256+
write_junit_xml(result, "reports/junit.xml")
257+
write_allure_results(result, "reports/allure")
258+
259+
``AC_run_suite`` writes them inline when given ``junit_path`` /
260+
``allure_dir``::
261+
262+
["AC_run_suite", {"spec": {...}, "junit_path": "reports/junit.xml"}]
263+
264+
Only report *generation* happens here (never parsing untrusted XML), so
265+
the stdlib ``xml.etree.ElementTree`` writer is safe.
266+
267+
268+
Accessibility & i18n audit
269+
==========================
270+
271+
Reuse the accessibility tree and OCR layer to *inspect* a UI for common
272+
accessibility / localisation defects rather than to drive it::
273+
274+
from je_auto_control import run_audit, contrast_ratio
275+
276+
report = run_audit(
277+
app_name="MyApp",
278+
contrast_pairs=[{"foreground": [120, 120, 120],
279+
"background": [255, 255, 255], "label": "hint"}],
280+
texts=["Save chang…"], # OCR strings to scan for truncation
281+
)
282+
283+
Checks:
284+
285+
* **Missing labels** — interactive widgets (button, menu item, link,
286+
field …) exposed through the a11y tree with no accessible name.
287+
* **Contrast** — WCAG 2.x relative-luminance contrast ratio with AA /
288+
AAA thresholds (``contrast_ratio([0,0,0],[255,255,255]) == 21.0``).
289+
* **Truncation** — OCR strings ending in an ellipsis (clipped after
290+
translation).
291+
292+
Executor: ``AC_audit_accessibility / AC_audit_contrast``.
293+
MCP: ``ac_audit_*``. GUI: **A11y Audit** tab.
294+
295+
296+
Mobile device matrix
297+
====================
298+
299+
Fan a single action list out across many Android / iOS devices **in
300+
parallel**, each on its own isolated executor (so runtime variable
301+
scopes never collide between threads). The script targets the current
302+
device through a bound ``${device.*}`` variable::
303+
304+
from je_auto_control import run_on_devices
305+
306+
report = run_on_devices(
307+
actions=[["AC_android_tap", {"x": 100, "y": 200,
308+
"serial": "${device.serial}"}]],
309+
devices=[{"platform": "android", "serial": "emulator-5554"},
310+
{"platform": "android", "serial": "emulator-5556"}],
311+
max_parallel=4,
312+
)
313+
print(report.passed, report.failed)
314+
315+
A failure on one device is isolated — it never aborts the others.
316+
317+
Executor: ``AC_run_device_matrix``. MCP: ``ac_run_device_matrix``.
318+
GUI: **Device Matrix** tab.

0 commit comments

Comments
 (0)