using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.S7.Szl; namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500; /// /// PR-S7-E1 / #302 — integration scaffold for SZL (System Status List) reads against /// a real S7-1500 CPU. snap7 (the simulator that backs ) /// does not implement SZL — every @System.* read returns BadNotSupported /// 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 (S7_LIVE_HOST) the same way other PR-S7-* live tests are. /// /// /// /// Why scaffolding rather than full live verification? 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. /// /// [Collection(Snap7ServerCollection.Name)] [Trait("Category", "Integration")] [Trait("Device", "S7_1500")] public sealed class S7_1500SzlTests(Snap7ServerFixture sim) { /// OPC UA BadNotSupported status code — same constant the driver uses. private const uint StatusBadNotSupported = 0x803D0000u; /// /// Re-build the simulator profile with ExposeSystemTags = true. /// is a class (not a record), so we copy fields manually rather than using a with expression. /// 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); } /// /// Live-firmware gate: when the env-var S7_LIVE_HOST 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 because S7netplus 0.20 doesn't /// expose a public SZL surface — even against a real CPU the production /// returns null. Flip this back on once the /// S7netplus PR for ReadSzlAsync lands or we ship a raw-PDU helper. /// [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 Folders { get; } = []; public List 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(); } } }