Files
UA-.NETStandard/tests/Opc.Ua.Subscriptions.Durable.Tests/SubscriptionDurableClassicTests.cs

1184 lines
45 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
using ISession = Opc.Ua.Client.ISession;
namespace Opc.Ua.Subscriptions.Durable.Tests
{
/// <summary>
/// compliance tests for Subscription Durable covering
/// durable subscription lifecycle, SetPublishingMode on durable
/// subscriptions, parameter modification, and edge cases.
/// </summary>
[NonParallelizable]
[TestFixture]
[Category("Conformance")]
[Category("Subscription")]
[Category("SubscriptionDurable")]
public class SubscriptionDurableClassicTests : TestFixture
{
[Test]
public async Task DurableSubscriptionCreatedWithPublishingEnabledAsync()
{
CreateSubscriptionResponse resp = await CreateSubAsync(Session)
.ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(resp.ResponseHeader.ServiceResult),
Is.True);
Assert.That(resp.SubscriptionId, Is.GreaterThan(0u));
await Session.DeleteSubscriptionsAsync(
null, new uint[] { resp.SubscriptionId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
[Test]
public async Task DurableSubscriptionSurvivesSessionCloseAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
// Close session without deleting subscription
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
// Reconnect and transfer
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
TransferSubscriptionsResponse xfer =
await TransferOrIgnoreAsync(session2, subId, true)
.ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(xfer.Results[0].StatusCode),
Is.True,
"Subscription should survive session close.");
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubscriptionTransferAfterReconnectAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
// Consume initial data
await Task.Delay(300).ConfigureAwait(false);
await session1.PublishAsync(
null, default,
CancellationToken.None).ConfigureAwait(false);
// Close without deleting
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
await Task.Delay(200).ConfigureAwait(false);
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, true)
.ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session2.PublishAsync(
null,
default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
Assert.That(pub.SubscriptionId, Is.EqualTo(subId));
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubscriptionNotAvailableIgnoredAsync()
{
// Test the ignore path for servers that don't support
// transfer (used as durable proxy)
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
// This will Assert.Ignore if not supported
await TransferOrIgnoreAsync(session2, subId, false)
.ConfigureAwait(false);
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubscriptionDeleteBeforeReconnectAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, false)
.ConfigureAwait(false);
DeleteSubscriptionsResponse del =
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(del.Results[0]), Is.True);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubSetPublishingModeDisableAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
SetPublishingModeResponse spResp =
await session1.SetPublishingModeAsync(
null, false, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(spResp.Results[0]), Is.True);
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubSetPublishingModeReEnableAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await session1.SetPublishingModeAsync(
null, false, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
SetPublishingModeResponse spResp =
await session1.SetPublishingModeAsync(
null, true, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(spResp.Results[0]), Is.True);
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubPublishingModePreservedAfterTransferAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
// Disable publishing
await session1.SetPublishingModeAsync(
null, false, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, false)
.ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session2.PublishAsync(
null,
default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
// Publishing was disabled → expect KeepAlive
Assert.That(
pub.NotificationMessage.NotificationData.Count,
Is.Zero);
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubDisabledNoNotificationsAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
await session1.SetPublishingModeAsync(
null, false, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session1.PublishAsync(
null, default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
Assert.That(
pub.NotificationMessage.NotificationData.Count,
Is.Zero,
"Disabled durable sub should only send KeepAlive.");
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubReEnableAfterTransferAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
await session1.SetPublishingModeAsync(
null, false, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, false)
.ConfigureAwait(false);
// Re-enable
await session2.SetPublishingModeAsync(
null, true, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session2.PublishAsync(
null,
default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
Assert.That(
pub.NotificationMessage.NotificationData.Count,
Is.GreaterThan(0),
"Re-enabled sub should produce notifications.");
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubModifyIntervalAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(
session1, interval: 2000).ConfigureAwait(false);
uint subId = resp.SubscriptionId;
ModifySubscriptionResponse mod =
await session1.ModifySubscriptionAsync(
null, subId, 100, DefaultLifetime, DefaultKeepAlive,
0, 0, CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(mod.ResponseHeader.ServiceResult),
Is.True);
Assert.That(mod.RevisedPublishingInterval,
Is.LessThanOrEqualTo(2000));
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubModifyKeepAliveCountAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
ModifySubscriptionResponse mod =
await session1.ModifySubscriptionAsync(
null, subId, DefaultInterval, DefaultLifetime, 50,
0, 0, CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(mod.ResponseHeader.ServiceResult),
Is.True);
Assert.That(mod.RevisedMaxKeepAliveCount, Is.GreaterThan(0u));
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubModifyLifetimeCountAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
ModifySubscriptionResponse mod =
await session1.ModifySubscriptionAsync(
null, subId, DefaultInterval, 500, DefaultKeepAlive,
0, 0, CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(mod.ResponseHeader.ServiceResult),
Is.True);
Assert.That(mod.RevisedLifetimeCount, Is.GreaterThan(0u));
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubModifyPriorityAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
ModifySubscriptionResponse mod =
await session1.ModifySubscriptionAsync(
null, subId, DefaultInterval, DefaultLifetime,
DefaultKeepAlive, 0, 200,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(mod.ResponseHeader.ServiceResult),
Is.True);
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubModifyMaxNotificationsAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
ModifySubscriptionResponse mod =
await session1.ModifySubscriptionAsync(
null, subId, DefaultInterval, DefaultLifetime,
DefaultKeepAlive, 5, 0,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(mod.ResponseHeader.ServiceResult),
Is.True);
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubWithMultipleMonitoredItemsAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(session1)
.ConfigureAwait(false);
uint subId = resp.SubscriptionId;
for (uint h = 1; h <= 5; h++)
{
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
handle: h, sampling: 50).ConfigureAwait(false);
}
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session1.PublishAsync(
null, default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
Assert.That(
pub.NotificationMessage.NotificationData.Count,
Is.GreaterThan(0));
await session1.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubTransferWithInitialTrueAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(
session1, interval: 100).ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, true)
.ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session2.PublishAsync(
null,
default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
Assert.That(
pub.NotificationMessage.NotificationData.Count,
Is.GreaterThan(0),
"Transfer with initial=true should send data.");
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubTransferWithInitialFalseAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(
session1, interval: 100).ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
ToNodeId(Constants.ScalarStaticInt32))
.ConfigureAwait(false);
// Consume initial
await Task.Delay(300).ConfigureAwait(false);
await session1.PublishAsync(
null, default,
CancellationToken.None).ConfigureAwait(false);
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, false)
.ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub = await session2.PublishAsync(
null,
default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubCreateMultipleSubsAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
var subIds = new List<uint>();
for (int i = 0; i < 3; i++)
{
CreateSubscriptionResponse resp =
await CreateSubAsync(session1).ConfigureAwait(false);
subIds.Add(resp.SubscriptionId);
}
Assert.That(subIds, Has.Count.EqualTo(3));
await session1.DeleteSubscriptionsAsync(
null, subIds.ToArray().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
await CloseSessionAsync(session1).ConfigureAwait(false);
}
[Test]
public async Task DurableSubSeqNumbersPreservedAsync()
{
ISession session1 = await CreateSessionAsync()
.ConfigureAwait(false);
CreateSubscriptionResponse resp = await CreateSubAsync(
session1, interval: 100).ConfigureAwait(false);
uint subId = resp.SubscriptionId;
await AddItemAsync(session1, subId,
VariableIds.Server_ServerStatus_CurrentTime,
sampling: 50).ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub1 = await session1.PublishAsync(
null, default,
CancellationToken.None).ConfigureAwait(false);
uint seqBefore = pub1.NotificationMessage.SequenceNumber;
await session1.CloseAsync(5000, false).ConfigureAwait(false);
session1.Dispose();
ISession session2 = await CreateSessionAsync()
.ConfigureAwait(false);
try
{
await TransferOrIgnoreAsync(session2, subId, true)
.ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
PublishResponse pub2 = await session2.PublishAsync(
null,
default,
CancellationToken.None).ConfigureAwait(false);
Assert.That(
pub2.NotificationMessage.SequenceNumber,
Is.GreaterThan(seqBefore),
"Seq numbers should continue after transfer.");
await session2.DeleteSubscriptionsAsync(
null, new uint[] { subId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
finally
{
await CloseSessionAsync(session2).ConfigureAwait(false);
}
}
private async Task<CreateSubscriptionResponse> CreateSubAsync(
ISession session, double interval = DefaultInterval)
{
return await session.CreateSubscriptionAsync(
null, interval, DefaultLifetime, DefaultKeepAlive,
0, true, 0,
CancellationToken.None).ConfigureAwait(false);
}
private async Task<uint> AddItemAsync(
ISession session, uint subId, NodeId nodeId,
uint handle = 1, double sampling = 100)
{
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 = 10
}
};
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 Task<ISession> CreateSessionAsync()
{
return ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None);
}
private async Task CloseSessionAsync(ISession session)
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
private const double DefaultInterval = 200;
private const uint DefaultLifetime = 100;
private const uint DefaultKeepAlive = 10;
private async Task<TransferSubscriptionsResponse>
TransferOrIgnoreAsync(
ISession target, uint subId, bool sendInitial)
{
try
{
TransferSubscriptionsResponse resp =
await target.TransferSubscriptionsAsync(
null,
new uint[] { subId }.ToArrayOf(),
sendInitial,
CancellationToken.None).ConfigureAwait(false);
if (resp.Results.Count > 0 &&
StatusCode.IsBad(resp.Results[0].StatusCode))
{
Assert.Ignore(
"Durable/Transfer subscriptions failed: " +
resp.Results[0].StatusCode.ToString());
}
return resp;
}
catch (ServiceResultException sre)
when (sre.StatusCode == StatusCodes.BadServiceUnsupported ||
sre.StatusCode == StatusCodes.BadNotSupported ||
sre.StatusCode == StatusCodes.BadNotImplemented ||
sre.StatusCode == StatusCodes.BadNoSubscription ||
sre.StatusCode == StatusCodes.BadSessionClosed)
{
Assert.Ignore(
"Durable/Transfer subscriptions not supported: " +
sre.StatusCode.ToString());
return null;
}
}
[Test]
public async Task DurableSetLifetimeMaxUint32Async()
{
// Create durable subscription; set lifetimeInHours to UInt32.MaxValue
CreateSubscriptionResponse resp = await CreateSubAsync().ConfigureAwait(false);
uint subId = resp.SubscriptionId;
try
{
uint revisedLifetime = await SetSubscriptionDurableAsync(
subId, uint.MaxValue).ConfigureAwait(false);
// Server may revise the lifetime, but should return a valid value
Assert.That(revisedLifetime, Is.GreaterThan(0u),
"Revised lifetime should be greater than 0.");
if (revisedLifetime != uint.MaxValue)
{
Assert.Warn($"Server revised lifetimeInHours from UInt32.MaxValue to {revisedLifetime}.");
}
}
finally
{
await DeleteSubAsync(subId).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSetLifetimeZeroRevisedGreaterThanZeroAsync()
{
// Set lifetimeInHours to 0; revised should be > 0
CreateSubscriptionResponse resp = await CreateSubAsync().ConfigureAwait(false);
uint subId = resp.SubscriptionId;
try
{
uint revisedLifetime = await SetSubscriptionDurableAsync(
subId, 0).ConfigureAwait(false);
Assert.That(revisedLifetime, Is.GreaterThan(0u),
"Revised lifetime should be greater than requested (0).");
}
finally
{
await DeleteSubAsync(subId).ConfigureAwait(false);
}
}
[Test]
public async Task DurableSubscriptionWithServerNotifierEventsAsync()
{
// Manual test: durable subscription with Server.Notifier events.
// This test is marked as skipped in the JS.
Assert.Ignore("Manual test for durable subscriptions " +
"with Server.Notifier events; skipped per definition.");
await Task.CompletedTask.ConfigureAwait(false);
}
[Test]
public async Task DurableWithZeroMonitoredItemsThenRepeatCallWithDifferentParamsAsync()
{
// Create durable sub with 0 monitored items, then repeat call with different params.
// Second call while monitored item exists should return BadInvalidState.
CreateSubscriptionResponse resp = await CreateSubAsync().ConfigureAwait(false);
uint subId = resp.SubscriptionId;
try
{
// First call with 0 monitored items should succeed
uint revisedLifetime1 = await SetSubscriptionDurableAsync(
subId, 5).ConfigureAwait(false);
Assert.That(revisedLifetime1, Is.GreaterThan(0u));
// Add a monitored item
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
uint monItemId = await AddMonitoredItemAsync(subId, nodeId).ConfigureAwait(false);
// Second call while monitored item exists should return BadInvalidState
try
{
CallMethodResult result = await CallSetSubscriptionDurableAsync(
subId, 10).ConfigureAwait(false);
// Some servers may return BadInvalidState in the result status
if (result.StatusCode == StatusCodes.BadInvalidState)
{
// Expected
}
else if (StatusCode.IsGood(result.StatusCode))
{
// Server accepted the call - may not enforce the restriction
Assert.Warn("Server accepted SetSubscriptionDurable with active " +
"monitored items; BadInvalidState was expected.");
}
}
catch (ServiceResultException sre)
when (sre.StatusCode == StatusCodes.BadInvalidState)
{
// Expected
}
// Delete monitored item and retry - should succeed
await Session.DeleteMonitoredItemsAsync(
null, subId,
new uint[] { monItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
uint revisedLifetime3 = await SetSubscriptionDurableAsync(
subId, 10).ConfigureAwait(false);
Assert.That(revisedLifetime3, Is.GreaterThan(0u));
}
finally
{
await DeleteSubAsync(subId).ConfigureAwait(false);
}
}
[Test]
public async Task DurableShortLivedSubscriptionModifyResetsStateAsync()
{
// Short-lived sub, durable call changes lifetime;
// ModifySubscription should reset durable state.
CreateSubscriptionResponse resp = await CreateSubAsync(
interval: 500, lifetime: 3, keepAlive: 3).ConfigureAwait(false);
uint subId = resp.SubscriptionId;
try
{
// Set durable with lifetime of 1 hour
uint revisedLifetime = await SetSubscriptionDurableAsync(
subId, 1).ConfigureAwait(false);
Assert.That(revisedLifetime, Is.GreaterThan(0u));
// Modify subscription to reset durable state
ModifySubscriptionResponse modResp = await Session.ModifySubscriptionAsync(
null, subId, 500, 3, 3, 0, 0,
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modResp.ResponseHeader.ServiceResult), Is.True);
// After modify, the subscription should behave as non-durable
// (shorter lifetime). Wait for keep-alive or timeout.
await Task.Delay(1000).ConfigureAwait(false);
PublishResponse pub = await Session.PublishWithTimeoutAsync().ConfigureAwait(false);
Assert.That(
StatusCode.IsGood(pub.ResponseHeader.ServiceResult) ||
pub.ResponseHeader.ServiceResult == StatusCodes.BadTimeout ||
pub.ResponseHeader.ServiceResult == StatusCodes.BadNoSubscription,
Is.True,
"Expected Good, BadTimeout, or BadNoSubscription after modify.");
}
finally
{
try
{
await DeleteSubAsync(subId).ConfigureAwait(false);
}
catch (ServiceResultException)
{
// Subscription may have already expired
}
}
}
[Test]
public async Task DurableDeleteSubscriptionRemovesDurableStateAsync()
{
// Delete a durable subscription, verify it is fully removed.
CreateSubscriptionResponse resp = await CreateSubAsync().ConfigureAwait(false);
uint subId = resp.SubscriptionId;
// Make it durable
uint revisedLifetime = await SetSubscriptionDurableAsync(
subId, 24).ConfigureAwait(false);
Assert.That(revisedLifetime, Is.GreaterThan(0u));
// Delete it
await DeleteSubAsync(subId).ConfigureAwait(false);
// Attempting to publish should eventually return BadNoSubscription
try
{
PublishResponse pub = await Session.PublishWithTimeoutAsync().ConfigureAwait(false);
Assert.That(
pub.ResponseHeader.ServiceResult == StatusCodes.BadNoSubscription ||
StatusCode.IsGood(pub.ResponseHeader.ServiceResult),
Is.True);
}
catch (ServiceResultException sre)
when (sre.StatusCode == StatusCodes.BadNoSubscription)
{
// Expected
}
}
private async Task<CreateSubscriptionResponse> CreateSubAsync(
double interval = DefaultInterval,
uint lifetime = DefaultLifetime,
uint keepAlive = DefaultKeepAlive,
bool enabled = true)
{
return await Session.CreateSubscriptionAsync(
null, interval, lifetime, keepAlive, 0,
enabled, 0,
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<uint> AddMonitoredItemAsync(
uint subId, NodeId nodeId,
uint handle = 1, double sampling = 250)
{
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 = 10
}
};
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;
}
/// <summary>
/// Calls the SetSubscriptionDurable method on the server.
/// Returns the revised lifetime in hours, or Assert.Ignore if not supported.
/// </summary>
private async Task<uint> SetSubscriptionDurableAsync(
uint subscriptionId, uint lifetimeInHours)
{
try
{
CallMethodResult result = await CallSetSubscriptionDurableAsync(
subscriptionId, lifetimeInHours).ConfigureAwait(false);
if (result.StatusCode == StatusCodes.BadMethodInvalid ||
result.StatusCode == StatusCodes.BadNotSupported ||
result.StatusCode == StatusCodes.BadNodeIdUnknown)
{
Assert.Ignore(
"SetSubscriptionDurable not supported: " + result.StatusCode.ToString());
}
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
$"SetSubscriptionDurable failed: {result.StatusCode}");
Assert.That(result.OutputArguments, Is.Not.Null);
Assert.That(result.OutputArguments.Count, Is.EqualTo(1));
return (uint)result.OutputArguments[0];
}
catch (ServiceResultException sre)
when (sre.StatusCode == StatusCodes.BadServiceUnsupported ||
sre.StatusCode == StatusCodes.BadNotSupported ||
sre.StatusCode == StatusCodes.BadNotImplemented ||
sre.StatusCode == StatusCodes.BadMethodInvalid ||
sre.StatusCode == StatusCodes.BadNodeIdUnknown)
{
Assert.Ignore(
"SetSubscriptionDurable not supported: " + sre.StatusCode.ToString());
return 0; // unreachable
}
}
private async Task<CallMethodResult> CallSetSubscriptionDurableAsync(
uint subscriptionId, uint lifetimeInHours)
{
var request = new CallMethodRequest
{
ObjectId = ServerObjectId,
MethodId = SetSubscriptionDurableMethodId,
InputArguments = new Variant[]
{
new(subscriptionId),
new(lifetimeInHours)
}.ToArrayOf()
};
CallResponse callResp = await Session.CallAsync(
null,
new CallMethodRequest[] { request }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(callResp.Results, Is.Not.Null);
Assert.That(callResp.Results.Count, Is.EqualTo(1));
return callResp.Results[0];
}
/// <summary>
/// <summary>
/// SetSubscriptionDurable method NodeIds (Part 5, 12.1.13)
/// </summary>
private static readonly NodeId SetSubscriptionDurableMethodId =
MethodIds.Server_SetSubscriptionDurable;
private static readonly NodeId ServerObjectId = ObjectIds.Server;
}
}