fix(adminui): authoring must not accept a TagConfig enum the runtime rejects (#549)
MTConnectTagConfigModel read `dataType` with TagConfigJson.GetEnum, which falls
back to String on ANY unrecognised value. A misspelled name ("Strng") from a
hand-authored blob, a CSV import or the API was therefore accepted at authoring
and silently rewritten, while the driver's strict parse rejected it and the tag
died with BadNodeIdUnknown. Found by the G-7 parity tests' falsifiability
control, which had to use the digits-only shape to have a case that failed.
The rule is not restated in the AdminUI. MTConnectTagDefinitionFactory exposes
its parse as `IsAcceptedDataTypeName`, documented as the one authority the
driver, the deploy gate and the authoring model all consult; the model's
Validate() asks it about the raw value the blob carries. The old digits-only
message is NOT retained as a special case: the runtime draws no distinction
between a bare number and a typo — one predicate, one Inspect warning — so a
second message here would be a second statement of a rule this model does not
own. The raw value is captured as the JSON TOKEN, because the runtime
deserialises the key into a `string?` DTO field and an unquoted number throws
there too.
The survey found the same asymmetry at six more editors, contrary to the
assumption that those runtimes silently default: R2-11 Phase C moved every
driver's TagConfig enum read to TagConfigJson.TryReadEnumStrict, which REJECTS
the tag on a present-but-invalid string, while the editors kept loading them
with a silent fallback. Modbus (region/dataType/byteOrder), S7, AbCip, AbLegacy,
TwinCAT and FOCAS now check their enums in Validate() through the runtime's own
TagConfigJson.DescribeInvalidEnumText — added alongside a public
ClassifyEnumText that TryReadEnum now delegates to, so the JSON-token door and
the string door reach one implementation. Mqtt and Sql already carried
equivalent strict checks and are untouched.
Care taken NOT to become stricter than the runtime: a present-but-non-string
token stays accepted for those six (TryReadEnum calls it absent), as does an
absent key, each pinned by a test.
Also adds a direct ProjectReference to Core.Abstractions from the AdminUI — the
save gate now leans on a type it was consuming only transitively.
This commit is contained in:
@@ -99,4 +99,25 @@ public sealed class AbCipTagConfigModelTests
|
||||
|
||||
json.ShouldContain("\"deviceHostAddress\":\"ab://host\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: the LOAD is lenient — an unrecognised name silently becomes <c>DInt</c> — but
|
||||
/// <c>AbCipTagDefinitionFactory</c> reads <c>dataType</c> with <c>TryReadEnumStrict</c> and rejects
|
||||
/// the whole tag, so the save gate must judge the value the blob actually carries rather than the
|
||||
/// one the load substituted.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_refuses_a_dataType_the_driver_would_reject()
|
||||
=> AbCipTagConfigModel.FromJson("""{"tagPath":"Motor1.Status","dataType":"DIntt"}""")
|
||||
.Validate().ShouldNotBeNull().ShouldContain("DIntt");
|
||||
|
||||
/// <summary>The other direction, which matters just as much: the check is the runtime's rule and not a
|
||||
/// stricter one — a valid name in any casing, an absent key, and a present-but-NON-STRING token (which
|
||||
/// <c>TryReadEnum</c> treats as absent) are all served by the driver and so must all save.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"tagPath":"Motor1.Status","dataType":"real"}""")]
|
||||
[InlineData("""{"tagPath":"Motor1.Status"}""")]
|
||||
[InlineData("""{"tagPath":"Motor1.Status","dataType":2}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> AbCipTagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -129,4 +129,25 @@ public sealed class AbLegacyTagConfigModelTests
|
||||
|
||||
m.Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: the LOAD is lenient — an unrecognised name silently becomes <c>Int</c> — but
|
||||
/// <c>AbLegacyTagDefinitionFactory</c> reads <c>dataType</c> with <c>TryReadEnumStrict</c> and
|
||||
/// rejects the whole tag, so the save gate must judge the value the blob actually carries rather
|
||||
/// than the one the load substituted.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_refuses_a_dataType_the_driver_would_reject()
|
||||
=> AbLegacyTagConfigModel.FromJson("""{"address":"N7:0","dataType":"Intt"}""")
|
||||
.Validate().ShouldNotBeNull().ShouldContain("Intt");
|
||||
|
||||
/// <summary>The other direction, which matters just as much: the check is the runtime's rule and not a
|
||||
/// stricter one — a valid name in any casing, an absent key, and a present-but-NON-STRING token (which
|
||||
/// <c>TryReadEnum</c> treats as absent) are all served by the driver and so must all save.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"address":"N7:0","dataType":"float"}""")]
|
||||
[InlineData("""{"address":"N7:0"}""")]
|
||||
[InlineData("""{"address":"N7:0","dataType":2}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> AbLegacyTagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -125,4 +125,25 @@ public sealed class FocasTagConfigModelTests
|
||||
|
||||
json.ShouldContain("\"deviceHostAddress\":\"ab://host\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: the LOAD is lenient — an unrecognised name silently becomes <c>Int32</c> — but
|
||||
/// <c>FocasTagDefinitionFactory</c> reads <c>dataType</c> with <c>TryReadEnumStrict</c> and rejects
|
||||
/// the whole tag, so the save gate must judge the value the blob actually carries. Same shape as
|
||||
/// the #540 address gap this model already closes, one field over.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_refuses_a_dataType_the_driver_would_reject()
|
||||
=> FocasTagConfigModel.FromJson("""{"address":"R100","dataType":"Int322"}""")
|
||||
.Validate().ShouldNotBeNull().ShouldContain("Int322");
|
||||
|
||||
/// <summary>The other direction, which matters just as much: the check is the runtime's rule and not a
|
||||
/// stricter one — a valid name in any casing, an absent key, and a present-but-NON-STRING token (which
|
||||
/// <c>TryReadEnum</c> treats as absent) are all served by the driver and so must all save.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"address":"R100","dataType":"float32"}""")]
|
||||
[InlineData("""{"address":"R100"}""")]
|
||||
[InlineData("""{"address":"R100","dataType":2}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> FocasTagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -176,4 +176,46 @@ public sealed class MTConnectTagConfigModelTests
|
||||
m.DataType.ShouldBe(DriverDataType.Float64); // 8 == DriverDataType.Float64 — parsed "successfully"
|
||||
m.Validate().ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: a MISSPELLED type name is the same defect the bare number was, and it was the more
|
||||
/// likely one — <c>TagConfigJson.GetEnum</c> falls back to <c>String</c> on any unrecognised value,
|
||||
/// so the blob was accepted here, silently rewritten to <c>"String"</c> on the next save, and
|
||||
/// rejected outright by the driver's strict parse in between. The message must name the value the
|
||||
/// operator actually wrote, not the <c>String</c> it was coerced to.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_rejects_a_misspelled_dataType_name_and_names_the_offending_value()
|
||||
{
|
||||
var m = MTConnectTagConfigModel.FromJson("""{"fullName":"dev1_pos","dataType":"Strng"}""");
|
||||
|
||||
m.DataType.ShouldBe(DriverDataType.String); // the lenient load's coercion — the trap itself
|
||||
m.Validate().ShouldNotBeNull().ShouldContain("Strng");
|
||||
}
|
||||
|
||||
/// <summary>The rejection is the RUNTIME's rule, so it must reject exactly what the runtime rejects and
|
||||
/// no more: a valid name in any casing is accepted, and so is an absent, blank or JSON-null type (the
|
||||
/// driver then falls back to the Agent's /probe declaration rather than rejecting the tag).</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"fullName":"dev1_pos","dataType":"float64"}""")]
|
||||
[InlineData("""{"fullName":"dev1_pos","dataType":"FLOAT64"}""")]
|
||||
[InlineData("""{"fullName":"dev1_pos","dataType":" Float64 "}""")]
|
||||
[InlineData("""{"fullName":"dev1_pos"}""")]
|
||||
[InlineData("""{"fullName":"dev1_pos","dataType":""}""")]
|
||||
[InlineData("""{"fullName":"dev1_pos","dataType":null}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> MTConnectTagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
|
||||
/// <summary>
|
||||
/// A NON-string <c>dataType</c> token is rejected too, and for a reason the string cases do not
|
||||
/// cover: the runtime deserialises this key into a <c>string?</c> DTO field, so an unquoted number
|
||||
/// throws <c>JsonException</c> there and takes the whole tag down. Reading the value with
|
||||
/// <c>GetString</c> (null for a non-string token) would have called that "absent" and accepted it —
|
||||
/// which is why the model captures the raw TOKEN. The companion assertion in ControlPlane.Tests'
|
||||
/// <c>EquipmentTagConfigInspectorTests</c> proves the runtime really does reject it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_rejects_a_dataType_written_as_an_unquoted_number()
|
||||
=> MTConnectTagConfigModel.FromJson("""{"fullName":"dev1_pos","dataType":8}""")
|
||||
.Validate().ShouldNotBeNull().ShouldContain("8");
|
||||
}
|
||||
|
||||
@@ -89,4 +89,29 @@ public sealed class ModbusTagConfigModelTests
|
||||
{
|
||||
new ModbusTagConfigModel().Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: the LOAD is lenient — an unrecognised name silently becomes the default — but
|
||||
/// <c>ModbusTagDefinitionFactory</c> reads all THREE enums with <c>TryReadEnumStrict</c> and
|
||||
/// rejects the whole tag on any one of them, so the save gate must judge the values the blob
|
||||
/// actually carries. Each key is asserted separately: a check that covered only <c>dataType</c>
|
||||
/// would leave a typo'd region or byte order accepted here and dead at the driver.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("""{"region":"HoldingRegister","address":1}""", "HoldingRegister")]
|
||||
[InlineData("""{"region":"HoldingRegisters","address":1,"dataType":"Intt16"}""", "Intt16")]
|
||||
[InlineData("""{"region":"HoldingRegisters","address":1,"byteOrder":"BigEndain"}""", "BigEndain")]
|
||||
public void Validate_refuses_an_enum_the_driver_would_reject(string json, string offending)
|
||||
=> ModbusTagConfigModel.FromJson(json).Validate().ShouldNotBeNull().ShouldContain(offending);
|
||||
|
||||
/// <summary>The other direction, which matters just as much: the check is the runtime's rule and not a
|
||||
/// stricter one. Valid names in any casing are accepted, an absent key is accepted (the runtime
|
||||
/// defaults it), and so is a present-but-NON-STRING token — <c>TryReadEnum</c> calls that "absent" and
|
||||
/// defaults it too, so refusing it here would block a blob the driver serves.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"region":"holdingregisters","address":1,"dataType":"float32","byteOrder":"wordswap"}""")]
|
||||
[InlineData("""{"address":1}""")]
|
||||
[InlineData("""{"region":3,"address":1,"dataType":true}""")]
|
||||
public void Validate_accepts_every_enum_value_the_driver_accepts(string json)
|
||||
=> ModbusTagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -79,4 +79,28 @@ public sealed class S7TagConfigModelTests
|
||||
{
|
||||
new S7TagConfigModel { Address = "DB1.DBW0" }.Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: the LOAD is lenient — an unrecognised name silently becomes <c>Int16</c> — but
|
||||
/// <c>S7TagDefinitionFactory</c> reads <c>dataType</c> with <c>TryReadEnumStrict</c> and rejects the
|
||||
/// whole tag, so the save gate must judge the value the blob actually carries. The blob reaches
|
||||
/// deployment unrewritten on the API path, and the operator would see a tag that reads
|
||||
/// <c>BadNodeIdUnknown</c> with nothing to explain it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_refuses_a_dataType_the_driver_would_reject()
|
||||
=> S7TagConfigModel.FromJson("""{"address":"DB1.DBW0","dataType":"Int166"}""")
|
||||
.Validate().ShouldNotBeNull().ShouldContain("Int166");
|
||||
|
||||
/// <summary>The other direction, which matters just as much: the check is the runtime's rule and not a
|
||||
/// stricter one. A valid name in any casing is accepted, and so is a present-but-NON-STRING token —
|
||||
/// <c>TryReadEnum</c> calls that "absent" and defaults it, so refusing it here would block a blob the
|
||||
/// driver serves. (An absent key is deliberately not asserted: S7 alone hard-requires the key, an
|
||||
/// asymmetry that predates this change and that its own <c>Inspect</c> does not report either.)</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":"float32"}""")]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":"FLOAT32"}""")]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":7}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> S7TagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
+72
-6
@@ -73,6 +73,60 @@ public sealed class TagConfigAuthoringDeployParityTests
|
||||
AssertParity(DriverTypeNames.MTConnect, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The six editors whose <c>Validate()</c> gained a strict enum check with Gitea #549. Each is
|
||||
/// built with its DEFAULT enum values on purpose: the check reads the loaded key bag, so a model
|
||||
/// that was never loaded from JSON has nothing to check, and the assertion that matters here is
|
||||
/// that the enum names the editor itself writes are ones the runtime parser reads back — the
|
||||
/// over-rejection direction the new check must not fall into.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Modbus_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new ModbusTagConfigModel { Address = 1 };
|
||||
AssertParity(DriverTypeNames.Modbus, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Modbus_config_the_editor_accepts_draws_no_deploy_warning"/>
|
||||
[Fact]
|
||||
public void S7_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new S7TagConfigModel { Address = "DB1.DBW0" };
|
||||
AssertParity(DriverTypeNames.S7, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Modbus_config_the_editor_accepts_draws_no_deploy_warning"/>
|
||||
[Fact]
|
||||
public void AbCip_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new AbCipTagConfigModel { TagPath = "Program:Main.Speed" };
|
||||
AssertParity(DriverTypeNames.AbCip, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Modbus_config_the_editor_accepts_draws_no_deploy_warning"/>
|
||||
[Fact]
|
||||
public void AbLegacy_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new AbLegacyTagConfigModel { Address = "N7:0" };
|
||||
AssertParity(DriverTypeNames.AbLegacy, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Modbus_config_the_editor_accepts_draws_no_deploy_warning"/>
|
||||
[Fact]
|
||||
public void TwinCAT_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new TwinCATTagConfigModel { SymbolPath = "MAIN.bStart" };
|
||||
AssertParity(DriverTypeNames.TwinCAT, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Modbus_config_the_editor_accepts_draws_no_deploy_warning"/>
|
||||
[Fact]
|
||||
public void Focas_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
var model = new FocasTagConfigModel { Address = "R100" };
|
||||
AssertParity(DriverTypeNames.FOCAS, model.Validate(), model.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_config_the_editor_accepts_draws_no_deploy_warning()
|
||||
{
|
||||
@@ -109,15 +163,27 @@ public sealed class TagConfigAuthoringDeployParityTests
|
||||
/// </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.
|
||||
// Both MTConnect shapes of an unreadable type, and they used to disagree: the digits-only "8" was
|
||||
// refused at authoring, while a misspelled NAME was silently loaded as String and rewritten on save —
|
||||
// accepted here, rejected by the driver, which is Gitea #549. MTConnectTagConfigModel.Validate now
|
||||
// asks MTConnectTagDefinitionFactory.IsAcceptedDataTypeName, the driver's own predicate, so the two
|
||||
// shapes are one rule and neither gate can drift from the runtime.
|
||||
[InlineData(DriverTypeNames.MTConnect, """{"fullName":"avail","dataType":"8"}""")]
|
||||
[InlineData(DriverTypeNames.MTConnect, """{"fullName":"avail","dataType":"Strng"}""")]
|
||||
[InlineData(DriverTypeNames.Calculation, """{"changeTriggered":true}""")]
|
||||
[InlineData(DriverTypeNames.OpcUaClient, """{"nodeId":""}""")]
|
||||
// #549 survey: the same lenient-load / strict-runtime asymmetry existed at every other typed editor
|
||||
// that reads an enum key, because R2-11 Phase C made every driver's TagConfig enum read strict
|
||||
// (TryReadEnumStrict REJECTS the tag on a present-but-invalid string) while the editors kept loading
|
||||
// them with a silent fallback. Each model's Validate() now consults the runtime's own
|
||||
// TagConfigJson.DescribeInvalidEnumText, so a typo is refused by BOTH gates.
|
||||
[InlineData(DriverTypeNames.Modbus, """{"region":"HoldingRegisters","address":1,"dataType":"Intt16"}""")]
|
||||
[InlineData(DriverTypeNames.Modbus, """{"region":"HoldingRegister","address":1}""")]
|
||||
[InlineData(DriverTypeNames.S7, """{"address":"DB1.DBW0","dataType":"Int166"}""")]
|
||||
[InlineData(DriverTypeNames.AbCip, """{"tagPath":"Program:Main.Speed","dataType":"DIntt"}""")]
|
||||
[InlineData(DriverTypeNames.AbLegacy, """{"address":"N7:0","dataType":"Intt"}""")]
|
||||
[InlineData(DriverTypeNames.TwinCAT, """{"symbolPath":"MAIN.bStart","dataType":"DIntt"}""")]
|
||||
[InlineData(DriverTypeNames.FOCAS, """{"address":"R100","dataType":"Int322"}""")]
|
||||
public void The_same_driver_types_do_warn_about_a_config_the_editor_refuses(string driverType, string tagConfig)
|
||||
{
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldNotBeNullOrEmpty(
|
||||
|
||||
@@ -99,4 +99,25 @@ public sealed class TwinCATTagConfigModelTests
|
||||
|
||||
json.ShouldContain("\"deviceHostAddress\":\"ab://host\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549: the LOAD is lenient — an unrecognised name silently becomes <c>DInt</c> — but
|
||||
/// <c>TwinCATTagDefinitionFactory</c> reads <c>dataType</c> with <c>TryReadEnumStrict</c> and
|
||||
/// rejects the whole tag, so the save gate must judge the value the blob actually carries rather
|
||||
/// than the one the load substituted.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_refuses_a_dataType_the_driver_would_reject()
|
||||
=> TwinCATTagConfigModel.FromJson("""{"symbolPath":"MAIN.bStart","dataType":"DIntt"}""")
|
||||
.Validate().ShouldNotBeNull().ShouldContain("DIntt");
|
||||
|
||||
/// <summary>The other direction, which matters just as much: the check is the runtime's rule and not a
|
||||
/// stricter one — a valid name in any casing, an absent key, and a present-but-NON-STRING token (which
|
||||
/// <c>TryReadEnum</c> treats as absent) are all served by the driver and so must all save.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"symbolPath":"MAIN.bStart","dataType":"real"}""")]
|
||||
[InlineData("""{"symbolPath":"MAIN.bStart"}""")]
|
||||
[InlineData("""{"symbolPath":"MAIN.bStart","dataType":2}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> TwinCATTagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -120,6 +120,18 @@ public sealed class EquipmentTagConfigInspectorTests
|
||||
=> EquipmentTagConfigInspector.Inspect("MTConnect", """{"fullName":"avail","dataType":"8"}""")
|
||||
.ShouldContain(w => w.Contains("dataType"));
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #549 companion to <c>MTConnectTagConfigModelTests</c>: an UNQUOTED number is rejected by
|
||||
/// the runtime too, and by a different mechanism than the quoted one — the blob DTO's type field is
|
||||
/// a <c>string?</c>, so the deserialize itself throws and the whole tag goes. The authoring model
|
||||
/// therefore has to judge the raw JSON token rather than "the string value, or null", or it would
|
||||
/// read this as an absent type and accept a blob the driver drops.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MTConnect_unquoted_numeric_dataType_is_reported()
|
||||
=> EquipmentTagConfigInspector.Inspect("MTConnect", """{"fullName":"avail","dataType":8}""")
|
||||
.ShouldNotBeEmpty();
|
||||
|
||||
[Fact]
|
||||
public void MTConnect_config_naming_no_dataitem_is_reported()
|
||||
=> EquipmentTagConfigInspector.Inspect("MTConnect", """{"dataType":"String"}""")
|
||||
|
||||
Reference in New Issue
Block a user