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
@@ -4,20 +4,36 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
internal class FakeFocasClient : IFocasClient
{
/// <summary>Gets a value indicating whether the client is connected.</summary>
public bool IsConnected { get; private set; }
/// <summary>Gets the count of connection attempts.</summary>
public int ConnectCount { get; private set; }
/// <summary>Gets the count of dispose operations.</summary>
public int DisposeCount { get; private set; }
/// <summary>Gets or sets a value indicating whether to throw on connect.</summary>
public bool ThrowOnConnect { 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 result of probe operations.</summary>
public bool ProbeResult { get; set; } = true;
/// <summary>Gets or sets the exception to throw.</summary>
public Exception? Exception { get; set; }
/// <summary>Gets the dictionary of read values keyed by address.</summary>
public Dictionary<string, object?> Values { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Gets the dictionary of read statuses keyed by address.</summary>
public Dictionary<string, uint> ReadStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Gets the dictionary of write statuses keyed by address.</summary>
public Dictionary<string, uint> WriteStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <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>
public virtual Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken ct)
{
ConnectCount++;
@@ -26,6 +42,10 @@ 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>
public virtual Task<(object? value, uint status)> ReadAsync(
FocasAddress address, FocasDataType type, CancellationToken ct)
{
@@ -36,6 +56,11 @@ 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>
public virtual Task<uint> WriteAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken ct)
{
@@ -46,24 +71,42 @@ internal class FakeFocasClient : IFocasClient
return Task.FromResult(status);
}
/// <summary>Probes the FOCAS connection asynchronously.</summary>
/// <param name="ct">The cancellation token.</param>
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>
public virtual Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken ct) =>
Task.FromResult<IReadOnlyList<FocasActiveAlarm>>([.. Alarms]);
// ---- Fixed-tree T1 ----
/// <summary>Gets or sets the system information.</summary>
public FocasSysInfo SysInfo { get; set; } = new(0, 3, "M", "M", "30i", "A1.0", 3);
/// <summary>Gets the list of axis names.</summary>
public List<FocasAxisName> AxisNames { get; } = [new("X", ""), new("Y", ""), new("Z", "")];
/// <summary>Gets the list of spindle names.</summary>
public List<FocasSpindleName> SpindleNames { get; } = [new("S", "1", "", "")];
/// <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>
public virtual Task<FocasSysInfo> GetSysInfoAsync(CancellationToken ct) => Task.FromResult(SysInfo);
/// <summary>Gets axis names asynchronously.</summary>
/// <param name="ct">The cancellation token.</param>
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>
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>
public virtual Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken ct)
{
if (!DynamicByAxis.TryGetValue(axisIndex, out var snap))
@@ -71,11 +114,18 @@ internal class FakeFocasClient : IFocasClient
return Task.FromResult(snap);
}
/// <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>
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>
public virtual Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken ct)
{
if (!Timers.TryGetValue(kind, out var t))
@@ -83,17 +133,27 @@ internal class FakeFocasClient : IFocasClient
return Task.FromResult(t);
}
/// <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>
public virtual Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken ct) =>
Task.FromResult<IReadOnlyList<FocasServoLoad>>([.. ServoLoads]);
/// <summary>Gets the list of spindle loads.</summary>
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>
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>
public virtual Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken ct) =>
Task.FromResult<IReadOnlyList<int>>([.. SpindleMaxRpms]);
/// <summary>Disposes the client.</summary>
public virtual void Dispose()
{
DisposeCount++;
@@ -101,11 +161,15 @@ internal class FakeFocasClient : IFocasClient
}
}
/// <summary>A factory for creating fake FOCAS clients.</summary>
internal sealed class FakeFocasClientFactory : IFocasClientFactory
{
/// <summary>Gets the list of created clients.</summary>
public List<FakeFocasClient> Clients { get; } = new();
/// <summary>Gets or sets a customization function for creating clients.</summary>
public Func<FakeFocasClient>? Customise { get; set; }
/// <summary>Creates a fake FOCAS client.</summary>
public IFocasClient Create()
{
var c = Customise?.Invoke() ?? new FakeFocasClient();
@@ -26,6 +26,7 @@ public sealed class FocasAlarmProjectionTests
return (drv, factory);
}
/// <summary>Verifies that subscribe without enable throws NotSupported.</summary>
[Fact]
public async Task Subscribe_without_Enable_throws_NotSupported()
{
@@ -36,6 +37,7 @@ public sealed class FocasAlarmProjectionTests
drv.SubscribeAlarmsAsync([], CancellationToken.None));
}
/// <summary>Verifies that raise then clear emits both events.</summary>
[Fact]
public async Task Raise_then_clear_emits_both_events()
{
@@ -67,6 +69,7 @@ public sealed class FocasAlarmProjectionTests
events[0].SourceNodeId.ShouldBe(Host);
}
/// <summary>Verifies that tick diffs raises and clears without polling loop.</summary>
[Fact]
public async Task Tick_diffs_raises_and_clears_without_polling_loop()
{
@@ -111,6 +114,7 @@ public sealed class FocasAlarmProjectionTests
events[0].AlarmType.ShouldBe("Parameter");
}
/// <summary>Verifies that severity mapping matches docs.</summary>
[Fact]
public void Severity_mapping_matches_docs()
{
@@ -16,6 +16,10 @@ public sealed class FocasCapabilityMatrixTests
{
// ---- Macro ranges ----
/// <summary>Verifies that macro range matches series.</summary>
/// <param name="series">The FOCAS CNC series to validate.</param>
/// <param name="number">The macro number to test.</param>
/// <param name="accepted">Whether the address should be accepted.</param>
[Theory]
[InlineData(FocasCncSeries.Sixteen_i, 999, true)]
[InlineData(FocasCncSeries.Sixteen_i, 1000, false)] // above legacy ceiling
@@ -37,6 +41,10 @@ public sealed class FocasCapabilityMatrixTests
// ---- Parameter ranges ----
/// <summary>Verifies that parameter range matches series.</summary>
/// <param name="series">The FOCAS CNC series to validate.</param>
/// <param name="number">The parameter number to test.</param>
/// <param name="accepted">Whether the address should be accepted.</param>
[Theory]
[InlineData(FocasCncSeries.Sixteen_i, 9999, true)]
[InlineData(FocasCncSeries.Sixteen_i, 10000, false)] // 16i capped at 9999
@@ -53,6 +61,10 @@ public sealed class FocasCapabilityMatrixTests
// ---- PMC letters ----
/// <summary>Verifies that PMC letter matches series.</summary>
/// <param name="series">The FOCAS CNC series to validate.</param>
/// <param name="letter">The PMC signal group letter to test.</param>
/// <param name="accepted">Whether the address should be accepted.</param>
[Theory]
[InlineData(FocasCncSeries.Sixteen_i, "X", true)]
[InlineData(FocasCncSeries.Sixteen_i, "Y", true)]
@@ -77,6 +89,11 @@ public sealed class FocasCapabilityMatrixTests
// ---- PMC number ceiling ----
/// <summary>Verifies that PMC number ceiling matches series.</summary>
/// <param name="series">The FOCAS CNC series to validate.</param>
/// <param name="letter">The PMC signal group letter to test.</param>
/// <param name="number">The PMC address number to test.</param>
/// <param name="accepted">Whether the address should be accepted.</param>
[Theory]
[InlineData(FocasCncSeries.Sixteen_i, "R", 999, true)]
[InlineData(FocasCncSeries.Sixteen_i, "R", 1000, false)]
@@ -95,6 +112,9 @@ public sealed class FocasCapabilityMatrixTests
// ---- Unknown series is permissive ----
/// <summary>Verifies that unknown series accepts any PMC.</summary>
/// <param name="letter">The PMC signal group letter to test.</param>
/// <param name="number">The PMC address number to test.</param>
[Theory]
[InlineData("Z", 999_999)] // absurd PMC address
[InlineData("Q", 0)] // non-existent letter
@@ -104,6 +124,7 @@ public sealed class FocasCapabilityMatrixTests
FocasCapabilityMatrix.Validate(FocasCncSeries.Unknown, address).ShouldBeNull();
}
/// <summary>Verifies that unknown series accepts any macro number.</summary>
[Fact]
public void Unknown_series_accepts_any_macro_number()
{
@@ -111,6 +132,7 @@ public sealed class FocasCapabilityMatrixTests
FocasCapabilityMatrix.Validate(FocasCncSeries.Unknown, address).ShouldBeNull();
}
/// <summary>Verifies that unknown series accepts any parameter number.</summary>
[Fact]
public void Unknown_series_accepts_any_parameter_number()
{
@@ -120,6 +142,7 @@ public sealed class FocasCapabilityMatrixTests
// ---- Reason messages include enough context to diagnose ----
/// <summary>Verifies that rejection message names series and limit.</summary>
[Fact]
public void Rejection_message_names_series_and_limit()
{
@@ -131,6 +154,7 @@ public sealed class FocasCapabilityMatrixTests
reason.ShouldContain("9999");
}
/// <summary>Verifies that PMC rejection lists accepted letters.</summary>
[Fact]
public void Pmc_rejection_lists_accepted_letters()
{
@@ -144,6 +168,8 @@ public sealed class FocasCapabilityMatrixTests
// ---- PMC address letter is case-insensitive ----
/// <summary>Verifies that PMC letter match is case insensitive on 30i.</summary>
/// <param name="letter">The PMC signal group letter to test in various cases.</param>
[Theory]
[InlineData("x")]
[InlineData("X")]
@@ -11,6 +11,7 @@ public sealed class FocasCapabilityTests
{
// ---- ITagDiscovery ----
/// <summary>Verifies that DiscoverAsync emits pre-declared tags.</summary>
[Fact]
public async Task DiscoverAsync_emits_pre_declared_tags()
{
@@ -39,6 +40,7 @@ public sealed class FocasCapabilityTests
// ---- ISubscribable ----
/// <summary>Verifies that the initial subscription poll raises an OnDataChange event.</summary>
[Fact]
public async Task Subscribe_initial_poll_raises_OnDataChange()
{
@@ -64,6 +66,7 @@ public sealed class FocasCapabilityTests
await drv.UnsubscribeAsync(handle, CancellationToken.None);
}
/// <summary>Verifies that ShutdownAsync cancels active subscriptions.</summary>
[Fact]
public async Task ShutdownAsync_cancels_active_subscriptions()
{
@@ -93,6 +96,7 @@ public sealed class FocasCapabilityTests
// ---- IHostConnectivityProbe ----
/// <summary>Verifies that GetHostStatuses returns one entry per device.</summary>
[Fact]
public async Task GetHostStatuses_returns_entry_per_device()
{
@@ -110,6 +114,7 @@ public sealed class FocasCapabilityTests
drv.GetHostStatuses().Count.ShouldBe(2);
}
/// <summary>Verifies that the probe transitions to Running on successful connection.</summary>
[Fact]
public async Task Probe_transitions_to_Running_on_success()
{
@@ -136,6 +141,7 @@ public sealed class FocasCapabilityTests
await drv.ShutdownAsync(CancellationToken.None);
}
/// <summary>Verifies that the probe transitions to Stopped on connection failure.</summary>
[Fact]
public async Task Probe_transitions_to_Stopped_on_failure()
{
@@ -164,6 +170,7 @@ public sealed class FocasCapabilityTests
// ---- IPerCallHostResolver ----
/// <summary>Verifies that ResolveHost returns the declared device for a known tag.</summary>
[Fact]
public async Task ResolveHost_returns_declared_device_for_known_tag()
{
@@ -187,6 +194,7 @@ public sealed class FocasCapabilityTests
drv.ResolveHost("B").ShouldBe("focas://10.0.0.6:8193");
}
/// <summary>Verifies that ResolveHost falls back to the first device for unknown tags.</summary>
[Fact]
public async Task ResolveHost_falls_back_to_first_device_for_unknown()
{
@@ -200,6 +208,7 @@ public sealed class FocasCapabilityTests
drv.ResolveHost("missing").ShouldBe("focas://10.0.0.5:8193");
}
/// <summary>Verifies that ResolveHost falls back to the driver instance ID when no devices are configured.</summary>
[Fact]
public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices()
{
@@ -218,24 +227,49 @@ public sealed class FocasCapabilityTests
await Task.Delay(20);
}
/// <summary>Test double that records IAddressSpaceBuilder calls.</summary>
private sealed class RecordingBuilder : IAddressSpaceBuilder
{
/// <summary>Gets the list of recorded folder calls.</summary>
public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
/// <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>
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>
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>
public void AddProperty(string _, DriverDataType __, object? ___) { }
private sealed class Handle(string fullRef) : IVariableHandle
{
/// <summary>Gets the full reference.</summary>
public string FullReference => fullRef;
/// <summary>Marks as alarm condition.</summary>
/// <param name="info">The alarm condition information.</param>
/// <returns>An alarm condition sink.</returns>
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
}
private sealed class NullSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } }
/// <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>
public void OnTransition(AlarmEventArgs args) { }
}
}
}
@@ -20,6 +20,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>
[Fact]
public async Task InitializeAsync_throws_when_tag_DeviceHostAddress_not_in_Devices()
{
@@ -41,6 +42,7 @@ public sealed class FocasDriverMediumFindingsTests
ex.Message.ShouldContain("not in the Devices list");
}
/// <summary>Verifies that initialization errors name the offending tag.</summary>
[Fact]
public async Task InitializeAsync_throws_naming_the_offending_tag()
{
@@ -61,6 +63,7 @@ public sealed class FocasDriverMediumFindingsTests
ex.Message.ShouldContain("TypoTag");
}
/// <summary>Verifies that initialization succeeds when all tags reference declared devices.</summary>
[Fact]
public async Task InitializeAsync_succeeds_when_all_tags_reference_declared_devices()
{
@@ -86,6 +89,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>
[Fact]
public async Task DiscoverAsync_all_user_tags_are_ViewOnly_regardless_of_Writable_field()
{
@@ -114,6 +118,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>
[Fact]
public async Task GetHealth_reflects_state_updated_from_concurrent_reads()
{
@@ -142,6 +147,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>
[Fact]
public async Task Read_recovers_after_client_is_externally_disposed()
{
@@ -177,6 +183,7 @@ public sealed class FocasDriverMediumFindingsTests
factory.Clients[1].ConnectCount.ShouldBe(1);
}
/// <summary>Verifies that reads dispose stale clients before creating fresh ones.</summary>
[Fact]
public async Task Read_disposes_stale_client_before_creating_fresh_one()
{
@@ -211,6 +218,7 @@ public sealed class FocasDriverMediumFindingsTests
// ---- Driver.FOCAS-012: factory round-trip for all three opt-in sections ----
/// <summary>Verifies factory round-trip with all optional configuration sections.</summary>
[Fact]
public void CreateInstance_full_round_trip_all_opt_in_sections()
{
@@ -240,24 +248,51 @@ public sealed class FocasDriverMediumFindingsTests
// ---- helpers ----
/// <summary>Records folder and variable additions for test verification.</summary>
private sealed class RecordingBuilder : IAddressSpaceBuilder
{
/// <summary>Gets or sets the list of added variables.</summary>
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
/// <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>
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>
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>
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>
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>
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
}
private sealed class NullSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } }
/// <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>
public void OnTransition(AlarmEventArgs args) { }
}
}
}
@@ -14,6 +14,7 @@ public sealed class FocasFactoryConfigTests
{
// ---- Driver.FOCAS-001: FixedTree / AlarmProjection / HandleRecycle config sections ----
/// <summary>Verifies that the FixedTree configuration section is mapped to driver options.</summary>
[Fact]
public void CreateInstance_maps_FixedTree_section_onto_options()
{
@@ -37,6 +38,7 @@ public sealed class FocasFactoryConfigTests
drv.Options.FixedTree.TimerPollInterval.ShouldBe(TimeSpan.FromSeconds(30));
}
/// <summary>Verifies that the AlarmProjection configuration section is mapped to driver options.</summary>
[Fact]
public void CreateInstance_maps_AlarmProjection_section_onto_options()
{
@@ -53,6 +55,7 @@ public sealed class FocasFactoryConfigTests
drv.Options.AlarmProjection.PollInterval.ShouldBe(TimeSpan.FromSeconds(2));
}
/// <summary>Verifies that the HandleRecycle configuration section is mapped to driver options.</summary>
[Fact]
public void CreateInstance_maps_HandleRecycle_section_onto_options()
{
@@ -69,6 +72,7 @@ public sealed class FocasFactoryConfigTests
drv.Options.HandleRecycle.Interval.ShouldBe(TimeSpan.FromHours(1));
}
/// <summary>Verifies that all three optional configuration sections are mapped together.</summary>
[Fact]
public void CreateInstance_round_trips_all_three_opt_in_sections_together()
{
@@ -88,6 +92,7 @@ public sealed class FocasFactoryConfigTests
drv.Options.HandleRecycle.Enabled.ShouldBeTrue();
}
/// <summary>Verifies that default disabled values are maintained when sections are absent.</summary>
[Fact]
public void CreateInstance_keeps_disabled_defaults_when_sections_absent()
{
@@ -98,6 +103,7 @@ public sealed class FocasFactoryConfigTests
drv.Options.HandleRecycle.Enabled.ShouldBeFalse();
}
/// <summary>Verifies that per-field defaults are maintained when only Enabled is supplied.</summary>
[Fact]
public void CreateInstance_keeps_per_field_defaults_when_only_Enabled_supplied()
{
@@ -112,6 +118,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>
[Fact]
public async Task FixedTree_bootstrap_marks_ProgramInfo_unsupported_when_probe_throws()
{
@@ -138,6 +145,7 @@ public sealed class FocasFactoryConfigTests
await drv.ShutdownAsync(CancellationToken.None);
}
/// <summary>Verifies that ProgramInfo is marked supported when probe succeeds.</summary>
[Fact]
public async Task FixedTree_bootstrap_marks_ProgramInfo_supported_when_probe_succeeds()
{
@@ -175,6 +183,10 @@ public sealed class FocasFactoryConfigTests
/// </summary>
private sealed class ProgramInfoFailingFocasClient : FakeFocasClient
{
/// <summary>Gets program info by throwing to simulate unsupported operations.</summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Never returns; always throws.</returns>
/// <inheritdoc />
public override Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken ct) =>
throw new InvalidOperationException(
"cnc_exeprgname2 failed EW_6 and cnc_rdopmode failed EW_6.");
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
[Trait("Category", "Unit")]
public sealed class FocasHandleRecycleTests
{
/// <summary>Verifies that the recycle loop disposes clients on interval and reopens fresh ones.</summary>
[Fact]
public async Task Recycle_loop_disposes_client_on_interval_reads_reopen_fresh_one()
{
@@ -38,6 +39,7 @@ public sealed class FocasHandleRecycleTests
await drv.ShutdownAsync(CancellationToken.None);
}
/// <summary>Verifies that the recycle loop stays off when not enabled.</summary>
[Fact]
public async Task Recycle_loop_stays_off_when_not_enabled()
{
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
[Trait("Category", "Unit")]
public sealed class FocasLoggingTests
{
/// <summary>Verifies that the constructor accepts an ILogger.</summary>
[Fact]
public void Constructor_accepts_an_ILogger()
{
@@ -29,6 +30,7 @@ public sealed class FocasLoggingTests
drv.ShouldNotBeNull();
}
/// <summary>Verifies that probe loop logs when an exception is swallowed.</summary>
[Fact]
public async Task ProbeLoop_logs_when_an_exception_is_swallowed()
{
@@ -82,10 +84,23 @@ public sealed class FocasLoggingTests
private sealed class CapturingLogger<T> : ILogger<T>
{
/// <summary>Gets the captured log entries.</summary>
public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();
/// <summary>Begins a logging scope.</summary>
/// <param name="state">The scope state.</param>
/// <typeparam name="TState">The type of the state.</typeparam>
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>
public bool IsEnabled(LogLevel logLevel) => true;
/// <summary>Logs a message.</summary>
/// <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>
/// <typeparam name="TState">The type of the state.</typeparam>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
@@ -94,7 +109,9 @@ public sealed class FocasLoggingTests
private sealed class NullScope : IDisposable
{
/// <summary>Gets the singleton instance.</summary>
public static NullScope Instance { get; } = new();
/// <summary>Disposes the scope.</summary>
public void Dispose() { }
}
}
@@ -20,6 +20,9 @@ public sealed class FocasLowFindingsTests
{
// ---- Driver.FOCAS-008 — parsed FocasAddress cached at init ----
/// <summary>
/// Verifies that ReadAsync uses cached FocasAddress when tag definition has a malformed address after init.
/// </summary>
[Fact]
public async Task ReadAsync_uses_cached_FocasAddress_when_tag_definition_has_a_malformed_address_after_init()
{
@@ -53,6 +56,9 @@ public sealed class FocasLowFindingsTests
.ShouldBeTrue("ReadAsync must reuse the FocasAddress parsed at init, not re-parse per read");
}
/// <summary>
/// Verifies that WriteAsync also uses cached FocasAddress.
/// </summary>
[Fact]
public async Task WriteAsync_uses_cached_FocasAddress_too()
{
@@ -90,6 +96,9 @@ public sealed class FocasLowFindingsTests
// ---- Driver.FOCAS-009 — Probe.Timeout applies to ProbeAsync ----
/// <summary>
/// Verifies that ProbeLoop cancels a slow ProbeAsync at Probe Timeout.
/// </summary>
[Fact]
public async Task ProbeLoop_cancels_a_slow_ProbeAsync_at_Probe_Timeout()
{
@@ -130,6 +139,11 @@ public sealed class FocasLowFindingsTests
// ---- Driver.FOCAS-010 — operation-mode → text mapping is consolidated ----
/// <summary>
/// Verifies that OpMode ToText yields the same label in both namespaces.
/// </summary>
/// <param name="code">The operation mode code to test.</param>
/// <param name="expected">The expected text representation.</param>
[Theory]
[InlineData(0, "MDI")]
[InlineData(1, "AUTO")]
@@ -149,6 +163,9 @@ public sealed class FocasLowFindingsTests
((FocasOperationMode)(short)code).ToText().ShouldBe(expected);
}
/// <summary>
/// Verifies that OpMode ToText fallback label is consistent.
/// </summary>
[Fact]
public void OpMode_ToText_fallback_label_is_consistent()
{
@@ -162,6 +179,9 @@ public sealed class FocasLowFindingsTests
// ---- Driver.FOCAS-011 — FocasAlarmType constants typed as short ----
/// <summary>
/// Verifies that FocasAlarmType constants are typed as short.
/// </summary>
[Fact]
public void FocasAlarmType_constants_are_typed_short()
{
@@ -184,6 +204,7 @@ public sealed class FocasLowFindingsTests
private sealed class CapturingFakeFocasClient(List<FocasAddress> captured) : FakeFocasClient
{
/// <inheritdoc />
public override Task<(object? value, uint status)> ReadAsync(
FocasAddress address, FocasDataType type, CancellationToken ct)
{
@@ -191,6 +212,7 @@ public sealed class FocasLowFindingsTests
return base.ReadAsync(address, type, ct);
}
/// <inheritdoc />
public override Task<uint> WriteAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken ct)
{
@@ -201,6 +223,7 @@ public sealed class FocasLowFindingsTests
private sealed class HangingProbeFakeClient(TaskCompletionSource cancelledSignal) : FakeFocasClient
{
/// <inheritdoc />
public override async Task<bool> ProbeAsync(CancellationToken ct)
{
try
@@ -15,8 +15,10 @@ public sealed class FocasPmcBitRmwTests
/// </summary>
private sealed class PmcRmwFake : FakeFocasClient
{
/// <summary>Gets the simulated PMC byte storage.</summary>
public byte[] PmcBytes { get; } = new byte[1024];
/// <inheritdoc />
public override Task<(object? value, uint status)> ReadAsync(
FocasAddress address, FocasDataType type, CancellationToken ct)
{
@@ -27,6 +29,7 @@ public sealed class FocasPmcBitRmwTests
return base.ReadAsync(address, type, ct);
}
/// <inheritdoc />
public override Task<uint> WriteAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken ct)
{
@@ -62,6 +65,7 @@ public sealed class FocasPmcBitRmwTests
return (drv, fake);
}
/// <summary>Verifies that a bit set operation surfaces as Good status and flips the bit.</summary>
[Fact]
public async Task Bit_set_surfaces_as_Good_status_and_flips_bit()
{
@@ -76,6 +80,7 @@ public sealed class FocasPmcBitRmwTests
fake.PmcBytes[100].ShouldBe((byte)0b0000_1001);
}
/// <summary>Verifies that clearing a bit preserves other bits.</summary>
[Fact]
public async Task Bit_clear_preserves_other_bits()
{
@@ -89,6 +94,7 @@ public sealed class FocasPmcBitRmwTests
fake.PmcBytes[100].ShouldBe((byte)0b1111_0111);
}
/// <summary>Verifies that subsequent bit sets in the same byte compose correctly.</summary>
[Fact]
public async Task Subsequent_bit_sets_in_same_byte_compose_correctly()
{
@@ -105,6 +111,7 @@ public sealed class FocasPmcBitRmwTests
fake.PmcBytes[100].ShouldBe((byte)0xFF);
}
/// <summary>Verifies that bit writes to different bytes do not contend.</summary>
[Fact]
public async Task Bit_write_to_different_bytes_does_not_contend()
{
@@ -22,6 +22,7 @@ public sealed class FocasReadWriteTests
// ---- 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 FocasReadWriteTests
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNodeIdUnknown);
}
/// <summary>Verifies that a successful PMC read returns a Good status value.</summary>
[Fact]
public async Task Successful_PMC_read_returns_Good_value()
{
@@ -45,6 +47,7 @@ public sealed class FocasReadWriteTests
snapshots.Single().Value.ShouldBe((sbyte)5);
}
/// <summary>Verifies that parameter reads route through the FocasAddress Parameter kind.</summary>
[Fact]
public async Task Parameter_read_routes_through_FocasAddress_Parameter_kind()
{
@@ -58,6 +61,7 @@ public sealed class FocasReadWriteTests
snapshots.Single().Value.ShouldBe(1500);
}
/// <summary>Verifies that macro reads route through the FocasAddress Macro kind.</summary>
[Fact]
public async Task Macro_read_routes_through_FocasAddress_Macro_kind()
{
@@ -70,6 +74,7 @@ public sealed class FocasReadWriteTests
snapshots.Single().Value.ShouldBe(3.14159);
}
/// <summary>Verifies that repeated reads reuse the connection.</summary>
[Fact]
public async Task Repeat_read_reuses_connection()
{
@@ -85,6 +90,7 @@ public sealed class FocasReadWriteTests
factory.Clients[0].ConnectCount.ShouldBe(1);
}
/// <summary>Verifies that FOCAS error statuses map correctly via the status mapper.</summary>
[Fact]
public async Task FOCAS_error_status_maps_via_status_mapper()
{
@@ -102,6 +108,7 @@ public sealed class FocasReadWriteTests
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNodeIdUnknown);
}
/// <summary>Verifies that a read exception surfaces BadCommunicationError.</summary>
[Fact]
public async Task Read_exception_surfaces_BadCommunicationError()
{
@@ -115,6 +122,7 @@ public sealed class FocasReadWriteTests
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
}
/// <summary>Verifies that a connection failure disposes the client and surfaces BadCommunicationError.</summary>
[Fact]
public async Task Connect_failure_disposes_client_and_surfaces_BadCommunicationError()
{
@@ -128,6 +136,7 @@ public sealed class FocasReadWriteTests
factory.Clients[0].DisposeCount.ShouldBe(1);
}
/// <summary>Verifies that batched reads preserve order across different address areas.</summary>
[Fact]
public async Task Batched_reads_preserve_order_across_areas()
{
@@ -154,6 +163,7 @@ public sealed class FocasReadWriteTests
// ---- Write ----
/// <summary>Verifies that a non-writable tag write is rejected with BadNotWritable.</summary>
[Fact]
public async Task Non_writable_tag_rejected_with_BadNotWritable()
{
@@ -166,6 +176,7 @@ public sealed class FocasReadWriteTests
results.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNotWritable);
}
/// <summary>Verifies that a successful write logs the address, type, and value.</summary>
[Fact]
public async Task Successful_write_logs_address_type_value()
{
@@ -183,6 +194,7 @@ public sealed class FocasReadWriteTests
write.value.ShouldBe((short)1800);
}
/// <summary>Verifies that write status codes map correctly via the FocasStatusMapper.</summary>
[Fact]
public async Task Write_status_code_maps_via_FocasStatusMapper()
{
@@ -201,6 +213,7 @@ public sealed class FocasReadWriteTests
results.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNotWritable);
}
/// <summary>Verifies that batched writes preserve order across different outcomes.</summary>
[Fact]
public async Task Batch_write_preserves_order_across_outcomes()
{
@@ -229,6 +242,7 @@ public sealed class FocasReadWriteTests
results[2].StatusCode.ShouldBe(FocasStatusMapper.BadNodeIdUnknown);
}
/// <summary>Verifies that cancellation signals are propagated.</summary>
[Fact]
public async Task Cancellation_propagates()
{
@@ -245,6 +259,7 @@ public sealed class FocasReadWriteTests
() => drv.ReadAsync(["X"], CancellationToken.None));
}
/// <summary>Verifies that ShutdownAsync disposes the client.</summary>
[Fact]
public async Task ShutdownAsync_disposes_client()
{
@@ -10,6 +10,10 @@ public sealed class FocasScaffoldingTests
{
// ---- FocasHostAddress ----
/// <summary>Verifies FocasHostAddress.TryParse correctly parses valid addresses.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="host">The expected host after parsing.</param>
/// <param name="port">The expected port after parsing.</param>
[Theory]
[InlineData("focas://10.0.0.5:8193", "10.0.0.5", 8193)]
[InlineData("focas://10.0.0.5", "10.0.0.5", 8193)] // default port
@@ -24,6 +28,8 @@ public sealed class FocasScaffoldingTests
parsed.Port.ShouldBe(port);
}
/// <summary>Verifies FocasHostAddress.TryParse rejects invalid addresses.</summary>
/// <param name="input">The input address string to test for rejection.</param>
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -38,6 +44,7 @@ public sealed class FocasScaffoldingTests
FocasHostAddress.TryParse(input).ShouldBeNull();
}
/// <summary>Verifies FocasHostAddress.ToString strips the default port.</summary>
[Fact]
public void HostAddress_ToString_strips_default_port()
{
@@ -47,6 +54,12 @@ public sealed class FocasScaffoldingTests
// ---- FocasAddress ----
/// <summary>Verifies FocasAddress.TryParse correctly parses PMC address forms.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="kind">The expected FocasAreaKind after parsing.</param>
/// <param name="letter">The expected PMC letter after parsing.</param>
/// <param name="num">The expected number after parsing.</param>
/// <param name="bit">The expected bit index after parsing.</param>
[Theory]
[InlineData("X0.0", FocasAreaKind.Pmc, "X", 0, 0)]
[InlineData("X0", FocasAreaKind.Pmc, "X", 0, null)]
@@ -70,6 +83,11 @@ public sealed class FocasScaffoldingTests
a.BitIndex.ShouldBe(bit);
}
/// <summary>Verifies FocasAddress.TryParse correctly parses parameter address forms.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="kind">The expected FocasAreaKind after parsing.</param>
/// <param name="num">The expected number after parsing.</param>
/// <param name="bit">The expected bit index after parsing.</param>
[Theory]
[InlineData("PARAM:1020", FocasAreaKind.Parameter, 1020, null)]
[InlineData("PARAM:1815/0", FocasAreaKind.Parameter, 1815, 0)]
@@ -84,6 +102,10 @@ public sealed class FocasScaffoldingTests
a.BitIndex.ShouldBe(bit);
}
/// <summary>Verifies FocasAddress.TryParse correctly parses macro address forms.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="kind">The expected FocasAreaKind after parsing.</param>
/// <param name="num">The expected number after parsing.</param>
[Theory]
[InlineData("MACRO:100", FocasAreaKind.Macro, 100)]
[InlineData("MACRO:500", FocasAreaKind.Macro, 500)]
@@ -96,6 +118,8 @@ public sealed class FocasScaffoldingTests
a.BitIndex.ShouldBeNull();
}
/// <summary>Verifies FocasAddress.TryParse rejects invalid address forms.</summary>
/// <param name="input">The input address string to test for rejection.</param>
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -114,6 +138,8 @@ public sealed class FocasScaffoldingTests
FocasAddress.TryParse(input).ShouldBeNull();
}
/// <summary>Verifies FocasAddress.Canonical roundtrips correctly.</summary>
/// <param name="input">The input address string to roundtrip.</param>
[Theory]
[InlineData("X0.0")]
[InlineData("R100")]
@@ -130,6 +156,7 @@ public sealed class FocasScaffoldingTests
// ---- FocasDataType ----
/// <summary>Verifies data type mapping covers all atomic FOCAS types.</summary>
[Fact]
public void DataType_mapping_covers_atomic_focas_types()
{
@@ -143,6 +170,9 @@ public sealed class FocasScaffoldingTests
// ---- FocasStatusMapper ----
/// <summary>Verifies status mapper covers all known FOCAS return codes.</summary>
/// <param name="ret">The FOCAS return code to map.</param>
/// <param name="expected">The expected mapped status code.</param>
[Theory]
[InlineData(0, FocasStatusMapper.Good)]
[InlineData(3, FocasStatusMapper.BadOutOfRange)] // EW_NUMBER
@@ -161,6 +191,7 @@ public sealed class FocasScaffoldingTests
// ---- FocasDriver ----
/// <summary>Verifies FocasDriver initializes with correct type and ID.</summary>
[Fact]
public void DriverType_is_FOCAS()
{
@@ -169,6 +200,7 @@ public sealed class FocasScaffoldingTests
drv.DriverInstanceId.ShouldBe("drv-1");
}
/// <summary>Verifies InitializeAsync parses device addresses correctly.</summary>
[Fact]
public async Task InitializeAsync_parses_device_addresses()
{
@@ -188,6 +220,7 @@ public sealed class FocasScaffoldingTests
drv.GetDeviceState("focas://10.0.0.6:12345")!.Options.DeviceName.ShouldBe("CNC-2");
}
/// <summary>Verifies InitializeAsync faults on malformed addresses.</summary>
[Fact]
public async Task InitializeAsync_malformed_address_faults()
{
@@ -201,6 +234,7 @@ public sealed class FocasScaffoldingTests
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
}
/// <summary>Verifies ShutdownAsync clears all devices.</summary>
[Fact]
public async Task ShutdownAsync_clears_devices()
{
@@ -218,6 +252,7 @@ public sealed class FocasScaffoldingTests
// ---- UnimplementedFocasClientFactory ----
/// <summary>Verifies UnimplementedFocasClientFactory throws on Create.</summary>
[Fact]
public void Unimplemented_factory_throws_on_Create_with_config_pointer()
{