From c48f2999948c54bee37027a0571b4ffe34ee658d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 12:02:10 -0400 Subject: [PATCH] test(ablegacy): bit write surfaces device rejection status (review) Adds `Bit_write_surfaces_device_rejection_status` to AbLegacyBitRmwTests, verifying that a non-zero libplctag status returned by the parent-word write in WriteBitInWordAsync propagates as a non-Good OPC UA StatusCode rather than being silently swallowed. Added a minimal `WriteStatusOverride` hook to FakeAbLegacyTag (test-project-only) so the read half of the RMW still returns 0/Good while the write half returns the seeded error code. --- .../AbLegacyBitRmwTests.cs | 34 +++++++++++++++++++ .../FakeAbLegacyTag.cs | 12 +++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs index 8f0b07a0..f5a196c3 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs @@ -177,4 +177,38 @@ public sealed class AbLegacyBitRmwTests Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(0xFF); } + + /// + /// A non-zero libplctag status returned by the parent-word write is surfaced as a non-Good + /// OPC UA StatusCode — the PCCC device rejection propagates to the caller. + /// + [Fact] + public async Task Bit_write_surfaces_device_rejection_status() + { + // Arrange: the parent tag reads OK (Status = 0) but write returns a timeout error. + const int errorTimeout = (int)libplctag.Status.ErrorTimeout; + var factory = new FakeAbLegacyTagFactory + { + Customise = p => new FakeAbLegacyTag(p) + { + Value = (short)0b0001, + WriteStatusOverride = errorTimeout, + }, + }; + var drv = new AbLegacyDriver(new AbLegacyDriverOptions + { + Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], + Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)], + Probe = new AbLegacyProbeOptions { Enabled = false }, + }, "drv-1", factory); + await drv.InitializeAsync("{}", CancellationToken.None); + + // Act + var results = await drv.WriteAsync([new WriteRequest("F", true)], CancellationToken.None); + + // Assert: the write rejection must NOT be silently swallowed as Good. + var statusCode = results.Single().StatusCode; + statusCode.ShouldNotBe(AbLegacyStatusMapper.Good); + statusCode.ShouldBe(AbLegacyStatusMapper.MapLibplctagStatus(errorTimeout)); + } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs index 68dfb4b6..4730777a 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs @@ -17,9 +17,16 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime /// public object? ArrayValue { get; set; } - /// Gets or sets the tag status code. + /// Gets or sets the tag status code (returned by for reads). public int Status { get; set; } + /// + /// Gets or sets an optional status code that overrides once a write + /// has been performed (i.e. after becomes > 0). Lets tests seed + /// a write-rejection status without also failing the preceding read. + /// + public int? WriteStatusOverride { get; set; } + /// Gets or sets a value indicating whether to throw on initialization. public bool ThrowOnInitialize { get; set; } @@ -80,7 +87,8 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime /// Gets the current tag status. /// The status code. - public virtual int GetStatus() => Status; + public virtual int GetStatus() => + WriteStatusOverride.HasValue && WriteCount > 0 ? WriteStatusOverride.Value : Status; /// Decodes the tag value based on the specified data type and bit index. /// The AbLegacy data type.