Skip to content

Commit b39e825

Browse files
authored
Merge pull request #414 from Integration-Automation/dev
Release: test-robustness lane — failure signatures, run diff, flake clustering, step timeline (v191–v194)
2 parents 4e00a9f + b25b747 commit b39e825

26 files changed

Lines changed: 1372 additions & 0 deletions

WHATS_NEW.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-06-25) — Per-Run Step Timeline (waterfall + bottleneck steps)
4+
5+
Read why *this* run was slow — a step waterfall and its bottlenecks. Full reference: [`docs/source/Eng/doc/new_features/v194_features_doc.rst`](docs/source/Eng/doc/new_features/v194_features_doc.rst).
6+
7+
- **`build_timeline` / `critical_steps`** (`AC_build_timeline`, `AC_critical_steps`): the action profiler aggregates timings by step *name* across runs — useless for "why was *this* run slow". This turns one run's ordered steps into a waterfall (each step's offset, duration, and `pct` share of the total) with the `bottleneck` step and a `parallelism` ratio (`> 1` when steps overlap via explicit `start` times); `critical_steps` ranks the dominant steps to optimise. A step is any `{name, duration, start?}` dict. Pure stdlib. No `PySide6`.
8+
9+
## What's new (2026-06-25) — Flaky-Test Co-Failure Clustering
10+
11+
Find the tests that flake *together* — and the shared root cause behind them. Full reference: [`docs/source/Eng/doc/new_features/v193_features_doc.rst`](docs/source/Eng/doc/new_features/v193_features_doc.rst).
12+
13+
- **`cofailure_pairs` / `failure_clusters`** (`AC_cofailure_pairs`, `AC_failure_clusters`): flaky tests are rarely independent — a wobbly fixture or noisy dependency makes a *group* fail in the same runs (~75% of flaky tests cluster). Ranking tests one-by-one by flip rate misses that. This measures how often each pair of tests fails in the *same* runs (Jaccard over their failing-run sets) and groups tests above a threshold into connected clusters with a cohesion score — so you chase one root cause instead of N symptoms. Input is a list of runs, each the test names that failed in it. Pure stdlib. No `PySide6`.
14+
15+
## What's new (2026-06-25) — Run-Trace Diff (what changed between two executions)
16+
17+
See exactly what changed between a passing run and a failing one. Full reference: [`docs/source/Eng/doc/new_features/v192_features_doc.rst`](docs/source/Eng/doc/new_features/v192_features_doc.rst).
18+
19+
- **`diff_runs` / `summarize_run_diff`** (`AC_diff_runs`): a run history says a run *failed* but not *what changed* from the run that passed. This aligns two step sequences with a longest-common-subsequence walk (so an inserted/removed step shifts the rest into place instead of mis-pairing everything) and classifies the differences: **added**/**removed** steps, **status_flips** (an aligned step that changed status — with the new failure's `failure_signature` when it carries an error), and **timing_regressions** (a step that got `regress_factor`× slower). `summarize_run_diff` renders a one-line summary. Pure stdlib over lists of `{name,status,duration,error}` step dicts. No `PySide6`.
20+
21+
## What's new (2026-06-25) — Stable Failure Signatures
22+
23+
Match the *same kind* of failure across runs, despite differing paths and ids. Full reference: [`docs/source/Eng/doc/new_features/v191_features_doc.rst`](docs/source/Eng/doc/new_features/v191_features_doc.rst).
24+
25+
- **`normalize_error` / `failure_signature` / `group_failures`** (`AC_failure_signature`, `AC_group_failures`): two runs that failed the same way rarely have byte-identical error text — paths, line numbers, addresses, ids and timestamps differ every time — which defeats "is this the same failure?" and "which tests fail together?". This strips the variable parts of an error to a canonical form and hashes it (SHA-256), so the same kind of failure gets the same short signature across runs — the join key the rest of the test-robustness tools (run diffing, flake clustering) group on. `group_failures` buckets a list of errors by signature, most frequent first. Pure stdlib (`re` + `hashlib`). No `PySide6`.
26+
327
## What's new (2026-06-24) — Visual Saliency (where to look — spectral-residual)
428

529
Find the region that stands out, with no template / colour / text. Full reference: [`docs/source/Eng/doc/new_features/v190_features_doc.rst`](docs/source/Eng/doc/new_features/v190_features_doc.rst).
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Stable Failure Signatures
2+
=========================
3+
4+
Two runs that failed the *same way* almost never have byte-identical error text —
5+
paths, line numbers, memory addresses, ids and timestamps differ every time. That
6+
defeats any attempt to ask "is this the same failure as yesterday?" or "which
7+
tests fail *together*?". ``failure_signature`` strips the variable parts of an
8+
error to a canonical form and hashes it (SHA-256), so the same *kind* of failure
9+
gets the same short signature across runs — the join key the rest of the
10+
test-robustness tools (run diffing, flake clustering) group on.
11+
12+
* :func:`normalize_error` — collapse paths / hex addresses / UUIDs / timestamps /
13+
line numbers / bare integers to placeholders,
14+
* :func:`failure_signature` — a short stable SHA-256 of the normalised message,
15+
* :func:`group_failures` — group a list of errors by signature, most frequent
16+
first.
17+
18+
Pure standard library (``re`` + ``hashlib``); no device, no ``PySide6``.
19+
20+
Headless API
21+
------------
22+
23+
.. code-block:: python
24+
25+
from je_auto_control import (normalize_error, failure_signature,
26+
group_failures)
27+
28+
a = r"Timeout at C:\app\run.py line 42 (0x7ffab12c) at 2026-06-24 11:03:21"
29+
b = r"Timeout at C:\app\run.py line 88 (0x1234abcd) at 2026-06-25 09:15:00"
30+
normalize_error(a) # "Timeout at <path> line <n> (0x<addr>) at <ts>"
31+
failure_signature(a) == failure_signature(b) # True — same failure
32+
33+
group_failures([a, b, "Connection refused to /tmp/x.sock"])
34+
# [{"signature": "...", "normalized": "...", "count": 2, "examples": [...]},
35+
# {"signature": "...", "count": 1, ...}]
36+
37+
Windows and POSIX paths, ``0x`` addresses, UUIDs, ISO timestamps, ``line N`` and
38+
any leftover integers become placeholders; whitespace is squeezed.
39+
``group_failures`` keeps up to three distinct raw examples per group and skips
40+
empty / ``None`` messages.
41+
42+
Executor commands
43+
-----------------
44+
45+
``AC_failure_signature`` (``error`` / ``length``) returns ``{signature,
46+
normalized}``; ``AC_group_failures`` (``errors``) returns the grouped list. They
47+
are exposed as read-only ``ac_*`` MCP tools and as Script Builder commands under
48+
**Testing**.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Run-Trace Diff (what changed between two executions)
2+
====================================================
3+
4+
A run history tells you a run *failed*, but not *what changed* from the run that
5+
passed: which step was added or dropped, which step flipped pass→fail, which step
6+
got slower. ``run_diff`` aligns the two step sequences with a longest-common-
7+
subsequence walk — so an inserted or removed step shifts the rest into place
8+
instead of mis-pairing everything — and classifies the differences:
9+
10+
* **added** / **removed** — steps present in only one run,
11+
* **status_flips** — an aligned step whose status changed, with the new failure's
12+
:func:`failure_signature` when it carries an ``error``,
13+
* **timing_regressions** — an aligned step that got ``regress_factor`` x slower.
14+
15+
A step is any dict with a name key (default ``"name"``) and optional ``status`` /
16+
``duration`` / ``error``. Pure standard library; no device, no ``PySide6``.
17+
18+
Headless API
19+
------------
20+
21+
.. code-block:: python
22+
23+
from je_auto_control import diff_runs, summarize_run_diff
24+
25+
before = [{"name": "login", "status": "ok", "duration": 1.0},
26+
{"name": "submit", "status": "ok", "duration": 1.0}]
27+
after = [{"name": "login", "status": "ok", "duration": 1.1},
28+
{"name": "accept_cookies", "status": "ok"}, # inserted
29+
{"name": "submit", "status": "error", "error": "Timeout ..."}]
30+
31+
diff = diff_runs(before, after)
32+
# {"added": [accept_cookies], "removed": [],
33+
# "status_flips": [{"name": "submit", "from": "ok", "to": "error",
34+
# "signature": "..."}],
35+
# "timing_regressions": [], "aligned": 2, "identical": False}
36+
37+
summarize_run_diff(diff) # "+1 added, 1 status flip(s)"
38+
39+
``regress_factor`` (default ``1.5``) is the slowdown ratio that counts as a
40+
regression; ``key`` selects the field steps are aligned on. ``summarize_run_diff``
41+
renders a one-line summary (``"no change"`` when identical).
42+
43+
Executor commands
44+
-----------------
45+
46+
``AC_diff_runs`` (``before`` / ``after`` / ``key`` / ``regress_factor``) returns
47+
the diff plus a ``summary`` field. It is exposed as the read-only ``ac_diff_runs``
48+
MCP tool and as a Script Builder command under **Testing**.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
Flaky-Test Co-Failure Clustering
2+
================================
3+
4+
Flaky tests are rarely independent: a wobbly shared fixture, a slow dependency or
5+
a noisy environment makes a *group* of tests fail in the same runs (research finds
6+
~75% of flaky tests fall into co-failure clusters). Ranking tests one-by-one by
7+
flip rate misses that shared root cause. ``flake_cluster`` measures how often each
8+
pair of tests fails in the *same* runs — Jaccard similarity over the set of runs
9+
each failed in — and groups tests whose co-failure exceeds a threshold, so you can
10+
chase one root cause instead of N symptoms.
11+
12+
* :func:`cofailure_pairs` — test pairs that fail together above a threshold,
13+
* :func:`failure_clusters` — connected clusters of co-failing tests with a
14+
cohesion score (mean pairwise Jaccard).
15+
16+
Input is a list of runs, each a collection of the test names that failed in that
17+
run. Pure standard library; no device, no ``PySide6``.
18+
19+
Headless API
20+
------------
21+
22+
.. code-block:: python
23+
24+
from je_auto_control import failure_clusters, cofailure_pairs
25+
26+
runs = [["test_a", "test_b"], # both failed in this run
27+
["test_a", "test_b"],
28+
["test_c"],
29+
["test_a", "test_b", "test_c"]]
30+
31+
failure_clusters(runs, threshold=0.6)
32+
# [{"tests": ["test_a", "test_b"], "size": 2, "cohesion": 1.0}]
33+
34+
cofailure_pairs(runs, threshold=0.6)
35+
# [{"tests": ["test_a", "test_b"], "jaccard": 1.0, "co_failures": 3}]
36+
37+
``threshold`` is the minimum co-failure Jaccard to link two tests; ``min_size``
38+
(default ``2``) drops singletons so only genuine clusters surface. Clusters come
39+
back largest / most cohesive first.
40+
41+
Executor commands
42+
-----------------
43+
44+
``AC_failure_clusters`` (``runs`` / ``threshold`` / ``min_size``) and
45+
``AC_cofailure_pairs`` (``runs`` / ``threshold``). They are exposed as read-only
46+
``ac_*`` MCP tools and as Script Builder commands under **Testing**.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Per-Run Step Timeline (waterfall + bottleneck steps)
2+
====================================================
3+
4+
The action profiler aggregates timings by step *name* across many runs — great
5+
for "which action is slow on average", useless for "why was *this* run slow". A
6+
single run is an ordered timeline: step A ran, then B, then C, and one of them
7+
dominated. ``step_timeline`` turns one run's steps into a waterfall (each step's
8+
offset from the start, its duration and its share of the total) and ranks the
9+
bottleneck steps, so you can read a single slow run instead of an average.
10+
11+
* :func:`build_timeline` — the waterfall + total / busy / bottleneck /
12+
parallelism,
13+
* :func:`critical_steps` — the steps that dominate the run, longest first.
14+
15+
A step is any dict with a name (default ``"name"``) and a ``duration``; an
16+
optional ``start`` places it on an absolute timeline (overlapping / parallel
17+
steps), else steps are laid out back-to-back. Pure standard library; no device,
18+
no ``PySide6``.
19+
20+
Headless API
21+
------------
22+
23+
.. code-block:: python
24+
25+
from je_auto_control import build_timeline, critical_steps
26+
27+
steps = [{"name": "login", "duration": 1.0},
28+
{"name": "load_dashboard", "duration": 4.0},
29+
{"name": "submit", "duration": 1.0}]
30+
31+
build_timeline(steps)
32+
# {"steps": [{"name": "login", "offset": 0.0, "duration": 1.0, "pct": 16.7},
33+
# {"name": "load_dashboard", "offset": 1.0, ..., "pct": 66.7}, ...],
34+
# "total": 6.0, "busy": 6.0,
35+
# "bottleneck": {"name": "load_dashboard", "duration": 4.0},
36+
# "parallelism": 1.0}
37+
38+
critical_steps(steps, top=2)
39+
# [{"name": "load_dashboard", "duration": 4.0, "pct": 66.7},
40+
# {"name": "login", "duration": 1.0, "pct": 16.7}]
41+
42+
``total`` is the wall-clock span, ``busy`` the summed step time; ``parallelism`` =
43+
busy / total is ``1.0`` for a purely sequential run and ``> 1`` when steps overlap
44+
(supply ``start`` times). ``pct`` is each step's share of the total time.
45+
46+
Executor commands
47+
-----------------
48+
49+
``AC_build_timeline`` (``steps``) and ``AC_critical_steps`` (``steps`` / ``top``).
50+
They are exposed as read-only ``ac_*`` MCP tools and as Script Builder commands
51+
under **Testing**.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
穩定的失敗簽章
2+
==============
3+
4+
兩次以*相同方式*失敗的執行,幾乎不會有逐位元組相同的錯誤文字——路徑、行號、記憶體位址、id 與
5+
時間戳每次都不同。這使得「這和昨天是同一個失敗嗎?」或「哪些測試會*一起*失敗?」無從問起。
6+
``failure_signature`` 把錯誤的變動部分剝離成標準形式並雜湊(SHA-256),於是*相同類型*的失敗在
7+
不同執行間會得到相同的短簽章——即其餘 test-robustness 工具(執行比較、flaky 分群)所依據的
8+
join key。
9+
10+
* :func:`normalize_error` ——把路徑 / 十六進位位址 / UUID / 時間戳 / 行號 / 裸整數收斂成佔位符,
11+
* :func:`failure_signature` ——正規化訊息的短而穩定的 SHA-256,
12+
* :func:`group_failures` ——把一組錯誤依簽章分組,最常見者在前。
13+
14+
純標準庫(``re`` + ``hashlib``);不涉及裝置,不匯入 ``PySide6``。
15+
16+
無頭 API
17+
--------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import (normalize_error, failure_signature,
22+
group_failures)
23+
24+
a = r"Timeout at C:\app\run.py line 42 (0x7ffab12c) at 2026-06-24 11:03:21"
25+
b = r"Timeout at C:\app\run.py line 88 (0x1234abcd) at 2026-06-25 09:15:00"
26+
normalize_error(a) # "Timeout at <path> line <n> (0x<addr>) at <ts>"
27+
failure_signature(a) == failure_signature(b) # True——同一個失敗
28+
29+
group_failures([a, b, "Connection refused to /tmp/x.sock"])
30+
# [{"signature": "...", "normalized": "...", "count": 2, "examples": [...]},
31+
# {"signature": "...", "count": 1, ...}]
32+
33+
Windows 與 POSIX 路徑、``0x`` 位址、UUID、ISO 時間戳、``line N`` 與任何殘留整數都會變成佔位符;
34+
空白會被壓縮。``group_failures`` 每組最多保留三個不同的原始範例,並略過空 / ``None`` 訊息。
35+
36+
執行器指令
37+
----------
38+
39+
``AC_failure_signature``(``error`` / ``length``)回傳 ``{signature, normalized}``;
40+
``AC_group_failures``(``errors``)回傳分組清單。皆以唯讀 ``ac_*`` MCP 工具及 Script Builder
41+
指令(位於 **Testing** 分類下)形式提供。
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
執行軌跡比較(兩次執行之間改變了什麼)
2+
======================================
3+
4+
執行歷史告訴你某次執行*失敗*了,卻不告訴你相較於通過的那次*改變了什麼*:哪個步驟被加入或移除、
5+
哪個步驟由通過翻轉成失敗、哪個步驟變慢了。``run_diff`` 以最長共同子序列(LCS)走訪對齊兩個步驟
6+
序列——這樣插入或移除一個步驟會把其餘步驟順移到位,而非整個錯位配對——並將差異分類:
7+
8+
* **added** / **removed** ——只存在於其中一次執行的步驟,
9+
* **status_flips** ——某個已對齊步驟的狀態改變,若帶有 ``error`` 則附上新失敗的
10+
:func:`failure_signature`,
11+
* **timing_regressions** ——某個已對齊步驟變慢了 ``regress_factor`` 倍。
12+
13+
步驟可為任何帶有名稱鍵(預設 ``"name"``)與選填 ``status`` / ``duration`` / ``error`` 的字典。
14+
純標準庫;不涉及裝置,不匯入 ``PySide6``。
15+
16+
無頭 API
17+
--------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import diff_runs, summarize_run_diff
22+
23+
before = [{"name": "login", "status": "ok", "duration": 1.0},
24+
{"name": "submit", "status": "ok", "duration": 1.0}]
25+
after = [{"name": "login", "status": "ok", "duration": 1.1},
26+
{"name": "accept_cookies", "status": "ok"}, # 插入
27+
{"name": "submit", "status": "error", "error": "Timeout ..."}]
28+
29+
diff = diff_runs(before, after)
30+
# {"added": [accept_cookies], "removed": [],
31+
# "status_flips": [{"name": "submit", "from": "ok", "to": "error",
32+
# "signature": "..."}],
33+
# "timing_regressions": [], "aligned": 2, "identical": False}
34+
35+
summarize_run_diff(diff) # "+1 added, 1 status flip(s)"
36+
37+
``regress_factor``(預設 ``1.5``)是算作退化的變慢比率;``key`` 選擇步驟對齊所依據的欄位。
38+
``summarize_run_diff`` 產生一行摘要(相同時為 ``"no change"``)。
39+
40+
執行器指令
41+
----------
42+
43+
``AC_diff_runs``(``before`` / ``after`` / ``key`` / ``regress_factor``)回傳該差異並附帶
44+
``summary`` 欄位。以唯讀 ``ac_diff_runs`` MCP 工具及 Script Builder 指令(位於 **Testing**
45+
分類下)形式提供。
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
不穩定測試的共同失敗分群
2+
========================
3+
4+
不穩定(flaky)測試很少是獨立的:搖晃的共用 fixture、緩慢的相依、或吵雜的環境,會讓*一群*測試在
5+
相同的執行中一起失敗(研究發現約 75% 的 flaky 測試落在共同失敗的群集裡)。逐一以翻轉率排名測試
6+
會錯過這個共同根因。``flake_cluster`` 量測每對測試多常在*相同*執行中失敗——即各自失敗的執行集合
7+
之間的 Jaccard 相似度——並把共同失敗超過門檻的測試分群,讓你能追一個根因,而非 N 個症狀。
8+
9+
* :func:`cofailure_pairs` ——共同失敗超過門檻的測試對,
10+
* :func:`failure_clusters` ——共同失敗測試的連通群集,附凝聚度分數(群內平均成對 Jaccard)。
11+
12+
輸入是一份執行清單,每個元素為該次執行中失敗的測試名稱集合。純標準庫;不涉及裝置,不匯入
13+
``PySide6``。
14+
15+
無頭 API
16+
--------
17+
18+
.. code-block:: python
19+
20+
from je_auto_control import failure_clusters, cofailure_pairs
21+
22+
runs = [["test_a", "test_b"], # 這次執行兩者皆失敗
23+
["test_a", "test_b"],
24+
["test_c"],
25+
["test_a", "test_b", "test_c"]]
26+
27+
failure_clusters(runs, threshold=0.6)
28+
# [{"tests": ["test_a", "test_b"], "size": 2, "cohesion": 1.0}]
29+
30+
cofailure_pairs(runs, threshold=0.6)
31+
# [{"tests": ["test_a", "test_b"], "jaccard": 1.0, "co_failures": 3}]
32+
33+
``threshold`` 是連結兩測試所需的最小共同失敗 Jaccard;``min_size``(預設 ``2``)會丟棄單例,
34+
讓只有真正的群集浮現。群集以最大 / 最凝聚者在前回傳。
35+
36+
執行器指令
37+
----------
38+
39+
``AC_failure_clusters``(``runs`` / ``threshold`` / ``min_size``)與
40+
``AC_cofailure_pairs``(``runs`` / ``threshold``)。皆以唯讀 ``ac_*`` MCP 工具及 Script Builder
41+
指令(位於 **Testing** 分類下)形式提供。

0 commit comments

Comments
 (0)