merge: G-7 closed — Contracts-side TagConfig Inspect for Sql/MTConnect/Calculation/OpcUaClient wired into the deploy gate
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Deferment-register G-7 pin: the AdminUI's authoring-time <see cref="TagConfigValidator"/> and the
|
||||
/// deploy-time <see cref="EquipmentTagConfigInspector"/> must agree in the direction that matters —
|
||||
/// <b>anything the modal lets an operator save must sail through the deploy gate.</b>
|
||||
/// <para>
|
||||
/// The reverse direction is deliberately NOT asserted, and must not be: the deploy gate follows the
|
||||
/// <i>runtime</i> parser, which is lenient in places the editor is opinionated (a calc tag with no
|
||||
/// trigger, a sub-50 ms timer). Config authored by CSV import or API meets only the deploy gate, so
|
||||
/// a gate stricter than the runtime would block deployments the driver would have served — and the
|
||||
/// gate's warnings can be configured (<c>TagConfigValidationMode.Error</c>) into blocking errors.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The configs are produced by the models' own <c>ToJson()</c></b> rather than hand-written JSON,
|
||||
/// which is the only way this proves anything about the real authoring path: a hand-written blob is
|
||||
/// a guess at what the editor writes, and the editors normalise keys on save (MTConnect collapses
|
||||
/// <c>dataItemId</c>/<c>address</c> onto <c>fullName</c>; Sql rewrites its enums as names). This
|
||||
/// lives in AdminUI.Tests because it is the only suite that can see both sides — the models are
|
||||
/// internal to the AdminUI project, which references ControlPlane.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class TagConfigAuthoringDeployParityTests
|
||||
{
|
||||
/// <summary>A Sql tag as the typed editor saves it: both the editor and the gate accept it.</summary>
|
||||
[Fact]
|
||||
public void Sql_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new SqlTagConfigModel
|
||||
{
|
||||
Model = ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.SqlTagModel.KeyValue,
|
||||
Table = "dbo.TagValues",
|
||||
KeyColumn = "TagName",
|
||||
KeyValue = "Line1.Speed",
|
||||
ValueColumn = "Value",
|
||||
Type = DriverDataType.Float32,
|
||||
};
|
||||
AssertParity(DriverTypeNames.Sql, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <summary>The WideRow shape too — its row selector is the half of the parser a KeyValue tag never
|
||||
/// exercises, and the editor and the parser have to agree that a where-pair needs BOTH keys.</summary>
|
||||
[Fact]
|
||||
public void Sql_wideRow_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new SqlTagConfigModel
|
||||
{
|
||||
Model = ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.SqlTagModel.WideRow,
|
||||
Table = "dbo.LineSnapshot",
|
||||
ColumnName = "Speed",
|
||||
RowSelector = new SqlRowSelectorModel { WhereColumn = "LineId", WhereValue = "L1" },
|
||||
};
|
||||
AssertParity(DriverTypeNames.Sql, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MTConnect_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new MTConnectTagConfigModel
|
||||
{
|
||||
FullName = "Xact",
|
||||
DataType = DriverDataType.Float64,
|
||||
MtCategory = "SAMPLE",
|
||||
MtType = "POSITION",
|
||||
Units = "MILLIMETER",
|
||||
};
|
||||
AssertParity(DriverTypeNames.MTConnect, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new CalculationTagConfigModel
|
||||
{
|
||||
ScriptId = "8f1c2f6e-0000-4000-8000-000000000001",
|
||||
ChangeTriggered = true,
|
||||
TimerIntervalMs = 5000,
|
||||
};
|
||||
AssertParity(DriverTypeNames.Calculation, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <summary>Change-trigger only — the editor omits <c>timerIntervalMs</c> entirely rather than writing a
|
||||
/// null or a zero, and an absent key must not read as a discarded value at the gate.</summary>
|
||||
[Fact]
|
||||
public void Calculation_change_triggered_only_config_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new CalculationTagConfigModel { ScriptId = "S1", ChangeTriggered = true };
|
||||
AssertParity(DriverTypeNames.Calculation, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpcUaClient_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new OpcUaClientTagConfigModel { NodeId = "nsu=urn:plc:sim;s=Line1.Speed" };
|
||||
AssertParity(DriverTypeNames.OpcUaClient, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Falsifiability control. Every assertion above would also pass against a build where the four G-7
|
||||
/// inspectors were never wired — an unmapped driver type returns no warnings for anything. This one
|
||||
/// proves the gate is actually looking: the same driver types must warn about a blob the editor
|
||||
/// itself would refuse.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(DriverTypeNames.Sql, """{"model":"KeyValu","table":"dbo.T"}""")]
|
||||
// Note the MTConnect case is the DIGITS-only dataType, not a spelling typo: MTConnectTagConfigModel
|
||||
// reads dataType leniently (an unrecognised NAME silently loads as String, and a save rewrites it), so
|
||||
// "Strng" is accepted at authoring while the strict runtime rejects it — a pre-existing
|
||||
// authoring-weaker-than-runtime gap in the editor, untouched here and out of G-7's scope. It does not
|
||||
// affect the parity direction this file pins, because a blob saved through the modal always carries a
|
||||
// valid NAME.
|
||||
[InlineData(DriverTypeNames.MTConnect, """{"fullName":"avail","dataType":"8"}""")]
|
||||
[InlineData(DriverTypeNames.Calculation, """{"changeTriggered":true}""")]
|
||||
[InlineData(DriverTypeNames.OpcUaClient, """{"nodeId":""}""")]
|
||||
public void The_same_driver_types_do_warn_about_a_config_the_editor_refuses(string driverType, string tagConfig)
|
||||
{
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldNotBeNullOrEmpty(
|
||||
$"{driverType}: the authoring gate is expected to refuse this blob — if it accepts it, the "
|
||||
+ "control below is testing the wrong thing.");
|
||||
EquipmentTagConfigInspector.Inspect(driverType, tagConfig).ShouldNotBeEmpty(
|
||||
$"{driverType}: the deploy gate accepted a blob the modal refuses, so config arriving by CSV "
|
||||
+ "import or API is unchecked — the exact G-7 asymmetry.");
|
||||
}
|
||||
|
||||
/// <summary>Asserts the editor accepted the config (so the parity claim is not vacuous) and that the
|
||||
/// deploy gate then found nothing to say about it.</summary>
|
||||
/// <param name="driverType">The canonical driver-type string both gates dispatch on.</param>
|
||||
/// <param name="validationError">What the model's own <c>Validate()</c> returned.</param>
|
||||
/// <param name="tagConfig">The config the model serialised.</param>
|
||||
private static void AssertParity(string driverType, string? validationError, string tagConfig)
|
||||
{
|
||||
validationError.ShouldBeNull(
|
||||
$"{driverType}: the model must accept this config, or the parity assertion below proves nothing.");
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldBeNull(
|
||||
$"{driverType}: the dispatched authoring validator must accept what the model itself accepts.");
|
||||
EquipmentTagConfigInspector.Inspect(driverType, tagConfig).ShouldBeEmpty(
|
||||
$"{driverType}: the deploy gate must never be stricter than the authoring gate — a config the "
|
||||
+ "operator was allowed to save cannot draw a deploy warning.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user