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);
}
}
}