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:
@@ -1094,8 +1094,9 @@ re-emit a concrete default. The defaults differ per driver (final review, 2026-1
|
|||||||
used to fail `S:`/`O:`/`I:` and `R` status bits there (incl. the default probe `S:0`) — its `pccc.c` needs explicit
|
used to fail `S:`/`O:`/`I:` and `R` status bits there (incl. the default probe `S:0`) — its `pccc.c` needs explicit
|
||||||
file numbers and has no R status-bit mnemonics; since #659 `LibplctagLegacyName` translates (`S2:`/`O0:`/`I1:`,
|
file numbers and has no R status-bit mnemonics; since #659 `LibplctagLegacyName` translates (`S2:`/`O0:`/`I1:`,
|
||||||
`R6:0.CON/13`) and both transports read identically on that press. The authored grammar is unchanged. Read-side
|
`R6:0.CON/13`) and both transports read identically on that press. The authored grammar is unchanged. Read-side
|
||||||
only: a libplctag WRITE to a rewritten status bit (R `EN…FD`, `C…UA`) is refused `BadNotSupported` before any runtime
|
only: a libplctag WRITE to any address the translation changes (`S:`/`O:`/`I:` incl. bit writes, R `EN…FD`, `C…UA`)
|
||||||
is created (it could never reach the PLC before); libplctag T/C status-bit writes it parses itself are unchanged.
|
is refused `BadNotSupported` before any runtime is created (none could reach the PLC before — no write-surface
|
||||||
|
widening on the default transport); libplctag T/C status-bit writes it parses itself are unchanged.
|
||||||
L/A files, function files, SLC, PLC-5 and `logixpccc` remain unverified natively. Explicit Native on PLC-5/`LogixPccc` logs a once-per-instance Warning.
|
L/A files, function files, SLC, PLC-5 and `logixpccc` remain unverified natively. Explicit Native on PLC-5/`LogixPccc` logs a once-per-instance Warning.
|
||||||
AbLegacy native refuses status-bit sub-element writes (`T4:0.DN`…, `BadNotSupported`).
|
AbLegacy native refuses status-bit sub-element writes (`T4:0.DN`…, `BadNotSupported`).
|
||||||
|
|
||||||
|
|||||||
@@ -243,13 +243,15 @@ status file when a digit follows it), and its sub-element table has only `con`/`
|
|||||||
bit libplctag lacks (`R` `EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) → `R6:0.CON/<bit>` (control word + bit, bit numbers
|
bit libplctag lacks (`R` `EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) → `R6:0.CON/<bit>` (control word + bit, bit numbers
|
||||||
from the native table). The authored grammar is unchanged; `S2:0`/`O0:0`/`I1:0` stay rejected by the parser (one
|
from the native table). The authored grammar is unchanged; `S2:0`/`O0:0`/`I1:0` stay rejected by the parser (one
|
||||||
spelling per address). Re-run on the same idle press 2026-10-09: every address above reads **identical** on both
|
spelling per address). Re-run on the same idle press 2026-10-09: every address above reads **identical** on both
|
||||||
transports (`S:4` is a free-running clock and differs read to read on each). **The translation is read-side only —
|
transports (`S:4` is a free-running clock and differs read to read on each). **The translation is read-side only — it does not widen the write surface:** before it, libplctag could not create a
|
||||||
it does not widen the write surface:** before it, a libplctag write to an `R` status bit (or `C…UA`) could never reach
|
tag for `S:`/`O:`/`I:` or for an `R`/`C…UA` status bit, so no write to one ever reached the PLC. Now that the
|
||||||
the PLC (tag creation failed), and `R6:0.CON/13` would otherwise turn it into a masked write to the control word. So
|
translated names parse, a write would drive a physical output, a status-file word or (masked) a control word. So on
|
||||||
on libplctag a write to any status bit the translation rewrites (`R` `EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) is refused
|
libplctag **every write whose address the translation changes** — `S:`, `O:`, `I:` (word and bit writes; a bit write
|
||||||
**`BadNotSupported` before a runtime is created** — nothing sent, health unchanged
|
such as `O:0/3` is refused ahead of the parent-word read-modify-write) and the rewritten status bits (`R`
|
||||||
(`LibplctagLegacyTagRuntime.WriteRefusal`, asked through `IAbLegacyTagFactory.WriteRefusal`, the same plumbing as the
|
`EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) — is refused **`BadNotSupported` before a runtime is created**: nothing read or
|
||||||
native refusal). T/C status bits libplctag parses itself keep their pre-existing write behaviour (note above).
|
sent, health unchanged (`LibplctagLegacyTagRuntime.WriteRefusal`, asked through `IAbLegacyTagFactory.WriteRefusal`, the
|
||||||
|
same plumbing as the native refusal). Those writes never worked on libplctag, so nothing is lost; the native
|
||||||
|
transport (opt-in) does write `S`/`O`/`I`. T/C status bits libplctag parses itself keep their pre-existing write behaviour (note above).
|
||||||
Not tested on hardware: L and A files (no press has them), function files, PLC-5, SLC, `logixpccc`.
|
Not tested on hardware: L and A files (no press has them), function files, PLC-5, SLC, `logixpccc`.
|
||||||
|
|
||||||
Simulator evidence:
|
Simulator evidence:
|
||||||
|
|||||||
@@ -700,3 +700,5 @@ ST14:10 String Native value="Z-33323" status=0x00000000 (
|
|||||||
```
|
```
|
||||||
|
|
||||||
Every address reads identically on both transports. `S:4` is the free-running clock, so its value differs read to read on each transport (both Good). `I:0` = -29376 = 0x8D40, and bits `/6` and `/15` read true on both while `/0`, `/1`, `/7` read false — the bit is extracted from the word libplctag read, not defaulted. ab_server has no S/O/I files, so this real-controller read is the only wire evidence; the translation is unit-pinned against a model of the `pccc.c` parser (`LibplctagLegacyNameTests`).
|
Every address reads identically on both transports. `S:4` is the free-running clock, so its value differs read to read on each transport (both Good). `I:0` = -29376 = 0x8D40, and bits `/6` and `/15` read true on both while `/0`, `/1`, `/7` read false — the bit is extracted from the word libplctag read, not defaulted. ab_server has no S/O/I files, so this real-controller read is the only wire evidence; the translation is unit-pinned against a model of the `pccc.c` parser (`LibplctagLegacyNameTests`).
|
||||||
|
|
||||||
|
Caveats: `R6:0.DN` and `R6:0.EN` read `false` on both transports, so this read does not discriminate the control-word bit number (a wrong bit would very likely also read false) — the R bit mapping rests on the native table and the `pccc.c` parser model, not on this wire evidence. Writes: on libplctag every address the translation changes (`S:`/`O:`/`I:`, R/UA status bits) is refused `BadNotSupported` (#659 review), so this fix adds no libplctag write path.
|
||||||
|
|||||||
@@ -46,17 +46,18 @@ internal static class LibplctagLegacyName
|
|||||||
public static string Translate(string canonical) => TranslateCore(canonical).Name;
|
public static string Translate(string canonical) => TranslateCore(canonical).Name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when <paramref name="canonical"/> is a status bit this translator rewrites to control word + bit
|
/// True when <see cref="Translate"/> changes <paramref name="canonical"/>: a file number supplied for
|
||||||
/// (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>; R <c>EN/EU/DN/EM/ER/UL/IN/FD</c>, counter <c>UA</c>). A WRITE to one is
|
/// <c>S:</c>/<c>O:</c>/<c>I:</c> (incl. bit forms such as <c>O:0/3</c>) or a status bit rewritten to control word +
|
||||||
/// refused (<see cref="LibplctagLegacyTagRuntime.WriteRefusal"/>): before the translation libplctag could not
|
/// bit (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>; R <c>EN/EU/DN/EM/ER/UL/IN/FD</c>, counter <c>UA</c>). A WRITE to such an
|
||||||
/// even create such a tag, so a write never reached the PLC, and the rewrite must not turn it into a new masked
|
/// address is refused (<see cref="LibplctagLegacyTagRuntime.WriteRefusal"/>): before #659 libplctag could not
|
||||||
/// write to a production controller's control word (the native transport refuses status-bit writes too).
|
/// even create these tags, so no write ever reached the PLC, and the read fix must not turn libplctag — the
|
||||||
|
/// default transport — into a new way to drive outputs, status-file words or control words.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="canonical">The canonical address.</param>
|
/// <param name="canonical">The canonical address.</param>
|
||||||
/// <returns><see langword="true"/> for a rewritten status bit.</returns>
|
/// <returns><see langword="true"/> when the libplctag name differs from the canonical one.</returns>
|
||||||
public static bool IsRewrittenStatusBit(string canonical) => TranslateCore(canonical).RewrittenStatusBit;
|
public static bool IsTranslated(string canonical) => TranslateCore(canonical).Changed;
|
||||||
|
|
||||||
private static (string Name, bool RewrittenStatusBit) TranslateCore(string canonical)
|
private static (string Name, bool Changed) TranslateCore(string canonical)
|
||||||
{
|
{
|
||||||
// Family-neutral parse is correct: the driver hands the runtime AbLegacyAddress.ToLibplctagName(), already
|
// Family-neutral parse is correct: the driver hands the runtime AbLegacyAddress.ToLibplctagName(), already
|
||||||
// decimal-normalised (PLC-5 octal I/O included) — the same reasoning as NativeAbLegacyTagRuntime.ToPcccAddress.
|
// decimal-normalised (PLC-5 octal I/O included) — the same reasoning as NativeAbLegacyTagRuntime.ToPcccAddress.
|
||||||
@@ -65,6 +66,7 @@ internal static class LibplctagLegacyName
|
|||||||
var fileNumber = a.FileNumber ?? PcccFileType.ImplicitFileNumber(a.FileLetter);
|
var fileNumber = a.FileNumber ?? PcccFileType.ImplicitFileNumber(a.FileLetter);
|
||||||
if (fileNumber is null) return (canonical, false);
|
if (fileNumber is null) return (canonical, false);
|
||||||
|
|
||||||
|
var fileNumberSupplied = a.FileNumber is null; // S:/O:/I: — libplctag needs S2/O0/I1
|
||||||
var name = $"{a.FileLetter}{fileNumber}:{a.WordNumber}";
|
var name = $"{a.FileLetter}{fileNumber}:{a.WordNumber}";
|
||||||
if (a.SubElement is { } sub)
|
if (a.SubElement is { } sub)
|
||||||
{
|
{
|
||||||
@@ -91,6 +93,6 @@ internal static class LibplctagLegacyName
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (a.BitIndex is int b) name += $"/{b}";
|
if (a.BitIndex is int b) name += $"/{b}";
|
||||||
return (name, false);
|
return (name, fileNumberSupplied);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,18 +71,20 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
|||||||
: RunReleasingOrphanAsync(() => _tag.WriteAsync(cancellationToken));
|
: RunReleasingOrphanAsync(() => _tag.WriteAsync(cancellationToken));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The writes the libplctag runtime refuses before the wire, or <see langword="null"/>: a status bit that
|
/// The writes the libplctag runtime refuses before the wire, or <see langword="null"/>: any address whose
|
||||||
/// <see cref="LibplctagLegacyName"/> rewrites to control word + bit (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>, R
|
/// libplctag name <see cref="LibplctagLegacyName"/> had to change (<see cref="LibplctagLegacyName.IsTranslated"/>)
|
||||||
/// <c>EN/EU/DN/EM/ER/UL/IN/FD</c>, counter <c>UA</c>). Before #659 libplctag could not create those tags at all,
|
/// — file-number-less <c>S:</c>/<c>O:</c>/<c>I:</c> (incl. bit writes such as <c>O:0/3</c>, refused before the
|
||||||
/// so a write to one never reached the controller; the read-side translation must not open a new masked write
|
/// parent-word read-modify-write) and the status bits rewritten to control word + bit (<c>R6:0.DN</c>, counter
|
||||||
/// to a control word. T/C status bits libplctag itself parses (<c>T4:0.DN</c>, <c>C5:0.CU</c>…) keep their
|
/// <c>UA</c>). Before #659 libplctag could not create those tags at all, so no write to one ever reached the
|
||||||
/// pre-existing behaviour — a masked bit write (documented hazard in <c>docs/drivers/AbLegacy.md</c>).
|
/// controller; the read fix must not widen the write surface of the default transport. T/C status bits
|
||||||
|
/// libplctag parses itself (<c>T4:0.DN</c>, <c>C5:0.CU</c>…) keep their pre-existing behaviour — a masked bit
|
||||||
|
/// write (documented hazard in <c>docs/drivers/AbLegacy.md</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="p">The tag's create parameters.</param>
|
/// <param name="p">The tag's create parameters.</param>
|
||||||
/// <returns>The refusal reason, or <see langword="null"/>.</returns>
|
/// <returns>The refusal reason, or <see langword="null"/>.</returns>
|
||||||
internal static string? WriteRefusal(AbLegacyTagCreateParams p) =>
|
internal static string? WriteRefusal(AbLegacyTagCreateParams p) =>
|
||||||
LibplctagLegacyName.IsRewrittenStatusBit(p.TagName)
|
LibplctagLegacyName.IsTranslated(p.TagName)
|
||||||
? $"Writes to the status bit '{p.TagName}' are not supported on the libplctag transport (libplctag has no mnemonic for it; it is read as control word + bit)."
|
? $"Writes to '{p.TagName}' are not supported on the libplctag transport (its name is translated for libplctag; reads only)."
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+53
-19
@@ -7,14 +7,15 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #659 review ruling: the libplctag read translation of a status bit libplctag has no mnemonic for
|
/// #659 review rulings: the libplctag read translation must not widen the WRITE surface of the default transport.
|
||||||
/// (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>) must not widen the WRITE surface. Before the translation libplctag could
|
/// 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
|
||||||
/// not create such a tag, so a write never reached the controller; now that the name parses, a write would go out
|
/// status bit, so no write to one ever reached the controller; now that the translated names parse, a write would
|
||||||
/// as a masked write to the control word. Such writes are refused <c>BadNotSupported</c> before any runtime is
|
/// drive an output, a status-file word or (masked) a control word. Every write whose address the translation
|
||||||
/// created, health unchanged. T/C status bits libplctag itself parses keep their pre-existing (unrefused) behaviour.
|
/// 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>
|
/// </summary>
|
||||||
[Trait("Category", "Unit")]
|
[Trait("Category", "Unit")]
|
||||||
public sealed class LibplctagStatusBitWriteRefusalTests
|
public sealed class LibplctagTranslatedWriteRefusalTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("R6:0.EN", true)]
|
[InlineData("R6:0.EN", true)]
|
||||||
@@ -31,19 +32,27 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
|||||||
[InlineData("C5:0.DN", false)]
|
[InlineData("C5:0.DN", false)]
|
||||||
[InlineData("R6:0.LEN", false)]
|
[InlineData("R6:0.LEN", false)]
|
||||||
[InlineData("R6:0.POS", false)]
|
[InlineData("R6:0.POS", false)]
|
||||||
[InlineData("S:0", false)] // translated, but not a status bit
|
[InlineData("S:0", true)] // file number supplied by the translation (S2:0)
|
||||||
[InlineData("O:0/3", false)]
|
[InlineData("S:1/5", true)]
|
||||||
[InlineData("I:0/0", false)]
|
[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", 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("B3:0/4", false)]
|
||||||
[InlineData("R6:0.FOO", false)]
|
[InlineData("R6:0.FOO", false)]
|
||||||
[InlineData("garbage", 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));
|
var p = new AbLegacyTagCreateParams("h", 44818, "", "micrologix", tag, TimeSpan.FromSeconds(1));
|
||||||
(LibplctagLegacyTagRuntime.WriteRefusal(p) is not null).ShouldBe(refused);
|
(LibplctagLegacyTagRuntime.WriteRefusal(p) is not null).ShouldBe(refused);
|
||||||
(new LibplctagLegacyTagFactory().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
|
/// <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)
|
#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/";
|
const string host = "ab://10.0.0.5/";
|
||||||
var factory = new CountingLibplctagRefusalFactory();
|
var factory = new CountingLibplctagRefusalFactory();
|
||||||
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||||
{
|
{
|
||||||
Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.MicroLogix)],
|
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),
|
Timeout = TimeSpan.FromSeconds(1),
|
||||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||||
}, "drv-libplctag-statusbit", factory);
|
}, "drv-libplctag-statusbit", factory);
|
||||||
@@ -84,17 +94,25 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
|||||||
return (driver, factory);
|
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]
|
[Theory]
|
||||||
[InlineData("R6:0.DN")]
|
[InlineData("R6:0.DN", AbLegacyDataType.Bit, true)]
|
||||||
[InlineData("R6:0.EN")]
|
[InlineData("R6:0.EN", AbLegacyDataType.Bit, true)]
|
||||||
[InlineData("C5:0.UA")]
|
[InlineData("C5:0.UA", AbLegacyDataType.Bit, true)]
|
||||||
public async Task A_rewritten_status_bit_write_is_BadNotSupported_with_no_runtime_and_health_unchanged(string address)
|
[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 ct = TestContext.Current.CancellationToken;
|
||||||
var (driver, factory) = await DriverAsync(address);
|
var (driver, factory) = await DriverAsync(address, type);
|
||||||
var health = driver.GetHealth();
|
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);
|
.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
||||||
|
|
||||||
factory.Inner.Tags.ShouldBeEmpty();
|
factory.Inner.Tags.ShouldBeEmpty();
|
||||||
@@ -120,5 +138,21 @@ public sealed class LibplctagStatusBitWriteRefusalTests
|
|||||||
factory.Inner.Tags["T4:0.DN"].WriteCount.ShouldBe(1);
|
factory.Inner.Tags["T4:0.DN"].WriteCount.ShouldBe(1);
|
||||||
await driver.ShutdownAsync(ct);
|
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
|
#pragma warning restore OTOPCUA0001
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user