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.
This commit is contained in:
Joseph Doherty
2026-06-17 12:02:10 -04:00
parent 340c145e87
commit c48f299994
2 changed files with 44 additions and 2 deletions
@@ -177,4 +177,38 @@ public sealed class AbLegacyBitRmwTests
Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(0xFF);
}
/// <summary>
/// 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.
/// </summary>
[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));
}
}
@@ -17,9 +17,16 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
/// </summary>
public object? ArrayValue { get; set; }
/// <summary>Gets or sets the tag status code.</summary>
/// <summary>Gets or sets the tag status code (returned by <see cref="GetStatus"/> for reads).</summary>
public int Status { get; set; }
/// <summary>
/// Gets or sets an optional status code that overrides <see cref="Status"/> once a write
/// has been performed (i.e. after <see cref="WriteCount"/> becomes &gt; 0). Lets tests seed
/// a write-rejection status without also failing the preceding read.
/// </summary>
public int? WriteStatusOverride { get; set; }
/// <summary>Gets or sets a value indicating whether to throw on initialization.</summary>
public bool ThrowOnInitialize { get; set; }
@@ -80,7 +87,8 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
/// <summary>Gets the current tag status.</summary>
/// <returns>The status code.</returns>
public virtual int GetStatus() => Status;
public virtual int GetStatus() =>
WriteStatusOverride.HasValue && WriteCount > 0 ? WriteStatusOverride.Value : Status;
/// <summary>Decodes the tag value based on the specified data type and bit index.</summary>
/// <param name="type">The AbLegacy data type.</param>