using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Mqtt; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// lmxopcua#754: a Sparkplug tag's array shape is read by THREE parties — the address-space composer /// (, which decides ValueRank/ArrayDimensions), the driver /// (, which decides whether an array metric feeds /// the tag) and the authoring gate ( / , /// which consult the driver's own parse). The driver's copy of the read lives in a Contracts assembly that cannot /// reference Commons, so this pins it against the composer's over a corpus of edge shapes, and pins that /// the array keys never change what authoring accepts. /// [Trait("Category", "Unit")] public sealed class MqttArrayShapeParityTests { private const string Tuple = """ "groupId":"Plant1","edgeNodeId":"EdgeA","deviceId":"Filler1","metricName":"Profile" """; private static string B(string extraKeys) => "{" + Tuple + extraKeys + "}"; public static TheoryData Corpus() => new() { B(""), B(""","isArray":true"""), B(""","isArray":false"""), B("""" ,"isArray":"true" """"), // a string is NOT an array to the composer B(""","isArray":1"""), B(""","isArray":true,"arrayLength":8"""), B(""","isArray":true,"arrayLength":0"""), B(""","isArray":true,"arrayLength":-1"""), B(""","isArray":true,"arrayLength":4294967296"""), B(""","isArray":true,"arrayLength":2.5"""), B("""" ,"isArray":true,"arrayLength":"8" """"), B(""","arrayLength":8"""), // a length without the bit is ignored B(""","isArray":true,"dataType":"Float64","arrayLength":3"""), }; /// The driver reads the shape exactly as the composer materialises the node. [Theory] [MemberData(nameof(Corpus))] public void The_driver_reads_the_array_shape_exactly_as_the_composer_does(string blob) { var intent = TagConfigIntent.Parse(blob); MqttTagDefinitionFactory.FromSparkplugTagConfig(blob, "Plant/Mqtt/Profile", out var def).ShouldBeTrue(blob); def.IsArray.ShouldBe(intent.IsArray); def.ArrayLength.ShouldBe(intent.ArrayLength); } /// /// The array keys are never a rejection cause: the node exists whatever they say, so a driver that /// refused the tag would serve BadNodeIdUnknown on a live node. Authoring therefore accepts every one /// of these, because the driver does — the model and the deploy-time inspector both consult its parse. /// [Theory] [MemberData(nameof(Corpus))] public void Authoring_accepts_exactly_what_the_driver_serves(string blob) { MqttTagDefinitionFactory.FromSparkplugTagConfig(blob, "Plant/Mqtt/Profile", out _).ShouldBeTrue(); MqttTagConfigModel.FromJson(blob).Validate().ShouldBeNull(); EquipmentTagConfigInspector.Inspect(DriverTypeNames.Mqtt, blob).ShouldBeEmpty(); } }