Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SzlTests.cs
2026-04-26 10:30:43 -04:00

140 lines
6.3 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.S7.Szl;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
/// <summary>
/// PR-S7-E1 / #302 — integration scaffold for SZL (System Status List) reads against
/// a real S7-1500 CPU. snap7 (the simulator that backs <see cref="Snap7ServerFixture"/>)
/// does not implement SZL — every <c>@System.*</c> read returns <c>BadNotSupported</c>
/// against the simulator — so the asserts here verify the not-supported semantics
/// when running against snap7, and the live-firmware tests are gated on a real-PLC
/// env-var (<c>S7_LIVE_HOST</c>) the same way other PR-S7-* live tests are.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why scaffolding rather than full live verification?</b> The plan section
/// calls for a "live-firmware test against dev-box S7-1500"; that's a hardware-
/// gated test and it is parked behind an env-var so the CI pipeline + a fresh
/// developer checkout both stay green. The not-supported assertion against snap7
/// is the "always-runs" piece — proves the dispatch path lights up + surfaces
/// the right StatusCode without a live CPU.
/// </para>
/// </remarks>
[Collection(Snap7ServerCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "S7_1500")]
public sealed class S7_1500SzlTests(Snap7ServerFixture sim)
{
/// <summary>OPC UA <c>BadNotSupported</c> status code — same constant the driver uses.</summary>
private const uint StatusBadNotSupported = 0x803D0000u;
/// <summary>
/// Re-build the simulator profile with <c>ExposeSystemTags = true</c>. <see cref="S7DriverOptions"/>
/// is a class (not a record), so we copy fields manually rather than using a <c>with</c> expression.
/// </summary>
private static S7DriverOptions BuildOptionsWithSystemTags(string host, int port, int diagBufferDepth = 10)
{
var baseOpts = S7_1500Profile.BuildOptions(host, port);
return new S7DriverOptions
{
Host = baseOpts.Host,
Port = baseOpts.Port,
CpuType = baseOpts.CpuType,
Rack = baseOpts.Rack,
Slot = baseOpts.Slot,
Timeout = baseOpts.Timeout,
Probe = baseOpts.Probe,
Tags = baseOpts.Tags,
ExposeSystemTags = true,
DiagBufferDepth = diagBufferDepth,
};
}
[Fact]
public async Task System_CpuType_returns_BadNotSupported_against_snap7_simulator()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var options = BuildOptionsWithSystemTags(sim.Host, sim.Port);
await using var drv = new S7Driver(options, driverInstanceId: "s7-szl-cputype");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var snaps = await drv.ReadAsync(["@System.CpuType"], TestContext.Current.CancellationToken);
snaps.Count.ShouldBe(1);
// snap7 doesn't implement SZL; the production S7NetSzlReader returns null too
// (S7netplus 0.20 has no public ReadSzlAsync surface). Both paths converge on
// BadNotSupported.
snaps[0].StatusCode.ShouldBe(StatusBadNotSupported);
}
[Fact]
public async Task DiscoverAsync_emits_diagnostics_folder_against_real_simulator_when_opted_in()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var options = BuildOptionsWithSystemTags(sim.Host, sim.Port, diagBufferDepth: 5);
await using var drv = new S7Driver(options, driverInstanceId: "s7-szl-discover");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var builder = new TestAddressSpaceBuilder();
await drv.DiscoverAsync(builder, TestContext.Current.CancellationToken);
builder.Folders.ShouldContain(S7SystemTags.FolderName);
builder.Folders.ShouldContain("DiagBuffer");
// 6 scalars + 5 buffer entries = 11 system variables.
builder.Variables
.Count(v => v.FullName.StartsWith(S7SystemTags.Prefix, StringComparison.Ordinal))
.ShouldBe(11);
}
/// <summary>
/// Live-firmware gate: when the env-var <c>S7_LIVE_HOST</c> points at a real
/// S7-1500, this test runs end-to-end against the live CPU and expects a
/// non-empty CpuType / Firmware / OrderNo. Hardware-gated; CI skips it.
/// Currently parked at <see cref="Assert.Skip"/> because S7netplus 0.20 doesn't
/// expose a public SZL surface — even against a real CPU the production
/// <see cref="S7NetSzlReader"/> returns null. Flip this back on once the
/// S7netplus PR for ReadSzlAsync lands or we ship a raw-PDU helper.
/// </summary>
[Fact(Skip = "Requires real S7-1500 + S7netplus public ReadSzlAsync surface; see PR-S7-E1 docs.")]
public Task System_CpuType_against_live_S7_1500_returns_non_empty_string()
{
// var liveHost = Environment.GetEnvironmentVariable("S7_LIVE_HOST");
// if (string.IsNullOrWhiteSpace(liveHost))
// Assert.Skip("S7_LIVE_HOST not set — skipping live-firmware SZL test");
// var options = new S7DriverOptions { Host = liveHost, ExposeSystemTags = true, ... };
// ...
return Task.CompletedTask;
}
private sealed class TestAddressSpaceBuilder : Core.Abstractions.IAddressSpaceBuilder
{
public List<string> Folders { get; } = [];
public List<Core.Abstractions.DriverAttributeInfo> Variables { get; } = [];
public Core.Abstractions.IAddressSpaceBuilder Folder(string browseName, string displayName)
{
Folders.Add(browseName);
return this;
}
public Core.Abstractions.IVariableHandle Variable(
string browseName, string displayName, Core.Abstractions.DriverAttributeInfo info)
{
Variables.Add(info);
return new StubHandle();
}
public void AddProperty(string browseName, Core.Abstractions.DriverDataType dataType, object? value) { }
private sealed class StubHandle : Core.Abstractions.IVariableHandle
{
public string FullReference => "stub";
public Core.Abstractions.IAlarmConditionSink MarkAsAlarmCondition(Core.Abstractions.AlarmConditionInfo info)
=> throw new NotImplementedException();
}
}
}