26b512adba
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).
93 lines
3.8 KiB
C#
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);
|
|
}
|
|
}
|