mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
93559d06d3
## Why the CTT *Alarms and Conditions* group was so slow
I measured every A&C CU with a fresh `ConsoleReferenceServer --ctt` (CTT
1.05.06, scripts 1.05.513), read the per-test `ResultNode` timestamps,
and added `addLog` timing/counter instrumentation to a copy of the CTT
project.
| Cause | Effect |
| --- | --- |
| **CertificateExpiration** opens a modal dialog in `initialize.js`
("adjust the clock on the server"), and it also appears in `--hidden`
runs | The CTT never exits. This is why the whole group "did not finish
in 40 minutes". |
| **Limit/Level CUs:** `initialize.js` idles the CU session for one
Alarm Cycle Time (60 s) during the initial capture.
`RequestedSessionTimeout` is also 60 s. | The server correctly closes
the session. `LimitHelper` then gets `BadSessionIdInvalid` for its 5
filter subscriptions, and Test_003–006 each run 180 s, about 14 min per
CU. |
| Alarm `Test_002` never gives the AcknowledgeableCondition type a
result | Always runs 3 × cycle (180 s). |
| Enable `Test_003` needs every alarm type to go active within cycle/10
| Timed out in 6 of 7 runs, because boolean and analog alarms changed
state on different ticks. |
| Enable `Err_004` answers each event with Disable/Disable/Enable |
Feedback burst, after which the CTT alarm thread sometimes returns no
events. Details below. |
| Comment `Test_001`–`004` compare against
`CallHelper.ServerTimeOfCall`, which the CTT never sets (`0001-01-01`) |
Every comment event counts as unrelated, and the tests skip after 3
retries. The server's events carry the expected comment. |
Measured CU by CU, the group took **about 87 min plus the hang**. With
the fixes below, running the whole group in one CTT process without
CertificateExpiration takes **18:45** at Alarm Cycle Time 30. The per-CU
table is in `plans/ctt-testing.md` §6.
## Server changes
- **Refresh one subscription per request, not one per Call**
(`AlarmNodeManager.CallAsync`). The sample answered every
ConditionRefresh/ConditionRefresh2 after the first one in a Call with
`Bad_RefreshInProgress`, even for other subscriptions, and never
forwarded them. So CTT Refresh `Err_004` (10 subscriptions per Call)
only ever refreshed one of them. The check is now keyed by
SubscriptionId and MonitoredItemId.
- Regression tests:
`AlarmsAndConditionsRefreshTests.ConditionRefresh[2]OfDifferentSubscriptionsInOneCallSucceedsAsync`.
Both fail without the fix.
- **Boolean alarm source now uses the analog source's steps and limits**
(`AlarmController`, `ConditionTypeHolder.GetSeverity`). OffNormal,
Discrete and SystemOffNormal alarms now change state on the same tick as
the limit alarms (5 per second, 70/90/30/10), and the shortest state
still lasts 4 s.
- With this, Enable `Test_003` passed in 4 s instead of a timeout.
Confirm `Test_001` for the boolean types also stopped failing in both
group runs.
- Test:
`AlarmControllerTests.BooleanAndAnalogSourcesChangeStateOnTheSameStep`
(fails without the change).
- **Alarm controllers step on shared interval boundaries**
(`AlarmController.SetNextInterval`). Scheduling relative to the last
step let the two controllers drift up to one 100 ms simulation tick
apart, so the boolean events sometimes landed in a later publish and
Enable `Test_003` still timed out in 3 of 5 runs. With shared boundaries
it passed in 6 of 6 runs.
- Test:
`AlarmControllerTests.ControllersScheduleTheirStepsOnSharedIntervalBoundaries`.
I did not make the simulation faster. Acknowledge, Confirm and Comment
tests call methods with the EventId of the event they just received, and
a shorter state would make them race the next transition.
## Not reproduced / not a server defect
- **Refresh `Err_004` `BadSubscriptionIdInvalid` cascade.** In the
original run the alarm-thread session's subscriptions (15.5 s lifetime)
were gone after the script's 30 s wait. Nine further CTT runs passed all
28 Refresh/Refresh2 cases. An in-process replay with a sequential
publisher kept every subscription; that replay is what exposed the
refresh defect above.
- **Enable event stall (C43) is inside the CTT.** A server build that
logs every returned notification message showed that during a stall the
server sent 435 and 420 events to the two CTT event items in 173
messages, and the CTT acknowledged all of them, while its alarm-thread
buffers returned nothing. It only happens after `Err_004`'s
Disable/Enable feedback burst, in 8 of 28 runs.
## Docs
- `plans/ctt-testing.md`: new §6 *Alarms and Conditions*, covering the
recommended run, where the time goes, how to instrument a run and the
expected result. Also a note that a hidden CTT run can block on dialogs,
so always use a timeout.
- `plans/ctt-issues.md`: the A&C "open server findings" are replaced
with these results, and there are new CTT script defects C39–C44 plus
configuration notes (Shelving chattering alarms, Alarm Cycle Time vs
session timeout).
## Tests
- `Opc.Ua.History.Tests` A&C + AlarmClient: 155 passed.
- `Opc.Ua.Server.Tests` `AlarmControllerTests` /
`QuickstartsServerUtilitiesTests`: 4 passed.
- CTT whole-group runs (27 CUs, 128 cases): 18:45 at cycle 30 and 29:23
at cycle 60 on this branch; 23:43 and 24:21 with only the refresh fix.
The spread comes from the Enable stall (C43).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
132 lines
5.7 KiB
C#
132 lines
5.7 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Alarms;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
/// <summary>
|
|
/// Tests for the alarm simulation that drives the Quickstarts alarm node manager
|
|
/// in CTT mode.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Alarms")]
|
|
[Parallelizable]
|
|
public class AlarmControllerTests
|
|
{
|
|
private static readonly string[] s_limitStates = ["HighHigh", "High", "Normal", "Low", "LowLow"];
|
|
|
|
[Test]
|
|
public void BooleanAndAnalogSourcesChangeStateOnTheSameStep()
|
|
{
|
|
// CTT A and C Enable Test_003 needs an active event from every alarm type
|
|
// within a tenth of the Alarm Cycle Time. That only holds deterministically
|
|
// when the boolean and analog sources cross their limits on the same step.
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
var analog = new SteppableAlarmController(new BaseDataVariableState(null), isBoolean: false, telemetry);
|
|
var boolean = new SteppableAlarmController(new BaseDataVariableState(null), isBoolean: true, telemetry);
|
|
analog.Start(uint.MaxValue);
|
|
boolean.Start(uint.MaxValue);
|
|
|
|
var bands = new HashSet<string>();
|
|
for (int step = 0; step < 80; step++)
|
|
{
|
|
(int analogValue, bool analogActive) = analog.Step();
|
|
(int booleanValue, bool booleanActive) = boolean.Step();
|
|
|
|
Assert.That(booleanValue, Is.EqualTo(analogValue), $"step {step}");
|
|
bool limitActive = analogValue >= AlarmDefines.HIGH_ALARM ||
|
|
analogValue <= AlarmDefines.LOW_ALARM;
|
|
Assert.That(booleanActive, Is.EqualTo(limitActive), $"step {step} value {booleanValue}");
|
|
Assert.That(analogActive, Is.EqualTo(limitActive), $"step {step} value {analogValue}");
|
|
|
|
bands.Add(analogValue switch
|
|
{
|
|
>= AlarmDefines.HIGHHIGH_ALARM => "HighHigh",
|
|
>= AlarmDefines.HIGH_ALARM => "High",
|
|
<= AlarmDefines.LOWLOW_ALARM => "LowLow",
|
|
<= AlarmDefines.LOW_ALARM => "Low",
|
|
_ => "Normal"
|
|
});
|
|
}
|
|
|
|
Assert.That(bands, Is.EquivalentTo(s_limitStates),
|
|
"One simulation period must visit every limit state.");
|
|
}
|
|
|
|
[Test]
|
|
public void ControllersScheduleTheirStepsOnSharedIntervalBoundaries()
|
|
{
|
|
// Two controllers that stepped a few milliseconds apart must still be due in the
|
|
// same simulation pass; relative scheduling let them drift a timer tick apart.
|
|
var analogStep = new DateTime(2026, 9, 14, 9, 17, 4, 980, DateTimeKind.Local);
|
|
DateTime booleanStep = analogStep.AddMilliseconds(15);
|
|
|
|
DateTime analogNext = SteppableAlarmController.NextBoundary(analogStep, 1000);
|
|
DateTime booleanNext = SteppableAlarmController.NextBoundary(booleanStep, 1000);
|
|
|
|
Assert.That(analogNext, Is.EqualTo(new DateTime(2026, 9, 14, 9, 17, 5, DateTimeKind.Local)));
|
|
Assert.That(booleanNext, Is.EqualTo(analogNext));
|
|
Assert.That(analogNext.Kind, Is.EqualTo(DateTimeKind.Local));
|
|
Assert.That(
|
|
SteppableAlarmController.NextBoundary(analogNext.AddMilliseconds(2), 1000),
|
|
Is.EqualTo(analogNext.AddSeconds(1)),
|
|
"A step taken just after a boundary is due at the next one.");
|
|
}
|
|
|
|
private sealed class SteppableAlarmController : AlarmController
|
|
{
|
|
public SteppableAlarmController(
|
|
BaseDataVariableState variable,
|
|
bool isBoolean,
|
|
ITelemetryContext telemetry)
|
|
: base(variable, 1000, isBoolean, telemetry)
|
|
{
|
|
}
|
|
|
|
public (int Value, bool Active) Step()
|
|
{
|
|
int value = 0;
|
|
bool active = false;
|
|
GetValue(ref value, ref active);
|
|
return (value, active);
|
|
}
|
|
|
|
public static DateTime NextBoundary(DateTime now, int interval)
|
|
{
|
|
return GetNextIntervalBoundary(now, interval);
|
|
}
|
|
}
|
|
}
|
|
}
|