Newer
Older
TIASShot / TIASshot.Tests / Form1UiIntegrationTests.cs
#if SELFTEST
using System;
using System.Threading.Tasks;
using NUnit.Framework;

namespace TIASshot.Tests {

    /// <summary>
    /// Form1 の UI 連携メソッドを UI スレッド外(ワーカースレッド)から呼んだときの結合ロジックテスト(層②).
    ///
    /// 仕様: testing.md「層②: UI/Form1 の UI 連携メソッド」(UI スレッド外から呼んだときのマーシャリング)・
    /// SPEC_01「撮影」「データ保存」(データ名は保存フォルダ名 `[時刻]-[データ名]` の一部,連続撮影の枚数・間隔は UI で変更可能).
    ///   - 取得系(GetDataName・GetNumMultiShots・GetMultiShotsInterval)は,どのスレッドから呼んでも UI の入力値を返す
    ///   - 通知系(ShowMessage・EnableShots)は,どのスレッドから呼んでも UI に反映される
    ///
    /// ワーカースレッドの呼び出しは Task.Run で行い,UI スレッド(ハーネスの STA スレッド)はメッセージを処理しながら
    /// 完了を待つ(Control.Invoke は UI スレッドがメッセージを処理しないと返らないため).
    /// 観測値は STA スレッド上で集めてテストスレッドへ返し,アサーションはテストスレッドで行う.
    /// 本クラスは SELFTEST 構成(Debug|x64)でのみコンパイルされる.
    /// </summary>
    [TestFixture]
    public class Form1UiIntegrationTests {

        // ワーカースレッドの呼び出し 1 回に許す時間(Invoke が UI スレッドで処理されるまで)
        private static readonly TimeSpan WorkerTimeout = TimeSpan.FromSeconds(5);

        // ================================================================================
        // 取得系(正常系)
        // ================================================================================

        /// <summary>
        /// ワーカースレッドから GetDataName を呼んだとき,UI に入力されたデータ名が返る(null にならない).
        /// 撮影(ShotOne / ShotMulti)は保存フォルダ名にデータ名を使うため,null だと保存先が誤る.
        /// </summary>
        [Test, Category("Integration")]
        public void GetDataName_ワーカースレッドから呼ぶ_UIに入力したデータ名を返す() {
            // Arrange / Act
            var result = IntegrationHarness.Run(h => {
                h.Form.SelfTestSetDataName("abc");
                return CallFromWorker(h, () => h.Form.GetDataName());
            });

            // Assert
            Assert.That(result.Error, Is.Null, $"ワーカースレッドからの呼び出しが例外にならないこと: {result.Error}");
            Assert.That(result.Value, Is.EqualTo("abc"));
        }

        /// <summary>ワーカースレッドから GetNumMultiShots を呼んだとき,UI に入力した枚数が返る</summary>
        [Test, Category("Integration")]
        public void GetNumMultiShots_ワーカースレッドから呼ぶ_UIに入力した枚数を返す() {
            // Arrange / Act
            var result = IntegrationHarness.Run(h => {
                h.Form.SelfTestSetMultiShots("7", "250");
                return CallFromWorker(h, () => h.Form.GetNumMultiShots());
            });

            // Assert
            Assert.That(result.Error, Is.Null, $"ワーカースレッドからの呼び出しが例外にならないこと: {result.Error}");
            Assert.That(result.Value, Is.EqualTo(7));
        }

        /// <summary>ワーカースレッドから GetMultiShotsInterval を呼んだとき,UI に入力した間隔が返る</summary>
        [Test, Category("Integration")]
        public void GetMultiShotsInterval_ワーカースレッドから呼ぶ_UIに入力した間隔を返す() {
            // Arrange / Act
            var result = IntegrationHarness.Run(h => {
                h.Form.SelfTestSetMultiShots("7", "250");
                return CallFromWorker(h, () => h.Form.GetMultiShotsInterval());
            });

            // Assert
            Assert.That(result.Error, Is.Null, $"ワーカースレッドからの呼び出しが例外にならないこと: {result.Error}");
            Assert.That(result.Value, Is.EqualTo(250));
        }

        // ================================================================================
        // 取得系(失敗注入: 数値として解釈できない入力)
        // ================================================================================

        /// <summary>
        /// 枚数・間隔の入力欄に数値でない文字列があるとき,ワーカースレッドから呼んでも例外にならず,
        /// UI スレッドから呼んだ場合と同じ値を返す(既定値そのものは仕様に定めがないためアサートしない).
        /// </summary>
        [Test, Category("Integration")]
        public void GetNumMultiShotsとGetMultiShotsInterval_数値でない入力をワーカースレッドから読む_UIスレッドと同じ値を返す() {
            // Arrange / Act
            var obs = IntegrationHarness.Run(h => {
                h.Form.SelfTestSetMultiShots("abc", "");
                return new {
                    UiNum = h.Form.GetNumMultiShots(),
                    UiInterval = h.Form.GetMultiShotsInterval(),
                    WorkerNum = CallFromWorker(h, () => h.Form.GetNumMultiShots()),
                    WorkerInterval = CallFromWorker(h, () => h.Form.GetMultiShotsInterval()),
                };
            });

            // Assert
            using (Assert.EnterMultipleScope()) {
                Assert.That(obs.WorkerNum.Error, Is.Null, $"枚数: ワーカースレッドからの呼び出しが例外にならないこと: {obs.WorkerNum.Error}");
                Assert.That(obs.WorkerInterval.Error, Is.Null, $"間隔: ワーカースレッドからの呼び出しが例外にならないこと: {obs.WorkerInterval.Error}");
                Assert.That(obs.WorkerNum.Value, Is.EqualTo(obs.UiNum), "枚数: UI スレッドから読んだ値と同じであること");
                Assert.That(obs.WorkerInterval.Value, Is.EqualTo(obs.UiInterval), "間隔: UI スレッドから読んだ値と同じであること");
            }
        }

        // ================================================================================
        // 通知系
        // ================================================================================

        /// <summary>ワーカースレッドから ShowMessage を呼んだとき,ステータス表示に反映される</summary>
        [Test, Category("Integration")]
        public void ShowMessage_ワーカースレッドから呼ぶ_ステータス表示に反映される() {
            // Arrange / Act
            var obs = IntegrationHarness.Run(h => {
                var call = CallFromWorker(h, () => { h.Form.ShowMessage("worker-message"); return 0; });
                return new { call.Error, Message = h.Form.SelfTestMessage };
            });

            // Assert
            Assert.That(obs.Error, Is.Null, $"ワーカースレッドからの呼び出しが例外にならないこと: {obs.Error}");
            Assert.That(obs.Message, Is.EqualTo("worker-message"));
        }

        /// <summary>
        /// ワーカースレッドから EnableShots(true) / EnableShots(false) を呼んだとき,撮影ボタンの有効・無効に反映される
        /// (SaveThread が保存完了後に撮影ボタンを再有効化する経路).
        /// </summary>
        [Test, Category("Integration")]
        public void EnableShots_ワーカースレッドから呼ぶ_撮影ボタンの有効無効に反映される() {
            // Arrange / Act
            var obs = IntegrationHarness.Run(h => {
                var before = h.Form.SelfTestShotsEnabled;
                var enable = CallFromWorker(h, () => { h.Form.EnableShots(true); return 0; });
                var afterEnable = h.Form.SelfTestShotsEnabled;
                var disable = CallFromWorker(h, () => { h.Form.EnableShots(false); return 0; });
                var afterDisable = h.Form.SelfTestShotsEnabled;
                return new { before, enable, afterEnable, disable, afterDisable };
            });

            // Assert
            Assert.That(obs.before, Is.False, "前提: 開始時(校正前)は撮影ボタンが無効であること");
            using (Assert.EnterMultipleScope()) {
                Assert.That(obs.enable.Error, Is.Null, $"有効化の呼び出しが例外にならないこと: {obs.enable.Error}");
                Assert.That(obs.disable.Error, Is.Null, $"無効化の呼び出しが例外にならないこと: {obs.disable.Error}");
                Assert.That(obs.afterEnable, Is.True, "EnableShots(true) で撮影ボタンが有効になること");
                Assert.That(obs.afterDisable, Is.False, "EnableShots(false) で撮影ボタンが無効になること");
            }
        }

        // ================================================================================
        // 共通処理
        // ================================================================================

        /// <summary>
        /// 関数をワーカースレッド(Task.Run)で実行し,UI スレッドのメッセージを処理しながら完了を待つ.
        /// 例外は戻り値の Error に入れて返す(テストスレッドでアサートするため).
        /// </summary>
        private static WorkerResult<T> CallFromWorker<T>(IntegrationHarness h, Func<T> func) {
            var task = Task.Run(func);
            h.PumpUntil(() => task.IsCompleted, "ワーカースレッドからの呼び出しの完了", WorkerTimeout);
            if (task.IsFaulted) {
                var ex = task.Exception?.GetBaseException();
                return new WorkerResult<T> { Error = $"{ex?.GetType().Name}: {ex?.Message}" };
            }
            return new WorkerResult<T> { Value = task.Result };
        }

        private sealed class WorkerResult<T> {
            public T Value;
            public string Error;
        }
    }
}
#endif