diff --git a/TIASshot.Tests/Form1CloseIntegrationTests.cs b/TIASshot.Tests/Form1CloseIntegrationTests.cs index 35603f0..e70aab6 100644 --- a/TIASshot.Tests/Form1CloseIntegrationTests.cs +++ b/TIASshot.Tests/Form1CloseIntegrationTests.cs @@ -15,7 +15,7 @@ /// 仕様: PLAN_05「束 B の方針」(アプリを閉じるとき)・testing.md「層②: CameraBase の切断・終了」 /// - 保存中なら閉じるのを取り消して「保存中」と知らせる(未保存の画像を失わない) /// - 閉じ始めたらカメラのコールバックから UI への呼び出しを行わない(切断がコールバックの終了を待つ間のハング防止) - /// - 閉じるときは「閉じ始め → 再描画タイマー停止 → カメラ切断 → 光源を消してポートを閉じる」の順に後始末する + /// - 閉じるときは「閉じ始め → 再描画タイマー停止 → カメラ切断 → 光源のポートを閉じる(消灯はしない)」の順に後始末する /// - 光源が開いていない(ヘッドレスでは実機が無い)場合も例外にならない /// /// 観測は Form1.SelfTest.cs のアクセサ(SelfTestRequestClose・SelfTestIsClosing・SelfTestCloseSteps・ diff --git a/TIASshot.Tests/HwTestJudgeTests.cs b/TIASshot.Tests/HwTestJudgeTests.cs new file mode 100644 index 0000000..2afcf15 --- /dev/null +++ b/TIASshot.Tests/HwTestJudgeTests.cs @@ -0,0 +1,1067 @@ +#if HWTEST +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using NUnit.Framework; + +namespace TIASshot.Tests { + /// + /// 実機テストの判断ロジック(HwTestJudge)のテスト(層①.SDK・UI に依存しない純粋関数). + /// + /// 仕様: PLAN_06「検査項目」「守ること」・.claude/verify-profile.md「実機テスト」・HwTestJudge の各関数の契約 + /// - preview: フレームレートの下限.close-preview: 所要時間の上限.ウォッチドッグは全体の上限を優先 + /// - exposure-gain: 露光は Linear なら ×0.5/×2,Log2Step なら最も近い 2^n から ±1 段(範囲内に収める). + /// ゲインは範囲の幅の ±10%.露光は比(1 段下げ ≦ 0.8・上げ ≧ 1.25,確認した向きはすべて期待どおり), + /// ゲインは差(≧ 3,片側だけでも合格).動かした側が飽和(飽和画素の割合 > 1%)していれば,その向きは確認しない. + /// 両方とも確認できなければ FAIL + /// - 基準探しの判断の順番: 範囲の端 → 測れない → 中間に入った → 飛び越えた → 段数の上限 → 続ける + /// - light: 点灯と消灯の差 ≧ 100.復元: 読み戻しの相対誤差 1%(ゲインは範囲の幅 × 0.1% との大きい方) + /// - チャート不要の検査では校正を止める.終了コード 2/1/0,作業フォルダは keep 指定または 1・3 のとき残す + /// 実測の例は .verify/hwtest/run-20261008-133812/hwtest-result.txt(DFK 33UX178)から採った. + /// 本クラスは HWTEST 構成(Debug|x64)でのみコンパイルされる. + /// + [TestFixture] + public class HwTestJudgeTests { + + private const double Eps = 1e-9; + + private static HwTestMeasurement M(double mean, double saturatedFraction) { + return new HwTestMeasurement { Mean = mean, SaturatedFraction = saturatedFraction, MeanSaturatedFraction = saturatedFraction, Count = 8 }; + } + + private static double NextExp(double current, double min, double max, HwTestValueScale scale, int direction) { + return HwTestJudge.NextExposure(current, min, max, scale, direction, + HwTestThresholds.ExposureLogStep, HwTestThresholds.ExposureDownFactor, HwTestThresholds.ExposureUpFactor); + } + + private static HwTestBaselineSearchDecision Decide(double mean, double sat, int direction, bool changed, int stepsDone) { + return HwTestJudge.DecideBaselineSearch(mean, sat, direction, changed, stepsDone, 12, 30, 220, 0.01); + } + + // ================================================================ + // フレームレート・しきい値 + // ================================================================ + + [Test] + public void FrameRate_フレーム数を秒で割る() { + Assert.That(HwTestJudge.FrameRate(42, TimeSpan.FromSeconds(3)), Is.EqualTo(14.0).Within(Eps)); + } + + [Test] + public void FrameRate_時間が0_0() { + Assert.That(HwTestJudge.FrameRate(42, TimeSpan.Zero), Is.EqualTo(0.0)); + } + + [Test] + public void FrameRate_時間が負_0() { + Assert.That(HwTestJudge.FrameRate(42, TimeSpan.FromSeconds(-1)), Is.EqualTo(0.0)); + } + + [Test] + public void FrameRate_フレーム0_0() { + Assert.That(HwTestJudge.FrameRate(0, TimeSpan.FromSeconds(3)), Is.EqualTo(0.0)); + } + + [Test] + public void MeetsMinFrameRate_下限ちょうど_合格() { + Assert.That(HwTestJudge.MeetsMinFrameRate(10.0, 10.0), Is.True); + } + + [Test] + public void MeetsMinFrameRate_下限未満_不合格() { + Assert.That(HwTestJudge.MeetsMinFrameRate(9.99, 10.0), Is.False); + } + + [Test] + public void MeetsMinFrameRate_NaN_不合格() { + Assert.That(HwTestJudge.MeetsMinFrameRate(double.NaN, 10.0), Is.False); + } + + [Test] + public void MeetsMinFrameRate_実測13_28fps_下限10で合格() { + // 実測: 13.28 fps(42 フレーム / 3.16 秒) + Assert.That(HwTestJudge.MeetsMinFrameRate(HwTestJudge.FrameRate(42, TimeSpan.FromSeconds(3.16)), HwTestThresholds.MinPreviewFps), Is.True); + } + + [Test] + public void WithinLimit_上限ちょうど_以内() { + Assert.That(HwTestJudge.WithinLimit(TimeSpan.FromMilliseconds(1000), TimeSpan.FromMilliseconds(1000)), Is.True); + } + + [Test] + public void WithinLimit_上限超え_超過() { + Assert.That(HwTestJudge.WithinLimit(TimeSpan.FromMilliseconds(1001), TimeSpan.FromMilliseconds(1000)), Is.False); + } + + [Test] + public void FindOverdue_上限内_None() { + Assert.That(HwTestJudge.FindOverdue(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(10), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(60)), + Is.EqualTo(HwTestOverdue.None)); + } + + [Test] + public void FindOverdue_上限ちょうど_None() { + Assert.That(HwTestJudge.FindOverdue(TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10), TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)), + Is.EqualTo(HwTestOverdue.None)); + } + + [Test] + public void FindOverdue_項目の上限超え_Item() { + Assert.That(HwTestJudge.FindOverdue(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(10), TimeSpan.FromSeconds(61), TimeSpan.FromSeconds(60)), + Is.EqualTo(HwTestOverdue.Item)); + } + + [Test] + public void FindOverdue_全体と項目の両方超え_全体を優先() { + Assert.That(HwTestJudge.FindOverdue(TimeSpan.FromMinutes(11), TimeSpan.FromMinutes(10), TimeSpan.FromSeconds(61), TimeSpan.FromSeconds(60)), + Is.EqualTo(HwTestOverdue.Overall)); + } + + [Test] + public void FindOverdue_実行中の項目なし_項目の上限を見ない() { + Assert.That(HwTestJudge.FindOverdue(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(10), TimeSpan.FromHours(1), null), + Is.EqualTo(HwTestOverdue.None)); + } + + // ================================================================ + // 露光・ゲインの段の計算 + // ================================================================ + + [Test] + public void NextExposure_Linear_下げる_半分() { + Assert.That(NextExp(0.01, 0.0001, 1, HwTestValueScale.Linear, -1), Is.EqualTo(0.005).Within(Eps)); + } + + [Test] + public void NextExposure_Linear_上げる_2倍() { + Assert.That(NextExp(0.01, 0.0001, 1, HwTestValueScale.Linear, +1), Is.EqualTo(0.02).Within(Eps)); + } + + [Test] + public void NextExposure_Linear_上げて最大を超える_最大に収める() { + Assert.That(NextExp(0.8, 0.0001, 1, HwTestValueScale.Linear, +1), Is.EqualTo(1.0)); + } + + [Test] + public void NextExposure_Linear_下げて最小を下回る_最小に収める() { + Assert.That(NextExp(0.0003, 0.0002, 1, HwTestValueScale.Linear, -1), Is.EqualTo(0.0002)); + } + + [Test] + public void NextExposure_Log2Step_下げる_最も近い2のn乗から1段下() { + // 実測: 露光 0.016667(最も近い 2^n は 2^-6 = 0.015625)→ 下げた 0.007813(2^-7) + Assert.That(NextExp(0.016667, 2e-5, 60, HwTestValueScale.Log2Step, -1), Is.EqualTo(Math.Pow(2, -7)).Within(1e-12)); + } + + [Test] + public void NextExposure_Log2Step_上げる_最も近い2のn乗から1段上() { + // 実測: 露光 0.016667 → 上げた 0.03125(2^-5) + Assert.That(NextExp(0.016667, 2e-5, 60, HwTestValueScale.Log2Step, +1), Is.EqualTo(Math.Pow(2, -5)).Within(1e-12)); + } + + [Test] + public void NextExposure_Log2Step_ちょうど2のn乗_そこから1段() { + Assert.That(NextExp(0.015625, 2e-5, 60, HwTestValueScale.Log2Step, +1), Is.EqualTo(0.03125).Within(1e-12)); + } + + [Test] + public void NextExposure_Log2Step_最大を超える_最大に収める() { + Assert.That(NextExp(0.5, 2e-5, 0.6, HwTestValueScale.Log2Step, +1), Is.EqualTo(0.6)); + } + + [Test] + public void NextExposure_Log2Step_最小を下回る_最小に収める() { + Assert.That(NextExp(3e-5, 2e-5, 60, HwTestValueScale.Log2Step, -1), Is.EqualTo(2e-5)); + } + + [Test] + public void NextExposure_向きが0_ArgumentException() { + Assert.Throws((Action)(() => NextExp(0.01, 0.0001, 1, HwTestValueScale.Linear, 0))); + } + + [Test] + public void NextExposure_最小が最大より大きい_ArgumentException() { + Assert.Throws((Action)(() => NextExp(0.01, 1, 0.0001, HwTestValueScale.Linear, 1))); + } + + [Test] + public void NextExposure_現在値がNaN_ArgumentException() { + Assert.Throws((Action)(() => NextExp(double.NaN, 0.0001, 1, HwTestValueScale.Linear, 1))); + } + + [Test] + public void NextExposure_範囲が無限大_ArgumentException() { + Assert.Throws((Action)(() => NextExp(0.01, 0, double.PositiveInfinity, HwTestValueScale.Linear, 1))); + } + + [Test] + public void NextExposure_Log2Stepで現在値が0_ArgumentException() { + Assert.Throws((Action)(() => NextExp(0, 0, 1, HwTestValueScale.Log2Step, 1))); + } + + [Test] + public void NextExposure_Log2Stepで段の量が0_ArgumentException() { + Assert.Throws((Action)(() => + HwTestJudge.NextExposure(0.01, 0, 1, HwTestValueScale.Log2Step, 1, 0, 0.5, 2))); + } + + [Test] + public void NextExposure_未知の種類_ArgumentException() { + Assert.Throws((Action)(() => NextExp(0.01, 0, 1, (HwTestValueScale)99, 1))); + } + + [Test] + public void AlignExposure_Linear_そのまま() { + Assert.That(HwTestJudge.AlignExposure(0.016667, 2e-5, 60, HwTestValueScale.Linear), Is.EqualTo(0.016667)); + } + + [Test] + public void AlignExposure_Log2Step_最も近い2のn乗に合わせる() { + // 実測: 元の 0.016667 に最も近い 2^n(0.015625)に合わせた + Assert.That(HwTestJudge.AlignExposure(0.016667, 2e-5, 60, HwTestValueScale.Log2Step), Is.EqualTo(0.015625).Within(1e-12)); + } + + [Test] + public void AlignExposure_Log2Step_最も近い2のn乗が最小未満_範囲内で最も近い2のn乗() { + // 0.02 に最も近いのは 2^-6(0.015625)だが最小 0.02 未満.範囲 [0.02, 1] の中で最も近いのは 2^-5 + Assert.That(HwTestJudge.AlignExposure(0.02, 0.02, 1, HwTestValueScale.Log2Step), Is.EqualTo(0.03125).Within(1e-12)); + } + + [Test] + public void AlignExposure_Log2Step_最も近い2のn乗が最大超え_範囲内で最も近い2のn乗() { + // 2.9 に最も近いのは 4(2^2)だが最大 2.9 を超える.範囲内で最も近いのは 2(2^1) + Assert.That(HwTestJudge.AlignExposure(2.9, 0.1, 2.9, HwTestValueScale.Log2Step), Is.EqualTo(2.0).Within(1e-12)); + } + + [Test] + public void AlignExposure_Log2Step_最大がちょうど2のn乗_最大に合わせる() { + // 7 に最も近いのは 8 = 最大(log の丸め誤差で 1 段ずれないこと) + Assert.That(HwTestJudge.AlignExposure(7, 0.1, 8, HwTestValueScale.Log2Step), Is.EqualTo(8.0).Within(1e-12)); + } + + [Test] + public void AlignExposure_Log2Step_範囲内に2のn乗が無い_そのまま() { + Assert.That(HwTestJudge.AlignExposure(0.35, 0.3, 0.4, HwTestValueScale.Log2Step), Is.EqualTo(0.35)); + } + + [Test] + public void AlignExposure_Log2Stepで現在値が0_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.AlignExposure(0, 0, 1, HwTestValueScale.Log2Step))); + } + + [Test] + public void AlignExposure_範囲が不正_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.AlignExposure(0.5, 1, 0, HwTestValueScale.Linear))); + } + + [Test] + public void ScaleSetting_倍率を掛ける() { + Assert.That(HwTestJudge.ScaleSetting(0.4, 0, 1, 0.5), Is.EqualTo(0.2).Within(Eps)); + } + + [Test] + public void ScaleSetting_範囲外_範囲内に収める() { + Assert.That(HwTestJudge.ScaleSetting(0.6, 0, 1, 2), Is.EqualTo(1.0)); + } + + [Test] + public void ScaleSetting_現在値0で上げる_最小から範囲の幅の5パーセント() { + Assert.That(HwTestJudge.ScaleSetting(0, 0, 10, 2), Is.EqualTo(0.5).Within(Eps)); + } + + [Test] + public void ScaleSetting_現在値0で下げる_変わらない() { + Assert.That(HwTestJudge.ScaleSetting(0, 0, 10, 0.5), Is.EqualTo(0.0)); + } + + [Test] + public void ScaleSetting_倍率0_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.ScaleSetting(0.5, 0, 1, 0))); + } + + [Test] + public void ScaleSetting_倍率NaN_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.ScaleSetting(0.5, 0, 1, double.NaN))); + } + + [Test] + public void StepSetting_範囲の幅の10パーセント上げる() { + // 実測: ゲイン 1(範囲 0〜48)→ 上げた 5.8 + Assert.That(HwTestJudge.StepSetting(1, 0, 48, HwTestThresholds.GainStepFraction), Is.EqualTo(5.8).Within(Eps)); + } + + [Test] + public void StepSetting_下げて最小を下回る_最小に収める() { + // 実測: ゲイン 1(範囲 0〜48)→ 下げた 0 + Assert.That(HwTestJudge.StepSetting(1, 0, 48, -HwTestThresholds.GainStepFraction), Is.EqualTo(0.0)); + } + + [Test] + public void StepSetting_上げて最大を超える_最大に収める() { + Assert.That(HwTestJudge.StepSetting(47, 0, 48, HwTestThresholds.GainStepFraction), Is.EqualTo(48.0)); + } + + [Test] + public void StepSetting_割合NaN_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.StepSetting(1, 0, 48, double.NaN))); + } + + [Test] + public void StepSetting_範囲が不正_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.StepSetting(1, 48, 0, 0.1))); + } + + // ================================================================ + // 輝度・飽和 + // ================================================================ + + [Test] + public void FrameStats_BGR_3チャネルの平均() { + var rows = new byte[] { 10, 20, 30, 40, 50, 60 }; + var s = HwTestJudge.FrameStats(rows, 1, 2, 6, 3, 1, 254); + Assert.That(s.Mean, Is.EqualTo(35.0).Within(Eps)); + } + + [Test] + public void FrameStats_飽和なし_割合0() { + var rows = new byte[] { 253, 253, 253, 0, 0, 0 }; + var s = HwTestJudge.FrameStats(rows, 1, 2, 6, 3, 1, 254); + Assert.That(s.SaturatedFraction, Is.EqualTo(0.0)); + } + + [Test] + public void FrameStats_いずれかのチャネルが254以上_飽和画素として数える() { + // 2 画素のうち 1 画素の R だけが 254 + var rows = new byte[] { 0, 0, 254, 100, 100, 100 }; + var s = HwTestJudge.FrameStats(rows, 1, 2, 6, 3, 1, HwTestThresholds.SaturatedPixelLevel); + Assert.That(s.SaturatedFraction, Is.EqualTo(0.5).Within(Eps)); + } + + [Test] + public void FrameStats_4チャネル_アルファを読まない() { + var rows = new byte[] { 0, 0, 0, 255, 30, 30, 30, 255 }; + var s = HwTestJudge.FrameStats(rows, 1, 2, 8, 4, 1, 254); + Assert.That(new[] { s.Mean, s.SaturatedFraction }, Is.EqualTo(new[] { 15.0, 0.0 }).Within(Eps)); + } + + [Test] + public void FrameStats_1チャネル_先頭チャネルの平均() { + var rows = new byte[] { 10, 30, 50, 70 }; + var s = HwTestJudge.FrameStats(rows, 2, 2, 2, 1, 1, 254); + Assert.That(s.Mean, Is.EqualTo(40.0).Within(Eps)); + } + + [Test] + public void FrameStats_間引き_指定画素ごとに読む() { + // 1 チャネル・4 画素で 2 画素ごと → 画素 0 と 2 だけを読む + var rows = new byte[] { 10, 200, 30, 200 }; + var s = HwTestJudge.FrameStats(rows, 1, 4, 4, 1, 2, 254); + Assert.That(s.Mean, Is.EqualTo(20.0).Within(Eps)); + } + + [Test] + public void FrameStats_行の詰め物_読まない() { + // 1 行 = 2 画素 × 3 バイト + 詰め物 2 バイト(255) + var rows = new byte[] { 10, 10, 10, 20, 20, 20, 255, 255, 30, 30, 30, 40, 40, 40, 255, 255 }; + var s = HwTestJudge.FrameStats(rows, 2, 2, 8, 3, 1, 254); + Assert.That(new[] { s.Mean, s.SaturatedFraction }, Is.EqualTo(new[] { 25.0, 0.0 }).Within(Eps)); + } + + [Test] + public void FrameStats_rowsがnull_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.FrameStats(null, 1, 1, 3, 3, 1, 254))); + } + + [Test] + public void FrameStats_1画素のバイト数が5_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.FrameStats(new byte[10], 1, 2, 10, 5, 1, 254))); + } + + [Test] + public void FrameStats_1行のバイト数が不足_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.FrameStats(new byte[6], 1, 2, 5, 3, 1, 254))); + } + + [Test] + public void FrameStats_画素データが不足_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.FrameStats(new byte[6], 2, 2, 6, 3, 1, 254))); + } + + [Test] + public void FrameStats_間引き0_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.FrameStats(new byte[6], 1, 2, 6, 3, 0, 254))); + } + + [Test] + public void FrameStats_画素なし_ArgumentException() { + Assert.Throws((Action)(() => HwTestJudge.FrameStats(new byte[6], 0, 2, 6, 3, 1, 254))); + } + + [Test] + public void IsSaturated_しきい値ちょうど_飽和でない() { + Assert.That(HwTestJudge.IsSaturated(0.01, 0.01), Is.False); + } + + [Test] + public void IsSaturated_しきい値超え_飽和() { + Assert.That(HwTestJudge.IsSaturated(0.0101, 0.01), Is.True); + } + + [Test] + public void IsSaturated_NaN_飽和でない() { + Assert.That(HwTestJudge.IsSaturated(double.NaN, 0.01), Is.False); + } + + [TestCase(30.0)] + [TestCase(125.0)] + [TestCase(220.0)] + public void IsMidBrightness_範囲内と境界_true(double mean) { + Assert.That(HwTestJudge.IsMidBrightness(mean, 30, 220), Is.True); + } + + [TestCase(29.99)] + [TestCase(220.01)] + [TestCase(double.NaN)] + public void IsMidBrightness_範囲外とNaN_false(double mean) { + Assert.That(HwTestJudge.IsMidBrightness(mean, 30, 220), Is.False); + } + + [Test] + public void IsBaselineCandidate_中間で飽和なし_true() { + // 実測: 露光 0.015625 の平均輝度 158.94・飽和画素の割合 0.00% は基準にできる + Assert.That(HwTestJudge.IsBaselineCandidate(158.94, 0.0, 30, 220, 0.01), Is.True); + } + + [Test] + public void IsBaselineCandidate_中間でも飽和_false() { + Assert.That(HwTestJudge.IsBaselineCandidate(200, 0.05, 30, 220, 0.01), Is.False); + } + + [Test] + public void IsBaselineCandidate_飽和割合NaN_false() { + Assert.That(HwTestJudge.IsBaselineCandidate(150, double.NaN, 30, 220, 0.01), Is.False); + } + + [Test] + public void IsBaselineCandidate_明るすぎ_false() { + Assert.That(HwTestJudge.IsBaselineCandidate(230, 0.0, 30, 220, 0.01), Is.False); + } + + [Test] + public void Summarize_平均輝度は平均() { + var m = HwTestJudge.Summarize(new[] { new HwTestFrameStats(100, 0.0), new HwTestFrameStats(110, 0.04) }); + Assert.That(m.Mean, Is.EqualTo(105.0).Within(Eps)); + } + + [Test] + public void Summarize_飽和割合は最大() { + var m = HwTestJudge.Summarize(new[] { new HwTestFrameStats(100, 0.0), new HwTestFrameStats(110, 0.04), new HwTestFrameStats(100, 0.02) }); + Assert.That(m.SaturatedFraction, Is.EqualTo(0.04).Within(Eps)); + } + + [Test] + public void Summarize_飽和割合の平均を参考に持つ() { + var m = HwTestJudge.Summarize(new[] { new HwTestFrameStats(100, 0.0), new HwTestFrameStats(110, 0.04) }); + Assert.That(m.MeanSaturatedFraction, Is.EqualTo(0.02).Within(Eps)); + } + + [Test] + public void Summarize_フレーム数() { + var m = HwTestJudge.Summarize(new[] { new HwTestFrameStats(100, 0.0), new HwTestFrameStats(110, 0.04) }); + Assert.That(m.Count, Is.EqualTo(2)); + } + + [Test] + public void Summarize_空_測定できない() { + var m = HwTestJudge.Summarize(new HwTestFrameStats[0]); + Assert.That(new object[] { double.IsNaN(m.Mean), double.IsNaN(m.SaturatedFraction), m.Count }, Is.EqualTo(new object[] { true, true, 0 })); + } + + [Test] + public void Summarize_null_測定できない() { + var m = HwTestJudge.Summarize(null); + Assert.That(new object[] { double.IsNaN(m.Mean), m.Count }, Is.EqualTo(new object[] { true, 0 })); + } + + // ================================================================ + // 基準探し + // ================================================================ + + [Test] + public void BaselineSearchDirection_明るすぎ_下げる() { + Assert.That(HwTestJudge.BaselineSearchDirection(230, 0.0, 30, 220, 0.01), Is.EqualTo(-1)); + } + + [Test] + public void BaselineSearchDirection_中間でも飽和_下げる() { + Assert.That(HwTestJudge.BaselineSearchDirection(200, 0.05, 30, 220, 0.01), Is.EqualTo(-1)); + } + + [Test] + public void BaselineSearchDirection_暗すぎ_上げる() { + Assert.That(HwTestJudge.BaselineSearchDirection(20, 0.0, 30, 220, 0.01), Is.EqualTo(1)); + } + + [Test] + public void BaselineSearchDirection_基準にできる明るさ_動かさない() { + Assert.That(HwTestJudge.BaselineSearchDirection(158.94, 0.0, 30, 220, 0.01), Is.EqualTo(0)); + } + + [Test] + public void BaselineSearchDirection_測定できない_動かさない() { + Assert.That(HwTestJudge.BaselineSearchDirection(double.NaN, double.NaN, 30, 220, 0.01), Is.EqualTo(0)); + } + + [Test] + public void DecideBaselineSearch_設定が変わらない_範囲の端を最優先() { + // 測定できない・中間に入った,のどちらにも当たる値でも範囲の端が先 + Assert.That(Decide(double.NaN, double.NaN, -1, false, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopRangeEnd)); + } + + [Test] + public void DecideBaselineSearch_設定が変わらない_中間でも範囲の端() { + Assert.That(Decide(150, 0.0, -1, false, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopRangeEnd)); + } + + [Test] + public void DecideBaselineSearch_平均輝度NaN_測れない() { + Assert.That(Decide(double.NaN, 0.0, -1, true, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopNotMeasured)); + } + + [Test] + public void DecideBaselineSearch_飽和割合NaN_測れない() { + Assert.That(Decide(150, double.NaN, -1, true, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopNotMeasured)); + } + + [Test] + public void DecideBaselineSearch_中間に入った_Found() { + Assert.That(Decide(150, 0.0, -1, true, 1), Is.EqualTo(HwTestBaselineSearchDecision.Found)); + } + + [Test] + public void DecideBaselineSearch_段数の上限で中間に入った_上限よりFoundが先() { + Assert.That(Decide(150, 0.0, -1, true, 12), Is.EqualTo(HwTestBaselineSearchDecision.Found)); + } + + [Test] + public void DecideBaselineSearch_下げる向きで暗すぎになった_飛び越えた() { + Assert.That(Decide(20, 0.0, -1, true, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopOvershoot)); + } + + [Test] + public void DecideBaselineSearch_上げる向きで明るすぎになった_飛び越えた() { + Assert.That(Decide(230, 0.0, 1, true, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopOvershoot)); + } + + [Test] + public void DecideBaselineSearch_上げる向きで飽和した_飛び越えた() { + Assert.That(Decide(200, 0.05, 1, true, 1), Is.EqualTo(HwTestBaselineSearchDecision.StopOvershoot)); + } + + [Test] + public void DecideBaselineSearch_段数の上限で飛び越えた_上限より飛び越えたが先() { + Assert.That(Decide(20, 0.0, -1, true, 12), Is.EqualTo(HwTestBaselineSearchDecision.StopOvershoot)); + } + + [Test] + public void DecideBaselineSearch_段数の上限でまだ明るすぎ_上限() { + Assert.That(Decide(230, 0.0, -1, true, 12), Is.EqualTo(HwTestBaselineSearchDecision.StopMaxSteps)); + } + + [Test] + public void DecideBaselineSearch_まだ明るすぎ_続ける() { + Assert.That(Decide(230, 0.0, -1, true, 3), Is.EqualTo(HwTestBaselineSearchDecision.Continue)); + } + + [Test] + public void DecideBaselineSearch_まだ暗すぎ_続ける() { + Assert.That(Decide(20, 0.0, 1, true, 3), Is.EqualTo(HwTestBaselineSearchDecision.Continue)); + } + + [Test] + public void ChooseBaselineIndex_中間に最も近い段() { + var samples = new List { + new HwTestBaselineSample(1, 250, 0), new HwTestBaselineSample(2, 228, 0), new HwTestBaselineSample(3, 10, 0), + }; + Assert.That(HwTestJudge.ChooseBaselineIndex(samples, 30, 220, 0.01), Is.EqualTo(1)); + } + + [Test] + public void ChooseBaselineIndex_飽和していない段を優先() { + // 段 0 は中間の範囲内だが飽和,段 1 は暗すぎるが飽和していない + var samples = new List { + new HwTestBaselineSample(1, 200, 0.05), new HwTestBaselineSample(2, 5, 0), + }; + Assert.That(HwTestJudge.ChooseBaselineIndex(samples, 30, 220, 0.01), Is.EqualTo(1)); + } + + [Test] + public void ChooseBaselineIndex_すべて飽和_その中で中間に最も近い段() { + var samples = new List { + new HwTestBaselineSample(1, 250, 0.5), new HwTestBaselineSample(2, 230, 0.2), + }; + Assert.That(HwTestJudge.ChooseBaselineIndex(samples, 30, 220, 0.01), Is.EqualTo(1)); + } + + [Test] + public void ChooseBaselineIndex_NaNは除く() { + var samples = new List { + new HwTestBaselineSample(1, double.NaN, double.NaN), new HwTestBaselineSample(2, 10, 0), + }; + Assert.That(HwTestJudge.ChooseBaselineIndex(samples, 30, 220, 0.01), Is.EqualTo(1)); + } + + [Test] + public void ChooseBaselineIndex_同じ近さ_先の段() { + var samples = new List { + new HwTestBaselineSample(1, 240, 0), new HwTestBaselineSample(2, 10, 0), + }; + Assert.That(HwTestJudge.ChooseBaselineIndex(samples, 30, 220, 0.01), Is.EqualTo(0)); + } + + [Test] + public void ChooseBaselineIndex_すべてNaN_マイナス1() { + var samples = new List { new HwTestBaselineSample(1, double.NaN, double.NaN) }; + Assert.That(HwTestJudge.ChooseBaselineIndex(samples, 30, 220, 0.01), Is.EqualTo(-1)); + } + + [Test] + public void ChooseBaselineIndex_空_マイナス1() { + Assert.That(HwTestJudge.ChooseBaselineIndex(new List(), 30, 220, 0.01), Is.EqualTo(-1)); + } + + [Test] + public void ChooseBaselineIndex_null_マイナス1() { + Assert.That(HwTestJudge.ChooseBaselineIndex(null, 30, 220, 0.01), Is.EqualTo(-1)); + } + + // ================================================================ + // 方向の判定(露光: 比・すべて確認した向きが期待どおり) + // ================================================================ + + [Test] + public void ExposureCriteria_比で見て下げる0_8以下_上げる1_25以上_すべて確認() { + var c = HwTestJudge.ExposureCriteria(); + Assert.That(new object[] { c.UseRatio, c.LowerLimit, c.RaiseLimit, c.MaxSaturatedFraction, c.RequireAllChecked }, + Is.EqualTo(new object[] { true, 0.8, 1.25, 0.01, true })); + } + + [Test] + public void JudgeDirection_露光_実測_下げる側が期待どおりで合格() { + // 実測: 基準 158.92 → 下げた 86.63(比 0.545).上げた 210.14 は飽和割合 70.18% + var r = HwTestJudge.JudgeDirection(M(158.92, 0.0), M(86.63, 0.0), M(210.14, 0.7018), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.True); + } + + [Test] + public void JudgeDirection_露光_実測_下げる側を確認して期待どおり() { + var r = HwTestJudge.JudgeDirection(M(158.92, 0.0), M(86.63, 0.0), M(210.14, 0.7018), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(new[] { r.LowerChecked, r.LowerOk }, Is.EqualTo(new[] { true, true })); + } + + [Test] + public void JudgeDirection_露光_実測_上げた側が飽和_確認しない() { + var r = HwTestJudge.JudgeDirection(M(158.92, 0.0), M(86.63, 0.0), M(210.14, 0.7018), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.RaiseChecked, Is.False); + } + + [Test] + public void JudgeDirection_露光_実測_説明に飽和のため確認しないと飽和割合が出る() { + var r = HwTestJudge.JudgeDirection(M(158.92, 0.0), M(86.63, 0.0), M(210.14, 0.7018), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Summary, Does.Contain("飽和のため確認しない(飽和割合 70.18%")); + } + + [Test] + public void JudgeDirection_露光_比が境界ちょうど_合格() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(80, 0), M(125, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.True); + } + + [Test] + public void JudgeDirection_露光_下げても暗くならない_不合格() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(81, 0), M(150, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.False); + } + + [Test] + public void JudgeDirection_露光_上げても明るくならない_不合格() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(50, 0), M(124, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.False); + } + + [Test] + public void JudgeDirection_露光_上げた側が飽和_比が足りなくても不合格にしない() { + // 飽和で頭打ちになり比 1.1 だが,飽和しているので確認しない(下げる側の合格で合格) + var r = HwTestJudge.JudgeDirection(M(100, 0), M(50, 0), M(110, 0.3), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.True); + } + + [Test] + public void JudgeDirection_露光_上げた側が飽和_比を満たしても期待どおりとしない() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(50, 0), M(200, 0.3), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.RaiseOk, Is.False); + } + + [Test] + public void JudgeDirection_露光_下げた側が飽和_確認しない() { + var r = HwTestJudge.JudgeDirection(M(200, 0), M(150, 0.05), M(255, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.LowerChecked, Is.False); + } + + [Test] + public void JudgeDirection_露光_下げた側が飽和_説明に飽和のため確認しないが出る() { + var r = HwTestJudge.JudgeDirection(M(200, 0), M(150, 0.05), M(255, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Summary, Does.Contain("下げる: 飽和のため確認しない(飽和割合")); + } + + [Test] + public void JudgeDirection_露光_飽和割合がしきい値ちょうど_確認する() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(50, 0), M(150, 0.01), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.RaiseChecked, Is.True); + } + + [Test] + public void JudgeDirection_露光_両側とも飽和_不合格() { + var r = HwTestJudge.JudgeDirection(M(200, 0), M(150, 0.05), M(255, 0.5), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.False); + } + + [Test] + public void JudgeDirection_露光_両側とも確認できない_説明に出る() { + var r = HwTestJudge.JudgeDirection(M(200, 0), M(150, 0.05), M(255, 0.5), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Summary, Does.Contain("どちらの向きも確認できません")); + } + + [Test] + public void JudgeDirection_露光_設定が変わらなかった向き_確認しない() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(100, 0), M(150, 0), false, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.LowerChecked, Is.False); + } + + [Test] + public void JudgeDirection_露光_片側だけ設定が変わり期待どおり_合格() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(100, 0), M(150, 0), false, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.True); + } + + [Test] + public void JudgeDirection_露光_基準が真っ暗_両側とも確認せず不合格() { + var r = HwTestJudge.JudgeDirection(M(3, 0), M(2, 0), M(8, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(new[] { r.LowerChecked, r.RaiseChecked, r.Pass }, Is.EqualTo(new[] { false, false, false })); + } + + [Test] + public void JudgeDirection_露光_基準が飽和_上げる向きを確認しない() { + var r = HwTestJudge.JudgeDirection(M(240, 0.2), M(120, 0), M(250, 0.2), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.RaiseChecked, Is.False); + } + + [Test] + public void JudgeDirection_露光_下げた側が測定できない_確認しない() { + var r = HwTestJudge.JudgeDirection(M(100, 0), HwTestMeasurement.NotMeasured, M(150, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.LowerChecked, Is.False); + } + + [Test] + public void JudgeDirection_基準が測定できない_不合格() { + var r = HwTestJudge.JudgeDirection(HwTestMeasurement.NotMeasured, M(50, 0), M(150, 0), true, true, HwTestJudge.ExposureCriteria()); + Assert.That(r.Pass, Is.False); + } + + [Test] + public void JudgeDirection_基準がnull_ArgumentNullException() { + Assert.Throws((Action)(() => + HwTestJudge.JudgeDirection(M(100, 0), M(50, 0), M(150, 0), true, true, null))); + } + + // ================================================================ + // 方向の判定(ゲイン: 差・片側だけでも合格) + // ================================================================ + + [Test] + public void GainCriteria_差で見て3以上_片側で合格() { + var c = HwTestJudge.GainCriteria(); + Assert.That(new object[] { c.UseRatio, c.LowerLimit, c.RaiseLimit, c.MaxSaturatedFraction, c.RequireAllChecked }, + Is.EqualTo(new object[] { false, 3.0, 3.0, 0.01, false })); + } + + [Test] + public void JudgeDirection_ゲイン_実測_上げた側が飽和でも下げる側で合格() { + // 実測: 基準 158.88 → 下げた 143.27(差 -15.6).上げた 206.02 は飽和割合 68.37% + var r = HwTestJudge.JudgeDirection(M(158.88, 0.0), M(143.27, 0.0), M(206.02, 0.6837), true, true, HwTestJudge.GainCriteria()); + Assert.That(r.Pass, Is.True); + } + + [Test] + public void JudgeDirection_ゲイン_実測_上げた側は確認しない() { + var r = HwTestJudge.JudgeDirection(M(158.88, 0.0), M(143.27, 0.0), M(206.02, 0.6837), true, true, HwTestJudge.GainCriteria()); + Assert.That(r.RaiseChecked, Is.False); + } + + [Test] + public void JudgeDirection_ゲイン_差が境界ちょうど_期待どおり() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(97, 0), M(103, 0), true, true, HwTestJudge.GainCriteria()); + Assert.That(new[] { r.LowerOk, r.RaiseOk }, Is.EqualTo(new[] { true, true })); + } + + [Test] + public void JudgeDirection_ゲイン_下げる側だけ足りない_片側で合格() { + // 範囲の下端で変化が小さい(DFK 33UX178 の実測: 10→0 で差 -3.2 の付近) + var r = HwTestJudge.JudgeDirection(M(100, 0), M(99, 0), M(110, 0), true, true, HwTestJudge.GainCriteria()); + Assert.That(r.Pass, Is.True); + } + + [Test] + public void JudgeDirection_ゲイン_片側で合格_説明に出る() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(99, 0), M(110, 0), true, true, HwTestJudge.GainCriteria()); + Assert.That(r.Summary, Does.Contain("一方の向きが期待どおりのため合格")); + } + + [Test] + public void JudgeDirection_ゲイン_両側とも足りない_不合格() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(99, 0), M(102, 0), true, true, HwTestJudge.GainCriteria()); + Assert.That(r.Pass, Is.False); + } + + [Test] + public void JudgeDirection_ゲイン_上げた側が飽和で下げる側が足りない_不合格() { + // 上げた側は差 +50 だが飽和しているので確認しない + var r = HwTestJudge.JudgeDirection(M(100, 0), M(99, 0), M(150, 0.5), true, true, HwTestJudge.GainCriteria()); + Assert.That(r.Pass, Is.False); + } + + [Test] + public void JudgeDirection_ゲイン_両側とも確認できない_不合格() { + var r = HwTestJudge.JudgeDirection(M(100, 0), M(100, 0), M(150, 0.5), false, true, HwTestJudge.GainCriteria()); + Assert.That(r.Pass, Is.False); + } + + // ================================================================ + // 光源・復元 + // ================================================================ + + [Test] + public void JudgeLight_実測_点灯168_63消灯15_20_合格() { + Assert.That(HwTestJudge.JudgeLight(168.63, 15.20, HwTestThresholds.MinLightDelta), Is.True); + } + + [Test] + public void JudgeLight_差が下限ちょうど_合格() { + Assert.That(HwTestJudge.JudgeLight(150, 50, 100), Is.True); + } + + [Test] + public void JudgeLight_差が下限未満_不合格() { + Assert.That(HwTestJudge.JudgeLight(149.9, 50, 100), Is.False); + } + + [Test] + public void JudgeLight_消灯の方が明るい_不合格() { + Assert.That(HwTestJudge.JudgeLight(50, 200, 100), Is.False); + } + + [Test] + public void JudgeLight_NaN_不合格() { + Assert.That(HwTestJudge.JudgeLight(double.NaN, 15, 100), Is.False); + } + + [Test] + public void MinLightDelta_100() { + Assert.That(HwTestThresholds.MinLightDelta, Is.EqualTo(100.0)); + } + + [Test] + public void IsSameSetting_相対誤差1パーセント以内_同じ() { + Assert.That(HwTestJudge.IsSameSetting(0.016667, 0.016667 * 1.005, 0.01, 0), Is.True); + } + + [Test] + public void IsSameSetting_相対誤差1パーセント超え_違う() { + Assert.That(HwTestJudge.IsSameSetting(0.016667, 0.016667 * 1.02, 0.01, 0), Is.False); + } + + [Test] + public void IsSameSetting_元が0でも絶対誤差の許容内_同じ() { + // ゲイン 0 dB(範囲 0〜48): 絶対誤差の許容 48 × 0.1% = 0.048 + Assert.That(HwTestJudge.IsSameSetting(0, 0.04, 0.01, 48 * HwTestThresholds.GainRestoreRangeFraction), Is.True); + } + + [Test] + public void IsSameSetting_元が0で絶対誤差の許容超え_違う() { + Assert.That(HwTestJudge.IsSameSetting(0, 0.05, 0.01, 48 * HwTestThresholds.GainRestoreRangeFraction), Is.False); + } + + [Test] + public void IsSameSetting_相対と絶対の大きい方を許容にする() { + // 元 1 → 1.04: 相対の許容は約 0.0104 だが,絶対の許容 0.048 の方が大きい + Assert.That(HwTestJudge.IsSameSetting(1, 1.04, 0.01, 0.048), Is.True); + } + + [Test] + public void IsSameSetting_NaN_違う() { + Assert.That(HwTestJudge.IsSameSetting(1, double.NaN, 0.01, 0.048), Is.False); + } + + [Test] + public void RestoreTolerances_相対1パーセント_ゲインは範囲の0_1パーセント() { + Assert.That(new[] { HwTestThresholds.RestoreRelativeTolerance, HwTestThresholds.GainRestoreRangeFraction }, + Is.EqualTo(new[] { 0.01, 0.001 })); + } + + [Test] + public void IsRestored_許容内_戻った() { + // 実測: 最初 168.73 → 元に戻した後 168.64 + Assert.That(HwTestJudge.IsRestored(168.73, 168.64, HwTestThresholds.RestoreTolerance), Is.True); + } + + [Test] + public void IsRestored_許容ちょうど_戻った() { + Assert.That(HwTestJudge.IsRestored(100, 110, 10), Is.True); + } + + [Test] + public void IsRestored_許容超え_戻っていない() { + Assert.That(HwTestJudge.IsRestored(100, 110.5, 10), Is.False); + } + + [Test] + public void IsRestored_NaN_戻っていない() { + Assert.That(HwTestJudge.IsRestored(100, double.NaN, 10), Is.False); + } + + [Test] + public void RelativeError_差を元の絶対値で割る() { + Assert.That(HwTestJudge.RelativeError(-2, -1), Is.EqualTo(0.5).Within(Eps)); + } + + [Test] + public void RelativeError_NaN_NaN() { + Assert.That(HwTestJudge.RelativeError(double.NaN, 1), Is.NaN); + } + + [Test] + public void RelativeError_元も読み戻しも0_0() { + Assert.That(HwTestJudge.RelativeError(0, 0), Is.EqualTo(0.0)); + } + + [Test] + public void RelativeError_元が0で読み戻しが0でない_正の無限大() { + Assert.That(HwTestJudge.RelativeError(0, 1), Is.EqualTo(double.PositiveInfinity)); + } + + [Test] + public void DescribeRestoredValue_丸めない値を往復できる書式で出す() { + var v = 1.0 / 60; + Assert.That(HwTestJudge.DescribeRestoredValue("露光", v, v), Does.Contain("読み戻し " + v.ToString("R", CultureInfo.InvariantCulture))); + } + + [Test] + public void DescribeRestoredValue_一致_相対誤差0パーセント() { + Assert.That(HwTestJudge.DescribeRestoredValue("露光", 0.5, 0.5), Does.Contain("相対誤差 0%")); + } + + [Test] + public void DescribeRestoredValue_元が0_元が0と出る() { + Assert.That(HwTestJudge.DescribeRestoredValue("ゲイン", 0, 0.01), Does.Contain("元が 0")); + } + + [Test] + public void DescribeRestoredValue_NaN_測定できないと出る() { + Assert.That(HwTestJudge.DescribeRestoredValue("ゲイン", 1, double.NaN), Does.Contain("測定できない")); + } + + [Test] + public void DescribeRestoredValue_小数点がカンマの文化でもピリオドで出す() { + var saved = Thread.CurrentThread.CurrentCulture; + string text; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); + text = HwTestJudge.DescribeRestoredValue("露光", 0.25, 0.25); + } finally { + Thread.CurrentThread.CurrentCulture = saved; + } + Assert.That(text, Does.Contain("読み戻し 0.25(元 0.25.")); + } + + // ================================================================ + // 校正の停止 + // ================================================================ + + [Test] + public void ShouldSuppressCalibration_チャート不要の検査_止める() { + Assert.That(HwTestJudge.ShouldSuppressCalibration(false), Is.True); + } + + [Test] + public void ShouldSuppressCalibration_チャートが必要な検査_止めない() { + Assert.That(HwTestJudge.ShouldSuppressCalibration(true), Is.False); + } + + [Test] + public void IsCalibrationLeaked_止めているのに校正中_漏れ() { + Assert.That(HwTestJudge.IsCalibrationLeaked(true, true, false), Is.True); + } + + [Test] + public void IsCalibrationLeaked_止めているのに校正済み_漏れ() { + Assert.That(HwTestJudge.IsCalibrationLeaked(true, false, true), Is.True); + } + + [Test] + public void IsCalibrationLeaked_止めていて校正なし_漏れでない() { + Assert.That(HwTestJudge.IsCalibrationLeaked(true, false, false), Is.False); + } + + [Test] + public void IsCalibrationLeaked_止めていない_校正中でも漏れでない() { + Assert.That(HwTestJudge.IsCalibrationLeaked(false, true, true), Is.False); + } + + // ================================================================ + // 終了コードと作業フォルダ + // ================================================================ + + [Test] + public void DecideExitCode_機種が見つからない_不合格数によらず2() { + Assert.That(HwTestJudge.DecideExitCode(true, 3), Is.EqualTo(2)); + } + + [Test] + public void DecideExitCode_不合格あり_1() { + Assert.That(HwTestJudge.DecideExitCode(false, 1), Is.EqualTo(1)); + } + + [Test] + public void DecideExitCode_すべて合格_0() { + Assert.That(HwTestJudge.DecideExitCode(false, 0), Is.EqualTo(0)); + } + + [Test] + public void ExitCodes_0合格_1不合格_2異常_3タイムアウト() { + Assert.That(new[] { HwTestExitCodes.Pass, HwTestExitCodes.Fail, HwTestExitCodes.Error, HwTestExitCodes.Timeout }, + Is.EqualTo(new[] { 0, 1, 2, 3 })); + } + + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void ShouldKeepWorkDir_keep指定_常に残す(int exitCode) { + Assert.That(HwTestJudge.ShouldKeepWorkDir(true, exitCode), Is.True); + } + + [TestCase(1)] + [TestCase(3)] + public void ShouldKeepWorkDir_不合格とタイムアウト_残す(int exitCode) { + Assert.That(HwTestJudge.ShouldKeepWorkDir(false, exitCode), Is.True); + } + + [TestCase(0)] + [TestCase(2)] + public void ShouldKeepWorkDir_合格と前提不足_消す(int exitCode) { + Assert.That(HwTestJudge.ShouldKeepWorkDir(false, exitCode), Is.False); + } + } +} +#endif diff --git a/TIASshot.Tests/HwTestOptionsTests.cs b/TIASshot.Tests/HwTestOptionsTests.cs new file mode 100644 index 0000000..7e9c80c --- /dev/null +++ b/TIASshot.Tests/HwTestOptionsTests.cs @@ -0,0 +1,223 @@ +#if HWTEST +using System; +using NUnit.Framework; + +namespace TIASshot.Tests { + /// + /// 実機テストの引数(HwTestOptions)の解析のテスト(層①). + /// + /// 仕様: PLAN_06「仕組み」・.claude/verify-profile.md「実機テスト」 + /// - 起動: TIASshot.exe --hwtest --camera <lucam|iscam> [--with-chart] [--hwtest-out <結果ファイル>] [--hwtest-keep] + /// - --camera は必須.機種は lucam / iscam のみ(指定した機種だけを使う) + /// - 未知のフラグ・値の欠落・重複・不正な機種は誤り(打ち間違いで別の検査が走らない) + /// - 先頭要素が実行ファイルのパス(Environment.GetCommandLineArgs の形)でもよい + /// 本クラスは HWTEST 構成(Debug|x64)でのみコンパイルされる. + /// + [TestFixture] + public class HwTestOptionsTests { + + private const string Exe = @"C:\app\TIASshot.exe"; + + private static HwTestOptions ParseOk(params string[] args) { + string error; + var options = HwTestOptions.Parse(args, out error); + Assert.That(error, Is.Null, "前提: 解析が成功すること"); + Assert.That(options, Is.Not.Null, "前提: 解析が成功すること"); + return options; + } + + private static string ParseError(params string[] args) { + string error; + var options = HwTestOptions.Parse(args, out error); + Assert.That(options, Is.Null, "誤りのときは null を返すこと"); + return error; + } + + // --------------------------------------------------------------- + // IsRequested + // --------------------------------------------------------------- + + [Test] + public void IsRequested_hwtestあり_true() { + Assert.That(HwTestOptions.IsRequested(new[] { Exe, "--hwtest", "--camera", "lucam" }), Is.True); + } + + [Test] + public void IsRequested_hwtestなし_false() { + Assert.That(HwTestOptions.IsRequested(new[] { Exe, "--selftest" }), Is.False); + } + + [Test] + public void IsRequested_null_false() { + Assert.That(HwTestOptions.IsRequested(null), Is.False); + } + + [Test] + public void IsRequested_空配列_false() { + Assert.That(HwTestOptions.IsRequested(new string[0]), Is.False); + } + + // --------------------------------------------------------------- + // TryParseCamera + // --------------------------------------------------------------- + + [Test] + public void TryParseCamera_lucam_Lucam() { + HwTestCameraKind kind; + var ok = HwTestOptions.TryParseCamera("lucam", out kind); + Assert.That(ok && kind == HwTestCameraKind.Lucam, Is.True); + } + + [Test] + public void TryParseCamera_iscam_IScam() { + HwTestCameraKind kind; + var ok = HwTestOptions.TryParseCamera("iscam", out kind); + Assert.That(ok && kind == HwTestCameraKind.IScam, Is.True); + } + + [Test] + public void TryParseCamera_大文字_受け付ける() { + HwTestCameraKind kind; + var ok = HwTestOptions.TryParseCamera("ISCAM", out kind); + Assert.That(ok && kind == HwTestCameraKind.IScam, Is.True); + } + + [TestCase("lumenera")] + [TestCase("")] + [TestCase(" lucam")] + public void TryParseCamera_不正な機種_false(string value) { + HwTestCameraKind kind; + Assert.That(HwTestOptions.TryParseCamera(value, out kind), Is.False); + } + + [Test] + public void TryParseCamera_null_false() { + HwTestCameraKind kind; + Assert.That(HwTestOptions.TryParseCamera(null, out kind), Is.False); + } + + // --------------------------------------------------------------- + // Parse: 正常系 + // --------------------------------------------------------------- + + [Test] + public void Parse_cameraのみ_機種が設定される() { + var o = ParseOk("--hwtest", "--camera", "iscam"); + Assert.That(o.Camera, Is.EqualTo(HwTestCameraKind.IScam)); + } + + [Test] + public void Parse_先頭が実行ファイルのパス_受け付ける() { + var o = ParseOk(Exe, "--hwtest", "--camera", "lucam"); + Assert.That(o.Camera, Is.EqualTo(HwTestCameraKind.Lucam)); + } + + [Test] + public void Parse_省略時_既定値() { + var o = ParseOk(Exe, "--hwtest", "--camera", "lucam"); + Assert.That(new object[] { o.OutPath, o.WithChart, o.Keep }, Is.EqualTo(new object[] { null, false, false })); + } + + [Test] + public void Parse_hwtestOut_出力先が設定される() { + var o = ParseOk(Exe, "--hwtest", "--camera", "lucam", "--hwtest-out", @"C:\tmp\result.txt"); + Assert.That(o.OutPath, Is.EqualTo(@"C:\tmp\result.txt")); + } + + [Test] + public void Parse_withChart_true() { + var o = ParseOk(Exe, "--hwtest", "--camera", "lucam", "--with-chart"); + Assert.That(o.WithChart, Is.True); + } + + [Test] + public void Parse_hwtestKeep_true() { + var o = ParseOk(Exe, "--hwtest", "--camera", "lucam", "--hwtest-keep"); + Assert.That(o.Keep, Is.True); + } + + [Test] + public void Parse_順序が入れ替わっても_同じ結果() { + var o = ParseOk(Exe, "--hwtest-out", "r.txt", "--with-chart", "--camera", "iscam", "--hwtest", "--hwtest-keep"); + Assert.That(new object[] { o.Camera, o.OutPath, o.WithChart, o.Keep }, + Is.EqualTo(new object[] { HwTestCameraKind.IScam, "r.txt", true, true })); + } + + [Test] + public void Parse_機種名の大文字_受け付ける() { + var o = ParseOk(Exe, "--hwtest", "--camera", "LuCam"); + Assert.That(o.Camera, Is.EqualTo(HwTestCameraKind.Lucam)); + } + + // --------------------------------------------------------------- + // Parse: 誤り + // --------------------------------------------------------------- + + [Test] + public void Parse_null_誤り() { + Assert.That(ParseError(null), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_cameraなし_誤り() { + Assert.That(ParseError(Exe, "--hwtest"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_cameraの値の欠落_末尾_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_cameraの値の欠落_次がフラグ_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "--with-chart"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_hwtestOutの値の欠落_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "--hwtest-out"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_hwtestOutの値が空白_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "--hwtest-out", " "), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_不正な機種_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lumenera"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_不正な機種_誤りの理由に値が含まれる() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lumenera"), Does.Contain("lumenera")); + } + + [Test] + public void Parse_cameraの重複_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "--camera", "iscam"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_hwtestOutの重複_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "--hwtest-out", "a.txt", "--hwtest-out", "b.txt"), + Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_未知のフラグ_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "--with-chrat"), Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Parse_未知のフラグ_誤りの理由にフラグが含まれる() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "--with-chrat"), Does.Contain("--with-chrat")); + } + + [Test] + public void Parse_先頭以外の余分な値_誤り() { + Assert.That(ParseError(Exe, "--hwtest", "--camera", "lucam", "extra"), Is.Not.Null.And.Not.Empty); + } + } +} +#endif diff --git a/TIASshot.Tests/IScamDeviceSelectorTests.cs b/TIASshot.Tests/IScamDeviceSelectorTests.cs new file mode 100644 index 0000000..9458d6c --- /dev/null +++ b/TIASshot.Tests/IScamDeviceSelectorTests.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using NUnit.Framework; + +namespace TIASshot.Tests { + /// + /// ImagingSource の接続先デバイスの選択(IScamDeviceSelector)のテスト(層①.IScam から切り出した判断ロジック). + /// + /// 仕様: PLAN_06「カメラの選択」(両機種を同時につないでも指定した機種だけを使う)・IScamDeviceSelector の契約 + /// - DeviceName(デバイス状態ファイル名・設定キー)は列挙名から空白を除いたもの + /// - キーが(大文字小文字を区別して)一致する最初の列挙名(空白入りの元の名前)を返す.見つからなければ null + /// + [TestFixture] + public class IScamDeviceSelectorTests { + + [Test] + public void ToDeviceKey_空白を除く() { + Assert.That(IScamDeviceSelector.ToDeviceKey("DFK 33UX178"), Is.EqualTo("DFK33UX178")); + } + + [Test] + public void ToDeviceKey_複数の空白_すべて除く() { + Assert.That(IScamDeviceSelector.ToDeviceKey(" DFK 33 UX178 "), Is.EqualTo("DFK33UX178")); + } + + [Test] + public void ToDeviceKey_null_null() { + Assert.That(IScamDeviceSelector.ToDeviceKey(null), Is.Null); + } + + [Test] + public void FindDeviceName_キーが一致する元の名前を返す() { + var names = new[] { "DFK 23UX249", "DFK 33UX178" }; + Assert.That(IScamDeviceSelector.FindDeviceName(names, "DFK33UX178"), Is.EqualTo("DFK 33UX178")); + } + + [Test] + public void FindDeviceName_同じキーが複数_最初の名前() { + var names = new[] { "DFK 33UX178", "DFK33UX178" }; + Assert.That(IScamDeviceSelector.FindDeviceName(names, "DFK33UX178"), Is.EqualTo("DFK 33UX178")); + } + + [Test] + public void FindDeviceName_大文字小文字が違う_一致しない() { + var names = new[] { "DFK 33UX178" }; + Assert.That(IScamDeviceSelector.FindDeviceName(names, "dfk33ux178"), Is.Null); + } + + [Test] + public void FindDeviceName_一致なし_null() { + var names = new[] { "DFK 23UX249" }; + Assert.That(IScamDeviceSelector.FindDeviceName(names, "DFK33UX178"), Is.Null); + } + + [Test] + public void FindDeviceName_一覧がnull_null() { + Assert.That(IScamDeviceSelector.FindDeviceName(null, "DFK33UX178"), Is.Null); + } + + [Test] + public void FindDeviceName_一覧が空_null() { + Assert.That(IScamDeviceSelector.FindDeviceName(new List(), "DFK33UX178"), Is.Null); + } + + [Test] + public void FindDeviceName_キーがnull_null() { + Assert.That(IScamDeviceSelector.FindDeviceName(new[] { "DFK 33UX178" }, null), Is.Null); + } + + [Test] + public void FindDeviceName_キーが空_null() { + // 一覧に空の名前があっても空のキーには一致させない + Assert.That(IScamDeviceSelector.FindDeviceName(new[] { "", "DFK 33UX178" }, ""), Is.Null); + } + + [Test] + public void FindDeviceName_一覧中のnullと空を飛ばす() { + var names = new[] { null, "", "DFK 33UX178" }; + Assert.That(IScamDeviceSelector.FindDeviceName(names, "DFK33UX178"), Is.EqualTo("DFK 33UX178")); + } + } +} diff --git a/TIASshot.Tests/LightSourceTests.cs b/TIASshot.Tests/LightSourceTests.cs new file mode 100644 index 0000000..995bb52 --- /dev/null +++ b/TIASshot.Tests/LightSourceTests.cs @@ -0,0 +1,35 @@ +using System; +using NUnit.Framework; + +namespace TIASshot.Tests { + /// + /// 光源(LightSource)のうち実機なしで確かめられる部分のテスト(層①). + /// + /// 仕様: PLAN_06「守ること」・.claude/verify-profile.md「実機テスト」の後始末 + /// - Close は消灯せずにポートを閉じる.開いていなければ何もしない(例外にならない) + /// - 終了処理・実機テストの後始末から何度呼ばれても止めない + /// ポートを開いた状態で消灯の命令を送らないことは実機でしか確かめられない(層④.実機テストの cleanup). + /// + [TestFixture] + public class LightSourceTests { + + [Test] + public void Close_ポートが開いていない_例外にならない() { + var light = new LightSource(); + Assert.DoesNotThrow((Action)(() => light.Close())); + } + + [Test] + public void Close_2回呼ぶ_例外にならない() { + var light = new LightSource(); + Assert.DoesNotThrow((Action)(() => { light.Close(); light.Close(); })); + } + + [Test] + public void Close_ポートが開いていない_閉じた状態のまま() { + var light = new LightSource(); + light.Close(); + Assert.That(light.IsOpen, Is.False); + } + } +} diff --git a/TIASshot.Tests/SelfTestConfigTests.cs b/TIASshot.Tests/SelfTestConfigTests.cs new file mode 100644 index 0000000..70f74b4 --- /dev/null +++ b/TIASshot.Tests/SelfTestConfigTests.cs @@ -0,0 +1,78 @@ +#if SELFTEST +using System; +using System.IO; +using NUnit.Framework; + +namespace TIASshot.Tests { + /// + /// 一時設定ファイルの準備(SelfTestConfig.Prepare)の書き出し先のファイル名のテスト. + /// + /// 仕様: .claude/verify-profile.md「実機テスト」の後始末(実機テストの一時設定ファイルは config.hwtest.xml)・PLAN_06「守ること」 + /// - fileName を省略したときは従来どおり config.selftest.xml に書き出す + /// - fileName に config.hwtest.xml を渡すとそのファイルに書き出し,config.selftest.xml は作らない + /// - 同梱の config.xml は読むだけ.作業は一時フォルダで行い,テストの終わりに削除する + /// 本クラスは SELFTEST 構成(Debug|x64)でのみコンパイルされる. + /// + [TestFixture] + public class SelfTestConfigTests { + + private string _sourceDir; + private string _workDir; + + [SetUp] + public void SetUp() { + _sourceDir = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "..", "..", "TIASshot")); + Assert.That(File.Exists(Path.Combine(_sourceDir, "config.xml")), Is.True, $"前提: 同梱 config.xml があること: {_sourceDir}"); + _workDir = Path.Combine(Path.GetTempPath(), "TIASshot-it", "selftestconfig-" + Guid.NewGuid().ToString("N")); + } + + [TearDown] + public void TearDown() { + if (Directory.Exists(_workDir)) Directory.Delete(_workDir, true); + } + + private SelfTestConfig.Prepared Prepare(string fileName) { + var source = Path.Combine(_sourceDir, "config.xml"); + return fileName == null + ? SelfTestConfig.Prepare(source, _sourceDir, _workDir) + : SelfTestConfig.Prepare(source, _sourceDir, _workDir, null, fileName); + } + + [Test] + public void Prepare_ファイル名の省略_configSelftestXmlのパスを返す() { + var prepared = Prepare(null); + Assert.That(prepared.ConfigPath, Is.EqualTo(Path.Combine(_workDir, "config.selftest.xml"))); + } + + [Test] + public void Prepare_ファイル名の省略_configSelftestXmlが作られる() { + Prepare(null); + Assert.That(File.Exists(Path.Combine(_workDir, "config.selftest.xml")), Is.True); + } + + [Test] + public void Prepare_configHwtestXmlを指定_そのパスを返す() { + var prepared = Prepare("config.hwtest.xml"); + Assert.That(prepared.ConfigPath, Is.EqualTo(Path.Combine(_workDir, "config.hwtest.xml"))); + } + + [Test] + public void Prepare_configHwtestXmlを指定_そのファイルが作られる() { + Prepare("config.hwtest.xml"); + Assert.That(File.Exists(Path.Combine(_workDir, "config.hwtest.xml")), Is.True); + } + + [Test] + public void Prepare_configHwtestXmlを指定_configSelftestXmlは作られない() { + Prepare("config.hwtest.xml"); + Assert.That(File.Exists(Path.Combine(_workDir, "config.selftest.xml")), Is.False); + } + + [Test] + public void Prepare_configHwtestXmlを指定_保存先は作業フォルダ配下() { + var prepared = Prepare("config.hwtest.xml"); + Assert.That(Path.GetFullPath(prepared.SaveRoot), Does.StartWith(Path.GetFullPath(_workDir))); + } + } +} +#endif diff --git a/TIASshot.Tests/TIASshot.Tests.csproj b/TIASshot.Tests/TIASshot.Tests.csproj index bcc7a6b..f745aa4 100644 --- a/TIASshot.Tests/TIASshot.Tests.csproj +++ b/TIASshot.Tests/TIASshot.Tests.csproj @@ -24,8 +24,8 @@ true bin\x64\Debug\ - - DEBUG;TRACE;SELFTEST + + DEBUG;TRACE;SELFTEST;HWTEST full x64 7.3 @@ -96,7 +96,12 @@ + + + + +