From f9cb10c46682ee67852c8a9bf22f18bf763b51bf Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 13 Mar 2026 16:22:06 +0000 Subject: [PATCH 1/4] Report template processing errors Any errors raised during the code generation process are surfaced through a single collection. Each code generation target is marked with its default target file name. --- src/FirmwareGenerator.cs | 31 +++++++++++++++---------------- src/InterfaceGenerator.cs | 16 ++++++++++------ src/TemplateBase.cs | 20 +++++++++++++++++--- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/FirmwareGenerator.cs b/src/FirmwareGenerator.cs index c9b7634..17de070 100644 --- a/src/FirmwareGenerator.cs +++ b/src/FirmwareGenerator.cs @@ -1,6 +1,6 @@ -using System.Collections; +using System.CodeDom.Compiler; +using System.Collections; using System.Collections.Generic; -using System.IO; namespace Harp.Generators; @@ -16,6 +16,7 @@ public class FirmwareGenerator readonly AppRegs _appRegsTemplate = new(); readonly AppRegsImpl _appRegsImplTemplate = new(); readonly Interrupts _interruptsTemplate = new(); + readonly CompilerErrorCollection errors = []; /// /// Initializes a new instance of the class with the @@ -30,22 +31,20 @@ public FirmwareGenerator(DeviceInfo deviceMetadata, Dictionary + /// Gets the collection of errors emitted during the code generation process. + /// + public CompilerErrorCollection Errors => errors; + /// /// Generates firmware header files complying with the specified metadata file. /// diff --git a/src/InterfaceGenerator.cs b/src/InterfaceGenerator.cs index 1b5aded..c847b15 100644 --- a/src/InterfaceGenerator.cs +++ b/src/InterfaceGenerator.cs @@ -1,6 +1,6 @@ -using System.Collections; +using System.CodeDom.Compiler; +using System.Collections; using System.Collections.Generic; -using System.IO; namespace Harp.Generators; @@ -11,6 +11,7 @@ public sealed class InterfaceGenerator { readonly Device _deviceTemplate = new(); readonly AsyncDevice _asyncDeviceTemplate = new(); + readonly CompilerErrorCollection errors = []; /// /// Initializes a new instance of the class with the @@ -25,12 +26,15 @@ public InterfaceGenerator(DeviceInfo deviceMetadata, string ns) { "Namespace", ns }, { "DeviceMetadata", deviceMetadata } }; - _deviceTemplate.Session = session; - _asyncDeviceTemplate.Session = session; - _deviceTemplate.Initialize(); - _asyncDeviceTemplate.Initialize(); + _deviceTemplate.Initialize(InterfaceImplementation.DeviceFileName, errors, session); + _asyncDeviceTemplate.Initialize(InterfaceImplementation.AsyncDeviceFileName, errors, session); } + /// + /// Gets the collection of errors emitted during the code generation process. + /// + public CompilerErrorCollection Errors => errors; + /// /// Generates a device interface implementation complying with the specified metadata file. /// diff --git a/src/TemplateBase.cs b/src/TemplateBase.cs index 55e2640..22c4073 100644 --- a/src/TemplateBase.cs +++ b/src/TemplateBase.cs @@ -13,6 +13,8 @@ internal abstract class TemplateBase private Stack indents; public virtual IDictionary Session { get; set; } + + public string FileName { get; set; } = string.Empty; public StringBuilder GenerationEnvironment { @@ -20,7 +22,11 @@ public StringBuilder GenerationEnvironment set => builder = value; } - protected CompilerErrorCollection Errors => errors ??= []; + public CompilerErrorCollection Errors + { + get => errors ??= []; + set => errors = value; + } public string CurrentIndent => currentIndent; @@ -31,15 +37,23 @@ public StringBuilder GenerationEnvironment public abstract string TransformText(); public virtual void Initialize() { } + + public void Initialize(string fileName, CompilerErrorCollection errors, Dictionary session) + { + FileName = fileName; + Errors = errors; + Session = session; + Initialize(); + } public void Error(string message) { - Errors.Add(new CompilerError(null, -1, -1, null, message)); + Errors.Add(new CompilerError(FileName, -1, -1, null, message)); } public void Warning(string message) { - Errors.Add(new CompilerError(null, -1, -1, null, message) + Errors.Add(new CompilerError(FileName, -1, -1, null, message) { IsWarning = true }); From 1cc9dc29d4f56b1430c1437ba899afe5c22abf5b Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 13 Mar 2026 16:26:31 +0000 Subject: [PATCH 2/4] Assert no errors raised during code generation In case the error collection is not empty, an assertion error is raised concatenating all reported errors. --- tests/FirmwareGeneratorTests.cs | 1 + tests/InterfaceGeneratorTests.cs | 1 + tests/TestHelper.cs | 19 ++++++++++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/FirmwareGeneratorTests.cs b/tests/FirmwareGeneratorTests.cs index e4eb242..bda37f2 100644 --- a/tests/FirmwareGeneratorTests.cs +++ b/tests/FirmwareGeneratorTests.cs @@ -37,6 +37,7 @@ public void FirmwareTemplate_GenerateAndBuildWithoutErrors(string metadataFileNa var interruptsOutputFileName = $"{outputFileName}.{FirmwareImplementation.InterruptsFileName}"; try { + TestHelper.AssertNoGeneratorErrors(generator.Errors); TestHelper.AssertExpectedOutput(headers.App, appOutputFileName); TestHelper.AssertExpectedOutput(implementation.App, appImplOutputFileName); TestHelper.AssertExpectedOutput(headers.AppFuncs, appFuncsOutputFileName); diff --git a/tests/InterfaceGeneratorTests.cs b/tests/InterfaceGeneratorTests.cs index 3840fa5..a9eda41 100644 --- a/tests/InterfaceGeneratorTests.cs +++ b/tests/InterfaceGeneratorTests.cs @@ -32,6 +32,7 @@ public void DeviceTemplate_GenerateAndBuildWithoutErrors(string metadataFileName var customImplementation = TestHelper.GetManifestResourceText($"EmbeddedSources.{outputFileName}.cs"); try { + TestHelper.AssertNoGeneratorErrors(generator.Errors); CompilerTestHelper.CompileFromSource(implementation.Device, implementation.AsyncDevice, payloadExtensions, customImplementation); TestHelper.AssertExpectedOutput(implementation.Device, deviceOutputFileName); TestHelper.AssertExpectedOutput(implementation.AsyncDevice, asyncDeviceOutputFileName); diff --git a/tests/TestHelper.cs b/tests/TestHelper.cs index 8c2281f..cc85901 100644 --- a/tests/TestHelper.cs +++ b/tests/TestHelper.cs @@ -1,4 +1,6 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.CodeDom.Compiler; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; using YamlDotNet.Core; namespace Harp.Generators.Tests; @@ -52,4 +54,19 @@ public static void AssertExpectedOutput(string actual, string outputFileName) } } } + + public static void AssertNoGeneratorErrors(CompilerErrorCollection errors) + { + if (errors.Count > 0) + { + var errorLog = new StringBuilder(); + errorLog.AppendLine("Code generation has completed with errors:"); + foreach (CompilerError error in errors) + { + var warningString = error.IsWarning ? "warning" : "error"; + errorLog.AppendLine($"{error.FileName}: {warningString}: {error.ErrorText}"); + } + Assert.Fail(errorLog.ToString()); + } + } } From 8bd65f5db44d921483a9bb2f35f995a73e9747fb Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 13 Mar 2026 17:06:18 +0000 Subject: [PATCH 3/4] Add regression tests for expected generator errors Added infrastructure for matching expected error messages against template processing errors. Added tests for invalid IO pin configuration. --- tests/FirmwareGeneratorTests.cs | 5 ++-- tests/Metadata/errors.ios.yml | 8 ++++++ tests/Metadata/errors.yml | 5 ++++ tests/TestHelper.cs | 44 +++++++++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 tests/Metadata/errors.ios.yml create mode 100644 tests/Metadata/errors.yml diff --git a/tests/FirmwareGeneratorTests.cs b/tests/FirmwareGeneratorTests.cs index bda37f2..7de67c3 100644 --- a/tests/FirmwareGeneratorTests.cs +++ b/tests/FirmwareGeneratorTests.cs @@ -17,7 +17,8 @@ public void Initialize() [DataTestMethod] [DataRow("device.yml")] - public void FirmwareTemplate_GenerateAndBuildWithoutErrors(string metadataFileName) + [DataRow("errors.yml", "Interrupt number must be specified if interrupt priority is set.")] + public void FirmwareTemplate_GenerateAndBuild(string metadataFileName, params string[] expectedErrors) { metadataFileName = TestHelper.GetMetadataPath(metadataFileName); var iosMetadataFileName = Path.ChangeExtension(metadataFileName, ".ios.yml"); @@ -37,7 +38,7 @@ public void FirmwareTemplate_GenerateAndBuildWithoutErrors(string metadataFileNa var interruptsOutputFileName = $"{outputFileName}.{FirmwareImplementation.InterruptsFileName}"; try { - TestHelper.AssertNoGeneratorErrors(generator.Errors); + TestHelper.AssertExpectedGeneratorErrors(generator.Errors, expectedErrors); TestHelper.AssertExpectedOutput(headers.App, appOutputFileName); TestHelper.AssertExpectedOutput(implementation.App, appImplOutputFileName); TestHelper.AssertExpectedOutput(headers.AppFuncs, appFuncsOutputFileName); diff --git a/tests/Metadata/errors.ios.yml b/tests/Metadata/errors.ios.yml new file mode 100644 index 0000000..32620d0 --- /dev/null +++ b/tests/Metadata/errors.ios.yml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/ios.json +IO_PULLDOWN: + port: PORTJ + pinNumber: 0 + direction: input + pinMode: pulldown + triggerMode: toggle + interruptPriority: low \ No newline at end of file diff --git a/tests/Metadata/errors.yml b/tests/Metadata/errors.yml new file mode 100644 index 0000000..c9c5a40 --- /dev/null +++ b/tests/Metadata/errors.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/registers.json +device: ErrorTests +firmwareVersion: "0.1" +hardwareTargets: "1.0" +registers: {} \ No newline at end of file diff --git a/tests/TestHelper.cs b/tests/TestHelper.cs index cc85901..f5d902d 100644 --- a/tests/TestHelper.cs +++ b/tests/TestHelper.cs @@ -55,16 +55,56 @@ public static void AssertExpectedOutput(string actual, string outputFileName) } } + static void AppendCompilerErrors(StringBuilder errorLog, CompilerErrorCollection errors) + { + foreach (CompilerError error in errors) + { + var warningString = error.IsWarning ? "warning" : "error"; + errorLog.AppendLine($"{error.FileName}: {warningString}: {error.ErrorText}"); + } + } + public static void AssertNoGeneratorErrors(CompilerErrorCollection errors) { if (errors.Count > 0) { var errorLog = new StringBuilder(); errorLog.AppendLine("Code generation has completed with errors:"); + AppendCompilerErrors(errorLog, errors); + Assert.Fail(errorLog.ToString()); + } + } + + public static void AssertExpectedGeneratorErrors(CompilerErrorCollection errors, params string[] expectedErrors) + { + if (expectedErrors.Length == 0) + { + AssertNoGeneratorErrors(errors); + return; + } + + var errorList = expectedErrors.ToList(); + errorList.RemoveAll(errorText => + { foreach (CompilerError error in errors) { - var warningString = error.IsWarning ? "warning" : "error"; - errorLog.AppendLine($"{error.FileName}: {warningString}: {error.ErrorText}"); + if (error.ErrorText.Contains(errorText)) + return true; + } + + return false; + }); + + if (errorList.Count > 0) + { + var errorLog = new StringBuilder(); + errorLog.AppendLine("Expected code generation errors, but the following errors were not raised:"); + foreach (var missingError in errorList) + errorLog.AppendLine(missingError); + if (errors.Count > 0) + { + errorLog.AppendLine("Code generation has completed with the following errors:"); + AppendCompilerErrors(errorLog, errors); } Assert.Fail(errorLog.ToString()); } From 440a18173c4cb105ebb9a3928e7b3b62d57c8c3a Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 13 Mar 2026 17:20:22 +0000 Subject: [PATCH 4/4] Format tests and helper classes --- src/Interface.cs | 6 +++--- src/TemplateBase.cs | 46 ++++++++++++++++++++--------------------- tests/PayloadMarshal.cs | 4 ++-- tests/TestHelper.cs | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Interface.cs b/src/Interface.cs index 5fa2365..b000b0c 100644 --- a/src/Interface.cs +++ b/src/Interface.cs @@ -468,7 +468,7 @@ public static string GetDefaultValueAssignment(float? defaultValue, float? minVa { defaultValue ??= minValue; var suffix = payloadType == PayloadType.Float ? "F" : string.Empty; - return defaultValue.HasValue? $" = {defaultValue}{suffix};" : string.Empty; + return defaultValue.HasValue ? $" = {defaultValue}{suffix};" : string.Empty; } public static string GetParseConversion(RegisterInfo register, string expression) @@ -655,7 +655,7 @@ public static string GetPayloadMemberValueFormatter( { if (member.HasConverter) expression = $"FormatPayload{name}({expression})"; - + if (member.Length > 0) return expression; @@ -835,7 +835,7 @@ public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerialize emitter.Emit(new Scalar(access.ToString())); return; } - + emitter.Emit(new SequenceStart(AnchorName.Empty, TagName.Empty, isImplicit: true, SequenceStyle.Flow)); if ((access | RegisterAccess.Read) != 0) emitter.Emit(new Scalar(nameof(RegisterAccess.Read))); diff --git a/src/TemplateBase.cs b/src/TemplateBase.cs index 22c4073..61f4fdd 100644 --- a/src/TemplateBase.cs +++ b/src/TemplateBase.cs @@ -7,31 +7,31 @@ namespace Harp.Generators; internal abstract class TemplateBase { - private StringBuilder builder; + private StringBuilder builder; private CompilerErrorCollection errors; private string currentIndent = string.Empty; private Stack indents; - + public virtual IDictionary Session { get; set; } public string FileName { get; set; } = string.Empty; - + public StringBuilder GenerationEnvironment { get => builder ??= new StringBuilder(); set => builder = value; } - + public CompilerErrorCollection Errors { get => errors ??= []; set => errors = value; } - + public string CurrentIndent => currentIndent; - + private Stack Indents => indents ??= []; - + public ToStringInstanceHelper ToStringHelper { get; } = new(); public abstract string TransformText(); @@ -45,12 +45,12 @@ public void Initialize(string fileName, CompilerErrorCollection errors, Dictiona Session = session; Initialize(); } - + public void Error(string message) { Errors.Add(new CompilerError(FileName, -1, -1, null, message)); } - + public void Warning(string message) { Errors.Add(new CompilerError(FileName, -1, -1, null, message) @@ -58,63 +58,63 @@ public void Warning(string message) IsWarning = true }); } - + public string PopIndent() { if (Indents.Count == 0) return string.Empty; - + int lastPos = currentIndent.Length - Indents.Pop(); string last = currentIndent.Substring(lastPos); currentIndent = currentIndent.Substring(0, lastPos); return last; } - + public void PushIndent(string indent) { Indents.Push(indent.Length); currentIndent += indent; } - + public void ClearIndent() { currentIndent = string.Empty; Indents.Clear(); } - + public void Write(string textToAppend) { GenerationEnvironment.Append(textToAppend); } - + public void Write(string format, params object[] args) { GenerationEnvironment.AppendFormat(format, args); } - + public void WriteLine(string textToAppend) { GenerationEnvironment.Append(currentIndent); GenerationEnvironment.AppendLine(textToAppend); } - + public void WriteLine(string format, params object[] args) { GenerationEnvironment.Append(currentIndent); GenerationEnvironment.AppendFormat(format, args); GenerationEnvironment.AppendLine(); } - + public class ToStringInstanceHelper - { + { private IFormatProvider formatProvider = System.Globalization.CultureInfo.InvariantCulture; - + public IFormatProvider FormatProvider { get => formatProvider; set => formatProvider = value ?? formatProvider; } - + public string ToStringWithCulture(object value) { if (value is null) @@ -122,11 +122,11 @@ public string ToStringWithCulture(object value) if (value is IConvertible convertible) return convertible.ToString(formatProvider); - + var method = value.GetType().GetMethod(nameof(ToString), [typeof(IFormatProvider)]); if (method is not null) return (string)method.Invoke(value, [formatProvider]); - + return value.ToString(); } } diff --git a/tests/PayloadMarshal.cs b/tests/PayloadMarshal.cs index 030099c..83caaa0 100644 --- a/tests/PayloadMarshal.cs +++ b/tests/PayloadMarshal.cs @@ -1,4 +1,4 @@ -#pragma warning disable IDE0005 +#pragma warning disable IDE0005 using System; #pragma warning restore IDE0005 using Bonsai.Harp; @@ -19,4 +19,4 @@ internal static void Write(ArraySegment segment, HarpVersion value) segment.Array[segment.Offset] = (byte)value.Major.GetValueOrDefault(); segment.Array[segment.Offset + 1] = (byte)value.Minor.GetValueOrDefault(); } -} \ No newline at end of file +} diff --git a/tests/TestHelper.cs b/tests/TestHelper.cs index f5d902d..b12b307 100644 --- a/tests/TestHelper.cs +++ b/tests/TestHelper.cs @@ -61,7 +61,7 @@ static void AppendCompilerErrors(StringBuilder errorLog, CompilerErrorCollection { var warningString = error.IsWarning ? "warning" : "error"; errorLog.AppendLine($"{error.FileName}: {warningString}: {error.ErrorText}"); - } + } } public static void AssertNoGeneratorErrors(CompilerErrorCollection errors)