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

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:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -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) { }
}
}
}
@@ -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)
@@ -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);