mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
334 lines
15 KiB
C#
334 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
[TestFixture]
|
|
[Category("Subscription")]
|
|
[Parallelizable]
|
|
public class SubscriptionTests
|
|
{
|
|
private Mock<IServerInternal> m_serverMock;
|
|
private Mock<ISession> m_sessionMock;
|
|
private Mock<IDiagnosticsNodeManager> m_diagnosticsNodeManagerMock;
|
|
private Mock<IMonitoredItemQueueFactory> m_queueFactoryMock;
|
|
private ITelemetryContext m_telemetry;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
m_telemetry = NUnitTelemetryContext.Create();
|
|
m_serverMock = new Mock<IServerInternal>();
|
|
m_sessionMock = new Mock<ISession>();
|
|
m_diagnosticsNodeManagerMock = new Mock<IDiagnosticsNodeManager>();
|
|
m_queueFactoryMock = new Mock<IMonitoredItemQueueFactory>();
|
|
|
|
m_serverMock.Setup(s => s.Telemetry).Returns(m_telemetry);
|
|
m_serverMock.Setup(s => s.DiagnosticsNodeManager).Returns(m_diagnosticsNodeManagerMock.Object);
|
|
m_serverMock.Setup(s => s.MonitoredItemQueueFactory).Returns(m_queueFactoryMock.Object);
|
|
|
|
var namespaceUris = new NamespaceTable();
|
|
m_serverMock.Setup(s => s.NamespaceUris).Returns(namespaceUris);
|
|
m_serverMock.Setup(s => s.ServerUris).Returns(new StringTable());
|
|
m_serverMock.Setup(s => s.TypeTree).Returns(new TypeTable(namespaceUris));
|
|
m_serverMock.Setup(s => s.Factory).Returns(new Mock<IEncodeableFactory>().Object);
|
|
|
|
// ServerSystemContext requires invoked server mock to have properties setup
|
|
m_serverMock.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(m_serverMock.Object));
|
|
|
|
m_sessionMock.Setup(s => s.Id).Returns(new NodeId(Guid.NewGuid()));
|
|
|
|
m_diagnosticsNodeManagerMock
|
|
.Setup(d => d.CreateSubscriptionDiagnosticsAsync(
|
|
It.IsAny<ServerSystemContext>(),
|
|
It.IsAny<SubscriptionDiagnosticsDataType>(),
|
|
It.IsAny<NodeValueSimpleEventHandler>()))
|
|
.ReturnsAsync(new NodeId(1));
|
|
}
|
|
|
|
private Subscription CreateSubscription(double publishingInterval = 1000)
|
|
{
|
|
return new Subscription(
|
|
m_serverMock.Object,
|
|
m_sessionMock.Object,
|
|
subscriptionId: 1,
|
|
publishingInterval: publishingInterval,
|
|
maxLifetimeCount: 10,
|
|
maxKeepAliveCount: 5,
|
|
maxNotificationsPerPublish: 0,
|
|
priority: 0,
|
|
publishingEnabled: true,
|
|
maxMessageCount: 10);
|
|
}
|
|
|
|
private static void SetExpiryTime(Subscription subscription, long expiryTime)
|
|
{
|
|
FieldInfo field = typeof(Subscription).GetField("m_publishTimerExpiry", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_publishTimerExpiry not found");
|
|
field.SetValue(subscription, expiryTime);
|
|
}
|
|
|
|
private static void ResetKeepAlive(Subscription subscription)
|
|
{
|
|
FieldInfo field = typeof(Subscription).GetField("m_keepAliveCounter", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_keepAliveCounter not found");
|
|
field.SetValue(subscription, (uint)0);
|
|
}
|
|
|
|
private static void AddMonitoredItem(Subscription subscription, IMonitoredItem item)
|
|
{
|
|
// Subscription has:
|
|
// private readonly Dictionary<uint, LinkedListNode<IMonitoredItem>> m_monitoredItems;
|
|
// private readonly LinkedList<IMonitoredItem> m_itemsToCheck;
|
|
|
|
FieldInfo monitoredItemsField = typeof(Subscription).GetField("m_monitoredItems", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_monitoredItems not found");
|
|
FieldInfo itemsToCheckField = typeof(Subscription).GetField("m_itemsToCheck", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_itemsToCheck not found");
|
|
|
|
var monitoredItems = (System.Collections.IDictionary)monitoredItemsField.GetValue(subscription);
|
|
var itemsToCheck = (LinkedList<IMonitoredItem>)itemsToCheckField.GetValue(subscription);
|
|
|
|
// Add to itemsToCheck first to get the node
|
|
LinkedListNode<IMonitoredItem> node = itemsToCheck.AddLast(item);
|
|
// Add to dictionary
|
|
monitoredItems.Add(item.Id, node);
|
|
}
|
|
|
|
private static void AddTriggerLink(Subscription subscription, uint triggeringId, ITriggeredMonitoredItem triggeredItem)
|
|
{
|
|
// private readonly Dictionary<uint, List<ITriggeredMonitoredItem>> m_itemsToTrigger;
|
|
FieldInfo itemsToTriggerField = typeof(Subscription).GetField("m_itemsToTrigger", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_itemsToTrigger not found");
|
|
var itemsToTrigger = (System.Collections.IDictionary)itemsToTriggerField.GetValue(subscription);
|
|
|
|
if (!itemsToTrigger.Contains(triggeringId))
|
|
{
|
|
itemsToTrigger.Add(triggeringId, new List<ITriggeredMonitoredItem>());
|
|
}
|
|
var list = (List<ITriggeredMonitoredItem>)itemsToTrigger[triggeringId];
|
|
list.Add(triggeredItem);
|
|
}
|
|
|
|
private static int GetItemsToPublishCount(Subscription subscription)
|
|
{
|
|
FieldInfo itemsToPublishField = typeof(Subscription).GetField("m_itemsToPublish", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_itemsToPublish not found");
|
|
var itemsToPublish = (LinkedList<IMonitoredItem>)itemsToPublishField.GetValue(subscription);
|
|
return itemsToPublish.Count;
|
|
}
|
|
|
|
[Test]
|
|
public void PublishTimerExpired_NotExpired_ReturnsIdle()
|
|
{
|
|
using Subscription subscription = CreateSubscription(1000);
|
|
ResetKeepAlive(subscription);
|
|
|
|
// Set expiry in far future
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() + 100000);
|
|
|
|
PublishingState state = subscription.PublishTimerExpired();
|
|
|
|
Assert.That(state, Is.EqualTo(PublishingState.Idle));
|
|
}
|
|
|
|
[Test]
|
|
public void PublishTimerExpired_Expired_ReturnsNotificationsAvailable_ForKeepAlive()
|
|
{
|
|
using Subscription subscription = CreateSubscription(1000);
|
|
// Don't reset keepalive, it should be maxKeepAliveCount initially.
|
|
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() - 100);
|
|
|
|
PublishingState state = subscription.PublishTimerExpired();
|
|
|
|
Assert.That(state, Is.EqualTo(PublishingState.NotificationsAvailable));
|
|
}
|
|
|
|
[Test]
|
|
public void PublishTimerExpired_Expired_ItemsReady_ReturnsNotificationsAvailable()
|
|
{
|
|
using Subscription subscription = CreateSubscription(1000);
|
|
ResetKeepAlive(subscription);
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() - 100);
|
|
|
|
// Mock Monitored Item
|
|
var itemMock = new Mock<IMonitoredItem>();
|
|
itemMock.Setup(i => i.Id).Returns(1);
|
|
itemMock.Setup(i => i.IsReadyToPublish).Returns(true);
|
|
|
|
AddMonitoredItem(subscription, itemMock.Object);
|
|
|
|
PublishingState state = subscription.PublishTimerExpired();
|
|
|
|
Assert.That(state, Is.EqualTo(PublishingState.NotificationsAvailable));
|
|
Assert.That(GetItemsToPublishCount(subscription), Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void PublishTimerExpired_Expired_ItemsNotReady_ReturnsIdle()
|
|
{
|
|
using Subscription subscription = CreateSubscription(1000);
|
|
ResetKeepAlive(subscription);
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() - 100);
|
|
|
|
// Mock Monitored Item
|
|
var itemMock = new Mock<IMonitoredItem>();
|
|
itemMock.Setup(i => i.Id).Returns(1);
|
|
itemMock.Setup(i => i.IsReadyToPublish).Returns(false);
|
|
|
|
AddMonitoredItem(subscription, itemMock.Object);
|
|
|
|
PublishingState state = subscription.PublishTimerExpired();
|
|
|
|
Assert.That(state, Is.EqualTo(PublishingState.Idle));
|
|
Assert.That(GetItemsToPublishCount(subscription), Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void PublishTimerExpired_Expired_IncrementsKeepAlive()
|
|
{
|
|
using Subscription subscription = CreateSubscription(1000);
|
|
ResetKeepAlive(subscription);
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() - 100);
|
|
|
|
Assert.That(subscription.Diagnostics.CurrentKeepAliveCount, Is.Zero);
|
|
|
|
subscription.PublishTimerExpired();
|
|
|
|
Assert.That(subscription.Diagnostics.CurrentKeepAliveCount, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void PublishTimerExpired_Triggering_CorrectlyTriggersAndPublishes()
|
|
{
|
|
using Subscription subscription = CreateSubscription(1000);
|
|
ResetKeepAlive(subscription);
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() - 100);
|
|
|
|
// Item A: Triggering item. Ready to publish, Ready to trigger.
|
|
var itemAMock = new Mock<IMonitoredItem>();
|
|
itemAMock.Setup(i => i.Id).Returns(1);
|
|
itemAMock.Setup(i => i.IsReadyToPublish).Returns(true);
|
|
itemAMock.SetupProperty(i => i.IsReadyToTrigger, true); // Use property behavior so it can be set to false by Subscription
|
|
|
|
// Item B: Triggered item. Initially NOT ready to publish.
|
|
// B must implement ITriggeredMonitoredItem as well.
|
|
var itemBMock = new Mock<IMonitoredItem>();
|
|
itemBMock.As<ITriggeredMonitoredItem>();
|
|
Mock<ITriggeredMonitoredItem> triggeredItemB = itemBMock.As<ITriggeredMonitoredItem>();
|
|
|
|
itemBMock.Setup(i => i.Id).Returns(2);
|
|
triggeredItemB.Setup(i => i.Id).Returns(2);
|
|
|
|
// "State" of ready
|
|
bool bIsReady = false;
|
|
itemBMock.Setup(i => i.IsReadyToPublish).Returns(() => bIsReady);
|
|
|
|
// SetTriggered updates state
|
|
triggeredItemB.Setup(i => i.SetTriggered()).Returns(() =>
|
|
{
|
|
bIsReady = true;
|
|
return true; // True indicates it has something to publish
|
|
});
|
|
|
|
// Add both items
|
|
AddMonitoredItem(subscription, itemAMock.Object);
|
|
AddMonitoredItem(subscription, itemBMock.Object);
|
|
|
|
// Add trigger link A -> B
|
|
AddTriggerLink(subscription, 1, triggeredItemB.Object);
|
|
|
|
PublishingState state = subscription.PublishTimerExpired();
|
|
|
|
Assert.That(state, Is.EqualTo(PublishingState.NotificationsAvailable));
|
|
|
|
// Both items should be in publish queue
|
|
Assert.That(GetItemsToPublishCount(subscription), Is.EqualTo(2), "Both items should be ready to publish");
|
|
|
|
// Verify trigger was called
|
|
triggeredItemB.Verify(i => i.SetTriggered(), Times.Once);
|
|
|
|
// Verify IsReadyToTrigger on A was reset to false
|
|
Assert.That(itemAMock.Object.IsReadyToTrigger, Is.False, "IsReadyToTrigger should be reset");
|
|
}
|
|
|
|
private static void AddMonitoredItemToPublish(Subscription subscription, IMonitoredItem item)
|
|
{
|
|
FieldInfo itemsToPublishField = typeof(Subscription).GetField("m_itemsToPublish", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException("Field m_itemsToPublish not found");
|
|
var itemsToPublish = (LinkedList<IMonitoredItem>)itemsToPublishField.GetValue(subscription);
|
|
itemsToPublish.AddLast(item);
|
|
}
|
|
|
|
[Test]
|
|
public void Publish_MultipleTimes_WithMaxMessageCount()
|
|
{
|
|
using var subscription = new Subscription(m_serverMock.Object, m_sessionMock.Object, 1, 100, 1000, 10, 1, 0, true, 2);
|
|
var itemMock = new Mock<IDataChangeMonitoredItem2>();
|
|
|
|
var values = new List<MonitoredItemNotification>
|
|
{
|
|
new() { Value = new DataValue(1) },
|
|
new() { Value = new DataValue(2) },
|
|
new() { Value = new DataValue(3) }
|
|
};
|
|
|
|
int counter = 0;
|
|
itemMock.Setup(i => i.Publish(
|
|
It.IsAny<OperationContext>(),
|
|
It.IsAny<Queue<MonitoredItemNotification>>(),
|
|
It.IsAny<Queue<DiagnosticInfo>>(),
|
|
It.IsAny<uint>(),
|
|
It.IsAny<Microsoft.Extensions.Logging.ILogger>()))
|
|
.Returns<OperationContext, Queue<MonitoredItemNotification>, Queue<DiagnosticInfo>, uint, Microsoft.Extensions.Logging.ILogger>(
|
|
(ctx, nq, dq, max, logger) =>
|
|
{
|
|
if (counter < values.Count)
|
|
{
|
|
nq.Enqueue(values[counter++]);
|
|
dq.Enqueue(new DiagnosticInfo());
|
|
itemMock.SetupGet(x => x.IsReadyToPublish).Returns(counter < values.Count);
|
|
return counter < values.Count;
|
|
}
|
|
return false;
|
|
});
|
|
itemMock.SetupGet(i => i.Id).Returns(1);
|
|
itemMock.SetupGet(i => i.IsReadyToPublish).Returns(true);
|
|
itemMock.SetupGet(i => i.AttributeId).Returns(Attributes.Value);
|
|
itemMock.SetupGet(i => i.MonitoredItemType).Returns(MonitoredItemTypeMask.DataChange);
|
|
|
|
AddMonitoredItem(subscription, itemMock.Object);
|
|
SetExpiryTime(subscription, TimeProvider.System.GetTimestampMilliseconds() - 100);
|
|
PublishingState state = subscription.PublishTimerExpired();
|
|
|
|
AddMonitoredItemToPublish(subscription, itemMock.Object);
|
|
|
|
var messages = new List<NotificationMessage>();
|
|
|
|
// First publish
|
|
var ctx1 = new OperationContext(m_sessionMock.Object, new DiagnosticsMasks());
|
|
NotificationMessage message = subscription.Publish(ctx1, out ArrayOf<uint> availableSequenceNumbers, out bool moreNotifications1);
|
|
messages.Add(message);
|
|
|
|
// Should be more because we generated multiple notifications and limit the max per publish to 1 for tests.
|
|
Assert.That(moreNotifications1, Is.True);
|
|
|
|
// Second publish
|
|
NotificationMessage message2 = subscription.Publish(ctx1, out availableSequenceNumbers, out bool moreNotifications2);
|
|
|
|
// third publish
|
|
NotificationMessage message3 = subscription.Publish(ctx1, out availableSequenceNumbers, out bool moreNotifications3);
|
|
|
|
Assert.That(message2, Is.Not.Null);
|
|
Assert.That(message3, Is.Not.Null);
|
|
Assert.That(moreNotifications2, Is.True);
|
|
Assert.That(moreNotifications3, Is.False);
|
|
}
|
|
}
|
|
}
|