using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// "The control did not say" must read Bad, never a fabricated Good 0. Ported from the sister /// ignitionoee FOCAS driver (commit 101a3b1, 2026-08-12): a cnc_exeprgname2 payload too /// short to carry the O-number published 0 at Good quality — indistinguishable from a /// 31i's genuine 0 for a path-executed program. The same idiom covered every sub-read of the /// program bundle here: a refused cnc_rdopmode published Mode 0 (= MDI, a real /// mode) and a refused cnc_rdblkcount a BlockCount of 0. /// [Trait("Category", "Unit")] public sealed class FocasProgramInfoQualityTests { private const string Host = "focas://10.0.0.5:8193"; private static FocasResult Name(string name, int? oNumber) => new(0, new WireProgramName(name, oNumber)); [Fact] public void A_payload_without_an_O_number_maps_to_null_not_zero() { var info = WireFocasClient.MapProgramInfo(Name("O1111", null), new(0, 42), new(0, 1)); info.Name.ShouldBe("O1111"); info.ONumber.ShouldBeNull(); info.BlockCount.ShouldBe(42); info.Mode.ShouldBe(1); } [Fact] public void A_genuine_zero_O_number_stays_zero() { // Live Z-34184: a 256-byte payload whose o_num is present and genuinely 0. WireFocasClient.MapProgramInfo(Name("//CNC_MEM/USER/LIBRARY/O1111", 0), new(0, 0), new(0, 1)) .ONumber.ShouldBe(0); } [Fact] public void Refused_sub_reads_map_to_null_not_a_fabricated_value() { var info = WireFocasClient.MapProgramInfo(Name("O1", 1), new FocasResult(1, 0), new FocasResult(1, 0)); info.BlockCount.ShouldBeNull(); info.Mode.ShouldBeNull("Mode 0 is MDI — a real mode, not 'unknown'"); var nameless = WireFocasClient.MapProgramInfo(new FocasResult(1, null), new(0, 3), new(0, 1)); nameless.Name.ShouldBeNull(); nameless.ONumber.ShouldBeNull(); } [Fact] public async Task A_missing_field_reads_BadNoData_while_its_siblings_stay_good() { var client = new FakeFocasClient { ProgramInfo = new FocasProgramInfo("O1111", null, 12, null) }; var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions(Host)], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(10), ProgramPollInterval = TimeSpan.FromMilliseconds(10), }, }, "drv-1", new FakeFocasClientFactory { Customise = () => client }); await drv.InitializeAsync("{}", CancellationToken.None); var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5); while (DateTime.UtcNow < deadline && drv.GetDeviceState(Host)?.LastProgramInfo is null) await Task.Delay(15); var read = await drv.ReadAsync( [$"{Host}/Program/Name", $"{Host}/Program/BlockCount", $"{Host}/Program/ONumber", $"{Host}/OperationMode/Mode", $"{Host}/OperationMode/ModeText"], CancellationToken.None); read[0].StatusCode.ShouldBe(FocasStatusMapper.Good); read[0].Value.ShouldBe("O1111"); read[1].StatusCode.ShouldBe(FocasStatusMapper.Good); read[1].Value.ShouldBe(12); read[2].StatusCode.ShouldBe(FocasStatusMapper.BadNoData); read[2].Value.ShouldBeNull(); read[3].StatusCode.ShouldBe(FocasStatusMapper.BadNoData); read[4].StatusCode.ShouldBe(FocasStatusMapper.BadNoData); await drv.ShutdownAsync(CancellationToken.None); } }