/* ========================================================================
* 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;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
using NUnit.Framework;
using Opc.Ua.Di;
using Opc.Ua.IA;
using Opc.Ua.Machinery;
using Opc.Ua.Tests;
using Opc.Ua.Vision.MachineVision;
using Opc.Ua.Vision.MachineVision.AMCM;
using Opc.Ua.Vision.Perception;
using Opc.Ua.Vision.Server;
namespace Opc.Ua.Vision.Tests
{
///
/// Pins the independent released models and their generated runtime contracts.
///
[TestFixture]
[Category("Vision")]
public sealed class MachineVisionModelTests
{
///
/// Published model data is retained byte-for-byte, including upstream identifiers.
///
[TestCase("Opc.Ua.MachineVision.NodeSet2.xml", MachineVisionModels.ControlNodeSetSha256)]
[TestCase("Opc.Ua.MachineVision.AMCM.NodeSet2.xml", MachineVisionModels.AssetNodeSetSha256)]
public void PublishedNodeSetIsUnmodified(string file, string expectedHash)
{
using var hash = SHA256.Create();
using FileStream stream = File.OpenRead(
Path.Combine(TestContext.CurrentContext.TestDirectory, "Model", file));
Assert.That(
CoreUtils.ToHexString(hash.ComputeHash(stream)),
Is.EqualTo(expectedHash));
}
///
/// Part 2 is an independent, observational model, not a Part 1 extension.
///
[Test]
public void AssetModelHasNoMethodsAndRequiresMachineryNotControl()
{
XDocument model = ReadModel("Opc.Ua.MachineVision.AMCM.NodeSet2.xml");
Assert.That(model.Descendants(s_ns + "UAMethod"), Is.Empty);
string?[] dependencies =
[.. model.Descendants(s_ns + "RequiredModel").Select(element => (string?)element.Attribute("ModelUri"))];
Assert.That(dependencies, Does.Contain(Machinery.Namespaces.Machinery));
Assert.That(dependencies, Does.Not.Contain(MachineVisionModels.ControlNamespaceUri));
XElement placeholder = model.Root!.Elements().Single(
element => (string?)element.Attribute("NodeId") == "ns=1;i=5004");
Assert.That(
placeholder.Descendants(s_ns + "Reference").Single(
reference => (string?)reference.Attribute("ReferenceType") == "HasModellingRule").Value,
Is.EqualTo("i=11510"));
}
///
/// All model loaders can coexist and preserve the distinct namespace identities.
///
[Test]
public void GeneratedModelsLoadTogetherAndCreateTypedInstances()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var messageContext = ServiceMessageContext.Create(telemetry);
var context = new SystemContext(telemetry)
{
NamespaceUris = messageContext.NamespaceUris,
EncodeableFactory = messageContext.Factory
};
foreach (string uri in new VisionModelProvider(MachineVisionParts.All).NamespaceUris)
{
context.NamespaceUris.GetIndexOrAppend(uri);
}
foreach (string uri in new VisionMachineryStandaloneModelProvider().NamespaceUris)
{
context.NamespaceUris.GetIndexOrAppend(uri);
}
var nodes = new NodeStateCollection();
nodes.AddOpcUaDi(context);
nodes.AddOpcUaIA(context);
nodes.AddOpcUaMachinery(context);
nodes.AddOpcUaVisionPerception(context);
Assert.That(nodes.AddOpcUaMachineVision(context), Is.SameAs(nodes));
Assert.That(nodes.AddOpcUaMachineVisionAMCM(context), Is.SameAs(nodes));
VisionSystemState control = context.CreateInstanceOfVisionSystemType();
VisionSystemAssetState asset = context.CreateInstanceOfVisionSystemAssetType();
Assert.Multiple(() =>
{
Assert.That(control.VisionStateMachine, Is.Not.Null);
Assert.That(asset.Identification, Is.Not.Null);
Assert.That(
control.TypeDefinitionId.NamespaceIndex,
Is.Not.EqualTo(asset.TypeDefinitionId.NamespaceIndex));
Assert.That(context.NamespaceUris.GetIndex(MachineVisionModels.ControlNamespaceUri), Is.GreaterThan(0));
Assert.That(context.NamespaceUris.GetIndex(MachineVisionModels.AssetNamespaceUri), Is.GreaterThan(0));
});
}
///
/// The short loader names register both encodeable and node state factories without changing CLR namespaces.
///
[Test]
public void ModelLoaderNamesRegisterTypedFactories()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var message = ServiceMessageContext.CreateEmpty(telemetry);
IEncodeableFactoryBuilder encodeables = message.Factory.Builder;
Assert.That(encodeables.AddOpcUaMachineVision(), Is.SameAs(encodeables));
Assert.That(encodeables.AddOpcUaMachineVisionAMCM(), Is.SameAs(encodeables));
encodeables.Commit();
Assert.That(message.Factory.ContainsEncodeableType(MachineVision.DataTypeIds.ResultDataType), Is.True);
Assert.That(message.Factory.ContainsEncodeableType(
MachineVision.AMCM.DataTypeIds.SEMI_E10SystemStateInfoDataType), Is.True);
var context = new SystemContext(telemetry) { NamespaceUris = message.NamespaceUris };
context.NamespaceUris.GetIndexOrAppend(MachineVisionModels.ControlNamespaceUri);
context.NamespaceUris.GetIndexOrAppend(MachineVisionModels.AssetNamespaceUri);
var factory = new NodeStateFactory();
Assert.That(factory.AddOpcUaMachineVision(), Is.SameAs(factory));
Assert.That(factory.AddOpcUaMachineVisionAMCM(), Is.SameAs(factory));
Assert.That(factory.CreateInstance(
context, null, NodeClass.Object, new QualifiedName("Control"), ReferenceTypeIds.HasComponent,
ExpandedNodeId.ToNodeId(MachineVision.ObjectTypeIds.VisionSystemType, context.NamespaceUris)),
Is.TypeOf());
Assert.That(factory.CreateInstance(
context, null, NodeClass.Object, new QualifiedName("Asset"), ReferenceTypeIds.HasComponent,
ExpandedNodeId.ToNodeId(
MachineVision.AMCM.ObjectTypeIds.VisionSystemAssetType, context.NamespaceUris)),
Is.TypeOf());
}
///
/// Both models reject missing loader arguments at their public entry points.
///
[Test]
public void ModelLoaderNamesRejectNullArguments()
{
var context = new SystemContext(NUnitTelemetryContext.Create());
var nodes = new NodeStateCollection();
Assert.Multiple(() =>
{
Assert.That(Assert.Throws(() =>
((NodeStateCollection)null!).AddOpcUaMachineVision(context))!.ParamName, Is.EqualTo("nodes"));
Assert.That(Assert.Throws(() =>
((NodeStateCollection)null!).AddOpcUaMachineVisionAMCM(context))!.ParamName, Is.EqualTo("nodes"));
Assert.That(Assert.Throws(() =>
nodes.AddOpcUaMachineVision(null!))!.ParamName, Is.EqualTo("context"));
Assert.That(Assert.Throws(() =>
nodes.AddOpcUaMachineVisionAMCM(null!))!.ParamName, Is.EqualTo("context"));
Assert.That(Assert.Throws(() =>
((IEncodeableFactoryBuilder)null!).AddOpcUaMachineVision())!.ParamName, Is.EqualTo("builder"));
Assert.That(Assert.Throws(() =>
((IEncodeableFactoryBuilder)null!).AddOpcUaMachineVisionAMCM())!.ParamName, Is.EqualTo("builder"));
Assert.That(Assert.Throws(() =>
((INodeStateFactoryBuilder)null!).AddOpcUaMachineVision())!.ParamName, Is.EqualTo("builder"));
Assert.That(Assert.Throws(() =>
((INodeStateFactoryBuilder)null!).AddOpcUaMachineVisionAMCM())!.ParamName, Is.EqualTo("builder"));
});
}
///
/// Existing hosting remains opt-in and invalid model selections are rejected.
///
[Test]
public void ReleasedModelsAreOptInAndModelNamespacesCannotOwnInstances()
{
var options = new VisionServerOptions();
Assert.That(options.MachineVisionParts, Is.EqualTo(MachineVisionParts.None));
options.MachineVisionParts = (MachineVisionParts)8;
Assert.Throws(options.Validate);
options.MachineVisionParts = MachineVisionParts.All;
options.InstanceNamespaceUri = MachineVisionModels.AssetNamespaceUri;
Assert.Throws(options.Validate);
}
private static XDocument ReadModel(string file)
{
return XDocument.Load(Path.Combine(TestContext.CurrentContext.TestDirectory, "Model", file));
}
private static readonly XNamespace s_ns = "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd";
}
}