docs: complete XML doc comments via fixdocs (2757 to 131 findings)
Add missing <returns>/<param>/<summary>/<typeparam> tags and clean up misused inheritdoc across 481 files so the documented API surface is complete. Documentation-only (zero code lines changed). The 131 remaining findings are inheritdoc-style warnings deliberately left to preserve hand-written implementation rationale (plan-decision notes, race-condition explanations).
This commit is contained in:
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class AlarmsCommandTests
|
||||
{
|
||||
/// <summary>Verifies that Execute subscribes to alarms.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_SubscribesToAlarms()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class AlarmsCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute with node passes source node ID.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_WithNode_PassesSourceNodeId()
|
||||
{
|
||||
@@ -58,6 +60,7 @@ public class AlarmsCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute with refresh requests condition refresh.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_WithRefresh_RequestsConditionRefresh()
|
||||
{
|
||||
@@ -83,6 +86,7 @@ public class AlarmsCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that refresh failure prints error.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_RefreshFailure_PrintsError()
|
||||
{
|
||||
@@ -110,6 +114,7 @@ public class AlarmsCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute unsubscribes on cancellation.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_UnsubscribesOnCancellation()
|
||||
{
|
||||
@@ -132,6 +137,7 @@ public class AlarmsCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute disconnects in finally block.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class BrowseCommandTests
|
||||
{
|
||||
/// <summary>Verifies that Execute prints browse results correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_PrintsBrowseResults()
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public class BrowseCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute browses from the specified node ID.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_BrowsesFromSpecifiedNode()
|
||||
{
|
||||
@@ -60,6 +62,7 @@ public class BrowseCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute browses from null node when not specified.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DefaultBrowsesFromNull()
|
||||
{
|
||||
@@ -81,6 +84,7 @@ public class BrowseCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute browses only a single level when not recursive.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_NonRecursive_BrowsesSingleLevel()
|
||||
{
|
||||
@@ -106,6 +110,7 @@ public class BrowseCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute browses child nodes when recursive flag is set.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_Recursive_BrowsesChildren()
|
||||
{
|
||||
@@ -129,6 +134,7 @@ public class BrowseCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute disconnects and disposes in the finally block.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class CommandBaseTests
|
||||
{
|
||||
/// <summary>Verifies that common options map to connection settings correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CommonOptions_MapToConnectionSettings_Correctly()
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public class CommandBaseTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that encrypt option maps to SignAndEncrypt.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SecurityOption_Encrypt_MapsToSignAndEncrypt()
|
||||
{
|
||||
@@ -57,6 +59,7 @@ public class CommandBaseTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that none option maps to None.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SecurityOption_None_MapsToNone()
|
||||
{
|
||||
@@ -75,6 +78,7 @@ public class CommandBaseTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that no failover URLs results in null FailoverUrls.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NoFailoverUrls_FailoverUrlsIsNull()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class CommandRangeValidationTests
|
||||
{
|
||||
/// <summary>Verifies that BrowseCommand rejects negative depth values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseCommand_NegativeDepth_ThrowsCommandException()
|
||||
{
|
||||
@@ -30,6 +31,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that BrowseCommand rejects zero depth values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseCommand_ZeroDepth_ThrowsCommandException()
|
||||
{
|
||||
@@ -47,6 +49,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SubscribeCommand rejects zero interval values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_ZeroInterval_ThrowsCommandException()
|
||||
{
|
||||
@@ -65,6 +68,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SubscribeCommand rejects negative interval values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_NegativeInterval_ThrowsCommandException()
|
||||
{
|
||||
@@ -82,6 +86,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SubscribeCommand in recursive mode rejects zero max depth with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_RecursiveZeroMaxDepth_ThrowsCommandException()
|
||||
{
|
||||
@@ -102,6 +107,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SubscribeCommand rejects negative duration values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_NegativeDuration_ThrowsCommandException()
|
||||
{
|
||||
@@ -119,6 +125,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AlarmsCommand rejects zero interval values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task AlarmsCommand_ZeroInterval_ThrowsCommandException()
|
||||
{
|
||||
@@ -136,6 +143,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that HistoryReadCommand rejects negative max values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadCommand_NegativeMax_ThrowsCommandException()
|
||||
{
|
||||
@@ -154,6 +162,7 @@ public class CommandRangeValidationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that HistoryReadCommand rejects zero interval values with a command exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadCommand_ZeroInterval_ThrowsCommandException()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class ConnectCommandTests
|
||||
{
|
||||
/// <summary>Verifies that execute prints connection info.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_PrintsConnectionInfo()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public class ConnectCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that execute calls connect and disconnect.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_CallsConnectAndDisconnect()
|
||||
{
|
||||
@@ -59,6 +61,7 @@ public class ConnectCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that execute disconnects on error.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsOnError()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class EventHandlerLifecycleTests
|
||||
{
|
||||
/// <summary>Verifies that SubscribeCommand detaches the DataChanged event handler after exit.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_AfterExit_DataChangedEventHasNoSubscribers()
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public class EventHandlerLifecycleTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AlarmsCommand detaches the AlarmEvent handler after exit.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AlarmsCommand_AfterExit_AlarmEventHasNoSubscribers()
|
||||
{
|
||||
|
||||
@@ -126,13 +126,13 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
/// <inheritdoc />
|
||||
public ConnectionInfo? CurrentConnectionInfo => ConnectCalled ? ConnectionInfoResult : null;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when a subscribed node value changes.</summary>
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when an alarm event is received from the server.</summary>
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when the connection state changes.</summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <summary>True when at least one handler is attached to <see cref="DataChanged" />.</summary>
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
}
|
||||
|
||||
/// <summary>Creates and returns the fake OPC UA client service.</summary>
|
||||
/// <returns>The pre-configured <see cref="FakeOpcUaClientService"/> instance.</returns>
|
||||
public IOpcUaClientService Create()
|
||||
{
|
||||
return _service;
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class HistoryReadCommandTests
|
||||
{
|
||||
/// <summary>Verifies RawRead execution prints values.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_RawRead_PrintsValues()
|
||||
{
|
||||
@@ -44,6 +45,7 @@ public class HistoryReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies RawRead execution calls HistoryReadRaw.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_RawRead_CallsHistoryReadRaw()
|
||||
{
|
||||
@@ -65,6 +67,7 @@ public class HistoryReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies AggregateRead execution calls HistoryReadAggregate.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_AggregateRead_CallsHistoryReadAggregate()
|
||||
{
|
||||
@@ -87,6 +90,7 @@ public class HistoryReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies AggregateRead execution prints aggregate info.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_AggregateRead_PrintsAggregateInfo()
|
||||
{
|
||||
@@ -109,6 +113,7 @@ public class HistoryReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies invalid aggregate throws CommandException.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_InvalidAggregate_ThrowsCommandException()
|
||||
{
|
||||
@@ -129,6 +134,7 @@ public class HistoryReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies disconnect is called in finally block.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class InputValidationErrorsTests
|
||||
{
|
||||
/// <summary>Verifies that HistoryReadCommand with invalid start time throws CommandException.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadCommand_InvalidStartTime_ThrowsCommandException()
|
||||
{
|
||||
@@ -32,6 +33,7 @@ public class InputValidationErrorsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that HistoryReadCommand with invalid end time throws CommandException.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadCommand_InvalidEndTime_ThrowsCommandException()
|
||||
{
|
||||
@@ -50,6 +52,7 @@ public class InputValidationErrorsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that HistoryReadCommand with invalid aggregate throws CommandException.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadCommand_InvalidAggregate_ThrowsCommandException()
|
||||
{
|
||||
@@ -68,6 +71,7 @@ public class InputValidationErrorsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadCommand with invalid node ID throws CommandException.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadCommand_InvalidNodeId_ThrowsCommandException()
|
||||
{
|
||||
@@ -85,6 +89,7 @@ public class InputValidationErrorsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SubscribeCommand with invalid node ID throws CommandException.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_InvalidNodeId_ThrowsCommandException()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class LoggerLifecycleTests
|
||||
{
|
||||
/// <summary>Verifies that ConfigureLogging disposes previous logger before reassigning.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConfigureLogging_DisposesPreviousLogger_BeforeReassigning()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class ReadCommandTests
|
||||
{
|
||||
/// <summary>Verifies that execute prints the read value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_PrintsReadValue()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public class ReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that execute calls read value with correct node ID.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_CallsReadValueWithCorrectNodeId()
|
||||
{
|
||||
@@ -60,6 +62,7 @@ public class ReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that execute disconnects in finally.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
@@ -79,6 +82,7 @@ public class ReadCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that execute disconnects even on read error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsEvenOnReadError()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class RedundancyCommandTests
|
||||
{
|
||||
/// <summary>Verifies that Execute prints redundancy information correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_PrintsRedundancyInfo()
|
||||
{
|
||||
@@ -36,6 +37,7 @@ public class RedundancyCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute omits the Server URIs section when none are present.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_NoServerUris_OmitsUriSection()
|
||||
{
|
||||
@@ -61,6 +63,7 @@ public class RedundancyCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute calls GetRedundancyInfo on the service.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_CallsGetRedundancyInfo()
|
||||
{
|
||||
@@ -78,6 +81,7 @@ public class RedundancyCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Execute disconnects and disposes in the finally block.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class SubscribeCommandSummaryTests
|
||||
{
|
||||
/// <summary>Verifies that nodes with no updates are counted separately from suspects.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Summary_NodeWithNoUpdate_IsCountedAsNeverNotAsNeverWentBad()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nodes with only good values are counted as never went bad.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Summary_NodeReceivedOnlyGoodValues_IsCountedAsNeverWentBad()
|
||||
{
|
||||
@@ -73,6 +75,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nodes with bad values are counted as ever went bad.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Summary_NodeReceivedBadValue_IsCountedAsEverWentBad()
|
||||
{
|
||||
@@ -102,6 +105,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscription auto-exits when duration expires.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Duration_ZeroOrPositive_AutoExits()
|
||||
{
|
||||
@@ -129,6 +133,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that --quiet suppresses updates but prints summary.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Quiet_SuppressesPerUpdateOutputButPrintsSummary()
|
||||
{
|
||||
@@ -160,6 +165,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that summary is written to disk when summary file is specified.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SummaryFile_WritesSummaryToDisk()
|
||||
{
|
||||
@@ -191,6 +197,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that recursive flag browses subtree and subscribes every variable.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Recursive_BrowsesSubtreeAndSubscribesEveryVariable()
|
||||
{
|
||||
@@ -230,6 +237,7 @@ public class SubscribeCommandSummaryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscription failures are handled gracefully.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeFailure_PrintsFailedMessage_DoesNotCrash()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class SubscribeCommandTests
|
||||
{
|
||||
/// <summary>Verifies that ExecuteAsync subscribes with the correct parameters.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_SubscribesWithCorrectParameters()
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public class SubscribeCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ExecuteAsync unsubscribes when cancellation is requested.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_UnsubscribesOnCancellation()
|
||||
{
|
||||
@@ -60,6 +62,7 @@ public class SubscribeCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ExecuteAsync disconnects and disposes in a finally block.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
@@ -84,6 +87,7 @@ public class SubscribeCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ExecuteAsync prints the correct subscription message.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_PrintsSubscriptionMessage()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class TestConsoleHelper
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FakeInMemoryConsole" /> for testing.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="FakeInMemoryConsole"/> instance.</returns>
|
||||
public static FakeInMemoryConsole CreateConsole()
|
||||
{
|
||||
return new FakeInMemoryConsole();
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
|
||||
public class WriteCommandTests
|
||||
{
|
||||
/// <summary>Verifies that write command executes successfully.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_WritesSuccessfully()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class WriteCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write command reports failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_ReportsFailure()
|
||||
{
|
||||
@@ -57,6 +59,7 @@ public class WriteCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write command reads current value before writing.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_ReadsCurrentValueThenWrites()
|
||||
{
|
||||
@@ -83,6 +86,7 @@ public class WriteCommandTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write command disconnects in finally block.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_DisconnectsInFinally()
|
||||
{
|
||||
|
||||
@@ -12,11 +12,7 @@ internal sealed class FakeEndpointDiscovery : IEndpointDiscovery
|
||||
/// <summary>Gets the last endpoint URL passed to SelectEndpoint.</summary>
|
||||
public string? LastEndpointUrl { get; private set; }
|
||||
|
||||
/// <summary>Selects an endpoint for the given configuration and URL, optionally throwing an exception.</summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <param name="endpointUrl">The endpoint URL to select.</param>
|
||||
/// <param name="requestedMode">The requested security mode.</param>
|
||||
/// <returns>The selected endpoint description.</returns>
|
||||
/// <inheritdoc />
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode)
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that a valid connection request returns populated connection metadata and marks the client as connected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_ValidSettings_ReturnsConnectionInfo()
|
||||
{
|
||||
@@ -58,6 +59,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that invalid connection settings fail validation before any OPC UA session is created.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_InvalidSettings_ThrowsBeforeCreatingSession()
|
||||
{
|
||||
@@ -71,6 +73,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that server and security details from the session are copied into the exposed connection info.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_PopulatesConnectionInfo()
|
||||
{
|
||||
@@ -96,6 +99,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that connection-state transitions are raised for the connecting and connected phases.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_RaisesConnectionStateChangedEvents()
|
||||
{
|
||||
@@ -114,6 +118,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that a failed session creation leaves the client in the disconnected state.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_SessionFactoryFails_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -130,6 +135,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that username and password settings are passed through to the session-creation pipeline.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_WithUsername_PassesThroughToFactory()
|
||||
{
|
||||
@@ -148,6 +154,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that disconnect closes the active session and clears exposed connection state.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_WhenConnected_ClosesSession()
|
||||
{
|
||||
@@ -164,6 +171,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that disconnect is safe to call when no server session is active.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_WhenNotConnected_IsIdempotent()
|
||||
{
|
||||
@@ -174,6 +182,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that repeated disconnect calls do not throw after cleanup has already run.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_CalledTwice_IsIdempotent()
|
||||
{
|
||||
@@ -187,6 +196,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that a connected client can read the current value of a node through the session adapter.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadValueAsync_WhenConnected_ReturnsValue()
|
||||
{
|
||||
@@ -206,6 +216,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that reads are rejected when the client is not connected to a server.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadValueAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -216,6 +227,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that session-level read failures are surfaced to callers instead of being swallowed.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadValueAsync_SessionThrows_PropagatesException()
|
||||
{
|
||||
@@ -232,6 +244,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that writes succeed through the session adapter when the client is connected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_WhenConnected_WritesValue()
|
||||
{
|
||||
@@ -252,6 +265,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that string inputs are coerced to the node's current data type before writing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_StringValue_CoercesToTargetType()
|
||||
{
|
||||
@@ -271,6 +285,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that non-string values are written directly without an extra type-inference read.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_NonStringValue_WritesDirectly()
|
||||
{
|
||||
@@ -287,6 +302,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that writes are rejected when the client is disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -298,6 +314,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Verifies that writing a string to a node whose current read returns a bad status
|
||||
/// surfaces a clear error instead of writing a mistyped string value (Client.Shared-008).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_StringValueWithBadReadStatus_ThrowsInvalidOperationException()
|
||||
{
|
||||
@@ -318,6 +335,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Verifies that writing a string to a node whose read returns bad status and null Value
|
||||
/// surfaces a clear error for both the bad-status case (Client.Shared-008).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_StringValueWithBadStatus_MessageMentionsNode()
|
||||
{
|
||||
@@ -339,6 +357,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that browse results are mapped into the client browse model used by CLI and UI consumers.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_WhenConnected_ReturnsMappedResults()
|
||||
{
|
||||
@@ -368,6 +387,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that a null browse root defaults to the OPC UA Objects folder.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_NullParent_UsesObjectsFolder()
|
||||
{
|
||||
@@ -386,6 +406,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that object nodes trigger child-detection checks so the client can mark expandable branches.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_ObjectNode_ChecksHasChildren()
|
||||
{
|
||||
@@ -414,6 +435,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that browse continuation points are followed so multi-page address-space branches are fully returned.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_WithContinuationPoint_FollowsIt()
|
||||
{
|
||||
@@ -452,6 +474,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that browse requests are rejected when the client is disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -463,6 +486,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that subscribing to a node creates a monitored item on a data-change subscription.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_CreatesSubscription()
|
||||
{
|
||||
@@ -479,6 +503,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that duplicate subscribe requests for the same node do not create duplicate monitored items.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_DuplicateNode_IsIdempotent()
|
||||
{
|
||||
@@ -495,6 +520,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that data-change notifications from the subscription are raised through the shared client event.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_RaisesDataChangedEvent()
|
||||
{
|
||||
@@ -520,6 +546,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that unsubscribing removes the corresponding monitored item from the active subscription.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_RemovesMonitoredItem()
|
||||
{
|
||||
@@ -537,6 +564,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that unsubscribing an unknown node is treated as a safe no-op.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_WhenNotSubscribed_DoesNotThrow()
|
||||
{
|
||||
@@ -551,6 +579,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that data subscriptions cannot be created while the client is disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -563,6 +592,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that alarm subscription requests create an event monitored item on the session.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_CreatesEventSubscription()
|
||||
{
|
||||
@@ -579,6 +609,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that duplicate alarm-subscription requests do not create duplicate event subscriptions.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_Duplicate_IsIdempotent()
|
||||
{
|
||||
@@ -595,6 +626,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that OPC UA event notifications are mapped into the shared client alarm event model.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_RaisesAlarmEvent()
|
||||
{
|
||||
@@ -643,6 +675,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that removing alarm monitoring deletes the underlying event subscription.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAlarmsAsync_DeletesSubscription()
|
||||
{
|
||||
@@ -660,6 +693,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that removing alarms is safe even when no alarm subscription exists.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAlarmsAsync_WhenNoSubscription_DoesNotThrow()
|
||||
{
|
||||
@@ -673,6 +707,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that condition refresh requests are forwarded to the active alarm subscription.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestConditionRefreshAsync_CallsAdapter()
|
||||
{
|
||||
@@ -690,6 +725,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that condition refresh fails fast when no alarm subscription is active.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestConditionRefreshAsync_NoAlarmSubscription_Throws()
|
||||
{
|
||||
@@ -704,6 +740,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that alarm subscriptions cannot be created while disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -716,6 +753,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that raw history reads return the session-provided values.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadRawAsync_ReturnsValues()
|
||||
{
|
||||
@@ -738,6 +776,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that raw history reads are rejected while disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadRawAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -748,6 +787,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that raw-history failures from the session are propagated to callers.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadRawAsync_SessionThrows_PropagatesException()
|
||||
{
|
||||
@@ -762,6 +802,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that aggregate history reads return the processed values from the session adapter.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadAggregateAsync_ReturnsValues()
|
||||
{
|
||||
@@ -784,6 +825,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that aggregate history reads are rejected while disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadAggregateAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -796,6 +838,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that aggregate-history failures from the session are propagated to callers.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HistoryReadAggregateAsync_SessionThrows_PropagatesException()
|
||||
{
|
||||
@@ -814,6 +857,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that redundancy mode, service level, and server URIs are read from the standard OPC UA redundancy nodes.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_ReturnsInfo()
|
||||
{
|
||||
@@ -846,6 +890,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that missing optional redundancy arrays do not prevent a redundancy snapshot from being returned.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_MissingOptionalArrays_ReturnsGracefully()
|
||||
{
|
||||
@@ -876,6 +921,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that redundancy inspection is rejected while disconnected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -887,6 +933,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Verifies that RedundancySupport boxed as a different numeric type (e.g. short) is handled
|
||||
/// without InvalidCastException — defensive Convert.ToInt32 coercion (Client.Shared-002).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_RedundancySupportBoxedAsShort_DoesNotThrow()
|
||||
{
|
||||
@@ -915,6 +962,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Verifies that a bad-status response for RedundancySupport/ServiceLevel falls back to defaults
|
||||
/// rather than throwing (Client.Shared-002).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_BadStatusOnRequiredReads_ReturnsDefaults()
|
||||
{
|
||||
@@ -944,6 +992,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Verifies that an alarm event with fewer than 6 fields (but at least 1) is still raised
|
||||
/// with available fields — the old hard <6 early return silently dropped it (Client.Shared-001).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnAlarmEvent_TruncatedFields_StillRaisesEvent()
|
||||
{
|
||||
@@ -978,6 +1027,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that a null or empty event field list is silently ignored (defensive guard).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnAlarmEvent_EmptyFields_DoesNotRaiseEvent()
|
||||
{
|
||||
@@ -1003,6 +1053,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Verifies that a successful acknowledge call returns <see cref="StatusCodes.Good"/>
|
||||
/// and reaches the session adapter's CallMethodAsync (Client.Shared-009).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarmAsync_OnSuccess_ReturnsGood()
|
||||
{
|
||||
@@ -1022,6 +1073,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <see cref="ServiceResultException"/>, so callers using
|
||||
/// <c>if (StatusCode.IsBad(result))</c> actually see the failure.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarmAsync_OnServiceResultException_ReturnsBadStatusCode()
|
||||
{
|
||||
@@ -1044,6 +1096,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// source node, but left alone when the caller already passes the condition node —
|
||||
/// matches the documented contract.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarmAsync_LeavesConditionSuffixAlone()
|
||||
{
|
||||
@@ -1066,6 +1119,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <c>InAlarm</c>/<c>Acked</c> from the condition node's Galaxy attributes. Verify
|
||||
/// the alarm event is delivered with the values from the supplemental reads.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnAlarmEvent_MissingAckedActiveButHasConditionNode_FallbackReadsAndRaisesEvent()
|
||||
{
|
||||
@@ -1139,6 +1193,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that a keep-alive failure moves the client to a configured failover endpoint.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_TriggersFailover()
|
||||
{
|
||||
@@ -1170,6 +1225,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that connection metadata is refreshed to reflect the newly active failover endpoint.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_UpdatesConnectionInfo()
|
||||
{
|
||||
@@ -1196,6 +1252,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that the client falls back to disconnected when every failover endpoint is unreachable.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_AllEndpointsFail_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -1217,6 +1274,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// failover loop is still in-flight must be ignored, so only one failover runs and only
|
||||
/// one replacement session is created.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_ReentrantWhileFailoverInFlight_RunsFailoverOnce()
|
||||
{
|
||||
@@ -1255,6 +1313,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// Regression for Client.Shared-005: concurrent subscribe/unsubscribe calls mutating the
|
||||
/// active-subscription bookkeeping must not corrupt the dictionary or throw.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAndUnsubscribe_ConcurrentCalls_DoNotCorruptState()
|
||||
{
|
||||
@@ -1283,6 +1342,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that dispose releases the underlying session and clears exposed connection state.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_CleansUpResources()
|
||||
{
|
||||
@@ -1299,6 +1359,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that dispose is safe to call even when no connection was established.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public void Dispose_WhenNotConnected_DoesNotThrow()
|
||||
{
|
||||
@@ -1308,6 +1369,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that public operations reject use after the shared client has been disposed.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OperationsAfterDispose_Throw()
|
||||
{
|
||||
@@ -1324,6 +1386,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that the factory creates a usable shared OPC UA client service instance.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public void OpcUaClientServiceFactory_CreatesService()
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ public class AlarmsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SubscribeCommand sets IsSubscribed flag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeCommand_SetsIsSubscribed()
|
||||
{
|
||||
@@ -68,6 +69,7 @@ public class AlarmsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UnsubscribeCommand clears IsSubscribed flag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeCommand_ClearsIsSubscribed()
|
||||
{
|
||||
@@ -81,6 +83,7 @@ public class AlarmsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RefreshCommand calls the service.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RefreshCommand_CallsService()
|
||||
{
|
||||
@@ -156,6 +159,7 @@ public class AlarmsViewModelTests
|
||||
/// Regression test for Client.UI-006 — when SubscribeAlarmsAsync throws, the failure must be
|
||||
/// surfaced to the operator via the view model's StatusMessage rather than silently swallowed.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_OnFailure_SurfacesStatusMessage()
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadRootsAsync populates root nodes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadRootsAsync_PopulatesRootNodes()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadRootsAsync browses with null parent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadRootsAsync_BrowsesWithNullParent()
|
||||
{
|
||||
@@ -61,6 +63,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nodes with children have a placeholder.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadRootsAsync_NodeWithChildren_HasPlaceholder()
|
||||
{
|
||||
@@ -73,6 +76,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nodes without children have no placeholder.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadRootsAsync_NodeWithoutChildren_HasNoPlaceholder()
|
||||
{
|
||||
@@ -84,6 +88,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that first tree node expand triggers child browse.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TreeNode_FirstExpand_TriggersChildBrowse()
|
||||
{
|
||||
@@ -114,6 +119,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that second tree node expand does not browse again.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TreeNode_SecondExpand_DoesNotBrowseAgain()
|
||||
{
|
||||
@@ -143,6 +149,7 @@ public class BrowseTreeViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that IsLoading transitions during browse.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TreeNode_IsLoading_TransitionsDuringBrowse()
|
||||
{
|
||||
|
||||
@@ -136,13 +136,13 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
/// <inheritdoc />
|
||||
public ConnectionInfo? CurrentConnectionInfo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when a subscribed node value changes.</summary>
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when an alarm condition event is received from the server.</summary>
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when the OPC UA session connection state changes.</summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -13,15 +13,14 @@ public sealed class FakeSettingsService : ISettingsService
|
||||
/// <summary>Gets the last settings that were saved.</summary>
|
||||
public UserSettings? LastSaved { get; private set; }
|
||||
|
||||
/// <summary>Loads and returns the current settings.</summary>
|
||||
/// <inheritdoc />
|
||||
public UserSettings Load()
|
||||
{
|
||||
LoadCallCount++;
|
||||
return Settings;
|
||||
}
|
||||
|
||||
/// <summary>Saves the specified settings.</summary>
|
||||
/// <param name="settings">The settings to save.</param>
|
||||
/// <inheritdoc />
|
||||
public void Save(UserSettings settings)
|
||||
{
|
||||
SaveCallCount++;
|
||||
|
||||
@@ -60,6 +60,7 @@ public class HistoryViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a raw history read populates results correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadHistoryCommand_Raw_PopulatesResults()
|
||||
{
|
||||
@@ -77,6 +78,7 @@ public class HistoryViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an aggregate history read populates results correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadHistoryCommand_Aggregate_PopulatesResults()
|
||||
{
|
||||
@@ -94,6 +96,7 @@ public class HistoryViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the read history command clears previous results before loading new ones.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadHistoryCommand_ClearsResultsBefore()
|
||||
{
|
||||
@@ -107,6 +110,7 @@ public class HistoryViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the loading state is false after the read history command completes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadHistoryCommand_IsLoading_FalseAfterComplete()
|
||||
{
|
||||
@@ -158,6 +162,7 @@ public class HistoryViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read history command errors are displayed in the results.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadHistoryCommand_Error_ShowsErrorInResults()
|
||||
{
|
||||
|
||||
@@ -76,6 +76,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that a successful connect command updates the shell into the connected state.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_TransitionsToConnected()
|
||||
{
|
||||
@@ -89,6 +90,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that the initial browse tree is loaded after a successful connect.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_LoadsRootNodes()
|
||||
{
|
||||
@@ -101,6 +103,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that redundancy details are fetched and exposed after connecting.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_FetchesRedundancyInfo()
|
||||
{
|
||||
@@ -114,6 +117,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that the session label shows the connected server and session identity.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SetsSessionLabel()
|
||||
{
|
||||
@@ -126,6 +130,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that disconnect returns the shell to the disconnected state.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisconnectCommand_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -140,6 +145,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that disconnect clears session-specific UI state such as browse data and redundancy details.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Disconnect_ClearsStateAndChildren()
|
||||
{
|
||||
@@ -155,6 +161,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that connection-state events from the client update the shell status text and state.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectionStateChangedEvent_UpdatesState()
|
||||
{
|
||||
@@ -171,6 +178,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that selecting a tree node updates the dependent read/write and history panels.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SelectedTreeNode_PropagatesToChildViewModels()
|
||||
{
|
||||
@@ -186,6 +194,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that a successful connect propagates connected state into the child tabs.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_PropagatesIsConnectedToChildViewModels()
|
||||
{
|
||||
@@ -200,6 +209,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that disconnect propagates disconnected state into the child tabs.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisconnectCommand_PropagatesIsConnectedFalseToChildViewModels()
|
||||
{
|
||||
@@ -215,6 +225,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that failed connection attempts restore the disconnected shell state and surface the error text.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectFailure_RevertsToDisconnected()
|
||||
{
|
||||
@@ -229,6 +240,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that connection-state transitions raise property-changed notifications for UI binding updates.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PropertyChanged_FiredForConnectionState()
|
||||
{
|
||||
@@ -260,6 +272,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that failover endpoint text is parsed into connection settings on connect.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsFailoverUrlsToSettings()
|
||||
{
|
||||
@@ -276,6 +289,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that empty failover text is normalized to no configured failover endpoints.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
|
||||
{
|
||||
@@ -289,6 +303,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that the configured session timeout is passed into the connection settings.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsSessionTimeoutToSettings()
|
||||
{
|
||||
@@ -302,6 +317,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that the auto-accept certificate toggle is passed into the connection settings.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
|
||||
{
|
||||
@@ -315,6 +331,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that a custom certificate store path is passed into the connection settings.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsCertificateStorePathToSettings()
|
||||
{
|
||||
@@ -328,6 +345,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that subscribing selected nodes adds subscriptions and switches the shell to the subscriptions tab.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
|
||||
{
|
||||
@@ -348,6 +366,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that subscribing selected nodes is a no-op when nothing is selected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
|
||||
{
|
||||
@@ -361,6 +380,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that the history command targets the selected node and switches the shell to the history tab.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab()
|
||||
{
|
||||
@@ -378,6 +398,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that history actions are enabled when a variable node is selected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode()
|
||||
{
|
||||
@@ -395,6 +416,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that history actions stay disabled when an object node rather than a variable is selected.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode()
|
||||
{
|
||||
@@ -470,6 +492,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that successful connections persist the current connection settings.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SavesSettingsOnSuccess()
|
||||
{
|
||||
@@ -487,6 +510,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that failed connection attempts do not overwrite saved settings.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_DoesNotSaveOnFailure()
|
||||
{
|
||||
@@ -500,6 +524,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that active subscriptions are persisted when the shell disconnects.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SavesSubscribedNodes()
|
||||
{
|
||||
@@ -522,6 +547,7 @@ public class MainWindowViewModelTests
|
||||
/// view model must leave RedundancyInfo null without crashing or hiding the diagnostic.
|
||||
/// The Status text is expected to remain "Connected" (redundancy is optional).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_RedundancyFailure_DoesNotBreakConnection()
|
||||
{
|
||||
@@ -538,6 +564,7 @@ public class MainWindowViewModelTests
|
||||
/// <summary>
|
||||
/// Verifies that saved subscriptions are restored after reconnecting the shell.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_RestoresSavedSubscriptions()
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ public class ReadWriteViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the read command updates value and status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadCommand_UpdatesValueAndStatus()
|
||||
{
|
||||
@@ -91,6 +92,7 @@ public class ReadWriteViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the write command updates write status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteCommand_UpdatesWriteStatus()
|
||||
{
|
||||
@@ -118,6 +120,7 @@ public class ReadWriteViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read command error sets error status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadCommand_Error_SetsErrorStatus()
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionCommand adds a new subscription to the active list.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionCommand_AddsItem()
|
||||
{
|
||||
@@ -58,6 +59,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RemoveSubscriptionCommand removes selected subscription.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task RemoveSubscriptionCommand_RemovesItem()
|
||||
{
|
||||
@@ -83,6 +85,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DataChanged event updates the matching subscription row.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DataChanged_UpdatesMatchingRow()
|
||||
{
|
||||
@@ -98,6 +101,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DataChanged event does not update non-matching subscription rows.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DataChanged_DoesNotUpdateNonMatchingRow()
|
||||
{
|
||||
@@ -146,6 +150,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionForNodeAsync adds a subscription.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_AddsSubscription()
|
||||
{
|
||||
@@ -160,6 +165,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionForNodeAsync skips duplicate subscriptions.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_SkipsDuplicate()
|
||||
{
|
||||
@@ -173,6 +179,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionForNodeAsync does nothing when disconnected.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_DoesNothing_WhenDisconnected()
|
||||
{
|
||||
@@ -185,6 +192,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetSubscribedNodeIds returns all active subscription node IDs.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetSubscribedNodeIds_ReturnsActiveNodeIds()
|
||||
{
|
||||
@@ -200,6 +208,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RestoreSubscriptionsAsync subscribes to all provided node IDs.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task RestoreSubscriptionsAsync_SubscribesAllNodes()
|
||||
{
|
||||
@@ -212,6 +221,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ValidateAndWriteAsync returns true on successful write.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ValidateAndWriteAsync_SuccessReturnsTrue()
|
||||
{
|
||||
@@ -227,6 +237,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ValidateAndWriteAsync returns false when value parsing fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ValidateAndWriteAsync_ParseFailureReturnsFalse()
|
||||
{
|
||||
@@ -242,6 +253,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ValidateAndWriteAsync returns false when write fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ValidateAndWriteAsync_WriteFailureReturnsFalse()
|
||||
{
|
||||
@@ -256,6 +268,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ValidateAndWriteAsync returns false when status is bad.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ValidateAndWriteAsync_BadStatusReturnsFalse()
|
||||
{
|
||||
@@ -270,6 +283,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionRecursiveAsync subscribes a variable directly.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionRecursiveAsync_SubscribesVariableDirectly()
|
||||
{
|
||||
@@ -282,6 +296,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionRecursiveAsync browses objects and subscribes variable children.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionRecursiveAsync_BrowsesObjectAndSubscribesVariableChildren()
|
||||
{
|
||||
@@ -302,6 +317,7 @@ public class SubscriptionsViewModelTests
|
||||
/// Regression test for Client.UI-006 — when SubscribeAsync throws, the failure must be surfaced
|
||||
/// to the operator via the view model's StatusMessage rather than silently swallowed.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscription_OnFailure_SurfacesStatusMessage()
|
||||
{
|
||||
@@ -320,6 +336,7 @@ public class SubscriptionsViewModelTests
|
||||
/// Regression test for Client.UI-006 — silent swallow when adding a subscription for a node
|
||||
/// (the context-menu helper) must also surface a status to the operator.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_OnFailure_SurfacesStatusMessage()
|
||||
{
|
||||
@@ -334,6 +351,7 @@ public class SubscriptionsViewModelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddSubscriptionRecursiveAsync recurses through nested objects.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionRecursiveAsync_RecursesNestedObjects()
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ public sealed class ClusterAuditQueryTests : IDisposable
|
||||
|
||||
/// <summary>Structured rows (ClusterId null, NodeId set) for a node in the cluster are now
|
||||
/// visible, alongside the SP-path rows that stamp ClusterId directly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Surfaces_both_clusterId_rows_and_structured_nodeId_rows()
|
||||
{
|
||||
@@ -77,6 +78,7 @@ public sealed class ClusterAuditQueryTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>An audit row stamped with another cluster's ClusterId never appears.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Does_not_surface_other_cluster_rows()
|
||||
{
|
||||
@@ -91,6 +93,7 @@ public sealed class ClusterAuditQueryTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Respects the page-size cap, newest first.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Caps_to_page_size_newest_first()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
public sealed class DriverHostStatusTests(SchemaComplianceFixture fixture)
|
||||
{
|
||||
/// <summary>Verifies that the composite key allows the same host across different nodes or drivers.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Composite_key_allows_same_host_across_different_nodes_or_drivers()
|
||||
{
|
||||
@@ -58,6 +59,7 @@ public sealed class DriverHostStatusTests(SchemaComplianceFixture fixture)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the upsert pattern updates existing records in place.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Upsert_pattern_for_same_key_updates_in_place()
|
||||
{
|
||||
@@ -97,6 +99,7 @@ public sealed class DriverHostStatusTests(SchemaComplianceFixture fixture)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the State enum is persisted as a string, not an integer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Enum_persists_as_string_not_int()
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
};
|
||||
|
||||
/// <summary>Verifies that reading a snapshot on first boot with no existing snapshot throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FirstBoot_NoSnapshot_ReadThrows()
|
||||
{
|
||||
@@ -43,6 +44,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that sealed snapshots can be read back correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SealThenRead_RoundTrips()
|
||||
{
|
||||
@@ -58,6 +60,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that sealed files are marked read-only on disk.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SealedFile_IsReadOnly_OnDisk()
|
||||
{
|
||||
@@ -71,6 +74,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the current generation pointer advances when a new generation is sealed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SealingTwoGenerations_PointerAdvances_ToLatest()
|
||||
{
|
||||
@@ -84,6 +88,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that prior generation files are preserved after a new seal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PriorGenerationFile_Survives_AfterNewSeal()
|
||||
{
|
||||
@@ -97,6 +102,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading a corrupt sealed file fails safely.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CorruptSealedFile_ReadFailsClosed()
|
||||
{
|
||||
@@ -113,6 +119,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading with a missing sealed file fails safely.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MissingSealedFile_ReadFailsClosed()
|
||||
{
|
||||
@@ -129,6 +136,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading with a corrupt pointer file fails safely.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CorruptPointerFile_ReadFailsClosed()
|
||||
{
|
||||
@@ -143,6 +151,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that sealing the same generation twice is idempotent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SealSameGenerationTwice_IsIdempotent()
|
||||
{
|
||||
@@ -155,6 +164,7 @@ public sealed class GenerationSealedCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that independent clusters do not interfere with each other.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task IndependentClusters_DoNotInterfere()
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
};
|
||||
|
||||
/// <summary>Verifies that Create sets Id and CreatedAtUtc.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Create_SetsId_AndCreatedAtUtc()
|
||||
{
|
||||
@@ -47,6 +48,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Create rejects empty LDAP group.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Create_Rejects_EmptyLdapGroup()
|
||||
{
|
||||
@@ -58,6 +60,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Create rejects system-wide mapping with ClusterId.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Create_Rejects_SystemWide_With_ClusterId()
|
||||
{
|
||||
@@ -69,6 +72,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Create rejects non-system-wide mapping without ClusterId.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Create_Rejects_NonSystemWide_WithoutClusterId()
|
||||
{
|
||||
@@ -80,6 +84,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetByGroups returns only matching grants.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetByGroups_Returns_MatchingGrants_Only()
|
||||
{
|
||||
@@ -96,6 +101,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetByGroups returns empty when input is empty.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetByGroups_Empty_Input_ReturnsEmpty()
|
||||
{
|
||||
@@ -108,6 +114,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ListAll orders results by group then cluster.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ListAll_Orders_ByGroupThenCluster()
|
||||
{
|
||||
@@ -125,6 +132,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Delete removes the matching row.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Delete_Removes_Matching_Row()
|
||||
{
|
||||
@@ -138,6 +146,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Delete with unknown Id is a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Delete_Unknown_Id_IsNoOp()
|
||||
{
|
||||
@@ -148,6 +157,7 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a system-wide row (IsSystemWide=true, ClusterId=null) appears in both ListAllAsync and GetByGroupsAsync.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SystemWide_Row_AppearsIn_ListAll_And_GetByGroups()
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
};
|
||||
|
||||
/// <summary>Verifies that payload is preserved through a write-then-read cycle.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Roundtrip_preserves_payload()
|
||||
{
|
||||
@@ -38,6 +39,7 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetMostRecentAsync returns the latest generation when multiple exist.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMostRecent_returns_latest_when_multiple_generations_present()
|
||||
{
|
||||
@@ -50,6 +52,7 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetMostRecentAsync returns null for an unknown cluster.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMostRecent_returns_null_for_unknown_cluster()
|
||||
{
|
||||
@@ -58,6 +61,7 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Prune keeps the latest N generations and drops older ones.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Prune_keeps_latest_N_and_drops_older()
|
||||
{
|
||||
@@ -81,6 +85,7 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writing the same cluster/generation twice replaces rather than duplicates.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Put_same_cluster_generation_twice_replaces_not_duplicates()
|
||||
{
|
||||
@@ -102,6 +107,7 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
// callers could both observe `existing is null` and both Insert.
|
||||
// ------------------------------------------------------------------------------------
|
||||
/// <summary>Verifies that concurrent PutAsync calls for the same cluster and generation do not create duplicates.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task PutAsync_concurrent_for_same_cluster_and_generation_does_not_duplicate()
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that successful central DB reads return value and mark fresh.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task CentralDbSucceeds_ReturnsValue_MarksFresh()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that exhausted retries fall back to cache and mark stale.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task CentralDbFails_ExhaustsRetries_FallsBackToCache_MarksStale()
|
||||
{
|
||||
@@ -78,6 +80,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DB failure with unavailable cache throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task CentralDbFails_AndCacheAlsoUnavailable_Throws()
|
||||
{
|
||||
@@ -99,6 +102,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation is not retried.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_NotRetried()
|
||||
{
|
||||
@@ -133,6 +137,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that command timeout TaskCanceledException falls back to cache.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task CommandTimeout_TaskCanceledException_FallsBackToCache()
|
||||
{
|
||||
@@ -163,6 +168,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Polly timeout rejection falls back to cache.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task PollyTimeout_TimeoutRejectedException_FallsBackToCache()
|
||||
{
|
||||
@@ -201,6 +207,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that fallback warnings do not log exceptions or password fragments.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task FallbackWarning_does_not_log_full_exception_object_or_password_fragment()
|
||||
{
|
||||
@@ -242,6 +249,7 @@ public sealed class ResilientConfigReaderTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that caller cancellation propagates rather than falling back.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task CallerCancellation_Propagates_NotFallback()
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the initial poll forces an event for every subscribed tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Initial_poll_force_raises_every_subscribed_tag()
|
||||
{
|
||||
@@ -54,6 +55,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unchanged values are only raised once.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unchanged_value_raises_only_once()
|
||||
{
|
||||
@@ -72,6 +74,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that value changes raise new events.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Value_change_raises_new_event()
|
||||
{
|
||||
@@ -92,6 +95,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribe halts the polling loop.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_halts_the_loop()
|
||||
{
|
||||
@@ -113,6 +117,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that intervals below the configured floor are clamped.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Interval_below_floor_is_clamped()
|
||||
{
|
||||
@@ -134,6 +139,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that multiple subscriptions operate independently.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Multiple_subscriptions_are_independent()
|
||||
{
|
||||
@@ -165,6 +171,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reader exceptions do not crash the polling loop.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reader_exception_does_not_crash_loop()
|
||||
{
|
||||
@@ -195,6 +202,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribing an unknown handle returns false.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_unknown_handle_returns_false()
|
||||
{
|
||||
@@ -206,6 +214,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the active subscription count tracks lifecycle changes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ActiveSubscriptionCount_tracks_lifecycle()
|
||||
{
|
||||
@@ -225,6 +234,7 @@ public sealed class PollGroupEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the engine cancels all active subscriptions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisposeAsync_cancels_all_subscriptions()
|
||||
{
|
||||
@@ -253,6 +263,7 @@ public sealed class PollGroupEngineTests
|
||||
/// must fire only the initial change event, not a spurious event on every tick, even
|
||||
/// when the driver produces a fresh array instance on each read.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Array_valued_tag_unchanged_contents_raises_only_once()
|
||||
{
|
||||
@@ -289,6 +300,7 @@ public sealed class PollGroupEngineTests
|
||||
/// Core.Abstractions-001: an array-valued tag whose contents change between polls
|
||||
/// must fire a change event for each distinct set of contents.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Array_valued_tag_changed_contents_raises_event()
|
||||
{
|
||||
@@ -320,6 +332,7 @@ public sealed class PollGroupEngineTests
|
||||
/// violates the documented contract. The engine must throw a descriptive exception
|
||||
/// rather than silently stalling.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reader_short_result_list_raises_descriptive_exception_and_loop_continues()
|
||||
{
|
||||
@@ -366,6 +379,7 @@ public sealed class PollGroupEngineTests
|
||||
/// must continue to swallow exceptions (backward compatible). When an error callback IS
|
||||
/// supplied, every exception caught during a poll cycle must be routed to it.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reader_exception_is_reported_to_onError_callback()
|
||||
{
|
||||
@@ -401,6 +415,7 @@ public sealed class PollGroupEngineTests
|
||||
/// must also be routed to the error callback so the driver health surface can observe
|
||||
/// repeated contract violations.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reader_contract_violation_routes_to_onError_callback()
|
||||
{
|
||||
@@ -433,6 +448,7 @@ public sealed class PollGroupEngineTests
|
||||
/// that itself throws — otherwise a buggy health-surface forwarder would crash the poll
|
||||
/// loop and silently stall the subscription, defeating the whole point of the callback.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnError_handler_that_throws_does_not_crash_loop()
|
||||
{
|
||||
@@ -463,7 +479,7 @@ public sealed class PollGroupEngineTests
|
||||
|
||||
private sealed record DummyHandle : ISubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets a diagnostic identifier for this handle.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => "dummy";
|
||||
}
|
||||
|
||||
|
||||
+27
-12
@@ -43,10 +43,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// <summary>Gets or sets an exception to throw once.</summary>
|
||||
public Exception? ThrowOnce { get; set; }
|
||||
|
||||
/// <summary>Writes a batch of events.</summary>
|
||||
/// <param name="batch">Events to write.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task returning the write outcomes.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken ct)
|
||||
{
|
||||
@@ -77,6 +74,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
TimestampUtc: ts ?? DateTime.UtcNow);
|
||||
|
||||
/// <summary>Verifies that acknowledged events are removed from the queue.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EnqueueThenDrain_Ack_removes_row()
|
||||
{
|
||||
@@ -98,6 +96,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that draining an empty queue is a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Drain_with_empty_queue_is_noop()
|
||||
{
|
||||
@@ -111,6 +110,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RetryPlease outcome bumps backoff and keeps the row queued.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RetryPlease_bumps_backoff_and_keeps_row()
|
||||
{
|
||||
@@ -128,6 +128,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an Ack after RetryPlease resets backoff to the floor.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Ack_after_Retry_resets_backoff()
|
||||
{
|
||||
@@ -147,6 +148,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that PermanentFail outcome dead-letters only the failed event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PermanentFail_dead_letters_one_row_only()
|
||||
{
|
||||
@@ -165,6 +167,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writer exceptions trigger retry for the entire batch.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Writer_exception_treated_as_retry_for_whole_batch()
|
||||
{
|
||||
@@ -185,6 +188,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that capacity eviction drops the oldest non-dead-lettered row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Capacity_eviction_drops_oldest_nondeadlettered_row()
|
||||
{
|
||||
@@ -209,6 +213,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dead-lettered rows are purged after retention period expires.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Deadlettered_rows_are_purged_past_retention()
|
||||
{
|
||||
@@ -233,6 +238,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RetryDeadLettered requeues dead-lettered rows for retry.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RetryDeadLettered_requeues_for_retry()
|
||||
{
|
||||
@@ -253,6 +259,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the exponential backoff ladder caps at 60 seconds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Backoff_ladder_caps_at_60s()
|
||||
{
|
||||
@@ -278,6 +285,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that NullAlarmHistorianSink silently swallows enqueue calls.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NullAlarmHistorianSink_swallows_enqueue()
|
||||
{
|
||||
@@ -298,6 +306,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a disposed sink rejects enqueue operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Disposed_sink_rejects_enqueue()
|
||||
{
|
||||
@@ -316,6 +325,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// forever), and every good row's outcome is applied to the CORRECT RowId —
|
||||
/// no good event is silently lost.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Drain_with_corrupt_payload_row_deadletters_it_and_keeps_good_rows_aligned()
|
||||
{
|
||||
@@ -347,6 +357,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// A corrupt row at the very head of the queue must be dead-lettered and not
|
||||
/// prevent the good rows behind it from draining.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Drain_with_corrupt_head_row_does_not_stall_queue()
|
||||
{
|
||||
@@ -373,6 +384,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// hammer the writer. We assert that after the backoff ladder advances, the
|
||||
/// observed inter-batch gap actually grows beyond the bare tick interval.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartDrainLoop_honors_backoff_and_slows_cadence_under_retry()
|
||||
{
|
||||
@@ -400,6 +412,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// steady cadence (backoff stays at the floor) — confirms the reschedule path
|
||||
/// does not get stuck after a successful tick.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartDrainLoop_keeps_steady_cadence_when_writer_is_healthy()
|
||||
{
|
||||
@@ -425,6 +438,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// be recorded into the status surface (LastError) and the drain loop must
|
||||
/// keep rescheduling rather than silently dying.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartDrainLoop_records_drain_fault_and_keeps_running()
|
||||
{
|
||||
@@ -454,6 +468,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// the busy_timeout + WAL pragmas in place the loser of the file-lock race
|
||||
/// waits the lock out instead of failing fast.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_enqueue_and_drain_do_not_throw_sqlite_busy()
|
||||
{
|
||||
@@ -501,10 +516,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// <summary>Gets the number of times WriteBatchAsync has been called.</summary>
|
||||
public int CallCount { get; private set; }
|
||||
|
||||
/// <summary>Writes a batch of events, throwing once then recovering.</summary>
|
||||
/// <param name="batch">Events to write.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task returning the write outcomes.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken ct)
|
||||
{
|
||||
@@ -525,6 +537,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// by the timer callback, but still left the rows stranded on the first
|
||||
/// cardinality-mismatched tick.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Writer_returning_wrong_cardinality_outcomes_sets_backing_off_and_keeps_rows()
|
||||
{
|
||||
@@ -555,6 +568,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// evicted row count must be surfaced in <see cref="HistorianSinkStatus.EvictedCount"/>
|
||||
/// so operators can detect bounded-durability overflow without log scraping.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Capacity_eviction_increments_evicted_count_on_status()
|
||||
{
|
||||
@@ -580,6 +594,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// snapshot of all status fields — no torn DateTime? or stale DrainState.
|
||||
/// Drive status writes from one thread and reads from another concurrently.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetStatus_snapshot_is_consistent_under_concurrent_drain()
|
||||
{
|
||||
@@ -632,10 +647,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// <summary>Fixes the writer to return correct cardinality.</summary>
|
||||
public void FixWriter() => _returnExtra = false;
|
||||
|
||||
/// <summary>Writes a batch of events, returning wrong cardinality until fixed.</summary>
|
||||
/// <param name="batch">Events to write.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task returning the write outcomes.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken ct)
|
||||
{
|
||||
@@ -654,6 +666,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// capacity-probe count must stay bounded — not grow proportionally to the
|
||||
/// enqueue count as the un-optimised path did.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_does_not_count_all_rows_on_every_call_below_capacity()
|
||||
{
|
||||
@@ -677,6 +690,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// stay aligned with a fresh <c>COUNT(*)</c> against the database. Catches drift
|
||||
/// bugs in the in-memory counter introduced by the perf optimisation.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Enqueue_and_drain_keep_queue_depth_consistent_with_storage()
|
||||
{
|
||||
@@ -724,6 +738,7 @@ public sealed class SqliteStoreAndForwardSinkTests : IDisposable
|
||||
/// with storage. Catches drift bugs in the optimised path that would only show
|
||||
/// up under contention.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Counter_remains_consistent_under_concurrent_enqueue_and_drain()
|
||||
{
|
||||
|
||||
@@ -38,15 +38,12 @@ public sealed class FakeUpstream : ITagUpstreamSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads the current value of a tag, or returns a bad-status snapshot if not set.</summary>
|
||||
/// <param name="path">The tag path to read.</param>
|
||||
/// <inheritdoc />
|
||||
public DataValueSnapshot ReadTag(string path)
|
||||
=> _values.TryGetValue(path, out var v) ? v
|
||||
: new DataValueSnapshot(null, 0x80340000u, null, DateTime.UtcNow);
|
||||
|
||||
/// <summary>Subscribes an observer to tag changes for the given path.</summary>
|
||||
/// <param name="path">The tag path to subscribe to.</param>
|
||||
/// <param name="observer">The observer callback to invoke on tag changes.</param>
|
||||
/// <inheritdoc />
|
||||
public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
|
||||
{
|
||||
var list = _subs.GetOrAdd(path, _ => []);
|
||||
|
||||
+37
-22
@@ -32,6 +32,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
PredicateScriptSource: predicate);
|
||||
|
||||
/// <summary>Verifies that LoadAsync compiles the alarm predicate and subscribes to all referenced upstream tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Load_compiles_and_subscribes_to_referenced_upstreams()
|
||||
{
|
||||
@@ -47,6 +48,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that compile failures across multiple alarms are aggregated into a single error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Compile_failures_aggregated_into_one_error()
|
||||
{
|
||||
@@ -63,6 +65,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an upstream tag change triggers predicate re-evaluation and emits an Activated event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Upstream_change_re_evaluates_predicate_and_emits_Activated()
|
||||
{
|
||||
@@ -84,6 +87,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing an upstream tag value emits a Cleared event and transitions the alarm to Inactive.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Clearing_upstream_emits_Cleared_event()
|
||||
{
|
||||
@@ -105,6 +109,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the message template resolves current tag values at the moment of alarm emission.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Message_template_resolves_tag_values_at_emission()
|
||||
{
|
||||
@@ -130,6 +135,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync records the operator user and persists the ack state to the store.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Ack_records_user_and_persists_to_store()
|
||||
{
|
||||
@@ -150,6 +156,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that startup recovery restores the persisted ack state but re-derives the active state from the live predicate.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Startup_recovery_preserves_ack_but_rederives_active_from_predicate()
|
||||
{
|
||||
@@ -198,6 +205,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a shelved alarm transitions its internal state on activation but suppresses the Activated emission.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Shelved_active_transitions_state_but_suppresses_emission()
|
||||
{
|
||||
@@ -222,6 +230,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a runtime exception thrown by a predicate script leaves the alarm state unchanged and does not affect other alarms.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Predicate_runtime_exception_does_not_transition_state()
|
||||
{
|
||||
@@ -239,6 +248,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a disabled alarm does not activate on predicate change and resumes normally after being re-enabled.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Disable_prevents_activation_until_re_enabled()
|
||||
{
|
||||
@@ -260,6 +270,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddCommentAsync appends to the audit trail without changing the alarm's active or ack state.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComment_appends_to_audit_without_state_change()
|
||||
{
|
||||
@@ -278,6 +289,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that predicate scripts are forbidden from calling SetVirtualTag, and that the exception is isolated without state change.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Predicate_scripts_cannot_SetVirtualTag()
|
||||
{
|
||||
@@ -302,6 +314,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the engine releases all upstream tag subscriptions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_releases_upstream_subscriptions()
|
||||
{
|
||||
@@ -317,6 +330,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent reads of alarm state during dictionary mutations do not throw (regression for Core.ScriptedAlarms-001).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_reads_during_mutation_do_not_throw(/* Core.ScriptedAlarms-001 */)
|
||||
{
|
||||
@@ -386,6 +400,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// injectable clock — the clock and scriptTimeout constructor parameters
|
||||
// exist for exactly this.
|
||||
/// <summary>Verifies that a timed shelve automatically expires when the engine's shelving check runs past the unshelve time.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TimedShelve_auto_expires_when_engine_shelving_check_runs(/* -012 (1) */)
|
||||
{
|
||||
@@ -423,6 +438,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// (2a) ConfirmAsync end-to-end through the engine.
|
||||
/// <summary>Verifies that ConfirmAsync records the confirming user and emits a Confirmed event persisted to the store.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConfirmAsync_records_user_and_emits_Confirmed(/* -012 (2) */)
|
||||
{
|
||||
@@ -450,6 +466,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// (2b) TimedShelveAsync / UnshelveAsync end-to-end through the engine.
|
||||
/// <summary>Verifies that TimedShelveAsync shelves with a deadline and UnshelveAsync removes the shelve before the timer expires.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TimedShelveAsync_and_UnshelveAsync_round_trip(/* -012 (2) */)
|
||||
{
|
||||
@@ -478,6 +495,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// (2c) EnableAsync end-to-end through the engine.
|
||||
/// <summary>Verifies that EnableAsync transitions the alarm back to Enabled state and emits an Enabled event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EnableAsync_re_enables_after_disable(/* -012 (2) */)
|
||||
{
|
||||
@@ -501,6 +519,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// the engine or prevent subsequent alarm state transitions. The engine logs
|
||||
// the exception and continues operating; any later alarm changes still work.
|
||||
/// <summary>Verifies that an exception thrown by an OnEvent subscriber is isolated and does not crash the engine or prevent further state transitions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnEvent_subscriber_exception_does_not_crash_engine(/* -012 (3) */)
|
||||
{
|
||||
@@ -534,6 +553,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (4) IAlarmStateStore.SaveAsync failure — in-memory state must remain at the
|
||||
// prior value after finding -007 fix (persist-before-update).
|
||||
/// <summary>Verifies that a store SaveAsync failure leaves the in-memory alarm state at its prior value (persist-before-update invariant, finding -007).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Store_save_failure_leaves_in_memory_state_unchanged(/* -012 (4) */)
|
||||
{
|
||||
@@ -565,6 +585,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (5) Re-entrant LoadAsync — the old timer must not keep firing after a second
|
||||
// call (regression for finding -002: _shelvingTimer?.Dispose() fix).
|
||||
/// <summary>Verifies that a second LoadAsync call disposes the prior shelving timer so it does not keep firing after reload (regression for finding -002).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Second_LoadAsync_does_not_leak_old_timer(/* -012 (5) */)
|
||||
{
|
||||
@@ -598,6 +619,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (6) Cold-start AreInputsReady guard — null value, Bad status, and Uncertain
|
||||
// status inputs are all handled correctly.
|
||||
/// <summary>Verifies that AreInputsReady blocks predicate evaluation when inputs have null values or Bad status codes, while Uncertain quality is accepted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AreInputsReady_blocks_evaluation_for_null_and_bad_inputs(/* -012 (6) */)
|
||||
{
|
||||
@@ -635,6 +657,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// not deadlock against _evalGate. Both regressions are covered here.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that an OnEvent subscriber can call engine methods (e.g. AcknowledgeAsync) without deadlocking against the evaluation gate (regression for Core.ScriptedAlarms-003).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnEvent_subscriber_can_call_back_into_engine_without_deadlock(/* -003 */)
|
||||
{
|
||||
@@ -748,6 +771,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// to a (possibly disposed) store after the engine has returned.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that Dispose blocks until in-flight background re-evaluation tasks complete, preventing the engine from outliving its store (regression for Core.ScriptedAlarms-006).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_drains_in_flight_reevaluation_tasks(/* -006 */)
|
||||
{
|
||||
@@ -794,6 +818,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// explicitly. The two policies are documented in docs/ScriptedAlarms.md.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that Uncertain-quality inputs are accepted by the predicate but rendered as "{?}" in the operator-facing message template (Core.ScriptedAlarms-010).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Uncertain_quality_drives_predicate_but_renders_question_mark_in_message(/* -010 */)
|
||||
{
|
||||
@@ -842,6 +867,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// consumers).
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that the Comments collection is an ImmutableList, enabling O(log n) append and satisfying IReadOnlyList consumers (Core.ScriptedAlarms-008).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Comments_collection_uses_ImmutableList_for_efficient_append(/* -008 */)
|
||||
{
|
||||
@@ -907,29 +933,22 @@ public sealed class ScriptedAlarmEngineTests
|
||||
/// <summary>Gets or sets a value indicating whether the next SaveAsync call should throw a simulated failure.</summary>
|
||||
public bool FailSave { get; set; }
|
||||
|
||||
/// <summary>Loads an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to load.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<AlarmConditionState?> LoadAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.LoadAsync(alarmId, ct);
|
||||
|
||||
/// <summary>Loads all alarm condition states from the inner store.</summary>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AlarmConditionState>> LoadAllAsync(CancellationToken ct)
|
||||
=> _inner.LoadAllAsync(ct);
|
||||
|
||||
/// <summary>Saves an alarm condition state, optionally throwing if FailSave is set.</summary>
|
||||
/// <param name="state">The alarm condition state to save.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task SaveAsync(AlarmConditionState state, CancellationToken ct)
|
||||
{
|
||||
if (FailSave) throw new InvalidOperationException("Simulated store failure");
|
||||
return _inner.SaveAsync(state, ct);
|
||||
}
|
||||
|
||||
/// <summary>Removes an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to remove.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task RemoveAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.RemoveAsync(alarmId, ct);
|
||||
}
|
||||
@@ -946,20 +965,15 @@ public sealed class ScriptedAlarmEngineTests
|
||||
/// <summary>Gets a value indicating whether a SaveAsync call is currently blocked waiting on BlockNextSave.</summary>
|
||||
public bool SaveInProgress { get; private set; }
|
||||
|
||||
/// <summary>Loads an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to load.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<AlarmConditionState?> LoadAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.LoadAsync(alarmId, ct);
|
||||
|
||||
/// <summary>Loads all alarm condition states from the inner store.</summary>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AlarmConditionState>> LoadAllAsync(CancellationToken ct)
|
||||
=> _inner.LoadAllAsync(ct);
|
||||
|
||||
/// <summary>Saves an alarm condition state, optionally blocking on BlockNextSave gate.</summary>
|
||||
/// <param name="state">The alarm condition state to save.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(AlarmConditionState state, CancellationToken ct)
|
||||
{
|
||||
var gate = BlockNextSave;
|
||||
@@ -973,9 +987,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
await _inner.SaveAsync(state, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Removes an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to remove.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task RemoveAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.RemoveAsync(alarmId, ct);
|
||||
}
|
||||
@@ -983,6 +995,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// --- Core.ScriptedAlarms-009: per-alarm evaluation-scratch reuse ---
|
||||
|
||||
/// <summary>Verifies that re-evaluations reuse the same read cache dictionary instance instead of allocating a new one.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reevaluation_reuses_the_same_read_cache_dictionary()
|
||||
{
|
||||
@@ -1016,6 +1029,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that re-evaluations reuse the same predicate context instance across evaluations.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reevaluation_reuses_the_same_predicate_context()
|
||||
{
|
||||
@@ -1042,6 +1056,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadAsync clears prior evaluation scratch so new alarms use fresh scratch.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_drops_the_prior_generations_scratch()
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribing with an empty filter receives every alarm emission.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_with_empty_filter_receives_every_alarm_emission()
|
||||
{
|
||||
@@ -66,6 +67,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribing with an equipment prefix filters alarms by that prefix.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_with_equipment_prefix_filters_by_that_prefix()
|
||||
{
|
||||
@@ -89,6 +91,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribing stops further alarm events.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_stops_further_events()
|
||||
{
|
||||
@@ -108,6 +111,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync routes to the engine with a default user.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_routes_to_engine_with_default_user()
|
||||
{
|
||||
@@ -130,6 +134,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that null arguments are rejected.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Null_arguments_rejected()
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class CompiledScriptCacheTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cached evaluator produces correct results when executed.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Cached_evaluator_still_runs_correctly()
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script can compile, run, and read a seeded tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Happy_path_script_runs_and_reads_seeded_tag()
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SetVirtualTag records write operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetVirtualTag_records_the_write()
|
||||
{
|
||||
@@ -316,6 +318,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an allowed generic type argument still compiles.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Allowed_generic_type_argument_still_compiles()
|
||||
{
|
||||
@@ -331,6 +334,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that typeof an allowed type still compiles.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Allowed_typeof_still_compiles()
|
||||
{
|
||||
@@ -342,6 +346,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script exceptions propagate unwrapped.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Script_exception_propagates_unwrapped()
|
||||
{
|
||||
@@ -370,6 +375,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LINQ Enumerable is available from scripts.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Linq_Enumerable_is_available_from_scripts()
|
||||
{
|
||||
@@ -385,6 +391,7 @@ public sealed class ScriptSandboxTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DataValueSnapshot is usable in scripts.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DataValueSnapshot_is_usable_in_scripts()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
|
||||
public sealed class TimedScriptEvaluatorTests
|
||||
{
|
||||
/// <summary>Verifies that fast scripts complete under timeout and return value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Fast_script_completes_under_timeout_and_returns_value()
|
||||
{
|
||||
@@ -28,6 +29,7 @@ public sealed class TimedScriptEvaluatorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that scripts longer than timeout throw ScriptTimeoutException.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Script_longer_than_timeout_throws_ScriptTimeoutException()
|
||||
{
|
||||
@@ -50,6 +52,7 @@ public sealed class TimedScriptEvaluatorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that caller cancellation takes precedence over timeout.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Caller_cancellation_takes_precedence_over_timeout()
|
||||
{
|
||||
@@ -108,6 +111,7 @@ public sealed class TimedScriptEvaluatorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script exceptions propagate unwrapped.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Script_exception_propagates_unwrapped()
|
||||
{
|
||||
@@ -124,6 +128,7 @@ public sealed class TimedScriptEvaluatorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ScriptTimeoutException message points at diagnostic path.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ScriptTimeoutException_message_points_at_diagnostic_path()
|
||||
{
|
||||
@@ -143,6 +148,7 @@ public sealed class TimedScriptEvaluatorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that caller cancellation wins even when timeout fires first.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Caller_cancellation_wins_even_when_timeout_fires_first()
|
||||
{
|
||||
|
||||
@@ -10,10 +10,10 @@ public sealed class DriverHostTests
|
||||
{
|
||||
private sealed class StubDriver(string id, bool failInit = false) : IDriver
|
||||
{
|
||||
/// <summary>Gets the driver instance identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId { get; } = id;
|
||||
|
||||
/// <summary>Gets the driver type name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "Stub";
|
||||
|
||||
/// <summary>Gets a value indicating whether the driver has been initialized.</summary>
|
||||
@@ -22,9 +22,7 @@ public sealed class DriverHostTests
|
||||
/// <summary>Gets a value indicating whether the driver has been shut down.</summary>
|
||||
public bool ShutDown { get; private set; }
|
||||
|
||||
/// <summary>Initializes the driver asynchronously.</summary>
|
||||
/// <param name="_">Configuration data (unused in stub).</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(string _, CancellationToken ct)
|
||||
{
|
||||
if (failInit) throw new InvalidOperationException("boom");
|
||||
@@ -32,28 +30,25 @@ public sealed class DriverHostTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Reinitializes the driver asynchronously.</summary>
|
||||
/// <param name="_">Configuration data (unused in stub).</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ReinitializeAsync(string _, CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Shuts down the driver asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken ct) { ShutDown = true; return Task.CompletedTask; }
|
||||
|
||||
/// <summary>Gets the current health status of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() =>
|
||||
new(Initialized ? DriverState.Healthy : DriverState.Unknown, null, null);
|
||||
|
||||
/// <summary>Gets the memory footprint of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public long GetMemoryFootprint() => 0;
|
||||
|
||||
/// <summary>Flushes optional caches asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registering a driver initializes it and tracks its health.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Register_initializes_driver_and_tracks_health()
|
||||
{
|
||||
@@ -68,6 +63,7 @@ public sealed class DriverHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registration rethrows initialization failures but keeps the driver registered.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Register_rethrows_init_failure_but_keeps_driver_registered()
|
||||
{
|
||||
@@ -81,6 +77,7 @@ public sealed class DriverHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that duplicate driver registration throws an exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Duplicate_registration_throws()
|
||||
{
|
||||
@@ -92,6 +89,7 @@ public sealed class DriverHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unregistering a driver shuts it down and removes it.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unregister_shuts_down_and_removes()
|
||||
{
|
||||
@@ -113,6 +111,7 @@ public sealed class DriverHostTests
|
||||
/// The driver awaits an unsettled TaskCompletionSource so it does not introduce its
|
||||
/// own capture — only DriverHost's await of the returned Task can drive a post.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task RegisterAsync_Does_Not_Capture_SynchronizationContext()
|
||||
{
|
||||
@@ -137,6 +136,7 @@ public sealed class DriverHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UnregisterAsync does not capture the synchronization context.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnregisterAsync_Does_Not_Capture_SynchronizationContext()
|
||||
{
|
||||
@@ -165,6 +165,7 @@ public sealed class DriverHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DisposeAsync does not capture the synchronization context.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisposeAsync_Does_Not_Capture_SynchronizationContext()
|
||||
{
|
||||
@@ -225,34 +226,28 @@ public sealed class DriverHostTests
|
||||
/// <summary>Driver whose Initialize / Shutdown completions are caller-controlled via TCS.</summary>
|
||||
private sealed class TcsDriver(string id, TaskCompletionSource initTcs, TaskCompletionSource? shutdownTcs = null) : IDriver
|
||||
{
|
||||
/// <summary>Gets the driver instance identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId { get; } = id;
|
||||
|
||||
/// <summary>Gets the driver type name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "Tcs";
|
||||
|
||||
/// <summary>Initializes the driver asynchronously.</summary>
|
||||
/// <param name="_">Configuration data (unused in TCS driver).</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(string _, CancellationToken ct) => initTcs.Task;
|
||||
|
||||
/// <summary>Reinitializes the driver asynchronously.</summary>
|
||||
/// <param name="_">Configuration data (unused in TCS driver).</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ReinitializeAsync(string _, CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Shuts down the driver asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken ct) => (shutdownTcs ?? CompletedTcs).Task;
|
||||
|
||||
/// <summary>Gets the current health status of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, null, null);
|
||||
|
||||
/// <summary>Gets the memory footprint of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public long GetMemoryFootprint() => 0;
|
||||
|
||||
/// <summary>Flushes optional caches asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
private static readonly TaskCompletionSource CompletedTcs = MakeCompleted();
|
||||
@@ -271,7 +266,6 @@ public sealed class DriverHostTests
|
||||
public int PostCount;
|
||||
public int SendCount;
|
||||
|
||||
/// <summary>Posts a callback to the work queue.</summary>
|
||||
/// <inheritdoc />
|
||||
public override void Post(SendOrPostCallback d, object? state)
|
||||
{
|
||||
@@ -279,7 +273,6 @@ public sealed class DriverHostTests
|
||||
_queue.Enqueue(() => d(state));
|
||||
}
|
||||
|
||||
/// <summary>Sends a callback synchronously.</summary>
|
||||
/// <inheritdoc />
|
||||
public override void Send(SendOrPostCallback d, object? state)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
/// This is the plumbing that PR 16's concrete OPC UA builder will use to update the actual
|
||||
/// AlarmConditionState nodes.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Alarm_events_are_routed_to_the_sink_registered_for_the_matching_source_node_id()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-alarm variables do not register sinks in the alarm tracker.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Non_alarm_variables_do_not_register_sinks()
|
||||
{
|
||||
@@ -59,6 +61,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm events with unknown source node IDs are silently dropped.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unknown_source_node_id_is_dropped_silently()
|
||||
{
|
||||
@@ -74,6 +77,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the node manager unsubscribes from alarm events.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_unsubscribes_from_OnAlarmEvent()
|
||||
{
|
||||
@@ -96,6 +100,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
/// must unsubscribe the old alarm forwarder and clear the sink registry before re-walking,
|
||||
/// so alarm transitions are not delivered twice.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Second_BuildAddressSpaceAsync_Does_Not_Double_Fire_Alarms()
|
||||
{
|
||||
@@ -121,6 +126,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a second call to BuildAddressSpaceAsync clears the old sink registry.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Second_BuildAddressSpaceAsync_Clears_Old_Sink_Registry()
|
||||
{
|
||||
@@ -137,6 +143,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that calling BuildAddressSpaceAsync after disposal throws ObjectDisposedException.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task BuildAddressSpaceAsync_After_Dispose_Throws_ObjectDisposedException()
|
||||
{
|
||||
@@ -154,6 +161,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
/// out of <c>BuildAddressSpaceAsync</c> unhandled so the Server layer's per-driver try/catch
|
||||
/// (<c>OpcUaApplicationHost.PopulateAddressSpaces</c>) can mark the subtree Faulted.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task BuildAddressSpaceAsync_Propagates_Discovery_Exceptions_To_Caller()
|
||||
{
|
||||
@@ -169,33 +177,25 @@ public sealed class GenericDriverNodeManagerTests
|
||||
/// <summary>Driver whose DiscoverAsync throws — exercises the exception-isolation boundary.</summary>
|
||||
private sealed class ThrowingDiscoveryDriver : IDriver, ITagDiscovery
|
||||
{
|
||||
/// <summary>Gets the driver instance identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId => "throwing";
|
||||
/// <summary>Gets the driver type name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "Throwing";
|
||||
|
||||
/// <summary>Initializes the driver with configuration.</summary>
|
||||
/// <param name="_">Configuration JSON (unused in test double).</param>
|
||||
/// <param name="__">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(string _, CancellationToken __) => Task.CompletedTask;
|
||||
/// <summary>Reinitializes the driver with new configuration.</summary>
|
||||
/// <param name="_">Configuration JSON (unused in test double).</param>
|
||||
/// <param name="__">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task ReinitializeAsync(string _, CancellationToken __) => Task.CompletedTask;
|
||||
/// <summary>Shuts down the driver.</summary>
|
||||
/// <param name="_">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken _) => Task.CompletedTask;
|
||||
/// <summary>Gets the current health status of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, null, null);
|
||||
/// <summary>Gets the memory footprint of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public long GetMemoryFootprint() => 0;
|
||||
/// <summary>Flushes optional caches in the driver.</summary>
|
||||
/// <param name="_">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task FlushOptionalCachesAsync(CancellationToken _) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Discovers the address space by throwing an exception.</summary>
|
||||
/// <param name="builder">The builder used to construct the address space.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
|
||||
=> throw new InvalidOperationException("discovery boom");
|
||||
}
|
||||
@@ -204,35 +204,27 @@ public sealed class GenericDriverNodeManagerTests
|
||||
|
||||
private sealed class FakeDriver : IDriver, ITagDiscovery, IAlarmSource
|
||||
{
|
||||
/// <summary>Gets the driver instance identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId => "fake";
|
||||
/// <summary>Gets the driver type name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "Fake";
|
||||
/// <summary>Occurs when an alarm event is raised.</summary>
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
|
||||
/// <summary>Initializes the driver with configuration.</summary>
|
||||
/// <param name="driverConfigJson">Configuration JSON.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
|
||||
/// <summary>Reinitializes the driver with new configuration.</summary>
|
||||
/// <param name="driverConfigJson">Configuration JSON.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ReinitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
|
||||
/// <summary>Shuts down the driver.</summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
/// <summary>Gets the current health status of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
/// <summary>Gets the memory footprint of the driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public long GetMemoryFootprint() => 0;
|
||||
/// <summary>Flushes optional caches in the driver.</summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Discovers the address space and registers alarm conditions.</summary>
|
||||
/// <param name="builder">The builder used to construct the address space.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
|
||||
{
|
||||
var folder = builder.Folder("Tank", "Tank");
|
||||
@@ -253,25 +245,19 @@ public sealed class GenericDriverNodeManagerTests
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
public void RaiseAlarm(AlarmEventArgs args) => OnAlarmEvent?.Invoke(this, args);
|
||||
|
||||
/// <summary>Subscribes to alarm events.</summary>
|
||||
/// <param name="_">Tag references to subscribe to (unused in test double).</param>
|
||||
/// <param name="__">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(IReadOnlyList<string> _, CancellationToken __)
|
||||
=> Task.FromResult<IAlarmSubscriptionHandle>(new FakeHandle("sub"));
|
||||
/// <summary>Unsubscribes from alarm events.</summary>
|
||||
/// <param name="_">The subscription handle (unused in test double).</param>
|
||||
/// <param name="__">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle _, CancellationToken __) => Task.CompletedTask;
|
||||
/// <summary>Acknowledges alarm notifications.</summary>
|
||||
/// <param name="_">Alarm acknowledgement requests (unused in test double).</param>
|
||||
/// <param name="__">Cancellation token (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public Task AcknowledgeAsync(IReadOnlyList<AlarmAcknowledgeRequest> _, CancellationToken __) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Test double for IAlarmSubscriptionHandle.</summary>
|
||||
private sealed class FakeHandle(string diagnosticId) : IAlarmSubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this subscription.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId { get; } = diagnosticId;
|
||||
}
|
||||
|
||||
@@ -281,31 +267,22 @@ public sealed class GenericDriverNodeManagerTests
|
||||
/// <summary>Gets the map of alarm sources to their sinks.</summary>
|
||||
public Dictionary<string, RecordingSink> Alarms { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Creates a folder in the address space.</summary>
|
||||
/// <param name="_">The contained name (unused in test double).</param>
|
||||
/// <param name="__">The display name (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string _, string __) => this;
|
||||
|
||||
/// <summary>Creates a variable in the address space.</summary>
|
||||
/// <param name="_">The contained name (unused in test double).</param>
|
||||
/// <param name="__">The display name (unused in test double).</param>
|
||||
/// <param name="info">The driver attribute information.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string _, string __, DriverAttributeInfo info)
|
||||
=> new Handle(info.FullName, Alarms);
|
||||
|
||||
/// <summary>Adds a property to the current variable.</summary>
|
||||
/// <param name="_">The property name (unused in test double).</param>
|
||||
/// <param name="__">The data type (unused in test double).</param>
|
||||
/// <param name="___">The initial value (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
/// <summary>Test double for IVariableHandle.</summary>
|
||||
public sealed class Handle(string fullRef, Dictionary<string, RecordingSink> alarms) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference name for this variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference { get; } = fullRef;
|
||||
/// <summary>Marks this variable as an alarm condition and registers its sink.</summary>
|
||||
/// <param name="_">The alarm condition info (unused in test double).</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo _)
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
@@ -319,8 +296,7 @@ public sealed class GenericDriverNodeManagerTests
|
||||
{
|
||||
/// <summary>Gets the list of alarm transitions received by this sink.</summary>
|
||||
public List<AlarmEventArgs> Received { get; } = new();
|
||||
/// <summary>Records an alarm transition.</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) => Received.Add(args);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Observability;
|
||||
public sealed class CapabilityInvokerEnrichmentTests
|
||||
{
|
||||
/// <summary>Verifies that InvokerExecute logs inside call site with structured properties.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task InvokerExecute_LogsInsideCallSite_CarryStructuredProperties()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class CapabilityInvokerEnrichmentTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InvokerExecute does not leak context outside the call site.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task InvokerExecute_DoesNotLeak_ContextOutsideCallSite()
|
||||
{
|
||||
|
||||
@@ -377,9 +377,7 @@ public sealed class EquipmentNodeWalkerTests
|
||||
/// <summary>Gets the list of properties.</summary>
|
||||
public List<RecordingProperty> Properties { get; } = new();
|
||||
|
||||
/// <summary>Creates a folder child node.</summary>
|
||||
/// <param name="name">The browse name of the folder.</param>
|
||||
/// <param name="_">The display name (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string name, string _)
|
||||
{
|
||||
var child = new RecordingBuilder(name);
|
||||
@@ -387,10 +385,7 @@ public sealed class EquipmentNodeWalkerTests
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>Creates a variable node.</summary>
|
||||
/// <param name="name">The browse name of the variable.</param>
|
||||
/// <param name="_">The display name (unused).</param>
|
||||
/// <param name="attr">The attribute information for the variable.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string name, string _, DriverAttributeInfo attr)
|
||||
{
|
||||
var v = new RecordingVariable(name, attr);
|
||||
@@ -398,10 +393,7 @@ public sealed class EquipmentNodeWalkerTests
|
||||
return v;
|
||||
}
|
||||
|
||||
/// <summary>Adds a property to the node.</summary>
|
||||
/// <param name="name">The browse name of the property.</param>
|
||||
/// <param name="_">The data type (unused).</param>
|
||||
/// <param name="value">The value of the property.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string name, DriverDataType _, object? value) =>
|
||||
Properties.Add(new RecordingProperty(name, value));
|
||||
}
|
||||
@@ -412,10 +404,9 @@ public sealed class EquipmentNodeWalkerTests
|
||||
/// <summary>Recorded variable for test verification.</summary>
|
||||
private sealed record RecordingVariable(string BrowseName, DriverAttributeInfo AttributeInfo) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference of the variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => AttributeInfo.FullName;
|
||||
/// <summary>Marks the variable as an alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,26 +18,18 @@ public sealed class IdentificationFolderBuilderTests
|
||||
/// <summary>Gets or sets the list of added properties.</summary>
|
||||
public List<(string BrowseName, DriverDataType DataType, object? Value)> Properties { get; } = [];
|
||||
|
||||
/// <summary>Records a folder and returns this builder for chaining.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{
|
||||
Folders.Add((browseName, displayName));
|
||||
return this; // flat recording — identification fields land in the same bucket
|
||||
}
|
||||
|
||||
/// <summary>Not supported in test context.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="attributeInfo">The attribute information.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
=> throw new NotSupportedException("Identification fields use AddProperty, not Variable");
|
||||
|
||||
/// <summary>Records a property addition.</summary>
|
||||
/// <param name="browseName">The browse name of the property.</param>
|
||||
/// <param name="dataType">The data type of the property.</param>
|
||||
/// <param name="value">The property value.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value)
|
||||
=> Properties.Add((browseName, dataType, value));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
private static readonly DriverResilienceOptions TierAOptions = new() { Tier = DriverTier.A };
|
||||
|
||||
/// <summary>Verifies SubscribeAsync on an empty list returns empty without calling the driver.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_EmptyList_ReturnsEmpty_WithoutDriverCall()
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies SubscribeAsync with no resolver routes through the default host.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_SingleHost_RoutesThroughDefaultHost()
|
||||
{
|
||||
@@ -38,6 +40,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies SubscribeAsync fans out correctly to multiple hosts based on resolver.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_MultiHost_FansOutByResolvedHost()
|
||||
{
|
||||
@@ -57,6 +60,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeAsync does not retry on failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_DoesNotRetry_OnFailure()
|
||||
{
|
||||
@@ -70,6 +74,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies SubscribeAsync retries on transient failures.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_Retries_Transient_Failures()
|
||||
{
|
||||
@@ -87,6 +92,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
/// Verify by using a per-call resolver with two distinct hosts and checking which host
|
||||
/// name reaches the driver's UnsubscribeAlarmsAsync.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_Routes_Through_Same_Host_As_Subscribe()
|
||||
{
|
||||
@@ -112,6 +118,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies UnsubscribeAsync with no resolver uses the default host.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_SingleHost_UsesDefaultHost()
|
||||
{
|
||||
@@ -158,10 +165,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
/// <summary>Gets the source node IDs from the most recent SubscribeAlarmsAsync call.</summary>
|
||||
public IReadOnlyList<string> LastSubscribedIds { get; private set; } = [];
|
||||
|
||||
/// <summary>Subscribes to alarms.</summary>
|
||||
/// <param name="sourceNodeIds">The source node IDs to subscribe to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An alarm subscription handle.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -172,20 +176,14 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
return Task.FromResult<IAlarmSubscriptionHandle>(new StubHandle($"h-{SubscribeCallCount}"));
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from alarms.</summary>
|
||||
/// <param name="handle">The subscription handle to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
UnsubscribeCallCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Acknowledges alarms.</summary>
|
||||
/// <param name="acknowledgements">The alarm acknowledgements to process.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -194,7 +192,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Occurs when an alarm event is raised.</summary>
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent { add { } remove { } }
|
||||
}
|
||||
|
||||
@@ -206,9 +204,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
/// <param name="map">The map of source node IDs to host names.</param>
|
||||
private sealed class StubResolver(Dictionary<string, string> map) : IPerCallHostResolver
|
||||
{
|
||||
/// <summary>Resolves the host for the given full reference.</summary>
|
||||
/// <param name="fullReference">The full reference to resolve.</param>
|
||||
/// <returns>The resolved host name.</returns>
|
||||
/// <inheritdoc />
|
||||
public string ResolveHost(string fullReference) => map[fullReference];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class CapabilityInvokerTests
|
||||
new(builder, "drv-test", () => options);
|
||||
|
||||
/// <summary>Verifies that the capability invoker returns the value from the call site.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_ReturnsValue_FromCallSite()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public sealed class CapabilityInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the capability invoker retries on transient failures.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Retries_OnTransientFailure()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public sealed class CapabilityInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-idempotent writes do not retry even when the policy has retries configured.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_NonIdempotent_DoesNotRetry_EvenWhenPolicyHasRetries()
|
||||
{
|
||||
@@ -85,6 +88,7 @@ public sealed class CapabilityInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that idempotent writes retry when the policy has retries configured.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Idempotent_Retries_WhenPolicyHasRetries()
|
||||
{
|
||||
@@ -116,6 +120,7 @@ public sealed class CapabilityInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writes do not retry when the policy has zero retries configured.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Default_DoesNotRetry_WhenPolicyHasZeroRetries()
|
||||
{
|
||||
@@ -143,6 +148,7 @@ public sealed class CapabilityInvokerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different hosts are honored independently in the resilience pipeline.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Execute_HonorsDifferentHosts_Independently()
|
||||
{
|
||||
@@ -161,6 +167,7 @@ public sealed class CapabilityInvokerTests
|
||||
/// redundant options objects on the per-write hot path and creates a consistency hazard
|
||||
/// where an Admin edit mid-call could observe two different snapshots.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteWriteAsync_NonIdempotent_Snapshots_Options_Once_Per_Call()
|
||||
{
|
||||
@@ -195,6 +202,7 @@ public sealed class CapabilityInvokerTests
|
||||
/// two derived values (<c>with</c> base + <c>Resolve(Write)</c>) come from the same options
|
||||
/// instance.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteWriteAsync_NonIdempotent_Uses_Consistent_Options_Snapshot()
|
||||
{
|
||||
|
||||
+10
@@ -13,6 +13,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
private static readonly DriverResilienceOptions TierAOptions = new() { Tier = DriverTier.A };
|
||||
|
||||
/// <summary>Verifies that read operations retry transient failures.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Retries_Transient_Failures()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write operations do not retry on failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_DoesNotRetry_OnFailure()
|
||||
{
|
||||
@@ -53,6 +55,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm acknowledge operations do not retry on failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AlarmAcknowledge_DoesNotRetry_OnFailure()
|
||||
{
|
||||
@@ -115,6 +118,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dead host does not open the breaker for a sibling host.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeadHost_DoesNotOpenBreaker_ForSiblingHost()
|
||||
{
|
||||
@@ -146,6 +150,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the circuit breaker opens after the failure threshold on tier A.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task CircuitBreaker_Opens_AfterFailureThreshold_OnTierA()
|
||||
{
|
||||
@@ -171,6 +176,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timeout cancels slow operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Timeout_Cancels_SlowOperation()
|
||||
{
|
||||
@@ -211,6 +217,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation is not retried.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_IsNot_Retried()
|
||||
{
|
||||
@@ -232,6 +239,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the tracker records failure on every retry.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tracker_RecordsFailure_OnEveryRetry()
|
||||
{
|
||||
@@ -253,6 +261,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the tracker stamps the breaker open when it trips.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tracker_StampsBreakerOpen_WhenBreakerTrips()
|
||||
{
|
||||
@@ -277,6 +286,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the tracker isolates counters per host.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tracker_IsolatesCounters_PerHost()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
|
||||
public sealed class FlakeyDriverIntegrationTests
|
||||
{
|
||||
/// <summary>Verifies read succeeds after transient failures with retries.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_SurfacesSuccess_AfterTransientFailures()
|
||||
{
|
||||
@@ -43,6 +44,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies non-idempotent write fails on first failure without replay.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_NonIdempotent_FailsOnFirstFailure_NoReplay()
|
||||
{
|
||||
@@ -68,6 +70,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies idempotent write retries until success.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Idempotent_RetriesUntilSuccess()
|
||||
{
|
||||
@@ -93,6 +96,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies multiple hosts have independent failure counts and circuit breakers.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task MultipleHosts_OnOneDriver_HaveIndependentFailureCounts()
|
||||
{
|
||||
@@ -141,10 +145,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
_failWritesBeforeIndex = failWritesBeforeIndex;
|
||||
}
|
||||
|
||||
/// <summary>Reads values, failing transiently until the threshold.</summary>
|
||||
/// <param name="fullReferences">Full references to read.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Data value snapshots.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -160,10 +161,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>Writes values, failing transiently until the threshold.</summary>
|
||||
/// <param name="writes">The write requests.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Write results.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
@@ -69,6 +69,7 @@ public sealed class InFlightCounterTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CapabilityInvoker increments the tracker during execution.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task CapabilityInvoker_IncrementsTracker_DuringExecution()
|
||||
{
|
||||
@@ -97,6 +98,7 @@ public sealed class InFlightCounterTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CapabilityInvoker decrements the counter on exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task CapabilityInvoker_ExceptionPath_DecrementsCounter()
|
||||
{
|
||||
@@ -119,6 +121,7 @@ public sealed class InFlightCounterTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CapabilityInvoker without a tracker does not throw.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task CapabilityInvoker_WithoutTracker_DoesNotThrow()
|
||||
{
|
||||
|
||||
+4
-2
@@ -22,13 +22,13 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
/// <param name="map">The mapping of full references to host names.</param>
|
||||
public StaticResolver(Dictionary<string, string> map) => _map = map;
|
||||
|
||||
/// <summary>Resolves a host name from the static mapping.</summary>
|
||||
/// <param name="fullReference">The full reference to resolve.</param>
|
||||
/// <inheritdoc />
|
||||
public string ResolveHost(string fullReference) =>
|
||||
_map.TryGetValue(fullReference, out var host) ? host : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dead PLC does not open the breaker for healthy PLCs when using a per-call resolver.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeadPlc_DoesNotOpenBreaker_For_HealthyPlc_With_Resolver()
|
||||
{
|
||||
@@ -80,6 +80,7 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that without a resolver, the same host shares one resilience pipeline.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task WithoutResolver_SameHost_Shares_One_Pipeline()
|
||||
{
|
||||
@@ -98,6 +99,7 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with a resolver, different hosts get separate resilience pipelines.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task WithResolver_TwoHosts_Get_Two_Pipelines()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Stability;
|
||||
public sealed class MemoryRecycleTests
|
||||
{
|
||||
/// <summary>Verifies that Tier C hard memory breach requests supervisor recycle.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TierC_HardBreach_RequestsSupervisorRecycle()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ public sealed class MemoryRecycleTests
|
||||
|
||||
/// <summary>Verifies that Tier A and B hard memory breach never request recycle.</summary>
|
||||
/// <param name="tier">The driver tier to test.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(DriverTier.A)]
|
||||
[InlineData(DriverTier.B)]
|
||||
@@ -40,6 +42,7 @@ public sealed class MemoryRecycleTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Tier C without supervisor hard breach is a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TierC_WithoutSupervisor_HardBreach_NoOp()
|
||||
{
|
||||
@@ -52,6 +55,7 @@ public sealed class MemoryRecycleTests
|
||||
|
||||
/// <summary>Verifies that soft memory breach never requests recycle at any tier.</summary>
|
||||
/// <param name="tier">The driver tier to test.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(DriverTier.A)]
|
||||
[InlineData(DriverTier.B)]
|
||||
@@ -69,6 +73,7 @@ public sealed class MemoryRecycleTests
|
||||
|
||||
/// <summary>Verifies that non-breach memory actions are no-ops.</summary>
|
||||
/// <param name="action">The non-breach memory tracking action to test.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(MemoryTrackingAction.None)]
|
||||
[InlineData(MemoryTrackingAction.Warming)]
|
||||
@@ -85,16 +90,14 @@ public sealed class MemoryRecycleTests
|
||||
|
||||
private sealed class FakeSupervisor : IDriverSupervisor
|
||||
{
|
||||
/// <summary>Gets the driver instance identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId => "fake-tier-c";
|
||||
/// <summary>Gets the count of recycle operations.</summary>
|
||||
public int RecycleCount { get; private set; }
|
||||
/// <summary>Gets the reason from the last recycle operation.</summary>
|
||||
public string? LastReason { get; private set; }
|
||||
|
||||
/// <summary>Recycles the driver asynchronously.</summary>
|
||||
/// <param name="reason">The reason for recycling.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task RecycleAsync(string reason, CancellationToken cancellationToken)
|
||||
{
|
||||
RecycleCount++;
|
||||
|
||||
@@ -36,6 +36,7 @@ public sealed class ScheduledRecycleSchedulerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies Tick before the next recycle time is a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tick_BeforeNextRecycle_NoOp()
|
||||
{
|
||||
@@ -49,6 +50,7 @@ public sealed class ScheduledRecycleSchedulerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies Tick at or after the next recycle time fires once and advances.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tick_AtOrAfterNextRecycle_FiresOnce_AndAdvances()
|
||||
{
|
||||
@@ -63,6 +65,7 @@ public sealed class ScheduledRecycleSchedulerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies RequestRecycleNow fires immediately without advancing the schedule.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestRecycleNow_Fires_Immediately_WithoutAdvancingSchedule()
|
||||
{
|
||||
@@ -78,6 +81,7 @@ public sealed class ScheduledRecycleSchedulerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies multiple ticks across the recycle interval each advance by one interval.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MultipleFires_AcrossTicks_AdvanceOneIntervalEach()
|
||||
{
|
||||
@@ -95,7 +99,7 @@ public sealed class ScheduledRecycleSchedulerTests
|
||||
/// <summary>Fake driver supervisor for testing.</summary>
|
||||
private sealed class FakeSupervisor : IDriverSupervisor
|
||||
{
|
||||
/// <summary>Gets the driver instance ID.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId => "tier-c-fake";
|
||||
|
||||
/// <summary>Gets the number of times RecycleAsync was called.</summary>
|
||||
@@ -104,10 +108,7 @@ public sealed class ScheduledRecycleSchedulerTests
|
||||
/// <summary>Gets the reason from the most recent recycle call.</summary>
|
||||
public string? LastReason { get; private set; }
|
||||
|
||||
/// <summary>Simulates a driver recycle operation.</summary>
|
||||
/// <param name="reason">The reason for the recycle.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task RecycleAsync(string reason, CancellationToken cancellationToken)
|
||||
{
|
||||
RecycleCount++;
|
||||
|
||||
@@ -42,16 +42,13 @@ public sealed class FakeUpstream : ITagUpstreamSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads the current value of a tag.</summary>
|
||||
/// <param name="path">The path to the tag.</param>
|
||||
/// <inheritdoc />
|
||||
public DataValueSnapshot ReadTag(string path)
|
||||
=> _values.TryGetValue(path, out var v)
|
||||
? v
|
||||
: new DataValueSnapshot(null, 0x80340000u, null, DateTime.UtcNow);
|
||||
|
||||
/// <summary>Subscribes to tag value changes.</summary>
|
||||
/// <param name="path">The path to the tag.</param>
|
||||
/// <param name="observer">The callback to invoke when the tag value changes.</param>
|
||||
/// <inheritdoc />
|
||||
public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
|
||||
{
|
||||
var list = _subs.GetOrAdd(path, _ => []);
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests;
|
||||
public sealed class TimerTriggerSchedulerTests
|
||||
{
|
||||
/// <summary>Verifies that timer interval causes periodic reevaluation of virtual tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Timer_interval_causes_periodic_reevaluation()
|
||||
{
|
||||
@@ -46,6 +47,7 @@ public sealed class TimerTriggerSchedulerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tags without TimerInterval are not scheduled.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tags_without_TimerInterval_not_scheduled()
|
||||
{
|
||||
@@ -98,6 +100,7 @@ public sealed class TimerTriggerSchedulerTests
|
||||
// ----- Core.VirtualTags-007: timer ticks must not block pool threads and must skip when prior tick is still running -----
|
||||
|
||||
/// <summary>Verifies that tick is skipped when the prior tick for the same group is still running.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tick_skips_when_prior_tick_for_the_same_group_is_still_running()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a simple virtual tag script can read an upstream tag and return a coerced value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Simple_script_reads_upstream_and_returns_coerced_value()
|
||||
{
|
||||
@@ -52,6 +53,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an upstream tag change triggers a cascade re-evaluation through two levels of dependent virtual tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Upstream_change_triggers_cascade_through_two_levels()
|
||||
{
|
||||
@@ -84,6 +86,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a circular dependency among virtual tags is rejected at load time.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cycle_in_virtual_tags_rejected_at_Load()
|
||||
{
|
||||
@@ -98,6 +101,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script compilation errors surface at load time with all failures aggregated.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Script_compile_error_surfaces_at_Load_with_all_failures()
|
||||
{
|
||||
@@ -116,6 +120,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a runtime exception in one virtual tag's script is isolated and does not affect other tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Script_runtime_exception_isolates_to_owning_tag()
|
||||
{
|
||||
@@ -138,6 +143,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script timeout is mapped to BadInternalError status without killing the engine.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Timeout_maps_to_BadInternalError_without_killing_the_engine()
|
||||
{
|
||||
@@ -159,6 +165,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribers receive notifications when the engine emits value changes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribers_receive_engine_emitted_changes()
|
||||
{
|
||||
@@ -180,6 +187,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the historize flag routes virtual tag values to the history writer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Historize_flag_routes_to_history_writer()
|
||||
{
|
||||
@@ -202,6 +210,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that upstream pushes are ignored when change-driven is false.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Change_driven_false_ignores_upstream_push()
|
||||
{
|
||||
@@ -224,6 +233,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reloading the engine replaces existing tags and resubscribes to upstream sources cleanly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reload_replaces_existing_tags_and_resubscribes_cleanly()
|
||||
{
|
||||
@@ -248,6 +258,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the engine releases all upstream subscriptions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_releases_upstream_subscriptions()
|
||||
{
|
||||
@@ -264,6 +275,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SetVirtualTag called within a script updates the target and triggers observers.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetVirtualTag_within_script_updates_target_and_triggers_observers()
|
||||
{
|
||||
@@ -288,6 +300,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SetVirtualTag within a script cascades to change-triggered dependents of the written tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetVirtualTag_within_script_cascades_to_dependents_of_the_written_tag()
|
||||
{
|
||||
@@ -321,6 +334,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that type coercion from script double to configured int32 works correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Type_coercion_from_script_double_to_config_int32()
|
||||
{
|
||||
@@ -339,6 +353,7 @@ public sealed class VirtualTagEngineTests
|
||||
// ----- Core.VirtualTags-012: previously-missing coverage -----
|
||||
|
||||
/// <summary>Verifies that the AreInputsReady guard publishes BadWaitingForInitialData when upstream tags have bad status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AreInputsReady_guard_publishes_BadWaitingForInitialData_when_upstream_is_bad()
|
||||
{
|
||||
@@ -361,6 +376,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the AreInputsReady guard recovers when an upstream tag transitions from bad to good status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AreInputsReady_guard_publishes_BadWaitingForInitialData_then_recovers_when_upstream_becomes_good()
|
||||
{
|
||||
@@ -386,6 +402,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SetVirtualTag cascades to change-triggered dependents.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetVirtualTag_cascades_to_change_triggered_dependent()
|
||||
{
|
||||
@@ -417,6 +434,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SetVirtualTag calls targeting unregistered paths are caught at load time.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetVirtualTag_on_non_registered_path_is_caught_at_Load()
|
||||
{
|
||||
@@ -441,6 +459,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that EvaluateOneAsync throws ArgumentException when called for an unregistered path.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EvaluateOneAsync_throws_ArgumentException_for_unregistered_path()
|
||||
{
|
||||
@@ -453,6 +472,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a type coercion failure maps to BadInternalError status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CoerceResult_failure_maps_to_BadInternalError()
|
||||
{
|
||||
@@ -475,6 +495,7 @@ public sealed class VirtualTagEngineTests
|
||||
// ----- Core.VirtualTags-011: Writes target validation at Load time -----
|
||||
|
||||
/// <summary>Verifies that Load rejects scripts that write to unregistered virtual tag paths.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Load_rejects_script_writing_to_unregistered_virtual_tag_path()
|
||||
{
|
||||
@@ -499,6 +520,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Load accepts scripts that write to registered virtual tag paths.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Load_accepts_script_writing_to_registered_virtual_tag_path()
|
||||
{
|
||||
@@ -559,6 +581,7 @@ public sealed class VirtualTagEngineTests
|
||||
// ----- Core.VirtualTags-004: CoerceResult default arm leaks uncoerced values -----
|
||||
|
||||
/// <summary>Verifies that CoerceResult correctly handles Int16, UInt16, UInt32, and UInt64 types.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CoerceResult_handles_Int16_UInt16_UInt32_UInt64()
|
||||
{
|
||||
@@ -588,6 +611,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Load rejects virtual tag definitions with unsupported DriverDataType values.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Load_rejects_definition_with_unsupported_DriverDataType()
|
||||
{
|
||||
@@ -608,6 +632,7 @@ public sealed class VirtualTagEngineTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Load rejects duplicate virtual tag paths with an aggregated error message.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Load_rejects_duplicate_path_with_aggregated_error()
|
||||
{
|
||||
@@ -684,9 +709,7 @@ public sealed class VirtualTagEngineTests
|
||||
/// <param name="buf">The list to store recorded history entries.</param>
|
||||
public TestHistory(List<(string, DataValueSnapshot)> buf) => _buf = buf;
|
||||
|
||||
/// <summary>Records a virtual tag path and value snapshot to the history buffer.</summary>
|
||||
/// <param name="path">The virtual tag path.</param>
|
||||
/// <param name="value">The data value snapshot to record.</param>
|
||||
/// <inheritdoc />
|
||||
public void Record(string path, DataValueSnapshot value)
|
||||
{
|
||||
lock (_buf) { _buf.Add((path, value)); }
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed class VirtualTagSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync returns cached engine values.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_returns_engine_cached_values()
|
||||
{
|
||||
@@ -42,6 +43,7 @@ public sealed class VirtualTagSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown paths return Bad status quality.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_unknown_path_returns_Bad_quality()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public sealed class VirtualTagSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribe fires the initial data callback immediately.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_fires_initial_data_callback()
|
||||
{
|
||||
@@ -75,6 +78,7 @@ public sealed class VirtualTagSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscription fires on upstream changes via engine cascade.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_fires_on_upstream_change_via_engine_cascade()
|
||||
{
|
||||
@@ -101,6 +105,7 @@ public sealed class VirtualTagSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribe stops further event emissions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_stops_further_events()
|
||||
{
|
||||
@@ -123,6 +128,7 @@ public sealed class VirtualTagSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that null arguments are rejected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Null_arguments_rejected()
|
||||
{
|
||||
|
||||
+3
@@ -19,6 +19,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests;
|
||||
public sealed class CommandCancellationTests
|
||||
{
|
||||
/// <summary>Verifies that probe command gracefully handles cancellation during initialization.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task ProbeCommand_swallows_cancellation_during_initialize()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public sealed class CommandCancellationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read command gracefully handles cancellation during initialization.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task ReadCommand_swallows_cancellation_during_initialize()
|
||||
{
|
||||
@@ -49,6 +51,7 @@ public sealed class CommandCancellationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write command gracefully handles cancellation during initialization.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task WriteCommand_swallows_cancellation_during_initialize()
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed class ModbusCommandBaseTests
|
||||
|
||||
/// <summary>Invokes BuildOptions with the given tags.</summary>
|
||||
/// <param name="tags">The list of tag definitions to build options for.</param>
|
||||
/// <returns>The <see cref="ModbusDriverOptions"/> produced by <see cref="ModbusCommandBase.BuildOptions"/>.</returns>
|
||||
public ModbusDriverOptions Invoke(IReadOnlyList<ModbusTagDefinition> tags) => BuildOptions(tags);
|
||||
|
||||
/// <summary>Invokes ValidateEndpoint.</summary>
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ public sealed class WriteCommandRegionValidationTests
|
||||
/// <param name="region">The read-only Modbus region to attempt a write against.</param>
|
||||
/// <param name="type">The data type used in the write attempt.</param>
|
||||
/// <param name="value">The raw string value supplied to the write command.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(ModbusRegion.DiscreteInputs, ModbusDataType.Bool, "0")]
|
||||
[InlineData(ModbusRegion.InputRegisters, ModbusDataType.UInt16, "1")]
|
||||
@@ -44,6 +45,7 @@ public sealed class WriteCommandRegionValidationTests
|
||||
|
||||
/// <summary>Verifies that Coils region requires Bool data type (Driver.Modbus.Cli-002).</summary>
|
||||
/// <param name="type">The non-Bool data type that should be rejected for the Coils region.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(ModbusDataType.UInt16)]
|
||||
[InlineData(ModbusDataType.Int16)]
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class AbCipReadSmokeTests
|
||||
|
||||
/// <summary>Verifies that the driver can read a seeded DInt value from an AB server.</summary>
|
||||
/// <param name="profile">The AB server profile to test against.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[AbServerTheory]
|
||||
[MemberData(nameof(Profiles))]
|
||||
public async Task Driver_reads_seeded_DInt_from_ab_server(AbServerProfile profile)
|
||||
|
||||
@@ -55,9 +55,11 @@ public sealed class AbServerFixture : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Initializes the fixture asynchronously (no-op for this fixture).</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
/// <inheritdoc />
|
||||
/// <summary>Disposes the fixture asynchronously (no-op for this fixture).</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
@@ -65,6 +67,7 @@ public sealed class AbServerFixture : IAsyncLifetime
|
||||
/// <see cref="AbServerFactAttribute"/> / <see cref="AbServerTheoryAttribute"/>
|
||||
/// to decide whether to skip tests on a fresh clone without a running container.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the server is reachable; <see langword="false"/> otherwise.</returns>
|
||||
public static bool IsServerAvailable() =>
|
||||
TcpProbe(ResolveHost(), ResolvePort());
|
||||
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;
|
||||
public sealed class AbCipEmulateAlmdTests
|
||||
{
|
||||
/// <summary>Verifies that real ALMD raise fires OnAlarmEvent through the driver projection.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[AbServerFact]
|
||||
public async Task Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection()
|
||||
{
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;
|
||||
public sealed class AbCipEmulateUdtReadTests
|
||||
{
|
||||
/// <summary>Verifies that reading a whole UDT decodes each member at its template object offset.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[AbServerFact]
|
||||
public async Task WholeUdt_read_decodes_each_member_at_its_Template_Object_offset()
|
||||
{
|
||||
|
||||
@@ -49,6 +49,7 @@ public sealed class AbCipAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disabled alarm projection returns a valid handle but does not poll.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task FeatureFlag_Off_SubscribeAlarms_Returns_Handle_But_Never_Polls()
|
||||
{
|
||||
@@ -76,6 +77,7 @@ public sealed class AbCipAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that enabled alarm projection starts polling and fires raise event on 0-to-1 transition.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task FeatureFlag_On_Subscribe_Starts_Polling_And_Fires_Raise_On_0_to_1()
|
||||
{
|
||||
@@ -120,6 +122,7 @@ public sealed class AbCipAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm clear event fires on 1-to-0 transition.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Clear_Event_Fires_On_1_to_0_Transition()
|
||||
{
|
||||
@@ -161,6 +164,7 @@ public sealed class AbCipAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribing stops the alarm poll loop.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_Stops_The_Poll_Loop()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class AbCipBoolInDIntRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit set reads parent, ORs bit, and writes back.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_set_reads_parent_ORs_bit_writes_back()
|
||||
{
|
||||
@@ -50,6 +51,7 @@ public sealed class AbCipBoolInDIntRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit clear preserves other bits.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_clear_preserves_other_bits()
|
||||
{
|
||||
@@ -73,6 +75,7 @@ public sealed class AbCipBoolInDIntRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent bit writes to same parent compose correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_bit_writes_to_same_parent_compose_correctly()
|
||||
{
|
||||
@@ -98,6 +101,7 @@ public sealed class AbCipBoolInDIntRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit writes to different parents each get their own runtime.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_writes_to_different_parents_each_get_own_runtime()
|
||||
{
|
||||
@@ -125,6 +129,7 @@ public sealed class AbCipBoolInDIntRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeat bit writes reuse one parent runtime.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Repeat_bit_writes_reuse_one_parent_runtime()
|
||||
{
|
||||
|
||||
+10
@@ -19,6 +19,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
// ---- Driver.AbCip-001 — ReinitializeAsync must apply a changed config JSON ----
|
||||
|
||||
/// <summary>Tests that InitializeAsync applies devices and tags from the config JSON.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_applies_devices_and_tags_from_the_config_json()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
}
|
||||
|
||||
/// <summary>Tests that ReinitializeAsync with changed config JSON picks up the new device.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_with_a_changed_config_json_picks_up_the_new_device()
|
||||
{
|
||||
@@ -63,6 +65,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
}
|
||||
|
||||
/// <summary>Tests that InitializeAsync with blank JSON keeps construction-time options.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_blank_json_keeps_construction_time_options()
|
||||
{
|
||||
@@ -82,6 +85,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
// ---- Driver.AbCip-003 — declaration-only whole-UDT grouping is opt-in ----
|
||||
|
||||
/// <summary>Tests that whole UDT grouping is off by default so members read per tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Whole_udt_grouping_is_off_by_default_so_members_read_per_tag()
|
||||
{
|
||||
@@ -137,6 +141,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
// ---- Driver.AbCip-008 — ShutdownAsync awaits probe loops; reads are concurrency-safe ----
|
||||
|
||||
/// <summary>Tests that ShutdownAsync awaits the probe loop before returning.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_awaits_the_probe_loop_before_returning()
|
||||
{
|
||||
@@ -162,6 +167,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
}
|
||||
|
||||
/// <summary>Tests that ShutdownAsync is idempotent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_is_idempotent()
|
||||
{
|
||||
@@ -176,6 +182,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
}
|
||||
|
||||
/// <summary>Tests that concurrent first reads of the same tag do not corrupt the runtime cache.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_first_reads_of_the_same_tag_do_not_corrupt_the_runtime_cache()
|
||||
{
|
||||
@@ -217,6 +224,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
}
|
||||
|
||||
/// <summary>Tests that read UDInt tag returns uint value not negative-wrapped int.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_UDInt_tag_returns_uint_value_not_negative_wrapped_int()
|
||||
{
|
||||
@@ -243,6 +251,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
// ---- Driver.AbCip-005 — Structure parent not registered; duplicate key check ----
|
||||
|
||||
/// <summary>Tests that structure parent tag read returns BadNotSupported not Good null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Structure_parent_tag_read_returns_BadNotSupported_not_Good_null()
|
||||
{
|
||||
@@ -318,6 +327,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
|
||||
// ---- Driver.AbCip-010 — stale runtime evicted on failure ----
|
||||
|
||||
/// <summary>Tests that read failure evicts runtime so next read creates fresh handle.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_failure_evicts_runtime_so_next_read_creates_fresh_handle()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
public sealed class AbCipDriverDiscoveryTests
|
||||
{
|
||||
/// <summary>Verifies that pre-declared tags emit as variables under device folder.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task PreDeclared_tags_emit_as_variables_under_device_folder()
|
||||
{
|
||||
@@ -35,6 +36,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that device folder display name falls back to host address when not provided.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Device_folder_displayname_falls_back_to_host_address()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pre-declared system tags are filtered out.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task PreDeclared_system_tags_are_filtered_out()
|
||||
{
|
||||
@@ -74,6 +77,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tags for mismatched devices are ignored.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tags_for_mismatched_device_are_ignored()
|
||||
{
|
||||
@@ -91,6 +95,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that controller enumeration adds tags under Discovered folder.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Controller_enumeration_adds_tags_under_Discovered_folder()
|
||||
{
|
||||
@@ -114,6 +119,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that controller enumeration honours system tag hint and filter.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Controller_enumeration_honours_system_tag_hint_and_filter()
|
||||
{
|
||||
@@ -136,6 +142,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that controller enumeration ReadOnly flag surfaces ViewOnly classification.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Controller_enumeration_ReadOnly_surfaces_ViewOnly_classification()
|
||||
{
|
||||
@@ -156,6 +163,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that controller enumeration receives correct device parameters.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Controller_enumeration_receives_correct_device_params()
|
||||
{
|
||||
@@ -236,6 +244,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that FlushOptionalCachesAsync clears the template cache.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FlushOptionalCachesAsync_clears_template_cache()
|
||||
{
|
||||
@@ -257,39 +266,29 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
/// <summary>Gets the list of recorded variables.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Records a folder node.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Records a variable node.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="info">The attribute information for the variable.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>Adds a property (no-op in test).</summary>
|
||||
/// <param name="_">Property name (unused in test).</param>
|
||||
/// <param name="__">Property data type (unused in test).</param>
|
||||
/// <param name="___">Property value (unused in test).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
/// <summary>Test variable handle.</summary>
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference of the variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
/// <summary>Marks the variable as an alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
/// <summary>Null sink for alarm conditions.</summary>
|
||||
private sealed class NullSink : IAlarmConditionSink
|
||||
{
|
||||
/// <summary>Handles alarm transition (no-op).</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
@@ -303,15 +302,13 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
/// <summary>Initializes a new instance of the FakeEnumeratorFactory.</summary>
|
||||
/// <param name="tags">The tags to enumerate.</param>
|
||||
public FakeEnumeratorFactory(params AbCipDiscoveredTag[] tags) => _tags = tags;
|
||||
/// <summary>Creates a new fake enumerator.</summary>
|
||||
/// <inheritdoc />
|
||||
public IAbCipTagEnumerator Create() => new FakeEnumerator(this);
|
||||
|
||||
/// <summary>Fake tag enumerator for testing.</summary>
|
||||
private sealed class FakeEnumerator(FakeEnumeratorFactory outer) : IAbCipTagEnumerator
|
||||
{
|
||||
/// <summary>Enumerates discovered tags asynchronously.</summary>
|
||||
/// <param name="deviceParams">The device parameters for enumeration.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||
AbCipTagCreateParams deviceParams,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown status.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
||||
{
|
||||
@@ -35,6 +36,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a tag on an unknown device maps to BadNodeIdUnknown status.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tag_on_unknown_device_maps_to_BadNodeIdUnknown()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful DInt read returns Good status with the correct value.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_DInt_read_returns_Good_with_value()
|
||||
{
|
||||
@@ -71,6 +74,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated reads reuse the runtime without reinitializing.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Repeat_read_reuses_runtime_without_reinitialise()
|
||||
{
|
||||
@@ -88,6 +92,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero libplctag status is mapped via AbCipStatusMapper.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task NonZero_libplctag_status_maps_via_AbCipStatusMapper()
|
||||
{
|
||||
@@ -103,6 +108,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an exception during read surfaces BadCommunicationError status.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Exception_during_read_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -119,6 +125,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batched reads preserve order and per-tag status.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Batched_reads_preserve_order_and_per_tag_status()
|
||||
{
|
||||
@@ -144,6 +151,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful read marks health as Healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_read_marks_health_Healthy()
|
||||
{
|
||||
@@ -158,6 +166,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tag creation parameters are built correctly from device and profile.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TagCreateParams_are_built_from_device_and_profile()
|
||||
{
|
||||
@@ -176,6 +185,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation propagates from read operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_propagates_from_read()
|
||||
{
|
||||
@@ -194,6 +204,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync disposes each tag runtime.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_disposes_each_tag_runtime()
|
||||
{
|
||||
@@ -211,6 +222,7 @@ public sealed class AbCipDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that initialization failure disposes the tag and surfaces communication error.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Initialize_failure_disposes_tag_and_surfaces_communication_error()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class AbCipDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies InitializeAsync with no devices succeeds and marks driver healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_empty_devices_succeeds_and_marks_healthy()
|
||||
{
|
||||
@@ -28,6 +29,7 @@ public sealed class AbCipDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies InitializeAsync registers devices with their respective PLC family profiles.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_registers_each_device_with_its_family_profile()
|
||||
{
|
||||
@@ -48,6 +50,7 @@ public sealed class AbCipDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies InitializeAsync rejects malformed host addresses and faults the driver.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_malformed_host_address_faults()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public sealed class AbCipDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies ShutdownAsync clears devices and marks driver state unknown.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_clears_devices_and_marks_unknown()
|
||||
{
|
||||
@@ -79,6 +83,7 @@ public sealed class AbCipDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies ReinitializeAsync stops and restarts all devices.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_cycles_devices()
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class AbCipDriverWholeUdtReadTests
|
||||
]);
|
||||
|
||||
/// <summary>Verifies that multiple members of the same UDT trigger only one parent read.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Two_members_of_same_udt_trigger_one_parent_read()
|
||||
{
|
||||
@@ -55,6 +56,7 @@ public sealed class AbCipDriverWholeUdtReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each UDT member is decoded at its correct offset.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Each_member_decodes_at_its_own_offset()
|
||||
{
|
||||
@@ -81,6 +83,7 @@ public sealed class AbCipDriverWholeUdtReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parent read failure marks all grouped members as Bad.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Parent_read_failure_stamps_every_grouped_member_Bad()
|
||||
{
|
||||
@@ -101,6 +104,7 @@ public sealed class AbCipDriverWholeUdtReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that mixed batches group UDT members and fall back to atomic reads.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mixed_batch_groups_udt_and_falls_back_atomics()
|
||||
{
|
||||
@@ -121,6 +125,7 @@ public sealed class AbCipDriverWholeUdtReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a single UDT member uses the per-tag read path rather than grouping.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Single_member_of_Udt_uses_per_tag_read_path()
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown reference maps to BadNodeIdUnknown status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-writable tags map to BadNotWritable status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Non_writable_tag_maps_to_BadNotWritable()
|
||||
{
|
||||
@@ -48,6 +50,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that successful DInt writes encode and flush values.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_DInt_write_encodes_and_flushes()
|
||||
{
|
||||
@@ -64,6 +67,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit-in-DInt writes succeed via read-modify-write.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_in_dint_write_now_succeeds_via_RMW()
|
||||
{
|
||||
@@ -85,6 +89,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero libplctag status after write maps correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Non_zero_libplctag_status_after_write_maps_via_AbCipStatusMapper()
|
||||
{
|
||||
@@ -100,6 +105,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that type mismatch surfaces BadTypeMismatch status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Type_mismatch_surfaces_BadTypeMismatch()
|
||||
{
|
||||
@@ -126,6 +132,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overflow surfaces BadOutOfRange status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Overflow_surfaces_BadOutOfRange()
|
||||
{
|
||||
@@ -144,6 +151,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that exceptions during write surface BadCommunicationError.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Exception_during_write_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -160,6 +168,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batch write preserves order across success and failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Batch_preserves_order_across_success_and_failure()
|
||||
{
|
||||
@@ -192,6 +201,7 @@ public sealed class AbCipDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation propagates from write operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_propagates_from_write()
|
||||
{
|
||||
|
||||
@@ -25,11 +25,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
/// <summary>Gets the last template ID read.</summary>
|
||||
public uint LastTemplateId { get; private set; }
|
||||
|
||||
/// <summary>Reads the template data for the specified device and template ID.</summary>
|
||||
/// <param name="deviceParams">The device parameters.</param>
|
||||
/// <param name="templateInstanceId">The template instance ID.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that returns the template response bytes.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> ReadAsync(AbCipTagCreateParams deviceParams, uint templateInstanceId, CancellationToken ct)
|
||||
{
|
||||
ReadCount++;
|
||||
@@ -50,8 +46,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
/// <summary>Gets or sets an optional customization function for reader creation.</summary>
|
||||
public Func<IAbCipTemplateReader>? Customise { get; set; }
|
||||
|
||||
/// <summary>Creates a new template reader.</summary>
|
||||
/// <returns>The created reader.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAbCipTemplateReader Create()
|
||||
{
|
||||
var r = Customise?.Invoke() ?? new FakeTemplateReader();
|
||||
@@ -93,6 +88,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that FetchUdtShapeAsync decodes a blob and caches the result.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FetchUdtShapeAsync_decodes_blob_and_caches_result()
|
||||
{
|
||||
@@ -123,6 +119,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different template IDs result in separate fetch operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FetchUdtShapeAsync_different_templateIds_each_fetch()
|
||||
{
|
||||
@@ -154,6 +151,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that FetchUdtShapeAsync returns null for an unknown device.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FetchUdtShapeAsync_unknown_device_returns_null()
|
||||
{
|
||||
@@ -170,6 +168,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a decode failure returns null and does not cache the result.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FetchUdtShapeAsync_decode_failure_returns_null_and_does_not_cache()
|
||||
{
|
||||
@@ -193,6 +192,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a reader exception returns null.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FetchUdtShapeAsync_reader_exception_returns_null()
|
||||
{
|
||||
@@ -211,6 +211,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that FlushOptionalCachesAsync empties the template cache.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FlushOptionalCachesAsync_empties_template_cache()
|
||||
{
|
||||
@@ -241,11 +242,7 @@ public sealed class AbCipFetchUdtShapeTests
|
||||
/// <summary>Test implementation of IAbCipTemplateReader that throws on read.</summary>
|
||||
private sealed class ThrowingTemplateReader : IAbCipTemplateReader
|
||||
{
|
||||
/// <summary>Throws an exception when read is attempted.</summary>
|
||||
/// <param name="p">The device parameters.</param>
|
||||
/// <param name="id">The template ID.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>Never returns; throws instead.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> ReadAsync(AbCipTagCreateParams p, uint id, CancellationToken ct) =>
|
||||
throw new InvalidOperationException("fake read failure");
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
public sealed class AbCipHostProbeTests
|
||||
{
|
||||
/// <summary>Verifies that GetHostStatuses returns one entry per configured device.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHostStatuses_returns_one_entry_per_device()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful probe read transitions the host state to Running.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_with_successful_read_transitions_to_Running()
|
||||
{
|
||||
@@ -58,6 +60,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failed probe read transitions the host state to Stopped.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_with_read_failure_transitions_to_Stopped()
|
||||
{
|
||||
@@ -87,6 +90,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe is disabled when the Enabled option is false.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_disabled_when_Enabled_is_false()
|
||||
{
|
||||
@@ -108,6 +112,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe is skipped when ProbeTagPath is null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_skipped_when_ProbeTagPath_is_null()
|
||||
{
|
||||
@@ -125,6 +130,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe loops across multiple devices independently.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_loops_across_multiple_devices_independently()
|
||||
{
|
||||
@@ -162,6 +168,7 @@ public sealed class AbCipHostProbeTests
|
||||
// ---- IPerCallHostResolver ----
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns the declared device for a known tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_returns_declared_device_for_known_tag()
|
||||
{
|
||||
@@ -186,6 +193,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to the first device for an unknown tag reference.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_first_device_for_unknown_reference()
|
||||
{
|
||||
@@ -200,6 +208,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to the driver instance ID when no devices are configured.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices()
|
||||
{
|
||||
@@ -210,6 +219,7 @@ public sealed class AbCipHostProbeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost for a UDT member walks to the synthesized definition.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_for_UDT_member_walks_to_synthesised_definition()
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ public sealed class AbCipLoggingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ProbeLoop logs when an exception is swallowed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProbeLoop_logs_when_an_exception_is_swallowed()
|
||||
{
|
||||
@@ -79,6 +80,7 @@ public sealed class AbCipLoggingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadFailure logs at warning level.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadFailure_logs_at_warning_level()
|
||||
{
|
||||
@@ -106,6 +108,7 @@ public sealed class AbCipLoggingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadException logs at warning level.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadException_logs_at_warning_level()
|
||||
{
|
||||
@@ -137,6 +140,7 @@ public sealed class AbCipLoggingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InitializeAsync warns when probe is enabled but ProbeTagPath is blank.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_warns_when_probe_is_enabled_but_ProbeTagPath_is_blank()
|
||||
{
|
||||
@@ -164,6 +168,7 @@ public sealed class AbCipLoggingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InitializeAsync does not warn when probe is disabled.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_does_not_warn_when_probe_is_disabled()
|
||||
{
|
||||
@@ -191,9 +196,11 @@ public sealed class AbCipLoggingTests
|
||||
/// <summary>Begins a scope (stub implementation).</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state.</param>
|
||||
/// <returns>A no-op disposable scope.</returns>
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
/// <summary>Checks if logging is enabled (always true).</summary>
|
||||
/// <param name="logLevel">The log level to check.</param>
|
||||
/// <returns><c>true</c> always — this capturing logger accepts every log level.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
/// <summary>Logs an entry and captures it.</summary>
|
||||
/// <typeparam name="TState">The type of the log state.</typeparam>
|
||||
|
||||
+5
@@ -21,6 +21,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
|
||||
private const string Device = "ab://10.0.0.5/1,0";
|
||||
|
||||
/// <summary>Verifies that per-device AllowPacking override is forwarded to tag creation parameters.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Device_AllowPacking_override_is_forwarded_to_tag_create_params()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AllowPacking defaults inherit from the family profile when not overridden.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Device_AllowPacking_default_inherits_from_family_profile()
|
||||
{
|
||||
@@ -61,6 +63,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Micro800 devices have AllowPacking defaulting to false from the family profile.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Micro800_default_AllowPacking_is_false_from_family_profile()
|
||||
{
|
||||
@@ -81,6 +84,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that per-device ConnectionSize override is forwarded to tag creation parameters.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Device_ConnectionSize_override_is_forwarded_to_tag_create_params()
|
||||
{
|
||||
@@ -99,6 +103,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ConnectionSize defaults inherit from the family profile when not overridden.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Device_ConnectionSize_default_inherits_from_family_profile()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a ControlLogix device initializes with the correct profile.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ControlLogix_device_initialises_with_correct_profile()
|
||||
{
|
||||
@@ -54,6 +55,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CompactLogix device initializes with a narrow connection size.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task CompactLogix_device_initialises_with_narrow_ConnectionSize()
|
||||
{
|
||||
@@ -85,6 +87,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Micro800 device with an empty CIP path parses correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Micro800_device_with_empty_cip_path_parses_correctly()
|
||||
{
|
||||
@@ -102,6 +105,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Micro800 read operations forward the empty path to tag creation parameters.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Micro800_read_forwards_empty_path_to_tag_create_params()
|
||||
{
|
||||
@@ -134,6 +138,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GuardLogix safety tags surface as ViewOnly in discovery.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GuardLogix_safety_tag_surfaces_as_ViewOnly_in_discovery()
|
||||
{
|
||||
@@ -160,6 +165,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GuardLogix safety tag writes are rejected even when the tag is marked Writable.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GuardLogix_safety_tag_writes_rejected_even_when_Writable_is_true()
|
||||
{
|
||||
@@ -206,37 +212,27 @@ public sealed class AbCipPlcFamilyTests
|
||||
/// <summary>Gets the list of variables recorded by this builder.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Adds a folder to the recorded list and returns this builder for chaining.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Adds a variable to the recorded list and returns a handle.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="info">The driver attribute information.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>No-op property adding operation for test compatibility.</summary>
|
||||
/// <param name="_">The property name.</param>
|
||||
/// <param name="__">The property data type.</param>
|
||||
/// <param name="___">The property value.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference for this variable handle.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
/// <summary>Marks this variable as an alarm condition and returns a null sink.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
private sealed class NullSink : IAlarmConditionSink
|
||||
{
|
||||
/// <summary>Called when an alarm state transitions.</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the initial poll raises OnDataChange events for every subscribed tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Initial_poll_raises_OnDataChange_for_every_tag()
|
||||
{
|
||||
@@ -47,6 +48,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unchanged values raise OnDataChange only once (on initial poll).</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Unchanged_value_raises_only_once()
|
||||
{
|
||||
@@ -66,6 +68,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that value changes between polls raise OnDataChange events.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Value_change_between_polls_raises_OnDataChange()
|
||||
{
|
||||
@@ -89,6 +92,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribe halts polling and no further events are raised.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_halts_polling()
|
||||
{
|
||||
@@ -112,6 +116,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that polling intervals below 100ms are floored to the minimum.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Interval_below_100ms_is_floored()
|
||||
{
|
||||
@@ -133,6 +138,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync cancels all active subscriptions.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_cancels_active_subscriptions()
|
||||
{
|
||||
@@ -154,6 +160,7 @@ public sealed class AbCipSubscriptionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscriptions on UDT members use the synthesized full reference.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task Subscription_on_UDT_member_uses_synthesised_full_reference()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
public sealed class AbCipUdtMemberTests
|
||||
{
|
||||
/// <summary>Verifies that UDT with declared members expands to individual member variables.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UDT_with_declared_members_fans_out_to_member_variables()
|
||||
{
|
||||
@@ -48,6 +49,7 @@ public sealed class AbCipUdtMemberTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UDT members can be read via synthesised full reference paths.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UDT_members_resolvable_for_read_via_synthesised_full_reference()
|
||||
{
|
||||
@@ -84,6 +86,7 @@ public sealed class AbCipUdtMemberTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UDT member writes route through synthesised tag paths.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UDT_member_write_routes_through_synthesised_tagpath()
|
||||
{
|
||||
@@ -110,6 +113,7 @@ public sealed class AbCipUdtMemberTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UDT member read/write operations respect the Writable flag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UDT_member_read_write_honours_member_Writable_flag()
|
||||
{
|
||||
@@ -135,6 +139,7 @@ public sealed class AbCipUdtMemberTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that structure tags without declared members appear as single variables.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Structure_tag_without_members_is_emitted_as_single_variable()
|
||||
{
|
||||
@@ -156,6 +161,7 @@ public sealed class AbCipUdtMemberTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty member lists are treated the same as null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Empty_Members_list_is_treated_like_null()
|
||||
{
|
||||
@@ -174,6 +180,7 @@ public sealed class AbCipUdtMemberTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UDT members and flat tags can coexist in the address space.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UDT_members_mixed_with_flat_tags_coexist()
|
||||
{
|
||||
@@ -209,39 +216,29 @@ public sealed class AbCipUdtMemberTests
|
||||
/// <summary>Gets the collected variables.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Records a folder in the address space.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Records a variable in the address space.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="info">The driver attribute information for the variable.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>Records a property (stub implementation for testing).</summary>
|
||||
/// <param name="_">The property name (unused in this stub).</param>
|
||||
/// <param name="__">The property data type (unused in this stub).</param>
|
||||
/// <param name="___">The property value (unused in this stub).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
/// <summary>Variable handle implementation for testing.</summary>
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference path.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
/// <summary>Marks this handle as an alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
/// <summary>Null alarm condition sink for testing.</summary>
|
||||
private sealed class NullSink : IAlarmConditionSink
|
||||
{
|
||||
/// <summary>Handles alarm transitions (stub).</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,7 @@ internal class FakeAbCipTag : IAbCipTagRuntime
|
||||
/// <param name="createParams">The tag creation parameters.</param>
|
||||
public FakeAbCipTag(AbCipTagCreateParams createParams) => CreationParams = createParams;
|
||||
|
||||
/// <summary>Increments the initialize count and simulates initialization.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
InitializeCount++;
|
||||
@@ -44,8 +43,7 @@ internal class FakeAbCipTag : IAbCipTagRuntime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Increments the read count and simulates a read operation.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task ReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ReadCount++;
|
||||
@@ -53,20 +51,17 @@ internal class FakeAbCipTag : IAbCipTagRuntime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Increments the write count and simulates a write operation.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task WriteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
WriteCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Returns the simulated status code.</summary>
|
||||
/// <inheritdoc />
|
||||
public virtual int GetStatus() => Status;
|
||||
|
||||
/// <summary>Returns the mock tag value.</summary>
|
||||
/// <param name="type">The data type being decoded.</param>
|
||||
/// <param name="bitIndex">The optional bit index for bit operations.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual object? DecodeValue(AbCipDataType type, int? bitIndex) => Value;
|
||||
|
||||
/// <summary>
|
||||
@@ -78,20 +73,14 @@ internal class FakeAbCipTag : IAbCipTagRuntime
|
||||
/// </summary>
|
||||
public Dictionary<int, object?> ValuesByOffset { get; } = new();
|
||||
|
||||
/// <summary>Returns the mock value at the specified offset.</summary>
|
||||
/// <param name="type">The data type being decoded.</param>
|
||||
/// <param name="offset">The byte offset into the tag storage.</param>
|
||||
/// <param name="bitIndex">The optional bit index for bit operations.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex)
|
||||
{
|
||||
if (ValuesByOffset.TryGetValue(offset, out var v)) return v;
|
||||
return offset == 0 ? Value : null;
|
||||
}
|
||||
|
||||
/// <summary>Encodes a value into the mock tag storage.</summary>
|
||||
/// <param name="type">The data type being encoded.</param>
|
||||
/// <param name="bitIndex">The optional bit index for bit operations.</param>
|
||||
/// <param name="value">The value to encode.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual void EncodeValue(AbCipDataType type, int? bitIndex, object? value) => Value = value;
|
||||
|
||||
/// <summary>Marks the tag as disposed.</summary>
|
||||
@@ -106,8 +95,7 @@ internal sealed class FakeAbCipTagFactory : IAbCipTagFactory
|
||||
/// <summary>Gets or sets an optional customization function to override the tag creation.</summary>
|
||||
public Func<AbCipTagCreateParams, FakeAbCipTag>? Customise { get; set; }
|
||||
|
||||
/// <summary>Creates a new fake tag and indexes it by name.</summary>
|
||||
/// <param name="createParams">The tag creation parameters.</param>
|
||||
/// <inheritdoc />
|
||||
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams)
|
||||
{
|
||||
var fake = Customise?.Invoke(createParams) ?? new FakeAbCipTag(createParams);
|
||||
|
||||
+2
@@ -37,6 +37,7 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
|
||||
/// <summary>Verifies that the driver reads seeded N file from the AB server via PCCC.</summary>
|
||||
/// <param name="profile">The AB Legacy server profile describing the fixture endpoint.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[AbLegacyTheory]
|
||||
[MemberData(nameof(Profiles))]
|
||||
public async Task Driver_reads_seeded_N_file_from_ab_server_PCCC(AbLegacyServerProfile profile)
|
||||
@@ -73,6 +74,7 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SLC500 write-then-read round trip succeeds on N7 scratch register.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[AbLegacyFact]
|
||||
public async Task Slc500_write_then_read_round_trip_on_N7_scratch_register()
|
||||
{
|
||||
|
||||
+4
@@ -85,9 +85,11 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime
|
||||
}
|
||||
|
||||
/// <summary>Initializes the fixture asynchronously.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
/// <summary>Disposes the fixture asynchronously.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
@@ -96,6 +98,7 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime
|
||||
/// fixture logic because attribute ctors fire before the collection fixture instance
|
||||
/// exists.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the server endpoint is reachable; otherwise <see langword="false"/>.</returns>
|
||||
public static bool IsServerAvailable()
|
||||
{
|
||||
var (host, port) = ResolveEndpoint();
|
||||
@@ -169,6 +172,7 @@ public static class KnownProfiles
|
||||
|
||||
/// <summary>Gets the profile for the specified PLC family.</summary>
|
||||
/// <param name="family">The PLC family.</param>
|
||||
/// <returns>The server profile for the specified family.</returns>
|
||||
public static AbLegacyServerProfile ForFamily(AbLegacyPlcFamily family) =>
|
||||
All.FirstOrDefault(p => p.Family == family)
|
||||
?? throw new ArgumentOutOfRangeException(nameof(family), family, "No integration profile for this family.");
|
||||
|
||||
@@ -68,6 +68,7 @@ public sealed class AbLegacyBitIndexRangeTests
|
||||
AbLegacyAddress.TryParse("N7:0/-1").ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that bit in word RMW against L file uses 32-bit parent and high bit.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_in_word_RMW_against_L_file_uses_32bit_parent_and_high_bit()
|
||||
{
|
||||
@@ -93,6 +94,7 @@ public sealed class AbLegacyBitIndexRangeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit in word RMW high bit 15 does not corrupt via sign extension.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_in_word_RMW_high_bit_15_does_not_corrupt_via_sign_extension()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
public sealed class AbLegacyBitRmwTests
|
||||
{
|
||||
/// <summary>Verifies that setting a bit reads the parent word, ORs the bit, and writes back.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_set_reads_parent_word_ORs_bit_writes_back()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing a bit preserves other bits in the word.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_clear_preserves_other_bits_in_N_file_word()
|
||||
{
|
||||
@@ -54,6 +56,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent bit writes to the same word compose correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_bit_writes_to_same_word_compose_correctly()
|
||||
{
|
||||
@@ -79,6 +82,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated bit writes reuse the parent word runtime.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Repeat_bit_writes_reuse_parent_runtime()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
// ---- ITagDiscovery ----
|
||||
|
||||
/// <summary>Verifies that DiscoverAsync emits pre-declared tags under the device folder.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_emits_pre_declared_tags_under_device_folder()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
// ---- ISubscribable ----
|
||||
|
||||
/// <summary>Verifies that Subscribe initial poll raises OnDataChange.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_initial_poll_raises_OnDataChange()
|
||||
{
|
||||
@@ -66,6 +68,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Unsubscribe halts polling.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_halts_polling()
|
||||
{
|
||||
@@ -96,6 +99,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
// ---- IHostConnectivityProbe ----
|
||||
|
||||
/// <summary>Verifies that GetHostStatuses returns one status per device.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHostStatuses_returns_one_per_device()
|
||||
{
|
||||
@@ -114,6 +118,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Probe transitions to Running on successful read.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Running_on_successful_read()
|
||||
{
|
||||
@@ -138,6 +143,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Probe transitions to Stopped on read failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Stopped_on_read_failure()
|
||||
{
|
||||
@@ -162,6 +168,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Probe is disabled when ProbeAddress is null.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_disabled_when_ProbeAddress_is_null()
|
||||
{
|
||||
@@ -180,6 +187,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
// ---- IPerCallHostResolver ----
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns declared device for known tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_returns_declared_device_for_known_tag()
|
||||
{
|
||||
@@ -204,6 +212,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to first device for unknown tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_first_device_for_unknown()
|
||||
{
|
||||
@@ -218,6 +227,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to DriverInstanceId when no devices exist.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices()
|
||||
{
|
||||
@@ -243,31 +253,22 @@ public sealed class AbLegacyCapabilityTests
|
||||
/// <summary>Gets list of variables created during discovery.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Records folder creation.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Records variable creation.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="info">The driver attribute information.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>Records property addition (stub implementation).</summary>
|
||||
/// <param name="_">The property name (unused).</param>
|
||||
/// <param name="__">The data type (unused).</param>
|
||||
/// <param name="___">The property value (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference of the variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
/// <summary>Marks the variable as an alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
/// <summary>Null sink for alarm condition transitions.</summary>
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
// ---- Driver.AbLegacy-011 ----
|
||||
|
||||
/// <summary>Verifies that Dispose performs teardown without blocking on async operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_runs_teardown_without_blocking_on_async_wait()
|
||||
{
|
||||
@@ -47,6 +48,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Dispose can be called multiple times without throwing.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_is_idempotent()
|
||||
{
|
||||
@@ -61,6 +63,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Dispose does not deadlock under a single-threaded synchronization context.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_under_single_threaded_sync_context_does_not_deadlock()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InitializeAsync with devices assigns family profiles.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_devices_assigns_family_profiles()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InitializeAsync with malformed host address faults.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_malformed_host_address_faults()
|
||||
{
|
||||
@@ -55,6 +57,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync clears devices.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_clears_devices()
|
||||
{
|
||||
@@ -115,6 +118,7 @@ public sealed class AbLegacyDriverTests
|
||||
// ---- Driver.AbLegacy-012: profile fields consumed ----
|
||||
|
||||
/// <summary>Verifies that EffectiveCipPath falls back to profile default when host path is empty.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EffectiveCipPath_falls_back_to_profile_default_when_host_path_is_empty()
|
||||
{
|
||||
@@ -135,6 +139,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that EffectiveCipPath preserves explicit host path.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EffectiveCipPath_preserves_explicit_host_path()
|
||||
{
|
||||
@@ -154,6 +159,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that long tag on MicroLogix device is rejected at initialization.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Long_tag_on_MicroLogix_device_rejected_at_init()
|
||||
{
|
||||
@@ -170,6 +176,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that long tag on SLC 500 device is accepted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Long_tag_on_Slc500_device_accepted()
|
||||
{
|
||||
@@ -186,6 +193,7 @@ public sealed class AbLegacyDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that string tag on PLC-5 device is rejected at initialization.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task String_tag_on_Plc5_device_rejected_at_init()
|
||||
{
|
||||
|
||||
+17
-4
@@ -19,13 +19,24 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
{
|
||||
public readonly List<(LogLevel Level, string Message)> Entries = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Begins a logical operation scope (returns a no-op scope).</summary>
|
||||
/// <typeparam name="TState">The type of the state to associate with the scope.</typeparam>
|
||||
/// <param name="state">The state identifier for the scope.</param>
|
||||
/// <returns>A no-op disposable scope.</returns>
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Checks whether logging is enabled for the given level (always true).</summary>
|
||||
/// <param name="logLevel">The log level to check.</param>
|
||||
/// <returns><see langword="true"/> always.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Records a log entry into the captured entries list.</summary>
|
||||
/// <typeparam name="TState">The type of the log state object.</typeparam>
|
||||
/// <param name="logLevel">The severity level of the log entry.</param>
|
||||
/// <param name="eventId">The event identifier for the log entry.</param>
|
||||
/// <param name="state">The state object associated with the log entry.</param>
|
||||
/// <param name="exception">An optional exception to log.</param>
|
||||
/// <param name="formatter">A function that formats the state and exception into a message string.</param>
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
=> Entries.Add((logLevel, formatter(state, exception)));
|
||||
@@ -33,7 +44,7 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
{
|
||||
public static readonly NullScope Instance = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Disposes the no-op scope (no-op).</summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -50,6 +61,7 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that driver initialization failure emits an error log.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_failure_emits_error_log()
|
||||
{
|
||||
@@ -68,6 +80,7 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the first non-zero libplctag status per device is logged.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task First_nonzero_libplctag_status_per_device_is_logged()
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
// ---- Read ----
|
||||
|
||||
/// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful N-file read returns a Good status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_N_file_read_returns_Good_value()
|
||||
{
|
||||
@@ -51,6 +53,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated reads reuse the runtime.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Repeat_read_reuses_runtime()
|
||||
{
|
||||
@@ -67,6 +70,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero libplctag status values map via AbLegacyStatusMapper.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
|
||||
{
|
||||
@@ -82,6 +86,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read exceptions surface as BadCommunicationError.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_exception_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -96,6 +101,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batched reads preserve order.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Batched_reads_preserve_order()
|
||||
{
|
||||
@@ -120,6 +126,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read tag creation parameters are composed from device and profile.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_TagCreateParams_composed_from_device_and_profile()
|
||||
{
|
||||
@@ -140,6 +147,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
// ---- Write ----
|
||||
|
||||
/// <summary>Verifies that a non-writable tag rejects with BadNotWritable.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Non_writable_tag_rejects_with_BadNotWritable()
|
||||
{
|
||||
@@ -153,6 +161,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful N-file write encodes and flushes the data.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_N_file_write_encodes_and_flushes()
|
||||
{
|
||||
@@ -169,6 +178,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit-within-word write now succeeds via RMW.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_within_word_write_now_succeeds_via_RMW()
|
||||
{
|
||||
@@ -190,6 +200,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write exceptions surface as BadCommunicationError.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_exception_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -204,6 +215,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batch write preserves order across different outcomes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Batch_write_preserves_order_across_outcomes()
|
||||
{
|
||||
@@ -233,6 +245,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation propagates through the driver.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_propagates()
|
||||
{
|
||||
@@ -250,6 +263,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync disposes all runtimes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_disposes_runtimes()
|
||||
{
|
||||
|
||||
+4
-6
@@ -30,9 +30,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
public OverlapDetectingFake(AbLegacyTagCreateParams p) : base(p) { }
|
||||
|
||||
/// <summary>Reads the tag asynchronously while tracking concurrent operations.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task representing the read operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public override async Task ReadAsync(CancellationToken ct)
|
||||
{
|
||||
EnterOp();
|
||||
@@ -45,9 +43,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
finally { LeaveOp(); }
|
||||
}
|
||||
|
||||
/// <summary>Writes to the tag asynchronously while tracking concurrent operations.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task representing the write operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteAsync(CancellationToken ct)
|
||||
{
|
||||
EnterOp();
|
||||
@@ -69,6 +65,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent reads of the same tag are serialised against the shared runtime.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_reads_of_same_tag_are_serialised_against_the_shared_runtime()
|
||||
{
|
||||
@@ -102,6 +99,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent read and write operations on the same tag do not overlap.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_read_and_write_of_same_tag_do_not_overlap()
|
||||
{
|
||||
|
||||
@@ -41,9 +41,7 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
public FakeAbLegacyTag(AbLegacyTagCreateParams p) => CreationParams = p;
|
||||
|
||||
/// <summary>Initializes the tag asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual Task InitializeAsync(CancellationToken ct)
|
||||
{
|
||||
InitializeCount++;
|
||||
@@ -51,9 +49,7 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Reads the tag value asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual Task ReadAsync(CancellationToken ct)
|
||||
{
|
||||
ReadCount++;
|
||||
@@ -61,9 +57,7 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Writes the tag value asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual Task WriteAsync(CancellationToken ct)
|
||||
{
|
||||
WriteCount++;
|
||||
@@ -71,20 +65,13 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Gets the current tag status.</summary>
|
||||
/// <returns>The status code.</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual int GetStatus() => Status;
|
||||
|
||||
/// <summary>Decodes the tag value based on the specified data type and bit index.</summary>
|
||||
/// <param name="type">The AbLegacy data type.</param>
|
||||
/// <param name="bitIndex">The bit index if applicable.</param>
|
||||
/// <returns>The decoded value.</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual object? DecodeValue(AbLegacyDataType type, int? bitIndex) => Value;
|
||||
|
||||
/// <summary>Encodes the tag value based on the specified data type and bit index.</summary>
|
||||
/// <param name="type">The AbLegacy data type.</param>
|
||||
/// <param name="bitIndex">The bit index if applicable.</param>
|
||||
/// <param name="value">The value to encode.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) => Value = value;
|
||||
|
||||
/// <summary>Disposes the tag.</summary>
|
||||
@@ -100,9 +87,7 @@ internal sealed class FakeAbLegacyTagFactory : IAbLegacyTagFactory
|
||||
/// <summary>Gets or sets an optional customization function for tag creation.</summary>
|
||||
public Func<AbLegacyTagCreateParams, FakeAbLegacyTag>? Customise { get; set; }
|
||||
|
||||
/// <summary>Creates a new AbLegacy tag with the specified parameters.</summary>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
/// <returns>The created tag.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams p)
|
||||
{
|
||||
var fake = Customise?.Invoke(p) ?? new FakeAbLegacyTag(p);
|
||||
|
||||
@@ -74,6 +74,7 @@ public sealed class FocasSimFixture : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>Disposes the fixture and releases any held resources.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
// ---- Admin API helpers ----
|
||||
@@ -85,23 +86,27 @@ public sealed class FocasSimFixture : IAsyncDisposable
|
||||
/// </summary>
|
||||
/// <param name="profileName">The DLL-stem name or OtOpcUa-style alias of the profile to load.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the JSON response from the admin endpoint.</returns>
|
||||
public Task<JsonElement> LoadProfileAsync(string profileName, CancellationToken ct = default) =>
|
||||
SendAdminAsync("mock_load_profile", new { profile = profileName }, ct);
|
||||
|
||||
/// <summary>Deep-merge <paramref name="state"/> into the mock's current state.</summary>
|
||||
/// <param name="state">The state object to deep-merge into the mock's current state.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the JSON response from the admin endpoint.</returns>
|
||||
public Task<JsonElement> PatchStateAsync(object state, CancellationToken ct = default) =>
|
||||
SendAdminAsync("mock_patch", new { state }, ct);
|
||||
|
||||
/// <summary>Reset the mock to the selected profile's default state.</summary>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the JSON response from the admin endpoint.</returns>
|
||||
public Task<JsonElement> ResetAsync(CancellationToken ct = default) =>
|
||||
SendAdminAsync("mock_reset", new { }, ct);
|
||||
|
||||
/// <summary>Install a time-scheduled alarm raise / clear sequence.</summary>
|
||||
/// <param name="sequence">The alarm sequence events to schedule.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the JSON response from the admin endpoint.</returns>
|
||||
public Task<JsonElement> ScheduleAlarmsAsync(IEnumerable<object> sequence, CancellationToken ct = default) =>
|
||||
SendAdminAsync("mock_schedule_alarms", new { sequence }, ct);
|
||||
|
||||
@@ -110,6 +115,7 @@ public sealed class FocasSimFixture : IAsyncDisposable
|
||||
/// <param name="method">The admin method name to invoke.</param>
|
||||
/// <param name="params">The parameters object to send with the request.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the JSON response from the admin endpoint.</returns>
|
||||
public async Task<JsonElement> SendAdminAsync(string method, object @params, CancellationToken ct = default)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
|
||||
+11
-16
@@ -26,6 +26,7 @@ public sealed class WireBackendCoverageTests
|
||||
private const string DeviceHost = "focas://127.0.0.1:8193";
|
||||
|
||||
/// <summary>Verifies that user tag reads route via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task User_tag_reads_route_via_wire_backend()
|
||||
{
|
||||
@@ -73,6 +74,7 @@ public sealed class WireBackendCoverageTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that discover emits device folder and tag variables.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Discover_emits_device_folder_and_tag_variables()
|
||||
{
|
||||
@@ -107,6 +109,7 @@ public sealed class WireBackendCoverageTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribe fires OnDataChange via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_fires_OnDataChange_via_wire_backend()
|
||||
{
|
||||
@@ -157,6 +160,7 @@ public sealed class WireBackendCoverageTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm raise then clear emits both events via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Alarm_raise_then_clear_emits_both_events_via_wire_backend()
|
||||
{
|
||||
@@ -210,6 +214,7 @@ public sealed class WireBackendCoverageTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe transitions to Running against the live mock.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Running_against_live_mock()
|
||||
{
|
||||
@@ -255,39 +260,29 @@ public sealed class WireBackendCoverageTests
|
||||
/// <summary>Gets the list of recorded variables.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Records a folder in the address space builder.</summary>
|
||||
/// <param name="browseName">The browse name for the folder.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Records a variable in the address space builder.</summary>
|
||||
/// <param name="browseName">The browse name for the variable.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="info">The driver attribute information.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>Records an address space property (no-op in this builder).</summary>
|
||||
/// <param name="_">The property name.</param>
|
||||
/// <param name="__">The property data type.</param>
|
||||
/// <param name="___">The property value.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full OPC UA reference for the variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
|
||||
/// <summary>Marks the variable as an alarm condition and returns a sink.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
|
||||
private sealed class NullSink : IAlarmConditionSink
|
||||
{
|
||||
/// <summary>Handles an alarm transition event (no-op in this sink).</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -29,6 +29,7 @@ public sealed class WireBackendTests
|
||||
private const string DeviceHost = "focas://127.0.0.1:8193";
|
||||
|
||||
/// <summary>Verifies that identity axes and dynamic data populate via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Identity_axes_and_dynamic_populate_via_wire_backend()
|
||||
{
|
||||
@@ -86,6 +87,7 @@ public sealed class WireBackendTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that program and operation mode data populate via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Program_and_operation_mode_populate_via_wire_backend()
|
||||
{
|
||||
@@ -151,6 +153,7 @@ public sealed class WireBackendTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timer data populates via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Timers_populate_via_wire_backend()
|
||||
{
|
||||
@@ -209,6 +212,7 @@ public sealed class WireBackendTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that spindle load and max RPM data populate via the wire backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Spindle_load_and_max_rpm_populate_via_wire_backend()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
|
||||
internal class FakeFocasClient : IFocasClient
|
||||
{
|
||||
/// <summary>Gets a value indicating whether the client is connected.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected { get; private set; }
|
||||
/// <summary>Gets the count of connection attempts.</summary>
|
||||
public int ConnectCount { get; private set; }
|
||||
@@ -30,10 +30,7 @@ internal class FakeFocasClient : IFocasClient
|
||||
/// <summary>Gets the log of write operations.</summary>
|
||||
public List<(FocasAddress addr, FocasDataType type, object? value)> WriteLog { get; } = new();
|
||||
|
||||
/// <summary>Connects to a FOCAS host asynchronously.</summary>
|
||||
/// <param name="address">The FOCAS host address.</param>
|
||||
/// <param name="timeout">The connection timeout duration.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken ct)
|
||||
{
|
||||
ConnectCount++;
|
||||
@@ -42,10 +39,7 @@ internal class FakeFocasClient : IFocasClient
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Reads a value from a FOCAS address asynchronously.</summary>
|
||||
/// <param name="address">The FOCAS address to read from.</param>
|
||||
/// <param name="type">The data type of the value.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<(object? value, uint status)> ReadAsync(
|
||||
FocasAddress address, FocasDataType type, CancellationToken ct)
|
||||
{
|
||||
@@ -56,11 +50,7 @@ internal class FakeFocasClient : IFocasClient
|
||||
return Task.FromResult((value, status));
|
||||
}
|
||||
|
||||
/// <summary>Writes a value to a FOCAS address asynchronously.</summary>
|
||||
/// <param name="address">The FOCAS address to write to.</param>
|
||||
/// <param name="type">The data type of the value.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<uint> WriteAsync(
|
||||
FocasAddress address, FocasDataType type, object? value, CancellationToken ct)
|
||||
{
|
||||
@@ -71,15 +61,13 @@ internal class FakeFocasClient : IFocasClient
|
||||
return Task.FromResult(status);
|
||||
}
|
||||
|
||||
/// <summary>Probes the FOCAS connection asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<bool> ProbeAsync(CancellationToken ct) => Task.FromResult(ProbeResult);
|
||||
|
||||
/// <summary>Gets the list of active alarms.</summary>
|
||||
public List<FocasActiveAlarm> Alarms { get; } = [];
|
||||
|
||||
/// <summary>Reads active alarms asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<FocasActiveAlarm>>([.. Alarms]);
|
||||
|
||||
@@ -93,20 +81,15 @@ internal class FakeFocasClient : IFocasClient
|
||||
/// <summary>Gets the dictionary of dynamic snapshots keyed by axis index.</summary>
|
||||
public Dictionary<int, FocasDynamicSnapshot> DynamicByAxis { get; } = [];
|
||||
|
||||
/// <summary>Gets system information asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<FocasSysInfo> GetSysInfoAsync(CancellationToken ct) => Task.FromResult(SysInfo);
|
||||
/// <summary>Gets axis names asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<FocasAxisName>>([.. AxisNames]);
|
||||
/// <summary>Gets spindle names asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<FocasSpindleName>>([.. SpindleNames]);
|
||||
/// <summary>Reads dynamic data for an axis asynchronously.</summary>
|
||||
/// <param name="axisIndex">The zero-based axis index.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken ct)
|
||||
{
|
||||
if (!DynamicByAxis.TryGetValue(axisIndex, out var snap))
|
||||
@@ -116,16 +99,13 @@ internal class FakeFocasClient : IFocasClient
|
||||
|
||||
/// <summary>Gets or sets the program information.</summary>
|
||||
public FocasProgramInfo ProgramInfo { get; set; } = new("O0001", 1, 0, 1);
|
||||
/// <summary>Gets program information asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken ct) =>
|
||||
Task.FromResult(ProgramInfo);
|
||||
|
||||
/// <summary>Gets the dictionary of timers keyed by timer kind.</summary>
|
||||
public Dictionary<FocasTimerKind, FocasTimer> Timers { get; } = [];
|
||||
/// <summary>Gets timer data asynchronously.</summary>
|
||||
/// <param name="kind">The timer kind to retrieve.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken ct)
|
||||
{
|
||||
if (!Timers.TryGetValue(kind, out var t))
|
||||
@@ -135,8 +115,7 @@ internal class FakeFocasClient : IFocasClient
|
||||
|
||||
/// <summary>Gets the list of servo loads.</summary>
|
||||
public List<FocasServoLoad> ServoLoads { get; } = [];
|
||||
/// <summary>Gets servo loads asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<FocasServoLoad>>([.. ServoLoads]);
|
||||
|
||||
@@ -144,12 +123,10 @@ internal class FakeFocasClient : IFocasClient
|
||||
public List<int> SpindleLoads { get; } = [];
|
||||
/// <summary>Gets the list of spindle maximum RPMs.</summary>
|
||||
public List<int> SpindleMaxRpms { get; } = [];
|
||||
/// <summary>Gets spindle loads asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<int>>([.. SpindleLoads]);
|
||||
/// <summary>Gets spindle maximum RPMs asynchronously.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<int>>([.. SpindleMaxRpms]);
|
||||
|
||||
@@ -169,7 +146,7 @@ internal sealed class FakeFocasClientFactory : IFocasClientFactory
|
||||
/// <summary>Gets or sets a customization function for creating clients.</summary>
|
||||
public Func<FakeFocasClient>? Customise { get; set; }
|
||||
|
||||
/// <summary>Creates a fake FOCAS client.</summary>
|
||||
/// <inheritdoc />
|
||||
public IFocasClient Create()
|
||||
{
|
||||
var c = Customise?.Invoke() ?? new FakeFocasClient();
|
||||
|
||||
@@ -27,6 +27,7 @@ public sealed class FocasAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribe without enable throws NotSupported.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_without_Enable_throws_NotSupported()
|
||||
{
|
||||
@@ -38,6 +39,7 @@ public sealed class FocasAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that raise then clear emits both events.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Raise_then_clear_emits_both_events()
|
||||
{
|
||||
@@ -70,6 +72,7 @@ public sealed class FocasAlarmProjectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tick diffs raises and clears without polling loop.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tick_diffs_raises_and_clears_without_polling_loop()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class FocasCapabilityTests
|
||||
// ---- ITagDiscovery ----
|
||||
|
||||
/// <summary>Verifies that DiscoverAsync emits pre-declared tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_emits_pre_declared_tags()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public sealed class FocasCapabilityTests
|
||||
// ---- ISubscribable ----
|
||||
|
||||
/// <summary>Verifies that the initial subscription poll raises an OnDataChange event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subscribe_initial_poll_raises_OnDataChange()
|
||||
{
|
||||
@@ -67,6 +69,7 @@ public sealed class FocasCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync cancels active subscriptions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_cancels_active_subscriptions()
|
||||
{
|
||||
@@ -97,6 +100,7 @@ public sealed class FocasCapabilityTests
|
||||
// ---- IHostConnectivityProbe ----
|
||||
|
||||
/// <summary>Verifies that GetHostStatuses returns one entry per device.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHostStatuses_returns_entry_per_device()
|
||||
{
|
||||
@@ -115,6 +119,7 @@ public sealed class FocasCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe transitions to Running on successful connection.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Running_on_success()
|
||||
{
|
||||
@@ -142,6 +147,7 @@ public sealed class FocasCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe transitions to Stopped on connection failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Stopped_on_failure()
|
||||
{
|
||||
@@ -171,6 +177,7 @@ public sealed class FocasCapabilityTests
|
||||
// ---- IPerCallHostResolver ----
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns the declared device for a known tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_returns_declared_device_for_known_tag()
|
||||
{
|
||||
@@ -195,6 +202,7 @@ public sealed class FocasCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to the first device for unknown tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_first_device_for_unknown()
|
||||
{
|
||||
@@ -209,6 +217,7 @@ public sealed class FocasCapabilityTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to the driver instance ID when no devices are configured.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices()
|
||||
{
|
||||
@@ -235,40 +244,27 @@ public sealed class FocasCapabilityTests
|
||||
/// <summary>Gets the list of recorded variable calls.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Records a folder call.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
/// <returns>This builder for chaining.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Records a variable call.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="info">The driver attribute information.</param>
|
||||
/// <returns>A variable handle for the recorded variable.</returns>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>Records a property call (no-op).</summary>
|
||||
/// <param name="_">The property name (unused).</param>
|
||||
/// <param name="__">The property data type (unused).</param>
|
||||
/// <param name="___">The property value (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
/// <summary>Marks as alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <returns>An alarm condition sink.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
/// <summary>Null alarm condition sink.</summary>
|
||||
private sealed class NullSink : IAlarmConditionSink {
|
||||
/// <summary>Handles transition (no-op).</summary>
|
||||
/// <param name="args">The alarm event arguments (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
|
||||
+13
-16
@@ -21,6 +21,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
// ---- Driver.FOCAS-003: unknown DeviceHostAddress fails at InitializeAsync ----
|
||||
|
||||
/// <summary>Verifies that initialization throws when a tag references an undeclared device.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_throws_when_tag_DeviceHostAddress_not_in_Devices()
|
||||
{
|
||||
@@ -43,6 +44,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that initialization errors name the offending tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_throws_naming_the_offending_tag()
|
||||
{
|
||||
@@ -64,6 +66,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that initialization succeeds when all tags reference declared devices.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_succeeds_when_all_tags_reference_declared_devices()
|
||||
{
|
||||
@@ -90,6 +93,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
// ---- Driver.FOCAS-004: all FOCAS user tags advertised as ViewOnly ----
|
||||
|
||||
/// <summary>Verifies that all user tags are advertised as ViewOnly regardless of Writable setting.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_all_user_tags_are_ViewOnly_regardless_of_Writable_field()
|
||||
{
|
||||
@@ -119,6 +123,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
// ---- Driver.FOCAS-005: Volatile-guarded _health survives concurrent reads ----
|
||||
|
||||
/// <summary>Verifies that GetHealth reflects state updated from concurrent reads.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHealth_reflects_state_updated_from_concurrent_reads()
|
||||
{
|
||||
@@ -148,6 +153,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
// ---- Driver.FOCAS-006: EnsureConnectedAsync recreates a disposed/stale client ----
|
||||
|
||||
/// <summary>Verifies that reads recover after client is externally disposed.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_recovers_after_client_is_externally_disposed()
|
||||
{
|
||||
@@ -184,6 +190,7 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reads dispose stale clients before creating fresh ones.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_disposes_stale_client_before_creating_fresh_one()
|
||||
{
|
||||
@@ -257,41 +264,31 @@ public sealed class FocasDriverMediumFindingsTests
|
||||
/// <summary>Gets or sets the list of added folders.</summary>
|
||||
public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
|
||||
|
||||
/// <summary>Records a folder and returns this builder for chaining.</summary>
|
||||
/// <param name="browseName">The OPC UA browse name for the folder.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{ Folders.Add((browseName, displayName)); return this; }
|
||||
|
||||
/// <summary>Records a variable and returns a handle for it.</summary>
|
||||
/// <param name="browseName">The OPC UA browse name for the variable.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="info">The driver attribute information for the variable.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
|
||||
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
|
||||
|
||||
/// <summary>No-op property addition for test compatibility.</summary>
|
||||
/// <param name="_">The property name (unused).</param>
|
||||
/// <param name="__">The property data type (unused).</param>
|
||||
/// <param name="___">The property value (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
/// <summary>Test variable handle implementation.</summary>
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference path of this variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
|
||||
/// <summary>Marks this variable as an alarm condition and returns a sink for it.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
|
||||
/// <summary>No-op alarm condition sink for testing.</summary>
|
||||
private sealed class NullSink : IAlarmConditionSink
|
||||
{
|
||||
/// <summary>Handles alarm condition transitions (no-op for testing).</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public sealed class FocasFactoryConfigTests
|
||||
// ---- Driver.FOCAS-002: fixed-tree bootstrap must not declare a false ProgramInfo capability ----
|
||||
|
||||
/// <summary>Verifies that ProgramInfo is marked unsupported when probe throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FixedTree_bootstrap_marks_ProgramInfo_unsupported_when_probe_throws()
|
||||
{
|
||||
@@ -146,6 +147,7 @@ public sealed class FocasFactoryConfigTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ProgramInfo is marked supported when probe succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FixedTree_bootstrap_marks_ProgramInfo_supported_when_probe_succeeds()
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user