fix(ablegacy): refuse every libplctag write the #659 translation changes (fix round 2)
Review M1: round 1 refused only the rewritten status bits, so S:/O:/I: writes - which never worked on libplctag because tag creation failed - would have started reaching the PLC (outputs, status-file words) on the default transport. WriteRefusal now keys on LibplctagLegacyName.IsTranslated: file number supplied (S/O/I, incl. bit writes like O:0/3, refused ahead of the parent-word RMW) or status bit rewritten. Reads unaffected. Docs/comments now true; gate doc notes R bit-number wire evidence is non-discriminating (L1). 12 new cases fail with the round-1 predicate. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+53
-19
@@ -7,14 +7,15 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #659 review ruling: the libplctag read translation of a status bit libplctag has no mnemonic for
|
||||
/// (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>) must not widen the WRITE surface. Before the translation libplctag could
|
||||
/// not create such a tag, so a write never reached the controller; now that the name parses, a write would go out
|
||||
/// as a masked write to the control word. Such writes are refused <c>BadNotSupported</c> before any runtime is
|
||||
/// created, health unchanged. T/C status bits libplctag itself parses keep their pre-existing (unrefused) behaviour.
|
||||
/// #659 review rulings: the libplctag read translation must not widen the WRITE surface of the default transport.
|
||||
/// Before it, libplctag could not create a tag for <c>S:</c>/<c>O:</c>/<c>I:</c> (no file number) or for an R/UA
|
||||
/// status bit, so no write to one ever reached the controller; now that the translated names parse, a write would
|
||||
/// drive an output, a status-file word or (masked) a control word. Every write whose address the translation
|
||||
/// changes is refused <c>BadNotSupported</c> before any runtime is created, health unchanged. T/C status bits
|
||||
/// libplctag itself parses keep their pre-existing (unrefused) behaviour.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class LibplctagStatusBitWriteRefusalTests
|
||||
public sealed class LibplctagTranslatedWriteRefusalTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("R6:0.EN", true)]
|
||||
@@ -31,19 +32,27 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
||||
[InlineData("C5:0.DN", false)]
|
||||
[InlineData("R6:0.LEN", false)]
|
||||
[InlineData("R6:0.POS", false)]
|
||||
[InlineData("S:0", false)] // translated, but not a status bit
|
||||
[InlineData("O:0/3", false)]
|
||||
[InlineData("I:0/0", false)]
|
||||
[InlineData("S:0", true)] // file number supplied by the translation (S2:0)
|
||||
[InlineData("S:1/5", true)]
|
||||
[InlineData("S:57", true)]
|
||||
[InlineData("O:0", true)]
|
||||
[InlineData("O:0/3", true)]
|
||||
[InlineData("I:0", true)]
|
||||
[InlineData("I:0/0", true)]
|
||||
[InlineData("N7:0", false)]
|
||||
[InlineData("N7:0/3", false)]
|
||||
[InlineData("F8:0", false)]
|
||||
[InlineData("ST14:10", false)]
|
||||
[InlineData("T4:0.PRE", false)]
|
||||
[InlineData("B3:0/4", false)]
|
||||
[InlineData("R6:0.FOO", false)]
|
||||
[InlineData("garbage", false)]
|
||||
public void The_libplctag_refusal_covers_exactly_the_rewritten_status_bits(string tag, bool refused)
|
||||
public void The_libplctag_refusal_covers_exactly_the_translated_addresses(string tag, bool refused)
|
||||
{
|
||||
var p = new AbLegacyTagCreateParams("h", 44818, "", "micrologix", tag, TimeSpan.FromSeconds(1));
|
||||
(LibplctagLegacyTagRuntime.WriteRefusal(p) is not null).ShouldBe(refused);
|
||||
(new LibplctagLegacyTagFactory().WriteRefusal(p) is not null).ShouldBe(refused);
|
||||
LibplctagLegacyName.IsRewrittenStatusBit(tag).ShouldBe(refused);
|
||||
LibplctagLegacyName.IsTranslated(tag).ShouldBe(refused);
|
||||
}
|
||||
|
||||
/// <summary>Defence in depth: a runtime for a rewritten status bit refuses the write itself, without touching the
|
||||
@@ -69,14 +78,15 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
||||
}
|
||||
|
||||
#pragma warning disable OTOPCUA0001 // direct driver calls pin the driver's own status mapping (no resilience wrapper)
|
||||
private static async Task<(AbLegacyDriver Driver, CountingLibplctagRefusalFactory Factory)> DriverAsync(string address)
|
||||
private static async Task<(AbLegacyDriver Driver, CountingLibplctagRefusalFactory Factory)> DriverAsync(
|
||||
string address, AbLegacyDataType type = AbLegacyDataType.Bit)
|
||||
{
|
||||
const string host = "ab://10.0.0.5/";
|
||||
var factory = new CountingLibplctagRefusalFactory();
|
||||
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.MicroLogix)],
|
||||
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit", host, address, AbLegacyDataType.Bit, Writable: true)]),
|
||||
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit", host, address, type, Writable: true)]),
|
||||
Timeout = TimeSpan.FromSeconds(1),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, "drv-libplctag-statusbit", factory);
|
||||
@@ -84,17 +94,25 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
||||
return (driver, factory);
|
||||
}
|
||||
|
||||
/// <summary>Includes bit writes to O/I/S (<c>O:0/3</c>): the refusal runs ahead of the bit read-modify-write, so
|
||||
/// not even the parent-word runtime (<c>O:0</c>) is created — nothing is read or written.</summary>
|
||||
[Theory]
|
||||
[InlineData("R6:0.DN")]
|
||||
[InlineData("R6:0.EN")]
|
||||
[InlineData("C5:0.UA")]
|
||||
public async Task A_rewritten_status_bit_write_is_BadNotSupported_with_no_runtime_and_health_unchanged(string address)
|
||||
[InlineData("R6:0.DN", AbLegacyDataType.Bit, true)]
|
||||
[InlineData("R6:0.EN", AbLegacyDataType.Bit, true)]
|
||||
[InlineData("C5:0.UA", AbLegacyDataType.Bit, true)]
|
||||
[InlineData("O:0/3", AbLegacyDataType.Bit, true)]
|
||||
[InlineData("I:0/0", AbLegacyDataType.Bit, true)]
|
||||
[InlineData("S:1/5", AbLegacyDataType.Bit, true)]
|
||||
[InlineData("O:0", AbLegacyDataType.Int, (short)1)]
|
||||
[InlineData("S:1", AbLegacyDataType.Int, (short)1)]
|
||||
public async Task A_translated_address_write_is_BadNotSupported_with_no_runtime_and_health_unchanged(
|
||||
string address, AbLegacyDataType type, object value)
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var (driver, factory) = await DriverAsync(address);
|
||||
var (driver, factory) = await DriverAsync(address, type);
|
||||
var health = driver.GetHealth();
|
||||
|
||||
(await driver.WriteAsync([new WriteRequest("Bit", true)], ct)).Single().StatusCode
|
||||
(await driver.WriteAsync([new WriteRequest("Bit", value)], ct)).Single().StatusCode
|
||||
.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
||||
|
||||
factory.Inner.Tags.ShouldBeEmpty();
|
||||
@@ -120,5 +138,21 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
||||
factory.Inner.Tags["T4:0.DN"].WriteCount.ShouldBe(1);
|
||||
await driver.ShutdownAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: an untranslated bit write (<c>B3:0/4</c>) still runs the parent-word
|
||||
/// read-modify-write, so the refusal is not swallowing bit writes in general.</summary>
|
||||
[Fact]
|
||||
public async Task An_untranslated_bit_write_still_runs_the_parent_word_read_modify_write()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var (driver, factory) = await DriverAsync("B3:0/4");
|
||||
factory.Inner.Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 };
|
||||
|
||||
(await driver.WriteAsync([new WriteRequest("Bit", true)], ct)).Single().StatusCode
|
||||
.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
|
||||
factory.Inner.Tags["B3:0"].WriteCount.ShouldBe(1);
|
||||
await driver.ShutdownAsync(ct);
|
||||
}
|
||||
#pragma warning restore OTOPCUA0001
|
||||
}
|
||||
Reference in New Issue
Block a user