merge: SQL contracts review findings resolved (Driver.Sql.Contracts-001 scalar keyValue, -002 fleet-wide numeric-enum rejection)
This commit is contained in:
@@ -184,6 +184,32 @@ public sealed class SqlTagConfigModelTests
|
||||
=> SqlTagConfigModel.FromJson("""{"model":"KeyValue","table":"t","keyColumn":"k","valueColumn":"c"}""")
|
||||
.Validate().ShouldNotBeNull();
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Sql.Contracts-001. The parser reads <c>keyValue</c> with the same scalar read it uses for
|
||||
/// <c>whereValue</c>, so a numerically-authored key is SERVED — the editor must load it (rather
|
||||
/// than seeing an absent key), let it save, and normalise it to a string on the way out, exactly as
|
||||
/// it already does for <c>whereValue</c>. An editor that refused it would be stricter than the
|
||||
/// runtime, which is the drift this parity suite exists to catch.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_numerically_authored_keyValue_loads_validates_and_normalises_to_a_string()
|
||||
{
|
||||
const string json =
|
||||
"""{"model":"KeyValue","table":"t","keyColumn":"station_id","keyValue":7,"valueColumn":"c"}""";
|
||||
SqlEquipmentTagParser.TryParse(json, "raw", out var served).ShouldBeTrue(
|
||||
"the runtime must serve this blob, or the authoring parity below proves nothing.");
|
||||
served.KeyValue.ShouldBe("7");
|
||||
|
||||
var m = SqlTagConfigModel.FromJson(json);
|
||||
m.KeyValue.ShouldBe("7");
|
||||
m.Validate().ShouldBeNull();
|
||||
|
||||
var saved = m.ToJson();
|
||||
saved.ShouldContain("\"keyValue\":\"7\"");
|
||||
SqlEquipmentTagParser.TryParse(saved, "raw", out var afterSave).ShouldBeTrue();
|
||||
afterSave.KeyValue.ShouldBe("7");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_wideRow_without_a_selector()
|
||||
=> SqlTagConfigModel.FromJson("""{"model":"WideRow","table":"t","columnName":"c"}""")
|
||||
|
||||
+31
@@ -220,6 +220,37 @@ public sealed class TagConfigAuthoringDeployParityTests
|
||||
+ "import or API is unchecked — the exact G-7 asymmetry.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Sql.Contracts-002, fleet-wide. A numerically-SPELLED enum (<c>"1"</c>, not the JSON
|
||||
/// number <c>1</c>) used to satisfy <c>Enum.TryParse</c> at every driver, so both doors accepted it
|
||||
/// and the tag was served under whatever member sat at that ordinal — or, out of range, under a
|
||||
/// value no member declares. The shared <c>TagConfigJson.ClassifyEnumText</c> now calls it what it
|
||||
/// is, and because BOTH gates ask that one method, they moved together: the fix is only real if
|
||||
/// each driver refuses it at the modal AND at the deploy gate.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
// Complete but for the enum spelling, so the refusal can only come from the enum read: "1" is
|
||||
// SqlTagModel.WideRow's ordinal, and every WideRow-required field is present.
|
||||
[InlineData(DriverTypeNames.Sql, """{"model":"1","table":"dbo.T","columnName":"c","rowSelector":{"topByTimestamp":"ts"}}""")]
|
||||
[InlineData(DriverTypeNames.Sql, """{"model":"KeyValue","table":"dbo.T","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"8"}""")]
|
||||
[InlineData(DriverTypeNames.Modbus, """{"region":"1","address":1}""")]
|
||||
[InlineData(DriverTypeNames.Modbus, """{"region":"HoldingRegisters","address":1,"dataType":"99"}""")]
|
||||
[InlineData(DriverTypeNames.S7, """{"address":"DB1.DBW0","dataType":"1"}""")]
|
||||
[InlineData(DriverTypeNames.AbCip, """{"tagPath":"Program:Main.Speed","dataType":"1"}""")]
|
||||
[InlineData(DriverTypeNames.AbLegacy, """{"address":"N7:0","dataType":"1"}""")]
|
||||
[InlineData(DriverTypeNames.TwinCAT, """{"symbolPath":"MAIN.bStart","dataType":"1"}""")]
|
||||
[InlineData(DriverTypeNames.FOCAS, """{"address":"R100","dataType":"1"}""")]
|
||||
[InlineData(DriverTypeNames.Mqtt, """{"topic":"otopcua/fixture/a","payloadFormat":"1"}""")]
|
||||
[InlineData(DriverTypeNames.Mqtt, """{"topic":"otopcua/fixture/a","dataType":"8"}""")]
|
||||
public void A_numerically_spelled_enum_is_refused_by_both_gates(string driverType, string tagConfig)
|
||||
{
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldNotBeNullOrEmpty(
|
||||
$"{driverType}: the modal would let an operator save an enum spelling the driver refuses.");
|
||||
EquipmentTagConfigInspector.Inspect(driverType, tagConfig).ShouldNotBeEmpty(
|
||||
$"{driverType}: the deploy gate accepted a numeric enum spelling, so a CSV-imported or "
|
||||
+ "API-authored tag binds to a type nobody named.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Gitea #550 — MTConnect's second spelling of the coercion type. The assertions below drive the REAL
|
||||
// runtime reader (MTConnectTagDefinitionFactory.TryFromTagConfig) rather than the deploy gate alone,
|
||||
|
||||
Reference in New Issue
Block a user