## 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>
28 KiB
Running the CTT against the reference server
How to run the OPC UA Compliance Test Tool (CTT) headless against
ConsoleReferenceServer, and how to triage the results. Findings that are CTT
script or configuration defects are recorded in ctt-issues.md.
Check that file before reporting a failure, and add new CTT defects there.
Setup
Placeholders used below:
| Placeholder | Meaning |
|---|---|
<CttDir> |
CTT installation folder, containing uacompliancetest.exe. Usually ...\OPC Foundation\UA 1.05\Compliance Test Tool. |
<ProjectDir> |
folder of a CTT server project (created in the CTT GUI) |
<Project> |
project name. The project files are <Project>.ctt.xml, <Project>.selection.xml and <Project>.results.xml. |
<LogDir> |
any scratch folder for logs, selections and results |
Inside <ProjectDir>:
| Item | Location |
|---|---|
| Script catalog | testscripts.xml, maintree\ (test scripts), library\ (helpers) |
| CU id → name map | Profiles\UACore\ProfileSet_UACore_1.05_*.xml |
Configure the project once in the GUI:
- Server URL:
opc.tcp://localhost:62541/Quickstarts/ReferenceServer. - Node settings: the HAProfile, aggregate, alarm and user settings for the reference server in CTT mode.
- Certificate trust: the CTT client certificate lives under
<ProjectDir>\PKI.
1. Build and start the server
dotnet build samples\Reference\ConsoleReferenceServer\ConsoleReferenceServer.csproj -c Release -f net10.0 -p:CustomTestTarget=net10.0
cd samples\Reference\ConsoleReferenceServer\bin\Release\net10.0
.\ConsoleReferenceServer.exe --ctt -a -c *> <LogDir>\refserver.log
--cttloads theCtt.ReferenceServerconfiguration section (Ctt.ReferenceServer.Config.xml), andApplyCTTModeAsyncstarts the CTT alarms and other presets. Always use it; without it many CUs fail for configuration reasons.-aauto-accepts the CTT client certificate. Without it, the first session is rejected unless the CTT cert is in the server's trusted store. Do not use-afor the security groups (section 8).-clogs to the console. Redirect to a file so you can correlate server errors with CTT timestamps.-l/-falso write an app log file.- The server is ready when it prints
Server started (... ms); allow about 10 s. Run it as a background task and wait until port 62541 is listening:Get-NetTCPConnection -LocalPort 62541 -State Listen. - Restart the server before every run. History, alarm and node-management tests change server state, and a reused server produces misleading follow-on failures.
- Before a comparison run, confirm the build commit: the server banner prints
OPC UA library: ... +<sha>.
2. Run the CTT from the command line
$p = Start-Process -FilePath "<CttDir>\uacompliancetest.exe" `
-ArgumentList @('--close','--hidden',
'--settings','"<ProjectDir>\<Project>.ctt.xml"',
'--selection','"<LogDir>\My.selection.xml"',
'--result','"<LogDir>\My.results.xml"') `
-WorkingDirectory "<CttDir>" `
-PassThru -Wait
$p.ExitCode # -1 = errors, 0 = OK, 1 = warnings only
| Flag | Meaning |
|---|---|
-s, --settings <file> |
CTT project to open (required) |
-l, --selection <file> |
checked profiles/CUs/test cases. Omit it to run the project's saved selection. |
-r, --result <file> |
results file. Omit it and the project's <Project>.results.xml is overwritten. |
-c, --close |
exit when finished (required for automation) |
-h, --hidden |
no GUI window |
-f, --file <script> |
run one specific script (mainly client testing) |
Things to know:
uacompliancetest.exeis a GUI binary. UseStart-Process -Wait -PassThruto wait for it and read its exit code. Stdout and stderr stay empty; everything goes to the results XML.- The CTT rewrites the selection file you pass on exit. Pass a copy you don't mind losing.
- The CTT also re-saves the project (
*.ctt.xml,testscripts.xml,libmodel.xml) on exit. That is normal. - Run it as a background task. A 4-test smoke run takes about 30 s. The full history selection (1,265 cases) takes about 1.5–2 minutes.
- Results accumulate. A results file that already exists gets a new top-level
ResultNode name="Debug RunN"appended. Always analyze the last run node, not the whole file. - The results file is written only when the run ends. Wait with a timeout instead of
-Wait($p.WaitForExit($ms), thenStop-Processon timeout): scripts that open a message box still do so with--hiddenand wait for input forever (A & C CertificateExpiration, see ctt-issues.md C44). A killed run leaves no results, so split long selections. - Only one CTT instance should talk to the server at a time. Check with
Get-Process uacompliancetestbefore starting another. - Full CTT documentation:
<CttDir>\help\command_line_interface.htm.
3. Build a selection file
A selection file lists conformance group → conformance unit → test case names.
The names must match the CTT's names exactly, including en dashes (Aggregate – Average)
and trailing spaces (Attribute Historical Read ). initialize.js and cleanup.js
run automatically; don't list them.
<UaTestCaseSelection xsi:noNamespaceSchemaLocation="testcaseselection.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ProjectInfo ProjectType="ServerProject" specificationversion="1.05.006" binaryversion="2.00.000+002" scriptversion="1.05.513"/>
<ConformanceGroups>
<ConformanceGroup name="Address Space Model">
<ConformanceUnit name="Address Space Base">
<TestCases>
<TestCase name="001.js"/>
</TestCases>
</ConformanceUnit>
</ConformanceGroup>
</ConformanceGroups>
</UaTestCaseSelection>
Copy the ProjectInfo versions from <ProjectDir>\testscripts.xml.
Don't derive CU names from script paths. Many CUs reuse another CU's scripts; all 37
Aggregate – * CUs point at Aggregates/Aggregate - Base/.... Resolve names by id
instead. testscripts.xml lists ConformanceUnit id=... with its test cases, and the
UACore profile set maps that id to Name and ConformanceGroup. This generator builds
a selection for every CU whose group or name matches a filter:
$r = "<ProjectDir>"
$p = [xml](Get-Content (Get-ChildItem "$r\Profiles\UACore\ProfileSet_UACore_1.05_*.xml")[0].FullName -Raw)
$cuName=@{}; $cuGroup=@{}; $grpName=@{}
foreach ($n in $p.SelectNodes("//*[local-name()='ConformanceGroup'][@Id]")) { $grpName[$n.Id] = $n.Name }
foreach ($n in $p.SelectNodes("//*[local-name()='ConformanceUnit'][@Id and @Name]")) {
$cuName[$n.Id] = $n.Name
$g = $n.SelectSingleNode("*[local-name()='ConformanceGroup']"); if ($g) { $cuGroup[$n.Id] = $g.InnerText } }
$x = [xml](Get-Content "$r\testscripts.xml" -Raw)
$filter = { param($g, $u) $g -in 'Historical Access','Aggregates' -or $u -match 'Hist' } # <- adjust
$map = [ordered]@{}
foreach ($cu in $x.UaTestScripts.ConformanceUnits.ConformanceUnit) {
$tcs = @($cu.TestCases.TestCase) | Where-Object { $_ }
if (-not $tcs -or -not $cuName.ContainsKey($cu.id)) { continue }
$u = $cuName[$cu.id]; $g = $grpName[$cuGroup[$cu.id]]
if (& $filter $g $u) { $map["$g`t$u"] = @($tcs | ForEach-Object name) } }
# Write $map as <ConformanceGroup>/<ConformanceUnit>/<TestCase> elements (UTF-8, no BOM).
The full history selection is the filter above: every CU in Historical Access and
Aggregates, plus every CU with Hist in its name (Attribute Historical Read/Update,
Auditing History Services, Base Info History * Capabilities). With scripts 1.05.513
that's 70 CUs and 1,265 test cases. Most Historical Access CUs other than Read Raw have
one placeholder case.
4. Read the results
The results XML (UaCttResults) is a tree of ResultNode elements. Test-case nodes
carry groupkey and unitkey, and their children are the individual messages. The
testresult attribute:
| Code | Meaning |
|---|---|
| 0 | Error (name="Error" message, or a test case that failed) |
| 1 | Warning |
| 2 | Recommendation / inconclusive |
| 3 | Not implemented (manual test) |
| 4 | Skipped (usually missing configuration) |
| 5 | Not supported (the server reports it doesn't support the feature) |
| 6 | Pass |
| 7 | Backtrace entry (filename, linenumber) that follows an Error/Warning |
Each Error message is followed by Backtrace nodes. The first backtrace entry inside
maintree/ is the failing line of the test. Entries in library/ show which helper
raised the error. Summary query:
$x = [xml](Get-Content "<LogDir>\My.results.xml" -Raw)
$run = @($x.DocumentElement.ChildNodes)[-1] # latest "Debug RunN"
$run.SelectNodes(".//ResultNode[@testresult='0' and @name='Error']") |
ForEach-Object { $tc = $_.ParentNode
[pscustomobject]@{ CU = $tc.GetAttribute('unitkey'); Test = $tc.GetAttribute('name')
Msg = ($_.GetAttribute('description') -split "`n")[0] } } |
Group-Object CU, Test, Msg | Sort-Object Count -Descending |
Select-Object Count, Name -First 80 | Format-Table -AutoSize -Wrap
Getting the values behind a failure
By default a project only records warnings and errors
(Advanced > Test Tool > SuppressLogEntries is checked). Aggregate failures then show
only "Query did not result in identical readings" with no values. To get evidence:
- Copy the project to a scratch folder, e.g.
robocopy <ProjectDir> <LogDir>\cttcopy\<Project> /E /XF <Project>.results.xml. Never patch the original project. - In the copy's
<Project>.ctt.xml, findSuppressLogEntriesand change the value in the nextColumn column="1"fromdata="2"todata="0".addLog()output then lands in the results asResultNode name="Log". print()output is never written to the results file; it only appears in the GUI output pane. The aggregate oracle comparison (HAAggregateHelper.js,CompareValues/CompareHistoryData) usesprint(). In the copy, switch those calls toaddLog()with a unique prefix (for exampleAGGDIAG SERVER/AGGDIAG CTT). SetprintResults = falseinPerformAggregateCheckso values are only logged for failing comparisons, and log the request (node, aggregate, start, end, interval, TreatUncertainAsBad, PercentDataGood/Bad, sloped, stepped) at the failure branch.- Restart the server and rerun the same selection against the copy. Error counts should match the official run within a handful of entries. Then parse the prefixed Log entries into a CSV; whitespace inside a description wraps, so normalize it first.
The CTT's own setup and teardown run as beforeTest.js / afterTest.js. A warning such
as ActivateSession ... delay in excess of 200ms on the first session is warm-up noise.
5. Triage: CTT defect or server defect?
For every distinct error signature:
- Check ctt-issues.md. Skip anything already documented there, including the known aggregate oracle differences and project configuration notes.
- Open the script at the backtrace line in
<ProjectDir>\maintree\...(orlibrary\...). JavaScriptTypeErrors (... [undefined] is not an object), wrong array indices, misspelled properties and inverted predicates are CTT defects. - Check the spec. Look up Part 4 (services), Part 11 (history) and Part 13 (aggregates) at https://reference.opcfoundation.org. Pay attention to service result vs operation result vs per-DataValue status.
- Check the server. Correlate the timestamp with the server log. Reproduce with a
focused test in
tests/Opc.Ua.Server.Tests(for aggregates:AggregateCttRegressionTests, which runs the calculator directly and through the live history dispatcher) before changing server code. - Classify as a server issue (with a spec reference and the location in
src/Opc.Ua.Serverorsamples/Quickstarts.Servers), a CTT issue (add toctt-issues.mdwith the test, line, the reason it is wrong and the recommended fix), or configuration (a project setting such as a blankProcessingIntervalor a non-historizing node).
6. Alarms and Conditions
The Alarms and Conditions group (28 CUs, 133 test cases with scripts 1.05.513) mostly waits for alarm events. Run it as one CTT process, without A & C CertificateExpiration, with a lower Alarm Cycle Time in a project copy. That takes about 19 minutes; running each CU on its own takes about 87 minutes plus a hang.
Recommended run
- Selection: every CU of the group except
A & C CertificateExpiration(27 CUs, 128 test cases). CertificateExpiration asks the operator to change the server clock through modal dialogs and hangs a--hiddenrun (ctt-issues.md C44); run it in the GUI if needed. The manual single-case CUs cost nothing. - One CTT process, fresh server. The CTT keeps one alarm thread for the whole group, so the initial event capture (one Alarm Cycle Time) is paid once, by the first A&C CU.
- Project copy with
/Server Test/Alarms and Conditions/Alarm Cycle Time= 30 (default 60). The setting is the length of the initial capture and one third of the maximum time of every collector test case. The reference server's alarm sources run a 40 s sawtooth and every alarm type reports an event at most 11 s apart, so 30 s still captures every type, and the longest event chain (about 45 s: active → inactive → acknowledge → confirm) stays below the 90 s maximum. - Timeout 45 minutes. The CTT writes results only at the end.
If you must split the group, do not start a part with a Limit/Level CU unless
/Server Test/Session/RequestedSessionTimeout is larger than Alarm Cycle Time × 1000: the CU session
idles during the initial capture, times out, and four test cases run to their maximum (C40).
Where the time goes
Measured on 2026-09-14 (CTT 1.05.06, scripts 1.05.513, ConsoleReferenceServer --ctt):
| CU | Own run, fresh server, cycle 60, before | Whole group, cycle 30, after | Dominant wait |
|---|---|---|---|
| Basic | 1:39 | 0:12 | initial capture (60 s) when first |
| Enable | 4:50 | 0:25 | Test_003.js phase race (C42), Err_004.js/Err_005.js event stall (C43) |
| Acknowledge | 2:48 | 1:43 | initial capture (first CU of the group run: 44 s), events |
| Confirm | 2:53 | 0:59 | events |
| Alarm | 4:48 | 1:54 | Test_002.js always runs to 3 × cycle (C41) |
| Refresh | 2:44 | 1:28 | fixed waits: Test_006.js 20 s, Err_004.js 30 s |
| Refresh2 | 3:02 | 1:44 | fixed waits: Test_006.js 20 s, Err_003.js 30 s |
| Shelving | 3:16 | 1:02 | one analog period in Test_002.js |
| Comment | 4:21 | 2:21 | four skipped test cases retry three times each (C39) |
| Exclusive Limit | 14:23 | 1:45 | before: session timeout during capture (C40); after: one limit sweep in Test_002.js/Test_003.js |
| Exclusive Level | 14:04 | 1:34 | same |
| Non-Exclusive Limit | 14:09 | 1:46 | same |
| Non-Exclusive Level | 14:05 | 1:46 | same |
| CertificateExpiration | hangs (modal dialog) | excluded | C44 |
| 13 manual CUs (incl. Dialog) | seconds | seconds | — |
| Total | about 87 min + hang | 18:45 |
The same group run with the default cycle of 60 s took 24:21 and 29:23. The spread comes from A & C
Enable: Test_003.js missed alarm types while the server's boolean and analog alarm sources stepped a
tick apart (C42, fixed on the server side), and after the Err_004.js burst the CTT alarm thread can stop
returning events (C43), so Err_004.js and Err_005.js run to 3 × cycle (6 minutes at cycle 60, 3 at
cycle 30).
How the A&C scripts spend time:
- Initial capture.
AlarmCollector.InitialEventCapturerecords events for one Alarm Cycle Time in the CU that starts the alarm thread (library/AlarmsAndConditions/AlarmCollector.js, lines 305–319). - Collector test cases (
AlarmCollector.RunSingleTest) track one condition per alarm type that sent an event and end when each has a pass, fail or skip, otherwise after 3 × Alarm Cycle Time (GetMaximumTestTime). A test case whose script never records a result for one type (C41, C42) always costs the maximum. - Server alarm simulation.
Alarms.AnalogSourceandAlarms.BooleanSourcestep by 5 per second from 50 to 100, down to 0 and back (40 s). Every condition changes state at the same ticks (70, 90, 85, 65, 30, 10, 15, 35), so the CTT sees an event from every alarm type at most 11 s apart and all limit states within one period. The shortest state lasts 4 s. Do not make the simulation faster: acknowledge, confirm and comment test cases call methods with the EventId of the event they just received and fail withBadEventIdUnknownwhen the next state change has replaced it. - Fixed waits in scripts: Refresh
Test_006.js20 s, RefreshErr_004.js/ Refresh2Err_003.js30 s, CommentErr_006.jsup to 20 s.
To measure it yourself, the test-case ResultNode timestamps are start times: a test case lasts until
the next one starts. For more detail, add
addLog( "ACTIMING test=" + testName + " ms=" + duration ) at the end of RunSingleTest in a project
copy with SuppressLogEntries unchecked (section 4); counters per alarm type
(this.TestTypeResults) show which type kept a test case open.
Expected result
With the server fixes of 2026-09-14, the recommended run reports errors only in A & C Alarm
Test_002.js (C10) and Test_004.js (C11), and A & C Enable Test_002.js (C12/C39). A & C Comment
skips Test_001.js–Test_004.js (C39) and Err_006.js; ten Shelving test cases pass without testing
anything unless chattering alarms are configured (see ctt-issues.md, CTT project configuration notes).
7. Session and Subscription Services
The Session Services group (4 CUs, 32 test cases) and the Subscription Services group (14 CUs, 215 test cases) are fast once the server is healthy. Run each group as one CTT process on a fresh server; splitting them per CU buys nothing.
Recommended run
- Selection: every CU of the group, built with the generator in section 3 (filter
$g -eq 'Session Services'or$g -eq 'Subscription Services'). Give each group its own fresh server; Session Base times sessions out and opens the maximum number of sessions (Err-019.js). - Server:
ConsoleReferenceServer.exe --ctt -a -c. Session Change User and Subscription Durable004.jslog in with/Server Test/Session/LoginNameGranted1(sysadmin/demo);LoginNameAccessDenied(username/password) is not a user of the reference server, so Session BaseErr-010.jsis rejected as it expects.Ctt.ReferenceServer.Config.xmlprovides MaxSessionCount 75, MinSessionTimeout 10 s and disables the certificate-keyed authentication lockout. The only test case with a not-trusted client certificate, Session Base012.js, uses SecurityPolicy None, where the certificate is not validated, and it passes with-a. - Timeouts: 20 minutes per group. The CTT writes results only at the end.
- Project path: keep the project copy on a short path (for example
%TEMP%\ctt\<Project>). The certificate settings are relative (PKI/CA/certs/ctt_appT.der); from a copy about 230 characters deep the CTT fails with "LoadCertificate failed to load certificate" and every test case errors.
Timings
Measured on 2026-09-14 (CTT 1.05.06, scripts 1.05.513, fresh server per part):
| CU | Cases | Duration |
|---|---|---|
| Subscription Basic | 99 | 5:17 |
| Subscription Durable | 20 | 1:45 |
| Subscription Minimum 02 | 29 | 2:05 |
| Subscription Minimum 05 | 12 | 0:40 |
| Subscription Publish Basic / Min 05 / Min 10 | 8 / 5 / 4 | 0:27 / 0:49 / 0:21 |
| Subscription Transfer | 29 | 0:47 |
| Subscription Multiple, PublishRequest Queue Overflow, Retransmission Queue, Durable StorageLevel High/Medium/Small | 1–3 each | 0:08–0:18 (manual or no test cases) |
| Subscription Services, one process | 215 | 10:56 |
| Session Services, one process | 32 | 0:45 (9:03 before the ActivateSession fix) |
The durations include the CTT's own start-up and project loading of about 10–15 s.
Before the server fix of 2026-09-14 the Session group took 9 minutes: after Session Base 002.js every
CreateSession waited for the CTT's 20 s request timeout (see ctt-issues.md, open server
findings).
Expected result
With the server fixes of 2026-09-14:
- Subscription Services: errors only in Subscription Minimum 02
020.js(issue 19) and Subscription Durable012.js(C38). Warnings: Durable002.js(RevisedLifetimeInHours 10 for a requested UInt32 max, expected), Publish Min 05003.js(project configuration), and CloseSession latency in Subscription BasicErr-011.js(always) and Publish Basiccleanup.js(sometimes). - Session Services: errors only in Session Base
Err-002.js,Err-005.jsandErr-022.js(C37). Skips:Err-009.js(no Kerberos in the CTT) andErr-023.js(the server offers SecurityPolicy None).
8. Monitored Item and Node Management Services
Both groups run with the normal recipe (--ctt -a -c), one CTT process per group and a fresh server.
| Group | CUs / test cases | Duration (2026-09-14) |
|---|---|---|
| Monitored Item Services | 14 / 208 | 9:45 |
| Node Management Services | 4 / 15 | 0:30 |
- Several Monitored Item test cases compare the request and response timestamps against
/Server Test/Time Tolerance(100 ms). Do not build or run tests in parallel with the group; a busy machine turns into "… Timestamp shows a delay in excess of …" warnings. - Leave
/Server Test/NodeIds/NodeManagement/RequestedNodeIddisabled (ctt-issues.md C34). - Delete Node
Err-002.jsadds 15,000 variables below one folder; it is the regression test for AddNodes below a parent with many children.
Expected results with the fixes of 2026-09-14: Monitor Basic 039.js fails (C17) and 038.js warns (C32);
Node Management Add Node Err-008.js fails (issue 9). Everything else passes or is Not Implemented, except
one or two sporadic "Timestamp shows a delay" warnings that move between Monitored Item test cases from run
to run (ctt-issues.md, open server findings).
9. Security groups
Security General and Security User Token test that the server rejects untrusted, expired,
revoked and otherwise invalid client and user certificates. Never run them against a server started
with -a: auto-accept makes every negative certificate test fail. Run the server with its normal
trust checks and trust the CTT's test PKI instead.
Certificate and trust setup
The CTT project ships the PKI the scripts expect in <ProjectDir>\PKI\copyToServer (created with
<CttDir>\create_ctt_pki.bat; rename_copyToServer_certs.bat only prefixes the file names with a
date). It mirrors a server's directory stores:
| CTT folder | Reference server store (Ctt.ReferenceServer.Config.xml) |
|---|---|
ApplicationInstance_PKI\trusted\certs, \crl |
TrustedPeerCertificates (pki/trusted) |
ApplicationInstance_PKI\issuers\certs, \crl |
TrustedIssuerCertificates (pki/issuer) |
X509UserIdentity_PKI\trusted\certs, \crl |
TrustedUserCertificates (pki/trustedUser) |
X509UserIdentity_PKI\issuers\certs, \crl |
UserIssuerCertificates (pki/issuerUser) |
The CTT connects with ctt_appT by default (CreateSession.js), which is in the trusted folder, so no
-a is needed for the positive tests. The folders deliberately also contain certificates that must be
rejected at connect time (for example ctt_appTE expired, ctt_appTV not yet valid, ctt_appTSincorrect
bad signature) and CRLs that revoke the ...R certificates. Copy them as they are.
Keep that PKI away from the server's default stores under %LocalAppData%\OPC Foundation\pki: other
runs (and -a) add certificates there, and the Push Model test cases change the trust lists and the
server certificate. Recommended layout:
- Create
<LogDir>\secpki\{own,trusted,issuer,trustedUser,issuerUser,rejected}withcerts/crl(ownneedscertsandprivate). - Copy the server's current application certificate and key (
pki\own\certsandpki\own\private,Quickstart Reference Server*) intosecpki\own, so the CTT keeps trusting the server certificate it already knows. Copy the fourcopyToServerfolders as in the table above. - Keep a pristine copy (
secpki.orig) and restore it (robocopy secpki.orig secpki /MIR) before every part: Push Model and Security Certificate Administration test cases write to the stores. - Copy
samples\Reference\ConsoleReferenceServer\bin\Release\net10.0to<LogDir>\srv-secand replace%LocalApplicationData%/OPC Foundation/pkiwith thesecpkipath in itsCtt.ReferenceServer.Config.xml(six store paths). LeaveAutoAcceptUntrustedCertificatesfalse. - Start
<LogDir>\srv-sec\ConsoleReferenceServer.exe --ctt -c(no-a).
Server and project settings
- User names:
/Server Test/Session/LoginNameGranted1=sysadmin/demoandLoginNameGranted2=user2/password1exist inReferenceServer.cs.LoginNameAccessDenied(username/password) does not exist; see ctt-issues.md for what that means for Security User Name Password 2012.js. Ctt.ReferenceServer.Config.xmlsetsMaxFailedAuthenticationAttemptsto 0. The user token CUs send many rejected tokens from the same client certificate; with the default lockout every later ActivateSession fails withBadUserAccessDenied. Security Invalid user token001.js/002.jstest that lockout and therefore need a run with the default value.- The CTT config enables Basic256Sha256 Sign and SignAndEncrypt plus None, and Anonymous, UserName and X509 user tokens on every endpoint.
Splitting the groups
Run Security User Token (14 CUs, 66 test cases) as one part and split Security General (53 CUs, 314 test cases) into Certificate Validation, certificate management (Push/Pull Model, Certificate Administration, Default ApplicationInstance Certificate, Security Administration), Role and User Management, and the rest. Restore the PKI and restart the server before every part. With scripts 1.05.513 most of Security General is Not Implemented, so every part finishes in under 30 seconds.
Wait for the server's Server started line, not only for the listening port: the port opens before startup
completes, and a CTT that connects too early gets BadServerHalted from GetEndpoints and records no test case
at all for the part.
Expected results
With the fixes of 2026-09-14 and the setup above:
| Part | Result |
|---|---|
| Security Certificate Validation | 23 pass, 24 Not Implemented; skips 004.js (CTT cannot send an empty certificate), 049.js/050.js (no Basic128Rsa15 endpoint) |
| Security None CreateSession ActivateSession (and 1.0) | all pass |
| Other Security General CUs | Not Implemented only |
| Security User Token | Anonymous 002.js fails (C35), User Name Password 2 015.js fails (C36); skips Anonymous 003.js and User Name Password 2 002.js (not applicable); X509 18 of 18 automated cases pass |
Negative certificate and user token tests passing here is only meaningful without -a.
Pitfalls
- Omitting
--resultsilently overwrites<Project>.results.xml. Back it up first if the previous run matters. - A server left running from an earlier session holds port 62541, and the CTT then
tests an old build. Check
Get-Process ConsoleReferenceServerand the banner sha. - Several automated runs sharing one machine must take turns. Wait until no
ConsoleReferenceServer, nouacompliancetest.exewith--settingsand no listener on port 62541 has been seen for about a minute, check again immediately before starting the server, and treat "Failed to establish tcp listener sockets on port 62541" in the server output as "busy, retry later". Never stop a server or CTT you did not start; auacompliancetest.exewithout arguments is the GUI.