Skip to content

Commit f98311a

Browse files
committed
feat: add Disassembler Availability report to header
At startup, probe all candidate IL disassemblers (dotnet-ildasm, ilspycmd) for availability and version, then display the results as a table in the Markdown, HTML, and audit log report headers. This makes it explicit which tools were available/unavailable during a run, improving the explainability of IL comparison confidence levels. - Add DisassemblerProbeResult record model - Add DisassemblerHelper.ProbeAllCandidates() probing logic - Add FileDiffResultLists.DisassemblerAvailability property - Add availability table to Markdown report header (HeaderSectionWriter) - Add availability table to HTML report header (AppendHeaderSection) - Add disassemblerAvailability array to audit log JSON - Add AuditLogDisassemblerAvailability model - Add 9 new tests across 4 test classes - Update doc/samples (diff_report.md, diff_report.html, audit_log.json) - Update DEVELOPER_GUIDE.md, TESTING_GUIDE.md (EN/JP bilingual) - Update CHANGELOG.md (EN/JP), README.md (EN/JP) https://claude.ai/code/session_01RN1uYPHDtxJL13wpqCeVwt
1 parent 5c3bb49 commit f98311a

21 files changed

Lines changed: 499 additions & 6 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
99

1010
### [Unreleased]
1111

12+
#### Added
13+
14+
- **Disassembler Availability report in header** — The report header now includes a "Disassembler Availability" table that lists every candidate IL disassembler tool (e.g. `dotnet-ildasm`, `ilspycmd`) and whether it was available or unavailable in the current environment, along with its version when available. This makes it easy to understand at a glance which tools influenced the comparison and the confidence level of IL-based results. At startup, `DisassemblerHelper.ProbeAllCandidates()` probes each candidate by running `--version` and stores the results in `FileDiffResultLists.DisassemblerAvailability` (new property). The table is rendered in Markdown (`diff_report.md`), HTML (`diff_report.html`), and as a `disassemblerAvailability` JSON array in the audit log (`audit_log.json`). New model: [`DisassemblerProbeResult`](Models/DisassemblerProbeResult.cs) (`ToolName`, `Available`, `Version`, `Path`). New audit model: [`AuditLogDisassemblerAvailability`](Models/AuditLogEntry.cs). Updated [`doc/samples/diff_report.md`](doc/samples/diff_report.md), [`doc/samples/diff_report.html`](doc/samples/diff_report.html), and [`doc/samples/audit_log.json`](doc/samples/audit_log.json). Added tests `GenerateDiffReport_HeaderShowsDisassemblerAvailabilityTable`, `GenerateDiffReport_HeaderOmitsAvailabilityTable_WhenProbeResultsAreNull` in [`ReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs), `GenerateDiffReportHtml_HeaderShowsDisassemblerAvailabilityTable`, `GenerateDiffReportHtml_HeaderOmitsAvailabilityTable_WhenProbeResultsAreNull` in [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs), `GenerateAuditLog_IncludesDisassemblerAvailability_WhenProbed`, `GenerateAuditLog_DisassemblerAvailabilityIsNull_WhenNotProbed` in [`AuditLogGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/AuditLogGenerateServiceTests.cs), and `ProbeAllCandidates_ReturnsNonEmptyList_WithUniqueToolNames`, `ProbeAllCandidates_IncludesExpectedToolNames`, `ProbeAllCandidates_AllResultsHaveNonEmptyToolName` in [`DisassemblerHelperTests`](FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs).
15+
1216
#### Changed
1317

1418
- **Interleave warning messages with detail tables in Warnings section** — Each warning bullet point is now immediately followed by its corresponding detail table, instead of listing all warning messages first and then all detail tables. When both SHA256Mismatch and Timestamp Regression warnings exist, the layout is: SHA256Mismatch warning → SHA256Mismatch detail table → Timestamp Regression warning → Timestamps Regressed detail table. This change applies to both Markdown and HTML reports. In the HTML report, each warning message is rendered in its own `<ul class="warnings">` element directly above its detail table. Updated [`doc/samples/diff_report.md`](doc/samples/diff_report.md) and [`doc/samples/diff_report.html`](doc/samples/diff_report.html). Added tests `GenerateDiffReport_Sha256MismatchDetailTable_AppearsImmediatelyAfterSha256Warning` in [`ReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs) and `GenerateDiffReportHtml_Sha256MismatchDetailTable_AppearsImmediatelyAfterSha256Warning` in [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs).
@@ -469,6 +473,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
469473

470474
### [Unreleased]
471475

476+
#### 追加
477+
478+
- **レポートヘッダに逆アセンブラ利用可否テーブルを追加** — レポートヘッダに「Disassembler Availability」テーブルを追加しました。すべての候補 IL 逆アセンブラツール(例: `dotnet-ildasm`、`ilspycmd`)について、現在の環境で利用可能か否か、および利用可能な場合はそのバージョンを一覧表示します。これにより、どのツールが比較に影響を与えたか、IL ベースの結果の信頼度を一目で把握できます。起動時に `DisassemblerHelper.ProbeAllCandidates()` が各候補を `--version` 実行でプローブし、結果を `FileDiffResultLists.DisassemblerAvailability`(新規プロパティ)に格納します。テーブルは Markdown(`diff_report.md`)、HTML(`diff_report.html`)、および監査ログ(`audit_log.json`)の `disassemblerAvailability` JSON 配列としてレンダリングされます。新規モデル: [`DisassemblerProbeResult`](Models/DisassemblerProbeResult.cs)(`ToolName`, `Available`, `Version`, `Path`)。新規監査モデル: [`AuditLogDisassemblerAvailability`](Models/AuditLogEntry.cs)。[`doc/samples/diff_report.md`](doc/samples/diff_report.md)、[`doc/samples/diff_report.html`](doc/samples/diff_report.html)、[`doc/samples/audit_log.json`](doc/samples/audit_log.json) を更新。テスト `GenerateDiffReport_HeaderShowsDisassemblerAvailabilityTable`、`GenerateDiffReport_HeaderOmitsAvailabilityTable_WhenProbeResultsAreNull` を [`ReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs) に、`GenerateDiffReportHtml_HeaderShowsDisassemblerAvailabilityTable`、`GenerateDiffReportHtml_HeaderOmitsAvailabilityTable_WhenProbeResultsAreNull` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に、`GenerateAuditLog_IncludesDisassemblerAvailability_WhenProbed`、`GenerateAuditLog_DisassemblerAvailabilityIsNull_WhenNotProbed` を [`AuditLogGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/AuditLogGenerateServiceTests.cs) に、`ProbeAllCandidates_ReturnsNonEmptyList_WithUniqueToolNames`、`ProbeAllCandidates_IncludesExpectedToolNames`、`ProbeAllCandidates_AllResultsHaveNonEmptyToolName` を [`DisassemblerHelperTests`](FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs) に追加。
479+
472480
#### 変更
473481

474482
- **警告セクションの警告メッセージと詳細テーブルのインターリーブ配置** — 各警告メッセージの直下に対応する詳細テーブルを配置するよう変更しました。従来はすべての警告メッセージを先に一括で列挙し、その後に詳細テーブルをまとめて出力していました。SHA256Mismatch 警告とタイムスタンプ逆行警告の両方がある場合、レイアウトは: SHA256Mismatch 警告 → SHA256Mismatch 詳細テーブル → タイムスタンプ逆行警告 → Timestamps Regressed 詳細テーブルとなります。Markdown と HTML の両レポートに適用。HTML レポートでは、各警告メッセージが独立した `<ul class="warnings">` 要素として詳細テーブルの直上にレンダリングされます。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) および [`doc/samples/diff_report.html`](doc/samples/diff_report.html) を更新。テスト `GenerateDiffReport_Sha256MismatchDetailTable_AppearsImmediatelyAfterSha256Warning` を [`ReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs) に、`GenerateDiffReportHtml_Sha256MismatchDetailTable_AppearsImmediatelyAfterSha256Warning` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に追加。

FolderDiffIL4DotNet.Tests/Services/AuditLogGenerateServiceTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,67 @@ public void Constructor_ThrowsOnNullLogger()
437437
Assert.Throws<ArgumentNullException>(() => new AuditLogGenerateService(_resultLists, null!));
438438
}
439439

440+
// ── Disassembler Availability / 逆アセンブラ利用可否 ───────────────────
441+
442+
/// <summary>
443+
/// Verifies that disassemblerAvailability is present in audit log when probe results exist.
444+
/// プローブ結果が存在する場合、監査ログに disassemblerAvailability が含まれることを確認する。
445+
/// </summary>
446+
[Fact]
447+
public void GenerateAuditLog_IncludesDisassemblerAvailability_WhenProbed()
448+
{
449+
var (oldDir, newDir, reportDir) = MakeDirs("disasm-avail");
450+
451+
_resultLists.DisassemblerAvailability = new List<DisassemblerProbeResult>
452+
{
453+
new("dotnet-ildasm", true, "0.12.2", "/usr/local/bin/dotnet-ildasm"),
454+
new("ilspycmd", false, null, null),
455+
};
456+
457+
_service.GenerateAuditLog(oldDir, newDir, reportDir,
458+
appVersion: "1.0.0", elapsedTimeString: "0h 0m 1.0s",
459+
computerName: "test-host", shouldGenerate: true);
460+
461+
var json = File.ReadAllText(Path.Combine(reportDir, AuditLogGenerateService.AUDIT_LOG_FILE_NAME));
462+
var doc = JsonDocument.Parse(json);
463+
var root = doc.RootElement;
464+
465+
Assert.True(root.TryGetProperty("disassemblerAvailability", out var availArr));
466+
Assert.Equal(JsonValueKind.Array, availArr.ValueKind);
467+
Assert.Equal(2, availArr.GetArrayLength());
468+
469+
var first = availArr[0];
470+
Assert.Equal("dotnet-ildasm", first.GetProperty("toolName").GetString());
471+
Assert.True(first.GetProperty("available").GetBoolean());
472+
Assert.Equal("0.12.2", first.GetProperty("version").GetString());
473+
474+
var second = availArr[1];
475+
Assert.Equal("ilspycmd", second.GetProperty("toolName").GetString());
476+
Assert.False(second.GetProperty("available").GetBoolean());
477+
Assert.Equal("", second.GetProperty("version").GetString());
478+
}
479+
480+
/// <summary>
481+
/// Verifies that disassemblerAvailability is null in audit log when no probing was done.
482+
/// プローブが実行されていない場合、disassemblerAvailability が null であることを確認する。
483+
/// </summary>
484+
[Fact]
485+
public void GenerateAuditLog_DisassemblerAvailabilityIsNull_WhenNotProbed()
486+
{
487+
var (oldDir, newDir, reportDir) = MakeDirs("no-probe");
488+
489+
_service.GenerateAuditLog(oldDir, newDir, reportDir,
490+
appVersion: "1.0.0", elapsedTimeString: "0h 0m 1.0s",
491+
computerName: "test-host", shouldGenerate: true);
492+
493+
var json = File.ReadAllText(Path.Combine(reportDir, AuditLogGenerateService.AUDIT_LOG_FILE_NAME));
494+
var doc = JsonDocument.Parse(json);
495+
var root = doc.RootElement;
496+
497+
Assert.True(root.TryGetProperty("disassemblerAvailability", out var availProp));
498+
Assert.Equal(JsonValueKind.Null, availProp.ValueKind);
499+
}
500+
440501
// ── Helpers / ヘルパー ────────────────────────────────────────────────────
441502

442503
private (string oldDir, string newDir, string reportDir) MakeDirs(string label)

FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,52 @@ public void EnumerateExecutableNames_OnWindows_CommandWithBatSuffix_DoesNotDupli
281281
var names = DisassemblerHelper.EnumerateExecutableNames("mytool.bat").ToList();
282282
Assert.Equal(1, names.Count(n => n.EndsWith(".bat", StringComparison.OrdinalIgnoreCase)));
283283
}
284+
// ── ProbeAllCandidates / 全候補プローブ ────────────────────────────────
285+
286+
/// <summary>
287+
/// Verifies that ProbeAllCandidates returns a non-empty list with unique tool names.
288+
/// ProbeAllCandidates が一意のツール名を持つ空でないリストを返すことを確認する。
289+
/// </summary>
290+
[Fact]
291+
public void ProbeAllCandidates_ReturnsNonEmptyList_WithUniqueToolNames()
292+
{
293+
var results = DisassemblerHelper.ProbeAllCandidates();
294+
295+
Assert.NotNull(results);
296+
Assert.NotEmpty(results);
297+
298+
var toolNames = results.Select(r => r.ToolName).ToList();
299+
Assert.Equal(toolNames.Count, toolNames.Distinct(StringComparer.OrdinalIgnoreCase).Count());
300+
}
301+
302+
/// <summary>
303+
/// Verifies that ProbeAllCandidates includes both dotnet-ildasm and ilspycmd candidates.
304+
/// ProbeAllCandidates が dotnet-ildasm と ilspycmd の両方を含むことを確認する。
305+
/// </summary>
306+
[Fact]
307+
public void ProbeAllCandidates_IncludesExpectedToolNames()
308+
{
309+
var results = DisassemblerHelper.ProbeAllCandidates();
310+
311+
var toolNames = results.Select(r => r.ToolName).ToList();
312+
Assert.Contains(FolderDiffIL4DotNet.Common.Constants.DOTNET_ILDASM, toolNames);
313+
Assert.Contains(FolderDiffIL4DotNet.Common.Constants.ILSPY_CMD, toolNames);
314+
}
315+
316+
/// <summary>
317+
/// Verifies that each ProbeAllCandidates result has a non-empty ToolName.
318+
/// 各プローブ結果が空でない ToolName を持つことを確認する。
319+
/// </summary>
320+
[Fact]
321+
public void ProbeAllCandidates_AllResultsHaveNonEmptyToolName()
322+
{
323+
var results = DisassemblerHelper.ProbeAllCandidates();
324+
325+
foreach (var result in results)
326+
{
327+
Assert.False(string.IsNullOrWhiteSpace(result.ToolName),
328+
$"ToolName should not be null or whitespace, got: '{result.ToolName}'");
329+
}
330+
}
284331
}
285332
}

FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,55 @@ public void LoadEmbeddedResource_InvalidResource_ThrowsFileNotFoundException()
14981498
HtmlReportGenerateService.LoadEmbeddedResource("NonExistent.Resource.Name"));
14991499
}
15001500

1501+
[Fact]
1502+
public void GenerateDiffReportHtml_HeaderShowsDisassemblerAvailabilityTable()
1503+
{
1504+
// Arrange: populate availability probe results
1505+
// 利用可否プローブ結果を設定
1506+
_resultLists.DisassemblerAvailability = new List<DisassemblerProbeResult>
1507+
{
1508+
new("dotnet-ildasm", true, "0.12.2", "/usr/local/bin/dotnet-ildasm"),
1509+
new("ilspycmd", false, null, null),
1510+
};
1511+
1512+
var (oldDir, newDir, reportDir) = MakeDirs("disasm-avail");
1513+
var config = CreateConfig();
1514+
1515+
_service.GenerateDiffReportHtml(oldDir, newDir, reportDir,
1516+
appVersion: "1.0", elapsedTimeString: "0h 0m 1.0s",
1517+
computerName: "test-host", config);
1518+
1519+
var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME));
1520+
1521+
// Assert: table structure and content present in HTML
1522+
// テーブルの構造と内容が HTML に含まれていることを検証
1523+
Assert.Contains("Disassembler Availability", html);
1524+
Assert.Contains("dotnet-ildasm", html);
1525+
Assert.Contains("ilspycmd", html);
1526+
Assert.Contains("color:#22863a", html); // green for Yes
1527+
Assert.Contains("color:#b31d28", html); // red for No
1528+
Assert.Contains("0.12.2", html);
1529+
}
1530+
1531+
[Fact]
1532+
public void GenerateDiffReportHtml_HeaderOmitsAvailabilityTable_WhenProbeResultsAreNull()
1533+
{
1534+
// Arrange: no probe results (default: null)
1535+
// プローブ結果なし(既定値: null)
1536+
var (oldDir, newDir, reportDir) = MakeDirs("no-probe-html");
1537+
var config = CreateConfig();
1538+
1539+
_service.GenerateDiffReportHtml(oldDir, newDir, reportDir,
1540+
appVersion: "1.0", elapsedTimeString: "0h 0m 1.0s",
1541+
computerName: "test-host", config);
1542+
1543+
var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME));
1544+
1545+
// Assert: no availability table when probe results are null
1546+
// プローブ結果が null の場合テーブルは出力されない
1547+
Assert.DoesNotContain("Disassembler Availability", html);
1548+
}
1549+
15011550
private static ConfigSettings CreateConfig(bool enableInlineDiff = true, bool lazyRender = false) => new()
15021551
{
15031552
IgnoredExtensions = new List<string>(),

FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,69 @@ public void GenerateDiffReport_HeaderShowsNotUsed_WhenNoDisassemblerWasObserved(
9494
Assert.Contains("- IL Disassembler: N/A", reportText);
9595
}
9696

97+
[Fact]
98+
public void GenerateDiffReport_HeaderShowsDisassemblerAvailabilityTable()
99+
{
100+
// Arrange: populate availability with one available and one unavailable tool
101+
// 1 つ利用可能、1 つ利用不可のツールで可用性を設定
102+
_resultLists.DisassemblerAvailability = new List<DisassemblerProbeResult>
103+
{
104+
new("dotnet-ildasm", true, "0.12.2", "/usr/local/bin/dotnet-ildasm"),
105+
new("ilspycmd", false, null, null),
106+
};
107+
108+
var oldDir = Path.Combine(_rootDir, "old-avail");
109+
var newDir = Path.Combine(_rootDir, "new-avail");
110+
var reportDir = Path.Combine(_rootDir, "report-avail");
111+
Directory.CreateDirectory(oldDir);
112+
Directory.CreateDirectory(newDir);
113+
Directory.CreateDirectory(reportDir);
114+
115+
var config = CreateConfig();
116+
_service.GenerateDiffReport(
117+
oldDir, newDir, reportDir,
118+
appVersion: "test",
119+
elapsedTimeString: "00:00:01.000",
120+
computerName: "test-host",
121+
config);
122+
123+
var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md"));
124+
125+
// Assert: availability table structure and content
126+
// テーブルの構造と内容を検証
127+
Assert.Contains("- Disassembler Availability:", reportText);
128+
Assert.Contains("| Tool | Available | Version |", reportText);
129+
Assert.Contains("| dotnet-ildasm | Yes | 0.12.2 |", reportText);
130+
Assert.Contains("| ilspycmd | No | N/A |", reportText);
131+
}
132+
133+
[Fact]
134+
public void GenerateDiffReport_HeaderOmitsAvailabilityTable_WhenProbeResultsAreNull()
135+
{
136+
// Arrange: no probe results (default: null)
137+
// プローブ結果なし(既定値: null)
138+
var oldDir = Path.Combine(_rootDir, "old-no-probe");
139+
var newDir = Path.Combine(_rootDir, "new-no-probe");
140+
var reportDir = Path.Combine(_rootDir, "report-no-probe");
141+
Directory.CreateDirectory(oldDir);
142+
Directory.CreateDirectory(newDir);
143+
Directory.CreateDirectory(reportDir);
144+
145+
var config = CreateConfig();
146+
_service.GenerateDiffReport(
147+
oldDir, newDir, reportDir,
148+
appVersion: "test",
149+
elapsedTimeString: "00:00:01.000",
150+
computerName: "test-host",
151+
config);
152+
153+
var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md"));
154+
155+
// Assert: no availability table when probe results are null
156+
// プローブ結果が null の場合、テーブルは出力されない
157+
Assert.DoesNotContain("Disassembler Availability", reportText);
158+
}
159+
97160
[Fact]
98161
public void GenerateDiffReport_HeaderShowsMvidReasonNote()
99162
{

0 commit comments

Comments
 (0)