Skip to content

Commit e31b81d

Browse files
committed
feat: CLIオプション拡充 (--help, --version, --config, --threads, --no-il-cache, --skip-il, --no-timestamp-warnings)
新規オプション: - --help/-h: 使い方を表示してコード 0 で終了(ロガー初期化なし) - --version: アプリバージョンを表示してコード 0 で終了(ロガー初期化なし) - --config <path>: 任意パスから config.json を読み込む - --threads <N>: MaxParallelism を上書き(0 = 自動) - --no-il-cache: 今回の実行に限り IL キャッシュを無効化 - --skip-il: .NET アセンブリの IL 比較をまるごとスキップ - --no-timestamp-warnings: タイムスタンプ逆転警告を抑制 未知フラグは ParseError にキャプチャされ TryValidateAndBuildRunArguments で 終了コード 2 として検出されるようになりました(旧来の黙無視を修正)。 変更ファイル: - ConfigSettings: SkipIL プロパティを追加(デフォルト false) - ConfigService: LoadConfigAsync に省略可能な configFilePath 引数を追加 - FileDiffService: PrecomputeAsync・FilesAreEqualAsync で SkipIL を確認 - ProgramRunner: ParseCliOptions / ApplyCliOverrides / CliOptions record を追加。 --help/--version を RunAsync 冒頭で早期終了。PromptForExitKeyIfNeeded を CliOptions ベースに変更 - Properties/AssemblyInfo.cs: InternalsVisibleTo("FolderDiffIL4DotNet.Tests") 追加 - CliOptionsTests.cs: ParseCliOptions の単体テスト 21 件追加 - ProgramRunnerTests.cs: --help/--version/--unknown-flag/--config/--threads の統合テストを追加 - ConfigServiceTests.cs: configFilePath パラメータの統合テスト 4 件追加 - ConfigSettingsTests.cs: SkipIL のデフォルト値・JSON デシリアライズ確認を追加 - CHANGELOG.md / README.md / doc/DEVELOPER_GUIDE.md: 日英ドキュメント更新 https://claude.ai/code/session_0174R4FzjudFBdynScKbhX6m
1 parent 57715ed commit e31b81d

12 files changed

Lines changed: 929 additions & 58 deletions

File tree

CHANGELOG.md

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

1212
#### Added
1313

14+
- Expanded CLI options: `--help`/`-h` prints usage and exits with code `0` before any logger initialization; `--version` prints the application version and exits with code `0`; `--config <path>` loads a config file from an arbitrary path instead of the default `<exe>/config.json`; `--threads <N>` overrides `MaxParallelism` in [`ConfigSettings`](Models/ConfigSettings.cs) for the current run; `--no-il-cache` forces `EnableILCache = false` for the current run; `--skip-il` skips IL decompilation and IL diff entirely for .NET assemblies (new `SkipIL` property in [`ConfigSettings`](Models/ConfigSettings.cs), also respected by [`FileDiffService`](Services/FileDiffService.cs)); `--no-timestamp-warnings` suppresses timestamp-regression warnings. Unknown flags now produce exit code `2` with a descriptive message instead of silently being ignored. [`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) now accepts an optional `configFilePath` parameter. Added [`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs) with 21 parser unit-test cases, and new integration tests in [`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs) and [`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs).
1415
- Added [`ConfigSettings.Validate()`](Models/ConfigSettings.cs) and the companion `ConfigValidationResult` class; [`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) now calls `Validate()` immediately after deserialization and throws [`InvalidDataException`](https://learn.microsoft.com/en-us/dotnet/api/system.io.invaliddataexception?view=net-8.0) listing all invalid settings when validation fails, so misconfigured runs are caught at startup with a clear error message instead of failing silently or causing undefined behavior later. Validated constraints: `MaxLogGenerations >= 1`; `TextDiffParallelThresholdKilobytes >= 1`; `TextDiffChunkSizeKilobytes >= 1`; and `TextDiffChunkSizeKilobytes < TextDiffParallelThresholdKilobytes`. Added validation unit tests to [`ConfigSettingsTests`](FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs) (7 cases) and validation integration tests to [`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs) (5 cases).
1516

1617
#### Fixed
@@ -248,6 +249,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
248249

249250
#### 追加
250251

252+
- CLI オプションを拡充しました。`--help`/`-h` は使い方を表示してロガー初期化前にコード `0` で終了します。`--version` はアプリバージョンを表示してコード `0` で終了します。`--config <path>` はデフォルトの `<exe>/config.json` に代わり任意のパスから設定ファイルを読み込みます。`--threads <N>` は今回の実行に限り [`ConfigSettings`](Models/ConfigSettings.cs) の `MaxParallelism` を上書きします。`--no-il-cache` は今回の実行に限り `EnableILCache = false` に設定します。`--skip-il` は .NET アセンブリの IL 逆アセンブルと IL 差分比較をまるごとスキップします([`ConfigSettings`](Models/ConfigSettings.cs) に新設した `SkipIL` プロパティとして保持され、[`FileDiffService`](Services/FileDiffService.cs) でも参照します)。`--no-timestamp-warnings` はタイムスタンプ逆転の警告を抑制します。未知のフラグを指定した場合は、これまで黙ってスルーされていた挙動を改め、説明付きで終了コード `2` を返します。[`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) にオプショナルな `configFilePath` パラメータを追加しました。[`CliOptionsTests`](FolderDiffIL4DotNet.Tests/CliOptionsTests.cs) にパーサー単体テスト 21 件を追加し、[`ProgramRunnerTests`](FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs) と [`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs) にも統合テストを追加しました。
251253
- [`ConfigSettings.Validate()`](Models/ConfigSettings.cs) と `ConfigValidationResult` クラスを追加しました。[`ConfigService.LoadConfigAsync()`](Services/ConfigService.cs) はデシリアライズ直後に `Validate()` を呼び出し、バリデーションが失敗した場合は全エラーを列挙した [`InvalidDataException`](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.invaliddataexception?view=net-8.0) をスローします。これにより、設定不正な実行は後から無言で失敗したり未定義の振る舞いを引き起こしたりする代わりに、起動時に分かりやすいエラーメッセージとして検出されます。検証対象の制約: `MaxLogGenerations >= 1`、`TextDiffParallelThresholdKilobytes >= 1`、`TextDiffChunkSizeKilobytes >= 1`、`TextDiffChunkSizeKilobytes < TextDiffParallelThresholdKilobytes`。あわせて [`ConfigSettingsTests`](FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs) にバリデーション単体テスト(7 件)、[`ConfigServiceTests`](FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs) にバリデーション統合テスト(5 件)を追加しました。
252254

253255
#### 修正
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
using Xunit;
2+
3+
namespace FolderDiffIL4DotNet.Tests
4+
{
5+
public sealed class CliOptionsTests
6+
{
7+
// -----------------------------------------------------------------------
8+
// ParseCliOptions – positional-only (no flags)
9+
// -----------------------------------------------------------------------
10+
11+
[Fact]
12+
public void ParseCliOptions_NullArgs_ReturnsAllDefaults()
13+
{
14+
var opts = ProgramRunner.ParseCliOptions(null);
15+
16+
Assert.False(opts.ShowHelp);
17+
Assert.False(opts.ShowVersion);
18+
Assert.False(opts.NoPause);
19+
Assert.Null(opts.ConfigPath);
20+
Assert.Null(opts.ThreadsOverride);
21+
Assert.False(opts.NoIlCache);
22+
Assert.False(opts.SkipIL);
23+
Assert.False(opts.NoTimestampWarnings);
24+
Assert.Null(opts.ParseError);
25+
}
26+
27+
[Fact]
28+
public void ParseCliOptions_EmptyArgs_ReturnsAllDefaults()
29+
{
30+
var opts = ProgramRunner.ParseCliOptions(System.Array.Empty<string>());
31+
32+
Assert.False(opts.ShowHelp);
33+
Assert.Null(opts.ParseError);
34+
}
35+
36+
[Fact]
37+
public void ParseCliOptions_PositionalArgsOnly_ReturnsAllDefaultFlags()
38+
{
39+
var opts = ProgramRunner.ParseCliOptions(new[] { "/old", "/new", "label" });
40+
41+
Assert.False(opts.ShowHelp);
42+
Assert.False(opts.ShowVersion);
43+
Assert.False(opts.NoPause);
44+
Assert.Null(opts.ConfigPath);
45+
Assert.Null(opts.ThreadsOverride);
46+
Assert.False(opts.NoIlCache);
47+
Assert.False(opts.SkipIL);
48+
Assert.False(opts.NoTimestampWarnings);
49+
Assert.Null(opts.ParseError);
50+
}
51+
52+
// -----------------------------------------------------------------------
53+
// --help / -h
54+
// -----------------------------------------------------------------------
55+
56+
[Theory]
57+
[InlineData("--help")]
58+
[InlineData("-h")]
59+
[InlineData("--HELP")]
60+
[InlineData("-H")]
61+
public void ParseCliOptions_HelpFlag_SetsShowHelp(string helpArg)
62+
{
63+
var opts = ProgramRunner.ParseCliOptions(new[] { helpArg });
64+
65+
Assert.True(opts.ShowHelp);
66+
Assert.Null(opts.ParseError);
67+
}
68+
69+
// -----------------------------------------------------------------------
70+
// --version
71+
// -----------------------------------------------------------------------
72+
73+
[Theory]
74+
[InlineData("--version")]
75+
[InlineData("--VERSION")]
76+
public void ParseCliOptions_VersionFlag_SetsShowVersion(string arg)
77+
{
78+
var opts = ProgramRunner.ParseCliOptions(new[] { arg });
79+
80+
Assert.True(opts.ShowVersion);
81+
Assert.Null(opts.ParseError);
82+
}
83+
84+
// -----------------------------------------------------------------------
85+
// --no-pause
86+
// -----------------------------------------------------------------------
87+
88+
[Fact]
89+
public void ParseCliOptions_NoPauseFlag_SetsNoPause()
90+
{
91+
var opts = ProgramRunner.ParseCliOptions(new[] { "--no-pause" });
92+
93+
Assert.True(opts.NoPause);
94+
Assert.Null(opts.ParseError);
95+
}
96+
97+
// -----------------------------------------------------------------------
98+
// --config <path>
99+
// -----------------------------------------------------------------------
100+
101+
[Fact]
102+
public void ParseCliOptions_ConfigWithPath_SetsConfigPath()
103+
{
104+
var opts = ProgramRunner.ParseCliOptions(new[] { "/old", "/new", "lbl", "--config", "/tmp/my-config.json" });
105+
106+
Assert.Equal("/tmp/my-config.json", opts.ConfigPath);
107+
Assert.Null(opts.ParseError);
108+
}
109+
110+
[Fact]
111+
public void ParseCliOptions_ConfigWithoutPath_SetsParseError()
112+
{
113+
var opts = ProgramRunner.ParseCliOptions(new[] { "--config" });
114+
115+
Assert.NotNull(opts.ParseError);
116+
Assert.Contains("--config", opts.ParseError, System.StringComparison.OrdinalIgnoreCase);
117+
}
118+
119+
[Fact]
120+
public void ParseCliOptions_ConfigFollowedByAnotherFlag_SetsParseError()
121+
{
122+
var opts = ProgramRunner.ParseCliOptions(new[] { "--config", "--no-pause" });
123+
124+
Assert.NotNull(opts.ParseError);
125+
}
126+
127+
// -----------------------------------------------------------------------
128+
// --threads <N>
129+
// -----------------------------------------------------------------------
130+
131+
[Theory]
132+
[InlineData("0", 0)]
133+
[InlineData("1", 1)]
134+
[InlineData("8", 8)]
135+
[InlineData("64", 64)]
136+
public void ParseCliOptions_ThreadsWithValidValue_SetsThreadsOverride(string value, int expected)
137+
{
138+
var opts = ProgramRunner.ParseCliOptions(new[] { "--threads", value });
139+
140+
Assert.Equal(expected, opts.ThreadsOverride);
141+
Assert.Null(opts.ParseError);
142+
}
143+
144+
[Theory]
145+
[InlineData("-1")]
146+
[InlineData("abc")]
147+
[InlineData("3.5")]
148+
public void ParseCliOptions_ThreadsWithInvalidValue_SetsParseError(string value)
149+
{
150+
var opts = ProgramRunner.ParseCliOptions(new[] { "--threads", value });
151+
152+
Assert.NotNull(opts.ParseError);
153+
Assert.Contains("--threads", opts.ParseError, System.StringComparison.OrdinalIgnoreCase);
154+
}
155+
156+
[Fact]
157+
public void ParseCliOptions_ThreadsWithoutValue_SetsParseError()
158+
{
159+
var opts = ProgramRunner.ParseCliOptions(new[] { "--threads" });
160+
161+
Assert.NotNull(opts.ParseError);
162+
}
163+
164+
// -----------------------------------------------------------------------
165+
// --no-il-cache
166+
// -----------------------------------------------------------------------
167+
168+
[Fact]
169+
public void ParseCliOptions_NoIlCacheFlag_SetsNoIlCache()
170+
{
171+
var opts = ProgramRunner.ParseCliOptions(new[] { "--no-il-cache" });
172+
173+
Assert.True(opts.NoIlCache);
174+
Assert.Null(opts.ParseError);
175+
}
176+
177+
// -----------------------------------------------------------------------
178+
// --skip-il
179+
// -----------------------------------------------------------------------
180+
181+
[Fact]
182+
public void ParseCliOptions_SkipILFlag_SetsSkipIL()
183+
{
184+
var opts = ProgramRunner.ParseCliOptions(new[] { "--skip-il" });
185+
186+
Assert.True(opts.SkipIL);
187+
Assert.Null(opts.ParseError);
188+
}
189+
190+
// -----------------------------------------------------------------------
191+
// --no-timestamp-warnings
192+
// -----------------------------------------------------------------------
193+
194+
[Fact]
195+
public void ParseCliOptions_NoTimestampWarningsFlag_SetsNoTimestampWarnings()
196+
{
197+
var opts = ProgramRunner.ParseCliOptions(new[] { "--no-timestamp-warnings" });
198+
199+
Assert.True(opts.NoTimestampWarnings);
200+
Assert.Null(opts.ParseError);
201+
}
202+
203+
// -----------------------------------------------------------------------
204+
// Unknown / invalid flags
205+
// -----------------------------------------------------------------------
206+
207+
[Theory]
208+
[InlineData("--unknown-flag")]
209+
[InlineData("--xyz")]
210+
public void ParseCliOptions_UnknownFlag_SetsParseError(string flag)
211+
{
212+
var opts = ProgramRunner.ParseCliOptions(new[] { flag });
213+
214+
Assert.NotNull(opts.ParseError);
215+
Assert.Contains(flag, opts.ParseError, System.StringComparison.OrdinalIgnoreCase);
216+
}
217+
218+
[Fact]
219+
public void ParseCliOptions_UnknownFlagAfterKnownFlag_ParseErrorCapturesFirst()
220+
{
221+
var opts = ProgramRunner.ParseCliOptions(new[] { "--no-pause", "--bogus", "--also-bogus" });
222+
223+
Assert.True(opts.NoPause);
224+
Assert.NotNull(opts.ParseError);
225+
Assert.Contains("--bogus", opts.ParseError, System.StringComparison.OrdinalIgnoreCase);
226+
// Second error is NOT overwritten (parseError uses ??=)
227+
Assert.DoesNotContain("--also-bogus", opts.ParseError, System.StringComparison.OrdinalIgnoreCase);
228+
}
229+
230+
// -----------------------------------------------------------------------
231+
// Combined flags
232+
// -----------------------------------------------------------------------
233+
234+
[Fact]
235+
public void ParseCliOptions_AllFlagsCombined_ParsedCorrectly()
236+
{
237+
var opts = ProgramRunner.ParseCliOptions(new[]
238+
{
239+
"/old", "/new", "label",
240+
"--no-pause",
241+
"--config", "/etc/my.json",
242+
"--threads", "4",
243+
"--no-il-cache",
244+
"--skip-il",
245+
"--no-timestamp-warnings"
246+
});
247+
248+
Assert.False(opts.ShowHelp);
249+
Assert.False(opts.ShowVersion);
250+
Assert.True(opts.NoPause);
251+
Assert.Equal("/etc/my.json", opts.ConfigPath);
252+
Assert.Equal(4, opts.ThreadsOverride);
253+
Assert.True(opts.NoIlCache);
254+
Assert.True(opts.SkipIL);
255+
Assert.True(opts.NoTimestampWarnings);
256+
Assert.Null(opts.ParseError);
257+
}
258+
}
259+
}

FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public void JsonDeserialize_ExplicitOverrides_AreApplied()
6666
"ILCacheMaxDiskMegabytes": 20,
6767
"ILPrecomputeBatchSize": 512,
6868
"OptimizeForNetworkShares": true,
69-
"AutoDetectNetworkShares": false
69+
"AutoDetectNetworkShares": false,
70+
"SkipIL": true
7071
}
7172
""";
7273

@@ -95,6 +96,7 @@ public void JsonDeserialize_ExplicitOverrides_AreApplied()
9596
Assert.Equal(512, config.ILPrecomputeBatchSize);
9697
Assert.True(config.OptimizeForNetworkShares);
9798
Assert.False(config.AutoDetectNetworkShares);
99+
Assert.True(config.SkipIL);
98100
}
99101

100102
[Fact]
@@ -248,6 +250,7 @@ private static void AssertMatchesDefaults(ConfigSettings config)
248250
Assert.Equal(2048, config.ILPrecomputeBatchSize);
249251
Assert.False(config.OptimizeForNetworkShares);
250252
Assert.True(config.AutoDetectNetworkShares);
253+
Assert.False(config.SkipIL);
251254
}
252255
}
253256
}

0 commit comments

Comments
 (0)