Skip to content

Commit 84fa8e8

Browse files
marcschierCopilotCopilotromanett
authored
CTT follow-up: monitored items, alarms, codecs, history, and model validation (#4002)
# Description Follow-up to #3978 based on the latest OPC UA CTT aggregate, matrix, Historical Access, Attribute, and Node Management runs. ## Aggregate run 16 - Ignore synthetic `BadBoundNotFound` raw-bound markers when feeding aggregate calculators. - Add direct and live historian regression coverage. ## Multi-dimensional Variant matrices - Fix `Variant.Copy()` so rank-2 Variant matrices preserve their payload and dimensions. - Enforce valid Variant matrix dimensions consistently across Binary, JSON, XML stream, and XML parser codecs. - Encoders return `BadEncodingError`; decoders return `BadDecodingError` for malformed matrix dimensions. - Add cross-codec valid/null/empty/malformed tests and a batched ReferenceServer Read regression. - Correct the previous CTT issue attribution: the invalid `Scalar_Static_Arrays2D_Variant` server encoding poisoned the batched CTT Read. ## Node Management AddNodes - Return `BadNodeIdRejected` for unsupported requested-NodeId namespaces. - Validate hierarchical ReferenceType source/target NodeClass constraints. - Document run-18 confirmation that all remaining 15 AddNodes errors are CTT project/script configuration issues. ## Historical Access and Attribute Services run 18 - Reject whitespace in `NumericRange` syntax with `BadIndexRangeInvalid`. - Add HistoryRead tests for operation-level invalid syntax and per-DataValue `BadIndexRangeNoData`. - Document CTT defects: - `012.js` expects `BadIndexRangeNoData` at the operation level instead of on returned DataValues; - `Err-012.js` uses a non-historizing node for an access-denied test; - generic Attribute array helpers omit NodeId[] and StatusCode[] conversion paths. ## Base Information model validation run 19 - Run the CTT main session as `sysadmin` over Basic256Sha256 SignAndEncrypt so SecurityAdmin RolePermissions and EncryptionRequired restrictions are both satisfied without weakening the server model. - Preserve predefined well-known NodeIds while making generated dynamic type and optional-child factories allocate compatible per-instance NodeIds, remap internal references, and update references from the owning subtree. - Materialize dynamic RoleType properties through generated helpers and keep role-child string NodeIds disjoint from numeric RoleManager root IDs. - Add net48/net10 regressions for generator output, NodeId allocation/reference remapping, DI mandatory children, dynamic roles, and low-privilege versus SecurityAdmin Browse visibility. - Document CTT defects for permission-unaware mandatory-child validation, the UA 1.04 reference model used against a UA 1.05 server, stale ServerDiagnostics cache, and the `ConformanceUnits` scalar/array mismatch. ## Related Issues - Follow-up to #3960 and #3978. ## Checklist - [x] I have signed the CLA and read the CONTRIBUTING document. - [x] Tests prove the fixes and improve coverage. - [x] Required CTT documentation is updated. - [x] Focused builds/tests introduce no warnings. - [x] Affected tests pass on .NET Framework and .NET 10. - [ ] CI and CodeQL are green. - [ ] All PR feedback is addressed. ## Latest CTT full run and A&C run 20 - Fix Monitor Value Change V2 `036.js`: an ambiguous `DataValue(StatusCode)` call encoded `BadIndexRangeNoData` as a Good value. - Prove all 19 configured matrix monitored items notify when the selected value actually changes; document the independent `042.js` script defects. - Rebase dynamic `CertificateExpired` / `TrustListOutOfDate` alarm roots and descendants, restore standard type declarations and ModellingRules, and expose the cross-node-manager references. - Deliver method-triggered events to every authorized Session, fixing the two-Session Confirm scenario. - Preserve locale-only `LocalizedText` values consistently across Binary, JSON, and XML and align Acknowledge/Confirm comment behavior and tests. - Document the remaining proven CTT defects for TransactionDiagnostics, SemanticChange, History `013`, PolicyId scope, A&C, durable subscriptions, and Subscription Minimum events. ## Base synchronization Merged `master` through `007cfaf90` (`#4010`, repository layout consolidation) and retained the matrix regression tests at their new `tests/` locations. ## Latest validation - Focused merged-tree tests pass on net10 and net48. - `Opc.Ua.Server` and `Quickstarts.Servers` Release builds complete with 0 warnings and 0 errors on net10 and net48. - The existing `DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync` timing test took its documented inconclusive timeout path after the layout merge; no test failure was reported. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: romanett <romanett98@gmail.com>
1 parent b76022d commit 84fa8e8

61 files changed

Lines changed: 4508 additions & 949 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/migrate/2.0.x/types.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ To migrate, perform the following general replacements in your code:
266266
6. **Decoders use the sentinel.** `IDecoder.ReadDataValue` (Binary, Xml, Json) returns `DataValue.Null` when the field is absent (or, for the binary encoder, when the encoding byte is `0`), allowing callers to distinguish "missing" from "present but empty".
267267
7. **Prefer `in DataValue` for synchronous method parameters.** The struct is large (~64 bytes after the IsNull sentinel) and copying it on every call is wasteful. The server `IDataChangeMonitoredItem.QueueValue(in DataValue, ...)` API has been updated accordingly. Async methods cannot use `in`/`ref` parameters, so leave those by-value.
268268
8. **`object? GetValue(Type)` and `T? GetValueOrDefault<T>()` are now `[Obsolete]`.** Use `WrappedValue.TryGetValue<T>(out T value)` or `WrappedValue.TryGetStructure<T>(out T value)` for type-safe extraction without throwing. `GetValue<T>(T defaultValue)` remains supported.
269-
9. **`DataValue.FromStatusCode(StatusCode)` and `FromStatusCode(StatusCode, DateTimeUtc serverTimestamp)`** are the preferred way to construct a `DataValue` that conveys only a status. The `DataValue(StatusCode)` and `DataValue(StatusCode, DateTimeUtc)` constructors are `[Obsolete]` because they conflict with overload resolution against the numeric `Variant` types (`uint`/`int`/`StatusCode` all implicitly convert in different directions).
269+
9. **`DataValue.FromStatusCode(StatusCode)` and `FromStatusCode(StatusCode, DateTimeUtc serverTimestamp)`** are the preferred way to construct a `DataValue` that conveys only a status. The `DataValue(StatusCode)` and `DataValue(StatusCode, DateTimeUtc)` constructors are `[Obsolete]` because they conflict with overload resolution against the numeric `Variant` types (`uint`/`int`/`StatusCode` all implicitly convert in different directions). Recompile existing `new DataValue(statusCode)` calls and replace the resulting obsolete warnings with `DataValue.FromStatusCode(statusCode)`; this also avoids relying on historical compiler overload selection that could encode the StatusCode as a Good Variant value.
270270

271271
**Change code as follows:**
272272

@@ -404,4 +404,3 @@ No changes are required, however there can be subtle bugs exposed, e.g.:
404404
- Related: [encoders.md](encoders.md), [source-generation.md](source-generation.md), [node-states.md](node-states.md).
405405
- [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table.
406406
- [Migration Guide](../../MigrationGuide.md) — landing page across versions.
407-

plans/ctt-issues.md

Lines changed: 179 additions & 647 deletions
Large diffs are not rendered by default.

samples/ConsoleReferenceClient/ClientSamples.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,7 @@ public async Task<ResultSet<DataValue>> ReadAllValuesAsync(
12051205
catch (ServiceResultException sre)
12061206
{
12071207
m_logger.Error(sre);
1208-
values.Add(new DataValue(sre.StatusCode));
1208+
values.Add(DataValue.FromStatusCode(sre.StatusCode));
12091209
errors.Add(sre.Result);
12101210
}
12111211
}

samples/Quickstarts.Servers/ReferenceServer/ReferenceNodeManager.cs

Lines changed: 257 additions & 22 deletions
Large diffs are not rendered by default.

samples/Quickstarts.Servers/SampleNodeManager/DataChangeMonitoredItem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public void ValueChanged(ISystemContext context)
370370

371371
if (ServiceResult.IsBad(error))
372372
{
373-
value = new DataValue(error.StatusCode);
373+
value = DataValue.FromStatusCode(error.StatusCode);
374374
}
375375

376376
value = value.WithServerTimestamp(DateTimeUtc.Now);

samples/UAReferenceServer.ctt.xml

Lines changed: 48 additions & 48 deletions
Large diffs are not rendered by default.

src/Opc.Ua.Core.Types/State/AcknowledgeableConditionState.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,8 @@ private static bool CanSetComment(LocalizedText comment)
487487
// Per OPC UA Part 9, a Null comment (no translation and empty/absent
488488
// text) passed to Acknowledge or Confirm must not overwrite the
489489
// Condition's existing Comment.
490-
return !comment.IsNullOrEmpty;
490+
return !comment.IsNullOrEmpty ||
491+
!string.IsNullOrEmpty(comment.Locale);
491492
}
492493

493494
private static LocalizedText NormalizeMethodComment(LocalizedText comment)

src/Opc.Ua.Di.Server/Builders/SoftwareUpdateFacetWiring.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,13 @@ private static void FinaliseChild(
394394
child.SymbolicName = browseName.Name ?? string.Empty;
395395
child.BrowseName = browseName;
396396
child.DisplayName = new LocalizedText(browseName.Name);
397-
child.NodeId = context.NodeIdFactory.New(context, child);
397+
NodeId previousNodeId = context.AssignInstanceNodeId(child);
398398
child.ReferenceTypeId = Types.ReferenceTypeIds.HasComponent;
399399
child.ModellingRuleId = NodeId.Null;
400-
context.AssignInstanceChildNodeIds(child);
400+
context.AssignInstanceChildNodeIds(
401+
child,
402+
previousNodeId,
403+
child.Parent ?? child);
401404
}
402405

403406
private static async ValueTask<ServiceResult> InvokePrepareAsync(

src/Opc.Ua.Server/Aggregates/AggregateCalculator.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ public bool QueueRawValue(DataValue value)
115115
}
116116

117117
// ignore placeholders in the stream.
118-
if (value.StatusCode == StatusCodes.BadNoData)
118+
if (value.StatusCode == StatusCodes.BadNoData ||
119+
value.StatusCode == StatusCodes.BadBoundNotFound)
119120
{
120121
return true;
121122
}
@@ -525,6 +526,11 @@ protected class TimeSlice
525526
/// </summary>
526527
public LinkedListNode<DataValue> EarlyBound { get; set; } = null!;
527528

529+
/// <summary>
530+
/// The latest non-Bad value before the slice.
531+
/// </summary>
532+
public LinkedListNode<DataValue> NonBadEarlyBound { get; set; } = null!;
533+
528534
/// <summary>
529535
/// The second early bound for the slice (always earlier than the first).
530536
/// </summary>
@@ -642,6 +648,11 @@ protected bool UpdateSlice(TimeSlice slice)
642648
// check if before the beginning of the slice.
643649
if (CompareTimestamps(slice.StartTime, ii) >= 0)
644650
{
651+
if (StatusCode.IsNotBad(ii.Value.StatusCode))
652+
{
653+
slice.NonBadEarlyBound = ii;
654+
}
655+
645656
if (IsGood(ii.Value))
646657
{
647658
slice.SecondEarlyBound = slice.EarlyBound;
@@ -678,6 +689,11 @@ protected bool UpdateSlice(TimeSlice slice)
678689
// check if before the beginning of the slice.
679690
if (CompareTimestamps(slice.StartTime, ii) > 0)
680691
{
692+
if (StatusCode.IsNotBad(ii.Value.StatusCode))
693+
{
694+
slice.NonBadEarlyBound = ii;
695+
}
696+
681697
if (IsGood(ii.Value))
682698
{
683699
slice.SecondEarlyBound = slice.EarlyBound;

src/Opc.Ua.Server/Aggregates/CountAggregateCalculator.cs

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -234,46 +234,30 @@ protected DataValue ComputeNumberOfTransitions(TimeSlice slice)
234234
return GetNoDataValue(slice);
235235
}
236236

237-
// determine whether a transition occurs at the StartTime
238-
double lastValue = double.NaN;
239-
240-
if (slice.EarlyBound != null && StatusCode.IsGood(slice.EarlyBound.Value.StatusCode))
241-
{
242-
try
243-
{
244-
lastValue = CastToDouble(slice.EarlyBound.Value);
245-
}
246-
catch (Exception)
247-
{
248-
lastValue = double.NaN;
249-
}
250-
}
237+
// The first non-Bad value is a transition when no previous non-Bad value exists.
238+
LinkedListNode<DataValue>? previousValue = slice.NonBadEarlyBound;
239+
bool hasLastValue = previousValue != null;
240+
Variant lastValue = previousValue != null
241+
? previousValue.Value.WrappedValue
242+
: Variant.Null;
251243

252244
// count the transitions.
253245
int count = 0;
254246

255247
for (int ii = 0; ii < values.Count; ii++)
256248
{
257-
if (!IsGood(values[ii]))
258-
{
259-
continue;
260-
}
261-
262-
double nextValue;
263-
try
264-
{
265-
nextValue = CastToDouble(values[ii]);
266-
}
267-
catch (Exception)
249+
if (StatusCode.IsBad(values[ii].StatusCode))
268250
{
269251
continue;
270252
}
271253

272-
if (!double.IsNaN(lastValue) && lastValue != nextValue)
254+
Variant nextValue = values[ii].WrappedValue;
255+
if (!hasLastValue || lastValue != nextValue)
273256
{
274257
count++;
275258
}
276259

260+
hasLastValue = true;
277261
lastValue = nextValue;
278262
}
279263

0 commit comments

Comments
 (0)