fix(abcip): #611 review round 1 — native keeps BOOL type distinction, WriteWidthRefusal tests, doc caveats
- L2: WriteWidthRefusal leaves a BOOL over a wider value to a runtime that sees the CIP type (native): mistyped integer member -> BadTypeMismatch (as its read), 0xD3 BOOL-array DWORD -> BadNotSupported; libplctag keeps BadNotSupported for both (size only). - L4: direct theory over WriteWidthRefusal (1/2/4/8 widths incl LINT/ULINT/LREAL, String/Dt, unknown size) + array/bit-selector exemptions with a non-vacuous control. - L1: CLAUDE.md member/BIT-member writes are ab_server-proven only. - L3: patch 0003 blind spots documented (Docker README, AbServer-Test-Fixture.md); nits. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+115
@@ -2,6 +2,7 @@ using System.Runtime.CompilerServices;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
@@ -153,6 +154,120 @@ public sealed class AbCipDiscoveredMemberWriteTests
|
||||
factory.Tags["Seg.ID"].WriteCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review L4 — <see cref="AbCipDriver.WriteWidthRefusal"/> directly, with a real size so the check runs:
|
||||
/// every atomic width (1/2/4/8, incl. LINT/ULINT/LREAL) passes at its own size and is refused at another;
|
||||
/// arrays, <c>Tag.N</c> bit selectors, STRING/DT and an unknown size (<c><= 0</c>) are never judged.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
// matching widths write
|
||||
[InlineData(AbCipDataType.Bool, 1, null)]
|
||||
[InlineData(AbCipDataType.SInt, 1, null)]
|
||||
[InlineData(AbCipDataType.USInt, 1, null)]
|
||||
[InlineData(AbCipDataType.Int, 2, null)]
|
||||
[InlineData(AbCipDataType.UInt, 2, null)]
|
||||
[InlineData(AbCipDataType.DInt, 4, null)]
|
||||
[InlineData(AbCipDataType.UDInt, 4, null)]
|
||||
[InlineData(AbCipDataType.Real, 4, null)]
|
||||
[InlineData(AbCipDataType.LInt, 8, null)]
|
||||
[InlineData(AbCipDataType.ULInt, 8, null)]
|
||||
[InlineData(AbCipDataType.LReal, 8, null)]
|
||||
// mismatches are refused
|
||||
[InlineData(AbCipDataType.LInt, 4, AbCipStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbCipDataType.LReal, 4, AbCipStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbCipDataType.ULInt, 2, AbCipStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbCipDataType.Real, 8, AbCipStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbCipDataType.Int, 4, AbCipStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbCipDataType.SInt, 2, AbCipStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbCipDataType.Bool, 4, AbCipStatusMapper.BadNotSupported)]
|
||||
// types with no fixed encoded width are never judged
|
||||
[InlineData(AbCipDataType.String, 88, null)]
|
||||
[InlineData(AbCipDataType.String, 4, null)]
|
||||
[InlineData(AbCipDataType.Dt, 8, null)]
|
||||
// unknown size is never judged
|
||||
[InlineData(AbCipDataType.DInt, 0, null)]
|
||||
[InlineData(AbCipDataType.DInt, -1, null)]
|
||||
public void WriteWidthRefusal_judges_scalar_widths_only(AbCipDataType type, int size, uint? expected)
|
||||
{
|
||||
var def = new AbCipTagDefinition("T", Device, "Seg.X", type);
|
||||
AbCipDriver.WriteWidthRefusal(def, bitIndex: null, size).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Review L4 — array tags and bit selectors are exempt even at a size that would otherwise refuse.</summary>
|
||||
[Fact]
|
||||
public void WriteWidthRefusal_exempts_arrays_and_bit_selectors()
|
||||
{
|
||||
// Control: the same scalar at the same size IS refused, so the exemptions below are not vacuous.
|
||||
AbCipDriver.WriteWidthRefusal(new AbCipTagDefinition("T", Device, "Seg.X", AbCipDataType.Int), null, 4)
|
||||
.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
||||
|
||||
AbCipDriver.WriteWidthRefusal(
|
||||
new AbCipTagDefinition("T", Device, "Seg.X", AbCipDataType.Int, ElementCount: 4, IsArray: true), null, 4)
|
||||
.ShouldBeNull();
|
||||
AbCipDriver.WriteWidthRefusal(
|
||||
new AbCipTagDefinition("T", Device, "Seg.X", AbCipDataType.Int, ElementCount: 2), null, 4)
|
||||
.ShouldBeNull();
|
||||
AbCipDriver.WriteWidthRefusal(
|
||||
new AbCipTagDefinition("T", Device, "Flags.3", AbCipDataType.Bool), bitIndex: 3, size: 4)
|
||||
.ShouldBeNull();
|
||||
AbCipDriver.WriteWidthRefusal(
|
||||
new AbCipTagDefinition("T", Device, "Word.3", AbCipDataType.Int), bitIndex: 3, size: 4)
|
||||
.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review L2 — a runtime that types BOOL writes itself (native) is left to judge a BOOL over a wider value;
|
||||
/// only libplctag, which sees a size and nothing else, gets the pre-check's BadNotSupported.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteWidthRefusal_leaves_a_wide_BOOL_to_a_runtime_that_sees_the_CIP_type()
|
||||
{
|
||||
var def = new AbCipTagDefinition("T", Device, "Seg.Flag", AbCipDataType.Bool);
|
||||
AbCipDriver.WriteWidthRefusal(def, null, 4, runtimeTypesBool: false).ShouldBe(AbCipStatusMapper.BadNotSupported);
|
||||
AbCipDriver.WriteWidthRefusal(def, null, 4, runtimeTypesBool: true).ShouldBeNull();
|
||||
// Non-BOOL mismatches are still refused up front on every runtime.
|
||||
AbCipDriver.WriteWidthRefusal(new AbCipTagDefinition("T", Device, "Seg.X", AbCipDataType.Int), null, 4,
|
||||
runtimeTypesBool: true).ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review L2, end to end on the native runtime: a BOOL authored over a mistyped DINT member writes
|
||||
/// BadTypeMismatch (the same verdict as its read); a BOOL over a packed BOOL-array DWORD writes
|
||||
/// BadNotSupported. Neither sends a write; health is untouched.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("C400", AbCipStatusMapper.BadTypeMismatch)] // DINT member mistyped as Bool
|
||||
[InlineData("D300", AbCipStatusMapper.BadNotSupported)] // packed BOOL-array DWORD
|
||||
public async Task A_native_BOOL_over_a_wider_value_keeps_its_type_distinction(string typeHex, uint expected)
|
||||
{
|
||||
var channels = new List<FakeCipChannel>();
|
||||
var factory = new NativeFactory(p =>
|
||||
{
|
||||
var ch = new FakeCipChannel { Respond = _ => Task.FromResult(Convert.FromHexString("D2000000" + typeHex + "00000000")) };
|
||||
lock (channels) channels.Add(ch);
|
||||
return NativeAbCipTagRuntime.ForTest(ch, p);
|
||||
});
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
RawTags = [Committed("Seg.Flag", "Bool")],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-native", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var health = drv.GetHealth();
|
||||
|
||||
(await drv.WriteAsync([new WriteRequest("Seg.Flag", true)], CancellationToken.None))
|
||||
.Single().StatusCode.ShouldBe(expected);
|
||||
|
||||
channels.ShouldHaveSingleItem().Sent.ShouldAllBe(req => req[0] != 0x4D); // no Write Tag request
|
||||
drv.GetHealth().ShouldBe(health);
|
||||
}
|
||||
|
||||
private sealed class NativeFactory(Func<AbCipTagCreateParams, IAbCipTagRuntime> create) : IAbCipTagFactory
|
||||
{
|
||||
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) => create(createParams);
|
||||
}
|
||||
|
||||
// ---- helpers ---------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Browses a UDT shaped like the Z-35162 press's UDT_TemperatureProfile_Segment (+ a nested struct).</summary>
|
||||
|
||||
Reference in New Issue
Block a user