Skip to content

Repository files navigation

Device Test Runner

Device Test Runner

Device Test Runner 使用 YAML 定義裝置測試流程,執行既有的 Bash、Python、ADB 等命令,並保存每次執行的輸出與 JSON 報告。它負責安排測試步驟、驗證輸出檔案,以及依設定重試失敗步驟;裝置操作仍由你的腳本處理。

目前 runtime/報告版本為 v1.6.1,支援五階段測試流程、步驟逾時、選擇性重試、輸出檔案驗證、程序群組清理與 Ctrl+C 取消。pyproject.toml 仍為 1.6.0,套件版本同步與發佈確認列於 完成條件。Recorder 管理與遠端執行仍在規劃中。

安裝

需要 Python 3.10+ 與 Poetry 2.x。程序群組清理使用 POSIX signal API;目前本機驗證為 macOS,沒有 Windows 支援驗證。在專案目錄執行:

git clone git@github.com:andrew771027/DeviceTestRunner.git
cd DeviceTestRunner
poetry install

Poetry 會建立虛擬環境,依 poetry.lock 安裝專案與開發依賴。後續指令都透過 poetry run 執行。

執行測試流程

poetry run python main.py --config configs/sample.yaml

範例設定會示範失敗情境。2026-09-12 的本機驗證結果為 FAILEDrun_unstable_command 失敗後未重試,下一步被跳過,另有五項必要檔案驗證失敗。完整紀錄見 v1.6.0 完成條件

每次執行會建立獨立的輸出目錄,保存 result.json 與各次嘗試的 stdout、stderr。請讀取報告中的 summary.status 判斷結果:PASSEDFAILEDCANCELLED

常用詞彙

詞彙 意義
Run 一次完整的測試流程
Stage 流程中的階段,例如 setupscenario
Step 階段中設定的一個命令步驟
Attempt 步驟的一次執行;重試會建立新的 attempt
Artifact 測試產生的檔案,例如 log、CSV 或 JSON
Cleanup 清理作業,包括 teardownglobal_teardown

測試生命週期

測試依序執行五個階段:

global_setup
    ↓
setup
    ↓
scenario
    ↓
teardown
    ↓
global_teardown

各階段用途:

Stage Responsibility
global_setup 整次測試執行前的一次性環境準備
setup Test case 執行前的裝置與環境設定
scenario 執行主要測試內容
teardown 清理單一 test case 產生的狀態
global_teardown 整次測試執行完成後的最終清理

階段失敗時,Runner 依下列規則路由:

失敗位置 後續行為
global_setup 停止當前 stage,跳過 setupscenarioteardown,仍執行 global_teardown
setup 停止當前 stage,跳過 scenario,仍執行 teardownglobal_teardown
scenario 停止當前 stage 的剩餘 steps,仍執行 teardownglobal_teardown
teardown 記錄失敗但繼續執行該 stage 的剩餘 steps,之後執行 global_teardown
global_teardown 記錄失敗但繼續執行該 stage 的剩餘 steps

上述為一般失敗路由。進入 setup 區塊後,即使 setup/scenario 取消,仍執行兩個 cleanup stages;run 開始前或 global_setup 取消時只執行 global_teardown。如果 global_setup 成功後、進入 setup 區塊前已觀察到取消,也會跳過 teardown。Cleanup attempt 使用新的 token;這些是受控流程的 best effort,不涵蓋未處理 Python exception 或 KeyboardInterrupt。

最終 summary.statusCANCELLED 優先;沒有取消時,failed step、skipped step 或 required artifact failure 任一存在即為 FAILED,其餘為 PASSED

設定測試流程

以下設定示範裝置命令、重試與 CSV 驗證:

test_case:
  id: power_idle_test
  name: Power Idle Test
  description: Measure device power consumption during idle state.

device:
  serial: ABC123
  product: pixel
  build: build_12345

retry:
  max_attempts: 3
  delay_seconds: 1
  retry_on:
    - timeout
    - device_offline
    - artifact_missing

lifecycle:
  global_setup:
    steps:
      - name: check_environment
        type: command
        command: echo "Check environment"
        timeout_second: 30

  setup:
    steps:
      - name: check_device
        type: command
        command: adb -s ABC123 get-state
        timeout_second: 30

  scenario:
    steps:
      - name: run_idle_scenario
        type: command
        command: |
          printf "timestamp,power\n1,110\n" > result.csv
        timeout_second: 300

  teardown:
    steps:
      - name: restore_device
        type: command
        command: adb -s ABC123 shell input keyevent HOME
        timeout_second: 30

  global_teardown:
    steps:
      - name: finalize
        type: command
        command: echo "Finalize test run"
        timeout_second: 30

artifact:
  output_dir: artifacts
  validation:
    rules:
      - name: check_result_exists
        type: exists
        path: result.csv

      - name: check_result_content
        type: csv_content
        path: result.csv
        after_step: run_idle_scenario
        required: true
        required_columns:
          - timestamp
          - power
        min_rows: 1

after_step 將 validation rule 綁定到指定 step,runner 會在該 step 每次 command 成功後立即驗證。required 預設為 true:required rule 失敗會使 attempt 失敗,且只有 failure type 出現在 retry.retry_on、尚未達 max_attempts 時才重試;required: false 的失敗仍寫入 report,但不影響 step 或 run 狀態。Lifecycle 結束後會再次對所有規則執行 final validation,包括有 after_step 的規則。YAML 未設定 retry_on 時預設為空清單,因此不重試;nonecancelled 與未知值會被拒絕。直接使用 Python RetryConfig() 的預設清單不同,詳見 v1.6.0 的設定說明

透過 Python API 取消

CLI 第一次 Ctrl+C/SIGINT 會呼叫 token.cancel();第二次會拋出 KeyboardInterrupt,可能中斷 cleanup 與報告寫入。CLI 正常完成回傳 0、FAILED 回傳 1、CANCELLED 或 run 期間的 KeyboardInterrupt 回傳 130。SIGTERM 尚未接到 token。

Python API 由呼叫端持有 token 並呼叫 cancel()。以下示範沿用載入的 configuration,在兩秒後提出取消請求:

from pathlib import Path
from threading import Timer

from runner.artifact import ArtifactManager
from runner.artifact_validator import ArtifactValidator
from runner.cancellation import CancellationToken
from runner.config import ConfigLoader
from runner.executor import SubprocessExecutor
from runner.failure import FailureClassifier
from runner.process import ProcessTerminator
from runner.reporter import JsonReporter
from runner.runner import DeviceTestRunner

config = ConfigLoader().load("configs/sample.yaml")
classifier = FailureClassifier()
runner = DeviceTestRunner(
    executor=SubprocessExecutor(
        project_directory=Path.cwd(),
        failure_classifier=classifier,
        process_terminator=ProcessTerminator(),
    ),
    artifact_manager=ArtifactManager(config.artifact.output_dir),
    artifact_validator=ArtifactValidator(),
    failure_classifier=classifier,
    reporter=JsonReporter(),
    show_console_output=False,
)
token = CancellationToken()
timer = Timer(2.0, token.cancel)
timer.start()
try:
    result = runner.run(config, cancellation_token=token)
    print(result.summary.status)
finally:
    timer.cancel()
    timer.join()

Executor 每 0.1 秒檢查取消與 timeout。每次 attempt 建立獨立 session,清理時對 process group 送 SIGTERM,預設等待 2 秒;仍有程序則送 SIGKILL,再等最多 2 秒。stdout/stderr reader 各有 2 秒 join 上限。上述 timer 不是兩秒內返回的保證。

同群組的 child/grandchild 已有清理測試;自行脫離群組的程序,以及直接 process 正常退出後留下的背景程序,不在目前保證內。詳見 Process Lifecycle。Cancelled attempt 不做 attempt validation 或 retry,但 run 最後仍驗證所有 artifacts。

從 v1.6.0 升級時,SubprocessExecutor 建構子需新增 process_terminator。從更舊版本升級時,Python 呼叫端也需調整 SubprocessExecutor.execute(..., cancellation_token=...),以及手動建構 result dataclasses 時的新欄位。run(config) 仍可不傳 token。完整差異見 Architecture

輸出檔案

每次測試執行會建立獨立的 run directory。

範例:

artifacts/
└── power_idle_test_20260722_223000/
    ├── result.json
    ├── global_setup/
    │   └── check_environment/
    │       ├── attempt_1.stdout.log
    │       └── attempt_1.stderr.log
    ├── scenario/
    │   └── run_idle_scenario/
    │       ├── attempt_1.stdout.log
    │       ├── attempt_1.stderr.log
    │       ├── attempt_2.stdout.log
    │       └── attempt_2.stderr.log
    └── result.csv

result.json 包含:

  • Test case metadata
  • Device metadata
  • Start time
  • End time
  • Total duration
  • Final status
  • Lifecycle stage results
  • Step results
  • stdout and stderr artifact paths
  • Validation results
  • Retry information
  • Per-attempt failure type, timed_out and cancelled
  • Metadata cancel_requested and summary cancelled_steps

相對路徑的 artifact validation rule 會以該次 run directory 為基準解析。每一次 retry 都有獨立的 stdout/stderr log,避免後一次 attempt 覆蓋先前的診斷資訊。

報告範例

以下是單一步驟、無 artifact rules 的示意資料,並非 sample.yaml 的實際執行報告。

{
  "metadata": {
    "test_case_id": "example_001",
    "test_case_name": "Example",
    "test_case_description": "Print one line.",
    "device_serial": "demo",
    "device_product": "demo",
    "device_build": "demo",
    "runner_version": "1.6.1",
    "started_at": "2026-09-12T00:00:00+00:00",
    "finished_at": "2026-09-12T00:00:01+00:00",
    "cancel_requested": false
  },
  "summary": {
    "status": "PASSED",
    "configured_steps": 1,
    "executed_steps": 1,
    "passed_steps": 1,
    "failed_steps": 0,
    "cancelled_steps": 0,
    "skipped_steps": 0,
    "configured_artifact_rules": 0,
    "passed_artifact_rules": 0,
    "failed_artifact_rules": 0,
    "failed_required_artifact_rules": 0,
    "duration_seconds": 1.0
  },
  "step_results": [
    {
      "stage": "scenario",
      "name": "hello",
      "command": "echo hello",
      "attempts": 1,
      "success": true,
      "cancelled": false,
      "attempt_results": [
        {
          "attempt": 1,
          "success": true,
          "failure_type": "none",
          "timed_out": false,
          "cancelled": false,
          "exit_code": 0,
          "duration_seconds": 0.1,
          "stdout": "hello\n",
          "stderr": "",
          "stdout_log_path": "artifacts/example/scenario/hello/attempt_1.stdout.log",
          "stderr_log_path": "artifacts/example/scenario/hello/attempt_1.stderr.log",
          "error": null,
          "artifact_validation_results": []
        }
      ],
      "duration_seconds": 0.2
    }
  ],
  "artifact_dir": "artifacts/example",
  "artifact_validation_results": []
}

結果消費端應以 summary.status 判斷 run;RunResult.passed 目前只檢查 step success,無法完整反映取消請求或 final artifact failure。StepAttemptResult.passed 也只檢查 exit code,請使用 success 與 failure flags。

架構

YAML Configuration
        ↓
Config Loader
        ↓
RunnerConfig
        ↓
DeviceTestRunner
        ├── Lifecycle Orchestration
        ├── CancellationToken
        ├── RetryPolicy
        ├── SubprocessExecutor
        ├── ArtifactManager
        ├── ArtifactValidator
        └── JsonReporter
                ↓
       result.json / per-attempt logs / validation results

執行專案測試

執行所有測試:

poetry run pytest

顯示較完整輸出:

poetry run pytest -v

只執行 retry 相關測試:

poetry run pytest -m retry

只執行 cancellation 標記測試(不等同完整 suite):

poetry run pytest -m cancelled

只執行 artifact 相關測試:

poetry run pytest -m artifact

歷史驗證紀錄(2026-09-12,Python 3.14):.venv/bin/python -m pytest -q153 passed in 39.39s。150 個測試函式皆有 Given/When/Then 說明;參數化後共 153 個案例。

本次 v1.6.1 驗證與 162 個測試函式/168 個案例的對照,見 完成條件測試矩陣

持續整合

CI workflow 會在 push 至 main 或建立以 main 為目標的 pull request 時執行。流程安裝 Python、Poetry 與專案依賴後,執行 pytest。

在本機執行相同的測試:

poetry install
poetry run pytest

手動更新版本文件

.github/workflows/manual.ymlworkflow_dispatch 接受 release_version(如 1.6.0,不含 v)、target_branch(實際 checkout 與 push 的既有分支)及 model。GitHub Actions 的 Use workflow from 選項決定讀取哪個分支的 workflow 定義。

流程先安裝專案、跑 baseline tests,再請 Codex 更新文件與測試說明;驗證 pytest -qgit diff --check 與 Given/When/Then 行數後,有變更才 commit 並直接 push 到目標分支。這個流程不建立 PR、tag 或 GitHub Release。

Repository secret 名稱為 OPENAI_API_KEY,在 CLI invocation 映射成 CODEX_API_KEY--approve-for-me--sandbox 不同時使用。目標分支需允許這次 push,API 帳戶需有可用額度及模型存取權。

開發與版本規劃

目前先補齊單機執行與取消流程,再加入可重用設定與 recorder 管理:

  1. v1.6.1:已實作程序群組終止、輸出串流收尾與 SIGINT handler;平台驗證、版本同步與發佈待完成。
  2. v1.6.2:整次 run 的逾時設定。
  3. v1.6.3:取消後的清理範圍、時間限制與部分結果保存。
  4. v1.7.0~v1.7.2:YAML 靜態變數、環境變數與執行資訊。
  5. v1.8 之後:recorder、hooks、執行摘要、批次與並行執行。
  6. v2.0:controller/worker 遠端執行。

詳細範圍與 keyword-driven 設計見 Roadmap。歷史變更見 CHANGELOG。提交與發佈前請使用 提交檢查清單

文件

文件 用途
架構 v1.6.1 元件、取消路由、報告欄位與相容性
測試指南 Pytest 工具、fixture、Mock 與各版用法
測試矩陣 v1.6.1 功能對應的測試與覆蓋限制
驗收條件 v1.6.1 可觀察的預期行為
完成條件 v1.6.1 驗證紀錄與待完成的發佈項目

docs/ 內的舊版文件保留當時的設計與介面,使用時請確認版本。

About

Define device tests in YAML. Run commands, validate output files and review results.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages