# テスト網羅性向上計画 (Test Coverage Improvement Plan)

## 目的 (Purpose)

人間による実機確認を「カメラ・光源 SDK の実呼び出し」と「実画像の見た目」だけに絞り，それ以外の回帰を自動テストで担保する．

方針の本体（常に従う規約）は [testing.md](../../.claude/rules/testing.md) に置き，本書はそこへ移行するための段取り（フェーズ・完了条件・リスク）を定める．

## 背景 (Background)

- 2026-10-06 の `/auto-audit` では，報告した指摘 26 件の多くが「`CameraBase`・`Form1`・カメラ層で再現テストを書けない」ことを理由に自動修正できなかった．中でも保存失敗が成功扱いになる件や終了時の画像取りこぼしは，データ欠損に直結する．
- 旧方針は「ハードウェア結合層は自動テスト対象外」としていた．しかし実際には，偽カメラ（`SelfTest/SelfTestCamera`）とヘッドレス `Form1`（`Form1.SelfTest.cs`）がすでにあり，セルフテストでは `CameraBase` の校正〜保存を通している．NUnit で細かく検証していないだけで，自動化できない範囲は思っていたより狭い．
- 異常系（保存失敗・終了時・設定ファイル破損）に問題が集中していた．正常系の目視確認だけでは見つからない種類の不具合である．

## 現状の手動確認の内訳 (Current Manual Checks)

| 確認内容 | 本当に実機が要るか | 移行先 |
| --- | --- | --- |
| カメラ接続・露光/ゲインの反映・フレーム取得 | 要る（SDK の実呼び出し） | 実機に残す（層④） |
| 光源の ON/OFF | 要る（シリアル通信） | 実機に残す（層④） |
| 校正の自動開始〜完了，撮影ボタンの有効化 | 判断ロジックは不要 | 層②（偽カメラ＋ヘッドレス Form1） |
| 単発・連続撮影でファイルが保存される | 不要 | 層②・セルフテスト |
| 保存失敗・ディスク満杯・設定破損時の挙動 | 不要（失敗を注入できる） | 層② |
| アプリを閉じてもエラーが出ない | 終了順序の判断は不要 | 層②（SDK の停止そのものは層④） |
| 校正結果・色の妥当性 | 合成チャートでは不十分 | 実機録画データによる回帰（層①の拡張） |

## 目標とする 4 層モデル (Target Four-Layer Model)

| 層 | 対象 | テスト手段 |
| --- | --- | --- |
| ① ピュアロジック | 色補正・チャート検出・CSV・設定・ゲイン計算・画像統計 | NUnit（実値で検証） |
| ② 結合ロジック | `CameraBase` の校正の状態遷移・撮影ループ・`SaveThread`・失敗通知・終了処理，`Form1` の UI 連携メソッド | NUnit＋偽カメラ＋ヘッドレス `Form1`（STA スレッドでメッセージを処理するハーネス） |
| ③ SDK アダプタ | `IScam`・`Lucam`・`LightSource` | 判断ロジックを純粋関数へ切り出して層①で検証する．SDK 呼び出しそのものは層④ |
| ④ 実機 | SDK の実呼び出し・実画像の見た目 | 短い手動チェックリスト（TEST_01） |

詳細な判断基準は [testing.md](../../.claude/rules/testing.md) を参照する．

## 決定事項 (Decisions)

2026-10-06 にユーザーと合意した．

| 項目 | 決定 |
| --- | --- |
| テストプロジェクトへの `System.Windows.Forms` 参照追加 | 許可する（.NET Framework 標準アセンブリで NuGet 依存は増えない） |
| 層②（`CameraBase` を NUnit で検証） | 行う |
| 層③（SDK アダプタからの判断ロジック切り出し） | 撮影・保存の信頼性修正（`/auto-audit` 報告の束 B）と同時に行う |
| 実機録画データ | 撮る．**舌診チャートと白板のみ**（舌の画像は個人情報のため扱わない） |
| カバレッジ | 数値目標は課さず，「一度も通っていない箇所」を見つける地図として使う |
| 実機録画データの置き場所 | **Git LFS** で管理する．CI が無くテストは開発マシンでしか回らないため，コードと同じ版のデータが clone で確実に手元に来る方式を選んだ．1 機種・1 場面あたり 3〜5 フレームに絞り，合計 100〜150MB 程度に抑える（GitHub の LFS 無料枠はストレージ 1GB・転送量 1GB/月） |
| 層②のテストの実行時間 | **層②全体で 30 秒を目安**とし，**1 件あたり 15 秒のタイムアウト**を付ける．超えそうなときはテスト用設定（校正フレーム数等）を見直して速くする．`/auto-audit` は修正 1 件ごとにテスト一式を何度も回すため，ここが延びると全作業が遅くなる |
| `/verify` に含めるか | **常に含める**（`run_selftest.py` は絞り込まずに全件実行するので追加設定は不要）．CI が無く，`/verify`・`/auto-audit` が実質唯一の自動チェックのため，カテゴリで分けて通常は回さない運用にはしない |
| メモリ量で判定するテスト | 2026-10-06 に追加したリーク再現テスト等は `[Category("Memory")]` に分け，環境による不安定さが出たら通常の実行から外せるようにする |

## フェーズ (Phases)

### フェーズ 0: 方針の文書化

- 本書を作成し，[testing.md](../../.claude/rules/testing.md) を 4 層モデルに改訂する
- 完了条件: ユーザーのレビューを経て main に取り込む

### フェーズ 1: 層②の基盤と試作

- テストプロジェクトに `System.Windows.Forms` の参照を追加する
- テスト用ハーネスを作る
  - STA スレッドを立て，ヘッドレス `Form1` を生成してハンドルを作る（`SelfTestStart`）
  - 条件が満たされるまでメッセージを処理する待機処理（タイムアウト必須）．`SelfTestRunner` の `Application.DoEvents` による待機と同じ考え方で，可能なら共通化する
  - 一時フォルダと一時 `config.xml` を用意する（`Config.Load(path)` を使う）
- テスト用の一時設定では校正フレーム数（`Calib/Frames`）等を小さくし，1 件あたり 1 秒前後で終わるようにする
- 既存のメモリ量判定テストに `[Category("Memory")]` を付ける
- 試作として「保存失敗が成功扱いになる」件（`/auto-audit` 報告台帳）の赤テストを書き，修正する
- 完了条件:
  - ハーネスを使うテストが 3 回連続で安定して緑になる
  - 層②の各テストに 15 秒のタイムアウトが付いている
  - Release ビルドに影響しない（`SELFTEST` は Debug 系のみ）
- **状態: 完了（2026-10-06）**
  - ハーネスは `TIASshot.Tests/IntegrationHarness.cs`．一時 config の作成と待機処理は `TIASshot/SelfTest/SelfTestConfig.cs`・`SelfTestPump.cs` に切り出し，セルフテストのランナーと共用する
  - 試作テスト `CameraBaseSaveIntegrationTests`（保存成功・単発の保存失敗・連続撮影の途中の保存失敗）は 3 回連続で緑．保存失敗の 2 件は修正前のコードで赤になることを確認した．層② 3 件の所要時間は計約 5 秒
  - 失敗の注入は，偽カメラの派生で保存先と同名のフォルダを作り `Cv2.ImWrite` に false を返させる方法をとった（例外ではなく false になることを単体テストで固定している）
  - 引き続き残る課題: `CalcTcc` の校正画像の保存失敗は「自動校正完了」と表示される（失敗時に校正済みとするかが仕様判断のため未修正）

### フェーズ 2: 層②の拡充

- 次の観点を網羅する．各観点に**失敗を注入するケースを 1 つ以上**含める
  - 校正の状態遷移（チャート検出 → 固定判定 → 変換行列算出 → 撮影ボタン有効化）
  - 撮影ループ（単発・連続・枚数 0・間隔 0）
  - `SaveThread`（保存失敗・例外時の後始末・残りフレームの破棄）
  - 切断・終了の順序（保存中の終了，校正中の終了）
  - `Form1` の UI 連携メソッド（UI スレッド外からの呼び出しを含む）
- `/auto-audit` 報告台帳のうち，層②で再現テストを書けるようになった項目をここで修正する
- 完了条件: 上記の各観点に正常系と失敗注入のテストがそろい，層②全体の実行時間が 30 秒の目安に収まる
- **状態: 完了（2026-10-06）**
  - 層②のテストは 3 ファイル 21 件を追加し計 24 件．3 回連続で緑，全体で約 19 秒
  - 報告台帳から 4 件を修正した: `Form1` の取得系メソッドが UI スレッド外で値を返さない，再検出時と切断時にチャートマスクを解放しない，`ResetShotQueue` が残った画像を解放しない，撮影失敗行の Note 欄のクォート漏れ．再現テスト 7 件は修正前のコードで赤になることを確認した
  - 撮影系のテストは変換次元を 1 つ（`Calib/ConversionChannels=4`）に絞って時間を抑えた．全次元での保存はフェーズ 1 の単発撮影テストで確認している
  - 方針未決のため観点から外したもの: `CalcTcc` の例外境界と校正画像の保存失敗，`Form1_FormClosing`，`SaveThread` の同期 Invoke，`DrainShots` の待ち時間，撮影枚数 0 の完了表示．束 B（フェーズ 3 と同時）で方針を決めてから扱う
  - 実機でしか確かめられない残り: 実カメラの `Disconnect` でストリーム停止の直後にマスクを解放してよいか（SDK がコールバックの終了を待つか）

### フェーズ 3: 層③の切り出し

- 撮影・保存の信頼性修正（束 B）と同時に，`IScam`・`Lucam`・`LightSource` から判断ロジックを純粋関数として切り出す
  - 例: ROI とフレームサイズの照合，ホワイトバランスのゲイン算出，フレームバッファのサイズ検査，光源ポートを開けなかったときの扱い，例外を通知に変える処理
- SDK を呼ぶ部分は「受け取って渡すだけ」の薄いアダプタに残す
- 完了条件: 切り出した関数に層①のテストがあり，切り出し直後に実機確認を 1 回行う
- 進め方: 量が多いため 2 回に分ける．**3a** で束 B の修正（下記の方針）を行い，修正で触る箇所の判断ロジックはその場で切り出す．**3b** で残りの切り出し（ROI の照合・フレームバッファのサイズ検査・光源ポートを開けなかったときの扱い等）を行う

#### 束 B の方針 (Decisions for Shot/Save Reliability)

2026-10-06 にユーザーと合意した．

| 論点 | 方針 |
| --- | --- |
| 校正中の例外（`CalcTcc`・`ProcessCalibrationFrame`） | カメラのコールバックへ例外を伝えない．捕捉してログを残し，「校正に失敗しました」と通知し，チャート検出からやり直せる状態（未校正・マスク解放）に戻す．通知がチャート検出の進捗表示ですぐ上書きされないよう，次に校正が完了するまで検出中の表示を「校正に失敗しました（理由）．チャートの検出からやり直しています」に差し替えて出し続ける（エラーダイアログはカメラのコールバックを止めるため使わない） |
| 校正画像（TccImage・TccRoisImage）の保存失敗 | 変換行列はメモリ上で有効なので校正済みのままにし，「自動校正完了（校正画像の保存に失敗）」のように警告を表示する |
| アプリを閉じるとき（`Form1_FormClosing`） | 保存中なら閉じるのを取り消して「保存中」と知らせる（閉じる理由は区別しない．Windows のシャットダウン時も取り消し，待つか強制終了するかはユーザーに選ばせる）．閉じ始めたらカメラのコールバックから UI への呼び出しを行わない（ハングの防止）．光源を消してポートを閉じる |
| 保存スレッドから UI への通知 | 進捗表示などは UI スレッドを待たない非同期の呼び出しにする（撮影中でも保存が進み，画像がキューに溜まり続けない） |
| 保存キューの待ち時間（`DrainShots`） | 固定 30 秒をやめ，「30 秒」と「撮影間隔の 2 倍」の大きい方にする |

- **3a の状態: 完了（2026-10-06）**
  - 上表の方針をすべて実装した．判断（閉じてよいか，待ち時間，通知文の組み立て）は `Form1.CanClose`・`CameraBase.GetDrainIdleTimeout`・`FormatCalibrationFailure`・`ShouldNotifyCalibrationFailure`・`FormatChartDetectionMessage`・`FormatCalibrationDone` として切り出し，層①でテストした
  - 層②の新規・書き換えテストは修正前のコードで 14 件が赤になることを確認した．層②は計 36 件で 1 回あたり約 21〜26 秒（目安 30 秒に近づいてきたので，次に層②を増やすときはテスト用設定の見直しを先に検討する）
  - 保存中の切断テストは，通知の非同期化で「保存スレッドが UI を待つ間は必ず生きている」前提が崩れたため，保存スレッドを意図的に止めておく形に書き直した
  - 実機でしか確かめられない残り: 保存中・プレビュー中・校正中に閉じたときの挙動（SDK のストリーム停止との待ち合い），光源の消灯とポートの解放，撮影と保存が並行するようになった後の連写間隔の精度
- **3b（未着手）**: ROI とフレームサイズの照合，フレームバッファのサイズ検査，光源ポートを開けなかったときの扱い（`/auto-audit` 報告台帳の該当項目）

### フェーズ 4: 実機録画データによる回帰

- 開始時に Git LFS を準備する（このマシンへの `git lfs` の導入確認，`.gitattributes` での対象パターン指定，GitHub 側の LFS の利用確認）
- 人間が実機（ImagingSource・Lumenera の両方）で舌診チャートと白板を撮影し，フレームを可逆形式（PNG）で保存する．1 機種・1 場面あたり 3〜5 フレームとする
- 録画フレームを繰り返し流して校正を通し，チャート検出のマスク位置，変換行列（基準 CSV との一致．許容誤差つき），変換後の色票の色差を自動で比較するテストを作る
- 完了条件: 両機種の録画データで回帰テストが緑になり，データが Git LFS で管理されている

### フェーズ 5: 手動チェックリストの縮小とカバレッジの確認

- `Microsoft.CodeCoverage`（テストプロジェクトに導入済み）で未通過の箇所を確認し，重要な経路の抜けを埋める
- [TEST_01](../06_TEST/TEST_01_テスト仕様.md) の手動テストを下記の目標形に改訂する
- 完了条件: TEST_01 の改訂が main に取り込まれる

## 手動確認の目標形 (Target Manual Checklist)

フェーズ 5 完了後に TEST_01 に残す項目の想定．

- 両機種（ImagingSource・Lumenera）でカメラが自動接続され，プレビューが出る
- 露光・ゲインの設定がプレビューに反映されている（目視）
- 実物の舌診チャートで自動校正が完了する
- 撮影した画像の色が目視で妥当である
- 光源が ON/OFF する
- 撮影中・保存中にアプリを閉じても固まらない（SDK の停止が絡むため最終確認として残す）

※ 保存ファイルの有無・内容，失敗時の通知，撮影ボタンの有効/無効といった項目は層②とセルフテストに移し，手動では確認しない．

## リスクと対策 (Risks)

| リスク | 対策 |
| --- | --- |
| WinForms のテストがハング・不安定になる（STA・メッセージ処理の誤り） | ハーネスを 1 か所に集約し，待機には必ずタイムアウトを付ける．新しいハーネス利用テストは 3 回連続実行で安定性を確認してから取り込む |
| テストの実行時間が延びて `/verify` や `/auto-audit` が遅くなる | 層②のテストに `[Category("Integration")]` を付けて所要時間を計測し，全体 30 秒の目安を超えそうならテスト用設定を見直して速くする（`/verify` から外すことはしない） |
| テストが `Form1` の内部構造に依存して壊れやすくなる | `Form1.SelfTest.cs` のアクセサ（`SelfTestMessage`・`SelfTestLastError`・`SelfTestShotsEnabled` 等）経由でのみ観測する．足りないアクセサはそこに追加する |
| メモリ量で判定するテストが環境によって不安定になる | 2026-10-06 に追加したリーク再現テスト（プライベートメモリ増分のしきい値判定，OpenCV 内部の参照カウント読み取り）を棚卸しし，必要なら `[Category("Memory")]` に分ける |
| 実機録画データが大きい／個人情報を含む | 舌診チャートと白板に限る．フレーム数を絞り Git LFS で管理する．LFS の転送量の無料枠（1GB/月）を超えそうなら，データの削減か有料枠を検討する |
