Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/VariableShapeTests.cs
T
Joseph Doherty ff4e210a27 fix(opcua): replay kept scripted conditions after a rebuild; fit window values onto re-shaped nodes (Lane B fix round 3)
N3 (critical): the node manager carries only NATIVE conditions' state over a
full rebuild; scripted conditions come back fresh and rely on the engine's
re-assert, which since the off-thread plan is handled in the window and lands
on the discarded instance. On a rebuild the condition replay scope is now
AddedConditions plus every enabled scripted condition in the new composition
(native kept conditions stay carried-over, not replayed). The test sink again
models the real rebuild: it clears non-native condition state only.

N4: a surgical in-place shape edit (DataType / IsArray / ArrayLength) resets the
raw node and its UNS mirror to BadWaitingForInitialData without ensuring them,
so the window's value was not replayed and the two NodeIds disagreed. The
applier records re-shaped nodes (MaterialisationRecording.Reshaped); their
deferred value replays only once fitted to the new shape (VariableShape.TryFit:
as is, or converted invariantly; never an old-typed value - WriteValues assigns
the value verbatim with no type check), otherwise it is dropped at Debug and the
node keeps waiting. Status-only entries are not replayed onto a re-shaped node.

Minors: m1/m2 documented on _replayScope (degraded-path only); m3 corrected the
false carry-over premise in the scope doc (the ScriptedAlarmHostActor ordering
remark is outside this lane - F8); m4 the replay Debug log reports alarm-state
and alarm-quality counts again.

Refs #732
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:46:29 -04:00

65 lines
2.6 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// Lane B review N4 — <see cref="VariableShape.TryFit"/> decides whether a value deferred during a plan window may
/// be replayed onto a node an in-place edit re-shaped: never an old-typed value the new DataType contradicts.
/// </summary>
public sealed class VariableShapeTests
{
[Fact]
public void A_value_of_the_new_type_fits_unchanged()
{
new VariableShape("Int32", false, null).TryFit(7, out var fitted).ShouldBeTrue();
fitted.ShouldBe(7);
}
[Fact]
public void A_convertible_old_typed_value_is_converted_to_the_new_type()
{
new VariableShape("Int32", false, null).TryFit(5.0f, out var fitted).ShouldBeTrue();
fitted.ShouldBeOfType<int>().ShouldBe(5);
new VariableShape("Double", false, null).TryFit((short)3, out var d).ShouldBeTrue();
d.ShouldBeOfType<double>().ShouldBe(3d);
}
[Theory]
[InlineData("Int32", "fast")] // not a number
[InlineData("Byte", 300)] // overflow
[InlineData("DateTime", 12)] // no conversion
public void A_value_the_new_type_cannot_hold_does_not_fit(string dataType, object value)
{
new VariableShape(dataType, false, null).TryFit(value, out var fitted).ShouldBeFalse();
fitted.ShouldBeNull();
}
[Fact]
public void Scalar_and_array_never_cross()
{
new VariableShape("Int32", false, null).TryFit(new[] { 1, 2 }, out _).ShouldBeFalse();
new VariableShape("Int32", true, 2).TryFit(1, out _).ShouldBeFalse();
new VariableShape("String", true, null).TryFit("ab", out _).ShouldBeFalse("a string is not an array of strings");
}
[Fact]
public void An_array_is_converted_element_wise_and_its_fixed_length_enforced()
{
new VariableShape("Int16", true, 3).TryFit(new[] { 1, 2, 3 }, out var fitted).ShouldBeTrue();
fitted.ShouldBeOfType<short[]>().ShouldBe(new short[] { 1, 2, 3 });
new VariableShape("Int16", true, 4).TryFit(new[] { 1, 2, 3 }, out _).ShouldBeFalse("wrong length");
new VariableShape("Int16", true, null).TryFit(new[] { 1, 2, 3 }, out _).ShouldBeTrue("no fixed length");
new VariableShape("Byte", true, null).TryFit(new[] { 1, 999 }, out _).ShouldBeFalse("an element overflows");
}
[Fact]
public void Null_and_unknown_types_always_fit()
{
new VariableShape("Int32", false, null).TryFit(null, out var n).ShouldBeTrue();
n.ShouldBeNull();
new VariableShape("Structure", false, null).TryFit("anything", out var any).ShouldBeTrue();
any.ShouldBe("anything");
}
}