diff --git a/plans/ctt-issues.md b/plans/ctt-issues.md index c8f2e2c3ca..7cdf4b4168 100644 --- a/plans/ctt-issues.md +++ b/plans/ctt-issues.md @@ -8,7 +8,7 @@ CTT project configuration notes follow the tables. The procedure for running the - "Resolved / fixed" in Mantis means the fix is in the CTT script repository. It ships with a script build after 1.05.513, so an installed 1.05.513 still shows the failure. -- The ids (1–19, C1–C55, U1–U5) are stable references for notes and commit messages; missing ids were +- The ids (1–19, C1–C56, U1–U5) are stable references for notes and commit messages; missing ids were withdrawn or no longer fail against the reference server. - Mantis states were last checked on 2026-09-25. @@ -167,6 +167,13 @@ CTT project configuration notes follow the tables. The procedure for running the > found there. A GDS that also exposes AliasNames for its own nodes (its AliasFor targets have ServerIndex 0) > fails although it aggregated nothing. Please only report AliasNameType instances that have an AliasFor > reference whose target ExpandedNodeId has a ServerIndex other than 0. +- **C56. Monitored Items Deadband Filter `Err-006.js`** modifies a MonitoredItem on the first static numeric + scalar (a BaseDataVariable without EURange) to a PercentDeadband and accepts only `Good` or + `Bad_MonitoredItemFilterUnsupported`. OPC 10000-8 §7.2 limits PercentDeadband to AnalogItems with an EURange, + and §7.3.2 Table 61 defines `Bad_DeadbandFilterInvalid` for "a PercentDeadband is not supported, since an + EURange is not configured", which the server returns since #4629 (Create and Modify alike). Monitor Basic + `Err-022.js` already accepts `Bad_FilterNotAllowed` for non-analog items; the accepted list of `Err-006.js` + needs `Bad_DeadbandFilterInvalid`. Draft ready, not filed yet. - **Aggregate expected-value differences.** - Non-numeric nodes: status-only aggregates (DurationGood/Bad, PercentGood/Bad, WorstQuality2, DurationInState*) differ on Boolean/String nodes from numeric nodes with the same status timeline, and the CTT returns diff --git a/samples/Quickstarts.Servers/ReferenceServer/Model/ReferenceServer.NodeSet2.xml b/samples/Quickstarts.Servers/ReferenceServer/Model/ReferenceServer.NodeSet2.xml index 5f7ecc1896..7f4667afe1 100644 --- a/samples/Quickstarts.Servers/ReferenceServer/Model/ReferenceServer.NodeSet2.xml +++ b/samples/Quickstarts.Servers/ReferenceServer/Model/ReferenceServer.NodeSet2.xml @@ -56318,6 +56318,7 @@ ucclQekzV2XU/r54d++wA0fvXZkJ9Kqh7A93bFFRYA== ns=1;s=DataAccess_ArrayItemType ns=1;s=DataAccess_SelectionList ns=1;s=DataAccess_Currency + ns=1;s=Views_Maintenance i=61 ns=1;s=CTT @@ -60583,6 +60584,7 @@ ucclQekzV2XU/r54d++wA0fvXZkJ9Kqh7A93bFFRYA== ns=1;s=Views_Operations ns=1;s=Views_Engineering + ns=1;s=Views_Maintenance i=61 ns=1;s=CTT @@ -60590,12 +60592,19 @@ ucclQekzV2XU/r54d++wA0fvXZkJ9Kqh7A93bFFRYA== Operations - ns=1;s=Views_Engineering - ns=1;s=Views_Engineering + ns=1;s=Views_Engineering + ns=1;s=Views_Maintenance ns=1;s=Scalar_Static_Mass i=87 + + Maintenance + + ns=1;s=DataAccess + i=87 + + Engineering diff --git a/tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs b/tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs index 4383d2216f..932750dd59 100644 --- a/tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs +++ b/tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs @@ -1069,6 +1069,47 @@ namespace Opc.Ua.Server.Tests Assert.That(references.ToArray().Count(r => r.NodeClass != nodeClass), Is.GreaterThanOrEqualTo(1)); } + /// + /// Views can only be the source of hierarchical References (Part 3 §5.4), and the + /// CTT (Base Info Core Structure 2 001.js) also rejects Views targeted by + /// non-hierarchical References. + /// + [Test] + [TestCase("Views_Operations")] + [TestCase("Views_Engineering")] + [TestCase("Views_Maintenance")] + public async Task SampleViewsHaveNoNonHierarchicalReferencesAsync(string identifier) + { + ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex( + Quickstarts.ReferenceServer.Namespaces.ReferenceServer); + ArrayOf nodesToBrowse = + [ + new BrowseDescription + { + NodeId = new NodeId(identifier, namespaceIndex), + BrowseDirection = BrowseDirection.Both, + ReferenceTypeId = ReferenceTypeIds.NonHierarchicalReferences, + IncludeSubtypes = true, + NodeClassMask = 0, + ResultMask = (uint)BrowseResultMask.All + } + ]; + + var requestHeader = (RequestHeader)m_requestHeader.Clone(); + requestHeader.Timestamp = DateTimeUtc.Now; + BrowseResponse response = await m_server.BrowseAsync( + m_secureChannelContext, + requestHeader, + null, + 0, + nodesToBrowse, + RequestLifetime.None).ConfigureAwait(false); + + Assert.That(response.Results, Has.Count.EqualTo(1)); + Assert.That(response.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); + Assert.That(response.Results[0].References.ToArray(), Is.Empty); + } + /// /// Test that the ReferenceNodeManager Currency variable exposes its value /// and its CurrencyUnit property. diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs index e3b0b5ced9..30aba8d712 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs @@ -1732,6 +1732,63 @@ namespace Opc.Ua.SourceGeneration.Generator.Tests Assert.That(CompileGeneratedAssembly(files), Is.Not.Null); } + /// + /// The source of a GeneratesEvent reference shall be an ObjectType, a VariableType + /// or a Method (Part 3 §7.15). An ObjectType or VariableType keeps the references it + /// declares, but its Object and Variable instances and instance declarations must not + /// inherit them (the CTT flagged + /// the GDS AuthorizationServiceType placeholder and the Default service). + /// + [Test] + public void TypeGeneratesEventReferencesAreNotCopiedToObjectInstances() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); + Dictionary files = GenerateFromModelDesign( + "TypeGeneratesEvent.ModelDesign.xml", + telemetry); + + string code = files.Single( + kv => kv.Key.EndsWith(".NodeStates.ex.g.cs", StringComparison.Ordinal)).Value; + const string generatesEvent = "state.AddReference(global::Opc.Ua.NodeId.Create(41u, "; + const string alwaysGeneratesEvent = "state.AddReference(global::Opc.Ua.NodeId.Create(3065u, "; + + // The public factory definition; call sites are qualified by "context.". + int instanceOf = code.IndexOf(" CreateInstanceOfThingType(", StringComparison.Ordinal); + Assert.That(instanceOf, Is.GreaterThanOrEqualTo(0)); + string instanceOfThingType = code[instanceOf..code.IndexOf( + "return state;", + instanceOf, + StringComparison.Ordinal)]; + + Assert.Multiple(() => + { + string type = ExtractMethodBody(code, "CreateThingType"); + Assert.That(type, Does.Contain(generatesEvent)); + Assert.That(type, Does.Contain(alwaysGeneratesEvent)); + + Assert.That(ExtractMethodBody(code, "CreateThingValueType"), Does.Contain(generatesEvent)); + + foreach (string instance in new[] + { + ExtractMethodBody(code, "CreateThingsFolderType_Thing_Placeholder"), + ExtractMethodBody(code, "CreateThing1"), + instanceOfThingType, + ExtractMethodBody(code, "CreateThingType_Value"), + ExtractMethodBody(code, "CreateThing1_Value"), + ExtractMethodBody(code, "CreateThingValue1") + }) + { + Assert.That(instance, Does.Not.Contain(generatesEvent)); + Assert.That(instance, Does.Not.Contain(alwaysGeneratesEvent)); + } + + Assert.That(ExtractMethodBody(code, "CreateThingType_Start"), Does.Contain(generatesEvent)); + Assert.That(ExtractMethodBody(code, "CreateThing1_Start"), Does.Contain(generatesEvent)); + Assert.That(ExtractMethodBody(code, "CreateExplicitSource"), Does.Contain(generatesEvent)); + }); + Assert.That(CompileGeneratedAssembly(files), Is.Not.Null); + } + private static Dictionary GenerateFromNodeSet( string nodeSetResource, ITelemetryContext telemetry) diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/TypeGeneratesEvent.ModelDesign.xml b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/TypeGeneratesEvent.ModelDesign.xml new file mode 100644 index 0000000000..906960ab02 --- /dev/null +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/TypeGeneratesEvent.ModelDesign.xml @@ -0,0 +1,104 @@ + + + + + http://test.org/UA/TypeGeneratesEvent/ + http://opcfoundation.org/UA/ + + + + + + + + ua:GeneratesEvent + ThingEventType + + + + + + + + + + + ua:GeneratesEvent + ThingEventType + + + + + + + ua:GeneratesEvent + ThingEventType + + + ua:AlwaysGeneratesEvent + ua:BaseEventType + + + + + + + + <Thing> + ua:Organizes + + + + + + + + ua:Organizes + ua:ObjectsFolder + + + + + + + + ua:Organizes + ua:ObjectsFolder + + + + + + + + ua:Organizes + ua:ObjectsFolder + + + ua:GeneratesEvent + ThingEventType + + + + diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs index 816a6e8da2..bb1647b5d0 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs @@ -4208,6 +4208,17 @@ namespace Opc.Ua.SourceGeneration bool isInverse = reference.IsInverse; + // The type's GeneratesEvent references describe its instances; an Object or + // Variable cannot be their source (Part 3 §7.15), so they stay on the type. + if (!isInverse && + reference.DefinedOnType && + reference.SourcePath == node.Path && + root is ObjectDesign or VariableDesign && + IsGeneratesEventReference(reference.ReferenceType)) + { + continue; + } + if (reference.TargetId != null) { if (!m_context.ModelDesign.TryFindNode( @@ -4302,6 +4313,37 @@ namespace Opc.Ua.SourceGeneration return references; } + /// + /// True for GeneratesEvent and its subtypes such as AlwaysGeneratesEvent. + /// + private bool IsGeneratesEventReference(XmlQualifiedName referenceTypeId) + { + var generatesEvent = new XmlQualifiedName("GeneratesEvent", Ua.Types.Namespaces.OpcUa); + if (referenceTypeId == generatesEvent) + { + return true; + } + if (referenceTypeId == null || + !m_context.ModelDesign.TryFindNode( + referenceTypeId, + string.Empty, + string.Empty, + out NodeDesign design)) + { + return false; + } + for (TypeDesign type = (design as ReferenceTypeDesign)?.BaseTypeNode; + type != null; + type = type.BaseTypeNode) + { + if (type.SymbolicId == generatesEvent) + { + return true; + } + } + return false; + } + private List GetAdditionalChildren(NodeToGenerate node) { List additionalChildren = []; diff --git a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesign.cs b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesign.cs index 3f026f8092..04174d39db 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesign.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesign.cs @@ -718,7 +718,7 @@ namespace Opc.Ua.Schema.Model public XmlQualifiedName TargetId { get; set; } /// - /// Defined on type + /// Declared on a type rather than on an instance or instance declaration. /// public bool DefinedOnType { get; set; } diff --git a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs index 0b69bec05b..caa3ec7caf 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs @@ -6082,6 +6082,7 @@ namespace Opc.Ua.Schema.Model currentPath, source.SymbolicId, source.References[ii]); + reference.DefinedOnType = source is TypeDesign; references.Add(reference);