using System;
using NUnit.Framework;
namespace TIASshot.Tests {
/// <summary>
/// 撮影・保存の信頼性(PLAN_05「束 B の方針」)で CameraBase から切り出した判断関数の単体テスト(層①).
///
/// 仕様: PLAN_05「束 B の方針」(2026-10-06 ユーザー合意)
/// - 校正中の例外は「校正に失敗しました」と(理由つきで)通知し,同じ失敗を繰り返し通知しない
/// - 次に校正が完了するまで,チャート検出の表示を「校正に失敗しました(理由).チャートの検出からやり直しています」に差し替える.
/// ただしチャートが上下逆の案内(設置し直しの案内)は差し替えない(ユーザー合意済み)
/// - 校正画像の保存に失敗しても校正済みとし,「自動校正完了(校正画像の保存に失敗)」のように警告する
/// - 保存キューの待ち時間は「30 秒」と「撮影間隔の 2 倍」の大きい方
/// </summary>
[TestFixture]
public class CameraBaseReliabilityDecisionTests {
// 校正失敗後に検出中の表示を差し替える文の末尾(PLAN_05「束 B の方針」)
private const string RetryingSuffix = ".チャートの検出からやり直しています";
// ================================================================================
// FormatCalibrationFailure(校正失敗の通知文)
// ================================================================================
/// <summary>校正失敗の通知は「校正に失敗しました」で始まり,理由(例外メッセージ)を含む</summary>
[Test]
public void FormatCalibrationFailure_例外_校正に失敗しましたで始まり理由を含む() {
var text = CameraBase.FormatCalibrationFailure(new InvalidOperationException("理由X"));
using (Assert.EnterMultipleScope()) {
Assert.That(text, Does.StartWith("校正に失敗しました"));
Assert.That(text, Does.Contain("理由X"));
}
}
/// <summary>
/// 失敗の通知文は,検出中の表示を差し替える文(…チャートの検出からやり直しています)とは区別できる
/// (通知は 1 回,差し替えは毎フレームのため,同じ文だと「通知を繰り返さない」の判定が崩れる).
/// </summary>
[Test]
public void FormatCalibrationFailure_例外_検出中の差し替え文とは異なる() {
var ex = new InvalidOperationException("理由X");
var notice = CameraBase.FormatCalibrationFailure(ex);
var retrying = CameraBase.FormatChartDetectionMessage(ex.Message, "舌診チャートの検出中");
Assert.That(notice, Is.Not.EqualTo(retrying));
}
// ================================================================================
// ShouldNotifyCalibrationFailure(同じ失敗を繰り返し通知しない)
// ================================================================================
/// <summary>未通知(直前の通知が null)なら通知する</summary>
[Test]
public void ShouldNotifyCalibrationFailure_未通知_trueを返す() {
Assert.That(CameraBase.ShouldNotifyCalibrationFailure(null, "校正に失敗しました(A)"), Is.True);
}
/// <summary>直前に通知した内容と同じなら通知しない(毎フレーム同じ例外が起きても通知が出続けない)</summary>
[Test]
public void ShouldNotifyCalibrationFailure_直前と同じ内容_falseを返す() {
Assert.That(CameraBase.ShouldNotifyCalibrationFailure("校正に失敗しました(A)", "校正に失敗しました(A)"), Is.False);
}
/// <summary>直前と異なる内容(別の理由の失敗)なら通知する</summary>
[Test]
public void ShouldNotifyCalibrationFailure_直前と異なる内容_trueを返す() {
Assert.That(CameraBase.ShouldNotifyCalibrationFailure("校正に失敗しました(A)", "校正に失敗しました(B)"), Is.True);
}
// ================================================================================
// FormatChartDetectionMessage(失敗表示の持続)
// ================================================================================
/// <summary>直前の校正失敗が無ければ,チャート検出の表示はそのまま出す</summary>
[Test]
public void FormatChartDetectionMessage_失敗なし_検出側の表示をそのまま返す() {
Assert.That(CameraBase.FormatChartDetectionMessage(null, "舌診チャートの検出中"), Is.EqualTo("舌診チャートの検出中"));
}
/// <summary>直前の校正失敗があれば,検出中の表示を「校正に失敗しました(理由).チャートの検出からやり直しています」に差し替える</summary>
[Test]
public void FormatChartDetectionMessage_失敗あり検出中_失敗の表示に差し替える() {
Assert.That(CameraBase.FormatChartDetectionMessage("理由X", "舌診チャートの検出中"),
Is.EqualTo("校正に失敗しました(理由X)" + RetryingSuffix));
}
/// <summary>直前の校正失敗があれば,チャート検出後の「校正中」の表示も差し替える(校正が完了するまで失敗を出し続ける)</summary>
[Test]
public void FormatChartDetectionMessage_失敗あり校正中_失敗の表示に差し替える() {
Assert.That(CameraBase.FormatChartDetectionMessage("理由X", "舌診チャート検出 校正中"),
Is.EqualTo("校正に失敗しました(理由X)" + RetryingSuffix));
}
/// <summary>チャートが上下逆の案内は,直前の校正失敗があっても差し替えない(設置し直しの案内のため.ユーザー合意済み)</summary>
[Test]
public void FormatChartDetectionMessage_失敗あり上下逆の案内_差し替えない() {
Assert.That(CameraBase.FormatChartDetectionMessage("理由X", ChartDetector.UpsideDownMessage), Is.EqualTo(ChartDetector.UpsideDownMessage));
}
/// <summary>上下逆の案内の文言は従来どおり(ChartDetector の表示の特性化)</summary>
[Test]
public void UpsideDownMessage_文言_従来の表示と同じ() {
Assert.That(ChartDetector.UpsideDownMessage, Is.EqualTo("舌診チャートが上下逆方向です"));
}
// ================================================================================
// FormatCalibrationDone(校正画像の保存失敗の警告)
// ================================================================================
/// <summary>校正画像を保存できたときは従来どおり「自動校正完了」</summary>
[Test]
public void FormatCalibrationDone_保存成功_自動校正完了() {
Assert.That(CameraBase.FormatCalibrationDone(true), Is.EqualTo("自動校正完了"));
}
/// <summary>校正画像の保存に失敗したときは「自動校正完了(校正画像の保存に失敗)」と警告を添える</summary>
[Test]
public void FormatCalibrationDone_保存失敗_保存失敗の警告を添える() {
Assert.That(CameraBase.FormatCalibrationDone(false), Is.EqualTo("自動校正完了(校正画像の保存に失敗)"));
}
// ================================================================================
// GetDrainIdleTimeout(保存キューの待ち時間: 30 秒と撮影間隔の 2 倍の大きい方)
// ================================================================================
/// <summary>単発撮影(間隔 0)は 30 秒</summary>
[Test]
public void GetDrainIdleTimeout_間隔0_30秒() {
Assert.That(CameraBase.GetDrainIdleTimeout(0), Is.EqualTo(TimeSpan.FromSeconds(30)));
}
/// <summary>負の間隔(不正値)は 30 秒(下限を割らない)</summary>
[Test]
public void GetDrainIdleTimeout_負の間隔_30秒() {
Assert.That(CameraBase.GetDrainIdleTimeout(-1000), Is.EqualTo(TimeSpan.FromSeconds(30)));
}
/// <summary>間隔 1000ms(2 倍が 30 秒未満)は 30 秒</summary>
[Test]
public void GetDrainIdleTimeout_間隔1000ms_30秒() {
Assert.That(CameraBase.GetDrainIdleTimeout(1000), Is.EqualTo(TimeSpan.FromSeconds(30)));
}
/// <summary>間隔 15000ms(2 倍がちょうど 30 秒)は 30 秒(境界)</summary>
[Test]
public void GetDrainIdleTimeout_間隔15000ms_30秒() {
Assert.That(CameraBase.GetDrainIdleTimeout(15000), Is.EqualTo(TimeSpan.FromSeconds(30)));
}
/// <summary>間隔 15001ms(2 倍が 30 秒をわずかに超える)は間隔の 2 倍(境界の直後)</summary>
[Test]
public void GetDrainIdleTimeout_間隔15001ms_間隔の2倍() {
Assert.That(CameraBase.GetDrainIdleTimeout(15001), Is.EqualTo(TimeSpan.FromMilliseconds(30002)));
}
/// <summary>間隔 20000ms は間隔の 2 倍(40 秒)</summary>
[Test]
public void GetDrainIdleTimeout_間隔20000ms_40秒() {
Assert.That(CameraBase.GetDrainIdleTimeout(20000), Is.EqualTo(TimeSpan.FromSeconds(40)));
}
/// <summary>間隔 int.MaxValue でも溢れず(負や 30 秒に戻らず),間隔の 2 倍になる</summary>
[Test]
public void GetDrainIdleTimeout_間隔intMaxValue_溢れずに間隔の2倍() {
var expected = TimeSpan.FromTicks(2L * int.MaxValue * TimeSpan.TicksPerMillisecond);
TimeSpan actual = TimeSpan.Zero;
Assert.DoesNotThrow((Action)(() => actual = CameraBase.GetDrainIdleTimeout(int.MaxValue)));
Assert.That(actual, Is.EqualTo(expected));
}
}
}