docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
This commit is contained in:
@@ -7,6 +7,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyAddressTests
|
||||
{
|
||||
/// <summary>Verifies that TryParse accepts valid PCCC addresses.</summary>
|
||||
/// <param name="input">The raw PCCC address string to parse.</param>
|
||||
/// <param name="letter">The expected file letter.</param>
|
||||
/// <param name="file">The expected file number, or null for I/O/S files.</param>
|
||||
/// <param name="word">The expected word number.</param>
|
||||
/// <param name="bit">The expected bit index, or null if no bit is specified.</param>
|
||||
/// <param name="sub">The expected sub-element name, or null if absent.</param>
|
||||
[Theory]
|
||||
[InlineData("N7:0", "N", 7, 0, null, null)]
|
||||
[InlineData("N7:15", "N", 7, 15, null, null)]
|
||||
@@ -34,6 +41,8 @@ public sealed class AbLegacyAddressTests
|
||||
a.SubElement.ShouldBe(sub);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TryParse rejects invalid address forms.</summary>
|
||||
/// <param name="input">The invalid address string to test, or null.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -51,6 +60,8 @@ public sealed class AbLegacyAddressTests
|
||||
AbLegacyAddress.TryParse(input).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ToLibplctagName correctly roundtrips addresses.</summary>
|
||||
/// <param name="input">The PCCC address string to roundtrip.</param>
|
||||
[Theory]
|
||||
[InlineData("N7:0")]
|
||||
[InlineData("F8:5")]
|
||||
@@ -68,12 +79,16 @@ public sealed class AbLegacyAddressTests
|
||||
|
||||
// ---- Driver.AbLegacy-003: Parser tightening ----
|
||||
|
||||
/// <summary>Verifies that TryParse rejects addresses with both sub-element and bit index.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("T4:0.ACC/2")] // sub-element + bit index — never valid in PCCC
|
||||
[InlineData("C5:0.PRE/3")]
|
||||
public void TryParse_rejects_subelement_plus_bitindex(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that TryParse rejects file numbers on system files (I, O, S).</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("I3:0")] // I is a system file — no file number allowed
|
||||
[InlineData("O2:1")]
|
||||
@@ -81,6 +96,8 @@ public sealed class AbLegacyAddressTests
|
||||
public void TryParse_rejects_file_number_on_IOS_files(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that TryParse rejects sub-element specifications on non-structured files.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("B3:0.DN")] // B (bit) file has no structured elements
|
||||
[InlineData("N7:0.FOO")] // N (integer) file has no structured elements
|
||||
@@ -89,6 +106,8 @@ public sealed class AbLegacyAddressTests
|
||||
public void TryParse_rejects_subelement_on_non_structured_file(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that TryParse accepts sub-element specifications only on T, C, and R files.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("T4:0.ACC")] // T, C, R are the only structured-element files
|
||||
[InlineData("C5:0.PRE")]
|
||||
@@ -96,6 +115,8 @@ public sealed class AbLegacyAddressTests
|
||||
public void TryParse_accepts_subelement_only_on_TCR_files(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldNotBeNull();
|
||||
|
||||
/// <summary>Verifies that TryParse accepts I, O, and S addresses without file numbers.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("I:0/0")] // I/O/S without file number are valid
|
||||
[InlineData("O:1/2")]
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyBitIndexRangeTests
|
||||
{
|
||||
/// <summary>Verifies that bit index 0 to 15 is accepted on 16-bit files.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("N7:0/15")]
|
||||
[InlineData("B3:0/15")]
|
||||
@@ -23,6 +25,8 @@ public sealed class AbLegacyBitIndexRangeTests
|
||||
public void Bit_index_0_to_15_accepted_on_16bit_files(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldNotBeNull();
|
||||
|
||||
/// <summary>Verifies that bit index above 15 is rejected on 16-bit files.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("N7:0/16")] // first bit past a 16-bit word
|
||||
[InlineData("N7:0/20")]
|
||||
@@ -35,6 +39,8 @@ public sealed class AbLegacyBitIndexRangeTests
|
||||
public void Bit_index_above_15_rejected_on_16bit_files(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that bit index 0 to 31 is accepted on L file.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("L9:0/0")]
|
||||
[InlineData("L9:0/15")]
|
||||
@@ -43,20 +49,25 @@ public sealed class AbLegacyBitIndexRangeTests
|
||||
public void Bit_index_0_to_31_accepted_on_L_file(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldNotBeNull();
|
||||
|
||||
/// <summary>Verifies that bit index above 31 is rejected on L file.</summary>
|
||||
[Fact]
|
||||
public void Bit_index_above_31_rejected_on_L_file() =>
|
||||
AbLegacyAddress.TryParse("L9:0/32").ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that bit index is rejected on float file.</summary>
|
||||
/// <param name="input">The PCCC address string to test.</param>
|
||||
[Theory]
|
||||
[InlineData("F8:0/0")] // float files are not bit-addressable at all
|
||||
[InlineData("F8:0/3")]
|
||||
public void Bit_index_rejected_on_float_file(string input) =>
|
||||
AbLegacyAddress.TryParse(input).ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that negative bit index is still rejected.</summary>
|
||||
[Fact]
|
||||
public void Negative_bit_index_still_rejected() =>
|
||||
AbLegacyAddress.TryParse("N7:0/-1").ShouldBeNull();
|
||||
|
||||
/// <summary>Verifies that bit in word RMW against L file uses 32-bit parent and high bit.</summary>
|
||||
[Fact]
|
||||
public async Task Bit_in_word_RMW_against_L_file_uses_32bit_parent_and_high_bit()
|
||||
{
|
||||
@@ -81,6 +92,7 @@ public sealed class AbLegacyBitIndexRangeTests
|
||||
Convert.ToInt32(factory.Tags["L9:0"].Value).ShouldBe(1 << 20);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit in word RMW high bit 15 does not corrupt via sign extension.</summary>
|
||||
[Fact]
|
||||
public async Task Bit_in_word_RMW_high_bit_15_does_not_corrupt_via_sign_extension()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyBitRmwTests
|
||||
{
|
||||
/// <summary>Verifies that setting a bit reads the parent word, ORs the bit, and writes back.</summary>
|
||||
[Fact]
|
||||
public async Task Bit_set_reads_parent_word_ORs_bit_writes_back()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0b1001);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing a bit preserves other bits in the word.</summary>
|
||||
[Fact]
|
||||
public async Task Bit_clear_preserves_other_bits_in_N_file_word()
|
||||
{
|
||||
@@ -51,6 +53,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(unchecked((short)0xFFF7));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent bit writes to the same word compose correctly.</summary>
|
||||
[Fact]
|
||||
public async Task Concurrent_bit_writes_to_same_word_compose_correctly()
|
||||
{
|
||||
@@ -75,6 +78,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0xFF);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated bit writes reuse the parent word runtime.</summary>
|
||||
[Fact]
|
||||
public async Task Repeat_bit_writes_reuse_parent_runtime()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
{
|
||||
// ---- ITagDiscovery ----
|
||||
|
||||
/// <summary>Verifies that DiscoverAsync emits pre-declared tags under the device folder.</summary>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_emits_pre_declared_tags_under_device_folder()
|
||||
{
|
||||
@@ -38,6 +39,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
|
||||
// ---- ISubscribable ----
|
||||
|
||||
/// <summary>Verifies that Subscribe initial poll raises OnDataChange.</summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_initial_poll_raises_OnDataChange()
|
||||
{
|
||||
@@ -63,6 +65,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
await drv.UnsubscribeAsync(handle, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Unsubscribe halts polling.</summary>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_halts_polling()
|
||||
{
|
||||
@@ -92,6 +95,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
|
||||
// ---- IHostConnectivityProbe ----
|
||||
|
||||
/// <summary>Verifies that GetHostStatuses returns one status per device.</summary>
|
||||
[Fact]
|
||||
public async Task GetHostStatuses_returns_one_per_device()
|
||||
{
|
||||
@@ -109,6 +113,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
drv.GetHostStatuses().Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Probe transitions to Running on successful read.</summary>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Running_on_successful_read()
|
||||
{
|
||||
@@ -132,6 +137,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Probe transitions to Stopped on read failure.</summary>
|
||||
[Fact]
|
||||
public async Task Probe_transitions_to_Stopped_on_read_failure()
|
||||
{
|
||||
@@ -155,6 +161,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Probe is disabled when ProbeAddress is null.</summary>
|
||||
[Fact]
|
||||
public async Task Probe_disabled_when_ProbeAddress_is_null()
|
||||
{
|
||||
@@ -172,6 +179,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
|
||||
// ---- IPerCallHostResolver ----
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns declared device for known tag.</summary>
|
||||
[Fact]
|
||||
public async Task ResolveHost_returns_declared_device_for_known_tag()
|
||||
{
|
||||
@@ -195,6 +203,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
drv.ResolveHost("B").ShouldBe("ab://10.0.0.6/1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to first device for unknown tags.</summary>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_first_device_for_unknown()
|
||||
{
|
||||
@@ -208,6 +217,7 @@ public sealed class AbLegacyCapabilityTests
|
||||
drv.ResolveHost("missing").ShouldBe("ab://10.0.0.5/1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost falls back to DriverInstanceId when no devices exist.</summary>
|
||||
[Fact]
|
||||
public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices()
|
||||
{
|
||||
@@ -228,22 +238,43 @@ public sealed class AbLegacyCapabilityTests
|
||||
|
||||
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
/// <summary>Gets list of folders created during discovery.</summary>
|
||||
public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
|
||||
/// <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>
|
||||
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>
|
||||
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>
|
||||
public void AddProperty(string _, DriverDataType __, object? ___) { }
|
||||
|
||||
private sealed class Handle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference of the variable.</summary>
|
||||
public string FullReference => fullRef;
|
||||
/// <summary>Marks the variable as an alarm condition.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
||||
}
|
||||
private sealed class NullSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } }
|
||||
/// <summary>Null sink for alarm condition transitions.</summary>
|
||||
private sealed class NullSink : IAlarmConditionSink
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -17,6 +17,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
{
|
||||
// ---- Driver.AbLegacy-011 ----
|
||||
|
||||
/// <summary>Verifies that Dispose performs teardown without blocking on async operations.</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_runs_teardown_without_blocking_on_async_wait()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Dispose can be called multiple times without throwing.</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_is_idempotent()
|
||||
{
|
||||
@@ -58,6 +60,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
Should.NotThrow(() => drv.Dispose());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Dispose does not deadlock under a single-threaded synchronization context.</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_under_single_threaded_sync_context_does_not_deadlock()
|
||||
{
|
||||
@@ -100,9 +103,14 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
{
|
||||
private readonly System.Collections.Concurrent.BlockingCollection<(SendOrPostCallback, object?)> _queue = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Send(SendOrPostCallback d, object? state) => d(state);
|
||||
|
||||
/// <summary>Runs the event loop until the stop signal is set.</summary>
|
||||
/// <param name="stop">The event to signal loop completion.</param>
|
||||
public void RunUntil(ManualResetEventSlim stop)
|
||||
{
|
||||
while (!stop.IsSet)
|
||||
@@ -114,11 +122,13 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disposes the internal queue.</summary>
|
||||
public void Dispose() => _queue.Dispose();
|
||||
}
|
||||
|
||||
// ---- Driver.AbLegacy-013 ----
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns the configured device for a known tag reference.</summary>
|
||||
[Fact]
|
||||
public void ResolveHost_known_reference_returns_tag_device()
|
||||
{
|
||||
@@ -130,6 +140,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
drv.ResolveHost("X").ShouldBe("ab://10.0.0.5/1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns the first configured device when reference is unknown.</summary>
|
||||
[Fact]
|
||||
public void ResolveHost_unknown_reference_with_devices_returns_first_device()
|
||||
{
|
||||
@@ -146,6 +157,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
|
||||
drv.ResolveHost("unknown").ShouldBe("ab://10.0.0.5/1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ResolveHost returns the driver instance ID when no devices are configured.</summary>
|
||||
[Fact]
|
||||
public void ResolveHost_unknown_reference_no_devices_returns_driver_instance_id()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyDriverTests
|
||||
{
|
||||
/// <summary>Verifies that driver type is AbLegacy.</summary>
|
||||
[Fact]
|
||||
public void DriverType_is_AbLegacy()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public sealed class AbLegacyDriverTests
|
||||
drv.DriverInstanceId.ShouldBe("drv-1");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InitializeAsync with devices assigns family profiles.</summary>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_devices_assigns_family_profiles()
|
||||
{
|
||||
@@ -38,6 +40,7 @@ public sealed class AbLegacyDriverTests
|
||||
drv.GetDeviceState("ab://10.0.0.7/1,0")!.Profile.ShouldBe(AbLegacyPlcFamilyProfile.Plc5);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InitializeAsync with malformed host address faults.</summary>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_malformed_host_address_faults()
|
||||
{
|
||||
@@ -51,6 +54,7 @@ public sealed class AbLegacyDriverTests
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync clears devices.</summary>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_clears_devices()
|
||||
{
|
||||
@@ -65,6 +69,7 @@ public sealed class AbLegacyDriverTests
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that family profiles expose expected defaults.</summary>
|
||||
[Fact]
|
||||
public void Family_profiles_expose_expected_defaults()
|
||||
{
|
||||
@@ -82,6 +87,9 @@ public sealed class AbLegacyDriverTests
|
||||
AbLegacyPlcFamilyProfile.LogixPccc.SupportsLongFile.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ForFamily dispatches correctly.</summary>
|
||||
/// <param name="family">The PLC family to dispatch for.</param>
|
||||
/// <param name="expectedAttribute">The expected libplctag PLC attribute.</param>
|
||||
[Theory]
|
||||
[InlineData(AbLegacyPlcFamily.Slc500, "slc500")]
|
||||
[InlineData(AbLegacyPlcFamily.MicroLogix, "micrologix")]
|
||||
@@ -92,6 +100,7 @@ public sealed class AbLegacyDriverTests
|
||||
AbLegacyPlcFamilyProfile.ForFamily(family).LibplctagPlcAttribute.ShouldBe(expectedAttribute);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that data type mapping covers atomic PCCC types.</summary>
|
||||
[Fact]
|
||||
public void DataType_mapping_covers_atomic_pccc_types()
|
||||
{
|
||||
@@ -105,6 +114,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>
|
||||
[Fact]
|
||||
public async Task EffectiveCipPath_falls_back_to_profile_default_when_host_path_is_empty()
|
||||
{
|
||||
@@ -124,6 +134,7 @@ public sealed class AbLegacyDriverTests
|
||||
factory.Tags["N7:0"].CreationParams.CipPath.ShouldBe("1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that EffectiveCipPath preserves explicit host path.</summary>
|
||||
[Fact]
|
||||
public async Task EffectiveCipPath_preserves_explicit_host_path()
|
||||
{
|
||||
@@ -142,6 +153,7 @@ public sealed class AbLegacyDriverTests
|
||||
factory.Tags["N7:0"].CreationParams.CipPath.ShouldBe("1,2");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that long tag on MicroLogix device is rejected at initialization.</summary>
|
||||
[Fact]
|
||||
public async Task Long_tag_on_MicroLogix_device_rejected_at_init()
|
||||
{
|
||||
@@ -157,6 +169,7 @@ public sealed class AbLegacyDriverTests
|
||||
ex.Message.ShouldContain("L-files");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that long tag on SLC 500 device is accepted.</summary>
|
||||
[Fact]
|
||||
public async Task Long_tag_on_Slc500_device_accepted()
|
||||
{
|
||||
@@ -172,6 +185,7 @@ public sealed class AbLegacyDriverTests
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that string tag on PLC-5 device is rejected at initialization.</summary>
|
||||
[Fact]
|
||||
public async Task String_tag_on_Plc5_device_rejected_at_init()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyHostAndStatusTests
|
||||
{
|
||||
/// <summary>Verifies that HostAddress parses valid URI strings correctly.</summary>
|
||||
/// <param name="input">The raw URI string to parse.</param>
|
||||
/// <param name="gateway">The expected gateway host.</param>
|
||||
/// <param name="port">The expected port number.</param>
|
||||
/// <param name="path">The expected CIP path component.</param>
|
||||
[Theory]
|
||||
[InlineData("ab://10.0.0.5/1,0", "10.0.0.5", 44818, "1,0")]
|
||||
[InlineData("ab://10.0.0.5/", "10.0.0.5", 44818, "")]
|
||||
@@ -22,6 +27,8 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
parsed.CipPath.ShouldBe(path);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that HostAddress rejects invalid URI strings.</summary>
|
||||
/// <param name="input">The invalid or null URI string to test.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("http://10.0.0.5/1,0")]
|
||||
@@ -33,6 +40,7 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
AbLegacyHostAddress.TryParse(input).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that HostAddress.ToString produces canonical URI format.</summary>
|
||||
[Fact]
|
||||
public void HostAddress_ToString_canonicalises()
|
||||
{
|
||||
@@ -40,6 +48,9 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
new AbLegacyHostAddress("10.0.0.5", 2222, "1,0").ToString().ShouldBe("ab://10.0.0.5:2222/1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that PCCC status codes are mapped to OPC UA status codes correctly.</summary>
|
||||
/// <param name="sts">The PCCC status byte to map.</param>
|
||||
/// <param name="expected">The expected OPC UA status code.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)0x00, AbLegacyStatusMapper.Good)]
|
||||
[InlineData((byte)0x10, AbLegacyStatusMapper.BadNotSupported)]
|
||||
@@ -54,6 +65,9 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
AbLegacyStatusMapper.MapPcccStatus(sts).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that libplctag Status enum members are mapped to OPC UA status codes correctly.</summary>
|
||||
/// <param name="status">The libplctag Status enum value to map.</param>
|
||||
/// <param name="expected">The expected OPC UA status code.</param>
|
||||
// Driver.AbLegacy-010 — tests use the libplctag.NET Status enum members (what
|
||||
// (int)Tag.GetStatus() actually returns) rather than the unverified magic integers
|
||||
// that predated this fix (-5/-7/-14/-16/-17 matched neither native PLCTAG_ERR_*
|
||||
@@ -78,6 +92,7 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
AbLegacyStatusMapper.MapLibplctagStatus((int)status).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timeout errors are distinguished from generic communication errors.</summary>
|
||||
[Fact]
|
||||
public void MapLibplctagStatus_distinguishes_timeout_from_generic_comms_error()
|
||||
{
|
||||
|
||||
@@ -18,18 +18,27 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
private sealed class CapturingLogger : ILogger<AbLegacyDriver>
|
||||
{
|
||||
public readonly List<(LogLevel Level, string Message)> Entries = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
=> Entries.Add((logLevel, formatter(state, exception)));
|
||||
private sealed class NullScope : IDisposable
|
||||
{
|
||||
public static readonly NullScope Instance = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the driver accepts an optional logger parameter.</summary>
|
||||
[Fact]
|
||||
public void Driver_accepts_optional_logger_parameter()
|
||||
{
|
||||
@@ -40,6 +49,7 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
drv.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that driver initialization failure emits an error log.</summary>
|
||||
[Fact]
|
||||
public async Task InitializeAsync_failure_emits_error_log()
|
||||
{
|
||||
@@ -57,6 +67,7 @@ public sealed class AbLegacyLoggerInjectionTests
|
||||
errors[0].Message.ShouldContain("drv-logged");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the first non-zero libplctag status per device is logged.</summary>
|
||||
[Fact]
|
||||
public async Task First_nonzero_libplctag_status_per_device_is_logged()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
|
||||
// ---- Read ----
|
||||
|
||||
/// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown.</summary>
|
||||
[Fact]
|
||||
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
||||
{
|
||||
@@ -32,6 +33,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful N-file read returns a Good status.</summary>
|
||||
[Fact]
|
||||
public async Task Successful_N_file_read_returns_Good_value()
|
||||
{
|
||||
@@ -48,6 +50,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
factory.Tags["N7:0"].ReadCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated reads reuse the runtime.</summary>
|
||||
[Fact]
|
||||
public async Task Repeat_read_reuses_runtime()
|
||||
{
|
||||
@@ -63,6 +66,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
factory.Tags["N7:0"].ReadCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero libplctag status values map via AbLegacyStatusMapper.</summary>
|
||||
[Fact]
|
||||
public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
|
||||
{
|
||||
@@ -77,6 +81,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read exceptions surface as BadCommunicationError.</summary>
|
||||
[Fact]
|
||||
public async Task Read_exception_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -90,6 +95,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batched reads preserve order.</summary>
|
||||
[Fact]
|
||||
public async Task Batched_reads_preserve_order()
|
||||
{
|
||||
@@ -113,6 +119,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
snapshots[2].Value.ShouldBe("hello");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that read tag creation parameters are composed from device and profile.</summary>
|
||||
[Fact]
|
||||
public async Task Read_TagCreateParams_composed_from_device_and_profile()
|
||||
{
|
||||
@@ -132,6 +139,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
|
||||
// ---- Write ----
|
||||
|
||||
/// <summary>Verifies that a non-writable tag rejects with BadNotWritable.</summary>
|
||||
[Fact]
|
||||
public async Task Non_writable_tag_rejects_with_BadNotWritable()
|
||||
{
|
||||
@@ -144,6 +152,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful N-file write encodes and flushes the data.</summary>
|
||||
[Fact]
|
||||
public async Task Successful_N_file_write_encodes_and_flushes()
|
||||
{
|
||||
@@ -159,6 +168,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
factory.Tags["N7:0"].WriteCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bit-within-word write now succeeds via RMW.</summary>
|
||||
[Fact]
|
||||
public async Task Bit_within_word_write_now_succeeds_via_RMW()
|
||||
{
|
||||
@@ -179,6 +189,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write exceptions surface as BadCommunicationError.</summary>
|
||||
[Fact]
|
||||
public async Task Write_exception_surfaces_BadCommunicationError()
|
||||
{
|
||||
@@ -192,6 +203,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that batch write preserves order across different outcomes.</summary>
|
||||
[Fact]
|
||||
public async Task Batch_write_preserves_order_across_outcomes()
|
||||
{
|
||||
@@ -220,6 +232,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
results[2].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation propagates through the driver.</summary>
|
||||
[Fact]
|
||||
public async Task Cancellation_propagates()
|
||||
{
|
||||
@@ -236,6 +249,7 @@ public sealed class AbLegacyReadWriteTests
|
||||
() => drv.ReadAsync(["X"], CancellationToken.None));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ShutdownAsync disposes all runtimes.</summary>
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_disposes_runtimes()
|
||||
{
|
||||
@@ -252,6 +266,11 @@ public sealed class AbLegacyReadWriteTests
|
||||
|
||||
private sealed class RmwThrowingFake(AbLegacyTagCreateParams p) : FakeAbLegacyTag(p)
|
||||
{
|
||||
/// <summary>Encodes a value for the tag, throwing for unsupported bit-within-word RMW operations.</summary>
|
||||
/// <param name="type">The data type of the tag.</param>
|
||||
/// <param name="bitIndex">Optional bit index within a word.</param>
|
||||
/// <param name="value">The value to encode.</param>
|
||||
/// <inheritdoc />
|
||||
public override void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value)
|
||||
{
|
||||
if (type == AbLegacyDataType.Bit && bitIndex is not null)
|
||||
|
||||
+12
@@ -22,10 +22,17 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
private sealed class OverlapDetectingFake : FakeAbLegacyTag
|
||||
{
|
||||
private int _inFlight;
|
||||
|
||||
/// <summary>Gets the maximum number of concurrent operations detected.</summary>
|
||||
public int MaxConcurrent { get; private set; }
|
||||
|
||||
/// <summary>Initializes a new instance of the OverlapDetectingFake class.</summary>
|
||||
/// <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>
|
||||
public override async Task ReadAsync(CancellationToken ct)
|
||||
{
|
||||
EnterOp();
|
||||
@@ -38,6 +45,9 @@ 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>
|
||||
public override async Task WriteAsync(CancellationToken ct)
|
||||
{
|
||||
EnterOp();
|
||||
@@ -58,6 +68,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
private void LeaveOp() => Interlocked.Decrement(ref _inFlight);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent reads of the same tag are serialised against the shared runtime.</summary>
|
||||
[Fact]
|
||||
public async Task Concurrent_reads_of_same_tag_are_serialised_against_the_shared_runtime()
|
||||
{
|
||||
@@ -90,6 +101,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
|
||||
reads.ShouldAllBe(r => r.Result.Single().Value!.Equals(7));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent read and write operations on the same tag do not overlap.</summary>
|
||||
[Fact]
|
||||
public async Task Concurrent_read_and_write_of_same_tag_do_not_overlap()
|
||||
{
|
||||
|
||||
@@ -4,20 +4,46 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
internal class FakeAbLegacyTag : IAbLegacyTagRuntime
|
||||
{
|
||||
/// <summary>Gets the tag creation parameters.</summary>
|
||||
public AbLegacyTagCreateParams CreationParams { get; }
|
||||
|
||||
/// <summary>Gets or sets the tag value.</summary>
|
||||
public object? Value { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the tag status code.</summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether to throw on initialization.</summary>
|
||||
public bool ThrowOnInitialize { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether to throw on read.</summary>
|
||||
public bool ThrowOnRead { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether to throw on write.</summary>
|
||||
public bool ThrowOnWrite { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the exception to throw.</summary>
|
||||
public Exception? Exception { get; set; }
|
||||
|
||||
/// <summary>Gets the count of initialization calls.</summary>
|
||||
public int InitializeCount { get; private set; }
|
||||
|
||||
/// <summary>Gets the count of read calls.</summary>
|
||||
public int ReadCount { get; private set; }
|
||||
|
||||
/// <summary>Gets the count of write calls.</summary>
|
||||
public int WriteCount { get; private set; }
|
||||
|
||||
/// <summary>Gets a value indicating whether the tag has been disposed.</summary>
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
/// <summary>Initializes a new instance of the FakeAbLegacyTag class.</summary>
|
||||
/// <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>
|
||||
public virtual Task InitializeAsync(CancellationToken ct)
|
||||
{
|
||||
InitializeCount++;
|
||||
@@ -25,6 +51,9 @@ 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>
|
||||
public virtual Task ReadAsync(CancellationToken ct)
|
||||
{
|
||||
ReadCount++;
|
||||
@@ -32,6 +61,9 @@ 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>
|
||||
public virtual Task WriteAsync(CancellationToken ct)
|
||||
{
|
||||
WriteCount++;
|
||||
@@ -39,17 +71,38 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Gets the current tag status.</summary>
|
||||
/// <returns>The status code.</returns>
|
||||
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>
|
||||
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>
|
||||
public virtual void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) => Value = value;
|
||||
|
||||
/// <summary>Disposes the tag.</summary>
|
||||
public virtual void Dispose() => Disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>Test factory for creating fake AbLegacy tags.</summary>
|
||||
internal sealed class FakeAbLegacyTagFactory : IAbLegacyTagFactory
|
||||
{
|
||||
/// <summary>Gets the collection of created tags, keyed by tag name.</summary>
|
||||
public Dictionary<string, FakeAbLegacyTag> Tags { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <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>
|
||||
public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams p)
|
||||
{
|
||||
var fake = Customise?.Invoke(p) ?? new FakeAbLegacyTag(p);
|
||||
|
||||
Reference in New Issue
Block a user