-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add processing warnings with report generation #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add comprehensive warning collection during template processing: - ProcessingWarning type with rich context (type, message, variable name, context) - IWarningCollector interface for collecting warnings during visitor traversal - Warnings for missing variables, missing loop collections, null loop collections - ProcessingResult.Warnings property and HasWarnings helper Add warning report generation: - GetWarningReport() returns MemoryStream with formatted .docx report - GetWarningReportBytes() returns byte array for convenience - Uses embedded template processed by Templify itself - WarningReportTemplateGenerator creates the embedded template Integrate into GUI: - Display warnings summary after template processing - "Generate Warning Report" button to save detailed report - Auto-opens generated report in default application Normalize loop behavior: - Null collections now treated same as missing (silent removal) - Previously null collections threw exception while missing were silently removed
- Add warnings when expression evaluation fails in PlaceholderVisitor (ArgumentException, InvalidOperationException, InvalidCastException, and parse failures now generate ExpressionFailed warnings) - Add .vscode/ to .gitignore and remove settings.json from tracking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a comprehensive processing warnings system for Templify that collects non-fatal warnings during template processing and enables generation of Word document reports containing all warnings. While the implementation addresses issue #69 and provides valuable diagnostic capabilities, there are critical bugs related to the ExpressionFailed warning type that must be fixed before merging.
Key Changes:
- Added warning collection infrastructure with
ProcessingWarning,IWarningCollector, and warning report generation - Extended visitors to collect warnings for missing variables, null/missing collections, and failed expressions
- Added GUI support with warning summary display and report generation button
Reviewed changes
Copilot reviewed 18 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| TriasDev.Templify/Core/ProcessingWarning.cs | Defines warning types and factory methods for creating warnings |
| TriasDev.Templify/Core/IWarningCollector.cs | Interface and implementation for collecting warnings during processing |
| TriasDev.Templify/Core/WarningReportGenerator.cs | [BUG] Generates Word reports from warnings but missing ExpressionFailed handling |
| TriasDev.Templify/Core/ProcessingResult.cs | Adds Warnings property and report generation methods to ProcessingResult |
| TriasDev.Templify/Visitors/PlaceholderVisitor.cs | Collects warnings for missing variables and failed expressions |
| TriasDev.Templify/Visitors/LoopVisitor.cs | Collects warnings for missing and null collections, removes unused ValueResolver |
| TriasDev.Templify/Visitors/ConditionalVisitor.cs | Updated constructor to accept IWarningCollector |
| TriasDev.Templify/Core/DocumentTemplateProcessor.cs | Integrates warning collector and passes warnings to result |
| TriasDev.Templify/Resources/WarningReportTemplate.docx | Embedded template for rendering warning reports |
| TriasDev.Templify/TriasDev.Templify.csproj | Adds embedded resource for warning report template |
| TriasDev.Templify.DocumentGenerator/Generators/WarningReportTemplateGenerator.cs | [BUG] Generates template but missing ExpressionFailed section |
| TriasDev.Templify.DocumentGenerator/Program.cs | Registers warning report template generator |
| TriasDev.Templify.Tests/Integration/ProcessingWarningsIntegrationTests.cs | [MISSING] Integration tests but lacks ExpressionFailed coverage |
| TriasDev.Templify.Tests/Visitors/*.cs | Updates all visitor tests with new constructor signatures |
| TriasDev.Templify.Gui/Views/MainWindow.axaml | Adds "Generate Warning Report" button to UI |
| TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs | Implements warning display and report generation logic |
| examples/templates/warning-report-template.docx | Example warning report template |
| examples/outputs/warning-report-output.docx | Example generated warning report output |
| .gitignore | Adds VS Code settings exclusion |
Comments suppressed due to low confidence (1)
TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs:230
- The LastProcessingResult is stored regardless of whether processing succeeded or failed. When processing fails (result.Success is false), the result may not have warnings or may have an error state. The CanGenerateWarningReport method checks for result.Success, but this creates an inconsistency where a failed result is stored but the warning report button is disabled. Consider only storing LastProcessingResult when result.Success is true, or clear it when processing fails, to avoid confusion and maintain consistency.
// Store for warning report generation
LastProcessingResult = result;
if (result.Success)
{
Results.Add("✓ Template processed successfully!");
Results.Add($"✓ Made {result.Processing.ReplacementCount} replacements");
Results.Add($"✓ Output saved to: {result.OutputPath}");
if (result.Processing.HasWarnings)
{
Results.Add($"⚠ {result.Processing.Warnings.Count} processing warnings:");
foreach (ProcessingWarning warning in result.Processing.Warnings.Take(5))
{
Results.Add($" - {warning.Type}: {warning.VariableName}");
}
if (result.Processing.Warnings.Count > 5)
{
Results.Add($" ... and {result.Processing.Warnings.Count - 5} more");
}
Results.Add(" (Use 'Generate Warning Report' for full details)");
}
StatusMessage = "Processing complete";
}
else
{
Results.Add($"✗ Processing failed: {result.Processing.ErrorMessage}");
StatusMessage = "Processing failed";
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch (warning.Type) | ||
| { | ||
| case ProcessingWarningType.MissingVariable: | ||
| missingVariables.Add(warningData); | ||
| break; | ||
| case ProcessingWarningType.MissingLoopCollection: | ||
| missingCollections.Add(warningData); | ||
| break; | ||
| case ProcessingWarningType.NullLoopCollection: | ||
| nullCollections.Add(warningData); | ||
| break; | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The switch statement does not handle ProcessingWarningType.ExpressionFailed warnings. When ExpressionFailed warnings are collected (e.g., from failed placeholder expressions in PlaceholderVisitor lines 90, 96, 102, 109), they will be silently dropped and not included in the warning report. Add a case for ExpressionFailed warnings, similar to how the other warning types are handled.
| AddParagraph(body, "{{/if}}"); | ||
|
|
||
| // Footer | ||
| AddEmptyParagraph(body); | ||
| AddParagraph(body, "End of Warning Report"); | ||
|
|
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The warning report template does not include a section for ExpressionFailed warnings. While the template includes sections for missing variables, missing collections, and null collections, it's missing a corresponding section for failed expressions. This means ExpressionFailed warnings won't be displayed in the report even if the generator is fixed to include them in the data.
| public override Dictionary<string, object> GetSampleData() | ||
| { | ||
| // Sample data for testing the template | ||
| return new Dictionary<string, object> | ||
| { | ||
| ["GeneratedAt"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), | ||
| ["TotalWarnings"] = 5, | ||
| ["MissingVariableCount"] = 2, | ||
| ["MissingCollectionCount"] = 2, | ||
| ["NullCollectionCount"] = 1, | ||
| ["HasMissingVariables"] = true, | ||
| ["HasMissingCollections"] = true, | ||
| ["HasNullCollections"] = true, | ||
| ["MissingVariables"] = new List<Dictionary<string, object>> | ||
| { | ||
| new() { ["VariableName"] = "CustomerName", ["Context"] = "placeholder" }, | ||
| new() { ["VariableName"] = "Customer.Email", ["Context"] = "placeholder" } | ||
| }, | ||
| ["MissingCollections"] = new List<Dictionary<string, object>> | ||
| { | ||
| new() { ["VariableName"] = "OrderItems", ["Context"] = "loop: OrderItems" }, | ||
| new() { ["VariableName"] = "Categories", ["Context"] = "loop: Categories" } | ||
| }, | ||
| ["NullCollections"] = new List<Dictionary<string, object>> | ||
| { | ||
| new() { ["VariableName"] = "Products", ["Context"] = "loop: Products" } | ||
| } | ||
| }; | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sample data for the warning report template does not include test data for ExpressionFailed warnings. While all other warning types (MissingVariable, MissingLoopCollection, NullLoopCollection) have sample data, ExpressionFailed is missing. This means the template cannot be properly tested for this warning type.
| public sealed class ProcessingWarningsIntegrationTests | ||
| { | ||
| [Fact] | ||
| public void ProcessTemplate_MissingVariable_CollectsWarning() | ||
| { | ||
| // Arrange | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("Hello {{MissingName}}!"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object>(); | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.True(result.HasWarnings); | ||
| Assert.Single(result.Warnings); | ||
|
|
||
| ProcessingWarning warning = result.Warnings[0]; | ||
| Assert.Equal(ProcessingWarningType.MissingVariable, warning.Type); | ||
| Assert.Equal("MissingName", warning.VariableName); | ||
| Assert.Contains("MissingName", warning.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_MultipleMissingVariables_CollectsAllWarnings() | ||
| { | ||
| // Arrange | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("{{Missing1}} and {{Missing2}} and {{Missing3}}"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object>(); | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.Equal(3, result.Warnings.Count); | ||
| Assert.All(result.Warnings, w => Assert.Equal(ProcessingWarningType.MissingVariable, w.Type)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_MissingLoopCollection_CollectsWarning() | ||
| { | ||
| // Arrange | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("{{#foreach MissingItems}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object>(); | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.True(result.HasWarnings); | ||
| Assert.Single(result.Warnings); | ||
|
|
||
| ProcessingWarning warning = result.Warnings[0]; | ||
| Assert.Equal(ProcessingWarningType.MissingLoopCollection, warning.Type); | ||
| Assert.Equal("MissingItems", warning.VariableName); | ||
| Assert.Contains("loop: MissingItems", warning.Context); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_NullLoopCollection_CollectsWarning() | ||
| { | ||
| // Arrange | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("{{#foreach NullItems}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object?> data = new Dictionary<string, object?> | ||
| { | ||
| ["NullItems"] = null | ||
| }; | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data!); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.True(result.HasWarnings); | ||
| Assert.Single(result.Warnings); | ||
|
|
||
| ProcessingWarning warning = result.Warnings[0]; | ||
| Assert.Equal(ProcessingWarningType.NullLoopCollection, warning.Type); | ||
| Assert.Equal("NullItems", warning.VariableName); | ||
| Assert.Contains("null", warning.Message.ToLowerInvariant()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_EmptyCollection_NoWarning() | ||
| { | ||
| // Arrange - empty collection should NOT produce a warning (by design) | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("{{#foreach EmptyItems}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object> | ||
| { | ||
| ["EmptyItems"] = new List<string>() // Empty, but valid | ||
| }; | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.False(result.HasWarnings); | ||
| Assert.Empty(result.Warnings); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_ValidData_NoWarnings() | ||
| { | ||
| // Arrange | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("Hello {{Name}}!"); | ||
| builder.AddParagraph("{{#foreach Items}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object> | ||
| { | ||
| ["Name"] = "World", | ||
| ["Items"] = new List<string> { "A", "B", "C" } | ||
| }; | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.False(result.HasWarnings); | ||
| Assert.Empty(result.Warnings); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_MixedWarnings_CollectsAll() | ||
| { | ||
| // Arrange - multiple warning types in one template | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("Hello {{MissingName}}!"); | ||
| builder.AddParagraph("{{#foreach MissingItems}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
| builder.AddParagraph("{{#foreach NullItems}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object?> data = new Dictionary<string, object?> | ||
| { | ||
| ["NullItems"] = null | ||
| }; | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data!); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.True(result.HasWarnings); | ||
| Assert.Equal(3, result.Warnings.Count); | ||
|
|
||
| // Verify we have different warning types | ||
| Assert.Contains(result.Warnings, w => w.Type == ProcessingWarningType.MissingVariable); | ||
| Assert.Contains(result.Warnings, w => w.Type == ProcessingWarningType.MissingLoopCollection); | ||
| Assert.Contains(result.Warnings, w => w.Type == ProcessingWarningType.NullLoopCollection); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessingWarning_ToString_ContainsUsefulInfo() | ||
| { | ||
| // Arrange | ||
| ProcessingWarning warning = ProcessingWarning.MissingVariable("TestVar"); | ||
|
|
||
| // Act | ||
| string str = warning.ToString(); | ||
|
|
||
| // Assert | ||
| Assert.Contains("MissingVariable", str); | ||
| Assert.Contains("TestVar", str); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProcessTemplate_NestedMissingVariable_CollectsWarning() | ||
| { | ||
| // Arrange - missing nested path like Customer.Name | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("{{Customer.Name}}"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object>(); | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| // Act | ||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Assert | ||
| Assert.True(result.IsSuccess); | ||
| Assert.True(result.HasWarnings); | ||
| Assert.Single(result.Warnings); | ||
| Assert.Equal("Customer.Name", result.Warnings[0].VariableName); | ||
| } | ||
|
|
||
| #region Warning Report Tests | ||
|
|
||
| [Fact] | ||
| public void GetWarningReport_WithWarnings_ReturnsValidDocx() | ||
| { | ||
| // Arrange - create a result with warnings | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("Hello {{MissingName}}!"); | ||
| builder.AddParagraph("{{#foreach MissingItems}}"); | ||
| builder.AddParagraph("Item: {{.}}"); | ||
| builder.AddParagraph("{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object>(); | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Act | ||
| using MemoryStream reportStream = result.GetWarningReport(); | ||
|
|
||
| // Assert - verify it's a valid docx | ||
| Assert.True(reportStream.Length > 0); | ||
| reportStream.Position = 0; | ||
|
|
||
| using WordprocessingDocument doc = WordprocessingDocument.Open(reportStream, false); | ||
| Body? body = doc.MainDocumentPart?.Document.Body; | ||
| Assert.NotNull(body); | ||
|
|
||
| string text = body.InnerText; | ||
| Assert.Contains("Warning Report", text); | ||
| Assert.Contains("2", text); // Total 2 warnings | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetWarningReport_NoWarnings_ReturnsEmptyReport() | ||
| { | ||
| // Arrange - create a result with no warnings | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("Hello {{Name}}!"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object> | ||
| { | ||
| ["Name"] = "World" | ||
| }; | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Act | ||
| using MemoryStream reportStream = result.GetWarningReport(); | ||
|
|
||
| // Assert - verify it's a valid docx with 0 warnings | ||
| Assert.True(reportStream.Length > 0); | ||
| reportStream.Position = 0; | ||
|
|
||
| using WordprocessingDocument doc = WordprocessingDocument.Open(reportStream, false); | ||
| Body? body = doc.MainDocumentPart?.Document.Body; | ||
| Assert.NotNull(body); | ||
|
|
||
| string text = body.InnerText; | ||
| Assert.Contains("Warning Report", text); | ||
| Assert.Contains("Total Warnings: 0", text); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetWarningReportBytes_WithWarnings_ReturnsByteArray() | ||
| { | ||
| // Arrange | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("Hello {{MissingVar}}!"); | ||
|
|
||
| Dictionary<string, object> data = new Dictionary<string, object>(); | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); | ||
|
|
||
| // Act | ||
| byte[] reportBytes = result.GetWarningReportBytes(); | ||
|
|
||
| // Assert | ||
| Assert.True(reportBytes.Length > 0); | ||
|
|
||
| // Verify it's a valid docx by loading it | ||
| using MemoryStream stream = new MemoryStream(reportBytes); | ||
| using WordprocessingDocument doc = WordprocessingDocument.Open(stream, false); | ||
| Assert.NotNull(doc.MainDocumentPart?.Document.Body); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetWarningReport_MixedWarnings_ContainsAllSections() | ||
| { | ||
| // Arrange - create warnings of all types | ||
| DocumentBuilder builder = new DocumentBuilder(); | ||
| builder.AddParagraph("{{MissingVar1}}"); | ||
| builder.AddParagraph("{{MissingVar2}}"); | ||
| builder.AddParagraph("{{#foreach MissingCollection}}{{.}}{{/foreach}}"); | ||
| builder.AddParagraph("{{#foreach NullCollection}}{{.}}{{/foreach}}"); | ||
|
|
||
| Dictionary<string, object?> data = new Dictionary<string, object?> | ||
| { | ||
| ["NullCollection"] = null | ||
| }; | ||
|
|
||
| using MemoryStream templateStream = builder.ToStream(); | ||
| using MemoryStream outputStream = new MemoryStream(); | ||
|
|
||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data!); | ||
|
|
||
| // Act | ||
| using MemoryStream reportStream = result.GetWarningReport(); | ||
|
|
||
| // Assert | ||
| reportStream.Position = 0; | ||
| using WordprocessingDocument doc = WordprocessingDocument.Open(reportStream, false); | ||
| Body? body = doc.MainDocumentPart?.Document.Body; | ||
| Assert.NotNull(body); | ||
|
|
||
| string text = body.InnerText; | ||
|
|
||
| // Should contain all sections | ||
| Assert.Contains("Missing Variables", text); | ||
| Assert.Contains("Missing Loop Collections", text); | ||
| Assert.Contains("Null Loop Collections", text); | ||
|
|
||
| // Should contain the specific variable names | ||
| Assert.Contains("MissingVar1", text); | ||
| Assert.Contains("MissingVar2", text); | ||
| Assert.Contains("MissingCollection", text); | ||
| Assert.Contains("NullCollection", text); | ||
|
|
||
| // Should have correct counts | ||
| Assert.Contains("Total Warnings: 4", text); | ||
| } | ||
|
|
||
| #endregion | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no integration tests for ExpressionFailed warnings. The PR description claims "Failed expressions generate warnings" was tested, but the test file only includes tests for MissingVariable, MissingLoopCollection, and NullLoopCollection. Add tests to verify that ExpressionFailed warnings are properly collected when expression evaluation fails (e.g., when using an invalid expression in a placeholder).
| foreach (ProcessingWarning warning in result.Processing.Warnings.Take(5)) | ||
| { | ||
| Results.Add($" - {missing}"); | ||
| Results.Add($" - {warning.Type}: {warning.VariableName}"); | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The warning summary display only shows the warning type and variable name, but not the full message. For ExpressionFailed warnings where the reason for failure is important, or for other warnings where additional context is valuable, users won't see the helpful error details. Consider including the warning message or at least a preview of it in the summary, especially since these are the first 5 warnings shown before suggesting users check the full report.
Address Copilot review comments by adding support for ExpressionFailed warning type in the warning report: - Add ExpressionFailed case to WarningReportGenerator switch statement - Add "Failed Expressions" section to warning report template - Add sample data for ExpressionFailed warnings in template generator - Regenerate embedded WarningReportTemplate.docx resource
Address remaining Copilot review comments: - Add integration tests for ExpressionFailed warnings (parse failures) - Add test for valid expressions (no warning expected) - Improve GUI warning summary to include truncated message text instead of just type and variable name
Add new documentation page covering: - How to access warnings from ProcessingResult - Warning types (MissingVariable, MissingLoopCollection, NullLoopCollection, ExpressionFailed) - Warning properties - Generating warning reports as Word documents - Example code for logging warnings - Behavior notes and edge cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 19 out of 23 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Implements processing warnings system for Templify (closes #69).
This feature collects non-fatal warnings during template processing and provides the ability to generate a Word document report of all warnings.
Key Changes
Core Library (TriasDev.Templify)
ProcessingWarningclass with factory methods for different warning typesIWarningCollectorinterface andWarningCollectorimplementationWarningReportGeneratorthat uses Templify itself to generate reportsProcessingResultwithWarnings,HasWarnings,GetWarningReport(), andGetWarningReportBytes()LoopVisitorto collect warnings for missing/null collectionsPlaceholderVisitorto collect warnings for missing variables and failed expressionsConditionalVisitorto acceptIWarningCollectorWarning Types
MissingVariable- placeholder variable not found in dataMissingLoopCollection- loop collection not found in dataNullLoopCollection- loop collection found but value is nullExpressionFailed- expression evaluation failedGUI (TriasDev.Templify.Gui)
Tests
Test plan
dotnet format --verify-no-changespasses