#if SELFTEST
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace TIASshot {
/// <summary>
/// セルフテスト項目の判定(SELFTEST 限定).
/// </summary>
internal enum SelfTestStatus {
Pass,
Fail,
}
/// <summary>
/// セルフテスト 1 項目分の記録(SELFTEST 限定).
/// Fail を 1 つでも記録した項目は FAIL となる.Warn は判定に影響しない参考情報.
/// </summary>
internal sealed class SelfTestCheck {
public string Id { get; }
public string Title { get; }
public List<string> Lines { get; } = new List<string>();
public int FailCount { get; private set; }
public int WarnCount { get; private set; }
public SelfTestStatus Status => FailCount == 0 ? SelfTestStatus.Pass : SelfTestStatus.Fail;
public SelfTestCheck(string id, string title) {
Id = id;
Title = title;
}
public void Info(string msg) => Lines.Add($" - {msg}");
public void Warn(string msg) {
WarnCount++;
Lines.Add($" WARN: {msg}");
}
public void Fail(string msg) {
FailCount++;
Lines.Add($" FAIL: {msg}");
}
/// <summary>条件が偽なら Fail を記録する.真なら ok メッセージを Info として残す(任意).</summary>
public bool Expect(bool condition, string failMsg, string okMsg = null) {
if (!condition) {
Fail(failMsg);
return false;
}
if (okMsg != null) Info(okMsg);
return true;
}
/// <summary>想定外の例外を詳細(例外型・メッセージ・アセンブリ名・Fusion ログ)付きで Fail として記録する.</summary>
public void FailException(string context, Exception ex) {
Fail($"{context}: {SelfTestReport.DescribeException(ex)}");
}
}
/// <summary>
/// セルフテスト結果の出力先(SELFTEST 限定).
/// WinForms アプリ(WinExe)は標準出力がコンソールに繋がらないため,
/// (1) 標準出力が既にリダイレクトされていればそこへ,無ければ親プロセスのコンソールへ AttachConsole して書き,
/// (2) 同じ内容を結果ファイル(UTF-8 BOM 付き)にも逐次書き出す(途中でハングしても書けた分は残る).
/// verifier 等の自動判定は結果ファイルと終了コードを正とする.
/// </summary>
internal sealed class SelfTestReport : IDisposable {
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int nStdHandle);
private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_OUTPUT_HANDLE = -11;
private readonly object _lock = new object();
private readonly StreamWriter _file;
private readonly bool _consoleAvailable;
public string OutputPath { get; }
public SelfTestReport(string outputPath) {
OutputPath = outputPath;
_consoleAvailable = PrepareConsole();
var dir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
_file = new StreamWriter(outputPath, false, new UTF8Encoding(true)) { AutoFlush = true };
}
/// <summary>
/// 標準出力の準備.リダイレクト済み(パイプ・ファイル)ならそのまま使い,
/// 未接続なら親プロセスのコンソールにアタッチする.どちらも無ければファイル出力のみとする.
/// </summary>
private static bool PrepareConsole() {
var handle = GetStdHandle(STD_OUTPUT_HANDLE);
if (handle != IntPtr.Zero && handle != new IntPtr(-1)) return true;
return AttachConsole(ATTACH_PARENT_PROCESS);
}
public void WriteLine(string line = "") {
lock (_lock) {
_file.WriteLine(line);
if (_consoleAvailable) {
try {
Console.Out.WriteLine(line);
Console.Out.Flush();
} catch (IOException ex) {
// コンソール側の失敗(パイプ切断等)は結果ファイルに残す(判定は結果ファイルで行える)
_file.WriteLine($" (標準出力への書き込みに失敗: {ex.Message})");
}
}
}
}
public void WriteCheck(SelfTestCheck check) {
WriteLine($"[{(check.Status == SelfTestStatus.Pass ? "PASS" : "FAIL")}] {check.Id}: {check.Title}");
foreach (var line in check.Lines) WriteLine(line);
}
public void Dispose() {
lock (_lock) {
_file.Dispose();
}
}
/// <summary>
/// 例外を 1 行に要約する.内部例外を辿り,アセンブリ解決系(FileNotFound / FileLoad / BadImageFormat)は
/// 対象アセンブリ名と Fusion ログの要点を,型ロード系は LoaderExceptions を含める.
/// </summary>
public static string DescribeException(Exception ex) {
var sb = new StringBuilder();
var depth = 0;
for (var e = ex; e != null && depth < 6; e = e.InnerException, depth++) {
if (depth > 0) sb.Append(" <- inner: ");
sb.Append($"{e.GetType().FullName}: {e.Message}");
switch (e) {
case FileNotFoundException fnf:
AppendAssemblyDetail(sb, fnf.FileName, fnf.FusionLog);
break;
case FileLoadException fl:
AppendAssemblyDetail(sb, fl.FileName, fl.FusionLog);
break;
case BadImageFormatException bif:
AppendAssemblyDetail(sb, bif.FileName, bif.FusionLog);
break;
case System.Reflection.ReflectionTypeLoadException rtl:
foreach (var le in rtl.LoaderExceptions.Where(x => x != null).Take(5)) {
sb.Append($" [LoaderException: {le.GetType().Name}: {le.Message}]");
}
break;
}
}
var frames = (ex.StackTrace ?? "")
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
.Take(4)
.Select(x => x.Trim());
var stack = string.Join(" | ", frames);
if (stack.Length > 0) sb.Append($" [stack: {stack}]");
return sb.ToString();
}
private static void AppendAssemblyDetail(StringBuilder sb, string fileName, string fusionLog) {
if (!string.IsNullOrEmpty(fileName)) sb.Append($" [assembly: {fileName}]");
if (string.IsNullOrEmpty(fusionLog)) return;
// Fusion ログは長いため,参照・リダイレクト・失敗理由に関する行だけを抜粋する
var keyLines = fusionLog
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
.Where(l => l.Contains("LOG: Post-policy reference") || l.Contains("LOG: Redirect")
|| l.Contains("WRN:") || l.Contains("ERR:") || l.Contains("Calling assembly"))
.Take(6)
.Select(l => l.Trim());
var joined = string.Join(" / ", keyLines);
if (joined.Length > 0) sb.Append($" [fusion: {joined}]");
}
}
}
#endif