/* ======================================================================== * 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 System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Opc.Ua.Client; using Opc.Ua.Client.TestFramework; namespace Opc.Ua.Subscriptions.Tests { /// /// compliance tests for Subscription Minimum 02 covering /// minimum publishing interval, minimum lifetime count, /// minimum keepalive count, and server revision behavior. /// [NonParallelizable] [TestFixture] [Category("Conformance")] [Category("Subscription")] [Category("SubscriptionMinimum")] [Category("SubscriptionMinimum")] public class SubscriptionMinimumTests : TestFixture { [Test] public async Task MinimumPublishingIntervalZeroRevisedUpAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: 0).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalNegativeRevisedUpAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: -1).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalVerySmallRevisedUpAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: 0.001).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalOneMillisecondRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: 1).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalTenMillisecondsAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: 10).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalFiftyAcceptedOrRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: 50).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalConsistentAcrossCreatesAsync() { CreateSubscriptionResponse r1 = await CreateSubAsync( interval: 1).ConfigureAwait(false); CreateSubscriptionResponse r2 = await CreateSubAsync( interval: 1).ConfigureAwait(false); Assert.That(r1.RevisedPublishingInterval, Is.EqualTo(r2.RevisedPublishingInterval), "Same requested interval should produce same revision."); await DeleteSubAsync(r1.SubscriptionId).ConfigureAwait(false); await DeleteSubAsync(r2.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumPublishingIntervalFromServerCapabilitiesAsync() { // Read MinSupportedSamplingInterval from ServerCapabilities ReadResponse readResp = await Session.ReadAsync( null, 0, TimestampsToReturn.Neither, new ReadValueId[] { new() { NodeId = VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, AttributeId = Attributes.Value } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); if (!StatusCode.IsGood(readResp.Results[0].StatusCode)) { Assert.Fail( "MinSupportedSamplingInterval not available."); } double minRate = readResp.Results[0].WrappedValue.GetDouble(); CreateSubscriptionResponse resp = await CreateSubAsync( interval: 0).ConfigureAwait(false); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThanOrEqualTo(minRate)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumLifetimeCountZeroRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: 0, keepAlive: 5).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumLifetimeCountOneRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: 1, keepAlive: 5).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThanOrEqualTo( 3 * resp.RevisedMaxKeepAliveCount)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumLifetimeCountTwoRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: 2, keepAlive: 5).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThanOrEqualTo( 3 * resp.RevisedMaxKeepAliveCount)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumLifetimeCountExactlyThreeTimesKeepAliveAsync() { // Request lifetime = 3 * keepAlive exactly CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: 15, keepAlive: 5).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThanOrEqualTo( 3 * resp.RevisedMaxKeepAliveCount)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumLifetimeCountLessThanThreeTimesRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: 10, keepAlive: 10).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThanOrEqualTo( 3 * resp.RevisedMaxKeepAliveCount)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumLifetimeCountMaxUint32RevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: uint.MaxValue).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task LifetimeCountRevisedValueIsPositiveAsync() { foreach (uint lt in new uint[] { 0, 1, 5, 100, uint.MaxValue }) { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: lt).ConfigureAwait(false); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThan(0u), $"Lifetime {lt} should produce positive revised."); await DeleteSubAsync(resp.SubscriptionId) .ConfigureAwait(false); } } [Test] public async Task MinimumKeepAliveCountZeroRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( keepAlive: 0).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumKeepAliveCountOneAcceptedOrRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( keepAlive: 1).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task MinimumKeepAliveCountMaxUint32RevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( keepAlive: uint.MaxValue).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task KeepAliveCountRevisedValueIsPositiveAsync() { foreach (uint ka in new uint[] { 0, 1, 5, 100, uint.MaxValue }) { CreateSubscriptionResponse resp = await CreateSubAsync( keepAlive: ka).ConfigureAwait(false); Assert.That(resp.RevisedMaxKeepAliveCount, Is.GreaterThan(0u), $"KeepAlive {ka} should produce positive revised."); await DeleteSubAsync(resp.SubscriptionId) .ConfigureAwait(false); } } [Test] public async Task KeepAliveCountRevisionConsistentAcrossCreatesAsync() { CreateSubscriptionResponse r1 = await CreateSubAsync( keepAlive: 1).ConfigureAwait(false); CreateSubscriptionResponse r2 = await CreateSubAsync( keepAlive: 1).ConfigureAwait(false); Assert.That(r1.RevisedMaxKeepAliveCount, Is.EqualTo(r2.RevisedMaxKeepAliveCount)); await DeleteSubAsync(r1.SubscriptionId).ConfigureAwait(false); await DeleteSubAsync(r2.SubscriptionId).ConfigureAwait(false); } [Test] public async Task ModifySubscriptionKeepAliveCountZeroRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync() .ConfigureAwait(false); uint id = resp.SubscriptionId; ModifySubscriptionResponse mod = await Session.ModifySubscriptionAsync( null, id, 500, DefaultLifetime, 0, 0, 0, CancellationToken.None).ConfigureAwait(false); Assert.That( StatusCode.IsGood(mod.ResponseHeader.ServiceResult), Is.True); Assert.That(mod.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(id).ConfigureAwait(false); } [Test] public async Task ModifySubscriptionKeepAliveCountRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync() .ConfigureAwait(false); uint id = resp.SubscriptionId; ModifySubscriptionResponse mod = await Session.ModifySubscriptionAsync( null, id, 500, DefaultLifetime, 1, 0, 0, CancellationToken.None).ConfigureAwait(false); Assert.That( StatusCode.IsGood(mod.ResponseHeader.ServiceResult), Is.True); Assert.That(mod.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(id).ConfigureAwait(false); } [Test] public async Task RevisedValuesConsistentWithinSameSessionAsync() { CreateSubscriptionResponse r1 = await CreateSubAsync( interval: 50, lifetime: 10, keepAlive: 3) .ConfigureAwait(false); CreateSubscriptionResponse r2 = await CreateSubAsync( interval: 50, lifetime: 10, keepAlive: 3) .ConfigureAwait(false); Assert.That(r1.RevisedPublishingInterval, Is.EqualTo(r2.RevisedPublishingInterval)); Assert.That(r1.RevisedLifetimeCount, Is.EqualTo(r2.RevisedLifetimeCount)); Assert.That(r1.RevisedMaxKeepAliveCount, Is.EqualTo(r2.RevisedMaxKeepAliveCount)); await DeleteSubAsync(r1.SubscriptionId).ConfigureAwait(false); await DeleteSubAsync(r2.SubscriptionId).ConfigureAwait(false); } [Test] public async Task RevisedPublishingIntervalGreaterThanZeroAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: -100).ConfigureAwait(false); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task RevisedLifetimeAlwaysThreeTimesKeepAliveAsync() { foreach (uint ka in new uint[] { 1, 5, 10, 50 }) { CreateSubscriptionResponse resp = await CreateSubAsync( lifetime: ka, keepAlive: ka) .ConfigureAwait(false); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThanOrEqualTo( 3 * resp.RevisedMaxKeepAliveCount), $"With KeepAlive={ka}"); await DeleteSubAsync(resp.SubscriptionId) .ConfigureAwait(false); } } [Test] public async Task AllRevisedValuesReturnedInResponseAsync() { CreateSubscriptionResponse resp = await CreateSubAsync() .ConfigureAwait(false); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThan(0u)); Assert.That(resp.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task ModifySubscriptionRevisesAllParametersAsync() { CreateSubscriptionResponse resp = await CreateSubAsync() .ConfigureAwait(false); uint id = resp.SubscriptionId; ModifySubscriptionResponse mod = await Session.ModifySubscriptionAsync( null, id, 0.001, 1, 1, 0, 0, CancellationToken.None).ConfigureAwait(false); Assert.That( StatusCode.IsGood(mod.ResponseHeader.ServiceResult), Is.True); Assert.That(mod.RevisedPublishingInterval, Is.GreaterThan(0)); Assert.That(mod.RevisedLifetimeCount, Is.GreaterThan(0u)); Assert.That(mod.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(id).ConfigureAwait(false); } [Test] public async Task CreateSubscriptionAllBelowMinimumAllRevisedAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: -1, lifetime: 0, keepAlive: 0) .ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); Assert.That(resp.RevisedPublishingInterval, Is.GreaterThan(0)); Assert.That(resp.RevisedLifetimeCount, Is.GreaterThan(0u)); Assert.That(resp.RevisedMaxKeepAliveCount, Is.GreaterThan(0u)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task RevisedValuesDoNotExceedServerMaximumsAsync() { CreateSubscriptionResponse resp = await CreateSubAsync( interval: double.MaxValue, lifetime: uint.MaxValue, keepAlive: uint.MaxValue).ConfigureAwait(false); Assert.That(resp.RevisedPublishingInterval, Is.LessThan(double.MaxValue)); Assert.That(resp.RevisedLifetimeCount, Is.LessThan(uint.MaxValue)); Assert.That(resp.RevisedMaxKeepAliveCount, Is.LessThan(uint.MaxValue)); await DeleteSubAsync(resp.SubscriptionId).ConfigureAwait(false); } [Test] public async Task CreateTwoEqualPrioritySubsPublishBothAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2, "dd"u8.ToArray()).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); var receivedSubs = new HashSet(); for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); receivedSubs.Add(pub.SubscriptionId); } Assert.That(receivedSubs, Has.Count.EqualTo(2), "Both equal-priority subscriptions should be serviced."); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task CreateTwoSubsWithItemsPublishCallbacksAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "At least one subscription should have received data."); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task CreateSubsMonitorWritePublishCleanupAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0)); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task CreateSubsWritePublishVerifyNotificationsAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); // Drain initial notifications for (int i = 0; i < 4; i++) { await PublishAsync().ConfigureAwait(false); } foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "Expected data change after writing values."); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task ModifySubRaisePriorityPublishVerifyAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2, [1, 1]).ConfigureAwait(false); // Raise priority of first subscription ModifySubscriptionResponse mr = await Session.ModifySubscriptionAsync( null, subs[0].SubId, DefaultInterval, DefaultLifetime, DefaultKeepAlive, 0, 200, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(mr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task ModifySubLowerPriorityPublishVerifyAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2, [200, 200]).ConfigureAwait(false); // Lower priority of first subscription ModifySubscriptionResponse mr = await Session.ModifySubscriptionAsync( null, subs[0].SubId, DefaultInterval, DefaultLifetime, DefaultKeepAlive, 0, 1, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(mr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task ModifySubRaiseThenLowerPriorityAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2, [1, 1]).ConfigureAwait(false); // Raise priority await Session.ModifySubscriptionAsync( null, subs[0].SubId, DefaultInterval, DefaultLifetime, DefaultKeepAlive, 0, 200, CancellationToken.None).ConfigureAwait(false); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); await PublishAsync().ConfigureAwait(false); // Lower priority to lowest ModifySubscriptionResponse mr = await Session.ModifySubscriptionAsync( null, subs[0].SubId, DefaultInterval, DefaultLifetime, DefaultKeepAlive, 0, 0, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(mr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task ModifySubSettingsWritePublishAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); ModifySubscriptionResponse mr = await Session.ModifySubscriptionAsync( null, subs[0].SubId, 500, DefaultLifetime, DefaultKeepAlive, 0, 100, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(mr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)mr.RevisedPublishingInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0)); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeToggleOnTwoSubsAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); // Drain initial for (int i = 0; i < 4; i++) { await PublishAsync().ConfigureAwait(false); } // Disable both SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, false, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach (StatusCode sc in sr.Results) { Assert.That(StatusCode.IsGood(sc), Is.True); } foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); PublishResponse pubDisabled = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pubDisabled)) { Assert.Ignore("Timing-sensitive: received stale notification while disabled."); } // Re-enable both sr = await Session.SetPublishingModeAsync( null, true, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "Expected data after re-enabling publishing."); foreach (uint id in ids) { await DeleteSubAsync(id).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeDisableOneOfTwoAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 4; i++) { await PublishAsync().ConfigureAwait(false); } // Disable second subscription only SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, false, new uint[] { subs[1].SubId }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); await WriteInt32ValueAsync(subs[0].NodeId, s_random.Next()).ConfigureAwait(false); await WriteInt32ValueAsync(subs[1].NodeId, s_random.Next()).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); bool gotFirst = false; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub) && pub.SubscriptionId == subs[0].SubId) { gotFirst = true; } } if (!gotFirst) { Assert.Ignore("Timing-sensitive: enabled subscription not serviced within publish window."); } foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeEnableDisabledSubAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2, enabled: false).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; // Enable both SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, true, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "Expected data after enabling previously disabled subscriptions."); foreach (uint id in ids) { await DeleteSubAsync(id).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeDisableBothSubsAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 4; i++) { await PublishAsync().ConfigureAwait(false); } SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, false, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { Assert.Ignore("Timing-sensitive: received stale notification on disabled subscriptions."); } foreach (uint id in ids) { await DeleteSubAsync(id).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeReEnableBothSubsAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 4; i++) { await PublishAsync().ConfigureAwait(false); } // Disable then re-enable await Session.SetPublishingModeAsync( null, false, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, true, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "Expected data after re-enabling both subscriptions."); foreach (uint id in ids) { await DeleteSubAsync(id).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeDisableOneVerifyOtherContinuesAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); // Verify both initially deliver var initialSubs = new HashSet(); for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { initialSubs.Add(pub.SubscriptionId); } } // Disable second SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, false, new uint[] { subs[1].SubId }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); await WriteInt32ValueAsync(subs[0].NodeId, s_random.Next()).ConfigureAwait(false); await WriteInt32ValueAsync(subs[1].NodeId, s_random.Next()).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); bool enabledGotData = false; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub) && pub.SubscriptionId == subs[0].SubId) { enabledGotData = true; } } if (!enabledGotData) { Assert.Ignore("Timing-sensitive: enabled subscription not serviced within publish window."); } foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task SetPublishingModeToggleVerifyStopsReportingAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 4; i++) { await PublishAsync().ConfigureAwait(false); } // Disable first await Session.SetPublishingModeAsync( null, false, new uint[] { subs[0].SubId }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); bool disabledGotData = false; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub) && pub.SubscriptionId == subs[0].SubId) { disabledGotData = true; } } Assert.That(disabledGotData, Is.False, "Disabled subscription should not report data."); foreach (uint id in ids) { await DeleteSubAsync(id).ConfigureAwait(false); } } [Test] public async Task DeleteMultipleValidSubscriptionsAsync() { const int count = 5; uint[] ids = new uint[count]; for (int i = 0; i < count; i++) { CreateSubscriptionResponse cr = await CreateSubAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(cr.ResponseHeader.ServiceResult), Is.True); ids[i] = cr.SubscriptionId; } DeleteSubscriptionsResponse dr = await Session.DeleteSubscriptionsAsync( null, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(dr.ResponseHeader.ServiceResult), Is.True); Assert.That(dr.Results.Count, Is.EqualTo(count)); foreach (StatusCode sc in dr.Results) { Assert.That(StatusCode.IsGood(sc), Is.True); } } [Test] public async Task CreateSubsWithItemsPublishThenDeleteAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0)); uint[] ids = [.. subs.Select(s => s.SubId)]; DeleteSubscriptionsResponse dr = await Session.DeleteSubscriptionsAsync( null, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(dr.ResponseHeader.ServiceResult), Is.True); } [Test] public async Task PublishRepublishVerifyRetransmissionAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(1).ConfigureAwait(false); uint id = subs[0].SubId; NodeId nodeId = subs[0].NodeId; await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); Assert.That(HasDataChangeNotification(pub), Is.True); uint seqNum = pub.NotificationMessage.SequenceNumber; try { RepublishResponse rp = await Session.RepublishAsync( null, id, seqNum, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(rp.ResponseHeader.ServiceResult), Is.True); Assert.That(rp.NotificationMessage.SequenceNumber, Is.EqualTo(seqNum)); } catch (ServiceResultException ex) when ( ex.StatusCode == StatusCodes.BadMessageNotAvailable) { Assert.Ignore("Republish not supported or message not available."); } await DeleteSubAsync(id).ConfigureAwait(false); } [Test] public async Task CreateSubsLifecycleCleanupAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); } uint[] ids = [.. subs.Select(s => s.SubId)]; DeleteSubscriptionsResponse dr = await Session.DeleteSubscriptionsAsync( null, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(dr.ResponseHeader.ServiceResult), Is.True); foreach (StatusCode sc in dr.Results) { Assert.That(StatusCode.IsGood(sc), Is.True); } } [Test] public async Task CreateSubsWriteVerifyNotificationDeliveryAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); var receivedSubs = new HashSet(); for (int i = 0; i < 4; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { receivedSubs.Add(pub.SubscriptionId); } } Assert.That(receivedSubs, Is.Not.Empty, "Expected at least one subscription to deliver data."); foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task FiveSubsWithPrioritiesHighestDominatesAsync() { try { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(5, [1, 1, 1, 1, 200]).ConfigureAwait(false); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); var firstResponders = new List(); for (int round = 0; round < 3; round++) { foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 200).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { firstResponders.Add(pub.SubscriptionId); } } // Drain remaining for (int i = 0; i < 10; i++) { await PublishAsync().ConfigureAwait(false); } // Priority 200 sub should be first most often (warning-level check) if (firstResponders.Count > 0) { int highPrioCount = firstResponders.Count( id => id == subs[4].SubId); Assert.That(highPrioCount, Is.GreaterThanOrEqualTo(0), "Highest priority subscription should be serviced " + "(server may not implement strict priority ordering)."); } foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } catch (ServiceResultException ex) when (ex.StatusCode == StatusCodes.BadRequestTimeout) { Assert.Ignore("Timing-sensitive: publish request timed out."); } } [Test] public async Task TwoSubsPublishBothReceiveCallbacksAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(2).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); // Drain any stale notifications first for (int i = 0; i < 10; i++) { try { await PublishAsync().ConfigureAwait(false); } catch (ServiceResultException) { break; } } // Write to trigger notifications on our subscriptions foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); var receivedSubs = new HashSet(); for (int i = 0; i < 10; i++) { try { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); receivedSubs.Add(pub.SubscriptionId); } catch (ServiceResultException) { break; } } foreach ((uint subId, _) in subs) { if (!receivedSubs.Contains(subId)) { Assert.Fail($"Timing-sensitive: subscription {subId} not serviced within publish window."); } } uint[] ids = [.. subs.Select(s => s.SubId)]; await DeleteSubsAsync(ids).ConfigureAwait(false); } [Test] public async Task FiveSubsDisabledThenEnablePublishAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(5, enabled: false).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; // Publishing should not produce data PublishResponse pubDisabled = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pubDisabled)) { Assert.Fail("Timing-sensitive: received stale notification on disabled subscriptions."); } // Enable all SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, true, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 10; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "Expected notifications after enabling all subscriptions."); await DeleteSubsAsync(ids).ConfigureAwait(false); } [Test] public async Task FiveSubsDisableEvenNumberedVerifyOddContinueAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(5).ConfigureAwait(false); uint[] allIds = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 10; i++) { await PublishAsync().ConfigureAwait(false); } // Disable even-indexed (1, 3) subscriptions uint[] evenIds = [subs[1].SubId, subs[3].SubId]; SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, false, evenIds.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); var enabledSet = new HashSet { subs[0].SubId, subs[2].SubId, subs[4].SubId }; bool enabledGotData = false; for (int i = 0; i < 10; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub) && enabledSet.Contains(pub.SubscriptionId)) { enabledGotData = true; } } if (!enabledGotData) { Assert.Fail("Timing-sensitive: enabled subscriptions not serviced within window."); } await DeleteSubsAsync(allIds).ConfigureAwait(false); } [Test] public async Task ThreeSubsWithPriorities1And125And255Async() { try { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(3, [1, 125, 255]).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); // Drain initial for (int i = 0; i < 6; i++) { await PublishAsync().ConfigureAwait(false); } var firstResponders = new List(); for (int round = 0; round < 3; round++) { foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 200).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { firstResponders.Add(pub.SubscriptionId); } } // Drain remaining for (int i = 0; i < 10; i++) { await PublishAsync().ConfigureAwait(false); } // Highest priority (255) should be serviced first (informational) if (firstResponders.Count > 0) { int hiPrioCount = firstResponders.Count( id => id == subs[2].SubId); Assert.That(hiPrioCount, Is.GreaterThanOrEqualTo(0), "Priority=255 subscription should tend to be serviced first."); } foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } catch (ServiceResultException ex) when (ex.StatusCode == StatusCodes.BadRequestTimeout) { Assert.Ignore("Timing-sensitive: publish request timed out."); } } [Test] public async Task FiveSamePrioritySubsRoundRobinFairnessAsync() { const int subCount = 5; List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(subCount, "22222"u8.ToArray()).ConfigureAwait(false); // Publish initial keep-alives await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < subCount * 2; i++) { await PublishAsync().ConfigureAwait(false); } // Write values and publish multiple rounds var servicedSubs = new HashSet(); for (int round = 0; round < 3; round++) { foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 200).ConfigureAwait(false); for (int i = 0; i < subCount; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { servicedSubs.Add(pub.SubscriptionId); } } } // All same-priority subscriptions should eventually be serviced Assert.That(servicedSubs, Has.Count.GreaterThanOrEqualTo(2), "Multiple equal-priority subscriptions should be serviced " + "in a fair manner."); uint[] allIds = [.. subs.Select(s => s.SubId)]; await DeleteSubsAsync(allIds).ConfigureAwait(false); } [Test] public async Task FiveSubsPriority1And200HighestDominatesAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(5, [1, 1, 1, 1, 200]).ConfigureAwait(false); var firstResponders = new List(); for (int round = 0; round < 5; round++) { foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 200).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { firstResponders.Add(pub.SubscriptionId); } // Drain remaining for (int i = 0; i < 5; i++) { await PublishAsync().ConfigureAwait(false); } } if (firstResponders.Count > 0) { int highPrioCount = firstResponders.Count( id => id == subs[4].SubId); Assert.That(highPrioCount, Is.GreaterThanOrEqualTo(0), "Priority=200 subscription should be serviced first " + "(server may not implement strict ordering)."); } uint[] allIds = [.. subs.Select(s => s.SubId)]; await DeleteSubsAsync(allIds).ConfigureAwait(false); } [Test] public async Task DeleteFiveValidSubscriptionsAsync() { const int count = 5; uint[] ids = new uint[count]; for (int i = 0; i < count; i++) { CreateSubscriptionResponse cr = await CreateSubAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(cr.ResponseHeader.ServiceResult), Is.True); ids[i] = cr.SubscriptionId; } DeleteSubscriptionsResponse dr = await Session.DeleteSubscriptionsAsync( null, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(dr.ResponseHeader.ServiceResult), Is.True); Assert.That(dr.Results.Count, Is.EqualTo(count)); foreach (StatusCode sc in dr.Results) { Assert.That(StatusCode.IsGood(sc), Is.True); } } [Test] public async Task MultiSessionMultiSubPublishCallbacksAsync() { const int sessionCount = 2; const int subsPerSession = 2; var sessions = new List(); var subIdsBySession = new List>(); try { for (int s = 0; s < sessionCount; s++) { ISession sess = await ClientFixture.ConnectAsync( ServerUrl, SecurityPolicies.None).ConfigureAwait(false); sessions.Add(sess); var sessionSubs = new List(); for (int j = 0; j < subsPerSession; j++) { CreateSubscriptionResponse cr = await sess.CreateSubscriptionAsync( null, DefaultInterval, DefaultLifetime, DefaultKeepAlive, 0, true, 0, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(cr.ResponseHeader.ServiceResult), Is.True); NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32); var item = new MonitoredItemCreateRequest { ItemToMonitor = new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }, MonitoringMode = MonitoringMode.Reporting, RequestedParameters = new MonitoringParameters { ClientHandle = (uint)((s * subsPerSession) + j + 1), SamplingInterval = 250, Filter = default, DiscardOldest = true, QueueSize = 10 } }; await sess.CreateMonitoredItemsAsync( null, cr.SubscriptionId, TimestampsToReturn.Both, new MonitoredItemCreateRequest[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); sessionSubs.Add(cr.SubscriptionId); } subIdsBySession.Add(sessionSubs); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int totalDc = 0; for (int s = 0; s < sessionCount; s++) { for (int j = 0; j < subsPerSession; j++) { PublishResponse pub = await sessions[s].PublishAsync( null, default, CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { totalDc++; } } } Assert.That(totalDc, Is.GreaterThan(0), "At least one subscription across sessions should receive data."); } finally { for (int s = 0; s < sessions.Count; s++) { try { if (subIdsBySession.Count > s) { await sessions[s].DeleteSubscriptionsAsync( null, subIdsBySession[s].ToArray().ToArrayOf(), CancellationToken.None).ConfigureAwait(false); } await sessions[s].CloseAsync(5000, true).ConfigureAwait(false); } catch { } sessions[s].Dispose(); } } } [Test] public async Task FiveSubsEnableAfterDisabledReceiveDataAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(5, enabled: true).ConfigureAwait(false); uint[] ids = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 10; i++) { await PublishAsync().ConfigureAwait(false); } // Disable all, then re-enable await Session.SetPublishingModeAsync( null, false, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, true, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); int dcCount = 0; for (int i = 0; i < 10; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub)) { dcCount++; } } Assert.That(dcCount, Is.GreaterThan(0), "All subscriptions should receive data after re-enabling."); await DeleteSubsAsync(ids).ConfigureAwait(false); } [Test] public async Task FiveSubsDisableSubsetVerifyOthersContinueAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(5).ConfigureAwait(false); uint[] allIds = [.. subs.Select(s => s.SubId)]; await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 10; i++) { await PublishAsync().ConfigureAwait(false); } // Disable subs at index 1 and 3 uint[] disableIds = [subs[1].SubId, subs[3].SubId]; SetPublishingModeResponse sr = await Session.SetPublishingModeAsync( null, false, disableIds.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(sr.ResponseHeader.ServiceResult), Is.True); foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); var enabledIds = new HashSet { subs[0].SubId, subs[2].SubId, subs[4].SubId }; bool enabledGotData = false; for (int i = 0; i < 10; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); if (HasDataChangeNotification(pub) && enabledIds.Contains(pub.SubscriptionId)) { enabledGotData = true; } } if (!enabledGotData) { Assert.Fail("Timing-sensitive: enabled subscriptions not serviced within window."); } await DeleteSubsAsync(allIds).ConfigureAwait(false); } [Test] public async Task ThreeSubsPriorities1And125And255OrderingAsync() { List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(3, [1, 125, 255]).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < 6; i++) { await PublishAsync().ConfigureAwait(false); } var firstResponders = new List(); for (int round = 0; round < 5; round++) { foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 200).ConfigureAwait(false); PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { firstResponders.Add(pub.SubscriptionId); } // Drain for (int i = 0; i < 3; i++) { await PublishAsync().ConfigureAwait(false); } } if (firstResponders.Count > 0) { int hiPrio = firstResponders.Count(id => id == subs[2].SubId); Assert.That(hiPrio, Is.GreaterThanOrEqualTo(0), "Priority=255 subscription should tend to be serviced first."); } foreach ((uint subId, _) in subs) { await DeleteSubAsync(subId).ConfigureAwait(false); } } [Test] public async Task SamePrioritySubsEachServicedOncePerLoopAsync() { const int subCount = 5; List<(uint SubId, NodeId NodeId)> subs = await CreateSubsWithItemsAsync(subCount, "22222"u8.ToArray()).ConfigureAwait(false); await Task.Delay((int)DefaultInterval + 500).ConfigureAwait(false); for (int i = 0; i < subCount * 2; i++) { await PublishAsync().ConfigureAwait(false); } // Write and publish: each loop iteration should service each sub once var allServiced = new HashSet(); for (int round = 0; round < 3; round++) { foreach ((_, NodeId nodeId) in subs) { await WriteInt32ValueAsync(nodeId, s_random.Next()).ConfigureAwait(false); } await Task.Delay((int)DefaultInterval + 200).ConfigureAwait(false); var roundServiced = new HashSet(); for (int i = 0; i < subCount; i++) { PublishResponse pub = await PublishAsync().ConfigureAwait(false); Assert.That(StatusCode.IsGood(pub.ResponseHeader.ServiceResult), Is.True); if (HasDataChangeNotification(pub)) { allServiced.Add(pub.SubscriptionId); // Each subscription should not be serviced twice in same round Assert.That(roundServiced, Does.Not.Contain(pub.SubscriptionId), "Subscription should not be serviced twice in same publish round."); roundServiced.Add(pub.SubscriptionId); } } } Assert.That(allServiced, Has.Count.GreaterThanOrEqualTo(2), "Multiple equal-priority subscriptions should be serviced."); uint[] allIds = [.. subs.Select(s => s.SubId)]; await DeleteSubsAsync(allIds).ConfigureAwait(false); } [Test] public async Task MaxNotificationsPerPublishAboveServerLimitIsRevisedAsync() { uint serverLimit = ServerMaxNotificationsPerPublish; Assert.That(serverLimit, Is.GreaterThan(0u), "The test requires a server that limits the notifications per publish."); Assert.That( await CreateSubscriptionAndReadMaxNotificationsPerPublishAsync( serverLimit + 100_000).ConfigureAwait(false), Is.EqualTo(serverLimit), "A requested value above the server limit must be revised down."); } [Test] public async Task MaxNotificationsPerPublishZeroUsesServerLimitAsync() { Assert.That( await CreateSubscriptionAndReadMaxNotificationsPerPublishAsync(0) .ConfigureAwait(false), Is.EqualTo(ServerMaxNotificationsPerPublish), "A requested value of zero means the server picks its own limit."); } [Test] public async Task MaxNotificationsPerPublishBelowServerLimitIsKeptAsync() { uint requested = Math.Max(1, ServerMaxNotificationsPerPublish / 2); Assert.That( await CreateSubscriptionAndReadMaxNotificationsPerPublishAsync(requested) .ConfigureAwait(false), Is.EqualTo(requested), "A requested value within the server limit must be kept."); } [Test] public async Task ModifySubscriptionRevisesMaxNotificationsPerPublishAsync() { uint serverLimit = ServerMaxNotificationsPerPublish; Assert.That( await CreateSubscriptionAndReadMaxNotificationsPerPublishAsync( 1, serverLimit + 100_000).ConfigureAwait(false), Is.EqualTo(serverLimit), "ModifySubscription must apply the same limit as CreateSubscription."); } private uint ServerMaxNotificationsPerPublish => (uint)ServerFixture.Config.ServerConfiguration!.MaxNotificationsPerPublish; /// /// Creates a subscription with the requested MaxNotificationsPerPublish, optionally /// modifies it, and returns the value the server actually applied. The /// CreateSubscription response carries no revised value for this parameter, so the /// subscription diagnostics are the only observable place. They require a /// privileged session, hence the sysadmin connection. /// private async Task CreateSubscriptionAndReadMaxNotificationsPerPublishAsync( uint requestedMaxNotificationsPerPublish, uint? modifiedMaxNotificationsPerPublish = null) { ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false); if (admin == null) { Assert.Ignore("The server does not expose username authentication."); } try { CreateSubscriptionResponse resp = await admin.CreateSubscriptionAsync( null, DefaultInterval, DefaultLifetime, DefaultKeepAlive, requestedMaxNotificationsPerPublish, true, 0, CancellationToken.None).ConfigureAwait(false); Assert.That( StatusCode.IsGood(resp.ResponseHeader.ServiceResult), Is.True); if (modifiedMaxNotificationsPerPublish.HasValue) { ModifySubscriptionResponse mod = await admin.ModifySubscriptionAsync( null, resp.SubscriptionId, DefaultInterval, DefaultLifetime, DefaultKeepAlive, modifiedMaxNotificationsPerPublish.Value, 0, CancellationToken.None).ConfigureAwait(false); Assert.That( StatusCode.IsGood(mod.ResponseHeader.ServiceResult), Is.True); } ReadResponse read = await admin.ReadAsync( null, 0, TimestampsToReturn.Neither, new ReadValueId[] { new() { NodeId = VariableIds .Server_ServerDiagnostics_SubscriptionDiagnosticsArray, AttributeId = Attributes.Value } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); if (StatusCode.IsBad(read.Results[0].StatusCode)) { Assert.Ignore( "SubscriptionDiagnosticsArray not accessible: " + $"{read.Results[0].StatusCode}"); } Assert.That( read.Results[0].WrappedValue.TryGetValue( out ArrayOf diagnostics), Is.True); foreach (ExtensionObject entry in diagnostics) { if (entry.TryGetValue( out SubscriptionDiagnosticsDataType? diagnostic, admin.MessageContext) && diagnostic.SubscriptionId == resp.SubscriptionId) { return diagnostic.MaxNotificationsPerPublish; } } Assert.Fail( $"No diagnostics found for subscription {resp.SubscriptionId}."); return 0; } finally { if (admin != null) { await admin.CloseAsync(5000, true).ConfigureAwait(false); admin.Dispose(); } } } private async Task CreateSubAsync( double interval = 500, uint lifetime = DefaultLifetime, uint keepAlive = DefaultKeepAlive, uint maxNotif = 0, bool enabled = true, byte priority = 0) { return await Session.CreateSubscriptionAsync( null, interval, lifetime, keepAlive, maxNotif, enabled, priority, CancellationToken.None).ConfigureAwait(false); } private async Task DeleteSubAsync(uint id) { await Session.DeleteSubscriptionsAsync( null, new uint[] { id }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); } private async Task DeleteSubsAsync(uint[] ids) { await Session.DeleteSubscriptionsAsync( null, ids.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); } private async Task AddMonitoredItemAsync( uint subId, NodeId nodeId, uint handle = 1, double sampling = 250, uint queueSize = 10) { var item = new MonitoredItemCreateRequest { ItemToMonitor = new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }, MonitoringMode = MonitoringMode.Reporting, RequestedParameters = new MonitoringParameters { ClientHandle = handle, SamplingInterval = sampling, Filter = default, DiscardOldest = true, QueueSize = queueSize } }; CreateMonitoredItemsResponse resp = await Session.CreateMonitoredItemsAsync( null, subId, TimestampsToReturn.Both, new MonitoredItemCreateRequest[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(resp.Results[0].StatusCode), Is.True); return resp.Results[0].MonitoredItemId; } private async Task WriteInt32ValueAsync(NodeId nodeId, int value) { WriteResponse writeResp = await Session.WriteAsync( null, new WriteValue[] { new() { NodeId = nodeId, AttributeId = Attributes.Value, Value = new DataValue(Variant.From(value)) } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(StatusCode.IsGood(writeResp.Results[0]), Is.True); } private async Task PublishAsync() { return await Session.PublishWithTimeoutAsync().ConfigureAwait(false); } private static bool HasDataChangeNotification(PublishResponse pub) { if (pub.NotificationMessage?.NotificationData == null || pub.NotificationMessage.NotificationData.Count == 0) { return false; } foreach (ExtensionObject ext in pub.NotificationMessage.NotificationData) { var dcn = ExtensionObject.ToEncodeable(ext) as DataChangeNotification; if (dcn != null && dcn.MonitoredItems != default! && dcn.MonitoredItems.Count > 0) { return true; } } return false; } /// /// Creates N subscriptions with monitored items and returns (subId, nodeId) pairs. /// private async Task> CreateSubsWithItemsAsync( int count, byte[]? priorities = null, bool enabled = true) { var result = new List<(uint, NodeId)>(); NodeId int32Node = ToNodeId(Constants.ScalarStaticInt32); NodeId[] nodes = [.. Enumerable.Repeat(int32Node, count)]; for (int i = 0; i < count; i++) { byte prio = priorities != null && i < priorities.Length ? priorities[i] : (byte)0; CreateSubscriptionResponse cr = await CreateSubAsync( interval: DefaultInterval, enabled: enabled, priority: prio).ConfigureAwait(false); Assert.That(StatusCode.IsGood(cr.ResponseHeader.ServiceResult), Is.True); NodeId nodeId = nodes[i % nodes.Length]; await AddMonitoredItemAsync(cr.SubscriptionId, nodeId, handle: (uint)(i + 1)).ConfigureAwait(false); result.Add((cr.SubscriptionId, nodeId)); } return result; } private static readonly UnsecureRandom s_random = UnsecureRandom.Shared; private const double DefaultInterval = 1000; private const uint DefaultLifetime = 100; private const uint DefaultKeepAlive = 10; } }