From 61b65d9bcd4e494d4921c28658471d90ebd7821a Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Sun, 27 Jul 2025 09:58:48 -0400 Subject: [PATCH 1/3] Fix HTML report missing detail data in change sections - Replaced problematic {{ include }} directives with custom RenderChangeGroup function - Added comprehensive test coverage for HTML formatter with all change types - Fixed template context passing issue that caused empty sections - All HTML formatter tests now pass (5/5) Fixes #23 --- src/DotNetApiDiff/Commands/CompareCommand.cs | 2 +- .../Reporting/HtmlFormatterScriban.cs | 74 +++---- .../HtmlTemplates/main-layout.scriban | 2 +- .../Reporting/HtmlFormatterScribanTests.cs | 196 ++++++++++++++++++ 4 files changed, 232 insertions(+), 42 deletions(-) create mode 100644 tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs diff --git a/src/DotNetApiDiff/Commands/CompareCommand.cs b/src/DotNetApiDiff/Commands/CompareCommand.cs index 6aaccf4..56d99ea 100644 --- a/src/DotNetApiDiff/Commands/CompareCommand.cs +++ b/src/DotNetApiDiff/Commands/CompareCommand.cs @@ -35,7 +35,7 @@ public class CompareCommandSettings : CommandSettings [DefaultValue("console")] public string OutputFormat { get; set; } = "console"; - [CommandOption("--output-file ")] + [CommandOption("-p|--output-file ")] [Description("Output file path (required for json, html, markdown formats)")] public string? OutputFile { get; set; } diff --git a/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs b/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs index 17ae130..8ee331d 100644 --- a/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs +++ b/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs @@ -14,24 +14,13 @@ namespace DotNetApiDiff.Reporting; public class HtmlFormatterScriban : IReportFormatter { private readonly Template _mainTemplate; - private readonly Dictionary _partialTemplates; public HtmlFormatterScriban() { - // Initialize templates from embedded resources + // Initialize main template from embedded resources try { _mainTemplate = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("main-layout.scriban")); - - _partialTemplates = new Dictionary(); - - // Load all partial templates - _partialTemplates["change-group"] = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("change-group.scriban")); - _partialTemplates["breaking-changes"] = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("breaking-changes.scriban")); - _partialTemplates["configuration"] = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("configuration.scriban")); - _partialTemplates["config-string-list"] = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("config-string-list.scriban")); - _partialTemplates["config-mappings"] = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("config-mappings.scriban")); - _partialTemplates["config-namespace-mappings"] = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("config-namespace-mappings.scriban")); } catch (Exception ex) { @@ -52,6 +41,7 @@ public string Format(ComparisonResult result) // Add custom functions scriptObject.Import("format_boolean", new Func(FormatBooleanValue)); + scriptObject.Import("render_change_group", new Func(RenderChangeGroup)); // Prepare data for the main template var resultData = PrepareResultData(result); @@ -248,20 +238,42 @@ private object[] PrepareBreakingChangesData(IEnumerable breakingC }).ToArray(); } - private string RenderPartial(string templateName, object data) + private string FormatBooleanValue(bool value) { - if (_partialTemplates.TryGetValue(templateName, out var template)) - { - return template.Render(data, member => member.Name); - } - - // Fallback for missing templates - return $""; + return value ? "✓ True" : "✗ False"; } - private string FormatBooleanValue(bool value) + private string RenderChangeGroup(object sectionData) { - return value ? "✓ True" : "✗ False"; + try + { + // Load and parse the change-group template + var templateContent = EmbeddedTemplateLoader.LoadTemplate("change-group.scriban"); + var template = Template.Parse(templateContent); + + // Create a new context for the template with the section data as root + var context = new TemplateContext(); + var scriptObject = new ScriptObject(); + + // Add the section data properties to the script object + if (sectionData != null) + { + var sectionType = sectionData.GetType(); + foreach (var property in sectionType.GetProperties()) + { + var value = property.GetValue(sectionData); + scriptObject.SetValue(property.Name.ToLowerInvariant().Replace("_", "_"), value, true); + } + } + + context.PushGlobal(scriptObject); + + return template.Render(context); + } + catch (Exception ex) + { + return $""; + } } private string GetCssStyles() @@ -290,24 +302,6 @@ private string GetJavaScriptCode() } } - private string GetFallbackTemplate() - { - return @" - - - API Comparison Report - - - -

API Comparison Report

-

Generated on {{ result.comparison_timestamp }}

-

Total Differences: {{ result.total_differences }}

- - - -"; - } - private string GetFallbackStyles() { return "body { font-family: Arial, sans-serif; margin: 20px; }"; diff --git a/src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban b/src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban index 743d22d..1dc7579 100644 --- a/src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban +++ b/src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban @@ -107,7 +107,7 @@ {{if section.description}}

{{ section.description }}

{{end}} - {{ include "change-group" section }} + {{ render_change_group section }} {{end}} diff --git a/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs b/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs new file mode 100644 index 0000000..45bd9e7 --- /dev/null +++ b/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs @@ -0,0 +1,196 @@ +// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT +using DotNetApiDiff.Models; +using DotNetApiDiff.Models.Configuration; +using DotNetApiDiff.Reporting; +using Xunit; + +namespace DotNetApiDiff.Tests.Reporting; + +public class HtmlFormatterScribanTests +{ + private readonly HtmlFormatterScriban _formatter; + + public HtmlFormatterScribanTests() + { + _formatter = new HtmlFormatterScriban(); + } + + [Fact] + public void Format_WithEmptyResult_ReturnsValidHtml() + { + // Arrange + var result = new ComparisonResult + { + OldAssemblyPath = "source.dll", + NewAssemblyPath = "target.dll", + ComparisonTimestamp = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc), + Configuration = CreateDefaultConfiguration() + }; + + // Act + var report = _formatter.Format(result); + + // Assert + Assert.NotNull(report); + Assert.NotEmpty(report); + Assert.Contains("", report); + Assert.Contains("API Comparison Report", report); + Assert.Contains("source.dll", report); + Assert.Contains("target.dll", report); + Assert.Contains("2023-01-01 12:00:00", report); + } + + [Fact] + public void Format_WithAddedItems_IncludesAddedSection() + { + // Arrange + var result = new ComparisonResult + { + OldAssemblyPath = "source.dll", + NewAssemblyPath = "target.dll", + ComparisonTimestamp = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc), + Configuration = CreateDefaultConfiguration(), + Differences = new List + { + new ApiDifference + { + ChangeType = ChangeType.Added, + ElementType = ApiElementType.Type, + ElementName = "NewClass", + Description = "Added new class", + Severity = SeverityLevel.Info + } + } + }; + + // Act + var report = _formatter.Format(result); + + // Assert + Assert.NotNull(report); + Assert.Contains("Added Items", report); + Assert.Contains("NewClass", report); + Assert.Contains("Added new class", report); + Assert.Contains("Type (1)", report); // Group header should show count + } + + [Fact] + public void Format_WithRemovedItems_IncludesRemovedSection() + { + // Arrange + var result = new ComparisonResult + { + OldAssemblyPath = "source.dll", + NewAssemblyPath = "target.dll", + ComparisonTimestamp = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc), + Configuration = CreateDefaultConfiguration(), + Differences = new List + { + new ApiDifference + { + ChangeType = ChangeType.Removed, + ElementType = ApiElementType.Method, + ElementName = "OldMethod", + Description = "Removed method", + Severity = SeverityLevel.Info + } + } + }; + + // Act + var report = _formatter.Format(result); + + // Assert + Assert.NotNull(report); + Assert.Contains("Removed Items", report); + Assert.Contains("OldMethod", report); + Assert.Contains("Removed method", report); + Assert.Contains("Method (1)", report); // Group header should show count + } + + [Fact] + public void Format_WithBreakingChanges_IncludesBreakingSection() + { + // Arrange + var result = new ComparisonResult + { + OldAssemblyPath = "source.dll", + NewAssemblyPath = "target.dll", + ComparisonTimestamp = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc), + Configuration = CreateDefaultConfiguration(), + Differences = new List + { + new ApiDifference + { + ChangeType = ChangeType.Removed, + ElementType = ApiElementType.Property, + ElementName = "ImportantProperty", + Description = "Removed important property", + IsBreakingChange = true, + Severity = SeverityLevel.Critical + } + } + }; + + // Act + var report = _formatter.Format(result); + + // Assert + Assert.NotNull(report); + Assert.Contains("Breaking Changes", report); + Assert.Contains("ImportantProperty", report); + Assert.Contains("Removed important property", report); + Assert.Contains("BREAKING", report); // Breaking badge should be present + } + + [Fact] + public void Format_WithSignatures_IncludesSignatureDetails() + { + // Arrange + var result = new ComparisonResult + { + OldAssemblyPath = "source.dll", + NewAssemblyPath = "target.dll", + ComparisonTimestamp = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc), + Configuration = CreateDefaultConfiguration(), + Differences = new List + { + new ApiDifference + { + ChangeType = ChangeType.Modified, + ElementType = ApiElementType.Method, + ElementName = "ChangedMethod", + Description = "Method signature changed", + OldSignature = "public void ChangedMethod(int param)", + NewSignature = "public void ChangedMethod(string param)", + Severity = SeverityLevel.Warning + } + } + }; + + // Act + var report = _formatter.Format(result); + + // Assert + Assert.NotNull(report); + Assert.Contains("ChangedMethod", report); + Assert.Contains("Method signature changed", report); + Assert.Contains("Old Signature:", report); + Assert.Contains("public void ChangedMethod(int param)", report); + Assert.Contains("New Signature:", report); + Assert.Contains("public void ChangedMethod(string param)", report); + } + + private static ComparisonConfiguration CreateDefaultConfiguration() + { + return new ComparisonConfiguration + { + Filters = new FilterConfiguration(), + Mappings = new MappingConfiguration(), + Exclusions = new ExclusionConfiguration(), + BreakingChangeRules = new BreakingChangeRules(), + OutputFormat = ReportFormat.Html, + OutputPath = "test.html" + }; + } +} From 3c72484b07377f23882edcea22bdeb1d73e80379 Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Sun, 27 Jul 2025 10:02:32 -0400 Subject: [PATCH 2/3] fix: formatting issues Signed-off-by: jbrinkman --- src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs | 1 - src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs | 1 - src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs | 11 +++++------ .../Reporting/HtmlFormatterScribanTests.cs | 4 ++-- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs b/src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs index 91934dd..af66f8b 100644 --- a/src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs +++ b/src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs @@ -1,5 +1,4 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT -using System.IO; using System.Reflection; using System.Security; using DotNetApiDiff.Interfaces; diff --git a/src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs b/src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs index 9f65772..471fb37 100644 --- a/src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs +++ b/src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs @@ -1,6 +1,5 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT using System.Reflection; -using System.Runtime.ExceptionServices; using System.Security; using DotNetApiDiff.Interfaces; using Microsoft.Extensions.Logging; diff --git a/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs b/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs index 8ee331d..4b5237d 100644 --- a/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs +++ b/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs @@ -4,7 +4,6 @@ using DotNetApiDiff.Models.Configuration; using Scriban; using Scriban.Runtime; -using System.Text; namespace DotNetApiDiff.Reporting; @@ -250,11 +249,11 @@ private string RenderChangeGroup(object sectionData) // Load and parse the change-group template var templateContent = EmbeddedTemplateLoader.LoadTemplate("change-group.scriban"); var template = Template.Parse(templateContent); - + // Create a new context for the template with the section data as root var context = new TemplateContext(); var scriptObject = new ScriptObject(); - + // Add the section data properties to the script object if (sectionData != null) { @@ -262,12 +261,12 @@ private string RenderChangeGroup(object sectionData) foreach (var property in sectionType.GetProperties()) { var value = property.GetValue(sectionData); - scriptObject.SetValue(property.Name.ToLowerInvariant().Replace("_", "_"), value, true); + scriptObject.SetValue(property.Name.ToLowerInvariant().Replace("_", "_"), value, true); } } - + context.PushGlobal(scriptObject); - + return template.Render(context); } catch (Exception ex) diff --git a/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs b/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs index 45bd9e7..1ec4dfb 100644 --- a/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs +++ b/tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs @@ -90,7 +90,7 @@ public void Format_WithRemovedItems_IncludesRemovedSection() { ChangeType = ChangeType.Removed, ElementType = ApiElementType.Method, - ElementName = "OldMethod", + ElementName = "OldMethod", Description = "Removed method", Severity = SeverityLevel.Info } @@ -149,7 +149,7 @@ public void Format_WithSignatures_IncludesSignatureDetails() // Arrange var result = new ComparisonResult { - OldAssemblyPath = "source.dll", + OldAssemblyPath = "source.dll", NewAssemblyPath = "target.dll", ComparisonTimestamp = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc), Configuration = CreateDefaultConfiguration(), From b49f9fb89474b0f6ce83b07e96b7533c47257525 Mon Sep 17 00:00:00 2001 From: jbrinkman Date: Sun, 27 Jul 2025 10:06:07 -0400 Subject: [PATCH 3/3] fix: remove unneeded code Signed-off-by: jbrinkman --- src/DotNetApiDiff/ApiExtraction/ApiComparer.cs | 1 - src/DotNetApiDiff/ApiExtraction/NameMapper.cs | 1 - src/DotNetApiDiff/AssemblyLoading/AssemblyLoader.cs | 1 - src/DotNetApiDiff/Interfaces/IApiComparer.cs | 1 - src/DotNetApiDiff/Interfaces/IAssemblyLoader.cs | 2 -- src/DotNetApiDiff/Interfaces/ITypeAnalyzer.cs | 1 - src/DotNetApiDiff/Models/Configuration/FilterConfiguration.cs | 1 - src/DotNetApiDiff/Models/Configuration/MappingConfiguration.cs | 1 - src/DotNetApiDiff/Program.cs | 2 -- src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs | 2 +- 10 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/DotNetApiDiff/ApiExtraction/ApiComparer.cs b/src/DotNetApiDiff/ApiExtraction/ApiComparer.cs index f0e8653..c460fc2 100644 --- a/src/DotNetApiDiff/ApiExtraction/ApiComparer.cs +++ b/src/DotNetApiDiff/ApiExtraction/ApiComparer.cs @@ -2,7 +2,6 @@ using System.Reflection; using DotNetApiDiff.Interfaces; using DotNetApiDiff.Models; -using DotNetApiDiff.Models.Configuration; using Microsoft.Extensions.Logging; namespace DotNetApiDiff.ApiExtraction; diff --git a/src/DotNetApiDiff/ApiExtraction/NameMapper.cs b/src/DotNetApiDiff/ApiExtraction/NameMapper.cs index 5f041e5..dd68c82 100644 --- a/src/DotNetApiDiff/ApiExtraction/NameMapper.cs +++ b/src/DotNetApiDiff/ApiExtraction/NameMapper.cs @@ -3,7 +3,6 @@ using DotNetApiDiff.Interfaces; using DotNetApiDiff.Models.Configuration; using Microsoft.Extensions.Logging; -using System.Text.RegularExpressions; namespace DotNetApiDiff.ApiExtraction; diff --git a/src/DotNetApiDiff/AssemblyLoading/AssemblyLoader.cs b/src/DotNetApiDiff/AssemblyLoading/AssemblyLoader.cs index fb24920..b2a549c 100644 --- a/src/DotNetApiDiff/AssemblyLoading/AssemblyLoader.cs +++ b/src/DotNetApiDiff/AssemblyLoading/AssemblyLoader.cs @@ -1,6 +1,5 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT using System.Reflection; -using System.Runtime.Loader; using System.Security; using DotNetApiDiff.Interfaces; using Microsoft.Extensions.Logging; diff --git a/src/DotNetApiDiff/Interfaces/IApiComparer.cs b/src/DotNetApiDiff/Interfaces/IApiComparer.cs index 12e45cd..97744bd 100644 --- a/src/DotNetApiDiff/Interfaces/IApiComparer.cs +++ b/src/DotNetApiDiff/Interfaces/IApiComparer.cs @@ -1,5 +1,4 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT -using System.Reflection; using DotNetApiDiff.Models; namespace DotNetApiDiff.Interfaces; diff --git a/src/DotNetApiDiff/Interfaces/IAssemblyLoader.cs b/src/DotNetApiDiff/Interfaces/IAssemblyLoader.cs index d0e1475..3e397c7 100644 --- a/src/DotNetApiDiff/Interfaces/IAssemblyLoader.cs +++ b/src/DotNetApiDiff/Interfaces/IAssemblyLoader.cs @@ -1,6 +1,4 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT -using System.Reflection; - namespace DotNetApiDiff.Interfaces; /// diff --git a/src/DotNetApiDiff/Interfaces/ITypeAnalyzer.cs b/src/DotNetApiDiff/Interfaces/ITypeAnalyzer.cs index 04f9a22..7011f97 100644 --- a/src/DotNetApiDiff/Interfaces/ITypeAnalyzer.cs +++ b/src/DotNetApiDiff/Interfaces/ITypeAnalyzer.cs @@ -1,5 +1,4 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT -using System.Reflection; using DotNetApiDiff.Models; namespace DotNetApiDiff.Interfaces; diff --git a/src/DotNetApiDiff/Models/Configuration/FilterConfiguration.cs b/src/DotNetApiDiff/Models/Configuration/FilterConfiguration.cs index 80f832c..7c69866 100644 --- a/src/DotNetApiDiff/Models/Configuration/FilterConfiguration.cs +++ b/src/DotNetApiDiff/Models/Configuration/FilterConfiguration.cs @@ -1,5 +1,4 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; namespace DotNetApiDiff.Models.Configuration; diff --git a/src/DotNetApiDiff/Models/Configuration/MappingConfiguration.cs b/src/DotNetApiDiff/Models/Configuration/MappingConfiguration.cs index 362fa24..721c959 100644 --- a/src/DotNetApiDiff/Models/Configuration/MappingConfiguration.cs +++ b/src/DotNetApiDiff/Models/Configuration/MappingConfiguration.cs @@ -1,5 +1,4 @@ // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; namespace DotNetApiDiff.Models.Configuration; diff --git a/src/DotNetApiDiff/Program.cs b/src/DotNetApiDiff/Program.cs index d558cc6..73ab989 100644 --- a/src/DotNetApiDiff/Program.cs +++ b/src/DotNetApiDiff/Program.cs @@ -4,9 +4,7 @@ using DotNetApiDiff.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Spectre.Console; using Spectre.Console.Cli; -using System.Diagnostics; namespace DotNetApiDiff; diff --git a/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs b/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs index 4b5237d..adc6329 100644 --- a/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs +++ b/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs @@ -261,7 +261,7 @@ private string RenderChangeGroup(object sectionData) foreach (var property in sectionType.GetProperties()) { var value = property.GetValue(sectionData); - scriptObject.SetValue(property.Name.ToLowerInvariant().Replace("_", "_"), value, true); + scriptObject.SetValue(property.Name.ToLowerInvariant(), value, true); } }