Add OPC UA array element write integration test

This commit is contained in:
Joseph Doherty
2026-03-25 11:05:04 -04:00
parent 4351854754
commit 3f813b3869
4 changed files with 134 additions and 1 deletions

View File

@@ -363,7 +363,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
{
try
{
var value = nodesToWrite[i].Value.WrappedValue.Value;
var writeValue = nodesToWrite[i];
var value = writeValue.Value.WrappedValue.Value;
if (!string.IsNullOrWhiteSpace(writeValue.IndexRange))
{
if (!TryApplyArrayElementWrite(tagRef, value, writeValue.IndexRange, out var updatedArray))
{
errors[i] = new ServiceResult(StatusCodes.BadIndexRangeInvalid);
continue;
}
value = updatedArray;
}
var success = _mxAccessClient.WriteAsync(tagRef, value).GetAwaiter().GetResult();
errors[i] = success ? ServiceResult.Good : new ServiceResult(StatusCodes.BadInternalError);
}
@@ -376,6 +389,53 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
}
private bool TryApplyArrayElementWrite(string tagRef, object? writeValue, string indexRange, out object updatedArray)
{
updatedArray = null!;
if (!int.TryParse(indexRange, out var index) || index < 0)
return false;
var currentValue = _mxAccessClient.ReadAsync(tagRef).GetAwaiter().GetResult().Value;
if (currentValue is not Array currentArray || currentArray.Rank != 1 || index >= currentArray.Length)
return false;
var nextArray = (Array)currentArray.Clone();
var elementType = currentArray.GetType().GetElementType();
if (elementType == null)
return false;
var normalizedValue = NormalizeIndexedWriteValue(writeValue);
nextArray.SetValue(ConvertArrayElementValue(normalizedValue, elementType), index);
updatedArray = nextArray;
return true;
}
private static object? NormalizeIndexedWriteValue(object? value)
{
if (value is Array array && array.Length == 1)
return array.GetValue(0);
return value;
}
private static object? ConvertArrayElementValue(object? value, Type elementType)
{
if (value == null)
{
if (elementType.IsValueType)
return Activator.CreateInstance(elementType);
return null;
}
if (elementType.IsInstanceOfType(value))
return value;
if (elementType == typeof(string))
return value.ToString();
return Convert.ChangeType(value, elementType);
}
#endregion
#region Subscription Delivery

View File

@@ -55,6 +55,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
public Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
{
WrittenValues.Add((fullTagReference, value));
if (WriteResult)
TagValues[fullTagReference] = Vtq.Good(value);
return Task.FromResult(WriteResult);
}

View File

@@ -114,6 +114,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return Session.ReadValue(nodeId);
}
/// <summary>
/// Write a node's value, optionally using an OPC UA index range for array element writes.
/// Returns the server status code for the write.
/// </summary>
public StatusCode Write(NodeId nodeId, object value, string? indexRange = null)
{
var nodesToWrite = new WriteValueCollection
{
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = indexRange,
Value = new DataValue(new Variant(value))
}
};
Session.Write(null, nodesToWrite, out var results, out _);
return results[0];
}
/// <summary>
/// Create a subscription with a monitored item on the given node.
/// Returns the subscription and monitored item for inspection.

View File

@@ -0,0 +1,50 @@
using System.Linq;
using System.Threading.Tasks;
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
public class ArrayWriteTests
{
[Fact]
public async Task Write_SingleArrayElement_UpdatesWholeArrayValue()
{
var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
fixture.MxAccessClient!.TagValues["MESReceiver_001.MoveInPartNumbers[]"] = Vtq.Good(
Enumerable.Range(0, 50).Select(i => $"PART-{i:00}").ToArray());
await fixture.InitializeAsync();
try
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
// Browse path: TestMachine_001 -> MESReceiver -> MoveInPartNumbers
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInPartNumbers[]");
var before = client.Read(nodeId).Value as string[];
before.ShouldNotBeNull();
before.Length.ShouldBe(50);
before[1].ShouldBe("PART-01");
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
StatusCode.IsGood(status).ShouldBe(true);
var after = client.Read(nodeId).Value as string[];
after.ShouldNotBeNull();
after.Length.ShouldBe(50);
after[0].ShouldBe("PART-00");
after[1].ShouldBe("UPDATED-PART");
after[2].ShouldBe("PART-02");
}
finally
{
await fixture.DisposeAsync();
}
}
}
}