using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.S7.SymbolImport; using S7NetCpuType = global::S7.Net.CpuType; namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500; /// /// PR-S7-D2 — UDT fan-out integration test against the python-snap7 S7-1500 fixture. /// Seeds a 3-member UDT (Real + Int + Bool) into DB1.MyUdt[400] via /// Docker/profiles/s7_1500.json's udt_layout meta-seed, declares the /// same layout in driver options, and verifies the fanned-out leaf reads return the /// seeded values end-to-end through real S7comm. Build-only by default — the /// simulator fixture skips when python-snap7 isn't running, so this test contributes /// to the CI matrix without requiring docker locally. /// [Collection(Snap7ServerCollection.Name)] [Trait("Category", "Integration")] [Trait("Device", "S7_1500")] public sealed class S7_1500UdtFanOutTests(Snap7ServerFixture sim) { private const string ParentTagName = "MyUdt"; /// /// UDT layout matching the udt_layout meta-seed in the JSON profile. /// Pressure (Real) at byte 0, Status (Int16) at byte 4, Enabled (Bool) at byte 6. /// private static readonly S7UdtDefinition MyUdt = new( Name: "MyUdt", Members: [ new S7UdtMember("Pressure", 0, S7DataType.Float32), new S7UdtMember("Status", 4, S7DataType.Int16), new S7UdtMember("Enabled", 6, S7DataType.Bool), ], SizeBytes: 7); private static S7DriverOptions BuildUdtOptions(string host, int port) => new() { Host = host, Port = port, CpuType = S7NetCpuType.S71500, Timeout = TimeSpan.FromSeconds(5), Probe = new S7ProbeOptions { Enabled = false }, Tags = [ // Parent UDT tag — base address points at byte 400 in DB1, where the // simulator seeded the UDT contents. Fan-out emits three scalar leaves: // MyUdt.Pressure -> DB1.DBD400 (Real 12.5) // MyUdt.Status -> DB1.DBW404 (Int16 7) // MyUdt.Enabled -> DB1.DBX406.0 (Bool true) new S7TagDefinition(ParentTagName, "DB1.DBX400.0", S7DataType.Byte, UdtName: "MyUdt"), ], Udts = [MyUdt], }; [Fact] public async Task Driver_fans_out_udt_into_member_tags() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); var options = BuildUdtOptions(sim.Host, sim.Port); await using var drv = new S7Driver(options, driverInstanceId: "s7-udt-fanout"); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); // After fan-out the parent UDT name is gone from the tag map; only the leaves // are readable. Reading the parent should surface BadNodeIdUnknown. var parent = await drv.ReadAsync([ParentTagName], TestContext.Current.CancellationToken); parent[0].StatusCode.ShouldNotBe(0u, "parent UDT tag must be replaced by its leaves"); // Read the three leaves and assert the seeded values come back. var leaves = await drv.ReadAsync( ["MyUdt.Pressure", "MyUdt.Status", "MyUdt.Enabled"], TestContext.Current.CancellationToken); leaves.Count.ShouldBe(3); foreach (var s in leaves) s.StatusCode.ShouldBe(0u, "every UDT leaf read must succeed end-to-end"); Convert.ToSingle(leaves[0].Value).ShouldBe(12.5f, tolerance: 0.0001f); Convert.ToInt32(leaves[1].Value).ShouldBe(7); Convert.ToBoolean(leaves[2].Value).ShouldBeTrue(); } }