Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasProgramInfoQualityTests.cs
T
Joseph Doherty 26b512adba fix(focas): a missing O-number or refused program sub-read goes Bad, not a Good 0
WireFocasClient.GetProgramInfoAsync defaulted every field the CNC did not report:
ONumber ?? 0 when the cnc_exeprgname2 payload was too short to carry it, and
Name '' / BlockCount 0 / Mode 0 when the sub-read was refused. 0 is a genuine
O-number (a 31i reports 0 for a path-executed program -- live Z-34184), a
genuine block count, and Mode 0 is MDI, a real operating mode.

FocasProgramInfo fields are now nullable; MapProgramInfo leaves unreported
fields null and the fixed tree publishes them BadNoData while the bundle's
other fields stay Good.

Ported from the sister ignitionoee FOCAS driver (101a3b1, 2026-08-12).
2026-10-07 14:08:26 -04:00

93 lines
3.8 KiB
C#

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;
/// <summary>
/// "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 <c>cnc_exeprgname2</c> payload too
/// short to carry the O-number published <c>0</c> 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 <c>cnc_rdopmode</c> published Mode <c>0</c> (= MDI, a real
/// mode) and a refused <c>cnc_rdblkcount</c> a BlockCount of 0.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasProgramInfoQualityTests
{
private const string Host = "focas://10.0.0.5:8193";
private static FocasResult<WireProgramName> 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<int>(1, 0), new FocasResult<short>(1, 0));
info.BlockCount.ShouldBeNull();
info.Mode.ShouldBeNull("Mode 0 is MDI — a real mode, not 'unknown'");
var nameless = WireFocasClient.MapProgramInfo(new FocasResult<WireProgramName>(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);
}
}