/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System.Globalization; using System.Linq; using Microsoft.CodeAnalysis; using NUnit.Framework; namespace Opc.Ua.SourceGeneration { /// /// Tests for the logger-to-diagnostic bridge in /// / . /// [TestFixture] [Category("SourceGeneration")] [SetCulture("en-us")] [SetUICulture("en-us")] public class SourceGeneratorTelemetryTests { /// /// Documents the underlying bug: reporting an already-formatted message /// directly against a descriptor whose format declares more placeholders /// than arguments supplied yields the raw, unsubstituted template. /// [Test] public void DirectReportAgainstMultiPlaceholderDescriptorLosesMessage() { Diagnostic diagnostic = Diagnostic.Create( SourceGenerator.Exception, Location.None, "boom"); string rendered = diagnostic.GetMessage(CultureInfo.InvariantCulture); // The single argument cannot satisfy the '{0}': '{1}' format, so the // real message is dropped and the placeholders leak through verbatim. Assert.That(rendered, Does.Not.Contain("boom")); Assert.That(rendered, Does.Contain("{1}")); } /// /// Verifies the fix: an already-formatted message is rendered verbatim /// under the correct diagnostic identity regardless of the looked-up /// descriptor's placeholder arity. /// [Test] public void CreateFormattedDiagnosticRendersMessageVerbatim() { Diagnostic diagnostic = SourceGenerator.CreateFormattedDiagnostic( SourceGenerator.Exception, "boom"); string rendered = diagnostic.GetMessage(CultureInfo.InvariantCulture); Assert.That(rendered, Is.EqualTo("boom")); Assert.That(rendered, Does.Not.Contain("{0}")); Assert.That(rendered, Does.Not.Contain("{1}")); } /// /// The passthrough descriptor preserves the looked-up descriptor's /// identity and metadata (id, severity, category, help link, tags). /// [Test] public void CreateFormattedDiagnosticPreservesDescriptorMetadata() { DiagnosticDescriptor source = SourceGenerator.Exception; Diagnostic diagnostic = SourceGenerator.CreateFormattedDiagnostic( source, "boom"); DiagnosticDescriptor result = diagnostic.Descriptor; Assert.That(result.Id, Is.EqualTo(source.Id)); Assert.That(result.Id, Is.EqualTo("MODELGEN003")); Assert.That(diagnostic.Severity, Is.EqualTo(source.DefaultSeverity)); Assert.That(result.Category, Is.EqualTo(source.Category)); Assert.That(result.Description, Is.EqualTo(source.Description)); Assert.That(result.HelpLinkUri, Is.EqualTo(source.HelpLinkUri)); Assert.That(result.IsEnabledByDefault, Is.EqualTo(source.IsEnabledByDefault)); Assert.That(result.CustomTags, Is.EquivalentTo(source.CustomTags.ToArray())); } /// /// Regression: the generator's logger reports Critical as enabled, but /// the level was not mapped to any descriptor, so every LogCritical /// message - including "Could not parse NodeSet" - was silently dropped. /// [Test] public void CriticalLogLevelMapsToADiagnostic() { bool mapped = SourceGenerator.TryGetDiagnostic( Microsoft.Extensions.Logging.LogLevel.Critical, default, out DiagnosticDescriptor descriptor); Assert.That(mapped, Is.True, "Critical must reach the build as a diagnostic"); Assert.That(descriptor.DefaultSeverity, Is.EqualTo(DiagnosticSeverity.Error)); } /// /// Levels below Warning still map to nothing - they are diagnostics-only /// tracing, and the logger reports them as disabled. /// [TestCase(Microsoft.Extensions.Logging.LogLevel.Trace)] [TestCase(Microsoft.Extensions.Logging.LogLevel.Debug)] [TestCase(Microsoft.Extensions.Logging.LogLevel.Information)] public void SubWarningLogLevelsMapToNothing( Microsoft.Extensions.Logging.LogLevel level) { Assert.That( SourceGenerator.TryGetDiagnostic(level, default, out _), Is.False); } /// /// Regression: MODELGEN003 declares two placeholders, so reporting it /// with a single argument rendered the raw template. The [DataType] /// paths now pass the annotated type name and the message separately. /// [Test] public void ExceptionDescriptorWithBothArgumentsRendersTheMessage() { Diagnostic diagnostic = Diagnostic.Create( SourceGenerator.Exception, Location.None, "TestApp.Config.ServerConfig", "InvalidOperationException: boom"); string rendered = diagnostic.GetMessage(CultureInfo.InvariantCulture); Assert.That(rendered, Does.Contain("TestApp.Config.ServerConfig")); Assert.That(rendered, Does.Contain("boom")); Assert.That(rendered, Does.Not.Contain("{0}")); Assert.That(rendered, Does.Not.Contain("{1}")); } /// /// The formatted message is emitted safely even when it contains brace /// characters that would otherwise be interpreted as format placeholders. /// [Test] public void CreateFormattedDiagnosticIsSafeForBracesInMessage() { const string message = "value {0} was not {closed"; Diagnostic diagnostic = SourceGenerator.CreateFormattedDiagnostic( SourceGenerator.GenericError, message); string rendered = diagnostic.GetMessage(CultureInfo.InvariantCulture); Assert.That(rendered, Is.EqualTo(message)); } } }