review(Driver.AbLegacy): fix Bit write 1-byte/2-byte encode-decode mismatch (Medium)

Re-review at 7286d320. -014 (Medium): Bit EncodeValue (no bitIndex) wrote SetInt8 while
DecodeValue read GetInt16 on a 16-bit B-file element, so a false write could round-trip
as true (stale high byte). Fix: SetInt16 + TDD. -015: tests pass CancellationToken.
This commit is contained in:
Joseph Doherty
2026-06-19 11:47:11 -04:00
parent be272d960f
commit 91e2609560
4 changed files with 131 additions and 7 deletions
@@ -89,7 +89,7 @@ public sealed class AbLegacyCapabilityTests
var afterUnsub = events.Count;
tagRef.Value = 999;
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
events.Count.ShouldBe(afterUnsub);
}
@@ -171,7 +171,7 @@ public sealed class AbLegacyCapabilityTests
Probe = new AbLegacyProbeOptions { Enabled = true, ProbeAddress = null },
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
drv.GetHostStatuses().Single().State.ShouldBe(HostState.Unknown);
await drv.ShutdownAsync(CancellationToken.None);
@@ -189,6 +189,40 @@ public sealed class AbLegacyReadWriteTests
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
}
/// <summary>
/// Driver.AbLegacy-014 — a Bit-typed tag with no bit suffix (e.g. B3:0, DataType=Bit)
/// takes the EncodeValue(Bit, bitIndex:null, …) path (not RMW). The encode must be
/// symmetric with the DecodeValue path, which reads the full 16-bit word via GetInt16.
/// Through the fake factory this verifies the driver dispatches through EncodeValue with
/// the right arguments; the SetInt16/SetInt8 delta is exercised against a live PLC.
/// </summary>
[Fact]
public async Task Bit_tag_without_suffix_writes_via_EncodeValue_not_RMW()
{
// A Bit-typed tag with NO /N bit suffix — bitIndex is null, so write must NOT
// route through WriteBitInWordAsync (which requires bitIndex). Instead it goes
// through EncodeValue(Bit, null, value) on the tag's own runtime, not a parent runtime.
var factory = new FakeAbLegacyTagFactory();
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Flag", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit)],
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var results = await drv.WriteAsync(
[new WriteRequest("Flag", true)], CancellationToken.None);
// Must succeed (Good) and route through the tag's own runtime, NOT a parent runtime.
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
factory.Tags.ShouldContainKey("B3:0");
factory.Tags.ShouldNotContainKey("B3"); // no parent-word runtime created
factory.Tags["B3:0"].WriteCount.ShouldBe(1);
// FakeAbLegacyTag.EncodeValue stores the raw value; verify it received true.
factory.Tags["B3:0"].Value.ShouldBe(true);
}
/// <summary>Verifies that write exceptions surface as BadCommunicationError.</summary>
[Fact]
public async Task Write_exception_surfaces_BadCommunicationError()