using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.Series; /// /// Dual-run companion to — exercises the same /// fixed-tree scenarios through the pure-managed /// instead of the shim/P-Invoke path. Proves both backends observe identical /// state against the same focas-mock instance. /// /// /// Scheduled for removal in Wire migration phase 3 (task #104) once the shim is /// deleted — at that point only this class survives and becomes the canonical /// fixed-tree integration test. /// [Collection(FocasSimCollection.Name)] public sealed class WireBackendTests { private readonly FocasSimFixture _fx; public WireBackendTests(FocasSimFixture fx) => _fx = fx; private const string DeviceHost = "focas://127.0.0.1:8193"; [Fact] public async Task Identity_axes_and_dynamic_populate_via_wire_backend() { if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason); var ct = TestContext.Current.CancellationToken; await _fx.LoadProfileAsync("FWLIB64", ct); await _fx.PatchStateAsync(new { sysinfo = new { addinfo = 0, max_axis = 8, cnc_type = "M ", mt_type = "M ", series = "30i ", version = "A1.0", axes = "3 ", }, axis_names = new[] { "X", "Y", "Z" }, rddynamic2 = new { axis = 1, alarm = 0, prgnum = 1, prgmnum = 1, seqnum = 42, actf = 1500, acts = 3200, pos = new { absolute = 123456, machine = 123450, relative = 6, distance = 0 }, }, }, ct); var driver = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions(DeviceHost)], Tags = [], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(100), }, }, driverInstanceId: "focas-wire-identity", clientFactory: new WireFocasClientFactory()); await using (driver) { await driver.InitializeAsync("{}", ct); await WaitFor(() => driver.GetDeviceState(DeviceHost) is { FixedTreeCache: not null }, TimeSpan.FromSeconds(5)); var state = driver.GetDeviceState(DeviceHost); state.ShouldNotBeNull(); state.FixedTreeCache.ShouldNotBeNull(); state.FixedTreeCache.SysInfo.Series.ShouldStartWith("30i"); state.FixedTreeCache.Axes.Count.ShouldBe(3); state.FixedTreeCache.Axes[0].Display.ShouldBe("X"); await WaitFor(() => state.LastFixedSnapshots.ContainsKey($"{DeviceHost}/Axes/X/AbsolutePosition"), TimeSpan.FromSeconds(3)); state.LastFixedSnapshots[$"{DeviceHost}/Axes/X/AbsolutePosition"].ShouldBe(123456); } } [Fact] public async Task Program_and_operation_mode_populate_via_wire_backend() { if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason); var ct = TestContext.Current.CancellationToken; await _fx.LoadProfileAsync("FWLIB64", ct); await _fx.PatchStateAsync(new { sysinfo = new { addinfo = 0, max_axis = 8, cnc_type = "M ", mt_type = "M ", series = "30i ", version = "A1.0", axes = "1 ", }, axis_names = new[] { "X" }, rddynamic2 = new { axis = 1, alarm = 0, prgnum = 42, prgmnum = 42, seqnum = 100, actf = 0, acts = 0, pos = new { absolute = 0, machine = 0, relative = 0, distance = 0 }, }, program = new { current = 42, main = 42, sequence = 100, block_count = 17, executing_path = "O0042.NC", }, operation_mode = new { mode = 3 }, }, ct); var driver = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions(DeviceHost)], Tags = [], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(100), ProgramPollInterval = TimeSpan.FromMilliseconds(200), }, }, driverInstanceId: "focas-wire-program", clientFactory: new WireFocasClientFactory()); await using (driver) { await driver.InitializeAsync("{}", ct); await WaitFor(() => driver.GetDeviceState(DeviceHost) is { LastProgramInfo: not null }, TimeSpan.FromSeconds(5)); var snapshots = await driver.ReadAsync( [$"{DeviceHost}/Program/Name", $"{DeviceHost}/Program/ONumber", $"{DeviceHost}/Program/BlockCount", $"{DeviceHost}/OperationMode/Mode"], ct); snapshots.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good); snapshots[0].Value!.ToString().ShouldStartWith("O0042"); Convert.ToInt32(snapshots[1].Value).ShouldBe(42); Convert.ToInt32(snapshots[2].Value).ShouldBe(17); Convert.ToInt32(snapshots[3].Value).ShouldBe(3); } } [Fact] public async Task Timers_populate_via_wire_backend() { if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason); var ct = TestContext.Current.CancellationToken; await _fx.LoadProfileAsync("FWLIB64", ct); await _fx.PatchStateAsync(new { axis_names = new[] { "X" }, timers = new { power_on = 3600, operating = 7200, cutting = 1800, cycle = 120, }, }, ct); var driver = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions(DeviceHost)], Tags = [], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(100), TimerPollInterval = TimeSpan.FromMilliseconds(200), ProgramPollInterval = TimeSpan.Zero, }, }, driverInstanceId: "focas-wire-timers", clientFactory: new WireFocasClientFactory()); await using (driver) { await driver.InitializeAsync("{}", ct); await WaitFor(() => { var state = driver.GetDeviceState(DeviceHost); return state is not null && state.LastTimers.Count == 4; }, TimeSpan.FromSeconds(5)); var snapshots = await driver.ReadAsync( [$"{DeviceHost}/Timers/PowerOnSeconds", $"{DeviceHost}/Timers/OperatingSeconds", $"{DeviceHost}/Timers/CuttingSeconds", $"{DeviceHost}/Timers/CycleSeconds"], ct); snapshots.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good); Convert.ToDouble(snapshots[0].Value).ShouldBe(3600.0, tolerance: 1); Convert.ToDouble(snapshots[1].Value).ShouldBe(7200.0, tolerance: 1); Convert.ToDouble(snapshots[2].Value).ShouldBe(1800.0, tolerance: 1); Convert.ToDouble(snapshots[3].Value).ShouldBe(120.0, tolerance: 1); } } [Fact] public async Task Spindle_load_and_max_rpm_populate_via_wire_backend() { if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason); var ct = TestContext.Current.CancellationToken; await _fx.LoadProfileAsync("FWLIB64", ct); await _fx.PatchStateAsync(new { axis_names = new[] { "X" }, spindle_names = new[] { "S1", "S2" }, spindle = new { load = new object[] { new { name = "S1", load = 56, speed = 3200 }, new { name = "S2", load = 12, speed = 1800 }, }, max_rpm = new[] { 6000, 4500 }, }, rddynamic2 = new { axis = 1, alarm = 0, prgnum = 1, prgmnum = 1, seqnum = 1, actf = 0, acts = 0, pos = new { absolute = 0, machine = 0, relative = 0, distance = 0 }, }, }, ct); var driver = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions(DeviceHost)], Tags = [], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(100), ProgramPollInterval = TimeSpan.Zero, TimerPollInterval = TimeSpan.Zero, }, }, driverInstanceId: "focas-wire-spindle", clientFactory: new WireFocasClientFactory()); await using (driver) { await driver.InitializeAsync("{}", ct); await WaitFor(() => { var state = driver.GetDeviceState(DeviceHost); return state?.FixedTreeCache is { Capabilities.SpindleLoad: true, Capabilities.SpindleMaxRpm: true } && state.LastSpindleLoads.Count >= 2; }, TimeSpan.FromSeconds(5)); var snapshots = await driver.ReadAsync( [$"{DeviceHost}/Spindle/S1/Load", $"{DeviceHost}/Spindle/S1/MaxRpm", $"{DeviceHost}/Spindle/S2/Load", $"{DeviceHost}/Spindle/S2/MaxRpm"], ct); snapshots.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good); Convert.ToInt32(snapshots[0].Value).ShouldBe(56); Convert.ToInt32(snapshots[1].Value).ShouldBe(6000); Convert.ToInt32(snapshots[2].Value).ShouldBe(12); Convert.ToInt32(snapshots[3].Value).ShouldBe(4500); } } private static async Task WaitFor(Func pred, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { if (pred()) return; await Task.Delay(50); } } }