64e3fbe035
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.
169 lines
8.2 KiB
C#
169 lines
8.2 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
|
|
|
/// <summary>
|
|
/// Shape tests for <see cref="S7Driver"/>'s <see cref="ITagDiscovery"/>,
|
|
/// <see cref="ISubscribable"/>, and <see cref="IHostConnectivityProbe"/> surfaces that
|
|
/// don't need a live PLC. Wire-level polling round-trips and probe transitions land in a
|
|
/// follow-up PR once we have a mock S7 server.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class S7DiscoveryAndSubscribeTests
|
|
{
|
|
private sealed class RecordingAddressSpaceBuilder : IAddressSpaceBuilder
|
|
{
|
|
public readonly List<string> Folders = new();
|
|
public readonly List<(string Name, DriverAttributeInfo Attr)> Variables = new();
|
|
|
|
/// <summary>Adds a folder to the address space.</summary>
|
|
/// <param name="browseName">The browse name of the folder.</param>
|
|
/// <param name="displayName">The display name of the folder.</param>
|
|
/// <returns>This builder instance for method chaining.</returns>
|
|
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
|
{
|
|
Folders.Add(browseName);
|
|
return this;
|
|
}
|
|
|
|
/// <summary>Adds a variable to the address space.</summary>
|
|
/// <param name="browseName">The browse name of the variable.</param>
|
|
/// <param name="displayName">The display name of the variable.</param>
|
|
/// <param name="attributeInfo">The attribute information for the variable.</param>
|
|
/// <returns>A handle to the created variable.</returns>
|
|
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
|
{
|
|
Variables.Add((browseName, attributeInfo));
|
|
return new StubHandle();
|
|
}
|
|
|
|
/// <summary>Adds a property to a variable.</summary>
|
|
/// <param name="browseName">The browse name of the property.</param>
|
|
/// <param name="dataType">The data type of the property.</param>
|
|
/// <param name="value">The initial value of the property.</param>
|
|
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
|
|
|
/// <summary>Attaches an alarm condition to a variable.</summary>
|
|
/// <param name="sourceVariable">The variable to attach the alarm to.</param>
|
|
/// <param name="alarmName">The name of the alarm.</param>
|
|
/// <param name="alarmInfo">The alarm information.</param>
|
|
public void AttachAlarmCondition(IVariableHandle sourceVariable, string alarmName, DriverAttributeInfo alarmInfo) { }
|
|
|
|
private sealed class StubHandle : IVariableHandle
|
|
{
|
|
/// <summary>Gets the full reference of the variable.</summary>
|
|
public string FullReference => "stub";
|
|
|
|
/// <summary>Marks this variable as an alarm condition.</summary>
|
|
/// <param name="info">The alarm condition information.</param>
|
|
/// <returns>An alarm condition sink.</returns>
|
|
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
|
|
=> throw new NotImplementedException("S7 driver never calls this — no alarm surfacing");
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that DiscoverAsync projects every configured tag into the address space.</summary>
|
|
[Fact]
|
|
public async Task DiscoverAsync_projects_every_tag_into_the_address_space()
|
|
{
|
|
var opts = new S7DriverOptions
|
|
{
|
|
Host = "192.0.2.1",
|
|
Tags =
|
|
[
|
|
new("TempSetpoint", "DB1.DBW0", S7DataType.Int16, Writable: true),
|
|
new("FaultBit", "M0.0", S7DataType.Bool, Writable: false),
|
|
new("PIDOutput", "DB5.DBD12", S7DataType.Float32, Writable: true),
|
|
],
|
|
};
|
|
using var drv = new S7Driver(opts, "s7-disco");
|
|
|
|
var builder = new RecordingAddressSpaceBuilder();
|
|
await drv.DiscoverAsync(builder, TestContext.Current.CancellationToken);
|
|
|
|
builder.Folders.ShouldContain("S7");
|
|
builder.Variables.Count.ShouldBe(3);
|
|
builder.Variables[0].Name.ShouldBe("TempSetpoint");
|
|
builder.Variables[0].Attr.SecurityClass.ShouldBe(SecurityClassification.Operate, "writable tags get Operate security class");
|
|
builder.Variables[1].Attr.SecurityClass.ShouldBe(SecurityClassification.ViewOnly, "read-only tags get ViewOnly");
|
|
builder.Variables[2].Attr.DriverDataType.ShouldBe(DriverDataType.Float32);
|
|
}
|
|
|
|
/// <summary>Verifies that DiscoverAsync propagates the WriteIdempotent flag from tag configuration to attribute info.</summary>
|
|
[Fact]
|
|
public async Task DiscoverAsync_propagates_WriteIdempotent_from_tag_to_attribute_info()
|
|
{
|
|
var opts = new S7DriverOptions
|
|
{
|
|
Host = "192.0.2.1",
|
|
Tags =
|
|
[
|
|
new("SetPoint", "DB1.DBW0", S7DataType.Int16, WriteIdempotent: true),
|
|
new("StartBit", "M0.0", S7DataType.Bool),
|
|
],
|
|
};
|
|
using var drv = new S7Driver(opts, "s7-idem");
|
|
|
|
var builder = new RecordingAddressSpaceBuilder();
|
|
await drv.DiscoverAsync(builder, TestContext.Current.CancellationToken);
|
|
|
|
builder.Variables.Single(v => v.Name == "SetPoint").Attr.WriteIdempotent.ShouldBeTrue();
|
|
builder.Variables.Single(v => v.Name == "StartBit").Attr.WriteIdempotent.ShouldBeFalse("default is opt-in per decision #44");
|
|
}
|
|
|
|
/// <summary>Verifies that GetHostStatuses returns one row with the host:port identity in pre-init state.</summary>
|
|
[Fact]
|
|
public void GetHostStatuses_returns_one_row_with_host_port_identity_pre_init()
|
|
{
|
|
var opts = new S7DriverOptions { Host = "plc1.internal", Port = 102 };
|
|
using var drv = new S7Driver(opts, "s7-host");
|
|
|
|
var rows = drv.GetHostStatuses();
|
|
rows.Count.ShouldBe(1);
|
|
rows[0].HostName.ShouldBe("plc1.internal:102");
|
|
rows[0].State.ShouldBe(HostState.Unknown, "pre-init / pre-probe state is Unknown");
|
|
}
|
|
|
|
/// <summary>Verifies that SubscribeAsync returns unique handles and UnsubscribeAsync correctly accepts them.</summary>
|
|
[Fact]
|
|
public async Task SubscribeAsync_returns_unique_handles_and_UnsubscribeAsync_accepts_them()
|
|
{
|
|
var opts = new S7DriverOptions { Host = "192.0.2.1" };
|
|
using var drv = new S7Driver(opts, "s7-sub");
|
|
|
|
// SubscribeAsync does not itself call ReadAsync (the poll task does), so this works
|
|
// even though the driver isn't initialized. The poll task catches the resulting
|
|
// InvalidOperationException and the loop quietly continues — same pattern as the
|
|
// Modbus driver's poll loop tolerating transient transport failures.
|
|
var h1 = await drv.SubscribeAsync(["T1"], TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
|
|
var h2 = await drv.SubscribeAsync(["T2"], TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
|
|
|
|
h1.DiagnosticId.ShouldStartWith("s7-sub-");
|
|
h2.DiagnosticId.ShouldStartWith("s7-sub-");
|
|
h1.DiagnosticId.ShouldNotBe(h2.DiagnosticId);
|
|
|
|
await drv.UnsubscribeAsync(h1, TestContext.Current.CancellationToken);
|
|
await drv.UnsubscribeAsync(h2, TestContext.Current.CancellationToken);
|
|
// UnsubscribeAsync with an unknown handle must be a no-op, not throw.
|
|
await drv.UnsubscribeAsync(h1, TestContext.Current.CancellationToken);
|
|
}
|
|
|
|
/// <summary>Verifies that Subscribe floors the publishing interval at 100ms.</summary>
|
|
[Fact]
|
|
public async Task Subscribe_publishing_interval_is_floored_at_100ms()
|
|
{
|
|
var opts = new S7DriverOptions { Host = "192.0.2.1", Probe = new S7ProbeOptions { Enabled = false } };
|
|
using var drv = new S7Driver(opts, "s7-floor");
|
|
|
|
// 50 ms requested — the floor protects the S7 CPU from sub-scan polling that would
|
|
// just queue wire-side. Test that the subscription is accepted (the floor is applied
|
|
// internally; the floor value isn't exposed, so we're really just asserting that the
|
|
// driver doesn't reject small intervals).
|
|
var h = await drv.SubscribeAsync(["T"], TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken);
|
|
h.ShouldNotBeNull();
|
|
await drv.UnsubscribeAsync(h, TestContext.Current.CancellationToken);
|
|
}
|
|
}
|