517837e798
Review 07 U10 (user decision: implement array encode now). Both runtimes encoded a scalar for an array tag: an int[] threw InvalidCastException into BadTypeMismatch + Degraded, a scalar wrote element 0 natively, and libplctag's SetInt32(0, ..) re-sent the stale last-read buffer for elements 1..n-1. IAbLegacyTagRuntime gains EncodeArray; both runtimes write every element at the element width (Int/AnalogInt 2 bytes, Long/Float 4). The driver converts the written value before any runtime exists: each element is converted, not exact-matched (ScadaBridge writes int[] to Int16 nodes), a length other than the tag's element count is BadOutOfRange, an overflowing element is BadOutOfRange, a non-array or non-numeric payload is BadTypeMismatch, and a Bit array (bit-packed words; an encode would clobber neighbour bits) is BadNotSupported. None of these touch health or the wire. Adds an env-gated ab_server round trip for an N-file array write. Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
71 lines
3.7 KiB
C#
71 lines
3.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Review 07 U10: an N-file array write (<c>N7:2</c>, 4 elements) round-trips every element against
|
|
/// <c>ab_server</c> on both transports, and leaves the words either side untouched. Simulator-only (gated by
|
|
/// <see cref="IntegrationWriteGate"/>); the real-controller proof is a hardware userGate.
|
|
/// </summary>
|
|
[Collection(AbLegacyServerCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Simulator", "ab_server-PCCC")]
|
|
public sealed class AbLegacyArrayWriteIntegrationTests(AbLegacyServerFixture sim)
|
|
{
|
|
/// <summary>Seeds the neighbours, writes an <c>int[]</c> to a 4-element Int array, reads it all back.</summary>
|
|
/// <param name="profile">The simulator profile.</param>
|
|
/// <param name="transport">The transport stack the driver runs on.</param>
|
|
[AbLegacyTheory]
|
|
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
|
|
public async Task N_file_array_write_round_trips_every_element(AbLegacyServerProfile profile, AbLegacyTransport transport)
|
|
{
|
|
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
|
|
IntegrationWriteGate.SkipUnlessWritesAllowed();
|
|
|
|
var deviceUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}";
|
|
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions(deviceUri, profile.Family)],
|
|
RawTags = AbLegacyRawTags.Entries(
|
|
new AbLegacyTagDefinition("Before", deviceUri, "N7:1", AbLegacyDataType.Int),
|
|
new AbLegacyTagDefinition("Arr", deviceUri, "N7:2", AbLegacyDataType.Int, ArrayLength: 4),
|
|
new AbLegacyTagDefinition("After", deviceUri, "N7:6", AbLegacyDataType.Int)),
|
|
Timeout = TimeSpan.FromSeconds(5),
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
Transport = transport,
|
|
}, driverInstanceId: $"ablegacy-arraywrite-{profile.Family}-{transport}");
|
|
var ct = TestContext.Current.CancellationToken;
|
|
await drv.InitializeAsync("{}", ct);
|
|
|
|
if (profile.Family == AbLegacyPlcFamily.Plc5 && transport == AbLegacyTransport.Native)
|
|
{
|
|
// Native PLC-5 writes are refused before the wire (no capture of the PLC-5 write frames exists).
|
|
(await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 2, 3, 4 })], ct)).Single()
|
|
.StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
|
return;
|
|
}
|
|
|
|
(await drv.WriteAsync(
|
|
[new WriteRequest("Before", (short)111), new WriteRequest("After", (short)666)], ct))
|
|
.ShouldAllBe(r => r.StatusCode == AbLegacyStatusMapper.Good);
|
|
|
|
// ScadaBridge shape: a CLR int[] to an Int16 array node.
|
|
(await drv.WriteAsync([new WriteRequest("Arr", new[] { 10, -20, 30, 32767 })], ct)).Single()
|
|
.StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
|
|
var back = await drv.ReadAsync(["Before", "Arr", "After"], ct);
|
|
back.ShouldAllBe(s => s.StatusCode == AbLegacyStatusMapper.Good);
|
|
back[1].Value.ShouldBe(new short[] { 10, -20, 30, 32767 });
|
|
Convert.ToInt32(back[0].Value).ShouldBe(111); // neighbours untouched
|
|
Convert.ToInt32(back[2].Value).ShouldBe(666);
|
|
|
|
// A length mismatch is refused before the wire and changes nothing.
|
|
(await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 2, 3 })], ct)).Single()
|
|
.StatusCode.ShouldBe(AbLegacyStatusMapper.BadOutOfRange);
|
|
(await drv.ReadAsync(["Arr"], ct)).Single().Value.ShouldBe(new short[] { 10, -20, 30, 32767 });
|
|
}
|
|
}
|