Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -586,11 +586,17 @@ public async Task RunAsync_OpenReportsFlag_WhenOutputTargetsExistingFile_Returns
Assert.Equal(4, exitCode);
string errorOutput = errorWriter.ToString();
Assert.Contains("Failed to open folder", errorOutput, StringComparison.Ordinal);
Assert.Contains("during stage 'validating target path'", errorOutput, StringComparison.Ordinal);
Assert.Contains(outputFile, errorOutput, StringComparison.Ordinal);
Assert.Contains("IOException", errorOutput, StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", errorOutput, StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", errorOutput, StringComparison.Ordinal);
Assert.Empty(startedProcesses);
var loggedMessage = Assert.Single(logger.Messages);
Assert.Contains("Failed to open folder", loggedMessage, StringComparison.Ordinal);
Assert.Contains("during stage 'validating target path'", loggedMessage, StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", loggedMessage, StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", loggedMessage, StringComparison.Ordinal);
}
finally
{
Expand Down Expand Up @@ -635,10 +641,12 @@ public async Task RunAsync_OpenReportsFlag_WhenFolderLauncherThrows_ReturnsExecu

string errorOutput = errorWriter.ToString();
Assert.Contains("Failed to open folder", errorOutput, StringComparison.Ordinal);
Assert.Contains("during stage 'launching file manager'", errorOutput, StringComparison.Ordinal);
Assert.Contains(Path.GetFullPath(tempDir), errorOutput, StringComparison.Ordinal);
Assert.Contains("InvalidOperationException", errorOutput, StringComparison.Ordinal);
var loggedMessage = Assert.Single(logger.Messages);
Assert.Contains("Failed to open folder", loggedMessage, StringComparison.Ordinal);
Assert.Contains("during stage 'launching file manager'", loggedMessage, StringComparison.Ordinal);
}
finally
{
Expand Down Expand Up @@ -853,8 +861,12 @@ public async Task RunAsync_MultipleOpenFolderFlags_WhenSecondLauncherFails_DoesN
Assert.Contains("Failed to open folder", errorOutput, StringComparison.Ordinal);
Assert.Contains(Path.GetFullPath(tempDir), errorOutput, StringComparison.Ordinal);
Assert.Contains("InvalidOperationException", errorOutput, StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", errorOutput, StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", errorOutput, StringComparison.Ordinal);
var loggedMessage = Assert.Single(logger.Messages);
Assert.Contains("Failed to open folder", loggedMessage, StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", loggedMessage, StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", loggedMessage, StringComparison.Ordinal);
}
finally
{
Expand Down Expand Up @@ -998,7 +1010,10 @@ public async Task RunAsync_OpenReportsFlag_WhenLoggerInitializationFails_WithJso
Assert.Contains("UnauthorizedAccessException", root.GetProperty("message").GetString(), StringComparison.Ordinal);
Assert.Contains(typeof(UnauthorizedAccessException).FullName, root.GetProperty("exceptionType").GetString(), StringComparison.Ordinal);
Assert.Contains("Failed to open folder", errorLines[1], StringComparison.Ordinal);
Assert.Contains("during stage 'launching file manager'", errorLines[1], StringComparison.Ordinal);
Assert.Contains(Path.GetFullPath(appDataScope.ReportsRootAbsolutePath), errorLines[1], StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", errorLines[1], StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", errorLines[1], StringComparison.Ordinal);
}
finally
{
Expand Down Expand Up @@ -1136,10 +1151,16 @@ public async Task RunAsync_MultipleOpenFolderFlags_WhenFirstLauncherFails_DoesNo

string errorOutput = errorWriter.ToString();
Assert.Contains("Failed to open folder", errorOutput, StringComparison.Ordinal);
Assert.Contains("during stage 'launching file manager'", errorOutput, StringComparison.Ordinal);
Assert.Contains(Path.GetFullPath(tempDir), errorOutput, StringComparison.Ordinal);
Assert.Contains("InvalidOperationException", errorOutput, StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", errorOutput, StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", errorOutput, StringComparison.Ordinal);
var loggedMessage = Assert.Single(logger.Messages);
Assert.Contains("Failed to open folder", loggedMessage, StringComparison.Ordinal);
Assert.Contains("during stage 'launching file manager'", loggedMessage, StringComparison.Ordinal);
Assert.Contains("TargetPathIsPathRooted=True", loggedMessage, StringComparison.Ordinal);
Assert.Contains("TargetPathLooksPathLike=True", loggedMessage, StringComparison.Ordinal);
}
finally
{
Expand Down
5 changes: 5 additions & 0 deletions FolderDiffIL4DotNet.Tests/Runner/DiffPipelineExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ public async Task ExecuteAsync_WhenPostProcessActionThrows_LogsActionAndExceptio
Assert.Contains(nameof(ThrowingPostProcessAction), warning.Message, StringComparison.Ordinal);
Assert.Contains("position 1/1", warning.Message, StringComparison.Ordinal);
Assert.Contains("Order=0", warning.Message, StringComparison.Ordinal);
Assert.Contains($"ReportsFolder='{reportDir}'", warning.Message, StringComparison.Ordinal);
Assert.Contains("Added=0", warning.Message, StringComparison.Ordinal);
Assert.Contains("Removed=0", warning.Message, StringComparison.Ordinal);
Assert.Contains("Modified=0", warning.Message, StringComparison.Ordinal);
Assert.Contains("Unchanged=0", warning.Message, StringComparison.Ordinal);
Assert.Contains(nameof(InvalidOperationException), warning.Message, StringComparison.Ordinal);
Assert.NotNull(warning.Exception);
}
Expand Down
24 changes: 24 additions & 0 deletions FolderDiffIL4DotNet.Tests/Runner/PluginLoaderTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using FolderDiffIL4DotNet.Runner;
using FolderDiffIL4DotNet.Tests.Helpers;
Expand Down Expand Up @@ -64,6 +65,27 @@ public void LoadPlugins_EmptySearchPath_ReturnsEmptyList()
Assert.Empty(result);
}

[Fact]
public void TryEnumeratePluginDirectories_InvalidSearchPath_LogsPathShapeDiagnosticsAndReturnsEmpty()
{
// Arrange / 準備
var method = typeof(PluginLoader).GetMethod("TryEnumeratePluginDirectories", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(method);

// Act / 実行
var result = method!.Invoke(_loader, new object[] { "bad\0plugin-search-path" });

// Assert / 検証
var directories = Assert.IsAssignableFrom<IEnumerable<string>>(result);
Assert.Empty(directories);

var entry = Assert.Single(_logger.Entries, e => e.Message.Contains("Failed to enumerate plugin search path", StringComparison.Ordinal));
Assert.NotNull(entry.Exception);
Assert.Contains("PluginSearchPathIsPathRooted=", entry.Message, StringComparison.Ordinal);
Assert.Contains("PluginSearchPathLooksPathLike=", entry.Message, StringComparison.Ordinal);
Assert.Contains(entry.Exception!.GetType().Name, entry.Message, StringComparison.Ordinal);
}

[Fact]
public void LoadPlugins_SubdirectoryWithoutMatchingDll_ReturnsEmptyList()
{
Expand Down Expand Up @@ -102,6 +124,8 @@ public void LoadPlugins_InvalidDll_LogsWarningWithExceptionTypeAndReturnsEmptyLi
Assert.Empty(result);
var entry = Assert.Single(_logger.Entries, e => e.Message.Contains("Failed to load plugin", StringComparison.Ordinal));
Assert.NotNull(entry.Exception);
Assert.Contains("PluginDllPathIsPathRooted=True", entry.Message, StringComparison.Ordinal);
Assert.Contains("PluginDllPathLooksPathLike=True", entry.Message, StringComparison.Ordinal);
Assert.Contains(entry.Exception!.GetType().Name, entry.Message, StringComparison.Ordinal);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ public void Remove_WhenCacheDirectoryBecomesInvalid_LogsCacheDirectoryAndKeyLeng
Assert.Null(exception);
var warning = Assert.Single(logger.Entries, entry => entry.LogLevel == AppLogLevel.Warning);
Assert.Contains("Failed to remove disk cache file", warning.Message, StringComparison.Ordinal);
Assert.Contains("during explicit remove", warning.Message, StringComparison.Ordinal);
Assert.DoesNotContain("during LRU eviction", warning.Message, StringComparison.Ordinal);
Assert.Contains(invalidCacheDir, warning.Message, StringComparison.Ordinal);
Assert.Contains("CacheDirectoryIsPathRooted=True", warning.Message, StringComparison.Ordinal);
Assert.Contains("CacheDirectoryLooksPathLike=True", warning.Message, StringComparison.Ordinal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public async Task FilesAreEqualAsync_BeforeCompareHookThrows_LogsAndProceedsToBu
Assert.Contains(logger.Messages,
m => m.Contains("Plugin BeforeCompare hook 'FakeFileComparisonHook' failed", StringComparison.Ordinal)
&& m.Contains("Order=0", StringComparison.Ordinal)
&& m.Contains("OldRoot='/virtual/old'", StringComparison.Ordinal)
&& m.Contains("NewRoot='/virtual/new'", StringComparison.Ordinal)
&& m.Contains("InvalidOperationException", StringComparison.Ordinal));
}

Expand Down Expand Up @@ -155,6 +157,8 @@ public async Task FilesAreEqualAsync_AfterCompareHookThrows_LogsButDoesNotAffect
Assert.Contains(logger.Messages,
m => m.Contains("Plugin AfterCompare hook 'FakeFileComparisonHook' failed", StringComparison.Ordinal)
&& m.Contains("Order=0", StringComparison.Ordinal)
&& m.Contains("OldRoot='/virtual/old'", StringComparison.Ordinal)
&& m.Contains("NewRoot='/virtual/new'", StringComparison.Ordinal)
&& m.Contains("InvalidOperationException", StringComparison.Ordinal));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ public async Task ExecuteFolderDiffAsync_WhenCreatingIlOutputDirectoryThrowsDire
entry => entry.LogLevel == AppLogLevel.Error
&& entry.Exception is DirectoryNotFoundException
&& entry.Message.Contains($"An error occurred while diffing '{oldDir}' and '{newDir}' during phase 'creating IL output directories'.", StringComparison.Ordinal)
&& entry.Message.Contains("OldFolderIsPathRooted=True", StringComparison.Ordinal)
&& entry.Message.Contains("OldFolderLooksPathLike=True", StringComparison.Ordinal)
&& entry.Message.Contains("NewFolderIsPathRooted=True", StringComparison.Ordinal)
&& entry.Message.Contains("NewFolderLooksPathLike=True", StringComparison.Ordinal)
&& entry.Message.Contains("Failure=DirectoryNotFoundException: parent directory missing", StringComparison.Ordinal));
}

Expand Down Expand Up @@ -489,6 +493,10 @@ public async Task ExecuteFolderDiffAsync_WhenFileDiffThrowsUnexpectedException_L
entry => entry.LogLevel == AppLogLevel.Error
&& entry.Exception is FormatException
&& entry.Message.Contains($"An unexpected error occurred while diffing '{oldDir}' and '{newDir}' during phase 'classifying files sequentially'.", StringComparison.Ordinal)
&& entry.Message.Contains("OldFolderIsPathRooted=True", StringComparison.Ordinal)
&& entry.Message.Contains("OldFolderLooksPathLike=True", StringComparison.Ordinal)
&& entry.Message.Contains("NewFolderIsPathRooted=True", StringComparison.Ordinal)
&& entry.Message.Contains("NewFolderLooksPathLike=True", StringComparison.Ordinal)
&& entry.Message.Contains("Failure=FormatException: unexpected compare failure", StringComparison.Ordinal));
}

Expand Down
27 changes: 27 additions & 0 deletions FolderDiffIL4DotNet.Tests/Services/ReviewChecklistLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void Load_WhenChecklistPathResolutionFails_LogsWarningWithExceptionTypeAn
&& entry.Message.Contains("Review checklist path could not be resolved", StringComparison.Ordinal));
Assert.Contains(AppDataPaths.LOCAL_APP_DATA_OVERRIDE_KEY, warning.Message, StringComparison.Ordinal);
Assert.Contains("OverridePresent=True", warning.Message, StringComparison.Ordinal);
Assert.Contains("OverrideValueType=String", warning.Message, StringComparison.Ordinal);
Assert.Contains(nameof(InvalidOperationException), warning.Message, StringComparison.Ordinal);
Assert.True(warning.ShouldOutputMessageToConsole);
}
Expand All @@ -55,6 +56,7 @@ public void Load_WhenChecklistPathOverrideIsMalformed_LogsWarningAndReturnsEmpty
&& entry.Message.Contains("Review checklist path could not be resolved", StringComparison.Ordinal));
Assert.Contains(AppDataPaths.LOCAL_APP_DATA_OVERRIDE_KEY, warning.Message, StringComparison.Ordinal);
Assert.Contains("OverridePresent=True", warning.Message, StringComparison.Ordinal);
Assert.Contains("OverrideValueType=String", warning.Message, StringComparison.Ordinal);
Assert.Contains(nameof(ArgumentException), warning.Message, StringComparison.Ordinal);
Assert.True(warning.ShouldOutputMessageToConsole);
}
Expand Down Expand Up @@ -93,5 +95,30 @@ public void Load_WhenChecklistPathIsUnreadable_LogsWarningWithConsistentExceptio
|| warning.Message.Contains($", {nameof(IOException)}):", StringComparison.Ordinal));
Assert.True(warning.ShouldOutputMessageToConsole);
}

[Fact]
[Trait("Category", "Unit")]
public void Load_WhenChecklistJsonIsInvalid_LogsJsonCoordinatesAndReturnsEmpty()
{
using var appDataScope = new AppDataOverrideScope(
Path.Combine(Path.GetTempPath(), "fd-review-checklist-loader-" + Guid.NewGuid().ToString("N")));
var checklistPath = AppDataPaths.GetDefaultReviewChecklistFileAbsolutePath();
Directory.CreateDirectory(Path.GetDirectoryName(checklistPath)!);
File.WriteAllText(checklistPath, "{\n invalid-json\n}", System.Text.Encoding.UTF8);

var logger = new TestLogger();

var items = ReviewChecklistLoader.Load(logger);

Assert.Empty(items);
var warning = Assert.Single(
logger.Entries,
entry => entry.LogLevel == AppLogLevel.Warning
&& entry.Message.Contains("invalid JSON", StringComparison.Ordinal));
Assert.Contains("LineNumber=", warning.Message, StringComparison.Ordinal);
Assert.Contains("BytePositionInLine=", warning.Message, StringComparison.Ordinal);
Assert.Contains(nameof(System.Text.Json.JsonException), warning.Message, StringComparison.Ordinal);
Assert.True(warning.ShouldOutputMessageToConsole);
}
}
}
2 changes: 1 addition & 1 deletion Runner/DiffPipelineExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private async Task RunPostProcessActionsAsync(
catch (Exception ex)
{
_logger.LogMessage(AppLogLevel.Warning,
$"Post-process action '{action.GetType().Name}' failed at position {actionIndex + 1}/{actions.Count} (Order={action.Order}, {ex.GetType().Name}): {ex.Message}",
$"Post-process action '{action.GetType().Name}' failed at position {actionIndex + 1}/{actions.Count} (Order={action.Order}, ReportsFolder='{context.ReportsFolderAbsolutePath}', Added={context.AddedCount}, Removed={context.RemovedCount}, Modified={context.ModifiedCount}, Unchanged={context.UnchangedCount}, {ex.GetType().Name}): {ex.Message}",
shouldOutputMessageToConsole: true, ex);
}
#pragma warning restore CA1031
Expand Down
5 changes: 3 additions & 2 deletions Runner/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using FolderDiffIL4DotNet.Common;
using FolderDiffIL4DotNet.Plugin.Abstractions;
using FolderDiffIL4DotNet.Services;

Expand Down Expand Up @@ -96,7 +97,7 @@ private IEnumerable<string> TryEnumeratePluginDirectories(string searchPath)
or UnauthorizedAccessException or ArgumentException or NotSupportedException)
{
_logger.LogMessage(AppLogLevel.Warning,
$"Failed to enumerate plugin search path '{searchPath}' ({ex.GetType().Name}): {ex.Message}",
$"Failed to enumerate plugin search path '{searchPath}' ({PathShapeDiagnostics.DescribeState("PluginSearchPath", searchPath)}, {ex.GetType().Name}): {ex.Message}",
shouldOutputMessageToConsole: true, ex);
return Array.Empty<string>();
}
Expand Down Expand Up @@ -177,7 +178,7 @@ private IEnumerable<string> TryEnumeratePluginDirectories(string searchPath)
catch (Exception ex)
{
_logger.LogMessage(AppLogLevel.Warning,
$"Failed to load plugin from '{pluginDllPath}': {ex.GetType().Name}: {ex.Message}",
$"Failed to load plugin from '{pluginDllPath}' ({PathShapeDiagnostics.DescribeState("PluginDllPath", pluginDllPath)}, {ex.GetType().Name}): {ex.Message}",
shouldOutputMessageToConsole: true, ex);
return null;
}
Expand Down
Loading
Loading