From 71fba920ecfb5bdbed46440e697f9c995d4b01a7 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 12 Aug 2026 09:30:50 +0200 Subject: [PATCH] Cover the data channel Service forwarders on the client sessions The advisory patch coverage gate reported 60.30% against a 75% floor. Most of the shortfall is structural rather than untested code: Opc.Ua.Bindings.Quic is 29.8% of the changed source lines and 60 of the 316 data channel tests skip on the CI runner because msquic is not installed there, so that assembly reads close to zero in the merged report while measuring 80.65% locally, where QUIC runs. Two genuine gaps were in that list and are closed here. ManagedSession and RedundantClientSession each forward the three data channel Services to the session they wrap, and neither forwarder was exercised: the existing integration test drives ISession directly, so it never passes through either facade. A dropped forwarder would be invisible until a caller found the Service silently doing nothing, and on the redundant facade it would leave a channel opened against a session the caller can no longer reach after failover. Both fixtures already assert this obligation for every other Service, so the new cases join the table-driven passthrough suite rather than standing alone. The [Experimental("DataChannels")] opt-in is scoped to the new cases with a pragma rather than switched on for the whole test project, so the attribute keeps its meaning everywhere else. Opc.Ua.Client.Tests 2126 pass, Opc.Ua.Redundancy.Client.Tests 123 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c --- .../Session/ManagedSessionComplianceTests.cs | 22 +++++++ .../RedundantClientSessionFacadeTests.cs | 59 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs b/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs index 6e3b3517e8..c3b1c00cc3 100644 --- a/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs +++ b/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs @@ -579,6 +579,28 @@ namespace Opc.Ua.Client.Tests.ManagedSession yield return Case( "Cancel", m => m.CancelAsync(null, 0, default).AsTask()); + + // The data channel Services are marked [Experimental("DataChannels")] + // so consumers opt in deliberately. This fixture asserts that every + // Service on the managed session reaches the inner one, and that + // obligation applies to an experimental Service as much as a stable + // one, so the opt-in is scoped to these three cases rather than + // switched on for the whole project. +#pragma warning disable DataChannels + yield return Case( + "OpenDataChannel", + m => m.OpenDataChannelAsync(null, + new NodeId(1u), 0, 0, + new DataChannelParametersDataType(), default).AsTask()); + yield return Case( + "ModifyDataChannel", + m => m.ModifyDataChannelAsync(null, 1, + new DataChannelParametersDataType(), default).AsTask()); + yield return Case( + "CloseDataChannel", + m => m.CloseDataChannelAsync(null, 1, + StatusCodes.Good, false, default).AsTask()); +#pragma warning restore DataChannels } private static TestCaseData Case( diff --git a/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs b/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs index 55b299d97a..0e64b3b251 100644 --- a/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs +++ b/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs @@ -659,6 +659,65 @@ namespace Opc.Ua.Client.Redundancy.Tests Assert.That(await facade.CancelAsync(null, 1, ct).ConfigureAwait(false), Is.SameAs(cancel)); } + /// + /// The data channel Services delegate to the current session like every + /// other Service on the facade. A redundant client that silently dropped + /// them would leave a channel opened against a session the caller can no + /// longer reach after a failover. + /// + [Test] + public async Task LeaderDataChannelServiceCallsDelegateToCurrentSessionAsync() + { + var session = new Mock(); + RedundantClientSession facade = CreateLeaderFacade(session); + CancellationToken ct = CancellationToken.None; + + var opened = new OpenDataChannelResponse(); + var modified = new ModifyDataChannelResponse(); + var closed = new CloseDataChannelResponse(); + + // Scoped opt-in: the Services carry [Experimental("DataChannels")], + // and the facade's delegation obligation applies to them regardless. +#pragma warning disable DataChannels + session.Setup(s => s.OpenDataChannelAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(opened)); + session.Setup(s => s.ModifyDataChannelAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(modified)); + session.Setup(s => s.CloseDataChannelAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(closed)); + + Assert.Multiple(async () => + { + Assert.That( + await facade.OpenDataChannelAsync(null, new NodeId(1u), 0, 0, null, ct) + .ConfigureAwait(false), + Is.SameAs(opened)); + Assert.That( + await facade.ModifyDataChannelAsync(null, 1, null, ct).ConfigureAwait(false), + Is.SameAs(modified)); + Assert.That( + await facade.CloseDataChannelAsync(null, 1, StatusCodes.Good, false, ct) + .ConfigureAwait(false), + Is.SameAs(closed)); + }); +#pragma warning restore DataChannels + } + [Test] public async Task LeaderConvenienceMethodsDelegateToCurrentSessionAsync() {