Newer
Older
TIASShot / TIASshot.Tests / Form1CloseIntegrationTests.cs
#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