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:
+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()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
public sealed class FocasHandleRecycleTests
|
||||
{
|
||||
/// <summary>Verifies that the recycle loop disposes clients on interval and reopens fresh ones.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Recycle_loop_disposes_client_on_interval_reads_reopen_fresh_one()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public sealed class FocasHandleRecycleTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the recycle loop stays off when not enabled.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Recycle_loop_stays_off_when_not_enabled()
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ public sealed class FocasLoggingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that probe loop 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()
|
||||
{
|
||||
@@ -90,9 +91,11 @@ public sealed class FocasLoggingTests
|
||||
/// <summary>Begins a logging scope.</summary>
|
||||
/// <param name="state">The scope state.</param>
|
||||
/// <typeparam name="TState">The type of the state.</typeparam>
|
||||
/// <returns>A disposable scope object.</returns>
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
/// <summary>Checks if logging is enabled for the specified level.</summary>
|
||||
/// <param name="logLevel">The log level.</param>
|
||||
/// <returns><c>true</c> if logging is enabled for <paramref name="logLevel"/>; otherwise, <c>false</c>.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
/// <summary>Logs a message.</summary>
|
||||
/// <param name="logLevel">The log level.</param>
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class FocasLowFindingsTests
|
||||
/// <summary>
|
||||
/// Verifies that ReadAsync uses cached FocasAddress when tag definition has a malformed address after init.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_uses_cached_FocasAddress_when_tag_definition_has_a_malformed_address_after_init()
|
||||
{
|
||||
@@ -59,6 +60,7 @@ public sealed class FocasLowFindingsTests
|
||||
/// <summary>
|
||||
/// Verifies that WriteAsync also uses cached FocasAddress.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAsync_uses_cached_FocasAddress_too()
|
||||
{
|
||||
@@ -99,6 +101,7 @@ public sealed class FocasLowFindingsTests
|
||||
/// <summary>
|
||||
/// Verifies that ProbeLoop cancels a slow ProbeAsync at Probe Timeout.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProbeLoop_cancels_a_slow_ProbeAsync_at_Probe_Timeout()
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class FocasPmcBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a bit set operation surfaces as Good status and flips the bit.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_set_surfaces_as_Good_status_and_flips_bit()
|
||||
{
|
||||
@@ -81,6 +82,7 @@ public sealed class FocasPmcBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing a bit preserves other bits.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_clear_preserves_other_bits()
|
||||
{
|
||||
@@ -95,6 +97,7 @@ public sealed class FocasPmcBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subsequent bit sets in the same byte compose correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Subsequent_bit_sets_in_same_byte_compose_correctly()
|
||||
{
|
||||
@@ -112,6 +115,7 @@ public sealed class FocasPmcBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit writes to different bytes do not contend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_write_to_different_bytes_does_not_contend()
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class FocasReadWriteTests
|
||||
// ---- Read ----
|
||||
|
||||
/// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful PMC read returns a Good status value.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_PMC_read_returns_Good_value()
|
||||
{
|
||||
@@ -48,6 +50,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parameter reads route through the FocasAddress Parameter kind.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Parameter_read_routes_through_FocasAddress_Parameter_kind()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that macro reads route through the FocasAddress Macro kind.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Macro_read_routes_through_FocasAddress_Macro_kind()
|
||||
{
|
||||
@@ -75,6 +79,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated reads reuse the connection.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Repeat_read_reuses_connection()
|
||||
{
|
||||
@@ -91,6 +96,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that FOCAS error statuses map correctly via the status mapper.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FOCAS_error_status_maps_via_status_mapper()
|
||||
{
|
||||
@@ -109,6 +115,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a read exception surfaces BadCommunicationError.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_exception_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -123,6 +130,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a connection failure disposes the client and surfaces BadCommunicationError.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Connect_failure_disposes_client_and_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -137,6 +145,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batched reads preserve order across different address areas.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Batched_reads_preserve_order_across_areas()
|
||||
{
|
||||
@@ -164,6 +173,7 @@ public sealed class FocasReadWriteTests
|
||||
// ---- Write ----
|
||||
|
||||
/// <summary>Verifies that a non-writable tag write is rejected with BadNotWritable.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Non_writable_tag_rejected_with_BadNotWritable()
|
||||
{
|
||||
@@ -177,6 +187,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful write logs the address, type, and value.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Successful_write_logs_address_type_value()
|
||||
{
|
||||
@@ -195,6 +206,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write status codes map correctly via the FocasStatusMapper.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_status_code_maps_via_FocasStatusMapper()
|
||||
{
|
||||
@@ -214,6 +226,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batched writes preserve order across different outcomes.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Batch_write_preserves_order_across_outcomes()
|
||||
{
|
||||
@@ -243,6 +256,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation signals are propagated.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_propagates()
|
||||
{
|
||||
@@ -260,6 +274,7 @@ public sealed class FocasReadWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync disposes the client.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_disposes_client()
|
||||
{
|
||||
|
||||
@@ -201,6 +201,7 @@ public sealed class FocasScaffoldingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies InitializeAsync parses device addresses correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_parses_device_addresses()
|
||||
{
|
||||
@@ -221,6 +222,7 @@ public sealed class FocasScaffoldingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies InitializeAsync faults on malformed addresses.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_malformed_address_faults()
|
||||
{
|
||||
@@ -235,6 +237,7 @@ public sealed class FocasScaffoldingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies ShutdownAsync clears all devices.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_clears_devices()
|
||||
{
|
||||
|
||||
+5
@@ -51,6 +51,7 @@ public sealed class GalaxyBrowseSessionTests
|
||||
/// <summary>Each session must publish a distinct <see cref="GalaxyBrowseSession.Token"/>
|
||||
/// so the AdminUI registry can disambiguate concurrent browse sessions against the
|
||||
/// same driver config.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Token_is_unique_per_session()
|
||||
{
|
||||
@@ -63,6 +64,7 @@ public sealed class GalaxyBrowseSessionTests
|
||||
/// <summary><see cref="GalaxyBrowseSession.LastUsedUtc"/> is primed to the
|
||||
/// construction time so the registry reaper has a sensible baseline before the
|
||||
/// first Root/Expand/Attributes call lands.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LastUsedUtc_is_initialized_at_construction()
|
||||
{
|
||||
@@ -79,6 +81,7 @@ public sealed class GalaxyBrowseSessionTests
|
||||
/// registry's reaper may race a client-initiated close, so the second call must
|
||||
/// no-op rather than throw <see cref="ObjectDisposedException"/> or hit the
|
||||
/// already-disposed gRPC channel.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisposeAsync_is_idempotent()
|
||||
{
|
||||
@@ -91,6 +94,7 @@ public sealed class GalaxyBrowseSessionTests
|
||||
/// <summary>After disposal, any <see cref="GalaxyBrowseSession.ExpandAsync"/> call
|
||||
/// must surface <see cref="ObjectDisposedException"/> — not a downstream channel
|
||||
/// fault — so the AdminUI sees a clean "session closed" signal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExpandAsync_after_dispose_throws_ObjectDisposedException()
|
||||
{
|
||||
@@ -104,6 +108,7 @@ public sealed class GalaxyBrowseSessionTests
|
||||
/// hasn't been seen by a prior Root/Expand call — the cache is the source of
|
||||
/// truth, and silently returning [] would mask AdminUI bugs that browse with a
|
||||
/// stale path.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExpandAsync_unknown_tag_throws_ArgumentException()
|
||||
{
|
||||
|
||||
+3
@@ -23,6 +23,7 @@ public sealed class GalaxyDriverBrowserTests
|
||||
|
||||
/// <summary>An empty Gateway.Endpoint must fail fast with a clear, endpoint-mentioning
|
||||
/// message rather than surfacing a downstream gRPC URI parse error.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_empty_endpoint_throws_InvalidOperationException()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public sealed class GalaxyDriverBrowserTests
|
||||
|
||||
/// <summary>An empty MxAccess.ClientName must fail fast — refused so the gateway
|
||||
/// side doesn't see anonymous browse sessions during triage.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_empty_clientName_throws_InvalidOperationException()
|
||||
{
|
||||
@@ -45,6 +47,7 @@ public sealed class GalaxyDriverBrowserTests
|
||||
|
||||
/// <summary>A JSON literal that deserializes to null must fail fast with a
|
||||
/// "deserialized to null" message — never a downstream NRE.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_null_json_throws_InvalidOperationException()
|
||||
{
|
||||
|
||||
@@ -120,6 +120,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bootstrap deploy events are suppressed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BootstrapEventIsSuppressed()
|
||||
{
|
||||
@@ -143,6 +144,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deployment time change fires a rediscovery event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeployTimeChangeFiresRediscover()
|
||||
{
|
||||
@@ -171,6 +173,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the same deployment time does not fire a rediscovery event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SameDeployTimeDoesNotFire()
|
||||
{
|
||||
@@ -195,6 +198,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deployment time presence flip fires a rediscovery event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TimeOfLastDeployPresentFlipFiresRediscover()
|
||||
{
|
||||
@@ -222,6 +226,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stop cancels the watcher loop cleanly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StopCancelsLoopCleanly()
|
||||
{
|
||||
@@ -243,6 +248,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing stops a running watcher.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisposeStopsRunningWatcher()
|
||||
{
|
||||
@@ -262,6 +268,7 @@ public sealed class DeployWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a source exception triggers retry with backoff.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SourceExceptionTriggersRetryWithBackoff()
|
||||
{
|
||||
|
||||
+20
-35
@@ -16,9 +16,7 @@ public sealed class GalaxyDiscovererTests
|
||||
{
|
||||
private sealed class FakeHierarchySource(IReadOnlyList<GalaxyObject> objects) : IGalaxyHierarchySource
|
||||
{
|
||||
/// <summary>Gets the hierarchy asynchronously from the fake source.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that returns the pre-built Galaxy object list.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(objects);
|
||||
}
|
||||
@@ -41,10 +39,7 @@ public sealed class GalaxyDiscovererTests
|
||||
public FakeBuilder() : this(null) { }
|
||||
private FakeBuilder(string? folder) { _currentFolder = folder; }
|
||||
|
||||
/// <summary>Adds a folder call to the recorded list.</summary>
|
||||
/// <param name="browseName">The browse name for the folder.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
/// <returns>An IAddressSpaceBuilder scoped to the new folder.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{
|
||||
Folders.Add(new FolderCall(browseName, displayName));
|
||||
@@ -52,11 +47,7 @@ public sealed class GalaxyDiscovererTests
|
||||
return new ChildBuilder(this, browseName);
|
||||
}
|
||||
|
||||
/// <summary>Adds a variable call to the recorded list.</summary>
|
||||
/// <param name="browseName">The browse name for the variable.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="attributeInfo">The attribute metadata for the variable.</param>
|
||||
/// <returns>An IVariableHandle for further configuration.</returns>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
{
|
||||
var folder = _currentFolder ?? "<root>";
|
||||
@@ -64,51 +55,36 @@ public sealed class GalaxyDiscovererTests
|
||||
return new FakeVariableHandle(this, attributeInfo.FullName);
|
||||
}
|
||||
|
||||
/// <summary>Adds a property call to the builder (not recorded in this fake).</summary>
|
||||
/// <param name="browseName">The browse name for the property.</param>
|
||||
/// <param name="dataType">The driver data type of the property.</param>
|
||||
/// <param name="value">The property value.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
|
||||
/// <summary>Child folder routes Variable calls back to the parent's lists with its own scope.</summary>
|
||||
private sealed class ChildBuilder(FakeBuilder parent, string folderBrowseName) : IAddressSpaceBuilder
|
||||
{
|
||||
/// <summary>Adds a child folder call to the parent builder's recorded list.</summary>
|
||||
/// <param name="browseName">The browse name for the folder.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
/// <returns>An IAddressSpaceBuilder scoped to the new child folder.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{
|
||||
parent.Folders.Add(new FolderCall(browseName, displayName));
|
||||
return new ChildBuilder(parent, browseName);
|
||||
}
|
||||
|
||||
/// <summary>Adds a variable call to the parent builder's recorded list, scoped to this folder.</summary>
|
||||
/// <param name="browseName">The browse name for the variable.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="attributeInfo">The attribute metadata for the variable.</param>
|
||||
/// <returns>An IVariableHandle for further configuration.</returns>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
{
|
||||
parent.Variables.Add(new VariableCall(folderBrowseName, browseName, attributeInfo));
|
||||
return new FakeVariableHandle(parent, attributeInfo.FullName);
|
||||
}
|
||||
|
||||
/// <summary>Adds a property call to the builder (not recorded in this fake).</summary>
|
||||
/// <param name="browseName">The browse name for the property.</param>
|
||||
/// <param name="dataType">The driver data type of the property.</param>
|
||||
/// <param name="value">The property value.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
}
|
||||
|
||||
private sealed class FakeVariableHandle(FakeBuilder owner, string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference for this variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference { get; } = fullRef;
|
||||
|
||||
/// <summary>Marks this variable as an alarm condition and records it.</summary>
|
||||
/// <param name="info">The alarm condition metadata.</param>
|
||||
/// <returns>An IAlarmConditionSink for further alarm configuration.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
|
||||
{
|
||||
owner.AlarmDeclarations[FullReference] = info;
|
||||
@@ -118,8 +94,7 @@ public sealed class GalaxyDiscovererTests
|
||||
|
||||
private sealed class NoopSink : IAlarmConditionSink
|
||||
{
|
||||
/// <summary>Records an alarm transition event (no-op in this fake).</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
@@ -156,6 +131,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that discovery creates one folder per object and one variable per attribute.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_BuildsOneFolderPerObject_AndOneVariablePerAttribute()
|
||||
{
|
||||
@@ -178,6 +154,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that full reference defaults to tag.attribute format when not explicitly supplied.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FullReference_DefaultsToTagDotAttribute()
|
||||
{
|
||||
@@ -193,6 +170,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that full reference uses gateway-supplied value when provided.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FullReference_PrefersGwSuppliedFullTagReference()
|
||||
{
|
||||
@@ -208,6 +186,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that browse name falls back to tag name when contained name is empty.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_BrowseName_FallsBackToTagName_WhenContainedEmpty()
|
||||
{
|
||||
@@ -223,6 +202,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that attribute metadata fields are all propagated to the discovered variable.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_AttributeMetadata_PropagatesEveryField()
|
||||
{
|
||||
@@ -248,6 +228,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm attributes populate all five sub-attribute references.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_AlarmAttribute_PopulatesAllFiveSubAttributeRefs()
|
||||
{
|
||||
@@ -270,6 +251,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-alarm attributes are not marked as alarm conditions.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_NonAlarmAttribute_DoesNotMarkCondition()
|
||||
{
|
||||
@@ -287,6 +269,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that objects with empty identity are skipped during discovery.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_SkipsObjectsWithEmptyIdentity()
|
||||
{
|
||||
@@ -304,6 +287,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that attributes with empty names are skipped during discovery.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_SkipsAttributesWithEmptyName()
|
||||
{
|
||||
@@ -320,6 +304,7 @@ public sealed class GalaxyDiscovererTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that driver discovery routes through the injected hierarchy source.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DriverDiscoverAsync_RoutesThroughInjectedSource()
|
||||
{
|
||||
|
||||
+4
-3
@@ -17,6 +17,7 @@ public sealed class GalaxyDriverAlarmEventArgsExtensionTests
|
||||
/// <summary>
|
||||
/// Verifies that acknowledge transition with full payload populates extended fields.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Acknowledge_transition_with_full_payload_populates_extended_fields()
|
||||
{
|
||||
@@ -53,6 +54,7 @@ public sealed class GalaxyDriverAlarmEventArgsExtensionTests
|
||||
/// <summary>
|
||||
/// Verifies that raise transition without optional fields leaves them null.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Raise_transition_without_optional_fields_leaves_them_null()
|
||||
{
|
||||
@@ -118,9 +120,8 @@ public sealed class GalaxyDriverAlarmEventArgsExtensionTests
|
||||
public void Emit(GalaxyAlarmTransition transition)
|
||||
=> OnAlarmTransition?.Invoke(this, transition);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the alarm feed asynchronously.
|
||||
/// </summary>
|
||||
/// <summary>Disposes the alarm feed asynchronously.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
|
||||
public sealed class GalaxyDriverAlarmSourceTests
|
||||
{
|
||||
/// <summary>Verifies that SubscribeAlarmsAsync starts the alarm feed and events fire on transition.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_starts_feed_and_event_fires_on_transition()
|
||||
{
|
||||
@@ -62,6 +63,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UnsubscribeAlarmsAsync stops event flow.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAlarmsAsync_stops_event_flow()
|
||||
{
|
||||
@@ -84,6 +86,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UnsubscribeAlarmsAsync throws for a foreign handle.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAlarmsAsync_throws_for_foreign_handle()
|
||||
{
|
||||
@@ -97,6 +100,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync routes each request to the acknowledger.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_routes_each_request_to_the_acknowledger()
|
||||
{
|
||||
@@ -119,6 +123,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync falls back to SourceNodeId when ConditionId is empty.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_falls_back_to_SourceNodeId_when_ConditionId_empty()
|
||||
{
|
||||
@@ -134,6 +139,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync throws NotSupportedException without an acknowledger.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_throws_NotSupported_without_acknowledger()
|
||||
{
|
||||
@@ -191,7 +197,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
/// <summary>Occurs when an alarm transition is emitted.</summary>
|
||||
public event EventHandler<GalaxyAlarmTransition>? OnAlarmTransition;
|
||||
|
||||
/// <summary>Marks the feed as started.</summary>
|
||||
/// <inheritdoc />
|
||||
public void Start() => Started = true;
|
||||
|
||||
/// <summary>Emits an alarm transition to all subscribers.</summary>
|
||||
@@ -210,12 +216,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
/// <summary>Gets the list of acknowledge calls recorded.</summary>
|
||||
public List<(string AlarmRef, string Comment, string Operator)> Calls { get; } = [];
|
||||
|
||||
/// <summary>Records an acknowledge call.</summary>
|
||||
/// <param name="alarmFullReference">The alarm full reference.</param>
|
||||
/// <param name="comment">The acknowledgment comment.</param>
|
||||
/// <param name="operatorUser">The operator user.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task AcknowledgeAsync(string alarmFullReference, string comment, string operatorUser, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls.Add((alarmFullReference, comment, operatorUser));
|
||||
@@ -226,7 +227,7 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
/// <summary>Test double that represents a foreign alarm subscription handle.</summary>
|
||||
private sealed class ForeignAlarmHandle : IAlarmSubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic ID for this handle.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => "foreign";
|
||||
}
|
||||
}
|
||||
|
||||
+14
-3
@@ -130,13 +130,24 @@ public sealed class GalaxyDriverApiKeyResolverTests
|
||||
/// <summary>Gets the list of captured log entries with their levels and messages.</summary>
|
||||
public List<(LogLevel Level, string Message)> Entries { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Begins a logging scope (returns null — no scope support in test logger).</summary>
|
||||
/// <typeparam name="TState">The type of the state object.</typeparam>
|
||||
/// <param name="state">The state object.</param>
|
||||
/// <returns>Null — test logger has no scope support.</returns>
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Returns true for all log levels so all messages are captured.</summary>
|
||||
/// <param name="logLevel">The log level to check.</param>
|
||||
/// <returns>Always true.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Captures the log message and level into the Entries list.</summary>
|
||||
/// <typeparam name="TState">The type of the state object.</typeparam>
|
||||
/// <param name="logLevel">The log level.</param>
|
||||
/// <param name="eventId">The event ID.</param>
|
||||
/// <param name="state">The state object.</param>
|
||||
/// <param name="exception">The exception, if any.</param>
|
||||
/// <param name="formatter">The formatter function.</param>
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
=> Entries.Add((logLevel, formatter(state, exception)));
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ public sealed class GalaxyDriverFactoryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that driver lifecycle toggles health state on initialize and shutdown.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DriverLifecycle_InitializeShutdown_ToggleHealth()
|
||||
{
|
||||
@@ -148,6 +149,7 @@ public sealed class GalaxyDriverFactoryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reinitializing with equivalent config refreshes health.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_RefreshesHealth_WhenConfigIsEquivalent()
|
||||
{
|
||||
@@ -218,6 +220,7 @@ public sealed class GalaxyDriverFactoryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that initializing after dispose throws an exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InitializeAfterDispose_Throws()
|
||||
{
|
||||
@@ -244,6 +247,7 @@ public sealed class GalaxyDriverFactoryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetHostStatuses returns an empty snapshot after initialization with seam.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHostStatuses_AfterInitWithSeam_ReturnsEmptySnapshot()
|
||||
{
|
||||
|
||||
+14
-18
@@ -34,6 +34,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that memory footprint is nonzero after subscriptions are active.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMemoryFootprint_IsNonZeroAfterSubscribe()
|
||||
{
|
||||
@@ -48,6 +49,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that memory footprint decreases after unsubscribing.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMemoryFootprint_DecreasesAfterUnsubscribe()
|
||||
{
|
||||
@@ -67,6 +69,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
// ===== Driver.Galaxy-007 regression: Dispose cancels the dispose CTS =====
|
||||
|
||||
/// <summary>Verifies that Dispose sets disposed flag and blocks further capability calls.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_SetsDisposedFlag_BlockingFurtherCapabilityCalls()
|
||||
{
|
||||
@@ -81,6 +84,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DisposeAsync can be awaited without deadlock.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisposeAsync_CanBeAwaitedWithoutDeadlock()
|
||||
{
|
||||
@@ -93,6 +97,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
// ===== Driver.Galaxy-013 regression: ReplayOnSessionLost gates the replay step =====
|
||||
|
||||
/// <summary>Verifies that ReplayOnSessionLost=false skips resubscription on reconnect.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReplayOnSessionLost_False_SkipsResubscribeBulk()
|
||||
{
|
||||
@@ -123,6 +128,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReplayOnSessionLost=true runs resubscription on reconnect.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReplayOnSessionLost_True_RunsResubscribeBulk()
|
||||
{
|
||||
@@ -152,10 +158,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
/// <summary>Gets the count of subscription calls.</summary>
|
||||
public int SubscribeCalls;
|
||||
|
||||
/// <summary>Subscribes to multiple tags and counts the call.</summary>
|
||||
/// <param name="fullReferences">List of tag addresses to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">Buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -169,14 +172,11 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from multiple tags.</summary>
|
||||
/// <param name="itemHandles">List of subscription handles to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams subscription events.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
}
|
||||
@@ -184,6 +184,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
// ===== Driver.Galaxy-013 regression: ReinitializeAsync rejects unsupported reapply =====
|
||||
|
||||
/// <summary>Verifies that ReinitializeAsync rejects non-equivalent config changes.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_RejectsNonEquivalentConfigChange()
|
||||
{
|
||||
@@ -203,6 +204,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReinitializeAsync accepts equivalent config.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_AcceptsEquivalentConfig()
|
||||
{
|
||||
@@ -227,10 +229,7 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
{
|
||||
private readonly Channel<MxEvent> _stream = Channel.CreateUnbounded<MxEvent>();
|
||||
|
||||
/// <summary>Subscribes to multiple tags (no-op).</summary>
|
||||
/// <param name="fullReferences">List of tag addresses to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">Buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -243,14 +242,11 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from multiple tags (no-op).</summary>
|
||||
/// <param name="itemHandles">List of subscription handles to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams subscription events (no-op).</summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
}
|
||||
|
||||
+12
-9
@@ -27,10 +27,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
/// <summary>Gets a mapping of tag addresses to their assigned item handles.</summary>
|
||||
public Dictionary<string, int> HandleByAddress { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Simulates a bulk subscribe operation by generating handles for each reference.</summary>
|
||||
/// <param name="fullReferences">The list of tag addresses to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -51,17 +48,14 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
/// <summary>Simulates a bulk unsubscribe operation by recording the handles.</summary>
|
||||
/// <param name="itemHandles">The list of item handles to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
{
|
||||
Unsubscribes.Add([.. itemHandles]);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Returns an empty event stream for testing.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> Empty();
|
||||
|
||||
@@ -73,6 +67,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that syncing platforms subscribes to the ScanState address for each platform.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SyncPlatformsAsync_SubscribesScanStateAddressForEachPlatform()
|
||||
{
|
||||
@@ -88,6 +83,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the default buffered interval is zero, matching gateway cadence.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SyncPlatformsAsync_DefaultBufferedIntervalIsZero_GwCadence()
|
||||
{
|
||||
@@ -100,6 +96,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configured buffered interval is forwarded to the gateway.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SyncPlatformsAsync_ConfiguredBufferedInterval_IsForwardedToGw()
|
||||
{
|
||||
@@ -125,6 +122,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that syncing the same platform set twice does not resubscribe.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SyncPlatformsAsync_SameSetTwice_DoesNotResubscribe()
|
||||
{
|
||||
@@ -139,6 +137,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that removed platforms are unsubscribed and dropped from the aggregator.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SyncPlatformsAsync_RemovedPlatforms_AreUnsubscribed_AndDroppedFromAggregator()
|
||||
{
|
||||
@@ -183,6 +182,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a running probe value is routed to the aggregator.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnProbeValueChanged_Running_RoutesToAggregator()
|
||||
{
|
||||
@@ -198,6 +198,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a probe value with bad quality routes as unknown state.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnProbeValueChanged_BadQuality_RoutesUnknown()
|
||||
{
|
||||
@@ -212,6 +213,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that foreign probe references are silently dropped.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnProbeValueChanged_ForeignReference_IsSilentlyDropped()
|
||||
{
|
||||
@@ -231,6 +233,7 @@ public sealed class PerPlatformProbeWatcherTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dispose unsubscribes all tracked platforms.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_UnsubscribesAllTrackedPlatforms()
|
||||
{
|
||||
|
||||
+6
-12
@@ -18,6 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
public sealed class EventPumpBoundedChannelTests
|
||||
{
|
||||
/// <summary>Verifies that the event pump drops newest events when the bounded channel fills and records metrics for dropped events.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Drops_newest_when_channel_fills_and_records_metric()
|
||||
{
|
||||
@@ -68,6 +69,7 @@ public sealed class EventPumpBoundedChannelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the event pump throws an exception when the channel capacity is invalid.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Throws_when_channelCapacity_is_invalid()
|
||||
{
|
||||
@@ -81,6 +83,7 @@ public sealed class EventPumpBoundedChannelTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that event pump metrics are tagged with the client name for tracking multiple driver hosts.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tags_metrics_with_client_name_for_multi_driver_hosts()
|
||||
{
|
||||
@@ -189,25 +192,16 @@ public sealed class EventPumpBoundedChannelTests
|
||||
private readonly Channel<MxEvent> _stream =
|
||||
Channel.CreateUnbounded<MxEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
/// <summary>Subscribes to a bulk list of tag references.</summary>
|
||||
/// <param name="fullReferences">The list of full references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An empty result list.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
|
||||
|
||||
/// <summary>Unsubscribes from a bulk list of item handles.</summary>
|
||||
/// <param name="itemHandles">The list of item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams events asynchronously.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An async enumerable of MxEvent objects.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
|
||||
+11
-18
@@ -19,6 +19,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
private const int WaitMs = 2_000;
|
||||
|
||||
/// <summary>Verifies that stream fault invokes the callback with the exception.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StreamFault_InvokesOnStreamFaultCallback_WithTheCause()
|
||||
{
|
||||
@@ -42,6 +43,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stream fault drives the reconnect supervisor through reopen and replay.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StreamFault_DrivesReconnectSupervisorReopenReplay()
|
||||
{
|
||||
@@ -79,6 +81,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a faulted pump cannot be restarted in place, but a fresh pump resumes dispatch.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FaultedPump_IsNotRestartableInPlace_ButAFreshPumpResumesDispatch()
|
||||
{
|
||||
@@ -122,6 +125,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clean shutdown does not invoke the stream fault callback.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CleanShutdown_DoesNotInvokeOnStreamFault()
|
||||
{
|
||||
@@ -150,22 +154,16 @@ public sealed class EventPumpStreamFaultTests
|
||||
private readonly Channel<MxEvent> _stream =
|
||||
Channel.CreateUnbounded<MxEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
/// <summary>Subscribes to multiple tags (test stub).</summary>
|
||||
/// <param name="fullReferences">The tag references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
|
||||
|
||||
/// <summary>Unsubscribes from multiple tags (test stub).</summary>
|
||||
/// <param name="itemHandles">The item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams events asynchronously (test stub).</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
@@ -183,28 +181,23 @@ public sealed class EventPumpStreamFaultTests
|
||||
private readonly Channel<MxEvent> _stream =
|
||||
Channel.CreateUnbounded<MxEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
/// <summary>Subscribes to multiple tags (test stub).</summary>
|
||||
/// <param name="fullReferences">The tag references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
|
||||
|
||||
/// <summary>Unsubscribes from multiple tags (test stub).</summary>
|
||||
/// <param name="itemHandles">The item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams events asynchronously (test stub).</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
/// <summary>Emits a data change event asynchronously.</summary>
|
||||
/// <param name="itemHandle">The item handle for the data change.</param>
|
||||
/// <param name="value">The numeric value of the change.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation.</returns>
|
||||
public ValueTask EmitAsync(int itemHandle, double value) =>
|
||||
_stream.Writer.WriteAsync(new MxEvent
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync routes through the injected reader.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_RoutesThroughInjectedReader()
|
||||
{
|
||||
@@ -58,6 +59,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync returns empty without calling the reader for an empty request.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_EmptyRequest_ReturnsEmpty_WithoutCallingReader()
|
||||
{
|
||||
@@ -71,6 +73,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync throws when seams and production runtime are not built.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_NoSeams_AndNoProductionRuntime_Throws()
|
||||
{
|
||||
@@ -86,6 +89,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync throws after the driver is disposed.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_AfterDispose_Throws()
|
||||
{
|
||||
@@ -96,6 +100,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync resolves from the first OnDataChange event on the subscribe-once path.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_SubscribeOncePath_ResolvesFromFirstOnDataChange()
|
||||
{
|
||||
@@ -120,6 +125,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync surfaces rejected tags as bad status on the subscribe-once path.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_SubscribeOncePath_RejectedTagSurfacesAsBadStatus()
|
||||
{
|
||||
@@ -140,6 +146,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync preserves reader status codes.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_PreservesReaderStatusCodes()
|
||||
{
|
||||
|
||||
+13
-13
@@ -40,11 +40,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
/// <summary>Gets or sets a function to decide whether to accept a subscription.</summary>
|
||||
public Func<string, bool> Decide { get; set; } = _ => true;
|
||||
|
||||
/// <summary>Subscribes to bulk updates for the specified tag references.</summary>
|
||||
/// <param name="fullReferences">The tag references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A list of subscription results.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -77,19 +73,14 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from bulk updates for the specified item handles.</summary>
|
||||
/// <param name="itemHandles">The handles to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
{
|
||||
UnsubscribedHandles.AddRange(itemHandles);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Streams events asynchronously.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>An async enumerable of MX events.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _events.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
@@ -113,6 +104,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription allocates a handle and dispatches value changes.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_AllocatesHandle_AndDispatchesValueChange()
|
||||
{
|
||||
@@ -136,6 +128,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies two subscriptions for the same tag each receive updates.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_TwoSubscriptions_SameTag_FanOutOnePerSubscription()
|
||||
{
|
||||
@@ -180,6 +173,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies failed subscriptions do not dispatch events.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_FailedTag_DoesNotDispatchEvents()
|
||||
{
|
||||
@@ -201,6 +195,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies unsubscribe removes registration and calls gateway unsubscribe.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_RemovesRegistration_AndCallsGwUnsubscribe()
|
||||
{
|
||||
@@ -224,6 +219,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies unsubscribing with an unknown handle is handled.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_UnknownHandle_NoOp()
|
||||
{
|
||||
@@ -239,6 +235,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription without a subscriber throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_NoSubscriber_Throws()
|
||||
{
|
||||
@@ -249,6 +246,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription falls back to configured interval when zero is passed.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_FallsBackToConfiguredInterval_WhenCallerPassesZero()
|
||||
{
|
||||
@@ -269,6 +267,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription respects caller's interval when non-zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_RespectsCallerInterval_WhenNonZero()
|
||||
{
|
||||
@@ -289,6 +288,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription with empty tag list returns handle without calling gateway.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_EmptyTagList_ReturnsHandleWithoutCallingGw()
|
||||
{
|
||||
@@ -304,7 +304,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
/// <summary>A subscription handle from a foreign source.</summary>
|
||||
private sealed class ForeignHandle : ISubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this handle.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => "foreign-x";
|
||||
}
|
||||
|
||||
|
||||
+14
-22
@@ -26,8 +26,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
|
||||
private sealed class FakeHierarchySource(IReadOnlyList<GalaxyObject> objects) : IGalaxyHierarchySource
|
||||
{
|
||||
/// <summary>Returns the fake Galaxy object hierarchy.</summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(objects);
|
||||
}
|
||||
@@ -37,36 +36,26 @@ public sealed class GalaxyDriverWriteTests
|
||||
/// <summary>Gets the list of variables added to this builder.</summary>
|
||||
public List<DriverAttributeInfo> Variables { get; } = [];
|
||||
|
||||
/// <summary>Adds 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) => this;
|
||||
/// <summary>Adds a variable to the variables 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="attributeInfo">The attribute information for the variable.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
{
|
||||
Variables.Add(attributeInfo);
|
||||
return new FakeHandle(attributeInfo.FullName);
|
||||
}
|
||||
/// <summary>No-op property adding operation for test compatibility.</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 value of the property.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
|
||||
private sealed class FakeHandle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference for this variable handle.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference { get; } = fullRef;
|
||||
/// <summary>Marks this variable as an alarm condition and returns a noop sink.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NoopSink();
|
||||
/// <summary>No-op alarm transition handler.</summary>
|
||||
private sealed class NoopSink : IAlarmConditionSink {
|
||||
/// <summary>Handles alarm state transition events.</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
@@ -77,10 +66,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
/// <summary>Gets the list of write calls received by this writer.</summary>
|
||||
public List<(string FullRef, object? Value, SecurityClassification Resolved)> Calls { get; } = [];
|
||||
|
||||
/// <summary>Records write requests with their resolved security classifications.</summary>
|
||||
/// <param name="writes">The list of write requests to process.</param>
|
||||
/// <param name="securityResolver">Function to resolve security classification for each request.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
Func<string, SecurityClassification> securityResolver,
|
||||
@@ -107,6 +93,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync routes through the injected writer and propagates values correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAsync_RoutesThroughInjectedWriter_AndPropagatesValues()
|
||||
{
|
||||
@@ -133,6 +120,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
/// <summary>Verifies that WriteAsync resolves every security classification from discovery data.</summary>
|
||||
/// <param name="mxSec">The raw MXAccess security integer from the discovery attribute.</param>
|
||||
/// <param name="expected">The expected resolved security classification.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(0, SecurityClassification.FreeAccess)]
|
||||
[InlineData(1, SecurityClassification.Operate)]
|
||||
@@ -157,6 +145,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown tags resolve to FreeAccess classification and writes proceed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAsync_UnknownTag_ResolvesToFreeAccess_DefaultsToWrite()
|
||||
{
|
||||
@@ -172,6 +161,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty write request returns empty without calling the writer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAsync_EmptyRequest_ReturnsEmpty_WithoutCallingWriter()
|
||||
{
|
||||
@@ -186,6 +176,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync throws when no writer is configured, referencing PR 4.4.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAsync_NoWriter_Throws_PointingAtPR44()
|
||||
{
|
||||
@@ -197,6 +188,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync throws ObjectDisposedException after the driver is disposed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAsync_AfterDispose_Throws()
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ public sealed class GalaxyTelemetryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TracedGalaxySubscriber emits a subscribe_bulk span with tag count.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TracedGalaxySubscriber_emits_subscribe_bulk_span_with_tag_count()
|
||||
{
|
||||
@@ -52,6 +53,7 @@ public sealed class GalaxyTelemetryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TracedGalaxySubscriber records error and rethrows on failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TracedGalaxySubscriber_records_error_and_rethrows_on_failure()
|
||||
{
|
||||
@@ -70,6 +72,7 @@ public sealed class GalaxyTelemetryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TracedGalaxyDataWriter tags the secured write count.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TracedGalaxyDataWriter_tags_secured_write_count()
|
||||
{
|
||||
@@ -106,6 +109,7 @@ public sealed class GalaxyTelemetryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TracedGalaxyHierarchySource tags the object count.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task TracedGalaxyHierarchySource_tags_object_count()
|
||||
{
|
||||
@@ -182,7 +186,9 @@ public sealed class GalaxyTelemetryTests
|
||||
|
||||
private sealed class FakeHierarchy : IGalaxyHierarchySource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>Returns a fixed two-element hierarchy for telemetry testing.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that resolves to a two-element Galaxy object list.</returns>
|
||||
public Task<IReadOnlyList<ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject>> GetHierarchyAsync(
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject>>(
|
||||
|
||||
+2
@@ -21,6 +21,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class GatewayGalaxyAlarmFeedLiveTests
|
||||
{
|
||||
/// <summary>Verifies that the live gateway delivers native alarm transitions through the consumer.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Live_gateway_delivers_native_alarm_transitions_through_the_consumer()
|
||||
{
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
public sealed class GatewayGalaxyAlarmFeedTests
|
||||
{
|
||||
/// <summary>Verifies that the feed decodes active alarm snapshots and live transitions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Decodes_active_alarm_snapshot_then_live_transition()
|
||||
{
|
||||
@@ -70,6 +71,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the feed drops transitions with unspecified kind and empty messages.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Drops_transition_with_unspecified_kind_and_empty_message()
|
||||
{
|
||||
@@ -107,6 +109,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the feed reopens the stream after a transport fault.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reopens_stream_after_a_transport_fault()
|
||||
{
|
||||
|
||||
+7
@@ -28,6 +28,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reporting a transport failure drives the supervisor through reopen and replay cycles back to healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReportTransportFailure_DrivesThroughReopenReplay_BackToHealthy()
|
||||
{
|
||||
@@ -57,6 +58,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reopen failures trigger retries and the supervisor stays in reopening state between attempts.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReopenFailure_RetriesUntilSuccess_StaysInReopeningBetweenAttempts()
|
||||
{
|
||||
@@ -75,6 +77,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that replay failures trigger a retry of the entire reopen-replay cycle.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReplayFailure_RetriesEntireCycle()
|
||||
{
|
||||
@@ -95,6 +98,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated failure reports during recovery do not spawn parallel recovery loops.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RepeatedFailureReports_DuringRecovery_DoNotSpawnParallelLoops()
|
||||
{
|
||||
@@ -122,6 +126,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the last error reflects the most recent failure cause from recovery attempts.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LastError_ReflectsMostRecentFailureCause()
|
||||
{
|
||||
@@ -141,6 +146,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the supervisor cancels a running recovery loop cleanly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_CancelsRunningRecoveryLoop_Cleanly()
|
||||
{
|
||||
@@ -170,6 +176,7 @@ public sealed class ReconnectSupervisorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that waiting for healthy state returns immediately when already healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WaitForHealthy_ReturnsImmediately_WhenAlreadyHealthy()
|
||||
{
|
||||
|
||||
+1
@@ -242,6 +242,7 @@ public sealed class SubscriptionRegistryTests
|
||||
public int TrackedItemHandleCount => _inner.TrackedItemHandleCount;
|
||||
|
||||
/// <summary>Gets the next subscription ID.</summary>
|
||||
/// <returns>A monotonically increasing subscription identifier.</returns>
|
||||
public long NextSubscriptionId() => _inner.NextSubscriptionId();
|
||||
|
||||
/// <summary>Registers a subscription with the given bindings.</summary>
|
||||
|
||||
+2
@@ -68,6 +68,7 @@ internal sealed class FakeSidecarServer : IAsyncDisposable
|
||||
public string PipeName => _pipeName;
|
||||
|
||||
/// <summary>Starts the fake sidecar server asynchronously.</summary>
|
||||
/// <returns>A task that completes once the server listener is ready to accept connections.</returns>
|
||||
public Task StartAsync()
|
||||
{
|
||||
_loop = Task.Run(() => RunAsync(_cts.Token));
|
||||
@@ -194,6 +195,7 @@ internal sealed class FakeSidecarServer : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>Releases all resources used by the fake sidecar server.</summary>
|
||||
/// <returns>A value task that represents the asynchronous dispose operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
|
||||
+18
@@ -29,6 +29,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
CallTimeout: TimeSpan.FromSeconds(2));
|
||||
|
||||
/// <summary>Verifies that ReadRawAsync round-trips samples and maps quality bytes to OPC UA status codes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRawAsync_RoundTripsSamples_AndMapsQualityByteToOpcUaStatusCode()
|
||||
{
|
||||
@@ -71,6 +72,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadProcessedAsync maps null buckets to BadNoData status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadProcessedAsync_NullBuckets_MapToBadNoData()
|
||||
{
|
||||
@@ -103,6 +105,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAtTimeAsync preserves timestamp order.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAtTimeAsync_PreservesTimestampOrder()
|
||||
{
|
||||
@@ -131,6 +134,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAtTimeAsync aligns by timestamp and fills gaps with bad status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAtTimeAsync_PartialAndReorderedReply_AlignsByTimestamp_AndFillsGapsAsBad()
|
||||
{
|
||||
@@ -185,6 +189,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadEventsAsync preserves event field values.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadEventsAsync_PreservesEventFields()
|
||||
{
|
||||
@@ -223,6 +228,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadRawAsync throws InvalidOperationException on server errors.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRawAsync_ServerError_ThrowsInvalidOperation()
|
||||
{
|
||||
@@ -241,6 +247,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteBatchAsync maps per-event results to acknowledge or retry outcomes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_PerEventOk_MapsToAckOrRetryPlease()
|
||||
{
|
||||
@@ -270,6 +277,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteBatchAsync returns retry outcomes for whole call failures.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_WholeCallFailure_ReturnsRetryPleaseForEveryEvent()
|
||||
{
|
||||
@@ -300,6 +308,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Hello handshake throws UnauthorizedAccessException on secret mismatch.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Hello_BadSecret_ThrowsUnauthorizedAccess()
|
||||
{
|
||||
@@ -315,6 +324,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the client retries after a transport drop.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconnect_AfterTransportDrop_RetriesOnce()
|
||||
{
|
||||
@@ -345,6 +355,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetHealthSnapshot tracks success and failure counts.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHealthSnapshot_TracksSuccessAndFailureCounts()
|
||||
{
|
||||
@@ -381,6 +392,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// (2) A transport drop during a write (the catch path in WriteBatchAsync) must return
|
||||
/// RetryPlease for every event in the batch — never throw, never PermanentFail.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_TransportDropDuringWrite_ReturnsRetryPleaseForEveryEvent()
|
||||
{
|
||||
@@ -413,6 +425,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// (3) When both the first attempt and the single retry fail (the "second attempt also
|
||||
/// fails" path in InvokeAsync), the exception propagates to the caller.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_BothAttemptsFailTransport_PropagatesException()
|
||||
{
|
||||
@@ -436,6 +449,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// (4) A stalled sidecar that never sends a reply must cause an
|
||||
/// <see cref="OperationCanceledException"/> within the configured CallTimeout.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRawAsync_StalledSidecar_TimesOutWithOperationCanceledException()
|
||||
{
|
||||
@@ -466,6 +480,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// <see cref="NotSupportedException"/> because Wonderware AnalogSummary has no Total
|
||||
/// aggregate column.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadProcessedAsync_TotalAggregate_ThrowsNotSupported()
|
||||
{
|
||||
@@ -486,6 +501,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// expect (e.g. ReadRawReply where ReadAtTimeReply was expected), the client must throw
|
||||
/// <see cref="InvalidDataException"/>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRawAsync_SidecarRepliesWithWrongKind_ThrowsInvalidDataException()
|
||||
{
|
||||
@@ -513,6 +529,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// a future regression to the "RecordSuccess then undo via ReclassifySuccessAsFailure"
|
||||
/// dance is caught.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHealthSnapshot_SidecarFailure_NeverInflatesSuccessCounter()
|
||||
{
|
||||
@@ -544,6 +561,7 @@ public sealed class WonderwareHistorianClientTests
|
||||
/// channel serializes calls, so the test is observable: each completed query strictly
|
||||
/// increments either successes or failures by one.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetHealthSnapshot_ConcurrentCallsAndReads_CountersAreInternallyConsistent()
|
||||
{
|
||||
|
||||
+10
-4
@@ -22,6 +22,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
public sealed class AahClientManagedAlarmEventWriterTests
|
||||
{
|
||||
/// <summary>Verifies that an empty batch returns an empty array without invoking the backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Empty_batch_returns_empty_array_without_invoking_backend()
|
||||
{
|
||||
@@ -35,6 +36,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a single acknowledgment outcome maps to true.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Single_ack_outcome_maps_to_true()
|
||||
{
|
||||
@@ -47,6 +49,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a mixed batch preserves per-slot outcome ordering.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mixed_batch_preserves_per_slot_ordering()
|
||||
{
|
||||
@@ -69,6 +72,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that backend exceptions mark the whole batch as RetryPlease.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Backend_exception_marks_whole_batch_RetryPlease()
|
||||
{
|
||||
@@ -85,6 +89,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation propagates from the backend.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Cancellation_propagates_from_backend()
|
||||
{
|
||||
@@ -97,6 +102,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a backend returning the wrong outcome count degrades to RetryPlease.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Backend_returning_wrong_count_degrades_to_RetryPlease()
|
||||
{
|
||||
@@ -116,6 +122,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
|
||||
/// <summary>Verifies that a large batch with all acknowledgments returns all true outcomes.</summary>
|
||||
/// <param name="batchSize">The batch size to test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(100)]
|
||||
[InlineData(1000)]
|
||||
@@ -139,6 +146,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
|
||||
/// <summary>Verifies that a large batch with alternating outcomes preserves positional ordering.</summary>
|
||||
/// <param name="batchSize">The batch size to test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(100)]
|
||||
[InlineData(1000)]
|
||||
@@ -165,6 +173,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retry then succeed correctly simulates cluster failover.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Backend_retry_then_succeed_simulates_cluster_failover()
|
||||
{
|
||||
@@ -255,10 +264,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
_produce = produce;
|
||||
}
|
||||
|
||||
/// <summary>Records a call and returns outcomes from the delegate.</summary>
|
||||
/// <param name="events">The events to write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The outcomes produced by the delegate.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<AlarmHistorianWriteOutcome[]> WriteBatchAsync(
|
||||
AlarmHistorianEventDto[] events, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
+6
@@ -19,6 +19,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Backend;
|
||||
public sealed class HistorianDataSourceConnectFailoverTests
|
||||
{
|
||||
/// <summary>Verifies that ReadRaw throws when no nodes are healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRaw_when_no_nodes_are_healthy_throws_so_IPC_surfaces_Success_false()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class HistorianDataSourceConnectFailoverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadRaw tries each cluster node in order.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRaw_tries_each_cluster_node_in_order_until_one_succeeds_or_all_fail()
|
||||
{
|
||||
@@ -71,6 +73,7 @@ public sealed class HistorianDataSourceConnectFailoverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that failed nodes are marked in cooldown and not retried immediately.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRaw_marks_failed_nodes_in_cooldown_so_a_subsequent_call_sees_no_healthy_nodes()
|
||||
{
|
||||
@@ -96,6 +99,7 @@ public sealed class HistorianDataSourceConnectFailoverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadEvents uses a separate event connection path.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadEvents_uses_a_separate_event_connection_path()
|
||||
{
|
||||
@@ -131,6 +135,7 @@ public sealed class HistorianDataSourceConnectFailoverTests
|
||||
/// <param name="config">The historian configuration.</param>
|
||||
/// <param name="type">The connection type.</param>
|
||||
/// <param name="readOnly">Whether to open a read-only connection.</param>
|
||||
/// <returns>Never returns; always throws <see cref="InvalidOperationException"/>.</returns>
|
||||
public HistorianAccess CreateAndConnect(
|
||||
HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true)
|
||||
=> throw new InvalidOperationException($"simulated connect failure to {config.ServerName}");
|
||||
@@ -149,6 +154,7 @@ public sealed class HistorianDataSourceConnectFailoverTests
|
||||
/// <param name="config">The historian configuration.</param>
|
||||
/// <param name="type">The connection type.</param>
|
||||
/// <param name="readOnly">Whether to open a read-only connection.</param>
|
||||
/// <returns>Never returns; always throws <see cref="InvalidOperationException"/>.</returns>
|
||||
public HistorianAccess CreateAndConnect(
|
||||
HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true)
|
||||
{
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ public sealed class HistorianDataSourceRequestTimeoutTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies short timeout values correctly fire cancellation on the linked token.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Small_timeout_cancels_the_linked_token()
|
||||
{
|
||||
|
||||
+7
-4
@@ -27,6 +27,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
// ── Connection-unavailable path (deterministic, no SDK load) ──────────
|
||||
|
||||
/// <summary>Verifies that an empty batch returns an empty outcome array.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Empty_batch_returns_empty_array()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when all nodes are unreachable, the entire batch is deferred as RetryPlease.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unreachable_node_defers_whole_batch_as_RetryPlease()
|
||||
{
|
||||
@@ -57,6 +59,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a large batch with unreachable nodes returns one outcome per event.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unreachable_node_large_batch_returns_one_outcome_per_event()
|
||||
{
|
||||
@@ -73,6 +76,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a connection failure marks the node as failed in the endpoint picker.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Connect_failure_marks_node_failed_in_picker()
|
||||
{
|
||||
@@ -229,6 +233,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
// these need a live AVEVA Historian and are un-skipped during the PR D.1 smoke.
|
||||
|
||||
/// <summary>Verifies that a single alarm event roundtrip returns an Ack outcome.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact(Skip = "rig-required: needs a live AVEVA Historian — un-skip during the PR D.1 rollout smoke")]
|
||||
public async Task Live_single_event_roundtrip_returns_Ack()
|
||||
{
|
||||
@@ -241,6 +246,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cluster failover rotates from a bad primary node to a secondary node.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact(Skip = "rig-required: needs a live AVEVA Historian cluster (two nodes) — un-skip during the PR D.1 rollout smoke")]
|
||||
public async Task Live_cluster_failover_primary_bad_rotates_to_secondary()
|
||||
{
|
||||
@@ -311,10 +317,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
/// </summary>
|
||||
private sealed class ThrowingConnectionFactory : IHistorianConnectionFactory
|
||||
{
|
||||
/// <summary>Creates and attempts to connect, always throwing a simulated connect failure.</summary>
|
||||
/// <param name="config">The historian configuration specifying the target server.</param>
|
||||
/// <param name="type">The connection type (Process or Event).</param>
|
||||
/// <param name="readOnly">Whether to open a read-only connection.</param>
|
||||
/// <inheritdoc />
|
||||
public HistorianAccess CreateAndConnect(
|
||||
HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true)
|
||||
=> throw new InvalidOperationException($"simulated connect failure to {config.ServerName}");
|
||||
|
||||
+10
-3
@@ -93,11 +93,10 @@ public sealed class PipeRoundTripTests
|
||||
public Task<List<BackendHistorianEventDto>> ReadEventsAsync(string? sourceName, DateTime startTime, DateTime endTime, int maxEvents, CancellationToken ct = default)
|
||||
=> Task.FromResult(Events);
|
||||
|
||||
/// <summary>Gets a health snapshot of the fake historian.</summary>
|
||||
/// <returns>A health snapshot.</returns>
|
||||
/// <inheritdoc />
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => new();
|
||||
|
||||
/// <summary>Disposes the fake historian.</summary>
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
@@ -153,6 +152,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that raw historian samples round-trip correctly through the frame handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRaw_RoundTripsSamples()
|
||||
{
|
||||
@@ -183,6 +183,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read failures are properly surfaced as error replies.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadRaw_FailureSurfacesAsErrorReply()
|
||||
{
|
||||
@@ -199,6 +200,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that processed (aggregate) historian samples round-trip correctly through the frame handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadProcessed_RoundTripsBuckets()
|
||||
{
|
||||
@@ -219,6 +221,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that at-time historian samples round-trip correctly through the frame handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAtTime_RoundTripsSamples()
|
||||
{
|
||||
@@ -240,6 +243,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that historian events round-trip correctly through the frame handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadEvents_RoundTripsEvents()
|
||||
{
|
||||
@@ -270,6 +274,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm events are routed to the writer and per-event status is returned.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAlarmEvents_RoutesToWriter_AndReturnsPerEventStatus()
|
||||
{
|
||||
@@ -303,6 +308,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writing alarm events fails cleanly when no writer is configured.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task WriteAlarmEvents_FailsCleanly_WhenNoWriterConfigured()
|
||||
{
|
||||
@@ -324,6 +330,7 @@ public sealed class PipeRoundTripTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that frame reader and writer preserve message kind and body through a round trip.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FrameReader_FrameWriter_RoundTripPreservesKindAndBody()
|
||||
{
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ public sealed class PipeServerSidRejectTests
|
||||
private static readonly ILogger Quiet = Logger.None;
|
||||
|
||||
/// <summary>Verifies that a caller SID mismatch sends HelloAck with reject reason before disconnect.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Caller_SID_mismatch_sends_HelloAck_with_reject_reason_before_disconnect()
|
||||
{
|
||||
|
||||
+4
@@ -36,6 +36,7 @@ public sealed class AddressingGrammarTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that 5-digit and 6-digit Modicon formats map to the same wire offset.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Modicon_5_And_6_Digit_Both_Map_To_Same_Wire_Offset()
|
||||
{
|
||||
@@ -54,6 +55,7 @@ public sealed class AddressingGrammarTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Float32 values roundtrip correctly with CDAB byte order.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Float32_With_CDAB_Roundtrips_Through_Wire()
|
||||
{
|
||||
@@ -76,6 +78,7 @@ public sealed class AddressingGrammarTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Int16 array reads surface as typed arrays.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Int16_Array_Reads_Surface_As_Typed_Array()
|
||||
{
|
||||
@@ -93,6 +96,7 @@ public sealed class AddressingGrammarTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that block read coalescing reduces PDU count end-to-end.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Block_Read_Coalescing_Reduces_PDU_Count_End_To_End()
|
||||
{
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205BcdQuirkTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies DL205 binary-coded-decimal register decodes as decimal value 1234.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_BCD16_decodes_HR1072_as_decimal_1234()
|
||||
{
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205CoilMappingTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DirectLOGIC Y0 output maps to Modbus coil 2048.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task DL260_Y0_maps_to_coil_2048()
|
||||
{
|
||||
@@ -42,6 +43,7 @@ public sealed class DL205CoilMappingTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DirectLOGIC C0 relay maps to Modbus coil 3072.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task DL260_C0_maps_to_coil_3072()
|
||||
{
|
||||
@@ -69,6 +71,7 @@ public sealed class DL205CoilMappingTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a scratch DirectLOGIC C relay supports write and read operations.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task DL260_scratch_Crelay_supports_write_then_read()
|
||||
{
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205ExceptionCodeTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DL205 FC03 reads at unmapped registers return BadOutOfRange status.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_FC03_at_unmapped_register_returns_BadOutOfRange()
|
||||
{
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205FloatCdabQuirkTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DL205 float32 CDAB word order correctly decodes 1.5f from HR1056.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_Float32_CDAB_decodes_1_5f_from_HR1056()
|
||||
{
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205SmokeTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DL205 roundtrip write then read of holding register succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_roundtrip_write_then_read_of_holding_register()
|
||||
{
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205StringQuirkTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DL205 low-byte-first string packing correctly decodes "Hello" from HR1040.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_string_low_byte_first_decodes_Hello_from_HR1040()
|
||||
{
|
||||
|
||||
+2
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205VMemoryQuirkTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DL205 V2000 user memory octal address resolves to PDU address 0x0400.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_V2000_user_memory_resolves_to_PDU_0x0400_marker()
|
||||
{
|
||||
@@ -53,6 +54,7 @@ public sealed class DL205VMemoryQuirkTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that DL205 V40400 system memory address resolves to PDU address 0x2100.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL205_V40400_system_memory_resolves_to_PDU_0x2100_marker()
|
||||
{
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||
public sealed class DL205XInputTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that DL260 X20 octal maps to discrete input 16 and reads the ON state.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DL260_X20_octal_maps_to_DiscreteInput_16_and_reads_ON()
|
||||
{
|
||||
|
||||
+3
@@ -64,6 +64,7 @@ public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
|
||||
/// <param name="address">The Modbus register address to read.</param>
|
||||
/// <param name="expectedStatus">The expected OPC UA status code.</param>
|
||||
/// <param name="scenario">Scenario description for assertion messages.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(1000, StatusBadNotSupported, "exc 0x01 (Illegal Function) -> BadNotSupported")]
|
||||
[InlineData(1001, StatusBadOutOfRange, "exc 0x02 (Illegal Data Address) -> BadOutOfRange")]
|
||||
@@ -82,6 +83,7 @@ public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reads at non-injected addresses return Good status.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task FC03_read_at_non_injected_address_returns_Good()
|
||||
{
|
||||
@@ -98,6 +100,7 @@ public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
|
||||
/// <param name="address">The Modbus register address to write.</param>
|
||||
/// <param name="expectedStatus">The expected OPC UA status code.</param>
|
||||
/// <param name="scenario">Scenario description for assertion messages.</param>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(2000, StatusBadDeviceFailure, "exc 0x04 on FC06 -> BadDeviceFailure (CPU in PROGRAM mode)")]
|
||||
[InlineData(2001, StatusBadDeviceFailure, "exc 0x06 on FC06 -> BadDeviceFailure (Server Busy)")]
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ public static class MitsubishiProfile
|
||||
/// <summary>Builds Modbus driver options configured for the Mitsubishi profile.</summary>
|
||||
/// <param name="host">The Modbus server hostname or IP address.</param>
|
||||
/// <param name="port">The Modbus server port number.</param>
|
||||
/// <returns>A <see cref="ModbusDriverOptions"/> instance configured for the Mitsubishi profile.</returns>
|
||||
public static ModbusDriverOptions BuildOptions(string host, int port) => new()
|
||||
{
|
||||
Host = host,
|
||||
|
||||
+6
@@ -20,6 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.Mitsubishi;
|
||||
public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that Mitsubishi D0 register fingerprint reads the expected value 0x1234.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_D0_fingerprint_reads_0x1234()
|
||||
{
|
||||
@@ -36,6 +37,7 @@ public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Mitsubishi Float32 with CDAB word order decodes correctly to 1.5 from D100.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_Float32_CDAB_decodes_1_5f_from_D100()
|
||||
{
|
||||
@@ -62,6 +64,7 @@ public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Mitsubishi D registers store binary values, not BCD.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_D10_is_binary_not_BCD()
|
||||
{
|
||||
@@ -81,6 +84,7 @@ public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading a binary D register as BCD throws when the value contains non-decimal nibbles.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_D10_as_BCD_throws_because_nibble_is_non_decimal()
|
||||
{
|
||||
@@ -102,6 +106,7 @@ public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Mitsubishi Q/L/iQ-R X inputs use hex addressing and X210 maps correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_QLiQR_X210_hex_maps_to_DI_528_reads_ON()
|
||||
{
|
||||
@@ -134,6 +139,7 @@ public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Mitsubishi M512 relay maps to coil address 512 and reads ON.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_M512_maps_to_coil_512_reads_ON()
|
||||
{
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.Mitsubishi;
|
||||
public sealed class MitsubishiSmokeTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies end-to-end write and read of a Mitsubishi holding register via the Modbus driver.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Mitsubishi_roundtrip_write_then_read_of_holding_register()
|
||||
{
|
||||
|
||||
+2
-1
@@ -82,7 +82,8 @@ public sealed class ModbusSimulatorFixture : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Disposes the fixture asynchronously (no-op for this fixture).</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7;
|
||||
public sealed class S7_1500SmokeTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that an S7-1500 roundtrip write then read of a holding register succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous test.</returns>
|
||||
[Fact]
|
||||
public async Task S7_1500_roundtrip_write_then_read_of_holding_register()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7;
|
||||
public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
|
||||
{
|
||||
/// <summary>Verifies that S7 Float32 with ABCD byte order correctly decodes 1.5f from HR100.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task S7_Float32_ABCD_decodes_1_5f_from_HR100()
|
||||
{
|
||||
@@ -63,6 +64,7 @@ public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that S7 Int32 with ABCD byte order correctly decodes 0x12345678 from HR300.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task S7_Int32_ABCD_decodes_0x12345678_from_HR300()
|
||||
{
|
||||
@@ -98,6 +100,7 @@ public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that S7 DB1 fingerprint marker reads 0xABCD from HR0.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task S7_DB1_fingerprint_marker_at_HR0_reads_0xABCD()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@ public sealed class MelsecAddressTests
|
||||
// --- X / Y hex vs octal family trap ---
|
||||
|
||||
/// <summary>Verifies that Q-series and iQR family X inputs parse as hexadecimal.</summary>
|
||||
/// <param name="x">The X input address string to parse.</param>
|
||||
/// <param name="expected">The expected discrete coil address after parsing.</param>
|
||||
[Theory]
|
||||
[InlineData("X0", (ushort)0)]
|
||||
[InlineData("X9", (ushort)9)]
|
||||
@@ -22,6 +24,8 @@ public sealed class MelsecAddressTests
|
||||
=> MelsecAddress.XInputToDiscrete(x, MelsecFamily.Q_L_iQR).ShouldBe(expected);
|
||||
|
||||
/// <summary>Verifies that F-series and iQF family X inputs parse as octal.</summary>
|
||||
/// <param name="x">The X input address string to parse.</param>
|
||||
/// <param name="expected">The expected discrete coil address after parsing.</param>
|
||||
[Theory]
|
||||
[InlineData("X0", (ushort)0)]
|
||||
[InlineData("X7", (ushort)7)]
|
||||
@@ -32,6 +36,8 @@ public sealed class MelsecAddressTests
|
||||
=> MelsecAddress.XInputToDiscrete(x, MelsecFamily.F_iQF).ShouldBe(expected);
|
||||
|
||||
/// <summary>Verifies that Q-series and iQR family Y outputs parse as hexadecimal.</summary>
|
||||
/// <param name="y">The Y output address string to parse.</param>
|
||||
/// <param name="expected">The expected coil address after parsing.</param>
|
||||
[Theory]
|
||||
[InlineData("Y0", (ushort)0)]
|
||||
[InlineData("Y1F", (ushort)31)]
|
||||
@@ -39,6 +45,8 @@ public sealed class MelsecAddressTests
|
||||
=> MelsecAddress.YOutputToCoil(y, MelsecFamily.Q_L_iQR).ShouldBe(expected);
|
||||
|
||||
/// <summary>Verifies that F-series and iQF family Y outputs parse as octal.</summary>
|
||||
/// <param name="y">The Y output address string to parse.</param>
|
||||
/// <param name="expected">The expected coil address after parsing.</param>
|
||||
[Theory]
|
||||
[InlineData("Y0", (ushort)0)]
|
||||
[InlineData("Y17", (ushort)15)]
|
||||
@@ -58,6 +66,7 @@ public sealed class MelsecAddressTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-octal X input addresses are rejected for F-series and iQF families.</summary>
|
||||
/// <param name="bad">An invalid X input address string that should be rejected.</param>
|
||||
[Theory]
|
||||
[InlineData("X8")] // 8 is non-octal
|
||||
[InlineData("X12G")] // G is non-hex
|
||||
@@ -65,6 +74,7 @@ public sealed class MelsecAddressTests
|
||||
=> Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.F_iQF));
|
||||
|
||||
/// <summary>Verifies that non-hexadecimal X input addresses are rejected for Q-series and iQR families.</summary>
|
||||
/// <param name="bad">An invalid X input address string that should be rejected.</param>
|
||||
[Theory]
|
||||
[InlineData("X12G")]
|
||||
public void XInputToDiscrete_QLiQR_rejects_non_hex(string bad)
|
||||
@@ -84,6 +94,8 @@ public sealed class MelsecAddressTests
|
||||
// --- M-relay (decimal, both families) ---
|
||||
|
||||
/// <summary>Verifies that M relay addresses parse as decimal.</summary>
|
||||
/// <param name="m">The M relay address string to parse.</param>
|
||||
/// <param name="expected">The expected coil address after parsing.</param>
|
||||
[Theory]
|
||||
[InlineData("M0", (ushort)0)]
|
||||
[InlineData("M10", (ushort)10)] // M addresses are DECIMAL, not hex or octal
|
||||
@@ -109,6 +121,8 @@ public sealed class MelsecAddressTests
|
||||
// --- D-register (decimal, both families) ---
|
||||
|
||||
/// <summary>Verifies that D register addresses parse as decimal.</summary>
|
||||
/// <param name="d">The D register address string to parse.</param>
|
||||
/// <param name="expected">The expected holding register address after parsing.</param>
|
||||
[Theory]
|
||||
[InlineData("D0", (ushort)0)]
|
||||
[InlineData("D100", (ushort)100)]
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies reading an Int16 array returns a typed array.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Int16_Array_Returns_Typed_Array()
|
||||
{
|
||||
@@ -36,6 +37,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies reading a Float32 array with word swap returns a typed array.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Float32_Array_Returns_Typed_Array_With_WordSwap()
|
||||
{
|
||||
@@ -63,6 +65,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies reading a coil array returns a bool array.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Coil_Array_Returns_Bool_Array()
|
||||
{
|
||||
@@ -78,6 +81,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies writing an Int16 array lands contiguously in the register bank.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Int16_Array_Lands_Contiguous_In_Bank()
|
||||
{
|
||||
@@ -96,6 +100,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies writing a coil array packs bits in LSB-first order.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Coil_Array_Packs_LSB_First()
|
||||
{
|
||||
@@ -114,6 +119,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies writing an array with mismatched length surfaces an error.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Array_Mismatch_Length_Surfaces_Error()
|
||||
{
|
||||
@@ -129,6 +135,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies discovery surfaces IsArray and ArrayDim correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Discovery_Surfaces_IsArray_And_ArrayDim()
|
||||
{
|
||||
@@ -145,6 +152,7 @@ public sealed class ModbusArrayTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies scalar tag discovery keeps IsArray false.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Scalar_Tag_Discovery_Stays_NonArray()
|
||||
{
|
||||
@@ -164,39 +172,27 @@ public sealed class ModbusArrayTests
|
||||
/// <param name="captured">List to capture discovered attributes into.</param>
|
||||
private sealed class RecordingBuilder(List<DriverAttributeInfo> captured) : IAddressSpaceBuilder
|
||||
{
|
||||
/// <summary>Creates 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>
|
||||
/// <returns>This builder instance.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
||||
|
||||
/// <summary>Creates 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="attributeInfo">The attribute information.</param>
|
||||
/// <returns>A variable handle.</returns>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
{
|
||||
captured.Add(attributeInfo);
|
||||
return new StubHandle(browseName);
|
||||
}
|
||||
|
||||
/// <summary>Adds a property to the current node.</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) { }
|
||||
|
||||
/// <summary>Stub variable handle for testing.</summary>
|
||||
/// <param name="fullRef">The full reference of the handle.</param>
|
||||
private sealed class StubHandle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
|
||||
/// <summary>Marks this variable as an alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <returns>An alarm condition sink.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
|
||||
=> throw new NotSupportedException("RecordingBuilder doesn't model alarms");
|
||||
}
|
||||
|
||||
@@ -14,16 +14,10 @@ public sealed class ModbusBitRmwTests
|
||||
public readonly ushort[] HoldingRegisters = new ushort[256];
|
||||
public readonly List<byte[]> Pdus = new();
|
||||
|
||||
/// <summary>Connects asynchronously (no-op for fake).</summary>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Sends a Modbus PDU and returns a response.</summary>
|
||||
/// <param name="unitId">The Modbus unit ID (unused).</param>
|
||||
/// <param name="pdu">The protocol data unit to send.</param>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
/// <returns>A task containing the response PDU.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
Pdus.Add(pdu);
|
||||
@@ -71,6 +65,7 @@ public sealed class ModbusBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting a bit reads the current register, ORs the bit, and writes back.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_set_reads_current_register_ORs_bit_writes_back()
|
||||
{
|
||||
@@ -90,6 +85,7 @@ public sealed class ModbusBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing a bit reads the current register, ANDs the bit off, and writes back.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_clear_reads_current_register_ANDs_bit_off_writes_back()
|
||||
{
|
||||
@@ -104,6 +100,7 @@ public sealed class ModbusBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent bit writes to the same register preserve all updates via serialization.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Concurrent_bit_writes_to_same_register_preserve_all_updates()
|
||||
{
|
||||
@@ -123,6 +120,7 @@ public sealed class ModbusBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit writes to different registers proceed in parallel without contention.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_write_on_different_registers_proceeds_in_parallel_without_contention()
|
||||
{
|
||||
@@ -140,6 +138,7 @@ public sealed class ModbusBitRmwTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit writes preserve other bits in the same register.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bit_write_preserves_other_bits_in_the_same_register()
|
||||
{
|
||||
|
||||
@@ -55,11 +55,13 @@ public sealed class ModbusCapTests
|
||||
return Task.FromException<byte[]>(new ModbusException(fc, 0x01, $"fc={fc} unsupported"));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Releases resources used by this transport instance.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a read within the cap issues a single FC03 request.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_within_cap_issues_single_FC03_request()
|
||||
{
|
||||
@@ -77,6 +79,7 @@ public sealed class ModbusCapTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a read above cap splits into two FC03 requests.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_above_cap_splits_into_two_FC03_requests()
|
||||
{
|
||||
@@ -112,6 +115,7 @@ public sealed class ModbusCapTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read cap honors Mitsubishi lower cap of 64 registers.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_cap_honors_Mitsubishi_lower_cap_of_64()
|
||||
{
|
||||
@@ -131,6 +135,7 @@ public sealed class ModbusCapTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write exceeding cap throws instead of splitting.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_exceeding_cap_throws_instead_of_splitting()
|
||||
{
|
||||
@@ -155,6 +160,7 @@ public sealed class ModbusCapTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write within cap proceeds normally.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_within_cap_proceeds_normally()
|
||||
{
|
||||
|
||||
+8
-1
@@ -49,11 +49,13 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
default: return Task.FromResult(new byte[] { pdu[0], 0, 0 });
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
/// <summary>Releases resources used by this transport instance.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the first failure falls back to per-tag reads in the same scan.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task First_Failure_Falls_Back_To_PerTag_Same_Scan()
|
||||
{
|
||||
@@ -82,6 +84,7 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the second scan skips coalesced reads of prohibited ranges.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Second_Scan_Skips_Coalesced_Read_Of_Prohibited_Range()
|
||||
{
|
||||
@@ -112,6 +115,7 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reprobe clears prohibition when the range becomes healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reprobe_Clears_Prohibition_When_Range_Becomes_Healthy()
|
||||
{
|
||||
@@ -142,6 +146,7 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reprobe leaves prohibition in place when the range is still bad.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reprobe_Leaves_Prohibition_When_Range_Is_Still_Bad()
|
||||
{
|
||||
@@ -166,6 +171,7 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetAutoProhibitedRanges surfaces an operator-visible snapshot.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetAutoProhibitedRanges_Surfaces_Operator_Visible_Snapshot()
|
||||
{
|
||||
@@ -199,6 +205,7 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tags outside prohibited ranges still coalesce.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Tags_Outside_Prohibited_Range_Still_Coalesce()
|
||||
{
|
||||
|
||||
+7
-16
@@ -22,15 +22,9 @@ public sealed class ModbusCoalescingBisectionTests
|
||||
{
|
||||
/// <summary>Gets or sets the protected address that will cause read failures.</summary>
|
||||
public ushort ProtectedAddress { get; set; } = ushort.MaxValue;
|
||||
/// <summary>Simulates connecting to the Modbus device.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
/// <summary>Simulates sending a Modbus PDU and failing if the protected address is accessed.</summary>
|
||||
/// <param name="unitId">The Modbus unit ID.</param>
|
||||
/// <param name="pdu">The protocol data unit.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The response PDU or an exception if the protected address is accessed.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
var addr = (ushort)((pdu[1] << 8) | pdu[2]);
|
||||
@@ -54,6 +48,7 @@ public sealed class ModbusCoalescingBisectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bisection narrows a multi-register prohibition on each reprobe cycle.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bisection_Narrows_Multi_Register_Prohibition_Per_Reprobe()
|
||||
{
|
||||
@@ -98,6 +93,7 @@ public sealed class ModbusCoalescingBisectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the prohibition is cleared when both bisected halves succeed in recovery.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bisection_Clears_When_Both_Halves_Are_Healthy()
|
||||
{
|
||||
@@ -127,6 +123,7 @@ public sealed class ModbusCoalescingBisectionTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the prohibition splits into two entries when both bisected halves still fail.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Bisection_Splits_Into_Two_When_Both_Halves_Still_Fail()
|
||||
{
|
||||
@@ -160,15 +157,9 @@ public sealed class ModbusCoalescingBisectionTests
|
||||
private sealed class TwoHoleTransport : IModbusTransport
|
||||
{
|
||||
public readonly HashSet<ushort> ProtectedAddresses = new();
|
||||
/// <summary>Simulates connecting to the Modbus device.</summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
/// <summary>Simulates sending a Modbus PDU and failing if any protected address is accessed.</summary>
|
||||
/// <param name="unitId">The Modbus unit ID.</param>
|
||||
/// <param name="pdu">The protocol data unit.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The response PDU or an exception if a protected address is accessed.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
var addr = (ushort)((pdu[1] << 8) | pdu[2]);
|
||||
|
||||
@@ -15,13 +15,9 @@ public sealed class ModbusCoalescingTests
|
||||
private sealed class CountingTransport : IModbusTransport
|
||||
{
|
||||
public readonly List<(byte Unit, byte Fc, ushort Address, ushort Quantity)> Reads = new();
|
||||
/// <summary>Establishes a connection asynchronously.</summary>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
/// <summary>Sends a Modbus PDU and receives the response.</summary>
|
||||
/// <param name="unitId">The Modbus unit identifier.</param>
|
||||
/// <param name="pdu">The Protocol Data Unit to send.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
var addr = (ushort)((pdu[1] << 8) | pdu[2]);
|
||||
@@ -38,11 +34,13 @@ public sealed class ModbusCoalescingTests
|
||||
default: return Task.FromResult(new byte[] { pdu[0], 0, 0 });
|
||||
}
|
||||
}
|
||||
/// <summary>Disposes the transport asynchronously.</summary>
|
||||
/// <summary>Disposes the transport; no-op for this in-memory fake.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that MaxReadGap=0 defaults to per-tag reads without coalescing.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task MaxReadGap_Zero_Defaults_To_Per_Tag_Reads()
|
||||
{
|
||||
@@ -62,6 +60,7 @@ public sealed class ModbusCoalescingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that MaxReadGap bridges adjacent tags into a single read.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task MaxReadGap_Bridges_Two_Adjacent_Tags_Into_One_Read()
|
||||
{
|
||||
@@ -84,6 +83,7 @@ public sealed class ModbusCoalescingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that MaxReadGap splits blocks when gaps exceed threshold.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task MaxReadGap_Splits_When_Gap_Exceeds_Threshold()
|
||||
{
|
||||
@@ -104,6 +104,7 @@ public sealed class ModbusCoalescingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tags with CoalesceProhibited are read separately.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task CoalesceProhibited_Tag_Reads_Alone()
|
||||
{
|
||||
@@ -125,6 +126,7 @@ public sealed class ModbusCoalescingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that coalescing does not cross unit ID boundaries.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Coalescing_Does_Not_Cross_UnitId_Boundaries()
|
||||
{
|
||||
@@ -145,6 +147,7 @@ public sealed class ModbusCoalescingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that coalescing splits blocks exceeding MaxRegistersPerRead.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Coalescing_Splits_Block_That_Exceeds_MaxRegistersPerRead()
|
||||
{
|
||||
@@ -166,6 +169,7 @@ public sealed class ModbusCoalescingTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that coalesced reads surface each tag value independently.</summary>
|
||||
/// <returns>A task that represents the asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task Coalesced_Read_Surfaces_Each_Tag_Value_Independently()
|
||||
{
|
||||
|
||||
@@ -24,15 +24,11 @@ public sealed class ModbusDriverTests
|
||||
/// <summary>Gets or sets a value indicating whether connect operations should fail.</summary>
|
||||
public bool ForceConnectFail { get; set; }
|
||||
|
||||
/// <summary>Initiates a connection to the Modbus server.</summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(CancellationToken ct)
|
||||
=> ForceConnectFail ? Task.FromException(new InvalidOperationException("connect refused")) : Task.CompletedTask;
|
||||
|
||||
/// <summary>Sends a Modbus PDU and receives the response.</summary>
|
||||
/// <param name="unitId">Modbus unit ID.</param>
|
||||
/// <param name="pdu">Protocol data unit bytes to send.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
var fc = pdu[0];
|
||||
@@ -112,6 +108,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Disposes the transport asynchronously.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -124,6 +121,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Initialize connects and populates the tag map.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Initialize_connects_and_populates_tag_map()
|
||||
{
|
||||
@@ -135,6 +133,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading Int16 holding registers returns big-endian values correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Int16_holding_register_returns_BigEndian_value()
|
||||
{
|
||||
@@ -148,6 +147,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading Float32 values spans two registers in big-endian format.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Float32_spans_two_registers_BigEndian()
|
||||
{
|
||||
@@ -165,6 +165,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading coils returns boolean values.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Read_Coil_returns_boolean()
|
||||
{
|
||||
@@ -177,6 +178,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading unknown tags returns BadNodeIdUnknown status instead of throwing.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unknown_tag_returns_BadNodeIdUnknown_not_an_exception()
|
||||
{
|
||||
@@ -188,6 +190,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writing UInt16 holding registers round-trips correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_UInt16_holding_register_roundtrips()
|
||||
{
|
||||
@@ -200,6 +203,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writing Float32 values uses function code 16 (WriteMultipleRegisters).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_Float32_uses_FC16_WriteMultipleRegisters()
|
||||
{
|
||||
@@ -218,6 +222,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writing to input registers returns BadNotWritable status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Write_to_InputRegister_returns_BadNotWritable()
|
||||
{
|
||||
@@ -229,6 +234,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Discover streams one folder per driver with a variable per tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Discover_streams_one_folder_per_driver_with_a_variable_per_tag()
|
||||
{
|
||||
@@ -250,6 +256,7 @@ public sealed class ModbusDriverTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Discover propagates WriteIdempotent from tag to attribute info.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Discover_propagates_WriteIdempotent_from_tag_to_attribute_info()
|
||||
{
|
||||
@@ -278,41 +285,31 @@ public sealed class ModbusDriverTests
|
||||
/// <summary>Gets the list of discovered variables.</summary>
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
||||
|
||||
/// <summary>Records a folder in the address space.</summary>
|
||||
/// <param name="browseName">Folder browse name.</param>
|
||||
/// <param name="displayName">Folder display name.</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">Variable browse name.</param>
|
||||
/// <param name="displayName">Variable display name.</param>
|
||||
/// <param name="info">Driver attribute information.</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 for recording).</summary>
|
||||
/// <param name="_">Property name (unused).</param>
|
||||
/// <param name="__">Property data type (unused).</param>
|
||||
/// <param name="___">Property value (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
/// <summary>Handle to a discovered variable.</summary>
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => fullRef;
|
||||
|
||||
/// <summary>Marks this variable as an alarm condition.</summary>
|
||||
/// <param name="info">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 transitions (no-op).</summary>
|
||||
/// <param name="args">Alarm event arguments.</param>
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user