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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,8 @@ Compose any of the flags above; the dispatcher applies impact selection → tag

**Impact-based selection.** `--affected-by KIND:VALUE` (repeatable; `KIND` is `locator`, `url`, `template` or `command`; `@file` reads one per line) runs only the action files that use one of those names. `--changed-since REF` runs the action files `git diff REF...HEAD` changed, plus every file sharing a locator or template with them, so changing a setup file that saves `login_button` also runs the tests that use it (URLs and commands are not followed: almost every file shares them). Both can be combined; tags, `--rerun-failed` and `--shard` then apply to what they selected. `--impact-cache PATH` keeps the impact index between runs.

`python -m je_web_runner` exits with status 1 when an action or a file of the run failed, after the rest has run, and prints `error: N action(s) or file(s) failed` on stderr. Under [TestPioneer](https://github.com/Integration-Automation/TestPioneer), which sets `TEST_PIONEER_ARTIFACT_DIR`, a report asked for under a relative name (JSON, XML, HTML, JUnit, Allure, interactive HTML, and `generate_all_reports`) is written below that directory.

## Test Record

```python
Expand Down
2 changes: 2 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,8 @@ python -m je_web_runner -d ./actions --changed-since origin/main --impact-cache

**按影响范围挑选。** `--affected-by KIND:VALUE`(可重复;`KIND` 为 `locator`、`url`、`template` 或 `command`;`@文件` 每行读一个)只跑用到这些名称的动作文件。`--changed-since REF` 跑 `git diff REF...HEAD` 改到的动作文件,以及与它们共用 locator 或模板的文件,所以改了存放 `login_button` 的设置文件,用到它的测试也会跑(URL 与命令不追:几乎每个文件都共用)。两者可以并用;之后才应用 tag、`--rerun-failed` 与 `--shard`。`--impact-cache PATH` 在多次执行之间保留影响索引。

`python -m je_web_runner` 在该次执行有动作或文件失败时,会在其余部分执行完之后以状态 1 结束,并在 stderr 打印 `error: N action(s) or file(s) failed`。由 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 启动时(它会设置 `TEST_PIONEER_ARTIFACT_DIR`),以相对名称要求的报告(JSON、XML、HTML、JUnit、Allure、交互式 HTML,以及 `generate_all_reports`)会写到该目录底下。

## 测试记录

```python
Expand Down
2 changes: 2 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,8 @@ python -m je_web_runner -d ./actions --changed-since origin/main --impact-cache

**依影響範圍挑選。** `--affected-by KIND:VALUE`(可重複;`KIND` 為 `locator`、`url`、`template` 或 `command`;`@檔案` 每行讀一個)只跑用到這些名稱的動作檔。`--changed-since REF` 跑 `git diff REF...HEAD` 改到的動作檔,以及與它們共用 locator 或範本的檔案,所以改了存放 `login_button` 的設定檔,用到它的測試也會跑(URL 與命令不追:幾乎每個檔都共用)。兩者可以並用;之後才套用 tag、`--rerun-failed` 與 `--shard`。`--impact-cache PATH` 在多次執行之間保留影響索引。

`python -m je_web_runner` 在該次執行有動作或檔案失敗時,會在其餘部分執行完之後以狀態 1 結束,並在 stderr 印出 `error: N action(s) or file(s) failed`。由 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 啟動時(它會設定 `TEST_PIONEER_ARTIFACT_DIR`),以相對名稱要求的報告(JSON、XML、HTML、JUnit、Allure、互動式 HTML,以及 `generate_all_reports`)會寫到該目錄底下。

## 測試記錄

```python
Expand Down
2 changes: 1 addition & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ python -m je_web_runner -d DIR [--tag/--exclude-tag] [--rerun-failed LEDGER] [--
| Jeffrey_RPA | `JeffreyRPA/webrunner_je_only.py` and `webrunner_novelai.py` insert a WebRunner working tree at `sys.path[0]`, so the **live working tree** is imported. The tree is `WEBRUNNER_PATH`, else `bot_config.json` `webrunner_path`, else the sibling `D:\Codes\WebRunner`, else the installed package (one rule, `JeffreyRPA/_webrunner_location.py`). Before importing it sets `WEBRUNNER_LOG_PATH` (if unset) to its own `webrunner.log`; `Jeffrey_RPA/requirements.txt` requires `je_web_runner>=0.0.88` because of the four wrapper methods listed here. | `TestObject`, `webdriver_wrapper_instance` and its methods `get_current_url`, `get_title`, `save_screenshot`, `add_script_to_evaluate_on_new_document`; for its je_web_runner variant, `set_driver(..., install_driver=False)` (start without webdriver-manager) and `executor.set_raise_wrapper_errors`; `test/test_je_facade.py` (in Jeffrey_RPA) imports `je_web_runner.webdriver.webdriver_wrapper` and checks `_webdriver_dict`. |
| Jeffrey_RPA tests | `JeffreyRPA/conftest.py` hooks the import of `je_web_runner.utils.logging.loggin_instance` to park the log file outside the repo. | That exact, misspelled module path. **Do not rename it.** |
| AutoControlGUI | Optional `utils/webrunner_bridge/bridge.py` (not a declared dependency; found with `importlib.util.find_spec`, imported lazily). | `je_web_runner.utils.executor.action_executor`: `execute_one` (one action through the gates, raising `WebRunnerExecuteException`), falling back to `executor.event_dict` on releases without it; `executor.event_dict` `WR_*` keys for listing; the commands `WR_get_webdriver_manager`, `WR_to_url`, `WR_quit`, `WR_save_screenshot`, `WR_get_current_url` (guarded by `test/unit_test/test_public_api.py`); `je_web_runner.utils.exception.exceptions.WebRunnerException` as the failure it wraps. |
| TestPioneer | Declared dependency; `from je_web_runner import execute_action` in-process. | `execute_action`. |
| TestPioneer | Declared dependency; `from je_web_runner import execute_action` in-process, and `parallel_run` spawns `python -m je_web_runner --execute_file <script>`. It sets `TEST_PIONEER_ARTIFACT_DIR` for both. | `execute_action`; the exit status of the CLI, 1 when an action or file failed (`utils/cli/failure_count.py`); the `<name>_success.json` / `<name>_failure.json` pair, written below the artifact directory for a relative name (`utils/generate_report/report_path.py`). |
| PyBreeze | Subprocess `python -m je_web_runner --execute_str <json>` / `--execute_file <path>`, reading stdout. | Legacy CLI flags, Windows double-encoded `--execute_str`, results printed to stdout; guarded by `test/unit_test/test_legacy_cli_contract.py`. |

**Outbound (required):** `je_action_core>=0.0.3` (PyPI), the executor core shared with APITestka, LoadDensity,
Expand Down
13 changes: 13 additions & 0 deletions docs/source/Eng/doc/cli/cli_doc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,16 @@ Error Handling

If no arguments are provided, WebRunner raises a ``WebRunnerExecuteException``.
All errors are printed to stderr and the process exits with code 1.

A failed action does not stop its list: the remaining actions run, and so do the remaining files of
``-d``. Once everything has run, the process exits with code 1 and prints
``error: N action(s) or file(s) failed`` when anything failed, and with code 0 otherwise.

Running Under TestPioneer
-------------------------

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 (JSON, XML, HTML, JUnit, Allure,
interactive HTML, and ``generate_all_reports``) is then written below that directory, and the
folders it needs are created. An absolute name, and a name that would leave the directory, are
used as given. Without the variable nothing changes.
11 changes: 11 additions & 0 deletions docs/source/Zh/doc/cli/cli_doc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,14 @@ WebRunner 可以直接從命令列使用 ``je_web_runner`` 模組執行。安裝

若未提供任何參數,WebRunner 會引發 ``WebRunnerExecuteException``。
所有錯誤會輸出到 stderr,程序以代碼 1 退出。

失敗的動作不會中止所在的清單:其餘動作會繼續執行,``-d`` 的其餘檔案也是。全部執行完之後,
只要有任何失敗,程序就以代碼 1 退出並印出 ``error: N action(s) or file(s) failed``,否則以 0 退出。

在 TestPioneer 之下執行
-----------------------

TestPioneer 啟動 runner 時會把 ``TEST_PIONEER_ARTIFACT_DIR`` 設為該次執行的輸出目錄。
以相對名稱要求的報告(JSON、XML、HTML、JUnit、Allure、互動式 HTML,以及 ``generate_all_reports``)
會寫到該目錄底下,所需的資料夾會自動建立。絕對路徑,以及會離開該目錄的名稱,則照原樣使用。
沒有設定這個變數時,行為不變。
12 changes: 12 additions & 0 deletions docs/updates/2026-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,15 @@ Index and query commands: [README.md](README.md). New entries go at the end.
- **Result / numbers**: built in a throwaway worktree with `build` 1.6.1 and `setuptools` 84.0.0 and no isolation: sdist and wheel build, `twine check` passes, the wheel has the 664 `je_web_runner` files.
- **Files**: `.github/requirements/publish.in`, `.github/requirements/publish.txt`, `.github/workflows/publish_stable.yml`, `.github/workflows/test_dev.yml`, `test/unit_test/test_workflow_actions.py`, `test/unit_test/test_dev_release.py`, `architecture.md`.
- **Open items**: none.

## U-20261008-01 · 2026-10-08 · The CLI exits 1 when the run 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_web_runner` ended with status 0 whatever happened, because the executor records a failed action and runs the next one. `cli_main.main` now runs the dispatch inside `FailureCount` (`utils/cli/failure_count.py`), which wraps the executor's reporter and counts `on_failure`, and exits 1 with `error: N action(s) or file(s) failed`. The rest of the run still happens first. A file that failed where this process's executor cannot see it, in `--parallel-mode process` or `async`, is counted through `note_failed_file`. LoadDensity made the same change in its PR #118.
- **Report location**: with `TEST_PIONEER_ARTIFACT_DIR` set, a report asked for under a relative name is written below that directory (`utils/generate_report/report_path.py`): JSON, XML, HTML, JUnit, Allure, interactive HTML, and `generate_all_reports`, which resolves its base name once so its manifest lists the files where they are. 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 reads stdout and only displays the status. `execute_action` and what it returns are unchanged.
- **Completion**: process workers count failed actions even when recording is disabled. Ledger and dependency execution use the current thread's failure count, avoiding failures from another concurrent file. Unreadable files are counted and remaining directory files still execute in sequential, thread, process and async modes. Invalid artifact paths fall back to the supplied name; the Allure directory is resolved before constructing the manifest.
- **Tests**: `test/unit_test/test_failure_count.py`, `test/unit_test/test_report_path.py`, `test/unit_test/test_legacy_cli_contract.py` (including real process workers), and file-selection regression tests.
- **Validation**: final Windows / Python 3.14 unit suite: 4868 passed, 5 skipped (63 subtests passed); integration suite: 43 passed, 19 skipped. A real TestPioneer workflow against this checkout ran a passing and a failing WebRunner in parallel, verified exit codes 0 / 1, generated JSON / XML / HTML / JUnit reports and their manifests under the supplied artifact directories, imported the cases, and ran the next workflow step. Six Grid e2e tests skipped because no Selenium Grid was available.
- **Docs**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` (CLI Usage), `docs/source/Eng/doc/cli/cli_doc.rst` and its `Zh` counterpart, `architecture.md`.
- **Files**: `je_web_runner/utils/cli/` (`failure_count.py` new, `cli_main.py`), `je_web_runner/utils/generate_report/` (`report_path.py` new; seven 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 CLI exits 1 when the run failed; reports go below TestPioneer's artifact directory | #feature #cli #report #testpioneer | [2026-10](2026-10.md) |
| U-20261001-53 | 2026-10-01 | The publish jobs build with the locked setuptools instead of downloading the newest | #ci #security #X-13 | [2026-10](2026-10.md) |
| U-20261001-52 | 2026-10-01 | Dependabot watches the hash-locked requirements; a guard keeps the publish jobs on them | #ci #security #X-13 | [2026-10](2026-10.md) |
| U-20261001-51 | 2026-10-01 | The autocontrol extra asks for je_auto_control 0.0.225, the first release with AC_write_secret | #change #dependencies #autocontrol #X-27 | [2026-10](2026-10.md) |
Expand Down Expand Up @@ -142,5 +143,5 @@ 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 | 53 |
| [2026-10.md](2026-10.md) | 2026-10 | 54 |
| [2026-09.md](2026-09.md) | 2026-09 | 25 |
50 changes: 43 additions & 7 deletions je_web_runner/utils/cli/cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pathlib import Path
from typing import Sequence

from je_web_runner.utils.cli.failure_count import FailureCount, note_failed_file, thread_failure_count
from je_web_runner.utils.exception.exception_tags import argparse_get_wrong_data
from je_web_runner.utils.exception.exceptions import WebRunnerExecuteException
from je_web_runner.utils.executor.action_executor import execute_action, execute_files
Expand Down Expand Up @@ -179,11 +180,15 @@ def _run_one_file(path: str) -> bool:
"""Execute one file; return True if no failures were recorded."""
failure_marker = "program_exception"
baseline = len(test_record_instance.test_record_list)
failure_baseline = thread_failure_count()
failed = False
try:
execute_action(read_action_json(path))
except Exception:
failed = True
note_failed_file()
if failure_baseline is not None:
return not failed and thread_failure_count() == failure_baseline
new_records = test_record_instance.test_record_list[baseline:]
if any(record.get(failure_marker, "None") != "None" for record in new_records):
failed = True
Expand All @@ -209,7 +214,9 @@ def _run_one_file_isolated(path: str) -> tuple[str, bool, list]:
baseline = len(_records.test_record_list)
failed = False
try:
_execute(_read_json(path))
with FailureCount() as failures:
_execute(_read_json(path))
failed = bool(failures.failed)
except Exception: # pylint: disable=broad-except
# Failure path — the tuple we return signals it to the parent.
failed = True
Expand All @@ -224,7 +231,7 @@ def _run_with_dependencies(
ledger_path: str | None,
) -> None:
"""Run files in topological order, skipping downstream when upstream fails."""
graph = build_dependency_graph(files)
graph = build_dependency_graph(files, allow_invalid=True)
ordered = topological_order(graph)
failed_set: set = set()
for path in ordered:
Expand Down Expand Up @@ -278,7 +285,12 @@ def _select_files(directory: str, selection: FileSelection) -> list:
def _run_sequential(files, ledger_path: str | None) -> None:
"""Sequential execution with optional ledger recording."""
if not ledger_path:
execute_files(files)
for path in files:
try:
execute_files([path])
except Exception as error:
web_runner_logger.error(f"execute_file, path: {path}, failed: {error!r}")
note_failed_file()
return
for path in files:
passed = _run_one_file(path)
Expand All @@ -290,18 +302,37 @@ def _run_with_process_pool(files, parallel: int, ledger_path: str | None) -> Non
with ProcessPoolExecutor(max_workers=parallel) as pool:
for path, passed, records in pool.map(_run_one_file_isolated, files):
test_record_instance.test_record_list.extend(records)
if not passed:
note_failed_file()
if ledger_path:
record_run(ledger_path, path, passed=passed)


def _run_with_asyncio(files, parallel: int, ledger_path: str | None) -> None:
"""asyncio branch: one browser, one context per file, at most ``parallel`` files at a time."""
from je_web_runner.utils.async_executor.executor import run_action_lists
results = run_action_lists([read_action_json(path) for path in files], concurrency=max(1, parallel))
for path, (record, failed) in zip(files, results):
loaded_files = []
action_lists = []
for path in files:
try:
actions = read_action_json(path)
except Exception as error:
web_runner_logger.error(f"execute_file, path: {path}, failed: {error!r}")
note_failed_file()
if ledger_path:
record_run(ledger_path, path, passed=False)
continue
loaded_files.append(path)
action_lists.append(actions)
if not loaded_files:
return
results = run_action_lists(action_lists, concurrency=max(1, parallel))
for path, (record, failed) in zip(loaded_files, results):
for key, value in record.items():
print(key)
print(value)
if failed:
note_failed_file()
if ledger_path:
record_run(ledger_path, path, passed=not failed)

Expand Down Expand Up @@ -330,7 +361,7 @@ def _run_dir(
parallel_mode: str = "thread",
) -> None:
files = _select_files(directory, selection)
if any(build_dependency_graph(files).get(path) for path in files):
if any(build_dependency_graph(files, allow_invalid=True).values()):
_run_with_dependencies(files, ledger_path)
return
if parallel_mode == "async":
Expand Down Expand Up @@ -440,5 +471,10 @@ def main(argv: Sequence[str] | None = None) -> int:
args = parser.parse_args(argv)
if not _has_any_action(args):
raise WebRunnerExecuteException(argparse_get_wrong_data)
_dispatch(args)
# A failed action does not stop its list, so the run is judged once everything has run.
with FailureCount() as failures:
_dispatch(args)
if failures.failed:
print(f"error: {failures.failed} action(s) or file(s) failed", file=sys.stderr)
return 1
return 0
Loading
Loading