Skip to content

Commit 08aec24

Browse files
committed
逆アセンブラ回帰対応とレポート改善、キャッシュ混在防止を実装
- 逆アセンブラ選択を「実行中固定」から「ファイルごと候補順試行」に戻し、 連続失敗ツールのブラックリストスキップを維持 - 例外時でも進捗スピナー/ステータスバーが残らないように ProgressReportService に Dispose を実装し Program 側で確実に解放 - diff_report.md の IL Disassembler ヘッダを常時3種 (dotnet-ildasm / ildasm / ilspycmd) 表示に変更 - 各ファイルの ILMatch / ILMismatch に 使用逆アセンブラとバージョンを併記するよう変更 (`ILMatch` / `ILMismatch` とツール情報を別々のバッククォートで表示) - IL比較結果に使用逆アセンブララベルを保持する辞書を追加 - バージョン取得失敗時のキャッシュキー分離を強化: ツール実体のフィンガープリント (実行ファイル名・サイズ・最終更新時刻) を付与し、 旧/新ツールのキャッシュ混在を防止 実体解決不可時は run 単位識別子で前回実行との混在を回避 - 関連する単体テストを追加/更新 - DotNetDisassembleService のフォールバック/ブラックリスト動作 - バージョン取得失敗時のキャッシュ混在防止 - ProgressReportService の解放動作 - ReportGenerateService のヘッダ/IL判定表示
1 parent c10b0e6 commit 08aec24

15 files changed

Lines changed: 862 additions & 121 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using Xunit;
2+
3+
[assembly: CollectionBehavior(DisableTestParallelization = true)]

FolderDiffIL4DotNet.Tests/Models/FileDiffResultListsTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ private static void ClearAll()
2626
FileDiffResultLists.RemovedFilesAbsolutePath = new System.Collections.Generic.List<string>();
2727
FileDiffResultLists.ModifiedFilesRelativePath = new System.Collections.Generic.List<string>();
2828
FileDiffResultLists.FileRelativePathToDiffDetailDictionary.Clear();
29+
FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary.Clear();
2930
FileDiffResultLists.IgnoredFilesRelativePathToLocation.Clear();
3031
FileDiffResultLists.DisassemblerToolVersions.Clear();
3132
FileDiffResultLists.DisassemblerToolVersionsFromCache.Clear();
@@ -61,6 +62,24 @@ public void RecordDiffDetail_MultipleEntries_AllStored()
6162
Assert.Equal(3, FileDiffResultLists.FileRelativePathToDiffDetailDictionary.Count);
6263
}
6364

65+
[Fact]
66+
public void RecordDiffDetail_IlResult_WithDisassemblerLabel_Stored()
67+
{
68+
FileDiffResultLists.RecordDiffDetail("a.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)");
69+
70+
Assert.True(FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary.ContainsKey("a.dll"));
71+
Assert.Equal("dotnet-ildasm (version: 0.12.0)", FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary["a.dll"]);
72+
}
73+
74+
[Fact]
75+
public void RecordDiffDetail_NonIlResult_ClearsExistingDisassemblerLabel()
76+
{
77+
FileDiffResultLists.RecordDiffDetail("a.dll", FileDiffResultLists.DiffDetailResult.ILMatch, "dotnet-ildasm (version: 0.12.0)");
78+
FileDiffResultLists.RecordDiffDetail("a.dll", FileDiffResultLists.DiffDetailResult.MD5Match);
79+
80+
Assert.False(FileDiffResultLists.FileRelativePathToIlDisassemblerLabelDictionary.ContainsKey("a.dll"));
81+
}
82+
6483
#endregion
6584

6685
#region HasAnyMd5Mismatch
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.IO;
4+
using System.Reflection;
5+
using System.Threading.Tasks;
6+
using FolderDiffIL4DotNet.Models;
7+
using FolderDiffIL4DotNet.Services;
8+
using FolderDiffIL4DotNet.Services.Caching;
9+
using Xunit;
10+
11+
namespace FolderDiffIL4DotNet.Tests.Services
12+
{
13+
public sealed class DotNetDisassembleServiceTests : IDisposable
14+
{
15+
private readonly string _rootDir;
16+
17+
public DotNetDisassembleServiceTests()
18+
{
19+
_rootDir = Path.Combine(Path.GetTempPath(), "fd-disasm-tests-" + Guid.NewGuid().ToString("N"));
20+
Directory.CreateDirectory(_rootDir);
21+
ResetDisassemblerFailureState();
22+
}
23+
24+
public void Dispose()
25+
{
26+
ResetDisassemblerFailureState();
27+
try
28+
{
29+
if (Directory.Exists(_rootDir))
30+
{
31+
Directory.Delete(_rootDir, recursive: true);
32+
}
33+
}
34+
catch
35+
{
36+
// ignore cleanup errors in tests
37+
}
38+
}
39+
40+
[Fact]
41+
public async Task DisassembleAsync_UsesPerFileFallback_WhenPrimaryToolFailsForSpecificFile()
42+
{
43+
if (OperatingSystem.IsWindows())
44+
{
45+
return;
46+
}
47+
48+
var binDir = Path.Combine(_rootDir, "bin");
49+
Directory.CreateDirectory(binDir);
50+
51+
WriteExecutable(binDir, "dotnet-ildasm", """
52+
#!/bin/sh
53+
if [ "$1" = "--version" ] || [ "$1" = "-v" ]; then
54+
echo "dotnet ildasm 0.12.0"
55+
exit 0
56+
fi
57+
case "$1" in
58+
*bad.dll) exit 90 ;;
59+
esac
60+
echo "IL_FROM_DOTNET_ILDASM"
61+
exit 0
62+
""");
63+
WriteExecutable(binDir, "dotnet", """
64+
#!/bin/sh
65+
exit 1
66+
""");
67+
WriteExecutable(binDir, "ilspycmd", """
68+
#!/bin/sh
69+
if [ "$1" = "--version" ] || [ "$1" = "-v" ] || [ "$1" = "-h" ]; then
70+
echo "ilspycmd 9.1.0"
71+
exit 0
72+
fi
73+
echo "IL_FROM_ILSPY"
74+
exit 0
75+
""");
76+
77+
var oldPath = Environment.GetEnvironmentVariable("PATH");
78+
var oldHome = Environment.GetEnvironmentVariable("HOME");
79+
try
80+
{
81+
Environment.SetEnvironmentVariable("PATH", binDir + Path.PathSeparator + oldPath);
82+
Environment.SetEnvironmentVariable("HOME", _rootDir);
83+
84+
var config = CreateConfig(enableIlCache: false);
85+
var service = new DotNetDisassembleService(config, ilCache: null);
86+
87+
var goodDll = Path.Combine(_rootDir, "good.dll");
88+
var badDll = Path.Combine(_rootDir, "bad.dll");
89+
await File.WriteAllTextAsync(goodDll, "dummy");
90+
await File.WriteAllTextAsync(badDll, "dummy");
91+
92+
var (_, command1) = await service.DisassembleAsync(goodDll);
93+
var (_, command2) = await service.DisassembleAsync(badDll);
94+
95+
Assert.Contains("dotnet-ildasm", command1, StringComparison.OrdinalIgnoreCase);
96+
Assert.Contains("ilspycmd", command2, StringComparison.OrdinalIgnoreCase);
97+
}
98+
finally
99+
{
100+
Environment.SetEnvironmentVariable("PATH", oldPath);
101+
Environment.SetEnvironmentVariable("HOME", oldHome);
102+
}
103+
}
104+
105+
[Fact]
106+
public async Task DisassembleAsync_BlacklistsConsecutiveFailures_AndSkipsFailedTool()
107+
{
108+
if (OperatingSystem.IsWindows())
109+
{
110+
return;
111+
}
112+
113+
var binDir = Path.Combine(_rootDir, "bin2");
114+
Directory.CreateDirectory(binDir);
115+
var counterPath = Path.Combine(_rootDir, "dotnet_ildasm_count.txt");
116+
117+
WriteExecutable(binDir, "dotnet-ildasm", $"""
118+
#!/bin/sh
119+
if [ "$1" = "--version" ] || [ "$1" = "-v" ]; then
120+
echo "dotnet ildasm 0.12.0"
121+
exit 0
122+
fi
123+
echo x >> "{counterPath}"
124+
exit 91
125+
""");
126+
WriteExecutable(binDir, "dotnet", """
127+
#!/bin/sh
128+
exit 1
129+
""");
130+
WriteExecutable(binDir, "ilspycmd", """
131+
#!/bin/sh
132+
if [ "$1" = "--version" ] || [ "$1" = "-v" ] || [ "$1" = "-h" ]; then
133+
echo "ilspycmd 9.1.0"
134+
exit 0
135+
fi
136+
echo "IL_FROM_ILSPY"
137+
exit 0
138+
""");
139+
140+
var oldPath = Environment.GetEnvironmentVariable("PATH");
141+
var oldHome = Environment.GetEnvironmentVariable("HOME");
142+
try
143+
{
144+
Environment.SetEnvironmentVariable("PATH", binDir + Path.PathSeparator + oldPath);
145+
Environment.SetEnvironmentVariable("HOME", _rootDir);
146+
147+
var config = CreateConfig(enableIlCache: false);
148+
var service = new DotNetDisassembleService(config, ilCache: null);
149+
150+
int countAfter1 = 0;
151+
int countAfter2 = 0;
152+
int countAfter3 = 0;
153+
for (int i = 1; i <= 3; i++)
154+
{
155+
var dllPath = Path.Combine(_rootDir, $"f{i}.dll");
156+
await File.WriteAllTextAsync(dllPath, "dummy");
157+
var (_, command) = await service.DisassembleAsync(dllPath);
158+
Assert.Contains("ilspycmd", command, StringComparison.OrdinalIgnoreCase);
159+
var currentCount = File.Exists(counterPath) ? await CountLinesAsync(counterPath) : 0;
160+
if (i == 1)
161+
{
162+
countAfter1 = currentCount;
163+
}
164+
else if (i == 2)
165+
{
166+
countAfter2 = currentCount;
167+
}
168+
else
169+
{
170+
countAfter3 = currentCount;
171+
}
172+
}
173+
174+
Assert.True(countAfter1 > 0);
175+
Assert.True(countAfter2 > countAfter1);
176+
Assert.Equal(countAfter2, countAfter3);
177+
}
178+
finally
179+
{
180+
Environment.SetEnvironmentVariable("PATH", oldPath);
181+
Environment.SetEnvironmentVariable("HOME", oldHome);
182+
}
183+
}
184+
185+
[Fact]
186+
public async Task DisassembleAsync_WhenVersionLookupFails_UsesFingerprintAndAvoidsCrossVersionCacheMix()
187+
{
188+
if (OperatingSystem.IsWindows())
189+
{
190+
return;
191+
}
192+
193+
var binDir = Path.Combine(_rootDir, "bin3");
194+
Directory.CreateDirectory(binDir);
195+
var cacheDir = Path.Combine(_rootDir, "ilcache");
196+
var counterPath = Path.Combine(_rootDir, "dotnet_ildasm_counter.txt");
197+
198+
WriteExecutable(binDir, "dotnet-ildasm", BuildVersionFailingDisassemblerScript(counterPath, "#REV-A"));
199+
WriteExecutable(binDir, "dotnet", """
200+
#!/bin/sh
201+
exit 1
202+
""");
203+
WriteExecutable(binDir, "ilspycmd", """
204+
#!/bin/sh
205+
exit 1
206+
""");
207+
208+
var oldPath = Environment.GetEnvironmentVariable("PATH");
209+
var oldHome = Environment.GetEnvironmentVariable("HOME");
210+
try
211+
{
212+
Environment.SetEnvironmentVariable("PATH", binDir + Path.PathSeparator + oldPath);
213+
Environment.SetEnvironmentVariable("HOME", _rootDir);
214+
215+
var dllPath = Path.Combine(_rootDir, "cache-target.dll");
216+
await File.WriteAllTextAsync(dllPath, "dummy");
217+
218+
var config = CreateConfig(enableIlCache: true);
219+
var service1 = new DotNetDisassembleService(config, new ILCache(cacheDir));
220+
var (_, command1) = await service1.DisassembleAsync(dllPath);
221+
var countAfterFirstRun = File.Exists(counterPath) ? await CountLinesAsync(counterPath) : 0;
222+
Assert.Contains("fingerprint:", command1, StringComparison.OrdinalIgnoreCase);
223+
Assert.True(countAfterFirstRun > 0);
224+
225+
// 実体更新(サイズ/更新時刻を変える)を模擬
226+
await Task.Delay(1100);
227+
WriteExecutable(binDir, "dotnet-ildasm", BuildVersionFailingDisassemblerScript(counterPath, "#REV-B-LONGER"));
228+
229+
var service2 = new DotNetDisassembleService(config, new ILCache(cacheDir));
230+
var (_, command2) = await service2.DisassembleAsync(dllPath);
231+
var countAfterSecondRun = File.Exists(counterPath) ? await CountLinesAsync(counterPath) : 0;
232+
233+
Assert.Contains("fingerprint:", command2, StringComparison.OrdinalIgnoreCase);
234+
Assert.NotEqual(command1, command2);
235+
Assert.True(countAfterSecondRun > countAfterFirstRun);
236+
}
237+
finally
238+
{
239+
Environment.SetEnvironmentVariable("PATH", oldPath);
240+
Environment.SetEnvironmentVariable("HOME", oldHome);
241+
}
242+
}
243+
244+
private static ConfigSettings CreateConfig(bool enableIlCache) => new()
245+
{
246+
EnableILCache = enableIlCache,
247+
IgnoredExtensions = new(),
248+
TextFileExtensions = new()
249+
};
250+
251+
private static void WriteExecutable(string directory, string fileName, string content)
252+
{
253+
var path = Path.Combine(directory, fileName);
254+
File.WriteAllText(path, content);
255+
if (!OperatingSystem.IsWindows())
256+
{
257+
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
258+
}
259+
}
260+
261+
private static async Task<int> CountLinesAsync(string path)
262+
{
263+
var count = 0;
264+
using var reader = new StreamReader(path);
265+
while (await reader.ReadLineAsync() != null)
266+
{
267+
count++;
268+
}
269+
return count;
270+
}
271+
272+
private static string BuildVersionFailingDisassemblerScript(string counterPath, string revisionMarker) => $"""
273+
#!/bin/sh
274+
if [ "$1" = "--version" ] || [ "$1" = "-v" ]; then
275+
exit 2
276+
fi
277+
echo x >> "{counterPath}"
278+
echo "IL_FROM_VERSION_FAILING_TOOL"
279+
exit 0
280+
{revisionMarker}
281+
""";
282+
283+
private static void ResetDisassemblerFailureState()
284+
{
285+
var field = typeof(DotNetDisassembleService).GetField("_disassembleFailCountAndTime", BindingFlags.Static | BindingFlags.NonPublic);
286+
Assert.NotNull(field);
287+
var dictionary = field.GetValue(null) as ConcurrentDictionary<string, (int FailCount, DateTime LastFailUtc)>;
288+
Assert.NotNull(dictionary);
289+
dictionary.Clear();
290+
}
291+
}
292+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Reflection;
2+
using FolderDiffIL4DotNet.Services;
3+
using Xunit;
4+
5+
namespace FolderDiffIL4DotNet.Tests.Services
6+
{
7+
public sealed class ProgressReportServiceTests
8+
{
9+
[Fact]
10+
public void Dispose_StopsTimer_AndFurtherCallsAreIgnored()
11+
{
12+
var service = new ProgressReportService();
13+
service.SetLabel("test");
14+
service.ReportProgress(0.0);
15+
16+
service.Dispose();
17+
service.ReportProgress(1.0);
18+
service.SetLabel("updated");
19+
20+
var timerField = typeof(ProgressReportService).GetField("_keepAliveTimer", BindingFlags.Instance | BindingFlags.NonPublic);
21+
Assert.NotNull(timerField);
22+
Assert.Null(timerField.GetValue(service));
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)