using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.Series; /// /// Gitea #548, wire half — duplicate sibling axis names must survive the managed wire /// client's cnc_rdaxisname parse (no dedupe, no error) and come out of the driver's /// bootstrap as disambiguated segments, exercised against the real focas-mock over TCP. /// /// /// The mock's own dynamic store is keyed by axis NAME, so it cannot serve two duplicate /// axes distinct position values — that half of #548 is pinned by the offline /// FocasDuplicateSiblingNameTests, which drive slot-distinct snapshots through the /// fake client. What only this test proves is the wire path: the duplicate names reach the /// driver intact and the published snapshot keys are the suffixed segments. /// [Collection(FocasSimCollection.Name)] public sealed class WireBackendDuplicateAxisTests { private readonly FocasSimFixture _fx; /// Initializes a new instance of the class. /// The FOCAS simulator fixture. public WireBackendDuplicateAxisTests(FocasSimFixture fx) => _fx = fx; private string DeviceHost => _fx.DeviceUri; /// Duplicate axis names arrive intact off the wire and publish as suffixed segments. [Fact] public async Task Duplicate_axis_names_disambiguate_through_the_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 ", }, // The Makino shape: two axes rendering the same display name, one unique between them. axis_names = new[] { "AA", "Y", "AA" }, }, ct); var driver = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions(DeviceHost)], RawTags = [], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(100), ProgramPollInterval = TimeSpan.Zero, TimerPollInterval = TimeSpan.Zero, }, }, driverInstanceId: "focas-wire-dup-axes", 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(); // The wire client must not collapse or drop the duplicates... state.FixedTreeCache.Axes.Count.ShouldBe(3); state.FixedTreeCache.Axes.Select(a => a.Display).ShouldBe(["AA", "Y", "AA"]); // ...and the driver must disambiguate them. state.FixedTreeCache.AxisSegments.ShouldBe(["AA_1", "Y", "AA_2"]); // Both duplicate axes publish their own snapshot slot; nothing publishes bare AA. await WaitFor(() => state.LastFixedSnapshots.ContainsKey($"{DeviceHost}/Axes/AA_2/AbsolutePosition"), TimeSpan.FromSeconds(3)); state.LastFixedSnapshots.ContainsKey($"{DeviceHost}/Axes/AA_1/AbsolutePosition").ShouldBeTrue(); state.LastFixedSnapshots.ContainsKey($"{DeviceHost}/Axes/Y/AbsolutePosition").ShouldBeTrue(); state.LastFixedSnapshots.ContainsKey($"{DeviceHost}/Axes/AA/AbsolutePosition").ShouldBeFalse(); } } 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); } } }