#if SELFTEST
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using OpenCvSharp;
namespace TIASshot.Tests {
/// <summary>
/// アプリを閉じるとき(Form1_FormClosing → ShutdownOnClose)の結合ロジックテスト(層②).
///
/// 仕様: PLAN_05「束 B の方針」(アプリを閉じるとき)・testing.md「層②: CameraBase の切断・終了」
/// - 保存中なら閉じるのを取り消して「保存中」と知らせる(未保存の画像を失わない)
/// - 閉じ始めたらカメラのコールバックから UI への呼び出しを行わない(切断がコールバックの終了を待つ間のハング防止)
/// - 閉じるときは「閉じ始め → 再描画タイマー停止 → カメラ切断 → 光源を消してポートを閉じる」の順に後始末する
/// - 光源が開いていない(ヘッドレスでは実機が無い)場合も例外にならない
///
/// 観測は Form1.SelfTest.cs のアクセサ(SelfTestRequestClose・SelfTestIsClosing・SelfTestCloseSteps・
/// SelfTestRefreshTimerRunning・SelfTestMessage)と SelfTestCamera のアクセサで行う.
/// 失敗の注入: 保存スレッドを動作中のまま保持する派生偽カメラ(<see cref="HeldSaveCamera"/>).
/// 本クラスは SELFTEST 構成(Debug|x64)でのみコンパイルされる.
/// </summary>
[TestFixture]
public class Form1CloseIntegrationTests {
// 閉じるときの後始末の順序(PLAN_05「束 B の方針」・Form1.ShutdownOnClose)
private static readonly string[] CloseOrder = { "closing", "timer", "camera", "light" };
// 閉じる処理がカメラのコールバックを待ってハングしていないとみなす上限
private static readonly TimeSpan CloseLimit = TimeSpan.FromSeconds(1);
// ================================================================================
// 保存中(失敗注入: 保存スレッドが動作中)
// ================================================================================
/// <summary>
/// 保存中に閉じようとすると,閉じるのが取り消され,「保存中」の表示が出て,後始末(閉じ始め・タイマー停止・切断・光源)は行われない.
/// </summary>
[Test, Category("Integration")]
public void 閉じる_保存中_取り消されて保存中と表示され後始末は行わない() {
// Arrange / Act
var obs = IntegrationHarness.Run(h => {
var camera = h.Connect(h.Track(new HeldSaveCamera(h.Form, h.ChartFrameFactory())));
h.Form.SelfTestAttachCamera(camera);
using (var release = new ManualResetEventSlim(false)) {
try {
camera.HoldSaveThread(release);
var saving = camera.IsSaving;
var closed = h.Form.SelfTestRequestClose();
return new {
saving,
closed,
message = h.Form.SelfTestMessage,
closing = h.Form.SelfTestIsClosing,
steps = h.Form.SelfTestCloseSteps.ToList(),
timer = h.Form.SelfTestRefreshTimerRunning,
};
} finally {
release.Set();
h.PumpUntil(() => !camera.IsSaving, "保存スレッド(保持)の終了", TimeSpan.FromSeconds(3));
}
}
});
// Assert
Assert.That(obs.saving, Is.True, "前提: 保存スレッドの動作中に閉じようとしたこと");
using (Assert.EnterMultipleScope()) {
Assert.That(obs.closed, Is.False, "保存中は閉じるのが取り消されること");
Assert.That(obs.message, Is.EqualTo(Form1.SavingOnCloseMessage), "保存中である旨が表示されること");
Assert.That(obs.closing, Is.False, "取り消したときは閉じ始めの状態にならないこと(以降の通知が止まらない)");
Assert.That(obs.steps, Is.Empty, "取り消したときは後始末を行わないこと");
Assert.That(obs.timer, Is.True, "取り消したときは再描画タイマーが動いたままであること");
}
}
/// <summary>
/// 保存が終わった後に閉じようとすると閉じると決まり,後始末が「閉じ始め → タイマー停止 → カメラ切断 → 光源」の順に行われ,
/// 再描画タイマーが止まる.光源は開いていない(ヘッドレスでは実機が無い)が例外にならない.
/// </summary>
[Test, Category("Integration")]
public void 閉じる_保存の終了後_閉じると決まり後始末が決められた順に行われる() {
// Arrange / Act
var obs = IntegrationHarness.Run(h => {
var camera = h.Connect(h.Track(new HeldSaveCamera(h.Form, h.ChartFrameFactory())));
h.Form.SelfTestAttachCamera(camera);
bool cancelledWhileSaving;
using (var release = new ManualResetEventSlim(false)) {
camera.HoldSaveThread(release);
cancelledWhileSaving = !h.Form.SelfTestRequestClose();
release.Set();
h.PumpUntil(() => !camera.IsSaving, "保存スレッド(保持)の終了", TimeSpan.FromSeconds(3));
}
bool closed = false;
string error = null;
try {
closed = h.Form.SelfTestRequestClose();
} catch (Exception ex) {
error = ex.ToString();
}
return new {
cancelledWhileSaving,
closed,
error,
closing = h.Form.SelfTestIsClosing,
steps = h.Form.SelfTestCloseSteps.ToList(),
timer = h.Form.SelfTestRefreshTimerRunning,
};
});
// Assert
Assert.That(obs.cancelledWhileSaving, Is.True, "前提: 保存中は取り消されたこと");
using (Assert.EnterMultipleScope()) {
Assert.That(obs.error, Is.Null, "閉じる処理が例外にならないこと(光源が開いていない場合を含む)");
Assert.That(obs.closed, Is.True, "保存の終了後は閉じると決まること");
Assert.That(obs.closing, Is.True, "閉じ始めの状態になること");
Assert.That(obs.steps, Is.EqualTo(CloseOrder), "後始末が「閉じ始め → タイマー停止 → カメラ切断 → 光源」の順に行われること");
Assert.That(obs.timer, Is.False, "再描画タイマーが止まること");
}
}
// ================================================================================
// ハング防止(カメラのコールバックが UI へ通知し続けている間に閉じる)
// ================================================================================
/// <summary>
/// チャートのプレビューを流したまま(コールバックが毎フレーム UI へ表示を通知している間に)閉じても,
/// 閉じる処理はコールバックとの相互待機にならずすぐに戻り(1 秒未満),プレビューは止まり,コールバックで例外が出ない.
/// 切断(偽カメラはプレビュースレッドの終了を待つ.実カメラの SDK のストリーム停止と同じ)の間に,
/// コールバックが UI スレッドを待つ実装だと,切断の待機上限まで戻らない.
/// </summary>
[Test, Category("Integration")]
public void 閉じる_プレビュー中_すぐに戻りプレビューが止まり例外が出ない() {
// Arrange: 固定判定を成立させない設定にして,毎フレーム「舌診チャートの検出中」を通知し続けさせる
var options = new IntegrationOptions().WithConfig("Calib/ChartSetCount", "100000");
// Act
var obs = IntegrationHarness.Run(h => {
var camera = h.CreateChartCamera();
h.Form.SelfTestAttachCamera(camera);
camera.StartPreview(1000000, 1, false);
h.PumpUntil(() => camera.FramesDelivered >= 5 || !camera.IsPreviewRunning, "プレビューの開始", TimeSpan.FromSeconds(5));
var runningBeforeClose = camera.IsPreviewRunning;
var sw = Stopwatch.StartNew();
var closed = h.Form.SelfTestRequestClose();
var elapsed = sw.Elapsed;
h.PumpUntil(() => !camera.IsPreviewRunning, "プレビューの停止", TimeSpan.FromSeconds(5));
return new {
runningBeforeClose,
closed,
elapsedMs = elapsed.TotalMilliseconds,
previewRunning = camera.IsPreviewRunning,
PreviewError = camera.PreviewError?.ToString(),
};
}, options);
// Assert
Assert.That(obs.runningBeforeClose, Is.True, "前提: プレビューを流したまま閉じたこと");
using (Assert.EnterMultipleScope()) {
Assert.That(obs.closed, Is.True, "保存中でないので閉じると決まること");
Assert.That(obs.elapsedMs, Is.LessThan(CloseLimit.TotalMilliseconds), "閉じる処理がコールバックとの相互待機にならずすぐに戻ること");
Assert.That(obs.previewRunning, Is.False, "閉じた後はプレビューが止まっていること");
Assert.That(obs.PreviewError, Is.Null, "閉じる間にカメラのコールバックで例外が出ないこと");
}
}
// ================================================================================
// 失敗注入用の偽カメラ
// ================================================================================
/// <summary>
/// 保存スレッド(_saveThread)を,指定のイベントが立つまで動作中のまま保持する偽カメラ(保存中の状況の注入用).
/// 実際の保存は行わない(IsSaving=保存スレッドが生存中,の判定だけを使う).
/// </summary>
private sealed class HeldSaveCamera : SelfTestCamera {
public HeldSaveCamera(Form1 form, Func<int, Mat> frameFactory) : base(form, frameFactory) { }
/// <summary>release が立つまで(最大 10 秒)生存する保存スレッドを起動する</summary>
public void HoldSaveThread(ManualResetEventSlim release) {
var started = new ManualResetEventSlim(false);
var thread = new Thread(() => {
started.Set();
release.Wait(TimeSpan.FromSeconds(10));
}) {
IsBackground = true,
Name = "HeldSaveCamera.Save",
};
_saveThread = thread;
thread.Start();
started.Wait(TimeSpan.FromSeconds(3));
started.Dispose();
}
}
}
}
#endif