Skip to content

Commit 54f6bc7

Browse files
committed
Add completion summary chart with file count distribution after successful run
Shows a mini horizontal bar chart (Unchanged/Added/Removed/Modified) with Unicode block characters and console colors before the "finished without errors" message. Extended DiffPipelineResult and RunCompletionState to propagate file counts from SummaryStatistics through the pipeline. https://claude.ai/code/session_01LEre156k3sMntSx8uGDXRj
1 parent 160cf25 commit 54f6bc7

5 files changed

Lines changed: 189 additions & 5 deletions

File tree

CHANGELOG.md

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

1414
- **`--banner` CLI option to display ASCII-art banner** — New CLI flag that prints the ASCII-art startup banner and exits with code `0`. Useful for verifying the tool is installed and accessible without running a full comparison. Modified: [`Runner/CliParser.cs`](Runner/CliParser.cs), [`Runner/CliOptions.cs`](Runner/CliOptions.cs), [`Runner/ProgramRunner.HelpText.cs`](Runner/ProgramRunner.HelpText.cs), [`ProgramRunner.cs`](ProgramRunner.cs), [`README.md`](README.md) (EN+JA options table, exit codes table). Tests: `ParseCliOptions_BannerFlag_SetsShowBanner` in [`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs), `RunAsync_BannerFlag_ExitsZeroWithBannerOutput` in [`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs). Test count: 1008 (+3).
1515

16+
- **Completion summary chart after successful run** — Displays a mini horizontal bar chart on the console after a successful diff run, showing the distribution of Unchanged, Added, Removed, and Modified files with counts and percentages. Each category uses a distinct console color (DarkGray, Green, Red, Yellow) and Unicode block characters (`█`/`░`). The chart appears just before the "finished without errors" message. Labels are left-aligned with bars starting at a consistent column. Skipped when total file count is zero. Modified: [`ProgramRunner.cs`](ProgramRunner.cs) (`OutputCompletionSummaryChart`, `OutputSummaryBar`), [`Runner/ProgramRunner.Types.cs`](Runner/ProgramRunner.Types.cs) (extended `RunCompletionState` and `ProgramRunResult` with count fields), [`Runner/DiffPipelineExecutor.cs`](Runner/DiffPipelineExecutor.cs) (extended `DiffPipelineResult` with count fields, propagated `SummaryStatistics`). Tests: `OutputCompletionSummaryChart_WritesBarForEachCategory`, `OutputCompletionSummaryChart_ZeroTotal_WritesNothing`, `OutputCompletionSummaryChart_BarsAreLeftAligned` in [`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs). Test count: 1011 (+3).
17+
1618
- **`--dry-run` CLI option for pre-execution preview** — New CLI flag that enumerates files in old/new folders and displays comparison statistics (file counts per folder, union count, .NET assembly candidates, ignored files, top file extensions, network path detection, parallelism settings) without running actual comparisons or generating reports. This allows users to verify scope and estimate execution time before committing to a potentially long-running diff on large folder trees or network shares. The Reports directory is not created during dry run. New files: [`Runner/DryRunExecutor.cs`](Runner/DryRunExecutor.cs). Modified: [`Runner/CliParser.cs`](Runner/CliParser.cs), [`Runner/CliOptions.cs`](Runner/CliOptions.cs), [`Runner/ProgramRunner.HelpText.cs`](Runner/ProgramRunner.HelpText.cs), [`ProgramRunner.cs`](ProgramRunner.cs), [`README.md`](README.md) (EN+JA options table). Tests: `ParseCliOptions_DryRunFlag_SetsDryRun` in [`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs), `RunAsync_HelpFlag_OutputContainsDryRunOption`, `RunAsync_DryRunFlag_ExitsZeroWithPreviewOutput`, `RunAsync_DryRunFlag_DoesNotCreateReportsDirectory` in [`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs). Test count: 998 (+4).
1719

1820
- **Strengthen disassembler warnings in reports** — Added two new warning banners to both Markdown and HTML report headers: (1) a prominent warning when no disassembler tool is available at startup, alerting users that .NET assembly comparison will fail; (2) a warning when multiple different disassembler tools (e.g., dotnet-ildasm and ilspycmd) were used in the same run, which could lead to false ILMismatch results due to format differences. Also corrected the SHA256Mismatch warning message from the misleading "not a .NET assembly or disassembler unavailable" to the accurate "not a .NET assembly and not a recognized text file" (since disassembler failure actually aborts the file comparison rather than falling through to binary-only). Modified: [`Services/SectionWriters/WarningsSectionWriter.cs`](Services/SectionWriters/WarningsSectionWriter.cs), [`Services/SectionWriters/HeaderSectionWriter.cs`](Services/SectionWriters/HeaderSectionWriter.cs), [`Services/ReportGenerateService.cs`](Services/ReportGenerateService.cs) (added `WriteDisassemblerWarnings`), [`Services/HtmlReport/HtmlReportGenerateService.Helpers.cs`](Services/HtmlReport/HtmlReportGenerateService.Helpers.cs) (added `AppendDisassemblerWarnings`), [`Services/HtmlReport/HtmlReportGenerateService.Sections.cs`](Services/HtmlReport/HtmlReportGenerateService.Sections.cs), [`Common/Constants.cs`](Common/Constants.cs), [`Services/HtmlReport/js/diff_report_excel.js`](Services/HtmlReport/js/diff_report_excel.js), [`doc/samples/diff_report.md`](doc/samples/diff_report.md), [`doc/samples/diff_report.html`](doc/samples/diff_report.html). Tests: `GenerateDiffReport_WarnsWhenNoDisassemblerAvailable`, `GenerateDiffReport_NoDisassemblerWarning_WhenOneIsAvailable`, `GenerateDiffReport_WarnsWhenMultipleDisassemblersUsed`, `GenerateDiffReport_NoMixedWarning_WhenSingleDisassemblerUsed` in [`ReportGenerateServiceTests.Header`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.Header.cs); `GenerateDiffReportHtml_WarnsWhenNoDisassemblerAvailable`, `GenerateDiffReportHtml_NoDisassemblerWarning_WhenOneAvailable`, `GenerateDiffReportHtml_WarnsWhenMultipleDisassemblersUsed` in [`HtmlReportGenerateServiceTests.Filtering`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.Filtering.cs). Test count: 1005 (+7).
@@ -778,6 +780,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
778780

779781
- **`--banner` CLI オプションで ASCII アートバナーを表示** — ASCII アートの起動バナーを表示してコード `0` で終了する新しい CLI フラグ。完全な比較を実行せずにツールがインストール済みでアクセス可能であることを確認するのに便利。変更: [`Runner/CliParser.cs`](Runner/CliParser.cs)[`Runner/CliOptions.cs`](Runner/CliOptions.cs)[`Runner/ProgramRunner.HelpText.cs`](Runner/ProgramRunner.HelpText.cs)[`ProgramRunner.cs`](ProgramRunner.cs)[`README.md`](README.md)(EN+JA オプション表・終了コード表)。テスト: `ParseCliOptions_BannerFlag_SetsShowBanner`[`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs))、`RunAsync_BannerFlag_ExitsZeroWithBannerOutput`[`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs))。テスト件数: 1008(+3)。
780782

783+
- **正常完了時の完了サマリーチャート** — 差分実行が正常完了した後、Unchanged・Added・Removed・Modified の各ファイル数と割合をミニ水平バーチャートでコンソールに表示。各カテゴリは固有のコンソール色(DarkGray、Green、Red、Yellow)と Unicode ブロック文字(`█`/`░`)を使用。チャートは「finished without errors」メッセージの直前に表示される。ラベルは左揃えでバーの開始位置を統一。合計ファイル数がゼロの場合はスキップ。変更: [`ProgramRunner.cs`](ProgramRunner.cs)(`OutputCompletionSummaryChart`、`OutputSummaryBar`)、[`Runner/ProgramRunner.Types.cs`](Runner/ProgramRunner.Types.cs)(`RunCompletionState` と `ProgramRunResult` にカウントフィールド追加)、[`Runner/DiffPipelineExecutor.cs`](Runner/DiffPipelineExecutor.cs)(`DiffPipelineResult` にカウントフィールド追加、`SummaryStatistics` 伝播)。テスト: `OutputCompletionSummaryChart_WritesBarForEachCategory`、`OutputCompletionSummaryChart_ZeroTotal_WritesNothing`、`OutputCompletionSummaryChart_BarsAreLeftAligned`([`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs))。テスト件数: 1011(+3)。
784+
781785
- **`--dry-run` CLI オプションによる実行前プレビュー** — 新しい CLI フラグ。old/new フォルダのファイルを列挙し、比較統計情報(フォルダ別ファイル数、和集合数、.NET アセンブリ候補数、無視ファイル数、拡張子別上位、ネットワークパス検出、並列度設定)を表示するのみで、実際の比較やレポート生成は行わない。大規模フォルダツリーやネットワーク共有上での長時間実行にコミットする前に、スコープの確認と実行時間の見積もりが可能になる。ドライラン時は Reports ディレクトリを作成しない。新規ファイル: [`Runner/DryRunExecutor.cs`](Runner/DryRunExecutor.cs)。変更: [`Runner/CliParser.cs`](Runner/CliParser.cs)、[`Runner/CliOptions.cs`](Runner/CliOptions.cs)、[`Runner/ProgramRunner.HelpText.cs`](Runner/ProgramRunner.HelpText.cs)、[`ProgramRunner.cs`](ProgramRunner.cs)、[`README.md`](README.md)(EN+JA オプション表)。テスト: `ParseCliOptions_DryRunFlag_SetsDryRun`([`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs))、`RunAsync_HelpFlag_OutputContainsDryRunOption`、`RunAsync_DryRunFlag_ExitsZeroWithPreviewOutput`、`RunAsync_DryRunFlag_DoesNotCreateReportsDirectory`([`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs))。テスト件数: 998(+4)。
782786

783787
- **レポートの逆アセンブラ警告を強化** — Markdown・HTML 両レポートのヘッダーに 2 種の新しい警告バナーを追加: (1) 起動時にどの逆アセンブラツールも利用不可の場合、.NET アセンブリ比較が失敗する旨の目立つ警告; (2) 同一実行で複数の異なる逆アセンブラツール(例: dotnet-ildasm と ilspycmd)が使用された場合、フォーマット差異により ILMismatch の誤検知が起こり得る旨の警告。また SHA256Mismatch の警告メッセージを修正 — 誤解を招く「not a .NET assembly or disassembler unavailable」を正確な「not a .NET assembly and not a recognized text file」に変更(逆アセンブラ失敗時は実際にはフォールスルーせず比較が中断されるため)。変更: [`Services/SectionWriters/WarningsSectionWriter.cs`](Services/SectionWriters/WarningsSectionWriter.cs)、[`Services/SectionWriters/HeaderSectionWriter.cs`](Services/SectionWriters/HeaderSectionWriter.cs)、[`Services/ReportGenerateService.cs`](Services/ReportGenerateService.cs)(`WriteDisassemblerWarnings` 追加)、[`Services/HtmlReport/HtmlReportGenerateService.Helpers.cs`](Services/HtmlReport/HtmlReportGenerateService.Helpers.cs)(`AppendDisassemblerWarnings` 追加)、[`Services/HtmlReport/HtmlReportGenerateService.Sections.cs`](Services/HtmlReport/HtmlReportGenerateService.Sections.cs)、[`Common/Constants.cs`](Common/Constants.cs)、[`Services/HtmlReport/js/diff_report_excel.js`](Services/HtmlReport/js/diff_report_excel.js)、[`doc/samples/diff_report.md`](doc/samples/diff_report.md)、[`doc/samples/diff_report.html`](doc/samples/diff_report.html)。テスト: Markdown 用 4 件([`ReportGenerateServiceTests.Header`](FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.Header.cs))、HTML 用 3 件([`HtmlReportGenerateServiceTests.Filtering`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.Filtering.cs))。テスト件数: 1005(+7)。

FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,119 @@ public async Task RunAsync_VersionFlag_ExitsZeroWithVersionOutput()
253253
}
254254
}
255255

256+
// -----------------------------------------------------------------------
257+
// Completion summary chart
258+
// 完了サマリーチャート
259+
// -----------------------------------------------------------------------
260+
261+
[Fact]
262+
public void OutputCompletionSummaryChart_WritesBarForEachCategory()
263+
{
264+
var stateType = typeof(ProgramRunner).GetNestedType("RunCompletionState", BindingFlags.NonPublic);
265+
Assert.NotNull(stateType);
266+
var state = Activator.CreateInstance(stateType, new object[] { false, false, 100, 20, 5, 30 });
267+
268+
var method = typeof(ProgramRunner).GetMethod("OutputCompletionSummaryChart", BindingFlags.NonPublic | BindingFlags.Static);
269+
Assert.NotNull(method);
270+
271+
var origOut = Console.Out;
272+
using var sw = new System.IO.StringWriter();
273+
Console.SetOut(sw);
274+
try
275+
{
276+
method.Invoke(null, new[] { state });
277+
var output = sw.ToString();
278+
279+
// Verify order: Unchanged, Added, Removed, Modified
280+
// 順序確認: Unchanged, Added, Removed, Modified
281+
int idxUnchanged = output.IndexOf("Unchanged", StringComparison.Ordinal);
282+
int idxAdded = output.IndexOf("Added", StringComparison.Ordinal);
283+
int idxRemoved = output.IndexOf("Removed", StringComparison.Ordinal);
284+
int idxModified = output.IndexOf("Modified", StringComparison.Ordinal);
285+
Assert.True(idxUnchanged < idxAdded, "Unchanged should appear before Added");
286+
Assert.True(idxAdded < idxRemoved, "Added should appear before Removed");
287+
Assert.True(idxRemoved < idxModified, "Removed should appear before Modified");
288+
289+
// Verify counts appear / 件数が出力されること
290+
Assert.Contains("100", output, StringComparison.Ordinal);
291+
Assert.Contains("20", output, StringComparison.Ordinal);
292+
Assert.Contains("5", output, StringComparison.Ordinal);
293+
Assert.Contains("30", output, StringComparison.Ordinal);
294+
295+
// Verify bar characters / バー文字が含まれること
296+
Assert.Contains("█", output, StringComparison.Ordinal);
297+
Assert.Contains("░", output, StringComparison.Ordinal);
298+
}
299+
finally
300+
{
301+
Console.SetOut(origOut);
302+
}
303+
}
304+
305+
[Fact]
306+
public void OutputCompletionSummaryChart_ZeroTotal_WritesNothing()
307+
{
308+
var stateType = typeof(ProgramRunner).GetNestedType("RunCompletionState", BindingFlags.NonPublic);
309+
Assert.NotNull(stateType);
310+
var state = Activator.CreateInstance(stateType, new object[] { false, false, 0, 0, 0, 0 });
311+
312+
var method = typeof(ProgramRunner).GetMethod("OutputCompletionSummaryChart", BindingFlags.NonPublic | BindingFlags.Static);
313+
Assert.NotNull(method);
314+
315+
var origOut = Console.Out;
316+
using var sw = new System.IO.StringWriter();
317+
Console.SetOut(sw);
318+
try
319+
{
320+
method.Invoke(null, new[] { state });
321+
Assert.Equal(string.Empty, sw.ToString());
322+
}
323+
finally
324+
{
325+
Console.SetOut(origOut);
326+
}
327+
}
328+
329+
[Fact]
330+
public void OutputCompletionSummaryChart_BarsAreLeftAligned()
331+
{
332+
var stateType = typeof(ProgramRunner).GetNestedType("RunCompletionState", BindingFlags.NonPublic);
333+
Assert.NotNull(stateType);
334+
var state = Activator.CreateInstance(stateType, new object[] { false, false, 50, 10, 5, 35 });
335+
336+
var method = typeof(ProgramRunner).GetMethod("OutputCompletionSummaryChart", BindingFlags.NonPublic | BindingFlags.Static);
337+
Assert.NotNull(method);
338+
339+
var origOut = Console.Out;
340+
using var sw = new System.IO.StringWriter();
341+
Console.SetOut(sw);
342+
try
343+
{
344+
method.Invoke(null, new[] { state });
345+
var lines = sw.ToString().Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
346+
347+
// All bar lines should start at the same column (bar character position)
348+
// すべてのバー行のバー文字開始位置が揃っていること
349+
int? firstBarPos = null;
350+
foreach (var line in lines)
351+
{
352+
int barPos = line.IndexOf('█');
353+
if (barPos < 0) barPos = line.IndexOf('░');
354+
if (barPos < 0) continue;
355+
356+
if (firstBarPos == null)
357+
firstBarPos = barPos;
358+
else
359+
Assert.Equal(firstBarPos.Value, barPos);
360+
}
361+
Assert.NotNull(firstBarPos);
362+
}
363+
finally
364+
{
365+
Console.SetOut(origOut);
366+
}
367+
}
368+
256369
// -----------------------------------------------------------------------
257370
// --banner early-exit test
258371
// --banner 早期終了テスト

ProgramRunner.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ private async Task<ProgramRunResult> RunWithResultAsync(string[] args, CliOption
144144
return completionStateResult.Failure!;
145145
}
146146

147+
var completionState = completionStateResult.Value!;
148+
OutputCompletionSummaryChart(completionState);
147149
_logger.LogMessage(AppLogLevel.Info, LOG_APP_FINISHED, shouldOutputMessageToConsole: true, ConsoleColor.Green);
148-
return ProgramRunResult.Success(completionStateResult.Value!);
150+
return ProgramRunResult.Success(completionState);
149151
}
150152
catch (Exception ex)
151153
{
@@ -165,6 +167,40 @@ private string InitializeLoggerAndGetAppVersion()
165167
return appVersion;
166168
}
167169

170+
private static void OutputCompletionSummaryChart(RunCompletionState state)
171+
{
172+
int total = state.UnchangedCount + state.AddedCount + state.RemovedCount + state.ModifiedCount;
173+
if (total == 0)
174+
{
175+
return;
176+
}
177+
178+
Console.WriteLine();
179+
OutputSummaryBar("Unchanged", state.UnchangedCount, total, ConsoleColor.DarkGray);
180+
OutputSummaryBar("Added", state.AddedCount, total, ConsoleColor.Green);
181+
OutputSummaryBar("Removed", state.RemovedCount, total, ConsoleColor.Red);
182+
OutputSummaryBar("Modified", state.ModifiedCount, total, ConsoleColor.Yellow);
183+
Console.WriteLine();
184+
}
185+
186+
private static void OutputSummaryBar(string label, int count, int total, ConsoleColor color)
187+
{
188+
const int BAR_WIDTH = 30;
189+
const int LABEL_WIDTH = 10;
190+
int filled = (int)Math.Round((double)count / total * BAR_WIDTH);
191+
if (filled > BAR_WIDTH) filled = BAR_WIDTH;
192+
193+
var bar = new string('█', filled) + new string('░', BAR_WIDTH - filled);
194+
var pct = (100.0 * count / total).ToString("F1");
195+
196+
Console.Write($" {label.PadRight(LABEL_WIDTH)} ");
197+
var prevColor = Console.ForegroundColor;
198+
Console.ForegroundColor = color;
199+
Console.Write(bar);
200+
Console.ForegroundColor = prevColor;
201+
Console.WriteLine($" {count,5} ({pct}%)");
202+
}
203+
168204
private void OutputCompletionWarnings(bool hasSha256MismatchWarnings, bool hasTimestampRegressionWarnings)
169205
{
170206
if (hasSha256MismatchWarnings)
@@ -254,7 +290,11 @@ private async Task<StepResult<RunCompletionState>> TryExecuteRunAsync(
254290
computerName);
255291
var completionState = new RunCompletionState(
256292
pipelineResult.HasSha256MismatchWarnings,
257-
pipelineResult.HasTimestampRegressionWarnings);
293+
pipelineResult.HasTimestampRegressionWarnings,
294+
pipelineResult.UnchangedCount,
295+
pipelineResult.AddedCount,
296+
pipelineResult.RemovedCount,
297+
pipelineResult.ModifiedCount);
258298
return StepResult<RunCompletionState>.FromValue(completionState);
259299
}
260300
catch (Exception ex) when (ex is ArgumentException or DirectoryNotFoundException

0 commit comments

Comments
 (0)