feat(twincat): resolve equipment-tag refs (read + write) via EquipmentTagRefResolver

This commit is contained in:
Joseph Doherty
2026-06-13 11:24:12 -04:00
parent e2ea720c08
commit 5ebf541f54
3 changed files with 144 additions and 3 deletions
@@ -0,0 +1,81 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
[Trait("Category", "Unit")]
public class TwinCATEquipmentTagTests
{
[Fact]
public void Parses_equipment_tagconfig_into_a_transient_definition()
{
var json = """{"deviceHostAddress":"ads://5.23.91.23.1.1:851","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
TwinCATEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
def!.Name.ShouldBe(json);
def.SymbolPath.ShouldBe("MAIN.Speed");
def.DeviceHostAddress.ShouldBe("ads://5.23.91.23.1.1:851");
def.DataType.ShouldBe(TwinCATDataType.DInt);
def.Writable.ShouldBeTrue();
}
[Fact]
public void Defaults_optional_fields_when_only_symbol_path_is_present()
{
TwinCATEquipmentTagParser.TryParse("""{"symbolPath":"GVL.X"}""", out var def).ShouldBeTrue();
def!.SymbolPath.ShouldBe("GVL.X");
def.DeviceHostAddress.ShouldBe(""); // absent host → empty
def.DataType.ShouldBe(TwinCATDataType.DInt); // enum default
}
[Fact]
public void Rejects_a_non_address_blob()
=> TwinCATEquipmentTagParser.TryParse("""{"FullName":"x"}""", out _).ShouldBeFalse();
[Fact]
public void Rejects_garbage()
=> TwinCATEquipmentTagParser.TryParse("not json", out _).ShouldBeFalse();
[Fact]
public void Rejects_blank_reference()
=> TwinCATEquipmentTagParser.TryParse(" ", out _).ShouldBeFalse();
[Fact]
public void Rejects_symbol_path_as_a_non_string()
=> TwinCATEquipmentTagParser.TryParse("""{"symbolPath":42}""", out _).ShouldBeFalse();
[Fact]
public void Ignores_a_non_string_data_type_and_falls_back_to_default()
{
// dataType as a number is not a String enum — the guard ignores it and keeps the default.
TwinCATEquipmentTagParser.TryParse("""{"symbolPath":"MAIN.X","dataType":3}""", out var def).ShouldBeTrue();
def!.DataType.ShouldBe(TwinCATDataType.DInt);
}
/// <summary>
/// End-to-end driver-level proof: a TwinCAT driver with NO authored tags can still read an
/// equipment-tag ref (the raw TagConfig JSON) — the resolver parses it into a transient
/// definition (with a deviceHostAddress matching a configured device) and the read reaches
/// the fake client instead of returning BadNodeIdUnknown.
/// </summary>
[Fact]
public async Task Driver_resolves_an_equipment_ref_and_reads_instead_of_BadNodeIdUnknown()
{
var host = "ads://5.23.91.23.1.1:851";
var json = $$"""{"deviceHostAddress":"{{host}}","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
var factory = new FakeTwinCATClientFactory { Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 4242 } } };
var drv = new TwinCATDriver(new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(host)],
Tags = [], // no authored tags — resolution must come from the equipment-ref parser
Probe = new TwinCATProbeOptions { Enabled = false },
}, "twincat-eq", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync([json], CancellationToken.None);
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
r[0].StatusCode.ShouldNotBe(TwinCATStatusMapper.BadNodeIdUnknown);
r[0].Value.ShouldBe(4242);
}
}