fix(virtual-tags): resolve Medium code-review findings (Core.VirtualTags-002, -003, -005, -008, -012)
Core.VirtualTags-002: cold-start guard publishes BadWaitingForInitialData instead of silently returning a stale value. Core.VirtualTags-003: Load detects duplicate Path values and keys the upstream-subscription loop off the registered tag set. Core.VirtualTags-005: VirtualTagSource fires the initial-data callback per path before registering the change observer, fixing an ordering race. Core.VirtualTags-008: DependencyGraph caches topological rank, lowering per-change-event cost from O(V+E) to O(closure). Core.VirtualTags-012: added 9 engine tests; CoerceResult null-return now maps to BadInternalError as the code comment intended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
| Review date | 2026-05-22 |
|
||||
| Commit reviewed | `76d35d1` |
|
||||
| Status | Reviewed |
|
||||
| Open findings | 12 |
|
||||
| Open findings | 7 |
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
@@ -67,7 +67,7 @@ code and docs agree.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Location | `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs:237` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** The cold-start guard `if (!AreInputsReady(ctxCache)) return;` silently
|
||||
abandons the evaluation when any input is null or Bad-quality. For a chained virtual tag
|
||||
@@ -87,7 +87,7 @@ rather than returning with no state change, so clients see a defined quality. If
|
||||
operators need scripts that handle Bad upstreams, consider a per-definition opt-out of
|
||||
the readiness guard.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — cold-start guard now publishes `BadWaitingForInitialData` (0x80320000) and notifies observers instead of silently returning, so OPC UA clients see a defined quality rather than a stale prior value.
|
||||
|
||||
### Core.VirtualTags-003
|
||||
|
||||
@@ -96,7 +96,7 @@ the readiness guard.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Location | `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs:117-120` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** The upstream-subscription loop in `Load` iterates
|
||||
`definitions.SelectMany(d => _tags[d.Path].Reads)`. If `definitions` contains two rows
|
||||
@@ -115,7 +115,7 @@ them to `compileFailures` (or a dedicated rejection list) so the aggregated
|
||||
`definitions.SelectMany(d => _tags[d.Path]...)` when collecting upstream paths so the
|
||||
collection is keyed off the registered set, not the raw input list.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — `Load` now tracks seen paths and adds a duplicate-path entry to `compileFailures`; the upstream-subscription loop iterates `_tags.Values` instead of the raw `definitions` list so it is keyed off the registered set.
|
||||
|
||||
### Core.VirtualTags-004
|
||||
|
||||
@@ -148,7 +148,7 @@ document precisely which `DriverDataType` values `CoerceResult` supports and val
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Location | `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs:50-64` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** `SubscribeAsync` registers the per-path engine observers first (lines
|
||||
52-56), then in a second loop reads the current value and fires the initial-data
|
||||
@@ -163,7 +163,7 @@ each path before registering the change observer for that path (or hold a per-ha
|
||||
lock spanning both so no engine callback interleaves). The initial value must be
|
||||
delivered before any subsequent change for that path.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — `SubscribeAsync` now fires the initial-data callback per path before registering the change observer for that path, eliminating the out-of-order delivery race.
|
||||
|
||||
### Core.VirtualTags-006
|
||||
|
||||
@@ -223,7 +223,7 @@ expected upper bound on group evaluation time relative to the interval.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Location | `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs:81-115` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** `TransitiveDependentsInOrder` calls `TopologicalSort()` (a full O(V+E)
|
||||
Kahn pass plus a Dictionary rank build) on every invocation, and it is invoked from
|
||||
@@ -237,7 +237,7 @@ end of `Load` and cache it on `DependencyGraph` (invalidated by `Add` / `Clear`)
|
||||
`TransitiveDependentsInOrder` then reuses the cached rank map. This turns a per-event
|
||||
O(V+E) cost into an O(closure) cost.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — `DependencyGraph` now caches the topological rank dictionary (invalidated by `Add`/`Clear`) via `GetOrBuildRank()`; `TransitiveDependentsInOrder` reuses it, reducing per-change-event cost from O(V+E) to O(closure).
|
||||
|
||||
### Core.VirtualTags-009
|
||||
|
||||
@@ -314,7 +314,7 @@ retained.
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Location | `tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** Several behaviours of the engine have no test coverage:
|
||||
(1) the cold-start `AreInputsReady` guard -- no test exercises an upstream that is
|
||||
@@ -333,7 +333,7 @@ double-to-int32 is tested);
|
||||
**Recommendation:** Add unit tests for each path above. Items (1), (2), and (6) directly
|
||||
correspond to open correctness findings and would have caught them.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — added 9 unit tests covering all 7 gaps: `AreInputsReady` guard publishes `BadWaitingForInitialData` and recovers; `SetVirtualTag` cascade to dependent; write to non-registered path; `EvaluateOneAsync` before `Load` and for unregistered path; `CoerceResult` failure maps to `BadInternalError`; duplicate-path rejection; `Read`/`Subscribe` before `Load`.
|
||||
|
||||
### Core.VirtualTags-013
|
||||
|
||||
|
||||
Reference in New Issue
Block a user