64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
267 lines
11 KiB
C#
267 lines
11 KiB
C#
using Opc.Ua;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
|
|
using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
|
|
using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
|
|
|
/// <summary>
|
|
/// Regression tests for SubscribeCommand summary bucketing, --duration, --quiet, --summary-file,
|
|
/// and recursive collection. Covers Client.CLI-002, -009, and -010.
|
|
/// </summary>
|
|
public class SubscribeCommandSummaryTests
|
|
{
|
|
/// <summary>Verifies that nodes with no updates are counted separately from suspects.</summary>
|
|
[Fact]
|
|
public async Task Summary_NodeWithNoUpdate_IsCountedAsNeverNotAsNeverWentBad()
|
|
{
|
|
// Client.CLI-002: A node that received no updates at all is "never received an update",
|
|
// NOT a "suspect that never went bad".
|
|
var fakeService = new FakeOpcUaClientService();
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=SilentNode",
|
|
DurationSeconds = 1
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
await command.ExecuteAsync(console);
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
output.ShouldContain("No update received at all: 1");
|
|
output.ShouldContain("NEVER went bad (suspect): 0");
|
|
// The "suspect" detail header should not appear when the suspect list is empty.
|
|
output.ShouldNotContain("--- Nodes that NEVER received a bad-quality update (suspect) ---");
|
|
// The "never received update" detail header should appear.
|
|
output.ShouldContain("--- Nodes that never received an update at all ---");
|
|
}
|
|
|
|
/// <summary>Verifies that nodes with only good values are counted as never went bad.</summary>
|
|
[Fact]
|
|
public async Task Summary_NodeReceivedOnlyGoodValues_IsCountedAsNeverWentBad()
|
|
{
|
|
var fakeService = new FakeOpcUaClientService();
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=GoodNode",
|
|
// Use --duration so the command auto-exits.
|
|
DurationSeconds = 2
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
var runTask = Task.Run(async () => await command.ExecuteAsync(console));
|
|
|
|
// Wait until the subscription is registered by the fake.
|
|
await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
|
|
|
|
fakeService.RaiseDataChanged(
|
|
"ns=2;s=GoodNode",
|
|
new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow));
|
|
|
|
await runTask;
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
output.ShouldContain("NEVER went bad (suspect): 1");
|
|
output.ShouldContain("Last status GOOD: 1");
|
|
output.ShouldContain("No update received at all: 0");
|
|
}
|
|
|
|
/// <summary>Verifies that nodes with bad values are counted as ever went bad.</summary>
|
|
[Fact]
|
|
public async Task Summary_NodeReceivedBadValue_IsCountedAsEverWentBad()
|
|
{
|
|
var fakeService = new FakeOpcUaClientService();
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=BadNode",
|
|
DurationSeconds = 2
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
var runTask = Task.Run(async () => await command.ExecuteAsync(console));
|
|
|
|
await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
|
|
|
|
fakeService.RaiseDataChanged(
|
|
"ns=2;s=BadNode",
|
|
new DataValue(Variant.Null, StatusCodes.BadDeviceFailure, DateTime.UtcNow, DateTime.UtcNow));
|
|
|
|
await runTask;
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
output.ShouldContain("Ever went BAD during window: 1");
|
|
output.ShouldContain("NEVER went bad (suspect): 0");
|
|
}
|
|
|
|
/// <summary>Verifies that subscription auto-exits when duration expires.</summary>
|
|
[Fact]
|
|
public async Task Duration_ZeroOrPositive_AutoExits()
|
|
{
|
|
// --duration > 0 should make the command exit on its own without needing cancellation.
|
|
var fakeService = new FakeOpcUaClientService();
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=AutoExit",
|
|
DurationSeconds = 1
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
await command.ExecuteAsync(console);
|
|
sw.Stop();
|
|
|
|
sw.Elapsed.ShouldBeGreaterThanOrEqualTo(TimeSpan.FromMilliseconds(900));
|
|
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10));
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
output.ShouldContain("==================== SUMMARY ====================");
|
|
}
|
|
|
|
/// <summary>Verifies that --quiet suppresses updates but prints summary.</summary>
|
|
[Fact]
|
|
public async Task Quiet_SuppressesPerUpdateOutputButPrintsSummary()
|
|
{
|
|
var fakeService = new FakeOpcUaClientService();
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=Quiet",
|
|
Quiet = true,
|
|
DurationSeconds = 2
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
var runTask = Task.Run(async () => await command.ExecuteAsync(console));
|
|
|
|
await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
|
|
fakeService.RaiseDataChanged(
|
|
"ns=2;s=Quiet",
|
|
new DataValue(new Variant(1.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow));
|
|
|
|
await runTask;
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
// No per-update "value = ..." line should appear because --quiet was set.
|
|
output.ShouldNotContain(" = 1 (");
|
|
// Summary section is still printed.
|
|
output.ShouldContain("==================== SUMMARY ====================");
|
|
}
|
|
|
|
/// <summary>Verifies that summary is written to disk when summary file is specified.</summary>
|
|
[Fact]
|
|
public async Task SummaryFile_WritesSummaryToDisk()
|
|
{
|
|
var fakeService = new FakeOpcUaClientService();
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var tempFile = Path.Combine(Path.GetTempPath(), $"otopcua-cli-summary-{Guid.NewGuid():N}.txt");
|
|
try
|
|
{
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=SummaryFileNode",
|
|
DurationSeconds = 1,
|
|
SummaryFile = tempFile
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
await command.ExecuteAsync(console);
|
|
|
|
File.Exists(tempFile).ShouldBeTrue();
|
|
var contents = await File.ReadAllTextAsync(tempFile);
|
|
contents.ShouldContain("SUMMARY");
|
|
contents.ShouldContain("Total subscribed: 1");
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile)) File.Delete(tempFile);
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that recursive flag browses subtree and subscribes every variable.</summary>
|
|
[Fact]
|
|
public async Task Recursive_BrowsesSubtreeAndSubscribesEveryVariable()
|
|
{
|
|
// Root has a Folder child and a top-level Variable; the Folder contains a second Variable.
|
|
// Each node id is distinct so the ToDictionary(t => t.nodeId.ToString()) collapse
|
|
// doesn't trip a duplicate-key error.
|
|
var fakeService = new FakeOpcUaClientService();
|
|
fakeService.BrowseResultsByParent["ns=2;s=Root"] = new List<BrowseResult>
|
|
{
|
|
new("ns=2;s=Folder", "Folder", "Object", true),
|
|
new("ns=2;s=Tag1", "Tag1", "Variable", false)
|
|
};
|
|
fakeService.BrowseResultsByParent["ns=2;s=Folder"] = new List<BrowseResult>
|
|
{
|
|
new("ns=2;s=Tag2", "Tag2", "Variable", false)
|
|
};
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=Root",
|
|
Recursive = true,
|
|
MaxDepth = 3,
|
|
DurationSeconds = 1
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
await command.ExecuteAsync(console);
|
|
|
|
// Both Variables (depth 1 + depth 2) should have been subscribed.
|
|
fakeService.SubscribeCalls.Count.ShouldBeGreaterThanOrEqualTo(2);
|
|
fakeService.SubscribeCalls.ShouldContain(c => c.NodeId.Identifier.ToString() == "Tag1");
|
|
fakeService.SubscribeCalls.ShouldContain(c => c.NodeId.Identifier.ToString() == "Tag2");
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
output.ShouldContain("Browsing subtree of ns=2;s=Root");
|
|
}
|
|
|
|
/// <summary>Verifies that subscription failures are handled gracefully.</summary>
|
|
[Fact]
|
|
public async Task SubscribeFailure_PrintsFailedMessage_DoesNotCrash()
|
|
{
|
|
var fakeService = new FakeOpcUaClientService
|
|
{
|
|
SubscribeException = new InvalidOperationException("forced failure")
|
|
};
|
|
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
|
var command = new SubscribeCommand(factory)
|
|
{
|
|
Url = "opc.tcp://localhost:4840",
|
|
NodeId = "ns=2;s=FailNode",
|
|
DurationSeconds = 1
|
|
};
|
|
|
|
using var console = TestConsoleHelper.CreateConsole();
|
|
await command.ExecuteAsync(console);
|
|
|
|
var output = TestConsoleHelper.GetOutput(console);
|
|
output.ShouldContain("FAILED to subscribe");
|
|
output.ShouldContain("forced failure");
|
|
// The summary block is still printed.
|
|
output.ShouldContain("==================== SUMMARY ====================");
|
|
}
|
|
|
|
private static async Task WaitForAsync(Func<bool> predicate, int timeoutMs = 5000)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
|
|
while (!predicate() && DateTime.UtcNow < deadline)
|
|
await Task.Delay(25);
|
|
if (!predicate())
|
|
throw new TimeoutException("Condition not met within timeout.");
|
|
}
|
|
}
|