diff --git a/.claude/agents/refactorer.md b/.claude/agents/refactorer.md index 42052d8..001c15a 100644 --- a/.claude/agents/refactorer.md +++ b/.claude/agents/refactorer.md @@ -22,6 +22,8 @@ - Bash(python -m pytest *) - Bash(pytest *) - Bash(python -m unittest *) + - Bash(python scripts/verify/run_tests.py) + - Bash(python scripts/verify/run_tests.py *) - Bash(ls *) - Bash(cat *) --- diff --git a/.claude/agents/tester.md b/.claude/agents/tester.md index ac5dd3b..f6e76ea 100644 --- a/.claude/agents/tester.md +++ b/.claude/agents/tester.md @@ -22,6 +22,8 @@ - Bash(python -m pytest *) - Bash(pytest *) - Bash(python -m unittest *) + - Bash(python scripts/verify/run_tests.py) + - Bash(python scripts/verify/run_tests.py *) - Bash(ls *) - Bash(cat *) - Bash(mkdir *) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 4c87451..3356866 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -149,6 +149,8 @@ **具体的なコマンドは [ENV_04_開発コマンド](../../docs/02_ENV/ENV_04_開発コマンド.md) を参照する**(本 rule は方針,ENV_04 はコマンドという役割分担).MSBuild・`vstest.console.exe` の場所は VS のエディション(Community / Professional / Enterprise / Build Tools)によって変わるため,パスを直書きせず ENV_04 の `vswhere` による解決手順を使うこと. +**エージェント(tester・refactorer 等)は ENV_04 の「テストランナー」(`python scripts/verify/run_tests.py`)を使う**.ビルドからテスト実行・結果の集計までを 1 コマンドで行い,`--filter`(カテゴリ等の絞り込み)と `--repeat`(安定性確認の連続実行)に対応する.オーケストレーターにテストの実行を代行させない. + Visual Studio 上ではテストエクスプローラーから実行できる(プラットフォームを x64 にすること). ## モック方針 (Mocking) diff --git a/.claude/template-overrides.md b/.claude/template-overrides.md index ad8e738..c46c546 100644 --- a/.claude/template-overrides.md +++ b/.claude/template-overrides.md @@ -18,6 +18,8 @@ | ファイル | 方針 | 理由 | 記録日 | | --- | --- | --- | --- | +| `.claude/agents/tester.md` | merge | レガシー csproj のため `dotnet test` が使えず MSBuild/vstest を直接呼べないので,テスト実行用ランナー `python scripts/verify/run_tests.py` の実行権限を tools に追加した | 2026-10-06 | +| `.claude/agents/refactorer.md` | merge | tester と同じ理由で `python scripts/verify/run_tests.py` の実行権限を tools に追加した | 2026-10-06 | ## 記入例 (Example) diff --git "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" index 877a070..e277d85 100644 --- "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" +++ "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" @@ -84,6 +84,26 @@ - `packages` の復元にはネットワークと時間がかかるため,通常の開発サイクルでは `bin` / `obj` の削除までで運用し,完全クリーン検証は依存変更時とマージ前に行う. - 検証が終わったら退避した `packages.bak` は削除してよい(`nuget restore` で再取得できるため). +## テストランナー (Test Runner) + +上記「テスト」のビルド〜vstest 実行を 1 コマンドにまとめたランナー.**tester・refactorer エージェントはこれを使う**(エージェントの環境からは MSBuild・vstest を直接呼べないため.実行権限は `.claude/agents/` の tools で許可している).人間が使ってもよい. + +```bash +# 全件(Debug 出力を削除 → ビルド → テスト.判定にはこれを使う) +python scripts/verify/run_tests.py +# 絞り込み(vstest の /TestCaseFilter の式をそのまま渡す) +python scripts/verify/run_tests.py --filter "TestCategory=Integration" +# 安定性の確認(ビルドは 1 回,テストを 3 回続けて実行する) +python scripts/verify/run_tests.py --filter "TestCategory=Integration" --repeat 3 +# 開発中の素早い再確認(Debug 出力を消さない.判定には使わない) +python scripts/verify/run_tests.py --filter "FullyQualifiedName~ConfigTests" --no-clean +``` + +- 出力先: `.verify/tests/run-<日時>/`(`build.log`・`test-.log`・`result-.trx`・`summary.json`).画面には件数と,失敗したテストの名前・メッセージが出る +- 終了コード: `0`=全て合格,`1`=不合格あり(ビルド失敗・テスト失敗・絞り込みで該当 0 件),`2`=前提条件を満たさない(Visual Studio / Build Tools 未導入等) +- 既定で Debug 出力を削除してからビルドする.修正を一時的に戻して赤を確かめるときなどに,更新日時の古いファイルが再コンパイルされず古い結果を見てしまうのを防ぐため +- セルフテスト(`TIASshot.exe --selftest`)は実行しない.それは下記「セルフテスト」の `run_selftest.py` で行う + ## 実行 (Run) ビルド後,構成に対応した実行ファイルを起動する.実行時参照ファイル(`config.xml`・カメラ設定 XML・TCC の CSV)が同ディレクトリに配置済みであること([ENV_02_環境構築手順](ENV_02_環境構築手順.md)). diff --git a/scripts/verify/run_tests.py b/scripts/verify/run_tests.py new file mode 100644 index 0000000..6c07020 --- /dev/null +++ b/scripts/verify/run_tests.py @@ -0,0 +1,136 @@ +"""TIASshot の NUnit テストのランナー(tester / refactorer エージェント・開発中の確認用). + +MSBuild・vstest を直接呼べない環境(エージェントの Git Bash 等)からも,ENV_04 の「テスト」と同じ手順を +1 コマンドで実行できるようにする.セルフテスト(TIASshot.exe --selftest)は実行しない(それは run_selftest.py). + +手順(すべてローカルのみ): + 1. Debug|x64 の出力(bin/x64/Debug・obj/x64/Debug)を削除する(--no-clean で省略) + 汚れた bin による依存漏れの見逃しや,更新日時の古いファイルが再コンパイルされない取りこぼしを防ぐ + 2. Debug|x64 をビルドする + 3. vstest でテストを実行する(--filter でカテゴリ等を絞り込み,--repeat で同じテストを繰り返す) + 結果は TRX から集計し,失敗したテストの名前とメッセージを表示する + +出力: .verify/tests/run-<日時>/ に build.log・test-.log・result-.trx・summary.json +終了コード: 0=全て合格,1=不合格あり(ビルド失敗・テスト失敗・該当テスト 0 件),2=前提条件を満たさない + +使い方(リポジトリルートで実行): + python scripts/verify/run_tests.py # 全件(判定用) + python scripts/verify/run_tests.py --filter "TestCategory=Integration" # 層②だけ + python scripts/verify/run_tests.py --filter "TestCategory=Integration" --repeat 3 # 安定性確認(3 回連続) + python scripts/verify/run_tests.py --filter "FullyQualifiedName~ConfigTests" --no-clean # 開発中の素早い再確認 +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import json +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +from run_selftest import (BUILD_TIMEOUT_SEC, EXIT_FAIL, EXIT_OK, EXIT_PRECONDITION, REPO, SLN, TEST_DLL, + PreconditionError, clean_build_outputs, find_vs, run_logged) + +TRX_NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} +MAX_MESSAGE_LINES = 8 + + +def parse_trx(trx: Path) -> dict: + """TRX から件数と失敗したテスト(名前・メッセージ)を取り出す(vstest の表示言語に依存しない).""" + root = ET.parse(trx).getroot() + counters = root.find("t:ResultSummary/t:Counters", TRX_NS) + counts = {k: int(counters.get(k, "0")) for k in ("total", "executed", "passed", "failed", "notExecuted")} \ + if counters is not None else {} + failures = [] + for r in root.iterfind("t:Results/t:UnitTestResult", TRX_NS): + if r.get("outcome") != "Failed": + continue + msg = r.find("t:Output/t:ErrorInfo/t:Message", TRX_NS) + text = (msg.text or "").strip() if msg is not None else "" + failures.append({"name": r.get("testName"), "duration": r.get("duration"), + "message": "\n".join(text.splitlines()[:MAX_MESSAGE_LINES])}) + return {"counts": counts, "failures": failures} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--filter", help="vstest の /TestCaseFilter に渡す式(例: TestCategory=Integration)") + parser.add_argument("--repeat", type=int, default=1, help="テストを繰り返す回数(ビルドは 1 回.既定 1)") + parser.add_argument("--no-clean", action="store_true", help="Debug 出力を削除しない(開発時の短縮用.判定には使わない)") + args = parser.parse_args() + if args.repeat < 1: + parser.error("--repeat は 1 以上を指定してください") + # エージェント(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" / "tests" / f"run-{stamp}" + run_dir.mkdir(parents=True, exist_ok=True) + summary: dict = {"run_dir": str(run_dir), "filter": args.filter, "repeat": args.repeat, "runs": [], "result": None} + + exit_code = EXIT_OK + try: + vs = find_vs() + msbuild = vs / "MSBuild" / "Current" / "Bin" / "MSBuild.exe" + vstest = vs / "Common7" / "IDE" / "CommonExtensions" / "Microsoft" / "TestWindow" / "vstest.console.exe" + if not msbuild.exists(): + raise PreconditionError(f"MSBuild.exe が見つかりません: {msbuild}") + if not vstest.exists(): + raise PreconditionError(f"vstest.console.exe が見つかりません: {vstest}") + + if not args.no_clean: + clean_build_outputs() + print("[OK] clean: Debug 出力(bin/obj の x64/Debug)を削除", flush=True) + + 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) + print(f"[{'OK' if code == 0 else 'NG'}] build: Debug|x64 ビルド exit {code}({run_dir / 'build.log'})", flush=True) + summary["build_exit_code"] = code + if code != 0: + for ln in (run_dir / "build.log").read_text(encoding="utf-8", errors="replace").splitlines(): + if " error " in ln: + print(f" {ln.strip()}", flush=True) + raise RuntimeError("ビルド失敗") + + for n in range(1, args.repeat + 1): + trx_name = f"result-{n}.trx" + cmd = [str(vstest), str(TEST_DLL), "/Platform:x64", + f"/logger:trx;LogFileName={trx_name}", f"/ResultsDirectory:{run_dir}"] + if args.filter: + cmd.append(f"/TestCaseFilter:{args.filter}") + code = run_logged(cmd, run_dir / f"test-{n}.log", BUILD_TIMEOUT_SEC) + trx = run_dir / trx_name + parsed = parse_trx(trx) if trx.exists() else {"counts": {}, "failures": []} + counts = parsed["counts"] + total = counts.get("total", 0) + failed = counts.get("failed", 0) + # 絞り込み式の誤りで 0 件になったのを合格と取り違えない + ok = code == 0 and total > 0 and failed == 0 + summary["runs"].append({"run": n, "exit_code": code, **parsed}) + print(f"[{'OK' if ok else 'NG'}] tests ({n}/{args.repeat}): 合計 {total},成功 {counts.get('passed', 0)}," + f"失敗 {failed},未実行 {counts.get('notExecuted', 0)}(exit {code}.{run_dir / f'test-{n}.log'})", flush=True) + if total == 0: + print(" 該当するテストがありません(--filter の式を確認してください)", flush=True) + for f in parsed["failures"]: + print(f" FAILED {f['name']}", flush=True) + for ln in f["message"].splitlines(): + print(f" {ln}", flush=True) + if not ok: + exit_code = EXIT_FAIL + except PreconditionError as ex: + print(f"[NG] precondition: {ex}", flush=True) + exit_code = EXIT_PRECONDITION + except RuntimeError as ex: + print(f"[NG] abort: {ex}", flush=True) + 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())