Newer
Older
TIASShot / TIASshot.Tests / SelfTestReportTests.cs
#if SELFTEST
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using NUnit.Framework;

namespace TIASshot.Tests {
    /// <summary>
    /// SelfTestCheck(項目判定)と SelfTestReport(結果ファイル・例外要約)のテスト.
    ///
    /// 仕様: .claude/verify-profile.md「判定の読み方」
    ///   - 項目ごとに `[PASS] &lt;ID&gt;: ...` / `[FAIL] &lt;ID&gt;: ...` の見出し行と,その下の詳細行
    ///     (`  - ` 情報,`  WARN:` 参考,`  FAIL:` 失敗理由)
    ///   - WARN は判定対象外(カメラ SDK 由来の失敗等).FAIL が 1 つでもあれば項目は FAIL
    ///   - 結果ファイルは UTF-8 BOM 付き
    ///   - `FAIL:` 行には例外型・メッセージ・対象アセンブリ名(`[assembly: ...]`)・Fusion ログの要点(`[fusion: ...]`)・スタックの先頭が入る
    /// 本クラスは SELFTEST 構成(Debug|x64)でのみコンパイルされる.
    /// </summary>
    [TestFixture]
    public class SelfTestReportTests {

        // ---------------------------------------------------------------
        // SelfTestCheck: PASS/FAIL/WARN の集約
        // ---------------------------------------------------------------

        [Test]
        public void Check_記録なし_PASS() {
            var c = new SelfTestCheck("deps", "t");
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Pass));
        }

        [Test]
        public void Check_Infoのみ_PASS() {
            var c = new SelfTestCheck("deps", "t");
            c.Info("情報");
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Pass));
        }

        [Test]
        public void Check_Warnのみ_判定に影響せずPASS() {
            var c = new SelfTestCheck("deps", "t");
            c.Warn("カメラ SDK のロード失敗");
            c.Warn("もう一件");
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Pass));
            Assert.That(c.WarnCount, Is.EqualTo(2));
        }

        [Test]
        public void Check_Failが1件_FAIL() {
            var c = new SelfTestCheck("deps", "t");
            c.Info("ok");
            c.Warn("warn");
            c.Fail("ng");
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Fail));
        }

        [Test]
        public void Check_詳細行の書式_情報WARNFAILの接頭辞() {
            var c = new SelfTestCheck("deps", "t");
            c.Info("i");
            c.Warn("w");
            c.Fail("f");
            Assert.That(c.Lines, Is.EqualTo(new[] { "  - i", "  WARN: w", "  FAIL: f" }));
        }

        [Test]
        public void Expect_条件偽_Failを記録して偽を返す() {
            var c = new SelfTestCheck("deps", "t");
            var r = c.Expect(false, "失敗理由");
            Assert.That(r, Is.False);
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Fail));
            Assert.That(c.Lines, Has.Some.Contains("失敗理由"));
        }

        [Test]
        public void Expect_条件真_PASSのまま真を返しokメッセージを残す() {
            var c = new SelfTestCheck("deps", "t");
            var r = c.Expect(true, "失敗理由", "成功");
            Assert.That(r, Is.True);
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Pass));
            Assert.That(c.Lines, Is.EqualTo(new[] { "  - 成功" }));
        }

        [Test]
        public void FailException_例外型とメッセージを含むFAIL行() {
            var c = new SelfTestCheck("deps", "t");
            c.FailException("ロード", new FileLoadException("load failed", "Foo, Version=1.0.0.0"));
            Assert.That(c.Status, Is.EqualTo(SelfTestStatus.Fail));
            Assert.That(c.Lines[0], Does.StartWith("  FAIL: ロード: "));
            Assert.That(c.Lines[0], Does.Contain("System.IO.FileLoadException"));
            Assert.That(c.Lines[0], Does.Contain("[assembly: Foo, Version=1.0.0.0]"));
        }

        // ---------------------------------------------------------------
        // DescribeException: 例外要約
        // ---------------------------------------------------------------

        [Test]
        public void Describe_FileLoadException_型メッセージアセンブリ名を含む() {
            var ex = new FileLoadException("Could not load file", "System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51");
            var s = SelfTestReport.DescribeException(ex);
            Assert.That(s, Does.Contain("System.IO.FileLoadException: Could not load file"));
            Assert.That(s, Does.Contain("[assembly: System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51]"));
        }

        [Test]
        public void Describe_FileNotFoundException_アセンブリ名を含む() {
            var s = SelfTestReport.DescribeException(new FileNotFoundException("not found", "OpenCvSharp.Extensions"));
            Assert.That(s, Does.Contain("System.IO.FileNotFoundException: not found"));
            Assert.That(s, Does.Contain("[assembly: OpenCvSharp.Extensions]"));
        }

        [Test]
        public void Describe_BadImageFormatException_アセンブリ名を含む() {
            var s = SelfTestReport.DescribeException(new BadImageFormatException("bad image", "OpenCvSharpExtern.dll"));
            Assert.That(s, Does.Contain("System.BadImageFormatException: bad image"));
            Assert.That(s, Does.Contain("[assembly: OpenCvSharpExtern.dll]"));
        }

        [Test]
        public void Describe_ファイル名なし_assembly欄を出さない() {
            var s = SelfTestReport.DescribeException(new FileLoadException("no name"));
            Assert.That(s, Does.Not.Contain("[assembly:"));
        }

        [Test]
        public void Describe_通常の例外_型とメッセージのみ() {
            var s = SelfTestReport.DescribeException(new InvalidOperationException("boom"));
            Assert.That(s, Is.EqualTo("System.InvalidOperationException: boom"));
        }

        [Test]
        public void Describe_内部例外_外側から内側まで辿りアセンブリ名も出す() {
            var inner = new FileLoadException("inner load", "System.Buffers, Version=4.0.3.0");
            var outer = new TypeInitializationException("TIASshot.Form1", inner);
            var s = SelfTestReport.DescribeException(outer);
            Assert.That(s, Does.StartWith("System.TypeInitializationException: "));
            Assert.That(s, Does.Contain(" <- inner: System.IO.FileLoadException: inner load"));
            Assert.That(s, Does.Contain("[assembly: System.Buffers, Version=4.0.3.0]"));
        }

        [Test]
        public void Describe_深い内部例外の連鎖_例外を出さず外側を含む() {
            Exception ex = new Exception("level-0");
            for (var i = 1; i <= 20; i++) ex = new Exception($"level-{i}", ex);
            string s = null;
            Assert.DoesNotThrow((Action)(() => s = SelfTestReport.DescribeException(ex)));
            Assert.That(s, Does.StartWith("System.Exception: level-20"));
        }

        [Test]
        public void Describe_ReflectionTypeLoadException_LoaderExceptionsを含みnullは無視() {
            var loaders = new Exception[] {
                new FileNotFoundException("missing dep", "Foo"),
                null,
                new TypeLoadException("bad type"),
            };
            var ex = new ReflectionTypeLoadException(new Type[] { null, null, null }, loaders, "types failed");
            var s = SelfTestReport.DescribeException(ex);
            Assert.That(s, Does.Contain("[LoaderException: FileNotFoundException: missing dep]"));
            Assert.That(s, Does.Contain("[LoaderException: TypeLoadException: bad type]"));
            Assert.That(s, Does.Not.Contain("[LoaderException: ]"));
        }

        [Test]
        public void Describe_Fusionログ_参照リダイレクト失敗理由の行だけを抜粋する() {
            var fusion = string.Join("\r\n", new[] {
                "=== Pre-bind state information ===",
                "LOG: DisplayName = System.Memory, Version=4.0.1.1",
                "Calling assembly : OpenCvSharp, Version=1.0.0.0.",
                "LOG: Redirect found in application configuration file: 4.0.1.1 redirected to 4.0.1.2.",
                "LOG: Post-policy reference: System.Memory, Version=4.0.1.2",
                "LOG: Attempting download of new URL file:///C:/app/System.Memory.DLL.",
                "WRN: Comparing the assembly name resulted in the mismatch: Minor Version",
                "ERR: Failed to complete setup of assembly (hr = 0x80131040). Probing terminated.",
            });
            var ex = CreateFileLoadExceptionWithFusionLog("mismatch", "System.Memory, Version=4.0.1.2", fusion);
            var s = SelfTestReport.DescribeException(ex);
            Assert.That(s, Does.Contain("[fusion: "));
            Assert.That(s, Does.Contain("LOG: Post-policy reference: System.Memory, Version=4.0.1.2"));
            Assert.That(s, Does.Contain("LOG: Redirect found in application configuration file"));
            Assert.That(s, Does.Contain("WRN: Comparing the assembly name"));
            Assert.That(s, Does.Contain("ERR: Failed to complete setup"));
            Assert.That(s, Does.Contain("Calling assembly : OpenCvSharp"));
            Assert.That(s, Does.Not.Contain("Attempting download"));
            Assert.That(s, Does.Not.Contain("Pre-bind state"));
        }

        [Test]
        public void Describe_送出された例外_スタックの先頭を含む() {
            Exception caught = null;
            try {
                throw new InvalidOperationException("thrown");
            } catch (InvalidOperationException ex) {
                caught = ex;
            }
            var s = SelfTestReport.DescribeException(caught);
            Assert.That(s, Does.Contain("[stack: "));
            Assert.That(s, Does.Contain(nameof(Describe_送出された例外_スタックの先頭を含む)));
        }

        // ---------------------------------------------------------------
        // SelfTestReport: 結果ファイル
        // ---------------------------------------------------------------

        [Test]
        public void Report_結果ファイル_UTF8BOM付きで見出し行と詳細行を書く() {
            var dir = Path.Combine(Path.GetTempPath(), "TIASshot-tests-" + Guid.NewGuid().ToString("N"), "sub");
            var path = Path.Combine(dir, "result.txt");
            try {
                var ok = new SelfTestCheck("deps", "依存");
                ok.Info("情報");
                var ng = new SelfTestCheck("preview", "プレビュー");
                ng.Fail("理由");
                using (var report = new SelfTestReport(path)) {
                    report.WriteCheck(ok);
                    report.WriteCheck(ng);
                    report.WriteLine("RESULT: FAIL (1/2 failed: preview)");
                }

                var bytes = File.ReadAllBytes(path);
                Assert.That(bytes.Length, Is.GreaterThan(3));
                Assert.That(new[] { bytes[0], bytes[1], bytes[2] }, Is.EqualTo(new byte[] { 0xEF, 0xBB, 0xBF }), "UTF-8 BOM 付き");
                var lines = File.ReadAllLines(path, Encoding.UTF8);
                Assert.That(lines, Is.EqualTo(new[] {
                    "[PASS] deps: 依存",
                    "  - 情報",
                    "[FAIL] preview: プレビュー",
                    "  FAIL: 理由",
                    "RESULT: FAIL (1/2 failed: preview)",
                }));
            } finally {
                var root = Path.GetDirectoryName(dir);
                if (Directory.Exists(root)) Directory.Delete(root, true);
            }
        }

        // ---------------------------------------------------------------
        // ヘルパ
        // ---------------------------------------------------------------

        /// <summary>
        /// FusionLog 付きの FileLoadException を作る(FusionLog は公開コンストラクタで設定できないため,
        /// 実物の直列化情報の FusionLog だけを差し替えて逆直列化コンストラクタで復元する).
        /// </summary>
        private static FileLoadException CreateFileLoadExceptionWithFusionLog(string message, string fileName, string fusionLog) {
            var src = new FileLoadException(message, fileName);
            var ctx = new StreamingContext(StreamingContextStates.All);
            var orig = new SerializationInfo(typeof(FileLoadException), new FormatterConverter());
            src.GetObjectData(orig, ctx);
            var info = new SerializationInfo(typeof(FileLoadException), new FormatterConverter());
            foreach (var entry in orig) {
                if (entry.Name == "FileLoad_FusionLog") continue;
                info.AddValue(entry.Name, entry.Value, entry.ObjectType);
            }
            info.AddValue("FileLoad_FusionLog", fusionLog, typeof(string));
            var ctor = typeof(FileLoadException).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null,
                new[] { typeof(SerializationInfo), typeof(StreamingContext) }, null);
            var ex = (FileLoadException)ctor.Invoke(new object[] { info, ctx });
            Assume.That(ex.FusionLog, Is.EqualTo(fusionLog), "テスト用の FusionLog を設定できること(前提)");
            return ex;
        }
    }
}
#endif