Files
Marc Schier 15d0ce7bd5 Implement OPC UA Part 10 Programs (#4789)
# Description

Implement OPC UA Part 10 Programs across `Opc.Ua.Core`/`Core.Types`,
`Opc.Ua.Server`, and `Opc.Ua.Client`, reusing the existing
state-machine, source-generation, node-management, auditing, and
streaming-subscription infrastructure.

- Add asynchronous provider-backed execution, standard controls,
internal/autonomous transitions, generation checks, and nested-state
activation/resumption.
- Add checked creation/adoption, vendor Create adapters, deletion,
recycling and instance limits, auto-delete, DI, and direct-construction
paths.
- Maintain ProgramDiagnostic2 and retained final results; publish
captured intermediate results and concrete transition/audit events with
access controls.
- Add generated SDK models/proxies, typed client discovery and reads,
event streams, and subscribe-before-call readiness handling.
- Add unit, connected client/server, generated Annex A-style model, and
NativeAOT coverage. Document usage and migration in `docs/Programs.md`
and related guides.

### Compatibility and design decisions

The linked Part 10 document is v1.05.00; namespace-zero definitions
remain aligned with 1.05.07. Documentation distinguishes normative
requirements from conflicting specification text and SDK conventions.

- Correct state numbers to Halted=11, Ready=12, Running=13,
Suspended=14. Generated subtypes retain inherited namespace-zero
state/transition identifiers.
- Managed Reset is Halted-to-Ready only. Preserve the existing low-level
Reset-from-Suspended behavior for compatibility.
- Preserve UInt32 limit types; use `UInt32.MaxValue` as a documented SDK
unlimited convention. RecycleCount overflow reports BadOutOfRange
without stopping unlimited execution.
- In-memory default with injectable providers. Durable recovery,
distributed execution/failover, and a production Annex A download engine
are excluded.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Tests and focused coverage evidence are above; the combined
tests/coverage checkbox remains open because a whole-repository coverage
increase has not been established. CLA status, CI/CodeQL results,
tracking-issue approval, and PR feedback are not asserted.
2026-10-15 09:19:25 +02:00

801 lines
28 KiB
C#

/* ========================================================================
* 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;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Core.Tests.Stack.State
{
/// <summary>
/// Tests for the NodeState classes.
/// </summary>
[TestFixture]
[Category("NodeStateTypes")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public class StateTypesTests
{
public const string ApplicationUri = "uri:localhost:opcfoundation.org:NodeStates";
public const string OpcUa = "http://opcfoundation.org/UA/";
public const string OpcUaOnboarding = "http://opcfoundation.org/UA/Onboarding/";
public IServiceMessageContext Context;
public ITelemetryContext Telemetry;
[DatapointSource]
public Type[] TypeArray = [.. typeof(BaseObjectState).Assembly.GetExportedTypes()
.Where(IsNodeStateType)];
[OneTimeSetUp]
protected void OneTimeSetUp()
{
Telemetry = NUnitTelemetryContext.Create();
Context = ServiceMessageContext.Create(Telemetry);
NamespaceTable nameSpaceUris = Context.NamespaceUris;
// namespace index 1 must be the ApplicationUri
nameSpaceUris.GetIndexOrAppend(ApplicationUri);
nameSpaceUris.GetIndexOrAppend(Namespaces.OpcUaGds);
nameSpaceUris.GetIndexOrAppend(OpcUaOnboarding);
nameSpaceUris.GetIndexOrAppend(global::Opc.Ua.Programs.Namespaces.OpcUaPrograms);
}
[OneTimeTearDown]
protected void OneTimeTearDown()
{
(Context as IDisposable)?.Dispose();
}
/// <summary>
/// Verify independent ownership throughout copied alarm subtrees.
/// </summary>
[TestCase(false)]
[TestCase(true)]
public void CopiedAlarmChildrenHaveIndependentOwnership(bool useClone)
{
var context = new SystemContext(Telemetry) { NamespaceUris = Context.NamespaceUris };
var original = new BaseObjectState(null)
{
NodeId = new NodeId(1000),
BrowseName = QualifiedName.From("Root")
};
var alarm = new OffNormalAlarmState(original);
alarm.Create(context, new NodeId(1001), QualifiedName.From("Alarm"), LocalizedText.From("Alarm"), false);
original.AddChild(alarm);
BaseObjectState copy;
if (useClone)
{
copy = (BaseObjectState)original.Clone();
}
else
{
copy = new BaseObjectState(null);
copy.Create(context, original);
}
int count = AssertOwnedChildren(original, copy);
Assert.That(count, Is.GreaterThan(30), "The inherited alarm subtree must be exercised.");
int AssertOwnedChildren(NodeState source, NodeState target)
{
var children = new List<BaseInstanceState>();
var copiedChildren = new List<BaseInstanceState>();
source.GetChildren(context, children);
target.GetChildren(context, copiedChildren);
Assert.That(copiedChildren, Has.Count.EqualTo(children.Count));
int visited = children.Count;
foreach (BaseInstanceState child in children)
{
BaseInstanceState copiedChild = target.FindChild(context, child.BrowseName)!;
Assert.That(copiedChild, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(copiedChild, Is.Not.SameAs(child), child.BrowseName.ToString());
Assert.That(copiedChild.Parent, Is.SameAs(target), child.BrowseName.ToString());
Assert.That(child.Parent, Is.SameAs(source), child.BrowseName.ToString());
Assert.That(copiedChild.NodeId, Is.EqualTo(child.NodeId));
});
visited += AssertOwnedChildren(child, copiedChild);
LocalizedText originalName = child.DisplayName;
copiedChild.DisplayName = LocalizedText.From("Changed copy");
Assert.That(child.DisplayName, Is.EqualTo(originalName));
}
return visited;
}
}
/// <summary>
/// Verify activation of a NodeState type.
/// </summary>
[Theory]
public void ActivateNodeStateType(Type systemType)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testObject = CreateDefaultNodeStateType(systemType) as NodeState;
Assert.That(testObject, Is.Not.Null);
var context = new SystemContext(telemetry) { NamespaceUris = Context.NamespaceUris };
Assert.That(context.NamespaceUris.GetIndexOrAppend(OpcUa), Is.Zero);
testObject.Create(context, new NodeId(1000), QualifiedName.From("Name"), LocalizedText.From("DisplayName"), true);
}
/// <summary>
/// Instantiate NodeState types across Opc.Ua assemblies and fail on placeholder children.
/// </summary>
[Test]
public void NodeStateTypesAcrossOpcUaAssembliesShouldNotInstantiatePlaceholderChildren()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var context = new SystemContext(telemetry) { NamespaceUris = Context.NamespaceUris };
var placeholders = new List<string>();
uint nodeId = 200000;
foreach (Type systemType in GetOpcUaNodeStateTypes().OrderBy(type => type.FullName))
{
if (CreateDefaultNodeStateType(systemType) is not NodeState testObject)
{
continue;
}
testObject.Create(context, new NodeId(nodeId++),
QualifiedName.From("Name"), LocalizedText.From("DisplayName"), true);
CollectInstantiatedPlaceholders(
context,
testObject,
systemType.Assembly.GetName().Name!,
systemType.FullName!,
placeholders);
}
Assert.That(
placeholders,
Is.Empty,
"Instantiated placeholder children were found:" +
Environment.NewLine + string.Join(Environment.NewLine, placeholders));
}
/// <summary>
/// Create an instance of a NodeState type with default values.
/// </summary>
/// <param name="systemType">The type to create</param>
private static object CreateDefaultNodeStateType(Type systemType)
{
System.Reflection.TypeInfo systemTypeInfo = systemType.GetTypeInfo();
object instance;
try
{
if (typeof(BaseObjectState).GetTypeInfo().IsAssignableFrom(systemTypeInfo) ||
typeof(BaseVariableState).GetTypeInfo().IsAssignableFrom(systemTypeInfo) ||
typeof(MethodState).GetTypeInfo().IsAssignableFrom(systemTypeInfo))
{
instance = Activator.CreateInstance(systemType, (NodeState)null!)!;
}
else if (systemType.IsAbstract)
{
instance = null!;
}
else
{
ConstructorInfo? defaultConstructor = systemType.GetConstructor([]);
if (defaultConstructor == null || !defaultConstructor.IsPublic)
{
instance = null!;
}
else
{
instance = Activator.CreateInstance(systemType)!;
}
}
}
catch
{
return null!;
}
return instance!;
}
/// <summary>
/// Return true if system Type is IEncodeable.
/// </summary>
private static bool IsNodeStateType(Type systemType)
{
if (systemType == null)
{
return false;
}
System.Reflection.TypeInfo systemTypeInfo = systemType.GetTypeInfo();
if (systemTypeInfo.IsAbstract ||
systemTypeInfo.IsGenericType ||
systemTypeInfo.IsGenericTypeDefinition ||
!typeof(NodeState).GetTypeInfo().IsAssignableFrom(systemTypeInfo))
{
return false;
}
return CreateDefaultNodeStateType(systemType) is NodeState;
}
/// <summary>
/// Recursively collect instantiated placeholder children for diagnostics.
/// </summary>
private static void CollectInstantiatedPlaceholders(
ISystemContext context,
NodeState nodeState,
string ownerAssembly,
string ownerType,
List<string> placeholders)
{
var children = new List<BaseInstanceState>();
nodeState.GetChildren(context, children);
foreach (BaseInstanceState child in children)
{
string browseName = child.BrowseName.Name ?? string.Empty;
bool hasPlaceholderName =
browseName.Length > 1 &&
browseName[0] == '<' &&
browseName[^1] == '>';
bool hasPlaceholderModellingRule =
child.ModellingRuleId == ObjectIds.ModellingRule_OptionalPlaceholder ||
child.ModellingRuleId == ObjectIds.ModellingRule_MandatoryPlaceholder;
if (hasPlaceholderName || hasPlaceholderModellingRule)
{
string modellingRule = child.ModellingRuleId.ToString();
placeholders.Add(
$"{ownerAssembly}: {ownerType}: {child.GetDisplayPath()} (BrowseName='{browseName}', ModellingRuleId='{modellingRule}')");
}
CollectInstantiatedPlaceholders(
context,
child,
ownerAssembly,
ownerType,
placeholders);
}
}
/// <summary>
/// Discover loadable public NodeState types from reachable Opc.Ua assemblies.
/// </summary>
private static IEnumerable<Type> GetOpcUaNodeStateTypes()
{
var assemblies = new Dictionary<string, Assembly>(StringComparer.OrdinalIgnoreCase);
var toScan = new Queue<Assembly>();
void TryEnqueueAssembly(Assembly assembly)
{
if (assembly == null || assembly.IsDynamic)
{
return;
}
AssemblyName assemblyName = assembly.GetName();
if (!assemblyName.Name!.StartsWith("Opc.Ua", StringComparison.Ordinal))
{
return;
}
if (assemblies.ContainsKey(assembly.FullName!))
{
return;
}
assemblies[assembly.FullName!] = assembly;
toScan.Enqueue(assembly);
}
TryEnqueueAssembly(typeof(NodeState).Assembly);
TryEnqueueAssembly(typeof(OrderedListState).Assembly);
TryEnqueueAssembly(typeof(StateTypesTests).Assembly);
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
TryEnqueueAssembly(assembly);
}
while (toScan.Count > 0)
{
Assembly assembly = toScan.Dequeue();
foreach (AssemblyName reference in assembly.GetReferencedAssemblies())
{
if (!reference.Name!.StartsWith("Opc.Ua", StringComparison.Ordinal))
{
continue;
}
try
{
TryEnqueueAssembly(Assembly.Load(reference));
}
catch
{
// Nothing we can do if an assembly fails to load, just skip it.
}
}
}
return assemblies.Values
.SelectMany(GetExportedTypesSafe)
.Where(IsNodeStateType)
.GroupBy(type => type.AssemblyQualifiedName)
.Select(group => group.First());
}
/// <summary>
/// Return exported types while tolerating partial type-load failures.
/// </summary>
private static IEnumerable<Type> GetExportedTypesSafe(Assembly assembly)
{
try
{
return assembly.GetExportedTypes();
}
catch (ReflectionTypeLoadException e)
{
// Continue with loadable public types if some types in the assembly fail to load.
return e.Types.OfType<Type>().Where(type => type.IsPublic);
}
}
}
/// <summary>
/// Tests for BaseEventState.MemberwiseClone.
/// </summary>
[TestFixture]
[Category("BaseEventState")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public class BaseEventStateTests
{
/// <summary>
/// Verify that Clone works correctly for BaseEventState.
/// </summary>
[Test]
public void CloneBaseEventStateSucceeds()
{
var parent = new BaseObjectState(null);
var eventState = new BaseEventState(parent);
var clone = (BaseEventState)eventState.Clone();
Assert.That(clone, Is.Not.Null);
Assert.That(clone.Parent, Is.SameAs(parent));
}
/// <summary>
/// Verify that Clone works correctly for BaseEventState with null parent.
/// </summary>
[Test]
public void CloneBaseEventStateWithNullParentSucceeds()
{
var eventState = new BaseEventState(null);
var clone = (BaseEventState)eventState.Clone();
Assert.That(clone, Is.Not.Null);
Assert.That(clone.Parent, Is.Null);
}
/// <summary>
/// Verify that Clone works correctly for derived event types.
/// </summary>
[Test]
public void CloneNonExclusiveLimitAlarmStateSucceeds()
{
var parent = new BaseObjectState(null);
var alarmState = new NonExclusiveLimitAlarmState(parent);
var clone = (NonExclusiveLimitAlarmState)alarmState.Clone();
Assert.That(clone, Is.Not.Null);
Assert.That(clone.Parent, Is.SameAs(parent));
}
[Test]
public void BaseObjectStateConstructorSetsNodeClass()
{
var node = new BaseObjectState(null);
Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Object));
}
[Test]
public void FolderStateConstructorSetsNodeClass()
{
var node = new FolderState(null);
Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Object));
}
[Test]
public void BaseVariableStateConstructorSetsNodeClass()
{
var node = new BaseDataVariableState(null);
Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Variable));
}
[Test]
public void MethodStateConstructorSetsNodeClass()
{
var node = new MethodState(null);
Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Method));
}
[Test]
public void NodeIdCanBeSetAndRetrieved()
{
var node = new BaseObjectState(null)
{
NodeId = new NodeId(1234, 2)
};
Assert.That(node.NodeId, Is.EqualTo(new NodeId(1234, 2)));
}
[Test]
public void BrowseNameCanBeSetAndRetrieved()
{
var node = new BaseObjectState(null)
{
BrowseName = new QualifiedName("TestObject", 2)
};
Assert.That(node.BrowseName.Name, Is.EqualTo("TestObject"));
Assert.That(node.BrowseName.NamespaceIndex, Is.EqualTo(2));
}
[Test]
public void DisplayNameCanBeSetAndRetrieved()
{
var node = new BaseObjectState(null)
{
DisplayName = new LocalizedText("en", "Test Display")
};
Assert.That(node.DisplayName.Text, Is.EqualTo("Test Display"));
}
[Test]
public void DescriptionCanBeSetAndRetrieved()
{
var node = new BaseObjectState(null)
{
Description = new LocalizedText("en", "A test node")
};
Assert.That(node.Description.Text, Is.EqualTo("A test node"));
}
[Test]
public void AddChildAddsToChildren()
{
var parent = new BaseObjectState(null)
{
NodeId = new NodeId(1, 0),
BrowseName = new QualifiedName("Parent")
};
var child = new BaseObjectState(parent)
{
NodeId = new NodeId(2, 0),
BrowseName = new QualifiedName("Child")
};
parent.AddChild(child);
List<BaseInstanceState> children = [];
parent.GetChildren(null!, children);
Assert.That(children, Has.Count.GreaterThanOrEqualTo(1));
}
[Test]
public void AddReferenceCreatesReference()
{
var node = new BaseObjectState(null)
{
NodeId = new NodeId(100, 0),
BrowseName = new QualifiedName("Source")
};
node.AddReference(
ReferenceTypeIds.Organizes,
false,
new NodeId(200, 0));
// Verify references exist
List<IReference> references = [];
node.GetReferences(null!, references);
Assert.That(references, Has.Count.GreaterThanOrEqualTo(1));
}
[Test]
public void AddMultipleReferences()
{
var node = new BaseObjectState(null)
{
NodeId = new NodeId(100, 0),
BrowseName = new QualifiedName("Source")
};
node.AddReference(ReferenceTypeIds.Organizes, false, new NodeId(200, 0));
node.AddReference(ReferenceTypeIds.HasComponent, false, new NodeId(201, 0));
List<IReference> references = [];
node.GetReferences(null!, references);
Assert.That(references, Has.Count.GreaterThanOrEqualTo(2));
}
[Test]
public void MethodStateExecutableDefault()
{
var method = new MethodState(null);
Assert.That(method.Executable, Is.True);
}
[Test]
public void MethodStateUserExecutableDefault()
{
var method = new MethodState(null);
Assert.That(method.UserExecutable, Is.True);
}
[Test]
public void MethodStateExecutableCanBeSet()
{
var method = new MethodState(null)
{
Executable = false
};
Assert.That(method.Executable, Is.False);
}
[Test]
public void MethodStateUserExecutableCanBeSet()
{
var method = new MethodState(null)
{
UserExecutable = false
};
Assert.That(method.UserExecutable, Is.False);
}
[Test]
public void MethodStateConstructCreatesNewInstance()
{
var parent = new BaseObjectState(null);
NodeState method = MethodState.Construct(parent);
Assert.That(method, Is.Not.Null);
Assert.That(method.NodeClass, Is.EqualTo(NodeClass.Method));
}
[Test]
public void MethodStateDeepEqualsIdentical()
{
var m1 = new MethodState(null)
{
NodeId = new NodeId(1),
BrowseName = new QualifiedName("Method1"),
Executable = true,
UserExecutable = true
};
var m2 = new MethodState(null)
{
NodeId = new NodeId(1),
BrowseName = new QualifiedName("Method1"),
Executable = true,
UserExecutable = true
};
Assert.That(m1.DeepEquals(m2), Is.True);
}
[Test]
public void MethodStateDeepEqualsDifferent()
{
var m1 = new MethodState(null)
{
NodeId = new NodeId(1),
BrowseName = new QualifiedName("Method1"),
Executable = true
};
var m2 = new MethodState(null)
{
NodeId = new NodeId(2),
BrowseName = new QualifiedName("Method2"),
Executable = false
};
Assert.That(m1.DeepEquals(m2), Is.False);
}
[Test]
public void MethodStateInputArgumentsDefault()
{
var method = new MethodState(null);
Assert.That(method.InputArguments, Is.Null);
}
[Test]
public void MethodStateDeepGetHashCodeReturnsValue()
{
var method = new MethodState(null)
{
NodeId = new NodeId(1),
BrowseName = new QualifiedName("Test")
};
int hash = method.DeepGetHashCode();
Assert.That(hash, Is.Not.Zero);
}
[Test]
public void BaseDataVariableStateValueCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
Value = 42
};
Assert.That(variable.Value, Is.EqualTo(42));
}
[Test]
public void BaseDataVariableStateDataTypeCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
DataType = DataTypeIds.Int32
};
Assert.That(variable.DataType, Is.EqualTo(DataTypeIds.Int32));
}
[Test]
public void BaseDataVariableStateValueRankCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
ValueRank = ValueRanks.OneDimension
};
Assert.That(variable.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
}
[Test]
public void BaseDataVariableStateAccessLevelCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
AccessLevel = AccessLevels.CurrentRead
};
Assert.That(variable.AccessLevel, Is.EqualTo(AccessLevels.CurrentRead));
}
[Test]
public void BaseDataVariableStateUserAccessLevelCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
UserAccessLevel = AccessLevels.CurrentReadOrWrite
};
Assert.That(
variable.UserAccessLevel,
Is.EqualTo(AccessLevels.CurrentReadOrWrite));
}
[Test]
public void BaseDataVariableStateHistorizingCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
Historizing = true
};
Assert.That(variable.Historizing, Is.True);
}
[Test]
public void BaseDataVariableStateMinimumSamplingInterval()
{
var variable = new BaseDataVariableState(null)
{
MinimumSamplingInterval = 500.0
};
Assert.That(variable.MinimumSamplingInterval, Is.EqualTo(500.0));
}
[Test]
public void FolderStateAddChildVariable()
{
var folder = new FolderState(null)
{
NodeId = new NodeId(1, 0),
BrowseName = new QualifiedName("MyFolder")
};
var variable = new BaseDataVariableState(folder)
{
NodeId = new NodeId(2, 0),
BrowseName = new QualifiedName("MyVar"),
Value = "hello"
};
folder.AddChild(variable);
var children = new List<BaseInstanceState>();
folder.GetChildren(null!, children);
Assert.That(children, Has.Count.GreaterThanOrEqualTo(1));
}
[Test]
public void NodeStateInitializedDefault()
{
var node = new BaseObjectState(null);
Assert.That(node.Initialized, Is.False);
}
[Test]
public void MethodStateMethodDeclarationIdCanBeSet()
{
var method = new MethodState(null)
{
MethodDeclarationId = new NodeId(999)
};
Assert.That(method.MethodDeclarationId, Is.EqualTo(new NodeId(999)));
}
[Test]
public void MethodStateOnCallMethodHandlerCanBeAssigned()
{
var method = new MethodState(null)
{
OnCallMethod = (context, methodState, inputArgs, outputArgs) => ServiceResult.Good
};
Assert.That(method.OnCallMethod, Is.Not.Null);
}
[Test]
public void MethodStateOnCallMethod2HandlerCanBeAssigned()
{
var method = new MethodState(null)
{
OnCallMethod2 = (context, methodToCall, objectId, inputArgs, outputArgs) => ServiceResult.Good
};
Assert.That(method.OnCallMethod2, Is.Not.Null);
}
[Test]
public void BaseDataVariableStateCopyPolicyDefault()
{
var variable = new BaseDataVariableState(null);
Assert.That(
variable.CopyPolicy,
Is.EqualTo(VariableCopyPolicy.CopyOnRead));
}
[Test]
public void BaseDataVariableStateCopyPolicyCanBeSet()
{
var variable = new BaseDataVariableState(null)
{
CopyPolicy = VariableCopyPolicy.Never
};
Assert.That(variable.CopyPolicy, Is.EqualTo(VariableCopyPolicy.Never));
}
}
}