Newer
Older
TIASShot / scripts / verify / run_hwtest.py
"""TIASshot の実機テストのランナー(PLAN_06.ユーザーが会話の中で実機テストを合図したときだけ実行する).

実機のカメラ(Lumenera / ImagingSource)と光源(シリアルポート)を操作する.撮影の本番中に実行しないこと.
NUnit テスト・セルフテストは実行しない(それぞれ run_tests.py・run_selftest.py).

手順:
  1. ランナーがビルドする構成(Debug|x64)の出力 bin/x64/Debug・obj/x64/Debug を削除する(--no-clean で省略)
  2. Debug|x64 をビルドする(HWTEST フラグ入り)
  3. TIASshot.exe が起動していないことを確かめる(起動中はカメラ・光源を使用中=撮影の本番中の可能性があるため実行しない)
  4. TIASshot.exe --hwtest --camera <機種> を実行ファイルのフォルダで実行し,終了コードと結果ファイルで判定する
     (実際のフォームが表示される.保存先は %TEMP%\\TIASshot-hwtest\\work-* に差し替えられる)

出力: .verify/hwtest/run-<日時>/ に build.log・hwtest-result.txt・summary.json
終了コード: 0=全項目合格,1=不合格あり(ビルド失敗・検査の不合格・タイムアウト),
            2=前提条件を満たさない(ツール未導入・TIASshot 起動中・指定機種が見つからない・引数不正・--with-chart(未実装))

使い方(リポジトリルートで実行):
  python scripts/verify/run_hwtest.py --camera lucam
  python scripts/verify/run_hwtest.py --camera iscam
  python scripts/verify/run_hwtest.py --camera iscam --no-clean   # Debug 出力を消さない(開発時の短縮用)
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import subprocess
import sys

from run_selftest import (BUILD_TIMEOUT_SEC, EXE, EXIT_FAIL, EXIT_OK, EXIT_PRECONDITION, REPO, SLN,
                          PreconditionError, clean_build_outputs, find_vs, run_logged)

# 実機テスト内部のウォッチドッグ(全体 10 分)より長くする.超えたらランナーがプロセスを打ち切る
HWTEST_TIMEOUT_SEC = 720

# 実機テスト(TIASshot.exe --hwtest)の終了コード
HW_PASS = 0
HW_FAIL = 1
HW_ERROR = 2
HW_TIMEOUT = 3


def is_tiasshot_running() -> bool:
    """TIASshot.exe が起動しているか(人間が撮影中ならカメラ・光源を使用中のため実機テストを行わない)."""
    proc = subprocess.run(["tasklist", "/FI", "IMAGENAME eq TIASshot.exe", "/FO", "CSV", "/NH"],
                          capture_output=True, check=False)
    out = proc.stdout.decode("cp932", errors="replace")
    return "tiasshot.exe" in out.lower()


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--camera", required=True, choices=["lucam", "iscam"], help="検査するカメラの機種")
    parser.add_argument("--with-chart", action="store_true",
                        help="チャートが必要な検査も行う(PLAN_06 フェーズ 2.未実装のため終了コード 2 になる)")
    parser.add_argument("--no-clean", action="store_true",
                        help="Debug 出力(bin/obj の x64/Debug)を削除しない(開発時の短縮用)")
    args = parser.parse_args()
    # エージェント(Git Bash 経由)が読めるよう,本ランナーの出力は UTF-8 に固定する
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")

    stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
    run_dir = REPO / ".verify" / "hwtest" / f"run-{stamp}"
    run_dir.mkdir(parents=True, exist_ok=True)
    summary: dict = {"run_dir": str(run_dir), "camera": args.camera, "with_chart": args.with_chart,
                     "steps": [], "result": None}

    def step(name: str, ok: bool, detail: str) -> None:
        summary["steps"].append({"name": name, "ok": ok, "detail": detail})
        print(f"[{'OK' if ok else 'NG'}] {name}: {detail}", flush=True)

    exit_code = EXIT_OK
    try:
        vs = find_vs()
        msbuild = vs / "MSBuild" / "Current" / "Bin" / "MSBuild.exe"
        if not msbuild.exists():
            raise PreconditionError(f"MSBuild.exe が見つかりません: {msbuild}")

        if is_tiasshot_running():
            raise PreconditionError("TIASshot.exe が起動しています(撮影の本番中の可能性.カメラ・光源を使用中のため実機テストを行いません)")

        if not args.no_clean:
            clean_build_outputs()
            step("clean", True, "Debug 出力(bin/obj の x64/Debug)を削除")

        code = run_logged([str(msbuild), str(SLN), "/t:Build", "/p:Configuration=Debug", "/p:Platform=x64",
                           "/nologo", "/v:minimal"], run_dir / "build.log", BUILD_TIMEOUT_SEC)
        step("build", code == 0, f"Debug|x64 ビルド exit {code}(build.log)")
        if code != 0:
            exit_code = EXIT_FAIL
            raise RuntimeError("ビルド失敗")

        result_file = run_dir / "hwtest-result.txt"
        cmd = [str(EXE), "--hwtest", "--camera", args.camera, "--hwtest-out", str(result_file)]
        if args.with_chart:
            cmd.append("--with-chart")
        summary["command"] = " ".join(cmd)
        try:
            # デバイス状態ファイル({DeviceName}.xml)は相対パスで読まれるため,通常起動と同じく実行ファイルのフォルダで実行する
            proc = subprocess.run(cmd, cwd=EXE.parent, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                                  timeout=HWTEST_TIMEOUT_SEC, check=False)
            code = proc.returncode
        except subprocess.TimeoutExpired:
            code = None
        text = result_file.read_text(encoding="utf-8-sig", errors="replace") if result_file.exists() else ""
        heads = [ln for ln in text.splitlines() if ln.startswith("[") or ln.startswith("RESULT:")]
        summary["hwtest_exit_code"] = code
        summary["hwtest_lines"] = heads
        if code is None:
            detail = f"{HWTEST_TIMEOUT_SEC} 秒で終わらないためランナーが打ち切りました(hwtest-result.txt)"
        else:
            detail = f"exit {code}(hwtest-result.txt)"
        step("hwtest", code == HW_PASS, detail)
        for ln in heads:
            print(f"    {ln}", flush=True)
        if code == HW_ERROR:
            # 指定機種が見つからない・引数不正・未実装の指定 → 実機テスト自体を行えない(検証不能)
            exit_code = EXIT_PRECONDITION
        elif code != HW_PASS:
            exit_code = EXIT_FAIL
    except PreconditionError as ex:
        step("precondition", False, str(ex))
        exit_code = EXIT_PRECONDITION
    except RuntimeError as ex:
        step("abort", False, str(ex))
        exit_code = EXIT_FAIL
    except subprocess.TimeoutExpired as ex:
        step("timeout", False, f"{ex.cmd} が {ex.timeout} 秒で終わりません")
        exit_code = EXIT_FAIL

    summary["result"] = {EXIT_OK: "PASS", EXIT_FAIL: "FAIL", EXIT_PRECONDITION: "PRECONDITION"}[exit_code]
    (run_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"RESULT: {summary['result']}  (出力: {run_dir})", flush=True)
    return exit_code


if __name__ == "__main__":
    sys.exit(main())