#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] <ID>: ...` / `[FAIL] <ID>: ...` の見出し行と,その下の詳細行
/// (` - ` 情報,` 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