review(Driver.TwinCAT): thread ArrayLength through factory DTO (Medium)
Re-review at 7286d320. -017 (Medium): TwinCATTagDto lacked ArrayLength, so JSON-authored
pre-declared array tags were silently scalar (Phase-4c array path dead for them). Fix:
add ArrayLength to the DTO + thread through BuildTag with positive-value guard + TDD.
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Module | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT` |
|
| Module | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT` |
|
||||||
| Reviewer | Claude Code |
|
| Reviewer | Claude Code |
|
||||||
| Review date | 2026-05-22 |
|
| Review date | 2026-06-19 |
|
||||||
| Commit reviewed | `76d35d1` |
|
| Commit reviewed | `04e0877b` |
|
||||||
| Status | Reviewed |
|
| Status | Reviewed |
|
||||||
| Open findings | 0 |
|
| Open findings | 0 |
|
||||||
|
|
||||||
@@ -27,6 +27,25 @@ a category produced nothing rather than leaving it blank.
|
|||||||
| 9 | Testing coverage | Driver.TwinCAT-016 |
|
| 9 | Testing coverage | Driver.TwinCAT-016 |
|
||||||
| 10 | Documentation & comments | Driver.TwinCAT-004 (data-type comment), Driver.TwinCAT-014 |
|
| 10 | Documentation & comments | Driver.TwinCAT-004 (data-type comment), Driver.TwinCAT-014 |
|
||||||
|
|
||||||
|
## Re-review 2026-06-19 (commit 04e0877b)
|
||||||
|
|
||||||
|
All 16 prior findings remain Resolved. One new finding (Driver.TwinCAT-017) identified and fixed in this session.
|
||||||
|
|
||||||
|
#### Re-review checklist
|
||||||
|
|
||||||
|
| # | Category | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Correctness & logic bugs | Driver.TwinCAT-017 |
|
||||||
|
| 2 | OtOpcUa conventions | No new issues |
|
||||||
|
| 3 | Concurrency & thread safety | No new issues |
|
||||||
|
| 4 | Error handling & resilience | No new issues |
|
||||||
|
| 5 | Security | No issues found |
|
||||||
|
| 6 | Performance & resource management | No new issues |
|
||||||
|
| 7 | Design-document adherence | No new issues |
|
||||||
|
| 8 | Code organization & conventions | No new issues |
|
||||||
|
| 9 | Testing coverage | No new issues (2 regression tests added for -017) |
|
||||||
|
| 10 | Documentation & comments | No new issues |
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|
||||||
### Driver.TwinCAT-001
|
### Driver.TwinCAT-001
|
||||||
@@ -424,3 +443,36 @@ addressed, especially a concurrency stress test for `EnsureConnectedAsync` and a
|
|||||||
`ReinitializeAsync`-applies-new-config test.
|
`ReinitializeAsync`-applies-new-config test.
|
||||||
|
|
||||||
**Resolution:** Resolved 2026-05-23 — the previously-closed High findings each grew their regression coverage as they were resolved (see `TwinCATHighFindingsRegressionTests`: `ReinitializeAsync_applies_changed_device_config` for -001, `LInt_read_round_trips_value_above_int_MaxValue` + `DataType_mapping_preserves_width_and_signedness` for -002, `Concurrent_reads_on_one_device_create_a_single_client` + `Concurrent_reads_and_writes_share_one_client` for -007, `Symbol_version_changed_raises_OnRediscoveryNeeded` + `TwinCATDriver_implements_IRediscoverable` for -013). This pass added the two remaining gaps: `Structure_typed_pre_declared_tag_is_rejected_at_config_parse` (-003) and `Probe_loop_and_read_share_one_client_per_device` (-009 disposal-race coverage races 64 readers against the probe loop for 500ms and asserts a single client / single connect). All coverage lives in the test files `TwinCATHighFindingsRegressionTests.cs` and the new `TwinCATLowFindingsRegressionTests.cs`.
|
**Resolution:** Resolved 2026-05-23 — the previously-closed High findings each grew their regression coverage as they were resolved (see `TwinCATHighFindingsRegressionTests`: `ReinitializeAsync_applies_changed_device_config` for -001, `LInt_read_round_trips_value_above_int_MaxValue` + `DataType_mapping_preserves_width_and_signedness` for -002, `Concurrent_reads_on_one_device_create_a_single_client` + `Concurrent_reads_and_writes_share_one_client` for -007, `Symbol_version_changed_raises_OnRediscoveryNeeded` + `TwinCATDriver_implements_IRediscoverable` for -013). This pass added the two remaining gaps: `Structure_typed_pre_declared_tag_is_rejected_at_config_parse` (-003) and `Probe_loop_and_read_share_one_client_per_device` (-009 disposal-race coverage races 64 readers against the probe loop for 500ms and asserts a single client / single connect). All coverage lives in the test files `TwinCATHighFindingsRegressionTests.cs` and the new `TwinCATLowFindingsRegressionTests.cs`.
|
||||||
|
|
||||||
|
### Driver.TwinCAT-017
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Severity | Medium |
|
||||||
|
| Category | Correctness & logic bugs |
|
||||||
|
| Location | `TwinCATDriverFactoryExtensions.cs:168-188` (`TwinCATTagDto`) |
|
||||||
|
| Status | Resolved |
|
||||||
|
|
||||||
|
**Description:** `TwinCATTagDto` (the JSON-deserialization DTO for pre-declared tags in the
|
||||||
|
driver config) lacked an `ArrayLength` field. `BuildTag` therefore always constructed
|
||||||
|
`TwinCATTagDefinition` with `ArrayLength: null`, making it impossible to declare a 1-D array
|
||||||
|
pre-declared tag via the JSON driver config. The Phase 4c array-read path (`ReadValueAsync`
|
||||||
|
with `arrayCount != null`) and the array-discovery path (`IsArray` / `ArrayDim` in
|
||||||
|
`DiscoverAsync`) were both silently unavailable to operators who author pre-declared tags in
|
||||||
|
the driver config JSON — only equipment-tag JSON blobs (`TwinCATEquipmentTagParser`) and the
|
||||||
|
in-memory `TwinCATTagDefinition` constructor (used in tests) supported arrays.
|
||||||
|
|
||||||
|
This is a correctness gap introduced alongside Phase 4c (the two authoring surfaces were
|
||||||
|
inconsistent) but not caught because the Phase 4c tests use the in-memory constructor directly
|
||||||
|
rather than round-tripping through the JSON parser.
|
||||||
|
|
||||||
|
**Recommendation:** Add `int? ArrayLength` to `TwinCATTagDto` and thread it through `BuildTag`
|
||||||
|
(only positive values are honoured; null / non-positive keeps the scalar default, mirroring the
|
||||||
|
equipment-tag parser's `ReadArrayLength` guard).
|
||||||
|
|
||||||
|
**Resolution:** Resolved 2026-06-19 — added `int? ArrayLength` to `TwinCATTagDto`; `BuildTag`
|
||||||
|
now passes `ArrayLength: t.ArrayLength is > 0 ? t.ArrayLength : null` to the
|
||||||
|
`TwinCATTagDefinition` constructor. Two regression tests in `TwinCATArraySupportTests`:
|
||||||
|
`Predeclared_array_tag_arrayLength_parses_from_driver_config_json` (parse) and
|
||||||
|
`Predeclared_array_tag_from_json_reports_IsArray_in_discovery` (end-to-end discovery). All 170
|
||||||
|
unit tests pass. (SHA blank — fix committed in this review session.)
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ public static class TwinCATDriverFactoryExtensions
|
|||||||
$"TwinCAT tag '{t.Name}' in '{driverInstanceId}' missing SymbolPath"),
|
$"TwinCAT tag '{t.Name}' in '{driverInstanceId}' missing SymbolPath"),
|
||||||
DataType: dataType,
|
DataType: dataType,
|
||||||
Writable: t.Writable ?? true,
|
Writable: t.Writable ?? true,
|
||||||
WriteIdempotent: t.WriteIdempotent ?? false);
|
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||||
|
// Driver.TwinCAT-017: thread arrayLength through so JSON-authored pre-declared tags can
|
||||||
|
// declare 1-D arrays (Phase 4c). Only positive values are honoured; null / non-positive
|
||||||
|
// keeps the scalar default (ArrayLength: null on TwinCATTagDefinition).
|
||||||
|
ArrayLength: t.ArrayLength is > 0 ? t.ArrayLength : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field)
|
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field)
|
||||||
@@ -184,6 +188,14 @@ public static class TwinCATDriverFactoryExtensions
|
|||||||
|
|
||||||
/// <summary>Gets or sets a value indicating whether writes are idempotent.</summary>
|
/// <summary>Gets or sets a value indicating whether writes are idempotent.</summary>
|
||||||
public bool? WriteIdempotent { get; init; }
|
public bool? WriteIdempotent { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional 1-D array element count. When positive, the tag is a 1-D array of this
|
||||||
|
/// many <see cref="DataType"/> elements — drives <c>IsArray</c>/<c>ArrayDim</c> at
|
||||||
|
/// discovery and a native ADS array read at runtime (Phase 4c, Driver.TwinCAT-017).
|
||||||
|
/// <c>null</c> or non-positive = scalar (the default).
|
||||||
|
/// </summary>
|
||||||
|
public int? ArrayLength { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class TwinCATProbeDto
|
internal sealed class TwinCATProbeDto
|
||||||
|
|||||||
@@ -186,6 +186,69 @@ public sealed class TwinCATArraySupportTests
|
|||||||
def!.ArrayLength.ShouldBeNull();
|
def!.ArrayLength.ShouldBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- (4) Driver.TwinCAT-017 — pre-declared array tag parseable via driver config JSON ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A pre-declared tag in the driver config JSON with <c>arrayLength</c> must produce a
|
||||||
|
/// <see cref="TwinCATTagDefinition"/> with the correct <see cref="TwinCATTagDefinition.ArrayLength"/>,
|
||||||
|
/// which in turn drives <c>IsArray</c>/<c>ArrayDim</c> at discovery and a native array read
|
||||||
|
/// at runtime (Driver.TwinCAT-017).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Predeclared_array_tag_arrayLength_parses_from_driver_config_json()
|
||||||
|
{
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
devices = new[] { new { hostAddress = Host } },
|
||||||
|
tags = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
name = "Speeds",
|
||||||
|
deviceHostAddress = Host,
|
||||||
|
symbolPath = "MAIN.Speeds",
|
||||||
|
dataType = "DInt",
|
||||||
|
arrayLength = 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var parsed = TwinCATDriverFactoryExtensions.ParseOptionsForTests(json, "drv-1");
|
||||||
|
parsed.Tags.Single().ArrayLength.ShouldBe(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A pre-declared array tag parsed from JSON must surface as <c>IsArray=true</c> /
|
||||||
|
/// <c>ArrayDim=8</c> in the discovery address space.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Predeclared_array_tag_from_json_reports_IsArray_in_discovery()
|
||||||
|
{
|
||||||
|
var configJson = System.Text.Json.JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
devices = new[] { new { hostAddress = Host } },
|
||||||
|
tags = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
name = "Speeds",
|
||||||
|
deviceHostAddress = Host,
|
||||||
|
symbolPath = "MAIN.Speeds",
|
||||||
|
dataType = "DInt",
|
||||||
|
arrayLength = 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var builder = new RecordingBuilder();
|
||||||
|
var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1", new FakeTwinCATClientFactory());
|
||||||
|
await drv.InitializeAsync(configJson, CancellationToken.None);
|
||||||
|
|
||||||
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
||||||
|
|
||||||
|
var v = builder.Variables.Single(x => x.BrowseName == "Speeds").Info;
|
||||||
|
v.IsArray.ShouldBeTrue();
|
||||||
|
v.ArrayDim.ShouldBe(8u);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- helpers ----
|
// ---- helpers ----
|
||||||
|
|
||||||
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
||||||
|
|||||||
Reference in New Issue
Block a user