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:
@@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user