Files
UA-.NETStandard/tests/Opc.Ua.Server.Tests/ReverseConnectServerTimerTests.cs
Marc Schier 0e600c0f0b Fix hosted-server DI composition gaps (#4456)
# Description

Complete the remaining hosted-server DI hooks so applications can
compose the regular server without subclassing `StandardServer` solely
for these extension points.

- Add `ConfigureServerProperties` and ordered, per-server
`ConfigureResources` callbacks. BuildInfo defaults come from the
effective application configuration; explicit property registrations
take precedence without mutating shared options.
- Add authenticator instance/validator-aware factory overloads and
generic/async-delegate startup tasks. Startup tasks are awaited in
registration order, and failures or cancellation clean up the server.
Host shutdown waits for execution cleanup before disposing injected
dependencies, including when its shutdown deadline expires.
- Add asynchronous and legacy node-manager factory instances plus
deferred, configuration-aware `ArrayOf<IAsyncNodeManagerFactory>`
registrations. Resolve factories after configuration and certificate
setup while preserving ownership and feature isolation.
- Register alias stores before address-space creation and add opt-in
standard `TagVariables`/`Topics` materialization. Existing
custom-category behavior is retained; browse nodes are startup snapshots
while queries and supported LastChange values remain live.
- Derive the default DI server from `ReverseConnectServer`, retaining DI
roles/session hooks and authoritative XML/stream configuration. Ignore
queued callbacks from stopped or superseded timers to prevent
reconnection or rearming after shutdown.
- Fix resource translation lookup/formatting exposed by the new
callbacks, retain existing constructor signatures and
direct/custom-server paths, and document the optional hosting APIs.

Adopting these hooks is optional; existing construction and
subclass-based customization remain supported. The migration guide is
unchanged.

No changes to UA-.NETStandard-Samples and no new dependencies.

## Related Issues

- Fixes #4410

## Validation

- Focused server hosting, aliases, reverse-connect, configuration, and
resource suites: **571 passed on net10.0 and 571 passed on net48**.
- Existing affected real-session alias integration fixtures: **11 passed
on each framework**.
- Real reverse-connect discovery, session creation, and BuildInfo reads
verified for options, XML-file, and stream configuration.
- Published and executed the Windows NativeAOT test binary: **7
HostingAotTests passed**.
- Final server-library build passed across all configured target
frameworks with **0 warnings and 0 errors**; no new diagnostics in
changed test files.
- Focused changed-line coverage: **336/338 executable production lines
(99.41%)**.
- Deterministic timer regressions failed before the shutdown fix and
pass afterward.
- CI shutdown follow-up: the unchanged
`UnavailableUpstreamEndpointFailsMappedReadAsync` scenario passes
locally, and **245 hosting tests pass on each of net10.0 and net48**,
including regressions for an expired host shutdown deadline and
already-canceled startup. The local net48 run uses a short NUnit work
directory to avoid Windows path-length limits.

## Azure build 18136 follow-up

- Await WoT browse reconciliation before upload, default-version, and
enable/disable handlers return. Three barrier-controlled regressions
reproduce the exact missing version metadata before the fix.
- Make the reconciliation test observe the actual corrective state frame
rather than require a new acknowledgement after a no-op reseed. The
corrected tests also verify value, status, timestamp, and quiescence;
production gossip behavior is unchanged.
- Drain redirected stdout/stderr when sample processes exit, replacing a
fixed grace delay that could lose the final success line. A controlled
real-process regression fails before the fix.
- Fix CA1508, CA1823, CA1850, CA2012, and CA1861 at their sources, and
correct unsupported legacy AOT declarations and the existing
stress-placeholder target. Modern test targets and AOT analysis remain
enabled.

Follow-up validation:

- Full **Release `UA.slnx` builds pass on net10.0 and net48 with 0
warnings and 0 errors**.
- Complete previously failing WoT net48 job: **1496 passed**. The
identical binary was run from a short local assembly path to avoid the
nested repair worktree exceeding Windows path-length limits.
- Complete previously failing Redundancy.Server net10.0 job: **783
passed**.
- All short-haul HA/process cases: **12 passed**, including both
original identity-failover topologies.
- Affected WoT lifecycle selection: **113 passed on Release net10.0**;
controlled replication selection: **15 passed on each framework**.
- No test assertions, timeouts, coverage thresholds, collector settings,
or CI failure gates were weakened. Existing legacy empty-project result
notices and auto-property coverage exclusions remain unchanged.

These are full solution builds plus the reported test scopes, not a full
local `UA.slnx` test run.

Remote validation at `77c1055f1adb1557c5bf99b70e76f4a0cd95ee94`: **all
265 PR checks passed**, including Azure build **18172**, GitHub Build
and Test, CodeQL, and Docker image checks.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

---------

Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-10 05:56:52 +02:00

135 lines
5.6 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 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 System.Threading;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Server.Tests
{
/// <summary>
/// Exercises callbacks already queued when their reverse-connect timer is disposed.
/// </summary>
[TestFixture]
[Category("ReverseConnect")]
[Parallelizable]
public sealed class ReverseConnectServerTimerTests
{
[TestCase(false)]
[TestCase(true)]
public async Task QueuedCallbackAfterShutdownCannotConnectOrRearmAsync(bool dispose)
{
var scheduler = new QueuedTimerScheduler();
using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), scheduler.Clock.Object);
server.AddReverseConnection(s_clientUrl);
Action queuedCallback = scheduler.Callbacks[0];
if (dispose)
{
server.Dispose();
}
else
{
await server.StopAsync(CancellationToken.None).ConfigureAwait(false);
}
queuedCallback();
Assert.That(scheduler.Timers, Has.Count.EqualTo(1));
scheduler.Timers[0].Verify(timer => timer.Dispose(), Times.Once);
Assert.That(server.GetReverseConnections()[s_clientUrl].LastState, Is.EqualTo(ReverseConnectState.Closed));
}
[Test]
public void QueuedCallbackFromRemovedTimerDoesNotReplaceCurrentTimer()
{
var scheduler = new QueuedTimerScheduler();
using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), scheduler.Clock.Object);
server.AddReverseConnection(s_clientUrl, enabled: false);
Action queuedCallback = scheduler.Callbacks[0];
Assert.That(server.RemoveReverseConnection(s_clientUrl), Is.True);
server.AddReverseConnection(s_clientUrl, enabled: false);
queuedCallback();
Assert.That(scheduler.Timers, Has.Count.EqualTo(2));
scheduler.Timers[0].Verify(timer => timer.Dispose(), Times.Once);
scheduler.Timers[1].Verify(timer => timer.Dispose(), Times.Never);
}
[Test]
public void ActiveCallbackSchedulesTheNextAttempt()
{
var scheduler = new QueuedTimerScheduler();
using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), scheduler.Clock.Object);
server.AddReverseConnection(s_clientUrl, enabled: false);
scheduler.Callbacks[0]();
Assert.That(scheduler.Timers, Has.Count.EqualTo(2));
scheduler.Timers[0].Verify(timer => timer.Dispose(), Times.Once);
scheduler.Timers[1].Verify(timer => timer.Dispose(), Times.Never);
Assert.That(scheduler.DueTimes[0], Is.EqualTo(TimeSpan.FromSeconds(1)));
Assert.That(scheduler.DueTimes[1],
Is.EqualTo(TimeSpan.FromMilliseconds(ReverseConnectServer.DefaultReverseConnectInterval)));
}
private sealed class QueuedTimerScheduler
{
public QueuedTimerScheduler()
{
Clock.Setup(clock => clock.CreateTimer(
It.IsAny<TimerCallback>(),
It.IsAny<object>(),
It.IsAny<TimeSpan>(),
It.IsAny<TimeSpan>()))
.Returns((TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) =>
{
Assert.That(period, Is.EqualTo(Timeout.InfiniteTimeSpan));
var timer = new Mock<ITimer>();
Timers.Add(timer);
Callbacks.Add(() => callback(state));
DueTimes.Add(dueTime);
return timer.Object;
});
}
public Mock<TimeProvider> Clock { get; } = new();
public List<Mock<ITimer>> Timers { get; } = [];
public List<Action> Callbacks { get; } = [];
public List<TimeSpan> DueTimes { get; } = [];
}
private static readonly Uri s_clientUrl = new("opc.tcp://localhost:4840/queued-timer");
}
}