530f4bdcd4
Review 07 P7 (rest): adjacent words on one data file were never coalesced (one round trip per tag) although the array path proves the transport can read a span. Within a device pass, scalar tags on one N/B/F/L file within 8 elements of each other (block <= 200 data bytes, under the 236-byte typed-read limit) now ride one multi-element read and are decoded from their offsets with exactly the CLR types the single-tag path returns (int / float / bool). Only shapes the single path decodes identically are coalesced: indirect, sub-element, array, file-number-less (S:/I:/O:) and width-mismatched tags keep the single path. A block the device refuses (an authored element past the end of the file, a native PLC-5 non-N file) falls back to per-tag reads for its members and its shape is remembered; a comms fault fails the members together. New driver-config key coalesceReads (default true) with an AdminUI switch (Razor change: AbLegacyDriverForm, for the Wave 4 live check). Adds an env-gated ab_server parity case (coalesced == per-tag values). Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
70 lines
3.3 KiB
C#
70 lines
3.3 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 P7: a coalesced read of nearby N7 words and bits must decode exactly what per-tag reads return,
|
|
/// on the wire, on both transports. Seeds distinct values first when writes are allowed (simulator only);
|
|
/// otherwise compares whatever the simulator holds.
|
|
/// </summary>
|
|
[Collection(AbLegacyServerCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Simulator", "ab_server-PCCC")]
|
|
public sealed class AbLegacyReadCoalescingIntegrationTests(AbLegacyServerFixture sim)
|
|
{
|
|
/// <summary>Coalesced and per-tag reads of the same tags agree value for value.</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 Coalesced_reads_match_single_reads(AbLegacyServerProfile profile, AbLegacyTransport transport)
|
|
{
|
|
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
|
|
var ct = TestContext.Current.CancellationToken;
|
|
var uri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}";
|
|
AbLegacyTagDefinition[] tags =
|
|
[
|
|
new("W0", uri, "N7:0", AbLegacyDataType.Int),
|
|
new("W1", uri, "N7:1", AbLegacyDataType.Int),
|
|
new("W4", uri, "N7:4", AbLegacyDataType.Int),
|
|
new("W4b3", uri, "N7:4/3", AbLegacyDataType.Bit),
|
|
];
|
|
string[] refs = [.. tags.Select(t => t.Name)];
|
|
|
|
AbLegacyDriver Make(bool coalesce) => new(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions(uri, profile.Family)],
|
|
RawTags = AbLegacyRawTags.Entries(tags),
|
|
Timeout = TimeSpan.FromSeconds(5),
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
Transport = transport,
|
|
CoalesceReads = coalesce,
|
|
}, driverInstanceId: $"ablegacy-coalesce-{coalesce}-{profile.Family}-{transport}");
|
|
|
|
await using var single = Make(coalesce: false);
|
|
await using var coalesced = Make(coalesce: true);
|
|
await single.InitializeAsync("{}", ct);
|
|
await coalesced.InitializeAsync("{}", ct);
|
|
|
|
var writesRefused = profile.Family == AbLegacyPlcFamily.Plc5 && transport == AbLegacyTransport.Native;
|
|
if (Environment.GetEnvironmentVariable(IntegrationWriteGate.AllowWritesVar) == "1"
|
|
&& Environment.GetEnvironmentVariable(IntegrationWriteGate.DifferentialEndpointVar) is not { Length: > 0 }
|
|
&& !writesRefused)
|
|
{
|
|
(await single.WriteAsync(
|
|
[new WriteRequest("W0", (short)101), new WriteRequest("W1", (short)-202), new WriteRequest("W4", (short)0x0008)], ct))
|
|
.ShouldAllBe(r => r.StatusCode == AbLegacyStatusMapper.Good);
|
|
}
|
|
|
|
var a = await single.ReadAsync(refs, ct);
|
|
var b = await coalesced.ReadAsync(refs, ct);
|
|
|
|
a.ShouldAllBe(s => s.StatusCode == AbLegacyStatusMapper.Good);
|
|
b.ShouldAllBe(s => s.StatusCode == AbLegacyStatusMapper.Good);
|
|
b.Select(s => s.Value).ShouldBe(a.Select(s => s.Value));
|
|
}
|
|
}
|