Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ je_auto_control version
`--var name=value` is parsed as JSON when possible (`count=10` becomes an int),
otherwise kept as a string. `run` exits 1 when any action failed (the run
still goes on to the end), so a CI step fails with it. The legacy
`python -m je_auto_control -e file.json` entry point still works.
`python -m je_auto_control -e file.json` entry point still works, and its
`-e`, `-d` and `--execute_str` flags exit 1 the same way. Under
[TestPioneer](https://github.com/Integration-Automation/TestPioneer), which
sets `TEST_PIONEER_ARTIFACT_DIR`, the HTML, JSON and XML reports asked for
under a relative name are written below that directory.

---

Expand Down
2 changes: 1 addition & 1 deletion README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ je_auto_control version
```

`--var name=value` 会尽量以 JSON 解析(`count=10` 会变成整数),否则视为字符串。
`run` 只要有任何动作失败就以 1 退出(仍会跑完整份脚本),CI 步骤会随之失败。旧版 `python -m je_auto_control -e file.json` 入口仍然可用。
`run` 只要有任何动作失败就以 1 退出(仍会跑完整份脚本),CI 步骤会随之失败。旧版 `python -m je_auto_control -e file.json` 入口仍然可用,它的 `-e`、`-d` 与 `--execute_str` 标志也同样在有动作失败时以 1 退出。由 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 启动时(它会设置 `TEST_PIONEER_ARTIFACT_DIR`),以相对名称要求的 HTML、JSON、XML 报告会写到该目录底下。

---

Expand Down
2 changes: 1 addition & 1 deletion README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ je_auto_control version
```

`--var name=value` 會盡量以 JSON 解析(`count=10` 會變成整數),否則視為字串。
`run` 只要有任何動作失敗就以 1 結束(仍會跑完整份腳本),CI 步驟會跟著失敗。舊版 `python -m je_auto_control -e file.json` 進入點仍然可用。
`run` 只要有任何動作失敗就以 1 結束(仍會跑完整份腳本),CI 步驟會跟著失敗。舊版 `python -m je_auto_control -e file.json` 進入點仍然可用,它的 `-e`、`-d` 與 `--execute_str` 旗標也同樣在有動作失敗時以 1 結束。由 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 啟動時(它會設定 `TEST_PIONEER_ARTIFACT_DIR`),以相對名稱要求的 HTML、JSON、XML 報告會寫到該目錄底下。

---

Expand Down
2 changes: 1 addition & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ wrapper/auto_control_record.record → OS listener (e.g. windows/record/win32_in
| --- | --- | --- |
| Jeffrey_RPA | Editable install of **this working tree**: uncommitted changes here reach it immediately. Single facade `JeffreyRPA/_gui_control.py`. | Top-level names (e.g. `click_mouse`, `hotkey`, `write`, `screen_size`, `get_pixel`, `post_click_to_window`) and internal paths `je_auto_control.wrapper.auto_control_window`, `je_auto_control.wrapper.auto_control_keyboard.WRITE_CONTROL_KEYS`, `je_auto_control.utils.monitor_layout` (`logical_virtual_rect`, `enumerate_monitors`), and `wrapper.platform_wrapper.keyboard_keys_table` / `mouse_keys_table` — it validates every key name a user types against the keyboard table and reverse-looks-up recorded virtual keys through it, so a name removed there becomes a rejected hotkey over in that repo. |
| PyBreeze | Subprocess `python -m je_auto_control --execute_str <json>` / `--execute_file <path>`; on Windows the JSON string arrives double-encoded. | Legacy CLI flags; also embeds `je_auto_control.gui.main_widget.AutoControlGUIWidget` and calls `record` / `stop_record` in-process. |
| TestPioneer | Optional extra `gui = ["je_auto_control"]`; `parallel_run` starts `python -m je_auto_control --execute_file <path>`. | `execute_action`, `execute_files`, `RecordingThread`; the `--execute_file` flag. |
| TestPioneer | Optional extra `gui = ["je_auto_control"]`; `parallel_run` starts `python -m je_auto_control --execute_file <path>`. It sets `TEST_PIONEER_ARTIFACT_DIR` for the runner. | `execute_action`, `execute_files`, `RecordingThread`; the `--execute_file` flag. | The flag's exit status, 1 when an action failed (`recorded_failures()` in `__main__.py`); the `<name>_success.json` / `<name>_failure.json` pair, written below the artifact directory for a relative name (`utils/generate_report/report_path.py`).
| WebRunner | Optional extra `autocontrol = ["je_auto_control>=0.0.224"]`; `je_web_runner/utils/autocontrol_bridge/` (`WR_ac_*`) imports this package only when one of those commands runs. | `je_auto_control.utils.executor.action_executor.executor`: `execute_action(actions, raise_on_error=True)`, whose record values it reads in action order, and `known_commands()`; its native commands send `AC_write` (`write_string`), `AC_write_secret` (`secret`, from `WR_ac_basic_auth`), `AC_type_keyboard` (`keycode`), `AC_get_keyboard_keys_table` (the `enter` or `return` key), `AC_locate_image_center` (`image`, `detect_threshold`) and `AC_click_mouse` (`mouse_keycode`, `x`, `y` in this DPI-aware process's coordinates). It refuses `AC_shell_command`, `AC_execute_process`, `AC_add_package_*`, `AC_execute_action`, `AC_execute_files`, `AC_run_agent` and `AC_web_*` by name, so renaming one of them would let it through WebRunner's bridge. |

**Guarded by** `test/unit_test/headless/test_cross_project_contracts.py`: every legacy CLI flag (short and long, run as a
Expand Down
16 changes: 8 additions & 8 deletions architecture_explore.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、

| 指標 | 數值 |
| --- | ---: |
| Python 模組總數(含周邊子專案) | 1,063 |
| 程式碼總行數 | 157,101 |
| Python 模組總數(含周邊子專案) | 1,064 |
| 程式碼總行數 | 157,154 |
| `je_auto_control/utils/` 子套件數 | 310 |
| `AC_*` 動作指令數(`known_commands()` 實測) | 776 |
| 套件門面 `__all__` 公開名稱數 | 1,244 |
Expand Down Expand Up @@ -155,7 +155,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
| 模組 | 行數 | 職責 |
| --- | ---: | --- |
| `je_auto_control/__init__.py` | 1,971 | **套件門面**。集中匯入並再匯出 1,200 個公開名稱,以功能區塊註解分段(callback/exception/executor/a11y/vision/clipboard…)。 |
| `je_auto_control/__main__.py` | 87 | 舊版 argparse 進入點:`-e` 執行單檔、`-d` 執行整個目錄、`--execute_str` 執行 JSON 字串、`-c` 建立專案。 |
| `je_auto_control/__main__.py` | 98 | 舊版 argparse 進入點:`-e` 執行單檔、`-d` 執行整個目錄、`--execute_str` 執行 JSON 字串、`-c` 建立專案。 |
| `je_auto_control/cli.py` | 353 | **主 CLI**(`je_auto_control` console script)。子命令:`run`(含 `--var`/`--dry-run`)、`validate`/`lint`、`list-commands`、`fmt`、`record`、`codegen`、`failure-bundle`、`list-jobs`、`start-server`、`start-rest`、`version`。所有子命令延遲匯入,確保不碰 Qt。 |
| `je_auto_control/api/__init__.py` | 22 | 版本化整合進入點。 |
| `je_auto_control/api/core.py` | 19 | **穩定無頭 API 門面**:只暴露 `execute_action`、`execute_action_with_vars`、`generate_code`、`run_diagnostics`、`create_failure_bundle`、`failure_bundle_on_error`、`FailureBundleOptions`。mypy 型別契約以此為起點,現已擴到整包(見「設定基線」)。 |
Expand Down Expand Up @@ -558,7 +558,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。

### 5.4.12 報表、可觀測性與測試治理

> 34 個套件、約 7,674 行。
> 34 個套件、約 7,716 行。

| 模組 | 行數 | 職責 |
| --- | ---: | --- |
Expand All @@ -573,7 +573,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
| `utils/failure_signature/` | 76 | 把錯誤訊息正規化成穩定的 SHA-256 失敗簽章並分群 |
| `utils/flake_cluster/` | 112 | 以共同失敗 Jaccard 相似度為易碎測試分群 |
| `utils/flakiness/` | 151 | 以執行歷史分析不穩定測試 |
| `utils/generate_report/` | 294 | HTML/JSON/XML 三種報表產生器(Template Method) |
| `utils/generate_report/` | 336 | HTML/JSON/XML 三種報表產生器(Template Method) |
| `utils/media_assert/` | 273 | 媒體斷言:音訊活動與影片動態檢查 |
| `utils/observability/` | 710 | Prometheus 格式指標 + OpenTelemetry 相容 trace + `/metrics` 匯出伺服器 |
| `utils/otlp_export/` | 114 | OTLP/JSON span 匯出 |
Expand Down Expand Up @@ -1077,7 +1077,7 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
| `utils/remote_desktop/` | 56 | 13,014 |
| `utils/executor/` | 8 | 9,560 |
| `utils/usb/` | 17 | 4,572 |
| `je_auto_control/`(頂層 3 檔) | 3 | 2,411 |
| `je_auto_control/`(頂層 3 檔) | 3 | 2,422 |
| `utils/accessibility/` | 14 | 3,143 |
| `wrapper/` | 19 | 3,667 |
| `windows/` | 23 | 1,959 |
Expand All @@ -1092,6 +1092,6 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
| `osx/` | 17 | 925 |
| `autocontrol-lsp/` | 8 | 744 |
| `utils/hotkey/` | 7 | 852 |
| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 681 | 57,038 |
| **總計** | **1,057** | **157,036** |
| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 682 | 57,080 |
| **總計** | **1,058** | **157,089** |

10 changes: 10 additions & 0 deletions docs/updates/2026-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,13 @@ WebRunner's native commands (its U-20261001-28 and -29), `WR_ac_fill_native_file
- **Docs**: `architecture.md` (§2 `scripts/` row, §3 PyPI packages row), `architecture_explore.md` (§5.6 `scripts/dev_release.py` row, §7 `dev.yml` row), `CLAUDE.md` › Key Conventions, `CHANGELOG.md` (Changed). No README or installation page mentions the dev package, so none changed.
- **Files**: `.github/workflows/dev.yml`, `scripts/dev_release.py`, `dev.toml`, `test/unit_test/headless/{test_dev_release,test_dev_toml_parity}.py`, `architecture.md`, `architecture_explore.md`, `CLAUDE.md`, `CHANGELOG.md`.
- **Open items**: the points under **Not running yet**. `Progress.md` is not part of this change, so it does not list them.

## U-20261008-01 · 2026-10-08 · The legacy CLI flags exit 1 when an action failed; reports go below TestPioneer's artifact directory · #feature #cli #report #testpioneer

- **What**: two things TestPioneer needs from a runner (its `docs/artifacts.rst`, "Native Support").
- **Exit status**: `python -m je_auto_control` with `-e`, `-d` or `--execute_str` ended with status 0 when an action of the script failed. `__main__.py` now resets and reads `recorded_failures()`, the count the `run` command already uses, and exits 1 with `error: N action(s) failed`. The rest of the script still runs first.
- **Report location**: with `TEST_PIONEER_ARTIFACT_DIR` set, the HTML, JSON and XML reports asked for under a relative name are written below that directory (`utils/generate_report/report_path.py`). An absolute name, a name that leaves the directory and a directory that cannot be created fall back to the name as given. Without the variable nothing changes.
- **Effect on callers**: a script with a failing action ends with status 1 where it ended with 0. PyBreeze only displays the status; TestPioneer's `parallel_run` reports the runner as failed.
- **Tests**: `test/unit_test/headless/test_cross_project_contracts.py` +3 (real child processes), `test/unit_test/headless/test_report_path.py`.
- **Docs**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`.
- **Files**: `je_auto_control/__main__.py`, `je_auto_control/utils/generate_report/` (`report_path.py` new; three writers), the tests and docs above.
3 changes: 2 additions & 1 deletion docs/updates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here

| ID | Date | Title | Tags | Batch |
|---|---|---|---|---|
| U-20261008-01 | 2026-10-08 | The legacy CLI flags exit 1 when an action failed; reports go below TestPioneer's artifact directory | #feature #cli #report #testpioneer | [2026-10](2026-10.md) |
| U-20261001-09 | 2026-10-01 | CI publishes je_auto_control_dev from the dev branch; dev.toml says what pyproject.toml says | #release #ci #X-13 | [2026-10](2026-10.md) |
| U-20261001-08 | 2026-10-01 | Package gate in front of AC_add_package_to_executor | #security #X-12 | [2026-10](2026-10.md) |
| U-20261001-07 | 2026-10-01 | write_secret / AC_write_secret: type a password without logging, recording or returning it | #done #keyboard #security #webrunner | [2026-10](2026-10.md) |
Expand Down Expand Up @@ -341,7 +342,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here

| File | Period | Entries |
|---|---|---:|
| [2026-10.md](2026-10.md) | 2026-10 | 5 |
| [2026-10.md](2026-10.md) | 2026-10 | 6 |
| [2026-09-e.md](2026-09-e.md) | 2026-09 | 7 |
| [2026-09-d.md](2026-09-d.md) | 2026-09 | 55 |
| [2026-09-c.md](2026-09-c.md) | 2026-09 | 38 |
Expand Down
11 changes: 11 additions & 0 deletions je_auto_control/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
)
from je_auto_control.utils.executor.action_executor import execute_action
from je_auto_control.utils.executor.action_executor import execute_files
from je_auto_control.utils.executor.action_executor import (
recorded_failures, reset_recorded_failures,
)
from je_auto_control.utils.file_process.get_dir_file_list import \
get_dir_files_as_list
from je_auto_control.utils.json.json_file import read_executable_action_json
Expand Down Expand Up @@ -63,6 +66,7 @@ def preprocess_read_str_execute_action(execute_str: str):
type=str, help="execute json str"
)
parsed = vars(parser.parse_args())
reset_recorded_failures()
for key, value in parsed.items():
if value is None:
continue
Expand All @@ -73,6 +77,13 @@ def preprocess_read_str_execute_action(execute_str: str):
handler(value)
if all(value is None for value in parsed.values()):
raise AutoControlArgparseException(argparse_get_wrong_data_error_message)
# Failed actions are recorded and the run goes on, as with ``run``;
# exiting 0 anyway left TestPioneer and a CI step unable to tell a
# failed run from a clean one.
failures = recorded_failures()
if failures:
sys.stderr.write(f"error: {failures} action(s) failed\n")
sys.exit(1)
# The message also goes to stderr: it went only to the log file, so a
# failed -e printed nothing at all.
except AutoControlArgparseException as error:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from je_auto_control.utils.exception.exception_tags import html_generate_no_data_tag_error_message
from je_auto_control.utils.exception.exceptions import AutoControlHTMLException
from je_auto_control.utils.generate_report.report_path import report_path
from je_auto_control.utils.json_store.json_store import atomic_write_text
from je_auto_control.utils.logging.logging_instance import autocontrol_logger
from je_auto_control.utils.test_record.record_test_class import test_record_instance
Expand Down Expand Up @@ -166,7 +167,7 @@ def generate_html_report(html_name: str = "default_name") -> None:

with _lock: # 使用 with 確保 Lock 正確釋放 Ensure lock is released properly
try:
atomic_write_text(html_name + ".html", new_html_string)
atomic_write_text(report_path(html_name + ".html"), new_html_string)
except OSError as error:
# Only logging it made a missing report look like a written one.
raise AutoControlHTMLException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from je_auto_control.utils.exception.exception_tags import cant_generate_json_report_error_message
from je_auto_control.utils.exception.exceptions import AutoControlGenerateJsonReportException
from je_auto_control.utils.generate_report.report_path import report_path
from je_auto_control.utils.json_store.json_store import atomic_write_text
from je_auto_control.utils.logging.logging_instance import autocontrol_logger
from je_auto_control.utils.test_record.record_test_class import test_record_instance
Expand Down Expand Up @@ -44,7 +45,7 @@ def generate_json() -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]
def _write_json_file(file_name: str, data: Dict[str, Dict[str, str]]) -> None:
"""Write ``data`` atomically; raise on failure instead of only logging it."""
try:
atomic_write_text(file_name, json.dumps(data, indent=4, ensure_ascii=False))
atomic_write_text(report_path(file_name), json.dumps(data, indent=4, ensure_ascii=False))
except (OSError, TypeError, ValueError) as error:
raise AutoControlGenerateJsonReportException(
f"cannot write report {file_name!r}: {error!r}") from error
Expand Down
3 changes: 2 additions & 1 deletion je_auto_control/utils/generate_report/generate_xml_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from je_auto_control.utils.exception.exceptions import XMLException
from je_auto_control.utils.generate_report.generate_json_report import generate_json
from je_auto_control.utils.generate_report.report_path import report_path
from je_auto_control.utils.json_store.json_store import atomic_write_text
from je_auto_control.utils.logging.logging_instance import autocontrol_logger
from je_auto_control.utils.xml.change_xml_structure.change_xml_structure import dict_to_elements_tree
Expand Down Expand Up @@ -33,7 +34,7 @@ def _write_xml_file(file_name: str, xml_content: str) -> None:
on as if the report existed.
"""
try:
atomic_write_text(file_name, xml_content)
atomic_write_text(report_path(file_name), xml_content)
except OSError as error:
raise XMLException(f"cannot write report {file_name!r}: {error!r}") from error

Expand Down
39 changes: 39 additions & 0 deletions je_auto_control/utils/generate_report/report_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Where a report file is written.

TestPioneer starts a runner with ``TEST_PIONEER_ARTIFACT_DIR`` set to the
directory that run's output belongs in. A report asked for under a relative
name goes there, so the workflow does not have to say which files to collect.
Outside TestPioneer nothing changes.
"""
import os
from pathlib import Path

from je_auto_control.utils.logging.logging_instance import autocontrol_logger

# Set by TestPioneer for every runner it starts, in a child process and in-process alike.
ARTIFACT_DIR_VARIABLE: str = "TEST_PIONEER_ARTIFACT_DIR"


def report_path(name: str) -> str:
"""Return where the report called ``name`` is written.

With ``TEST_PIONEER_ARTIFACT_DIR`` set, a relative ``name`` is placed below
that directory and the folders it needs are created. ``name`` is returned
as given when the variable is not set, when ``name`` is absolute, when it
would leave the directory (``../x``), and when the folders cannot be
created.
"""
directory = os.environ.get(ARTIFACT_DIR_VARIABLE, "").strip()
if not directory or Path(name).is_absolute():
return name
base = Path(directory).resolve()
target = (base / name).resolve()
if base not in target.parents:
return name
try:
target.parent.mkdir(parents=True, exist_ok=True)
except OSError as error:
autocontrol_logger.error(
"report_path: %r, the artifact directory is not usable: %r", name, error)
return name
return str(target)
Loading
Loading