From e2110be4dd016152f48b520f13d3cec8b7ac245b Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Thu, 31 Jul 2025 08:57:57 -0400 Subject: [PATCH 1/4] Fix configuration precedence for OutputFormat and OutputPath Fixes #38: Configuration values for OutputFormat and OutputPath are now properly used when included in configuration files. Changes: - Modified CompareCommand.cs to fix configuration precedence logic - Changed OutputFormat property to nullable to detect explicit user input - Fixed JSON enum serialization in ComparisonConfiguration.cs - Added comprehensive unit tests for JSON enum parsing - Added integration tests for configuration precedence scenarios The fix ensures: - Config file values are used when no CLI override is provided - Explicit CLI arguments properly override config values - JSON enum parsing works with both 'Html' and 'html' formats - Round-trip serialization maintains enum values correctly All 425 tests passing with no regressions. --- src/DotNetApiDiff/Commands/CompareCommand.cs | 72 +++--- .../Configuration/ComparisonConfiguration.cs | 5 +- .../Integration/CliWorkflowTests.cs | 242 ++++++++++++++++++ .../config-output-format-and-path.json | 32 +++ .../Unit/JsonEnumSerializationTests.cs | 133 ++++++++++ 5 files changed, 443 insertions(+), 41 deletions(-) create mode 100644 tests/DotNetApiDiff.Tests/TestData/config-output-format-and-path.json create mode 100644 tests/DotNetApiDiff.Tests/Unit/JsonEnumSerializationTests.cs diff --git a/src/DotNetApiDiff/Commands/CompareCommand.cs b/src/DotNetApiDiff/Commands/CompareCommand.cs index 0a420a1..8ba86c3 100644 --- a/src/DotNetApiDiff/Commands/CompareCommand.cs +++ b/src/DotNetApiDiff/Commands/CompareCommand.cs @@ -32,8 +32,7 @@ public class CompareCommandSettings : CommandSettings [CommandOption("-o|--output ")] [Description("Output format (console, json, html, markdown)")] - [DefaultValue("console")] - public string OutputFormat { get; set; } = "console"; + public string? OutputFormat { get; set; } [CommandOption("-p|--output-file ")] [Description("Output file path (required for json, html, markdown formats)")] @@ -137,27 +136,30 @@ public override ValidationResult Validate([NotNull] CommandContext context, [Not return ValidationResult.Error($"Configuration file not found: {settings.ConfigFile}"); } - // Validate output format - string format = settings.OutputFormat.ToLowerInvariant(); - if (format != "console" && format != "json" && format != "html" && format != "markdown") + // Validate output format if provided + if (!string.IsNullOrEmpty(settings.OutputFormat)) { - return ValidationResult.Error($"Invalid output format: {settings.OutputFormat}. Valid formats are: console, json, html, markdown"); - } - - // Validate output file requirements - if (format == "html") - { - // HTML format requires an output file - if (string.IsNullOrEmpty(settings.OutputFile)) + string format = settings.OutputFormat.ToLowerInvariant(); + if (format != "console" && format != "json" && format != "html" && format != "markdown") { - return ValidationResult.Error($"Output file is required for {settings.OutputFormat} format. Use --output-file to specify the output file path."); + return ValidationResult.Error($"Invalid output format: {settings.OutputFormat}. Valid formats are: console, json, html, markdown"); } - // Validate output directory exists - var outputDir = Path.GetDirectoryName(settings.OutputFile); - if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) + // Validate output file requirements + if (format == "html") { - return ValidationResult.Error($"Output directory does not exist: {outputDir}"); + // HTML format requires an output file + if (string.IsNullOrEmpty(settings.OutputFile)) + { + return ValidationResult.Error($"Output file is required for {settings.OutputFormat} format. Use --output-file to specify the output file path."); + } + + // Validate output directory exists + var outputDir = Path.GetDirectoryName(settings.OutputFile); + if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) + { + return ValidationResult.Error($"Output directory does not exist: {outputDir}"); + } } } else if (!string.IsNullOrEmpty(settings.OutputFile)) @@ -369,8 +371,8 @@ private int ExecuteWithConfiguredServices(CompareCommandSettings settings, Compa // Use the single CompareAssemblies method - configuration is now injected into dependencies comparisonResult = apiComparer.CompareAssemblies(sourceAssembly, targetAssembly); - // Update configuration with actual command-line values used for this comparison - if (Enum.TryParse(settings.OutputFormat, true, out var outputFormat)) + // Update configuration with actual command-line values ONLY if explicitly provided by user + if (!string.IsNullOrEmpty(settings.OutputFormat) && Enum.TryParse(settings.OutputFormat, true, out var outputFormat)) { comparisonResult.Configuration.OutputFormat = outputFormat; } @@ -395,26 +397,20 @@ private int ExecuteWithConfiguredServices(CompareCommandSettings settings, Compa // Generate report using (_logger.BeginScope("Report generation")) { - _logger.LogInformation("Generating {Format} report", settings.OutputFormat); - var reportGenerator = serviceProvider.GetRequiredService(); + // Use the configuration from the comparison result, which now has the correct precedence applied + var effectiveFormat = comparisonResult.Configuration.OutputFormat; + var effectiveOutputFile = comparisonResult.Configuration.OutputPath; - // Convert string format to ReportFormat enum - ReportFormat format = settings.OutputFormat.ToLowerInvariant() switch - { - "json" => ReportFormat.Json, - "xml" => ReportFormat.Xml, - "html" => ReportFormat.Html, - "markdown" => ReportFormat.Markdown, - _ => ReportFormat.Console - }; + _logger.LogInformation("Generating {Format} report", effectiveFormat); + var reportGenerator = serviceProvider.GetRequiredService(); string report; try { - if (string.IsNullOrEmpty(settings.OutputFile)) + if (string.IsNullOrEmpty(effectiveOutputFile)) { // No output file specified - output to console regardless of format - report = reportGenerator.GenerateReport(comparisonResult, format); + report = reportGenerator.GenerateReport(comparisonResult, effectiveFormat); // Output the formatted report to the console // Use Console.Write to avoid format string interpretation issues @@ -423,20 +419,18 @@ private int ExecuteWithConfiguredServices(CompareCommandSettings settings, Compa else { // Output file specified - save to the specified file - reportGenerator.SaveReportAsync(comparisonResult, format, settings.OutputFile).GetAwaiter().GetResult(); - _logger.LogInformation("Report saved to {OutputFile}", settings.OutputFile); + reportGenerator.SaveReportAsync(comparisonResult, effectiveFormat, effectiveOutputFile).GetAwaiter().GetResult(); + _logger.LogInformation("Report saved to {OutputFile}", effectiveOutputFile); } } catch (Exception ex) { - _logger.LogError(ex, "Error generating {Format} report", format); + _logger.LogError(ex, "Error generating {Format} report", effectiveFormat); AnsiConsole.MarkupLine($"[red]Error generating report:[/] {ex.Message}"); return _exitCodeManager.GetExitCodeForException(ex); } - } - - // Use the ExitCodeManager to determine the appropriate exit code + } // Use the ExitCodeManager to determine the appropriate exit code int exitCode = _exitCodeManager.GetExitCode(comparison); if (comparison.HasBreakingChanges) diff --git a/src/DotNetApiDiff/Models/Configuration/ComparisonConfiguration.cs b/src/DotNetApiDiff/Models/Configuration/ComparisonConfiguration.cs index 69b69d3..f6743d4 100644 --- a/src/DotNetApiDiff/Models/Configuration/ComparisonConfiguration.cs +++ b/src/DotNetApiDiff/Models/Configuration/ComparisonConfiguration.cs @@ -90,7 +90,7 @@ public static ComparisonConfiguration LoadFromJsonFile(string path) PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter(null, false) } }; var config = JsonSerializer.Deserialize(json, options); @@ -129,7 +129,8 @@ public void SaveToJsonFile(string path) var options = new JsonSerializerOptions { WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(null, false) } }; var json = JsonSerializer.Serialize(this, options); diff --git a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs index 4ecadec..2c4247a 100644 --- a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs +++ b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using System.Text.Json; using Xunit; using Xunit.Abstractions; @@ -167,6 +168,82 @@ public void CliWorkflow_WithConfigFile_ShouldApplyConfiguration() Assert.False(string.IsNullOrEmpty(result.StandardOutput), "Should produce JSON output"); } + [Fact] + public void CliWorkflow_WithConfigFileOutputSettings_ShouldUseConfigurationValues() + { + // Arrange + var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll"); + var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll"); + var configFile = Path.Combine(_testDataPath, "config-output-format-and-path.json"); + var expectedOutputFile = Path.Combine(_tempOutputPath, "comparison-report.html"); + + // Fail test if prerequisites are not met + Assert.NotNull(_executablePath); + Assert.True(File.Exists(sourceAssembly), $"Source test assembly not found: {sourceAssembly}"); + Assert.True(File.Exists(targetAssembly), $"Target test assembly not found: {targetAssembly}"); + Assert.True(File.Exists(configFile), $"Config file not found: {configFile}"); + + // Update the config file to use our temp output path + var configContent = File.ReadAllText(configFile); + var configWithTempPath = configContent.Replace("comparison-report.html", expectedOutputFile); + var tempConfigFile = Path.Combine(_tempOutputPath, "temp-config.json"); + File.WriteAllText(tempConfigFile, configWithTempPath); + + // Execute with config file but NO command line output options + var arguments = $"compare \"{sourceAssembly}\" \"{targetAssembly}\" --config \"{tempConfigFile}\""; + + // Act + var result = RunCliCommand(arguments); + + // Assert + // Exit code 2 is expected when there are breaking changes, which is normal for our test assemblies + Assert.True(result.ExitCode == 0 || result.ExitCode == 2, $"CLI should execute successfully with config output settings. Exit code: {result.ExitCode}"); + + // The output file should have been created (this tests that OutputPath from config was used) + Assert.True(File.Exists(expectedOutputFile), $"Output file should have been created at: {expectedOutputFile}"); + + // The output file should contain HTML content (this tests that OutputFormat from config was used) + var outputContent = File.ReadAllText(expectedOutputFile); + Assert.Contains("", outputContent, StringComparison.OrdinalIgnoreCase); + Assert.Contains("API Comparison Report", outputContent); + } + + [Fact] + public void CliWorkflow_CommandLineOverridesConfigOutputSettings_ShouldUseCommandLineValues() + { + // Arrange + var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll"); + var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll"); + var configFile = Path.Combine(_testDataPath, "config-output-format-and-path.json"); + var commandLineOutputFile = Path.Combine(_tempOutputPath, "command-line-output.json"); + + // Fail test if prerequisites are not met + Assert.NotNull(_executablePath); + Assert.True(File.Exists(sourceAssembly), $"Source test assembly not found: {sourceAssembly}"); + Assert.True(File.Exists(targetAssembly), $"Target test assembly not found: {targetAssembly}"); + Assert.True(File.Exists(configFile), $"Config file not found: {configFile}"); + + // Execute with config file AND command line output options (command line should override config) + var arguments = $"compare \"{sourceAssembly}\" \"{targetAssembly}\" --config \"{configFile}\" --output json --output-file \"{commandLineOutputFile}\""; + + // Act + var result = RunCliCommand(arguments); + + // Assert + // Exit code 2 is expected when there are breaking changes, which is normal for our test assemblies + Assert.True(result.ExitCode == 0 || result.ExitCode == 2, $"CLI should execute successfully with command line override. Exit code: {result.ExitCode}"); + + // The command line output file should have been created (not the config one) + Assert.True(File.Exists(commandLineOutputFile), $"Command line output file should have been created at: {commandLineOutputFile}"); + + // The output file should contain JSON content (command line format should override config HTML format) + var outputContent = File.ReadAllText(commandLineOutputFile); + Assert.Contains("{", outputContent); + Assert.Contains("}", outputContent); + Assert.DoesNotContain("", outputContent, StringComparison.OrdinalIgnoreCase); + Assert.Contains("API Comparison Report", outputContent); + + // Verify the logs show the correct format was read from config and used + var combinedOutput = result.StandardOutput + result.StandardError; + Assert.Contains("Configuration loaded successfully", combinedOutput); + Assert.Contains("Generating Html report", combinedOutput); + Assert.Contains("Report saved successfully", combinedOutput); + } + + [Fact] + public void CliWorkflow_CommandLineOverridesConfig_ShouldUseExplicitCommandLineFormat() + { + // Arrange + var sourceAssembly = Path.Combine(_testDataPath, "TestAssemblyV1.dll"); + var targetAssembly = Path.Combine(_testDataPath, "TestAssemblyV2.dll"); + var outputFile = Path.Combine(_tempOutputPath, "override-test-output.json"); + + // Fail test if prerequisites are not met + Assert.NotNull(_executablePath); + Assert.True(File.Exists(sourceAssembly), $"Source test assembly not found: {sourceAssembly}"); + Assert.True(File.Exists(targetAssembly), $"Target test assembly not found: {targetAssembly}"); + + // Create config with Html format + var testConfigJson = $$""" + { + "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": "Html", + "outputPath": "{{outputFile}}", + "failOnBreakingChanges": false + } + """; + + var testConfigFile = Path.Combine(_tempOutputPath, "override-test-config.json"); + File.WriteAllText(testConfigFile, testConfigJson); + + // Execute with config file that has Html format, but override with Json format on command line + var arguments = $"compare \"{sourceAssembly}\" \"{targetAssembly}\" --config \"{testConfigFile}\" --output json"; + + // Act + var result = RunCliCommand(arguments); + + // Assert + // Exit code 2 is expected when there are breaking changes + Assert.True(result.ExitCode == 0 || result.ExitCode == 2, $"CLI should execute successfully. Exit code: {result.ExitCode}"); + + // Verify the output file was created + Assert.True(File.Exists(outputFile), $"Output file should have been created at: {outputFile}"); + + // Verify JSON content was generated (command line Json overrode config Html) + var outputContent = File.ReadAllText(outputFile); + try + { + JsonDocument.Parse(outputContent); // Should not throw if valid JSON + } + catch (JsonException) + { + Assert.Fail("Output should be valid JSON when Json format is specified"); + } + + // Verify the logs show Json format was used (command line override) + var combinedOutput = result.StandardOutput + result.StandardError; + Assert.Contains("Configuration loaded successfully", combinedOutput); + Assert.Contains("Generating Json report", combinedOutput); + Assert.Contains("Report saved successfully", combinedOutput); + } + public void Dispose() { // Clean up temporary files diff --git a/tests/DotNetApiDiff.Tests/TestData/config-output-format-and-path.json b/tests/DotNetApiDiff.Tests/TestData/config-output-format-and-path.json new file mode 100644 index 0000000..bcdcbee --- /dev/null +++ b/tests/DotNetApiDiff.Tests/TestData/config-output-format-and-path.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": "html", + "outputPath": "comparison-report.html", + "failOnBreakingChanges": true +} diff --git a/tests/DotNetApiDiff.Tests/Unit/JsonEnumSerializationTests.cs b/tests/DotNetApiDiff.Tests/Unit/JsonEnumSerializationTests.cs new file mode 100644 index 0000000..19091f3 --- /dev/null +++ b/tests/DotNetApiDiff.Tests/Unit/JsonEnumSerializationTests.cs @@ -0,0 +1,133 @@ +// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT +using System.Text.Json; +using System.Text.Json.Serialization; +using DotNetApiDiff.Models; +using DotNetApiDiff.Models.Configuration; +using Xunit; + +namespace DotNetApiDiff.Tests.Unit; + +/// +/// Tests for JSON enum serialization/deserialization +/// +public class JsonEnumSerializationTests +{ + [Fact] + public void ComparisonConfiguration_JsonDeserialization_ShouldParseEnumCorrectly() + { + // Arrange + var json = """ + { + "outputFormat": "Html", + "outputPath": "test.html" + } + """; + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + Converters = { new JsonStringEnumConverter(null, false) } + }; + + // Act + var config = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(config); + Assert.Equal(ReportFormat.Html, config.OutputFormat); + Assert.Equal("test.html", config.OutputPath); + } + + [Theory] + [InlineData("\"Html\"", ReportFormat.Html)] + [InlineData("\"html\"", ReportFormat.Html)] + [InlineData("\"HTML\"", ReportFormat.Html)] + [InlineData("\"Console\"", ReportFormat.Console)] + [InlineData("\"console\"", ReportFormat.Console)] + [InlineData("\"Json\"", ReportFormat.Json)] + [InlineData("\"json\"", ReportFormat.Json)] + public void ReportFormat_JsonDeserialization_ShouldHandleCaseVariations(string jsonValue, ReportFormat expectedFormat) + { + // Arrange + var json = $"{{\"outputFormat\": {jsonValue}}}"; + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + Converters = { new JsonStringEnumConverter(null, false) } + }; + + // Act + var config = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(config); + Assert.Equal(expectedFormat, config.OutputFormat); + } + + [Fact] + public void ComparisonConfiguration_LoadFromJsonFile_ShouldWorkWithTestData() + { + // Arrange + var tempFile = Path.GetTempFileName(); + var json = """ + { + "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": "Html", + "outputPath": "comparison-report.html", + "failOnBreakingChanges": false + } + """; + + try + { + File.WriteAllText(tempFile, json); + + // Act + var config = ComparisonConfiguration.LoadFromJsonFile(tempFile); + + // Assert + Assert.NotNull(config); + Assert.Equal(ReportFormat.Html, config.OutputFormat); + Assert.Equal("comparison-report.html", config.OutputPath); + Assert.False(config.FailOnBreakingChanges); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } +} From 0196d63155d1b011b8b2aaac1fa59231e21b3454 Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Thu, 31 Jul 2025 09:02:04 -0400 Subject: [PATCH 2/4] chore: resolve stylecop issue Signed-off-by: jbrinkman --- src/DotNetApiDiff/Commands/CompareCommand.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DotNetApiDiff/Commands/CompareCommand.cs b/src/DotNetApiDiff/Commands/CompareCommand.cs index 8ba86c3..bae5e93 100644 --- a/src/DotNetApiDiff/Commands/CompareCommand.cs +++ b/src/DotNetApiDiff/Commands/CompareCommand.cs @@ -430,7 +430,9 @@ private int ExecuteWithConfiguredServices(CompareCommandSettings settings, Compa return _exitCodeManager.GetExitCodeForException(ex); } - } // Use the ExitCodeManager to determine the appropriate exit code + } + + // Use the ExitCodeManager to determine the appropriate exit code int exitCode = _exitCodeManager.GetExitCode(comparison); if (comparison.HasBreakingChanges) From ce602cdfebdb1108bdfeaabf4420e92edcc796df Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Thu, 31 Jul 2025 09:10:08 -0400 Subject: [PATCH 3/4] Fix Windows CI: Escape backslashes in JSON path interpolation Fixes Windows CI failure where file paths with backslashes in JSON strings were not properly escaped, causing JSON parsing errors. Changes: - Escape backslashes in outputPath interpolation for integration tests - Use .Replace('\', '\\') to convert single backslashes to double backslashes - Fixes both CliWorkflow tests that create JSON config files with paths This resolves the System.Text.Json.JsonException on Windows CI builds while maintaining compatibility with Unix-style paths on macOS/Linux. --- tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs index 2c4247a..53c22bf 100644 --- a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs +++ b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs @@ -515,7 +515,7 @@ public void CliWorkflow_ConfigurationJsonEnumParsing_ShouldParseOutputFormatCorr "treatSignatureChangeAsBreaking": true }, "outputFormat": "Html", - "outputPath": "{{outputFile}}", + "outputPath": "{{outputFile.Replace("\\", "\\\\")}}", "failOnBreakingChanges": false } """; @@ -597,7 +597,7 @@ public void CliWorkflow_CommandLineOverridesConfig_ShouldUseExplicitCommandLineF "treatSignatureChangeAsBreaking": true }, "outputFormat": "Html", - "outputPath": "{{outputFile}}", + "outputPath": "{{outputFile.Replace("\\", "\\\\")}}", "failOnBreakingChanges": false } """; From 0c0cd2beb057b96de5141d5017e893d0d35e96e0 Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Thu, 31 Jul 2025 09:25:13 -0400 Subject: [PATCH 4/4] Fix additional Windows CI issue: Escape file paths in JSON replacement - Fixed CliWorkflow_WithConfigFileOutputSettings_ShouldUseConfigurationValues test - Added proper JSON escaping when replacing paths in config file content - Use .Replace('\', '\\') to escape backslashes for Windows paths - Resolves JSON parsing error: 'U' is an invalid escapable character This complements the previous Windows CI fix and ensures all integration tests work correctly on Windows by properly escaping file paths when they are inserted into JSON configuration files. --- tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs index 53c22bf..1a5edfd 100644 --- a/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs +++ b/tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs @@ -185,7 +185,9 @@ public void CliWorkflow_WithConfigFileOutputSettings_ShouldUseConfigurationValue // Update the config file to use our temp output path var configContent = File.ReadAllText(configFile); - var configWithTempPath = configContent.Replace("comparison-report.html", expectedOutputFile); + // Escape backslashes for JSON format on Windows + var escapedOutputPath = expectedOutputFile.Replace("\\", "\\\\"); + var configWithTempPath = configContent.Replace("comparison-report.html", escapedOutputPath); var tempConfigFile = Path.Combine(_tempOutputPath, "temp-config.json"); File.WriteAllText(tempConfigFile, configWithTempPath);