/* ========================================================================
* Copyright (c) 2005-2025 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.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.SourceGeneration.Generator.Tests
{
///
/// Unit tests for cross-pass [NodeManager] binding diagnostics.
/// Bindings are resolved across two independent generation passes
/// (NodeSet2 and ModelDesign); the unmatched-binding diagnostics must be
/// reported once, after both passes, against a shared "used" set —
/// reporting per pass false-positives a binding matched by the other pass
/// (regression for issue #3937).
///
[TestFixture]
[Category("Generator")]
[Parallelizable(ParallelScope.All)]
public class NodeManagerBindingReportingTests
{
[Test]
public void BindingMatchedByAnyPassIsNotReportedUnmatched()
{
// The binding is recorded in the shared used-set (matched by one
// pass). Even though this reporter call also spans the other pass
// that never matched it, it must stay silent. This is the core
// #3937 case: a NodeSet2 model matched, a ModelDesign model did not.
NodeManagerAttributeBinding matched = CreateBinding(namespaceUri: "urn:types");
HashSet used = [matched];
var reports = new List();
Generators.ReportUnmatchedNodeManagerBindings(
[matched],
used,
totalModelCount: 2,
(_, message) => reports.Add(message));
Assert.That(reports, Is.Empty);
}
[Test]
public void BindingMatchedByNoPassIsReportedOnce()
{
NodeManagerAttributeBinding orphan = CreateBinding(namespaceUri: "urn:missing");
var reports = new List();
Generators.ReportUnmatchedNodeManagerBindings(
[orphan],
[],
totalModelCount: 2,
(_, message) => reports.Add(message));
Assert.That(reports, Has.Count.EqualTo(1));
Assert.That(reports[0], Does.Contain("did not match any model"));
Assert.That(reports[0], Does.Contain("NamespaceUri='urn:missing'"));
}
[Test]
public void SelectorlessBindingWithMultipleModelsIsReportedAsAmbiguity()
{
NodeManagerAttributeBinding selectorless = CreateBinding();
var reports = new List();
Generators.ReportUnmatchedNodeManagerBindings(
[selectorless],
[],
totalModelCount: 2,
(_, message) => reports.Add(message));
Assert.That(reports, Has.Count.EqualTo(1));
Assert.That(reports[0], Does.Contain("multiple models"));
Assert.That(reports[0], Does.Contain("Specify NamespaceUri"));
}
[Test]
public void SelectorlessBindingWithSingleModelIsReportedAsUnmatched()
{
// With a single model a selector-less binding is not ambiguous; it
// simply did not match (for example the model was skipped).
NodeManagerAttributeBinding selectorless = CreateBinding();
var reports = new List();
Generators.ReportUnmatchedNodeManagerBindings(
[selectorless],
[],
totalModelCount: 1,
(_, message) => reports.Add(message));
Assert.That(reports, Has.Count.EqualTo(1));
Assert.That(reports[0], Does.Contain("did not match any model"));
Assert.That(reports[0], Does.Contain("(no selector)"));
}
[Test]
public void ReportIsNoOpWhenBindingsAreNullOrEmpty()
{
Assert.DoesNotThrow(() =>
Generators.ReportUnmatchedNodeManagerBindings(
null!, null!, 2, (_, _) => Assert.Fail("should not report")));
Assert.DoesNotThrow(() =>
Generators.ReportUnmatchedNodeManagerBindings(
[], null!, 2, (_, _) => Assert.Fail("should not report")));
}
[Test]
public void GenerateCodeDefersUnmatchedReportWhenSharedSetProvided()
{
// In shared (multi-pass) mode the pass must not self-report an
// unmatched binding; the caller reports once after all passes.
var reports = new List();
RunDesignGenerate(
CreateBinding(namespaceUri: "urn:not:in:test:model"),
sharedUsedBindings: [],
report: (_, message) => reports.Add(message));
Assert.That(
reports,
Is.Empty,
"a shared-mode pass must defer unmatched-binding reporting");
}
[Test]
public void GenerateCodeReportsUnmatchedWhenNoSharedSet()
{
// Single-pass mode (no shared set): the pass reports directly.
var reports = new List();
RunDesignGenerate(
CreateBinding(namespaceUri: "urn:not:in:test:model"),
sharedUsedBindings: null!,
report: (_, message) => reports.Add(message));
Assert.That(reports, Has.Count.EqualTo(1));
Assert.That(reports[0], Does.Contain("did not match any model"));
}
private static NodeManagerAttributeBinding CreateBinding(
string? namespaceUri = null,
string? design = null,
string className = "TestNodeManager")
{
return new NodeManagerAttributeBinding
{
TargetNamespace = "Test",
TargetClassName = className,
NamespaceUri = namespaceUri!,
Design = design!
};
}
private static void RunDesignGenerate(
NodeManagerAttributeBinding binding,
HashSet sharedUsedBindings,
System.Action report)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error);
using var fileSystem = new VirtualFileSystem();
string resources = Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources");
new DesignFileCollection
{
Targets = [Path.Combine(resources, "TestModel.xml")],
IdentifierFilePath = Path.Combine(resources, "TestModel.csv")
}.GenerateCode(
fileSystem,
string.Empty,
telemetry,
nodeManagerBindings: [binding],
reportBindingDiagnostic: report,
sharedUsedBindings: sharedUsedBindings);
}
}
}