fix(driver-abcip): resolve Medium code-review finding (Driver.AbCip-014)
Add regression tests for the Medium findings resolved in this series: - AbCipDataType_maps_large_integer_types (theory: LInt→Int64, ULInt→UInt64, UDInt→UInt32) and Read_UDInt_tag_returns_uint_value_not_negative_wrapped_int cover Driver.AbCip-004. - Structure_parent_tag_is_not_readable_after_member_fan_out, InitializeAsync_throws_on_duplicate_tag_name, and InitializeAsync_throws_when_member_name_collides_with_independent_tag cover Driver.AbCip-005. - Read_failure_evicts_runtime_so_next_read_creates_fresh_handle covers Driver.AbCip-010. AbCipDriverTests.AbCipDataType_maps_atomics_to_driver_types extended with LInt/ULInt/UDInt assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -228,13 +228,13 @@
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Location | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs:28-40` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** `AbCipStatusMapperTests.MapLibplctagStatus_maps_known_codes` asserts the mapper against the same wrong integer constants (-5, -7, -14, -16, -17) the production code uses (see Driver.AbCip-002). The test locks in the bug rather than catching it, giving false confidence that libplctag error mapping is correct. There is no test that drives an actual libplctag `Status` enum value through `LibplctagTagRuntime.GetStatus()` plus `MapLibplctagStatus` end-to-end. Separately, the broken `ReinitializeAsync` config-discard behavior (Driver.AbCip-001) and the declaration-order whole-UDT decode risk (Driver.AbCip-003) have no test that would fail when those defects are present: `AbCipDriverWholeUdtReadTests` only exercises a UDT whose declaration order happens to match a simple alignment layout.
|
||||
|
||||
**Recommendation:** Rewrite the libplctag-status test to use the real `libplctag.Status` enum members and their documented integer values. Add a test that `ReinitializeAsync` with a changed config JSON actually applies the change (or asserts the documented immutability contract). Add a whole-UDT decode test where the controller compiled layout differs from declaration order.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — status mapper test already uses real `Status` enum members (fixed with Driver.AbCip-002); `ReinitializeAsync` config-change coverage already added with Driver.AbCip-001. Added to `AbCipDriverCodeReviewRegressionTests`: three tests for 004 (LInt/ULInt/UDInt type mapping theory + UDInt uint-vs-negative-int read test), three tests for 005 (Structure parent is BadNodeIdUnknown, duplicate scalar key throws, member-collision-with-independent-tag throws), and one test for 010 (eviction on bad status means next read creates a fresh handle). `AbCipDriverTests.AbCipDataType_maps_atomics_to_driver_types` extended with LInt/ULInt/UDInt assertions.
|
||||
|
||||
### Driver.AbCip-015
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using libplctag;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -6,7 +7,8 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the High code-review findings Driver.AbCip-001 / -003 / -008.
|
||||
/// Regression tests for the High code-review findings Driver.AbCip-001 / -003 / -008,
|
||||
/// and the Medium findings Driver.AbCip-004 / -005 / -010.
|
||||
/// (Driver.AbCip-002 is covered by <see cref="AbCipStatusMapperTests"/>.)
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
@@ -188,4 +190,147 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
foreach (var result in allResults)
|
||||
result.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
}
|
||||
|
||||
// ---- Driver.AbCip-004 — LInt/ULInt/UDInt declared type must agree with runtime value type ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(AbCipDataType.LInt, DriverDataType.Int64)]
|
||||
[InlineData(AbCipDataType.ULInt, DriverDataType.UInt64)]
|
||||
[InlineData(AbCipDataType.UDInt, DriverDataType.UInt32)]
|
||||
public void AbCipDataType_maps_large_integer_types_to_correct_driver_types(
|
||||
AbCipDataType abType, DriverDataType expected)
|
||||
{
|
||||
// Regression for Driver.AbCip-004: LInt/ULInt were mapped to Int32 (truncation);
|
||||
// UDInt was mapped to Int32 (negative wrap for values > Int32.MaxValue).
|
||||
abType.ToDriverDataType().ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_UDInt_tag_returns_uint_value_not_negative_wrapped_int()
|
||||
{
|
||||
// A UDInt value above Int32.MaxValue (e.g. uint.MaxValue) used to be decoded as
|
||||
// (int)GetUInt32, which wraps to -1. After the fix it must be decoded as uint.
|
||||
const uint largeUDInt = uint.MaxValue; // would wrap to -1 as (int)
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
factory.Customise = p => new FakeAbCipTag(p) { Value = largeUDInt };
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
Tags = [new AbCipTagDefinition("Counter", Device, "Counter", AbCipDataType.UDInt)],
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var results = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
// The value must be the uint, not a negative int
|
||||
results.Single().Value.ShouldBe(largeUDInt);
|
||||
results.Single().Value.ShouldNotBe(-1);
|
||||
}
|
||||
|
||||
// ---- Driver.AbCip-005 — Structure parent not registered; duplicate key check ----
|
||||
|
||||
[Fact]
|
||||
public async Task Structure_parent_tag_is_not_readable_after_member_fan_out()
|
||||
{
|
||||
// Regression for Driver.AbCip-005: the bare parent "Motor" used to be added to
|
||||
// _tagsByName before member fan-out, so ReadAsync("Motor") returned Good/null.
|
||||
// After the fix, reading the parent name returns BadNodeIdUnknown.
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure, Members:
|
||||
[
|
||||
new AbCipStructureMember("Speed", AbCipDataType.DInt),
|
||||
new AbCipStructureMember("Torque", AbCipDataType.Real),
|
||||
]),
|
||||
],
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var results = await drv.ReadAsync(["Motor"], CancellationToken.None);
|
||||
|
||||
// Parent is not a readable tag — BadNodeIdUnknown, not Good/null.
|
||||
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializeAsync_throws_on_duplicate_tag_name()
|
||||
{
|
||||
// Regression for Driver.AbCip-005: silently-overwritten duplicate keys.
|
||||
// Two independently-declared tags with the same name must throw.
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt),
|
||||
new AbCipTagDefinition("Speed", Device, "SpeedAlias", AbCipDataType.Real), // same name
|
||||
],
|
||||
}, "drv-1");
|
||||
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
drv.InitializeAsync("{}", CancellationToken.None).GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializeAsync_throws_when_member_name_collides_with_independent_tag()
|
||||
{
|
||||
// A Structure fan-out member path ("Motor.Speed") that collides with a separately-
|
||||
// declared tag ("Motor.Speed") must throw rather than silently overwrite.
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure, Members:
|
||||
[
|
||||
new AbCipStructureMember("Speed", AbCipDataType.DInt),
|
||||
]),
|
||||
new AbCipTagDefinition("Motor.Speed", Device, "Motor.Speed", AbCipDataType.DInt), // collision
|
||||
],
|
||||
}, "drv-1");
|
||||
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
drv.InitializeAsync("{}", CancellationToken.None).GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
// ---- Driver.AbCip-010 — stale runtime evicted on failure ----
|
||||
|
||||
[Fact]
|
||||
public async Task Read_failure_evicts_runtime_so_next_read_creates_fresh_handle()
|
||||
{
|
||||
// Regression for Driver.AbCip-010: a non-zero status was returned forever because
|
||||
// the cached runtime was never evicted. After the fix the next read creates a new one.
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var callCount = 0;
|
||||
factory.Customise = p =>
|
||||
{
|
||||
// First tag creation → returns error; second → returns success.
|
||||
callCount++;
|
||||
return callCount == 1
|
||||
? new FakeAbCipTag(p) { Status = (int)libplctag.Status.ErrorBadConnection }
|
||||
: new FakeAbCipTag(p) { Status = 0, Value = 42 };
|
||||
};
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// First read — bad connection, runtime is evicted.
|
||||
var first = await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
first.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
|
||||
|
||||
// Second read — fresh handle, succeeds.
|
||||
var second = await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
second.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
second.Single().Value.ShouldBe(42);
|
||||
// The factory was called twice — once for the failed handle, once for the fresh one.
|
||||
callCount.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,5 +111,9 @@ public sealed class AbCipDriverTests
|
||||
AbCipDataType.Real.ToDriverDataType().ShouldBe(DriverDataType.Float32);
|
||||
AbCipDataType.LReal.ToDriverDataType().ShouldBe(DriverDataType.Float64);
|
||||
AbCipDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String);
|
||||
// Driver.AbCip-004: 64-bit and unsigned-32-bit types must map to their correct equivalents.
|
||||
AbCipDataType.LInt.ToDriverDataType().ShouldBe(DriverDataType.Int64);
|
||||
AbCipDataType.ULInt.ToDriverDataType().ShouldBe(DriverDataType.UInt64);
|
||||
AbCipDataType.UDInt.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user