#if SELFTEST
using System;
using System.Diagnostics;
using System.Threading;
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 に反映される
/// - PLAN_05「束 B の方針」: UI スレッド外からの通知系は 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) で撮影ボタンが無効になること");
}
}
// ================================================================================
// 通知系(UI スレッドを待たない.PLAN_05「束 B の方針」: 保存スレッドから UI への通知)
// ================================================================================
/// <summary>
/// ワーカースレッドから ShowMessage を呼んだとき,UI スレッドがメッセージを処理していなくても待たずに戻り,
/// その後 UI スレッドが投稿を処理するとステータス表示に反映される.
/// </summary>
[Test, Category("Integration")]
public void ShowMessage_ワーカースレッドから呼ぶ_UIスレッドの処理を待たずに戻り後で反映される() {
// Arrange / Act
var obs = IntegrationHarness.Run(h => {
h.Form.ShowMessage("before");
var task = Task.Run(() => h.Form.ShowMessage("async-message"));
var returned = WaitWithoutPumping(() => task.IsCompleted, NonBlockingLimit);
// 待っていた場合(不具合時)も後始末できるよう,メッセージを処理して完了させる
h.PumpUntil(() => task.IsCompleted, "ワーカースレッドからの呼び出しの完了", WorkerTimeout);
h.FlushPostedUiCalls();
return new { returned, Error = task.Exception?.GetBaseException()?.Message, Message = h.Form.SelfTestMessage };
});
// Assert
using (Assert.EnterMultipleScope()) {
Assert.That(obs.returned, Is.True, "UI スレッドがメッセージを処理していなくても ShowMessage が待たずに戻ること");
Assert.That(obs.Error, Is.Null, "ワーカースレッドからの呼び出しが例外にならないこと");
Assert.That(obs.Message, Is.EqualTo("async-message"), "UI スレッドが投稿を処理した後はステータス表示に反映されること");
}
}
/// <summary>
/// ワーカースレッドから EnableShots を呼んだとき,UI スレッドがメッセージを処理していなくても待たずに戻り,
/// その後 UI スレッドが投稿を処理すると撮影ボタンに反映される.
/// </summary>
[Test, Category("Integration")]
public void EnableShots_ワーカースレッドから呼ぶ_UIスレッドの処理を待たずに戻り後で反映される() {
// Arrange / Act
var obs = IntegrationHarness.Run(h => {
var before = h.Form.SelfTestShotsEnabled;
var task = Task.Run(() => h.Form.EnableShots(true));
var returned = WaitWithoutPumping(() => task.IsCompleted, NonBlockingLimit);
h.PumpUntil(() => task.IsCompleted, "ワーカースレッドからの呼び出しの完了", WorkerTimeout);
h.FlushPostedUiCalls();
return new { before, returned, Error = task.Exception?.GetBaseException()?.Message, enabled = h.Form.SelfTestShotsEnabled };
});
// Assert
Assert.That(obs.before, Is.False, "前提: 開始時(校正前)は撮影ボタンが無効であること");
using (Assert.EnterMultipleScope()) {
Assert.That(obs.returned, Is.True, "UI スレッドがメッセージを処理していなくても EnableShots が待たずに戻ること");
Assert.That(obs.Error, Is.Null, "ワーカースレッドからの呼び出しが例外にならないこと");
Assert.That(obs.enabled, Is.True, "UI スレッドが投稿を処理した後は撮影ボタンに反映されること");
}
}
// ================================================================================
// 通知系(失敗注入: 閉じ始めた後・ハンドル未生成)
// ================================================================================
/// <summary>
/// 閉じ始めた後(Form1_FormClosing で閉じると決めた後)は,UI スレッド・ワーカースレッドのどちらから
/// ShowMessage / EnableShots を呼んでも例外にならず,表示も撮影ボタンも変わらない(閉じるときのハング防止).
/// </summary>
[Test, Category("Integration")]
public void ShowMessageとEnableShots_閉じ始めた後_例外にならず表示もボタンも変わらない() {
// Arrange / Act
var obs = IntegrationHarness.Run(h => {
h.Form.ShowMessage("before-close");
var closed = h.Form.SelfTestRequestClose();
var messageBefore = h.Form.SelfTestMessage;
var enabledBefore = h.Form.SelfTestShotsEnabled;
string uiError = null;
try {
h.Form.ShowMessage("after-close-ui");
h.Form.EnableShots(!enabledBefore);
} catch (Exception ex) {
uiError = ex.ToString();
}
var worker = CallFromWorker(h, () => {
h.Form.ShowMessage("after-close-worker");
h.Form.EnableShots(!enabledBefore);
return 0;
});
h.FlushPostedUiCalls();
return new {
closed,
closing = h.Form.SelfTestIsClosing,
messageBefore,
enabledBefore,
uiError,
workerError = worker.Error,
message = h.Form.SelfTestMessage,
enabled = h.Form.SelfTestShotsEnabled,
};
});
// Assert
Assert.That(obs.closed, Is.True, "前提: 保存中でないので閉じると決まったこと");
Assert.That(obs.closing, Is.True, "前提: 閉じ始めの状態であること");
using (Assert.EnterMultipleScope()) {
Assert.That(obs.uiError, Is.Null, "閉じ始めた後の UI スレッドからの呼び出しが例外にならないこと");
Assert.That(obs.workerError, Is.Null, "閉じ始めた後のワーカースレッドからの呼び出しが例外にならないこと");
Assert.That(obs.message, Is.EqualTo(obs.messageBefore), "閉じ始めた後は表示が変わらないこと");
Assert.That(obs.enabled, Is.EqualTo(obs.enabledBefore), "閉じ始めた後は撮影ボタンが変わらないこと");
}
}
/// <summary>
/// ウィンドウハンドルが未生成のフォームでは,UI スレッド・ワーカースレッドのどちらから ShowMessage / EnableShots を
/// 呼んでも例外にならず,表示も撮影ボタンも変わらない.
/// </summary>
[Test, Category("Integration")]
public void ShowMessageとEnableShots_ハンドル未生成_例外にならず表示もボタンも変わらない() {
// Arrange / Act
var obs = IntegrationHarness.Run(h => {
var form = h.AddDisposable(new Form1(Form1.SelfTestHeadless.Instance));
var handleBefore = form.IsHandleCreated;
var messageBefore = form.SelfTestMessage;
var enabledBefore = form.SelfTestShotsEnabled;
string uiError = null;
try {
form.ShowMessage("no-handle-ui");
form.EnableShots(!enabledBefore);
} catch (Exception ex) {
uiError = ex.ToString();
}
var worker = CallFromWorker(h, () => {
form.ShowMessage("no-handle-worker");
form.EnableShots(!enabledBefore);
return 0;
});
h.PumpFor(TimeSpan.FromMilliseconds(50));
return new {
handleBefore,
uiError,
workerError = worker.Error,
messageBefore,
message = form.SelfTestMessage,
enabledBefore,
enabled = form.SelfTestShotsEnabled,
};
});
// Assert
Assert.That(obs.handleBefore, Is.False, "前提: ハンドル未生成のフォームであること");
using (Assert.EnterMultipleScope()) {
Assert.That(obs.uiError, Is.Null, "ハンドル未生成での UI スレッドからの呼び出しが例外にならないこと");
Assert.That(obs.workerError, Is.Null, "ハンドル未生成でのワーカースレッドからの呼び出しが例外にならないこと");
Assert.That(obs.message, Is.EqualTo(obs.messageBefore), "ハンドル未生成では表示が変わらないこと");
Assert.That(obs.enabled, Is.EqualTo(obs.enabledBefore), "ハンドル未生成では撮影ボタンが変わらないこと");
}
}
// ================================================================================
// 共通処理
// ================================================================================
/// <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 };
}
// 「待たずに戻る」とみなす上限(UI スレッドの処理を待つ実装なら,メッセージを処理しない限り戻らない)
private static readonly TimeSpan NonBlockingLimit = TimeSpan.FromSeconds(1);
/// <summary>
/// UI スレッドのメッセージを処理せずに,条件の成立を上限つきで待つ(成立したら true).
/// WaitHandle・Join は STA スレッドで一部のメッセージを処理しうるため使わず,Yield とスピンで待つ.
/// </summary>
private static bool WaitWithoutPumping(Func<bool> condition, TimeSpan limit) {
var sw = Stopwatch.StartNew();
while (!condition()) {
if (sw.Elapsed > limit) return false;
Thread.Yield();
Thread.SpinWait(1000);
}
return true;
}
private sealed class WorkerResult<T> {
public T Value;
public string Error;
}
}
}
#endif