#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