Skip to content

Commit f9eeeed

Browse files
committed
FolderDiffServiceの探索・並列度決定ポリシーを分離する
1 parent 788bcf8 commit f9eeeed

9 files changed

Lines changed: 346 additions & 88 deletions

File tree

CHANGELOG.md

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

1212
#### Changed
1313

14+
- Refactored discovery filtering and auto-parallelism policy out of [`FolderDiffService`](Services/FolderDiffService.cs) into [`FolderDiffExecutionStrategy`](Services/FolderDiffExecutionStrategy.cs), reducing orchestration sprawl while keeping runtime behavior stable.
15+
- Added focused unit coverage in [`FolderDiffExecutionStrategyTests`](FolderDiffIL4DotNet.Tests/Services/FolderDiffExecutionStrategyTests.cs) for ignored-file filtering, relative-path union counting, and network-aware auto-parallelism, and updated the [README](README.md), [developer guide](doc/DEVELOPER_GUIDE.md), and [testing guide](doc/TESTING_GUIDE.md) to reflect the new boundary and latest passing test count (`227`).
1416
- Refactored [`ILCache`](Services/Caching/ILCache.cs) into a thinner coordinator backed by [`ILMemoryCache`](Services/Caching/ILMemoryCache.cs) and [`ILDiskCache`](Services/Caching/ILDiskCache.cs), keeping the public API stable while separating in-memory retention from disk persistence/quota handling.
1517
- Added regression coverage in [`ILCacheTests`](FolderDiffIL4DotNet.Tests/Services/Caching/ILCacheTests.cs) for same-key updates at memory-capacity limits and for coordinated disk cleanup when LRU eviction removes an entry.
16-
- Updated the [developer guide](doc/DEVELOPER_GUIDE.md) and [testing guide](doc/TESTING_GUIDE.md) to describe the split cache internals and reflect the latest passing test count (`223`).
18+
- Updated the [developer guide](doc/DEVELOPER_GUIDE.md) and [testing guide](doc/TESTING_GUIDE.md) to describe the split cache internals and reflect the latest passing test count (`227`).
1719
- Replaced eager `Directory.GetFiles(...)` usage in [`FolderDiffService`](Services/FolderDiffService.cs) with lazy `Directory.EnumerateFiles(...)` behind [`IFileSystemService`](Services/IFileSystemService.cs), reducing discovery-side allocations for large trees and network shares while keeping folder-diff behavior unchanged.
1820
- Added unit-test coverage for streaming file discovery in [`FolderDiffServiceUnitTests`](FolderDiffIL4DotNet.Tests/Services/FolderDiffServiceUnitTests.cs) and updated the [README](README.md), [developer guide](doc/DEVELOPER_GUIDE.md), and [testing guide](doc/TESTING_GUIDE.md) accordingly.
1921
- Fixed missing link to [Developer Guide](doc/DEVELOPER_GUIDE.md).
@@ -226,9 +228,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
226228

227229
#### 変更
228230

231+
- [`FolderDiffService`](Services/FolderDiffService.cs) に埋め込まれていた列挙フィルタと自動並列度決定を [`FolderDiffExecutionStrategy`](Services/FolderDiffExecutionStrategy.cs) へ抽出し、実行時挙動を変えずにオーケストレーション責務を整理しました。
232+
- [`FolderDiffExecutionStrategyTests`](FolderDiffIL4DotNet.Tests/Services/FolderDiffExecutionStrategyTests.cs) を追加し、無視ファイルの扱い、相対パス和集合件数、自動並列度のネットワーク考慮を回帰テスト化しました。あわせて [README](README.md)[開発者ガイド](doc/DEVELOPER_GUIDE.md)[テストガイド](doc/TESTING_GUIDE.md) を更新し、最新の通過テスト件数(`227` 件)を反映しました。
229233
- [`ILCache`](Services/Caching/ILCache.cs) を、公開 API を維持したまま [`ILMemoryCache`](Services/Caching/ILMemoryCache.cs)[`ILDiskCache`](Services/Caching/ILDiskCache.cs) を使う薄い調停役へ整理し、メモリ保持とディスク永続化/クォータ制御の責務を分離しました。
230234
- [`ILCacheTests`](FolderDiffIL4DotNet.Tests/Services/Caching/ILCacheTests.cs) に、メモリ上限到達時の同一キー再保存と、LRU 退避時のディスクキャッシュ連動削除に対する回帰テストを追加しました。
231-
- [開発者ガイド](doc/DEVELOPER_GUIDE.md)[テストガイド](doc/TESTING_GUIDE.md) を更新し、キャッシュ内部の分離方針と最新の通過テスト件数(`223` 件)を反映しました。
235+
- [開発者ガイド](doc/DEVELOPER_GUIDE.md)[テストガイド](doc/TESTING_GUIDE.md) を更新し、キャッシュ内部の分離方針と最新の通過テスト件数(`227` 件)を反映しました。
232236
- [`FolderDiffService`](Services/FolderDiffService.cs) 内で使っていた即時配列化の `Directory.GetFiles(...)` 相当を、[`IFileSystemService`](Services/IFileSystemService.cs) 越しの遅延列挙 `Directory.EnumerateFiles(...)` へ置き換えました。これにより、大量ファイルやネットワーク共有上の列挙で不要な配列確保を減らしつつ、フォルダ差分の振る舞いは維持しています。
233237
- [`FolderDiffServiceUnitTests`](FolderDiffIL4DotNet.Tests/Services/FolderDiffServiceUnitTests.cs) にストリーミング列挙のテストを追加し、あわせて [README](README.md)[開発者ガイド](doc/DEVELOPER_GUIDE.md)[テストガイド](doc/TESTING_GUIDE.md) を更新しました。
234238
- [開発者ガイド](doc/DEVELOPER_GUIDE.md)のリンク付与漏れを修正しました。
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using FolderDiffIL4DotNet.Models;
5+
using FolderDiffIL4DotNet.Services;
6+
using Xunit;
7+
8+
namespace FolderDiffIL4DotNet.Tests.Services
9+
{
10+
[Trait("Category", "Unit")]
11+
public sealed class FolderDiffExecutionStrategyTests
12+
{
13+
[Fact]
14+
public void EnumerateIncludedFiles_FiltersIgnoredFiles_AndRecordsIgnoredLocations()
15+
{
16+
const string oldDir = "/virtual/old";
17+
const string newDir = "/virtual/new";
18+
const string reportDir = "/virtual/report";
19+
20+
var fileSystem = new FakeFileSystemService();
21+
fileSystem.SetFiles(oldDir,
22+
Path.Combine(oldDir, "keep.txt"),
23+
Path.Combine(oldDir, "ignored.pdb"));
24+
fileSystem.SetFiles(newDir,
25+
Path.Combine(newDir, "keep.txt"),
26+
Path.Combine(newDir, "ignored.pdb"));
27+
28+
var resultLists = new FileDiffResultLists();
29+
var strategy = CreateStrategy(
30+
CreateConfig(),
31+
CreateExecutionContext(oldDir, newDir, reportDir),
32+
resultLists,
33+
fileSystem);
34+
35+
var oldIncludedFiles = strategy.EnumerateIncludedFiles(oldDir, FileDiffResultLists.IgnoredFileLocation.Old);
36+
var newIncludedFiles = strategy.EnumerateIncludedFiles(newDir, FileDiffResultLists.IgnoredFileLocation.New);
37+
38+
Assert.Equal(new[] { Path.Combine(oldDir, "keep.txt") }, oldIncludedFiles);
39+
Assert.Equal(new[] { Path.Combine(newDir, "keep.txt") }, newIncludedFiles);
40+
Assert.Equal(
41+
FileDiffResultLists.IgnoredFileLocation.Old | FileDiffResultLists.IgnoredFileLocation.New,
42+
resultLists.IgnoredFilesRelativePathToLocation["ignored.pdb"]);
43+
}
44+
45+
[Fact]
46+
public void ComputeUnionFileCount_UsesRelativePathUnionAcrossRoots()
47+
{
48+
const string oldDir = "/virtual/old";
49+
const string newDir = "/virtual/new";
50+
const string reportDir = "/virtual/report";
51+
52+
var strategy = CreateStrategy(
53+
CreateConfig(),
54+
CreateExecutionContext(oldDir, newDir, reportDir),
55+
new FileDiffResultLists(),
56+
new FakeFileSystemService());
57+
58+
var count = strategy.ComputeUnionFileCount(
59+
new[]
60+
{
61+
Path.Combine(oldDir, "shared.txt"),
62+
Path.Combine(oldDir, "old-only.txt")
63+
},
64+
new[]
65+
{
66+
Path.Combine(newDir, "shared.txt"),
67+
Path.Combine(newDir, "new-only.txt")
68+
});
69+
70+
Assert.Equal(3, count);
71+
}
72+
73+
[Fact]
74+
public void DetermineMaxParallel_WhenAutoAndNetworkOptimized_CapsByNetworkLimit()
75+
{
76+
var strategy = CreateStrategy(
77+
CreateConfig(maxParallelism: 0),
78+
CreateExecutionContext("/virtual/old", "/virtual/new", "/virtual/report", optimizeForNetworkShares: true),
79+
new FileDiffResultLists(),
80+
new FakeFileSystemService());
81+
82+
Assert.Equal(Math.Min(Environment.ProcessorCount, 8), strategy.DetermineMaxParallel());
83+
}
84+
85+
[Fact]
86+
public void DetermineMaxParallel_WhenConfiguredPositive_ReturnsConfiguredValue()
87+
{
88+
var strategy = CreateStrategy(
89+
CreateConfig(maxParallelism: 12),
90+
CreateExecutionContext("/virtual/old", "/virtual/new", "/virtual/report", optimizeForNetworkShares: true),
91+
new FileDiffResultLists(),
92+
new FakeFileSystemService());
93+
94+
Assert.Equal(12, strategy.DetermineMaxParallel());
95+
}
96+
97+
private static FolderDiffExecutionStrategy CreateStrategy(
98+
ConfigSettings config,
99+
DiffExecutionContext executionContext,
100+
FileDiffResultLists resultLists,
101+
IFileSystemService fileSystem)
102+
=> new(config, executionContext, resultLists, fileSystem);
103+
104+
private static ConfigSettings CreateConfig(int maxParallelism = 0) => new()
105+
{
106+
IgnoredExtensions = new List<string> { ".pdb" },
107+
ShouldIncludeIgnoredFiles = true,
108+
MaxParallelism = maxParallelism
109+
};
110+
111+
private static DiffExecutionContext CreateExecutionContext(
112+
string oldDir,
113+
string newDir,
114+
string reportDir,
115+
bool optimizeForNetworkShares = false)
116+
=> new(oldDir, newDir, reportDir, optimizeForNetworkShares, detectedNetworkOld: false, detectedNetworkNew: false);
117+
118+
private sealed class FakeFileSystemService : IFileSystemService
119+
{
120+
private readonly Dictionary<string, IReadOnlyList<string>> _filesByRoot = new(StringComparer.OrdinalIgnoreCase);
121+
122+
public void SetFiles(string rootFolderAbsolutePath, params string[] files)
123+
=> _filesByRoot[rootFolderAbsolutePath] = files;
124+
125+
public IEnumerable<string> EnumerateFiles(string rootFolderAbsolutePath, string searchPattern, SearchOption searchOption)
126+
=> _filesByRoot.TryGetValue(rootFolderAbsolutePath, out var files)
127+
? files
128+
: Array.Empty<string>();
129+
130+
public void CreateDirectory(string path)
131+
{
132+
}
133+
134+
public DateTime GetLastWriteTimeUtc(string path) => DateTime.UnixEpoch;
135+
}
136+
}
137+
}

ProgramRunner.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ private ServiceProvider BuildRunServiceProvider(ConfigSettings config, DiffExecu
253253
services.AddScoped<ProgressReportService>();
254254
services.AddScoped<ReportGenerateService>();
255255
services.AddScoped<IFileSystemService, FileSystemService>();
256+
services.AddScoped<IFolderDiffExecutionStrategy, FolderDiffExecutionStrategy>();
256257
services.AddScoped<IFileComparisonService, FileComparisonService>();
257258
services.AddScoped<IILTextOutputService, ILTextOutputService>();
258259
services.AddScoped<IDotNetDisassembleService, DotNetDisassembleService>();

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ Example `diff_report.md` (trimmed):
132132
- [`ProgramRunner`](ProgramRunner.cs) validates arguments, loads [`config.json`](config.json), builds a per-run DI container, and executes the diff/report pipeline.
133133
- [`ProgramRunner`](ProgramRunner.cs) also owns aggregated end-of-run console warnings such as `MD5Mismatch` and timestamp-regression notices.
134134
- [`DiffExecutionContext`](Services/DiffExecutionContext.cs) carries run-specific paths and network-mode decisions.
135+
- [`FolderDiffExecutionStrategy`](Services/FolderDiffExecutionStrategy.cs) owns discovery filtering and auto-parallelism policy, so [`FolderDiffService`](Services/FolderDiffService.cs) can stay focused on progress, orchestration, and result routing.
135136
- [`FolderDiffService`](Services/FolderDiffService.cs) uses [`IFileSystemService`](Services/IFileSystemService.cs) for discovery/output I/O, including lazy file enumeration via `EnumerateFiles(...)`, and [`FileDiffService`](Services/FileDiffService.cs) uses [`IFileComparisonService`](Services/IFileComparisonService.cs) for hash, text, and chunk-read operations, which keeps permission and disk-failure paths unit-testable without changing runtime behavior.
136137
- Core pipeline services ([`FolderDiffService`](Services/FolderDiffService.cs), [`FileDiffService`](Services/FileDiffService.cs), [`ILOutputService`](Services/ILOutputService.cs)) depend on interfaces and injected context rather than static fields or `ActivatorUtilities.CreateInstance`, which keeps behavior stable while improving test substitution.
137138

@@ -481,6 +482,7 @@ dotnet run "/Users/UserA/workspace/old" "/Users/UserA/workspace/new" "YYYYMMDD"
481482
- [`ProgramRunner`](ProgramRunner.cs) が引数検証、[`config.json`](config.json) 読込、実行単位 DI コンテナ生成、差分/レポート処理の実行を担います。
482483
- [`ProgramRunner`](ProgramRunner.cs)`MD5Mismatch` や更新日時逆転のような集約後の終了時コンソール警告も担当します。
483484
- [`DiffExecutionContext`](Services/DiffExecutionContext.cs) が実行ごとのパスやネットワークモード判定を保持します。
485+
- [`FolderDiffExecutionStrategy`](Services/FolderDiffExecutionStrategy.cs) が、ファイル探索時の除外ルール適用と自動並列度の決定を担当し、[`FolderDiffService`](Services/FolderDiffService.cs) は進捗・実行制御・結果振り分けへ寄せています。
484486
- [`FolderDiffService`](Services/FolderDiffService.cs) は列挙/出力系 I/O を [`IFileSystemService`](Services/IFileSystemService.cs) に委譲しており、ファイル列挙も `EnumerateFiles(...)` による遅延列挙で扱います。[`FileDiffService`](Services/FileDiffService.cs) はハッシュ/テキスト/チャンク読み出し系 I/O を [`IFileComparisonService`](Services/IFileComparisonService.cs) に委譲しており、権限エラーやディスク系失敗の経路も実ファイルなしでユニットテストできます。
485487
- 主要パイプラインサービス([`FolderDiffService`](Services/FolderDiffService.cs), [`FileDiffService`](Services/FileDiffService.cs), [`ILOutputService`](Services/ILOutputService.cs))は、静的フィールドや `ActivatorUtilities.CreateInstance` ではなく、インターフェースとコンテキスト注入に依存します。これにより既存動作を維持したままテスト差し替え性を高めています。
486488

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using FolderDiffIL4DotNet.Models;
6+
using FolderDiffIL4DotNet.Utils;
7+
8+
namespace FolderDiffIL4DotNet.Services
9+
{
10+
/// <summary>
11+
/// フォルダ差分の探索条件と並列実行ポリシーを決定します。
12+
/// </summary>
13+
public sealed class FolderDiffExecutionStrategy : IFolderDiffExecutionStrategy
14+
{
15+
/// <summary>
16+
/// ネットワーク最適化時の自動並列度上限。
17+
/// </summary>
18+
private const int MAX_PARALLEL_NETWORK_LIMIT = 8;
19+
20+
private readonly ConfigSettings _config;
21+
private readonly FileDiffResultLists _fileDiffResultLists;
22+
private readonly IFileSystemService _fileSystem;
23+
private readonly string _oldFolderAbsolutePath;
24+
private readonly string _newFolderAbsolutePath;
25+
private readonly bool _optimizeForNetworkShares;
26+
private readonly HashSet<string> _ignoredExtensions;
27+
28+
/// <summary>
29+
/// コンストラクタ。
30+
/// </summary>
31+
public FolderDiffExecutionStrategy(
32+
ConfigSettings config,
33+
DiffExecutionContext executionContext,
34+
FileDiffResultLists fileDiffResultLists,
35+
IFileSystemService fileSystem)
36+
{
37+
ArgumentNullException.ThrowIfNull(config);
38+
ArgumentNullException.ThrowIfNull(executionContext);
39+
ArgumentNullException.ThrowIfNull(fileDiffResultLists);
40+
ArgumentNullException.ThrowIfNull(fileSystem);
41+
42+
_config = config;
43+
_fileDiffResultLists = fileDiffResultLists;
44+
_fileSystem = fileSystem;
45+
_oldFolderAbsolutePath = executionContext.OldFolderAbsolutePath;
46+
_newFolderAbsolutePath = executionContext.NewFolderAbsolutePath;
47+
_optimizeForNetworkShares = executionContext.OptimizeForNetworkShares;
48+
_ignoredExtensions = new HashSet<string>(_config.IgnoredExtensions ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
49+
}
50+
51+
/// <inheritdoc />
52+
public List<string> EnumerateIncludedFiles(string rootFolderAbsolutePath, FileDiffResultLists.IgnoredFileLocation locationFlag)
53+
{
54+
var includedFiles = new List<string>();
55+
foreach (var fileAbsolutePath in _fileSystem.EnumerateFiles(rootFolderAbsolutePath, "*", SearchOption.AllDirectories))
56+
{
57+
if (_ignoredExtensions.Contains(Path.GetExtension(fileAbsolutePath)))
58+
{
59+
if (_config.ShouldIncludeIgnoredFiles)
60+
{
61+
var relativePath = Path.GetRelativePath(rootFolderAbsolutePath, fileAbsolutePath);
62+
_fileDiffResultLists.RecordIgnoredFile(relativePath, locationFlag);
63+
}
64+
65+
continue;
66+
}
67+
68+
includedFiles.Add(fileAbsolutePath);
69+
}
70+
71+
return includedFiles;
72+
}
73+
74+
/// <inheritdoc />
75+
public int ComputeUnionFileCount(IReadOnlyCollection<string> oldFilesAbsolutePath, IReadOnlyCollection<string> newFilesAbsolutePath)
76+
{
77+
var oldRelativePathSet = new HashSet<string>(
78+
oldFilesAbsolutePath.Select(path => Path.GetRelativePath(_oldFolderAbsolutePath, path)),
79+
StringComparer.OrdinalIgnoreCase);
80+
var newRelativePathSet = new HashSet<string>(
81+
newFilesAbsolutePath.Select(path => Path.GetRelativePath(_newFolderAbsolutePath, path)),
82+
StringComparer.OrdinalIgnoreCase);
83+
84+
oldRelativePathSet.UnionWith(newRelativePathSet);
85+
return oldRelativePathSet.Count;
86+
}
87+
88+
/// <inheritdoc />
89+
public int DetermineMaxParallel()
90+
{
91+
if (_config.MaxParallelism <= 0)
92+
{
93+
return _optimizeForNetworkShares
94+
? Math.Min(Environment.ProcessorCount, MAX_PARALLEL_NETWORK_LIMIT)
95+
: Environment.ProcessorCount;
96+
}
97+
98+
return _config.MaxParallelism;
99+
}
100+
101+
/// <inheritdoc />
102+
public int CountDotNetAssemblyCandidates(IEnumerable<string> oldFilesAbsolutePath, IEnumerable<string> newFilesAbsolutePath)
103+
=> oldFilesAbsolutePath
104+
.Concat(newFilesAbsolutePath)
105+
.Distinct(StringComparer.OrdinalIgnoreCase)
106+
.Count(DotNetDetector.IsDotNetExecutable);
107+
}
108+
}

0 commit comments

Comments
 (0)