diff --git a/.kiro/specs/dotnet-api-diff/tasks.md b/.kiro/specs/dotnet-api-diff/tasks.md
index d6b32be..8568a57 100644
--- a/.kiro/specs/dotnet-api-diff/tasks.md
+++ b/.kiro/specs/dotnet-api-diff/tasks.md
@@ -146,14 +146,14 @@ Each task should follow this git workflow:
- _Requirements: 1.4, 3.4, 6.6_
- [ ] 8. Create integration tests and end-to-end scenarios
- - [ ] 8.1 Build test assembly pairs for integration testing
+ - [x] 8.1 Build test assembly pairs for integration testing
- Create sample assemblies with known API differences for testing
- Include scenarios with namespace mappings, exclusions, and breaking changes
- Write integration tests using these test assemblies
- **Git Workflow**: Create branch `feature/task-8.1-integration-tests`, commit, push, and create PR
- _Requirements: 1.1, 1.2, 1.3_
- - [ ] 8.2 Test complete workflows with configuration files
+ - [x] 8.2 Test complete workflows with configuration files
- Create sample configuration files for different use cases
- Test end-to-end workflows from CLI input to formatted output
- Validate exit codes and error handling in realistic scenarios
diff --git a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs
new file mode 100644
index 0000000..e20112f
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs
@@ -0,0 +1,496 @@
+// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace DotNetApiDiff.Tests.Integration;
+
+///
+/// Integration tests for CLI workflows using the actual executable
+///
+public class CliWorkflowTests : IDisposable
+{
+ private readonly ITestOutputHelper _output;
+ private readonly string _testDataPath;
+ private readonly string _tempOutputPath;
+ private readonly string? _executablePath;
+
+ public CliWorkflowTests(ITestOutputHelper output)
+ {
+ _output = output;
+ _testDataPath = Path.Combine(Directory.GetCurrentDirectory(), "TestData");
+ _tempOutputPath = Path.Combine(Path.GetTempPath(), "DotNetApiDiff.CliTests", Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempOutputPath);
+
+ // Find the executable path
+ _executablePath = FindExecutablePath();
+ }
+
+ private string? FindExecutablePath()
+ {
+ // Look for the built executable in common locations
+ var possiblePaths = new[]
+ {
+ Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "src", "DotNetApiDiff", "bin", "Debug", "net8.0", "DotNetApiDiff.exe"),
+ Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "src", "DotNetApiDiff", "bin", "Debug", "net8.0", "DotNetApiDiff"),
+ Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "src", "DotNetApiDiff", "bin", "Release", "net8.0", "DotNetApiDiff.exe"),
+ Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "src", "DotNetApiDiff", "bin", "Release", "net8.0", "DotNetApiDiff")
+ };
+
+ foreach (var path in possiblePaths)
+ {
+ if (File.Exists(path))
+ {
+ return path;
+ }
+ }
+
+ // Check if the project file exists for dotnet run
+ var projectPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "src", "DotNetApiDiff", "DotNetApiDiff.csproj");
+ if (File.Exists(projectPath))
+ {
+ return "dotnet";
+ }
+
+ // Return null if neither executable nor project file found
+ return null;
+ }
+
+ private ProcessResult RunCliCommand(string arguments, int expectedExitCode = -1)
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ return new ProcessResult { ExitCode = -1, StandardOutput = "SKIPPED", StandardError = "CLI executable not found" };
+ }
+
+ var processInfo = new ProcessStartInfo
+ {
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ if (_executablePath == "dotnet")
+ {
+ processInfo.FileName = "dotnet";
+ var projectPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "src", "DotNetApiDiff", "DotNetApiDiff.csproj");
+ processInfo.Arguments = $"run --project \"{projectPath}\" -- {arguments}";
+ }
+ else
+ {
+ processInfo.FileName = _executablePath;
+ processInfo.Arguments = arguments;
+ }
+
+ var output = new StringBuilder();
+ var error = new StringBuilder();
+
+ using var process = new Process { StartInfo = processInfo };
+
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (e.Data != null)
+ {
+ output.AppendLine(e.Data);
+ _output.WriteLine($"STDOUT: {e.Data}");
+ }
+ };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (e.Data != null)
+ {
+ error.AppendLine(e.Data);
+ _output.WriteLine($"STDERR: {e.Data}");
+ }
+ };
+
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ // Set a reasonable timeout
+ bool exited = process.WaitForExit(30000); // 30 seconds
+
+ if (!exited)
+ {
+ process.Kill();
+ throw new TimeoutException("Process did not exit within the expected time");
+ }
+
+ var result = new ProcessResult
+ {
+ ExitCode = process.ExitCode,
+ StandardOutput = output.ToString(),
+ StandardError = error.ToString()
+ };
+
+ _output.WriteLine($"Process exited with code: {result.ExitCode}");
+
+ if (expectedExitCode >= 0)
+ {
+ Assert.Equal(expectedExitCode, result.ExitCode);
+ }
+
+ return result;
+ }
+
+ [Fact]
+ public void CliWorkflow_WithValidAssemblies_ShouldSucceed()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if test assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --output console";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.True(result.ExitCode >= 0, $"CLI should execute successfully. Exit code: {result.ExitCode}");
+ Assert.False(string.IsNullOrEmpty(result.StandardOutput), "Should produce output");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithConfigFile_ShouldApplyConfiguration()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+ var configFile = Path.Combine(_testDataPath, "config-lenient-changes.json");
+
+ // Skip test if files don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly) || !File.Exists(configFile))
+ {
+ _output.WriteLine("Skipping test - required files not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --config \"{configFile}\" --output json";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.True(result.ExitCode >= 0, $"CLI should execute successfully with config. Exit code: {result.ExitCode}");
+ Assert.False(string.IsNullOrEmpty(result.StandardOutput), "Should produce JSON output");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithNonExistentSourceAssembly_ShouldFail()
+ {
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "non-existent.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if target assembly doesn't exist
+ if (!File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - target assembly not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\"";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.True(result.StandardError.Contains("not found") || result.StandardOutput.Contains("not found"),
+ "Should indicate file not found");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithNonExistentTargetAssembly_ShouldFail()
+ {
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "non-existent.dll");
+
+ // Skip test if source assembly doesn't exist
+ if (!File.Exists(sourceAssembly))
+ {
+ _output.WriteLine("Skipping test - source assembly not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\"";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.True(result.StandardError.Contains("not found") || result.StandardOutput.Contains("not found"),
+ "Should indicate file not found");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithNonExistentConfigFile_ShouldFail()
+ {
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+ var configFile = Path.Combine(_testDataPath, "non-existent-config.json");
+
+ // Skip test if assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --config \"{configFile}\"";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.True(result.StandardError.Contains("not found") || result.StandardOutput.Contains("not found"),
+ "Should indicate config file not found");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithMalformedConfigFile_ShouldFail()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+ var configFile = Path.Combine(_testDataPath, "config-malformed.json");
+
+ // Skip test if files don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly) || !File.Exists(configFile))
+ {
+ _output.WriteLine("Skipping test - required files not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --config \"{configFile}\"";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.True(result.StandardError.Contains("JSON") || result.StandardOutput.Contains("JSON") ||
+ result.StandardError.Contains("configuration") || result.StandardOutput.Contains("configuration"),
+ "Should indicate JSON/configuration error");
+ }
+
+ [Theory]
+ [InlineData("console")]
+ [InlineData("json")]
+ [InlineData("markdown")]
+ public void CliWorkflow_WithDifferentOutputFormats_ShouldSucceed(string outputFormat)
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --output {outputFormat}";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.True(result.ExitCode >= 0, $"CLI should succeed with {outputFormat} format. Exit code: {result.ExitCode}");
+ Assert.False(string.IsNullOrEmpty(result.StandardOutput), $"Should produce {outputFormat} output");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithInvalidOutputFormat_ShouldFail()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --output invalid_format";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.True(result.StandardError.Contains("Invalid output format") || result.StandardOutput.Contains("Invalid output format"),
+ "Should indicate invalid output format");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithNamespaceFiltering_ShouldApplyFilters()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --filter System.Text --output console";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.True(result.ExitCode >= 0, $"CLI should succeed with namespace filtering. Exit code: {result.ExitCode}");
+ Assert.False(string.IsNullOrEmpty(result.StandardOutput), "Should produce filtered output");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithVerboseOutput_ShouldProduceDetailedLogs()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --verbose --output console";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.True(result.ExitCode >= 0, $"CLI should succeed with verbose output. Exit code: {result.ExitCode}");
+ Assert.False(string.IsNullOrEmpty(result.StandardOutput), "Should produce verbose output");
+ }
+
+ [Fact]
+ public void CliWorkflow_WithNoColorOption_ShouldDisableColors()
+ {
+ // Skip test if executable/project not found
+ if (_executablePath == null)
+ {
+ _output.WriteLine("Skipping test - CLI executable or project file not found");
+ return;
+ }
+
+ // Arrange
+ var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll");
+ var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll");
+
+ // Skip test if assemblies don't exist
+ if (!File.Exists(sourceAssembly) || !File.Exists(targetAssembly))
+ {
+ _output.WriteLine("Skipping test - test assemblies not found");
+ return;
+ }
+
+ var arguments = $"\"{sourceAssembly}\" \"{targetAssembly}\" --no-color --output console";
+
+ // Act
+ var result = RunCliCommand(arguments);
+
+ // Assert
+ Assert.True(result.ExitCode >= 0, $"CLI should succeed with no-color option. Exit code: {result.ExitCode}");
+ Assert.False(string.IsNullOrEmpty(result.StandardOutput), "Should produce output without colors");
+ }
+
+ public void Dispose()
+ {
+ // Clean up temporary files
+ if (Directory.Exists(_tempOutputPath))
+ {
+ try
+ {
+ Directory.Delete(_tempOutputPath, true);
+ }
+ catch
+ {
+ // Ignore cleanup errors in tests
+ }
+ }
+ }
+
+ private class ProcessResult
+ {
+ public int ExitCode { get; set; }
+ public string StandardOutput { get; set; } = string.Empty;
+ public string StandardError { get; set; } = string.Empty;
+ }
+}
diff --git a/tests/DotNetApiDiff.Tests/Integration/ConfigurationWorkflowTests.cs b/tests/DotNetApiDiff.Tests/Integration/ConfigurationWorkflowTests.cs
new file mode 100644
index 0000000..4ff3f3d
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/Integration/ConfigurationWorkflowTests.cs
@@ -0,0 +1,319 @@
+// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
+using DotNetApiDiff.Models.Configuration;
+using DotNetApiDiff.Models;
+using System.IO;
+using System.Text.Json;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace DotNetApiDiff.Tests.Integration;
+
+///
+/// Integration tests for configuration file workflows
+///
+public class ConfigurationWorkflowTests : IDisposable
+{
+ private readonly ITestOutputHelper _output;
+ private readonly string _testDataPath;
+ private readonly string _tempConfigPath;
+
+ public ConfigurationWorkflowTests(ITestOutputHelper output)
+ {
+ _output = output;
+ _testDataPath = Path.Combine(Directory.GetCurrentDirectory(), "TestData");
+ _tempConfigPath = Path.Combine(Path.GetTempPath(), "DotNetApiDiff.ConfigTests", Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempConfigPath);
+ }
+
+ [Fact]
+ public void LoadConfiguration_WithValidStrictConfig_ShouldLoadCorrectly()
+ {
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, "config-strict-breaking-changes.json");
+
+ // Act
+ var config = ComparisonConfiguration.LoadFromJsonFile(configPath);
+
+ // Assert
+ Assert.NotNull(config);
+ Assert.True(config.IsValid());
+ Assert.True(config.BreakingChangeRules.TreatTypeRemovalAsBreaking);
+ Assert.True(config.BreakingChangeRules.TreatMemberRemovalAsBreaking);
+ Assert.True(config.BreakingChangeRules.TreatSignatureChangeAsBreaking);
+ Assert.False(config.BreakingChangeRules.TreatAddedTypeAsBreaking);
+ Assert.False(config.BreakingChangeRules.TreatAddedMemberAsBreaking);
+ Assert.True(config.FailOnBreakingChanges);
+ Assert.Equal(ReportFormat.Console, config.OutputFormat);
+ }
+
+ [Fact]
+ public void LoadConfiguration_WithValidLenientConfig_ShouldLoadCorrectly()
+ {
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, "config-lenient-changes.json");
+
+ // Act
+ var config = ComparisonConfiguration.LoadFromJsonFile(configPath);
+
+ // Assert
+ Assert.NotNull(config);
+ Assert.True(config.IsValid());
+ Assert.False(config.BreakingChangeRules.TreatTypeRemovalAsBreaking);
+ Assert.False(config.BreakingChangeRules.TreatMemberRemovalAsBreaking);
+ Assert.False(config.BreakingChangeRules.TreatSignatureChangeAsBreaking);
+ Assert.False(config.BreakingChangeRules.TreatAddedTypeAsBreaking);
+ Assert.False(config.BreakingChangeRules.TreatAddedMemberAsBreaking);
+ Assert.False(config.FailOnBreakingChanges);
+ Assert.Equal(ReportFormat.Json, config.OutputFormat);
+ Assert.True(config.Filters.IncludeInternals);
+ Assert.True(config.Filters.IncludeCompilerGenerated);
+ Assert.True(config.Mappings.IgnoreCase);
+ Assert.Contains("OldNamespace", config.Mappings.NamespaceMappings.Keys);
+ Assert.Contains("OldClass", config.Mappings.TypeMappings.Keys);
+ }
+
+ [Fact]
+ public void LoadConfiguration_WithNamespaceFilteringConfig_ShouldLoadCorrectly()
+ {
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, "config-namespace-filtering.json");
+
+ // Act
+ var config = ComparisonConfiguration.LoadFromJsonFile(configPath);
+
+ // Assert
+ Assert.NotNull(config);
+ Assert.True(config.IsValid());
+ Assert.Contains("System.Text", config.Filters.IncludeNamespaces);
+ Assert.Contains("System.IO", config.Filters.IncludeNamespaces);
+ Assert.Contains("System.Text.Json.Serialization", config.Filters.ExcludeNamespaces);
+ Assert.Contains("System.Text.*", config.Filters.IncludeTypes);
+ Assert.Contains("System.IO.File*", config.Filters.IncludeTypes);
+ Assert.Equal(ReportFormat.Markdown, config.OutputFormat);
+ }
+
+ [Fact]
+ public void LoadConfiguration_WithNonExistentFile_ShouldThrowFileNotFoundException()
+ {
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, "non-existent-config.json");
+
+ // Act & Assert
+ Assert.Throws(() => ComparisonConfiguration.LoadFromJsonFile(configPath));
+ }
+
+ [Fact]
+ public void LoadConfiguration_WithMalformedJson_ShouldThrowJsonException()
+ {
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, "config-malformed.json");
+
+ // Act & Assert
+ Assert.Throws(() => ComparisonConfiguration.LoadFromJsonFile(configPath));
+ }
+
+ [Fact]
+ public void SaveAndLoadConfiguration_ShouldRoundTripCorrectly()
+ {
+ // Arrange
+ var originalConfig = new ComparisonConfiguration
+ {
+ OutputFormat = ReportFormat.Json,
+ FailOnBreakingChanges = false,
+ Filters = new FilterConfiguration
+ {
+ IncludeNamespaces = { "Test.Namespace" },
+ ExcludeNamespaces = { "Test.Internal" },
+ IncludeInternals = true,
+ IncludeCompilerGenerated = false
+ },
+ Mappings = new MappingConfiguration
+ {
+ NamespaceMappings = { { "Old", new List { "New" } } },
+ TypeMappings = { { "OldType", "NewType" } },
+ AutoMapSameNameTypes = false,
+ IgnoreCase = true
+ },
+ Exclusions = new ExclusionConfiguration
+ {
+ ExcludedTypes = { "ExcludedType" },
+ ExcludedMembers = { "ExcludedMember" },
+ ExcludedTypePatterns = { "*.Test*" },
+ ExcludedMemberPatterns = { "*.get_*" }
+ },
+ BreakingChangeRules = new BreakingChangeRules
+ {
+ TreatTypeRemovalAsBreaking = false,
+ TreatMemberRemovalAsBreaking = true,
+ TreatAddedTypeAsBreaking = false,
+ TreatAddedMemberAsBreaking = false,
+ TreatSignatureChangeAsBreaking = true
+ }
+ };
+
+ var tempConfigPath = Path.Combine(_tempConfigPath, "roundtrip-config.json");
+
+ // Act
+ originalConfig.SaveToJsonFile(tempConfigPath);
+ var loadedConfig = ComparisonConfiguration.LoadFromJsonFile(tempConfigPath);
+
+ // Assert
+ Assert.NotNull(loadedConfig);
+ Assert.True(loadedConfig.IsValid());
+ Assert.Equal(originalConfig.OutputFormat, loadedConfig.OutputFormat);
+ Assert.Equal(originalConfig.FailOnBreakingChanges, loadedConfig.FailOnBreakingChanges);
+ Assert.Equal(originalConfig.Filters.IncludeInternals, loadedConfig.Filters.IncludeInternals);
+ Assert.Equal(originalConfig.Filters.IncludeCompilerGenerated, loadedConfig.Filters.IncludeCompilerGenerated);
+ Assert.Equal(originalConfig.Mappings.AutoMapSameNameTypes, loadedConfig.Mappings.AutoMapSameNameTypes);
+ Assert.Equal(originalConfig.Mappings.IgnoreCase, loadedConfig.Mappings.IgnoreCase);
+ Assert.Equal(originalConfig.BreakingChangeRules.TreatTypeRemovalAsBreaking, loadedConfig.BreakingChangeRules.TreatTypeRemovalAsBreaking);
+ Assert.Equal(originalConfig.BreakingChangeRules.TreatMemberRemovalAsBreaking, loadedConfig.BreakingChangeRules.TreatMemberRemovalAsBreaking);
+ Assert.Equal(originalConfig.BreakingChangeRules.TreatSignatureChangeAsBreaking, loadedConfig.BreakingChangeRules.TreatSignatureChangeAsBreaking);
+
+ // Check collections
+ Assert.Contains("Test.Namespace", loadedConfig.Filters.IncludeNamespaces);
+ Assert.Contains("Test.Internal", loadedConfig.Filters.ExcludeNamespaces);
+ Assert.Contains("Old", loadedConfig.Mappings.NamespaceMappings.Keys);
+ Assert.Contains("OldType", loadedConfig.Mappings.TypeMappings.Keys);
+ Assert.Contains("ExcludedType", loadedConfig.Exclusions.ExcludedTypes);
+ Assert.Contains("ExcludedMember", loadedConfig.Exclusions.ExcludedMembers);
+ Assert.Contains("*.Test*", loadedConfig.Exclusions.ExcludedTypePatterns);
+ Assert.Contains("*.get_*", loadedConfig.Exclusions.ExcludedMemberPatterns);
+ }
+
+ [Fact]
+ public void CreateDefaultConfiguration_ShouldBeValid()
+ {
+ // Act
+ var config = ComparisonConfiguration.CreateDefault();
+
+ // Assert
+ Assert.NotNull(config);
+ Assert.True(config.IsValid());
+ Assert.Equal(ReportFormat.Console, config.OutputFormat);
+ Assert.True(config.FailOnBreakingChanges);
+ Assert.NotNull(config.Filters);
+ Assert.NotNull(config.Mappings);
+ Assert.NotNull(config.Exclusions);
+ Assert.NotNull(config.BreakingChangeRules);
+ }
+
+ [Theory]
+ [InlineData("config-strict-breaking-changes.json")]
+ [InlineData("config-lenient-changes.json")]
+ [InlineData("config-namespace-filtering.json")]
+ [InlineData("sample-config.json")]
+ public void LoadConfiguration_WithAllValidConfigs_ShouldSucceed(string configFileName)
+ {
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, configFileName);
+
+ // Skip test if config file doesn't exist
+ if (!File.Exists(configPath))
+ {
+ _output.WriteLine($"Skipping test - config file not found: {configFileName}");
+ return;
+ }
+
+ // Act
+ var config = ComparisonConfiguration.LoadFromJsonFile(configPath);
+
+ // Assert
+ Assert.NotNull(config);
+ Assert.True(config.IsValid(), $"Configuration {configFileName} should be valid");
+
+ // Verify all required properties are set
+ Assert.NotNull(config.Filters);
+ Assert.NotNull(config.Mappings);
+ Assert.NotNull(config.Exclusions);
+ Assert.NotNull(config.BreakingChangeRules);
+ Assert.True(Enum.IsDefined(typeof(ReportFormat), config.OutputFormat));
+ }
+
+ [Fact]
+ public void ConfigurationValidation_WithInvalidOutputFormat_ShouldFailValidation()
+ {
+ // Arrange
+ var config = ComparisonConfiguration.CreateDefault();
+ config.OutputFormat = (ReportFormat)999; // Invalid enum value
+
+ // Act & Assert
+ Assert.False(config.IsValid());
+ }
+
+ [Fact]
+ public void ConfigurationSerialization_ShouldProduceReadableJson()
+ {
+ // Arrange
+ var config = ComparisonConfiguration.CreateDefault();
+ config.Filters.IncludeNamespaces.Add("Test.Namespace");
+ config.Mappings.TypeMappings.Add("OldType", "NewType");
+
+ var tempConfigPath = Path.Combine(_tempConfigPath, "readable-config.json");
+
+ // Act
+ config.SaveToJsonFile(tempConfigPath);
+ var jsonContent = File.ReadAllText(tempConfigPath);
+
+ // Assert
+ Assert.False(string.IsNullOrEmpty(jsonContent));
+ Assert.Contains("filters", jsonContent);
+ Assert.Contains("mappings", jsonContent);
+ Assert.Contains("exclusions", jsonContent);
+ Assert.Contains("breakingChangeRules", jsonContent);
+ Assert.Contains("Test.Namespace", jsonContent);
+ Assert.Contains("OldType", jsonContent);
+ Assert.Contains("NewType", jsonContent);
+
+ // Verify it's properly formatted (indented)
+ Assert.Contains(" ", jsonContent); // Should contain indentation
+ Assert.Contains("\n", jsonContent); // Should contain line breaks
+ }
+
+ [Fact]
+ public void ConfigurationMerging_WithCommandLineOverrides_ShouldWork()
+ {
+ // This test verifies that configuration can be loaded and then modified
+ // to simulate command-line overrides
+
+ // Arrange
+ var configPath = Path.Combine(_testDataPath, "sample-config.json");
+
+ // Skip test if config file doesn't exist
+ if (!File.Exists(configPath))
+ {
+ _output.WriteLine("Skipping test - sample config file not found");
+ return;
+ }
+
+ // Act
+ var config = ComparisonConfiguration.LoadFromJsonFile(configPath);
+
+ // Simulate command-line overrides
+ config.Filters.IncludeNamespaces.Add("CommandLine.Override");
+ config.Filters.IncludeInternals = true;
+ config.OutputFormat = ReportFormat.Json;
+
+ // Assert
+ Assert.True(config.IsValid());
+ Assert.Contains("CommandLine.Override", config.Filters.IncludeNamespaces);
+ Assert.True(config.Filters.IncludeInternals);
+ Assert.Equal(ReportFormat.Json, config.OutputFormat);
+ }
+
+ public void Dispose()
+ {
+ // Clean up temporary files
+ if (Directory.Exists(_tempConfigPath))
+ {
+ try
+ {
+ Directory.Delete(_tempConfigPath, true);
+ }
+ catch
+ {
+ // Ignore cleanup errors in tests
+ }
+ }
+ }
+}
diff --git a/tests/DotNetApiDiff.Tests/Integration/WorkflowTests.cs b/tests/DotNetApiDiff.Tests/Integration/WorkflowTests.cs
new file mode 100644
index 0000000..e69de29
diff --git a/tests/DotNetApiDiff.Tests/TestData/config-invalid-format.json b/tests/DotNetApiDiff.Tests/TestData/config-invalid-format.json
new file mode 100644
index 0000000..204cd14
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/TestData/config-invalid-format.json
@@ -0,0 +1,32 @@
+{
+ "filters": {
+ "includeNamespaces": [],
+ "excludeNamespaces": [],
+ "includeTypes": [],
+ "excludeTypes": [],
+ "includeInternals": false,
+ "includeCompilerGenerated": false
+ },
+ "mappings": {
+ "namespaceMappings": {},
+ "typeMappings": {},
+ "autoMapSameNameTypes": true,
+ "ignoreCase": false
+ },
+ "exclusions": {
+ "excludedTypes": [],
+ "excludedMembers": [],
+ "excludedTypePatterns": [],
+ "excludedMemberPatterns": []
+ },
+ "breakingChangeRules": {
+ "treatTypeRemovalAsBreaking": true,
+ "treatMemberRemovalAsBreaking": true,
+ "treatAddedTypeAsBreaking": false,
+ "treatAddedMemberAsBreaking": false,
+ "treatSignatureChangeAsBreaking": true
+ },
+ "outputFormat": "invalid_format",
+ "outputPath": null,
+ "failOnBreakingChanges": true
+}
diff --git a/tests/DotNetApiDiff.Tests/TestData/config-lenient-changes.json b/tests/DotNetApiDiff.Tests/TestData/config-lenient-changes.json
new file mode 100644
index 0000000..c3ceec2
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/TestData/config-lenient-changes.json
@@ -0,0 +1,41 @@
+{
+ "filters": {
+ "includeNamespaces": [],
+ "excludeNamespaces": [],
+ "includeTypes": [],
+ "excludeTypes": [],
+ "includeInternals": true,
+ "includeCompilerGenerated": true
+ },
+ "mappings": {
+ "namespaceMappings": {
+ "OldNamespace": ["NewNamespace"],
+ "Legacy.Api": ["Modern.Api"]
+ },
+ "typeMappings": {
+ "OldClass": "NewClass",
+ "DeprecatedType": "ReplacementType"
+ },
+ "autoMapSameNameTypes": true,
+ "ignoreCase": true
+ },
+ "exclusions": {
+ "excludedTypes": ["ObsoleteClass", "TemporaryClass"],
+ "excludedMembers": [
+ "SomeClass.ObsoleteMethod",
+ "AnotherClass.DeprecatedProperty"
+ ],
+ "excludedTypePatterns": ["*.Test*", "*.Temp*"],
+ "excludedMemberPatterns": ["*.get_Obsolete*", "*.set_Obsolete*"]
+ },
+ "breakingChangeRules": {
+ "treatTypeRemovalAsBreaking": false,
+ "treatMemberRemovalAsBreaking": false,
+ "treatAddedTypeAsBreaking": false,
+ "treatAddedMemberAsBreaking": false,
+ "treatSignatureChangeAsBreaking": false
+ },
+ "outputFormat": "json",
+ "outputPath": null,
+ "failOnBreakingChanges": false
+}
diff --git a/tests/DotNetApiDiff.Tests/TestData/config-malformed.json b/tests/DotNetApiDiff.Tests/TestData/config-malformed.json
new file mode 100644
index 0000000..af08b09
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/TestData/config-malformed.json
@@ -0,0 +1,32 @@
+{
+ "filters": {
+ "includeNamespaces": [],
+ "excludeNamespaces": [],
+ "includeTypes": [],
+ "excludeTypes": [],
+ "includeInternals": false,
+ "includeCompilerGenerated": false
+ },
+ "mappings": {
+ "namespaceMappings": {},
+ "typeMappings": {},
+ "autoMapSameNameTypes": true,
+ "ignoreCase": false
+ },
+ "exclusions": {
+ "excludedTypes": [],
+ "excludedMembers": [],
+ "excludedTypePatterns": [],
+ "excludedMemberPatterns": []
+ },
+ "breakingChangeRules": {
+ "treatTypeRemovalAsBreaking": true,
+ "treatMemberRemovalAsBreaking": true,
+ "treatAddedTypeAsBreaking": false,
+ "treatAddedMemberAsBreaking": false,
+ "treatSignatureChangeAsBreaking": true
+ },
+ "outputFormat": "console",
+ "outputPath": null,
+ "failOnBreakingChanges": true
+ // Missing closing brace to make it malformed
diff --git a/tests/DotNetApiDiff.Tests/TestData/config-namespace-filtering.json b/tests/DotNetApiDiff.Tests/TestData/config-namespace-filtering.json
new file mode 100644
index 0000000..4147f85
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/TestData/config-namespace-filtering.json
@@ -0,0 +1,32 @@
+{
+ "filters": {
+ "includeNamespaces": ["System.Text", "System.IO"],
+ "excludeNamespaces": ["System.Text.Json.Serialization"],
+ "includeTypes": ["System.Text.*", "System.IO.File*"],
+ "excludeTypes": ["*.Internal*", "*.Helper*"],
+ "includeInternals": false,
+ "includeCompilerGenerated": false
+ },
+ "mappings": {
+ "namespaceMappings": {},
+ "typeMappings": {},
+ "autoMapSameNameTypes": true,
+ "ignoreCase": false
+ },
+ "exclusions": {
+ "excludedTypes": [],
+ "excludedMembers": [],
+ "excludedTypePatterns": [],
+ "excludedMemberPatterns": []
+ },
+ "breakingChangeRules": {
+ "treatTypeRemovalAsBreaking": true,
+ "treatMemberRemovalAsBreaking": true,
+ "treatAddedTypeAsBreaking": false,
+ "treatAddedMemberAsBreaking": false,
+ "treatSignatureChangeAsBreaking": true
+ },
+ "outputFormat": "markdown",
+ "outputPath": null,
+ "failOnBreakingChanges": true
+}
diff --git a/tests/DotNetApiDiff.Tests/TestData/config-strict-breaking-changes.json b/tests/DotNetApiDiff.Tests/TestData/config-strict-breaking-changes.json
new file mode 100644
index 0000000..3f11552
--- /dev/null
+++ b/tests/DotNetApiDiff.Tests/TestData/config-strict-breaking-changes.json
@@ -0,0 +1,32 @@
+{
+ "filters": {
+ "includeNamespaces": [],
+ "excludeNamespaces": ["System.Diagnostics", "System.Internal"],
+ "includeTypes": [],
+ "excludeTypes": ["*.Internal*", "*.Helper*"],
+ "includeInternals": false,
+ "includeCompilerGenerated": false
+ },
+ "mappings": {
+ "namespaceMappings": {},
+ "typeMappings": {},
+ "autoMapSameNameTypes": true,
+ "ignoreCase": false
+ },
+ "exclusions": {
+ "excludedTypes": [],
+ "excludedMembers": [],
+ "excludedTypePatterns": ["*.Internal*"],
+ "excludedMemberPatterns": []
+ },
+ "breakingChangeRules": {
+ "treatTypeRemovalAsBreaking": true,
+ "treatMemberRemovalAsBreaking": true,
+ "treatAddedTypeAsBreaking": false,
+ "treatAddedMemberAsBreaking": false,
+ "treatSignatureChangeAsBreaking": true
+ },
+ "outputFormat": "console",
+ "outputPath": null,
+ "failOnBreakingChanges": true
+}