Skip to content

Commit c02e0dd

Browse files
committed
fix: FileDiffResultListsの静的状態をスレッドセーフ化し、ResetAll追加
1 parent e007c6c commit c02e0dd

6 files changed

Lines changed: 213 additions & 74 deletions

File tree

FolderDiffIL4DotNet.Tests/Models/FileDiffResultListsTests.cs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
24
using FolderDiffIL4DotNet.Models;
35
using Xunit;
46

@@ -19,17 +21,7 @@ public void Dispose()
1921

2022
private static void ClearAll()
2123
{
22-
FileDiffResultLists.OldFilesAbsolutePath = new System.Collections.Generic.List<string>();
23-
FileDiffResultLists.NewFilesAbsolutePath = new System.Collections.Generic.List<string>();
24-
FileDiffResultLists.UnchangedFilesRelativePath = new System.Collections.Generic.List<string>();
25-
FileDiffResultLists.AddedFilesAbsolutePath = new System.Collections.Generic.List<string>();
26-
FileDiffResultLists.RemovedFilesAbsolutePath = new System.Collections.Generic.List<string>();
27-
FileDiffResultLists.ModifiedFilesRelativePath = new System.Collections.Generic.List<string>();
28-
FileDiffResultLists.FileRelativePathToDiffDetailDictionary.Clear();
29-
FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary.Clear();
30-
FileDiffResultLists.IgnoredFilesRelativePathToLocation.Clear();
31-
FileDiffResultLists.DisassemblerToolVersions.Clear();
32-
FileDiffResultLists.DisassemblerToolVersionsFromCache.Clear();
24+
FileDiffResultLists.ResetAll();
3325
}
3426

3527
#region RecordDiffDetail
@@ -192,5 +184,71 @@ public void RecordDisassemblerToolVersion_NoVersion_UsesToolNameOnly()
192184
}
193185

194186
#endregion
187+
188+
#region CollectionState
189+
190+
[Fact]
191+
public void SetOldFilesAbsolutePath_ReplacesExistingEntries()
192+
{
193+
FileDiffResultLists.SetOldFilesAbsolutePath(new[] { "old-a.dll", "old-b.dll" });
194+
FileDiffResultLists.SetOldFilesAbsolutePath(new[] { "old-c.dll" });
195+
196+
Assert.Single(FileDiffResultLists.OldFilesAbsolutePath);
197+
Assert.Equal("old-c.dll", FileDiffResultLists.OldFilesAbsolutePath.Single());
198+
}
199+
200+
[Fact]
201+
public void AddUnchangedFileRelativePath_Parallel_AllEntriesRecorded()
202+
{
203+
const int total = 1000;
204+
Parallel.For(0, total, i =>
205+
{
206+
FileDiffResultLists.AddUnchangedFileRelativePath($"unchanged-{i}.dll");
207+
});
208+
209+
Assert.Equal(total, FileDiffResultLists.UnchangedFilesRelativePath.Count);
210+
}
211+
212+
[Fact]
213+
public void ResetAll_ClearsAllState()
214+
{
215+
FileDiffResultLists.SetOldFilesAbsolutePath(new[] { "old-a.dll" });
216+
FileDiffResultLists.SetNewFilesAbsolutePath(new[] { "new-a.dll" });
217+
FileDiffResultLists.AddUnchangedFileRelativePath("same.dll");
218+
FileDiffResultLists.AddAddedFileAbsolutePath("added.dll");
219+
FileDiffResultLists.AddRemovedFileAbsolutePath("removed.dll");
220+
FileDiffResultLists.AddModifiedFileRelativePath("modified.dll");
221+
FileDiffResultLists.RecordDiffDetail("same.dll", FileDiffResultLists.DiffDetailResult.MD5Match);
222+
FileDiffResultLists.RecordIgnoredFile("ignored.pdb", FileDiffResultLists.IgnoredFileLocation.Old);
223+
FileDiffResultLists.RecordDisassemblerToolVersion("dotnet-ildasm", "1.0.0");
224+
FileDiffResultLists.RecordDisassemblerToolVersion("dotnet-ildasm", "1.0.0", fromCache: true);
225+
226+
FileDiffResultLists.ResetAll();
227+
228+
Assert.Empty(FileDiffResultLists.OldFilesAbsolutePath);
229+
Assert.Empty(FileDiffResultLists.NewFilesAbsolutePath);
230+
Assert.Empty(FileDiffResultLists.UnchangedFilesRelativePath);
231+
Assert.Empty(FileDiffResultLists.AddedFilesAbsolutePath);
232+
Assert.Empty(FileDiffResultLists.RemovedFilesAbsolutePath);
233+
Assert.Empty(FileDiffResultLists.ModifiedFilesRelativePath);
234+
Assert.Empty(FileDiffResultLists.FileRelativePathToDiffDetailDictionary);
235+
Assert.Empty(FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary);
236+
Assert.Empty(FileDiffResultLists.IgnoredFilesRelativePathToLocation);
237+
Assert.Empty(FileDiffResultLists.DisassemblerToolVersions);
238+
Assert.Empty(FileDiffResultLists.DisassemblerToolVersionsFromCache);
239+
}
240+
241+
[Fact]
242+
public void ResultQueueProperties_AreReadOnly()
243+
{
244+
Assert.False(typeof(FileDiffResultLists).GetProperty(nameof(FileDiffResultLists.OldFilesAbsolutePath))?.CanWrite);
245+
Assert.False(typeof(FileDiffResultLists).GetProperty(nameof(FileDiffResultLists.NewFilesAbsolutePath))?.CanWrite);
246+
Assert.False(typeof(FileDiffResultLists).GetProperty(nameof(FileDiffResultLists.UnchangedFilesRelativePath))?.CanWrite);
247+
Assert.False(typeof(FileDiffResultLists).GetProperty(nameof(FileDiffResultLists.AddedFilesAbsolutePath))?.CanWrite);
248+
Assert.False(typeof(FileDiffResultLists).GetProperty(nameof(FileDiffResultLists.RemovedFilesAbsolutePath))?.CanWrite);
249+
Assert.False(typeof(FileDiffResultLists).GetProperty(nameof(FileDiffResultLists.ModifiedFilesRelativePath))?.CanWrite);
250+
}
251+
252+
#endregion
195253
}
196254
}

FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ public void GenerateDiffReport_IlDiffDetailsIncludeDisassemblerLabel()
9797
Directory.CreateDirectory(newDir);
9898
Directory.CreateDirectory(reportDir);
9999

100-
FileDiffResultLists.OldFilesAbsolutePath = new List<string> { Path.Combine(oldDir, "a.dll"), Path.Combine(oldDir, "b.dll") };
101-
FileDiffResultLists.NewFilesAbsolutePath = new List<string> { Path.Combine(newDir, "a.dll"), Path.Combine(newDir, "b.dll") };
102-
FileDiffResultLists.UnchangedFilesRelativePath = new List<string> { "a.dll" };
103-
FileDiffResultLists.ModifiedFilesRelativePath = new List<string> { "b.dll" };
100+
FileDiffResultLists.SetOldFilesAbsolutePath(new List<string> { Path.Combine(oldDir, "a.dll"), Path.Combine(oldDir, "b.dll") });
101+
FileDiffResultLists.SetNewFilesAbsolutePath(new List<string> { Path.Combine(newDir, "a.dll"), Path.Combine(newDir, "b.dll") });
102+
FileDiffResultLists.AddUnchangedFileRelativePath("a.dll");
103+
FileDiffResultLists.AddModifiedFileRelativePath("b.dll");
104104

105105
FileDiffResultLists.RecordDiffDetail("a.dll", FileDiffResultLists.DiffDetailResult.ILMatch, "dotnet-ildasm (version: dotnet ildasm 0.12.0)");
106106
FileDiffResultLists.RecordDiffDetail("b.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: dotnet ildasm 0.12.0)");
@@ -191,17 +191,7 @@ public void GenerateDiffReport_HeaderOmitsIlContainsIgnoreNote_WhenDisabled()
191191

192192
private static void ClearResultLists()
193193
{
194-
FileDiffResultLists.OldFilesAbsolutePath = new List<string>();
195-
FileDiffResultLists.NewFilesAbsolutePath = new List<string>();
196-
FileDiffResultLists.UnchangedFilesRelativePath = new List<string>();
197-
FileDiffResultLists.AddedFilesAbsolutePath = new List<string>();
198-
FileDiffResultLists.RemovedFilesAbsolutePath = new List<string>();
199-
FileDiffResultLists.ModifiedFilesRelativePath = new List<string>();
200-
FileDiffResultLists.FileRelativePathToDiffDetailDictionary.Clear();
201-
FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary.Clear();
202-
FileDiffResultLists.IgnoredFilesRelativePathToLocation.Clear();
203-
FileDiffResultLists.DisassemblerToolVersions.Clear();
204-
FileDiffResultLists.DisassemblerToolVersionsFromCache.Clear();
194+
FileDiffResultLists.ResetAll();
205195
}
206196
}
207197
}

Models/FileDiffResultLists.cs

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,32 @@ public enum IgnoredFileLocation
4545
/// <summary>
4646
/// 旧バージョン側(比較元)ファイルの絶対パスのリスト
4747
/// </summary>
48-
public static List<string> OldFilesAbsolutePath { get; set; } = [];
48+
public static ConcurrentQueue<string> OldFilesAbsolutePath { get; } = new ConcurrentQueue<string>();
4949

5050
/// <summary>
5151
/// 新バージョン側(比較先)ファイルの絶対パスのリスト
5252
/// </summary>
53-
public static List<string> NewFilesAbsolutePath { get; set; } = [];
53+
public static ConcurrentQueue<string> NewFilesAbsolutePath { get; } = new ConcurrentQueue<string>();
5454

5555
/// <summary>
5656
/// 差異のないファイルの相対パスのリスト
5757
/// </summary>
58-
public static List<string> UnchangedFilesRelativePath { get; set; } = [];
58+
public static ConcurrentQueue<string> UnchangedFilesRelativePath { get; } = new ConcurrentQueue<string>();
5959

6060
/// <summary>
6161
/// 追加されたファイルの絶対パスのリスト
6262
/// </summary>
63-
public static List<string> AddedFilesAbsolutePath { get; set; } = [];
63+
public static ConcurrentQueue<string> AddedFilesAbsolutePath { get; } = new ConcurrentQueue<string>();
6464

6565
/// <summary>
6666
/// 削除されたファイルの絶対パスのリスト
6767
/// </summary>
68-
public static List<string> RemovedFilesAbsolutePath { get; set; } = [];
68+
public static ConcurrentQueue<string> RemovedFilesAbsolutePath { get; } = new ConcurrentQueue<string>();
6969

7070
/// <summary>
7171
/// 変更されたファイルの相対パスのリスト
7272
/// </summary>
73-
public static List<string> ModifiedFilesRelativePath { get; set; } = [];
73+
public static ConcurrentQueue<string> ModifiedFilesRelativePath { get; } = new ConcurrentQueue<string>();
7474

7575
/// <summary>
7676
/// ファイル間の比較結果を保持する辞書 (並列比較で安全に書き込みできるよう ConcurrentDictionary)。
@@ -103,6 +103,78 @@ public enum IgnoredFileLocation
103103
public static ConcurrentDictionary<string, byte> DisassemblerToolVersionsFromCache { get; } = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
104104
#endregion
105105

106+
/// <summary>
107+
/// 旧バージョン側(比較元)ファイルの絶対パス一覧を置き換えます。
108+
/// </summary>
109+
/// <param name="oldFilesAbsolutePath">旧バージョン側(比較元)ファイルの絶対パス一覧。</param>
110+
public static void SetOldFilesAbsolutePath(IEnumerable<string> oldFilesAbsolutePath)
111+
{
112+
ReplaceQueueItems(OldFilesAbsolutePath, oldFilesAbsolutePath, nameof(oldFilesAbsolutePath));
113+
}
114+
115+
/// <summary>
116+
/// 新バージョン側(比較先)ファイルの絶対パス一覧を置き換えます。
117+
/// </summary>
118+
/// <param name="newFilesAbsolutePath">新バージョン側(比較先)ファイルの絶対パス一覧。</param>
119+
public static void SetNewFilesAbsolutePath(IEnumerable<string> newFilesAbsolutePath)
120+
{
121+
ReplaceQueueItems(NewFilesAbsolutePath, newFilesAbsolutePath, nameof(newFilesAbsolutePath));
122+
}
123+
124+
/// <summary>
125+
/// 差異のないファイルの相対パスを記録します。
126+
/// </summary>
127+
/// <param name="fileRelativePath">ファイルの相対パス。</param>
128+
public static void AddUnchangedFileRelativePath(string fileRelativePath)
129+
{
130+
EnqueuePath(UnchangedFilesRelativePath, fileRelativePath, nameof(fileRelativePath));
131+
}
132+
133+
/// <summary>
134+
/// 追加されたファイルの絶対パスを記録します。
135+
/// </summary>
136+
/// <param name="newFileAbsolutePath">追加されたファイルの絶対パス。</param>
137+
public static void AddAddedFileAbsolutePath(string newFileAbsolutePath)
138+
{
139+
EnqueuePath(AddedFilesAbsolutePath, newFileAbsolutePath, nameof(newFileAbsolutePath));
140+
}
141+
142+
/// <summary>
143+
/// 削除されたファイルの絶対パスを記録します。
144+
/// </summary>
145+
/// <param name="oldFileAbsolutePath">削除されたファイルの絶対パス。</param>
146+
public static void AddRemovedFileAbsolutePath(string oldFileAbsolutePath)
147+
{
148+
EnqueuePath(RemovedFilesAbsolutePath, oldFileAbsolutePath, nameof(oldFileAbsolutePath));
149+
}
150+
151+
/// <summary>
152+
/// 変更されたファイルの相対パスを記録します。
153+
/// </summary>
154+
/// <param name="fileRelativePath">変更されたファイルの相対パス。</param>
155+
public static void AddModifiedFileRelativePath(string fileRelativePath)
156+
{
157+
EnqueuePath(ModifiedFilesRelativePath, fileRelativePath, nameof(fileRelativePath));
158+
}
159+
160+
/// <summary>
161+
/// 比較結果の静的状態をすべて初期化します。
162+
/// </summary>
163+
public static void ResetAll()
164+
{
165+
OldFilesAbsolutePath.Clear();
166+
NewFilesAbsolutePath.Clear();
167+
UnchangedFilesRelativePath.Clear();
168+
AddedFilesAbsolutePath.Clear();
169+
RemovedFilesAbsolutePath.Clear();
170+
ModifiedFilesRelativePath.Clear();
171+
FileRelativePathToDiffDetailDictionary.Clear();
172+
FileRelativePathToIlDisassemblerLabelDictionary.Clear();
173+
IgnoredFilesRelativePathToLocation.Clear();
174+
DisassemblerToolVersions.Clear();
175+
DisassemblerToolVersionsFromCache.Clear();
176+
}
177+
106178
/// <summary>
107179
/// ファイルの比較結果を記録します。
108180
/// </summary>
@@ -162,5 +234,42 @@ public static void RecordDisassemblerToolVersion(string toolName, string version
162234
var target = fromCache ? DisassemblerToolVersionsFromCache : DisassemblerToolVersions;
163235
target[label] = 0;
164236
}
237+
238+
/// <summary>
239+
/// スレッドセーフキュー内の要素を指定した内容で置き換えます。
240+
/// </summary>
241+
/// <param name="targetQueue">置き換え先キュー。</param>
242+
/// <param name="items">置き換える要素。</param>
243+
/// <param name="paramName">null チェック用の引数名。</param>
244+
/// <exception cref="ArgumentNullException">items が null の場合。</exception>
245+
private static void ReplaceQueueItems(ConcurrentQueue<string> targetQueue, IEnumerable<string> items, string paramName)
246+
{
247+
if (items is null)
248+
{
249+
throw new ArgumentNullException(paramName);
250+
}
251+
252+
targetQueue.Clear();
253+
foreach (var item in items)
254+
{
255+
EnqueuePath(targetQueue, item, paramName);
256+
}
257+
}
258+
259+
/// <summary>
260+
/// null でないことを確認してキューに追加します。
261+
/// </summary>
262+
/// <param name="targetQueue">追加先キュー。</param>
263+
/// <param name="path">追加するパス。</param>
264+
/// <param name="paramName">null チェック用の引数名。</param>
265+
/// <exception cref="ArgumentNullException">path が null の場合。</exception>
266+
private static void EnqueuePath(ConcurrentQueue<string> targetQueue, string path, string paramName)
267+
{
268+
if (path is null)
269+
{
270+
throw new ArgumentNullException(paramName);
271+
}
272+
targetQueue.Enqueue(path);
273+
}
165274
}
166275
}

README.en.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ Relation to CI:
7171
- Recursively compares the old folder (CLI arg #1) and the new folder (CLI arg #2).
7272
- Tracks each file as `MD5Match`, `MD5Mismatch`, `ILMatch`, `ILMismatch`, `TextMatch`, or `TextMismatch` (for IL results, the used disassembler/version is also recorded).
7373
- Groups files into `Unchanged`, `Added`, `Removed`, and `Modified` buckets.
74+
- Uses `ConcurrentQueue` / `ConcurrentDictionary` inside `FileDiffResultLists` so result aggregation is thread-safe during parallel comparison.
75+
- Calls `FileDiffResultLists.ResetAll()` at the start of `FolderDiffService.ExecuteFolderDiffAsync` to prevent state leakage across multiple runs in the same process.
7476
- Writes per-bucket listings to `Reports/<report label>/diff_report.md`; paths are relative for `Unchanged`/`Modified` and absolute for `Added`/`Removed`.
7577
- For `ILMatch` / `ILMismatch`, the report also includes the disassembler tool and version used (including cache hits).
7678
- Summarizes counts per bucket in the same report.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ CI との関係:
7979
- TextMatch: テキストベースで一致
8080
- TextMismatch: テキストベースで不一致
8181
- 比較結果区分ごと(Unchanged/Added/Removed/Modified)にファイルを分類
82+
- 比較結果の内部保持(`FileDiffResultLists`)は `ConcurrentQueue` / `ConcurrentDictionary` を使用し、並列実行時の追加処理をスレッドセーフに実行
83+
- `FolderDiffService.ExecuteFolderDiffAsync` の開始時に `FileDiffResultLists.ResetAll()` を実行し、同一プロセス内の複数回比較でも前回結果が混入しないように初期化
8284
- 比較結果区分ごとのファイル一覧を`Reports/<コマンドライン第3引数に指定したレポートのラベル>/diff_report.md`に出力(ファイルのパス、最終更新日時[`config.json`のShouldOutputFileTimestampsが `true` の場合]、判定根拠)
8385
- Unchanged/Modified は相対パスで記載されます。
8486
- Added/Removed は絶対パスで記載されます。

0 commit comments

Comments
 (0)