"""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-* に差し替えられる)
--with-chart を付けると,チャート不要の検査(preview〜reconnect)の代わりにチャートが必要な検査
(calibrate・shot・close-saving・close-calibrating.校正を止めない.カメラの前に舌診チャートがはまっている前提)を行う.
チャート不要の検査と続けて実行する(connect・cleanup は両方で行う).
出力: .verify/hwtest/run-<日時>/ に build.log・hwtest-result.txt・summary.json
終了コード: 0=全項目合格,1=不合格あり(ビルド失敗・検査の不合格・タイムアウト),
2=前提条件を満たさない(ツール未導入・TIASshot 起動中・指定機種が見つからない・引数不正)
使い方(リポジトリルートで実行):
python scripts/verify/run_hwtest.py --camera lucam
python scripts/verify/run_hwtest.py --camera iscam
python scripts/verify/run_hwtest.py --camera iscam --with-chart # チャートが必要な検査
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 分・--with-chart 20 分)より長くする.超えたらランナーがプロセスを打ち切る
HWTEST_TIMEOUT_SEC = 720
HWTEST_WITH_CHART_TIMEOUT_SEC = 1320
# 実機テスト(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="チャートが必要な検査(calibrate・shot・close-saving・close-calibrating.PLAN_06 フェーズ 2)を行う"
"(チャート不要の検査は行わない.舌診チャートをはめてから実行する)")
parser.add_argument("--keep", action="store_true",
help="合格時も作業フォルダ(%%TEMP%%/TIASshot-hwtest/work-*)を残し,--with-chart では証拠の画像(evidence/*.png)も保存する(TIASshot.exe の --hwtest-keep)")
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, "keep": args.keep,
"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.keep:
cmd.append("--hwtest-keep")
if args.with_chart:
cmd.append("--with-chart")
summary["command"] = " ".join(cmd)
timeout_sec = HWTEST_WITH_CHART_TIMEOUT_SEC if args.with_chart else HWTEST_TIMEOUT_SEC
try:
# デバイス状態ファイル({DeviceName}.xml)は相対パスで読まれるため,通常起動と同じく実行ファイルのフォルダで実行する
proc = subprocess.run(cmd, cwd=EXE.parent, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=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"{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())