merge: browse-committed MTConnect tags carry the browsed inferred type (Gitea #560 closed; Driver.MTConnect-003 Resolved)
This commit is contained in:
@@ -61,7 +61,7 @@ false premise concealed a live defect rather than merely misinforming a reader.
|
|||||||
|
|
||||||
The second is what makes this worth recording rather than a tidy-up: a reader auditing the browse
|
The second is what makes this worth recording rather than a tidy-up: a reader auditing the browse
|
||||||
path would have taken that remark as evidence the case was handled. The underlying defect is
|
path would have taken that remark as evidence the case was handled. The underlying defect is
|
||||||
recorded as `Driver.MTConnect-003` (Open, fix belongs in `RawBrowseCommitMapper`).
|
recorded as `Driver.MTConnect-003` (Resolved `64be41d8`, 2026-08-10, in `RawBrowseCommitMapper`).
|
||||||
|
|
||||||
**Recommendation:** Correct both remarks to describe the callers that exist, and — rather than
|
**Recommendation:** Correct both remarks to describe the callers that exist, and — rather than
|
||||||
deleting the precedence rule, which is still correct for hand-authored, CSV-imported and API-authored
|
deleting the precedence rule, which is still correct for hand-authored, CSV-imported and API-authored
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| Review date | 2026-08-10 |
|
| Review date | 2026-08-10 |
|
||||||
| Commit reviewed | `b990adae` |
|
| Commit reviewed | `b990adae` |
|
||||||
| Status | Complete |
|
| Status | Complete |
|
||||||
| Open findings | 5 |
|
| Open findings | 4 |
|
||||||
|
|
||||||
## Checklist coverage
|
## Checklist coverage
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ requested `/probe`, expected `/VMC-1/probe`) and the control
|
|||||||
| Severity | High |
|
| Severity | High |
|
||||||
| Category | Correctness & logic bugs |
|
| Category | Correctness & logic bugs |
|
||||||
| Location | `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/RawBrowseCommitMapper.cs:256` (cause) · `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:3286` (`BuildBinding` precedence) |
|
| Location | `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/RawBrowseCommitMapper.cs:256` (cause) · `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:3286` (`BuildBinding` precedence) |
|
||||||
| Status | Open |
|
| Status | Resolved |
|
||||||
|
|
||||||
**Description:** **Every browse-committed MTConnect tag is coerced to `String` at runtime while its
|
**Description:** **Every browse-committed MTConnect tag is coerced to `String` at runtime while its
|
||||||
OPC UA node is typed from the probe inference**, so a browsed `SAMPLE` publishes text into a
|
OPC UA node is typed from the probe inference**, so a browsed `SAMPLE` publishes text into a
|
||||||
@@ -150,9 +150,16 @@ behaviour for AbCip/TwinCAT/FOCAS, and assert the **pair** — that the tag row'
|
|||||||
blob's coercion type agree — rather than either alone: asserting only the blob would pass against a
|
blob's coercion type agree — rather than either alone: asserting only the blob would pass against a
|
||||||
mapper that had stopped setting the row's type.
|
mapper that had stopped setting the row's type.
|
||||||
|
|
||||||
**Resolution:** _(open — the fix belongs in `ZB.MOM.WW.OtOpcUa.AdminUI`, outside this review's
|
**Resolution:** Fixed `64be41d8`, 2026-08-10 (Gitea **#560**) — cross-module, in
|
||||||
modules; tracked as Gitea **#560**. The stale premises this defect invalidated inside these two
|
`ZB.MOM.WW.OtOpcUa.AdminUI`: `RawBrowseCommitMapper.MapLeaf` now threads the browsed leaf's
|
||||||
modules are corrected under `Driver.MTConnect-008` and `Driver.MTConnect.Contracts-001`.)_
|
`driverDataType` into `BuildTagConfig`, whose MTConnect branch uses it as the `NativeType` fallback,
|
||||||
|
so the committed blob's coercion type and the raw tag row's OPC UA type both come from the one probe
|
||||||
|
inference — the same forwarding the AbCip / TwinCAT / FOCAS branches already did for #536. A leaf
|
||||||
|
inferred (or reported) as `String` still commits `"dataType":"String"`, and an unusable name still
|
||||||
|
falls back to it through the shared numeric-string / unparseable guards. Pinned by
|
||||||
|
`RawBrowseCommitMapperTests` asserting the PAIR plus that `String` control; the stale premises this
|
||||||
|
defect left inside these two modules are corrected under `Driver.MTConnect-008` and
|
||||||
|
`Driver.MTConnect.Contracts-001`.
|
||||||
|
|
||||||
### Driver.MTConnect-004
|
### Driver.MTConnect-004
|
||||||
|
|
||||||
|
|||||||
@@ -194,15 +194,23 @@ into) is resolved in this order, first non-null wins:
|
|||||||
(category/type/units/representation → `DriverDataType`).
|
(category/type/units/representation → `DriverDataType`).
|
||||||
4. `DriverDataType.String` — the coercion that cannot fail.
|
4. `DriverDataType.String` — the coercion that cannot fail.
|
||||||
|
|
||||||
> ⚠️ **Step 3 does not currently rescue a browse-committed tag.** `RawBrowseCommitMapper`'s
|
> **A browse-committed tag resolves at step 1, carrying the step-3 answer.** `RawBrowseCommitMapper`'s
|
||||||
> MTConnect branch emits a whole `MTConnectTagConfigModel`, whose `ToJson()` always writes
|
> MTConnect branch emits a whole `MTConnectTagConfigModel`, whose `ToJson()` always writes `dataType`
|
||||||
> `dataType` from a field defaulting to `String` — a non-blank authored type, so step 1 wins
|
> — so a committed blob always states a non-blank type and step 1 always wins. Since Gitea **#560**
|
||||||
> and step 3 is never consulted. The raw tag's OPC UA `DataType` meanwhile *is* taken from the
|
> that stated type is the **browsed leaf's own inferred `DriverDataType`**, the very value step 3
|
||||||
> browsed leaf's inferred type (`RawBrowseCommitMapper.MapLeaf` → `MapDataType`), so a browsed
|
> would compute, forwarded by `MapLeaf` into `BuildTagConfig` exactly as the AbCip / TwinCAT / FOCAS
|
||||||
> `SAMPLE` lands as a `Double` node whose driver coerces the observation to text. Fix belongs in
|
> branches forward their browsed native type (Gitea #536). It therefore agrees by construction with
|
||||||
> the commit mapper (forward the leaf's `DriverDataType` into the model, as the AbCip / TwinCAT /
|
> the raw tag row's OPC UA `DataType`, which `MapLeaf` → `MapDataType` takes from the same inference.
|
||||||
> FOCAS branches already do). Recorded as `Driver.MTConnect-003` in
|
>
|
||||||
> [`code-reviews/Driver.MTConnect/findings.md`](../../code-reviews/Driver.MTConnect/findings.md).
|
> Before #560 the branch stamped the model's `String` default instead, so a browsed `SAMPLE` landed
|
||||||
|
> as a `Double` node whose driver coerced the observation to text — recorded as `Driver.MTConnect-003`
|
||||||
|
> in [`code-reviews/Driver.MTConnect/findings.md`](../../code-reviews/Driver.MTConnect/findings.md).
|
||||||
|
> A leaf the browser reports **no** type for still commits `String`, and a leaf legitimately inferred
|
||||||
|
> as `String` (an `EVENT` item) commits `String` too — in both cases the type the driver would have
|
||||||
|
> reached anyway.
|
||||||
|
>
|
||||||
|
> **Step 3 remains the rescue for a blob that states no type at all** — hand-authored, CSV-imported,
|
||||||
|
> or API-posted `RawTags[]` entries carrying only an id, which is the shape it was written for.
|
||||||
|
|
||||||
### Quality mapping
|
### Quality mapping
|
||||||
|
|
||||||
|
|||||||
+9
-7
@@ -37,13 +37,15 @@ public readonly record struct MTConnectInferredType(DriverDataType DataType, boo
|
|||||||
/// blob declares no type).
|
/// blob declares no type).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// ⚠️ <b>Two call sites this remark used to name do not exist.</b> The universal browser's
|
/// <b>Two further call sites this remark used to name do not exist, and should not.</b> The
|
||||||
/// commit path (<c>RawBrowseCommitMapper</c>) never calls this — it re-maps the browsed leaf's
|
/// universal browser's commit path (<c>RawBrowseCommitMapper</c>) never calls this: it
|
||||||
/// already-inferred <c>DriverDataType</c> onto the raw tag's OPC UA type but writes a
|
/// FORWARDS the browsed leaf's already-inferred <c>DriverDataType</c> — onto the raw tag's OPC
|
||||||
/// <c>TagConfig</c> whose <c>dataType</c> is the model default — and the AdminUI typed editor
|
/// UA type, and (since Gitea #560) into the committed <c>TagConfig</c>'s <c>dataType</c> — so
|
||||||
/// (<c>MTConnectTagConfigModel</c>) deliberately does not recompute the inference at all. The
|
/// both come from this rule without re-running it. Before #560 the blob carried the model's
|
||||||
/// first of those is a live defect, not merely a stale comment: see
|
/// <c>String</c> default instead and disagreed with the node beside it
|
||||||
/// <c>code-reviews/Driver.MTConnect/findings.md</c> (Driver.MTConnect-003).
|
/// (<c>Driver.MTConnect-003</c>). The AdminUI typed editor (<c>MTConnectTagConfigModel</c>)
|
||||||
|
/// deliberately does not recompute the inference at all — it re-serialises whatever the
|
||||||
|
/// operator picked or the commit stamped.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>MTConnect is weakly typed on the wire</b> — every observation is text, and the standard
|
/// <b>MTConnect is weakly typed on the wire</b> — every observation is text, and the standard
|
||||||
|
|||||||
@@ -87,10 +87,13 @@ public sealed class MTConnectDriverOptions
|
|||||||
/// <see cref="Tags"/> entry naming the same DataItem id; failing that, from the Agent's
|
/// <see cref="Tags"/> entry naming the same DataItem id; failing that, from the Agent's
|
||||||
/// own <c>/probe</c> declaration (<see cref="MTConnectDataTypeInference"/>); failing
|
/// own <c>/probe</c> declaration (<see cref="MTConnectDataTypeInference"/>); failing
|
||||||
/// that, <see cref="DriverDataType.String"/> — the coercion that cannot fail.
|
/// that, <see cref="DriverDataType.String"/> — the coercion that cannot fail.
|
||||||
/// ⚠️ The <c>/probe</c> step is <b>not</b> reached by a browse-committed tag at HEAD:
|
/// The <c>/probe</c> step is not reached by a browse-committed tag — the AdminUI's
|
||||||
/// the AdminUI's commit mapper writes a full typed blob whose <c>dataType</c> defaults
|
/// commit mapper writes a full typed blob, so the first step always answers — but
|
||||||
/// to <c>String</c>, which the first step then honours. See
|
/// since Gitea #560 the type it writes IS the browsed leaf's inference, so the answer
|
||||||
/// <c>code-reviews/Driver.MTConnect/findings.md</c> (Driver.MTConnect-003).</item>
|
/// is the same one. It previously wrote the model's <c>String</c> default and silently
|
||||||
|
/// mistyped every browsed tag (<c>code-reviews/Driver.MTConnect/findings.md</c>,
|
||||||
|
/// Driver.MTConnect-003). The step still serves a hand-authored / CSV / API blob that
|
||||||
|
/// states no type.</item>
|
||||||
/// <item>A tag in <see cref="Tags"/> only is reachable by its DataItem <c>id</c>
|
/// <item>A tag in <see cref="Tags"/> only is reachable by its DataItem <c>id</c>
|
||||||
/// (the driver CLI's authoring surface, and every pre-v3 test) and still contributes
|
/// (the driver CLI's authoring surface, and every pre-v3 test) and still contributes
|
||||||
/// its coercion type to the index.</item>
|
/// its coercion type to the index.</item>
|
||||||
|
|||||||
@@ -3227,19 +3227,18 @@ public sealed class MTConnectDriver
|
|||||||
/// data item, else <see cref="DriverDataType.String"/>.
|
/// data item, else <see cref="DriverDataType.String"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// ⚠️ <b>The third step does NOT currently rescue a browse-committed tag, and the
|
/// <b>A browse-committed tag never reaches the third step, and no longer needs to.</b>
|
||||||
/// AdminUI is where that has to be fixed.</b> It was written on the premise that
|
/// The MTConnect branch of <c>RawBrowseCommitMapper.BuildTagConfig</c> emits a whole
|
||||||
/// <c>RawBrowseCommitMapper</c> writes only the address into the blob — true when this
|
|
||||||
/// was authored, false at HEAD: the MTConnect branch of
|
|
||||||
/// <c>RawBrowseCommitMapper.BuildTagConfig</c> now emits a whole
|
|
||||||
/// <c>MTConnectTagConfigModel</c>, whose <c>ToJson</c> unconditionally writes
|
/// <c>MTConnectTagConfigModel</c>, whose <c>ToJson</c> unconditionally writes
|
||||||
/// <c>"dataType"</c> from a field defaulting to <see cref="DriverDataType.String"/>. That
|
/// <c>"dataType"</c> — always a non-blank authored type, so step 1 always wins. Since
|
||||||
/// is a non-blank authored type, so step 1 wins and this step is never reached — every
|
/// Gitea #560 the value it writes is the browsed leaf's own inferred
|
||||||
/// browse-committed tag coerces to text while its raw-tag node was typed <c>Double</c> /
|
/// <see cref="DriverDataType"/>, i.e. what this step would have computed, so it agrees
|
||||||
/// <c>Int64</c> from the SAME probe inference (<c>RawBrowseCommitMapper.MapLeaf</c>).
|
/// with the raw-tag node's OPC UA type (taken from the same inference by
|
||||||
/// The step is kept because it is still the correct rule and still serves a blob authored
|
/// <c>RawBrowseCommitMapper.MapLeaf</c>). Historically it wrote the model's
|
||||||
/// by hand, by CSV or by API with no type; see
|
/// <see cref="DriverDataType.String"/> default instead, and every browse-committed tag
|
||||||
/// <c>code-reviews/Driver.MTConnect/findings.md</c> (Driver.MTConnect-003).
|
/// coerced to text inside a <c>Double</c>/<c>Int64</c> node — <c>Driver.MTConnect-003</c>.
|
||||||
|
/// The step remains the rescue for a blob authored by hand, by CSV or by API that states
|
||||||
|
/// no type at all.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="options">The options document to derive from.</param>
|
/// <param name="options">The options document to derive from.</param>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public static class RawBrowseCommitMapper
|
|||||||
IReadOnlyDictionary<string, string>? addressFields = null)
|
IReadOnlyDictionary<string, string>? addressFields = null)
|
||||||
{
|
{
|
||||||
var dataType = MapDataType(driverDataType) ?? defaultDataType;
|
var dataType = MapDataType(driverDataType) ?? defaultDataType;
|
||||||
var tagConfig = BuildTagConfig(driverType, fullName, addressFields);
|
var tagConfig = BuildTagConfig(driverType, fullName, addressFields, driverDataType);
|
||||||
var name = ToRawSegment(browseName);
|
var name = ToRawSegment(browseName);
|
||||||
var mirrored = createGroups && folderPath is { Count: > 0 }
|
var mirrored = createGroups && folderPath is { Count: > 0 }
|
||||||
? string.Join(RawPaths.Separator, folderPath)
|
? string.Join(RawPaths.Separator, folderPath)
|
||||||
@@ -212,16 +212,30 @@ public static class RawBrowseCommitMapper
|
|||||||
/// <c>(group, edgeNode, device?, metric)</c> tuple — so it is built from the browse session's stated
|
/// <c>(group, edgeNode, device?, metric)</c> tuple — so it is built from the browse session's stated
|
||||||
/// <paramref name="addressFields"/> instead. See <see cref="BuildMqttTagConfig"/>.
|
/// <paramref name="addressFields"/> instead. See <see cref="BuildMqttTagConfig"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>MTConnect binds by the driver-AGNOSTIC type, so it is forwarded from
|
||||||
|
/// <paramref name="driverDataType"/> rather than from a stated native field</b> (Gitea #560). Its
|
||||||
|
/// coercion type IS a <see cref="DriverDataType"/> — the very enum the browse leaf carries, produced
|
||||||
|
/// by the driver's own <c>/probe</c> inference — so there is no separate native spelling for the
|
||||||
|
/// browser to state and nothing to re-derive. Left unforwarded, the model's <c>String</c> default was
|
||||||
|
/// written as a non-blank authored type, which OUT-RANKS that same inference at the driver: every
|
||||||
|
/// browsed SAMPLE published text into the numeric node <see cref="MapLeaf"/> had just typed from the
|
||||||
|
/// identical inference. Same defect class as #536, one door along.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="driverType">The owning device's driver type.</param>
|
/// <param name="driverType">The owning device's driver type.</param>
|
||||||
/// <param name="fullName">The leaf's driver-side full reference to write into the address field.</param>
|
/// <param name="fullName">The leaf's driver-side full reference to write into the address field.</param>
|
||||||
/// <param name="addressFields">The leaf's structured address, when the driver binds by a tuple —
|
/// <param name="addressFields">The leaf's structured address, when the driver binds by a tuple —
|
||||||
/// see <see cref="BuildMqttTagConfig"/>. Ignored by every single-reference driver.</param>
|
/// see <see cref="BuildMqttTagConfig"/>. Ignored by every single-reference driver.</param>
|
||||||
|
/// <param name="driverDataType">The leaf's <see cref="DriverDataType"/> NAME, for a driver whose tag
|
||||||
|
/// config carries that agnostic enum rather than one of its own (MTConnect); null when the browser
|
||||||
|
/// reported no type, which keeps the driver model's default.</param>
|
||||||
/// <returns>The serialised driver-typed <c>TagConfig</c> JSON.</returns>
|
/// <returns>The serialised driver-typed <c>TagConfig</c> JSON.</returns>
|
||||||
public static string BuildTagConfig(
|
public static string BuildTagConfig(
|
||||||
string driverType,
|
string driverType,
|
||||||
string fullName,
|
string fullName,
|
||||||
IReadOnlyDictionary<string, string>? addressFields = null)
|
IReadOnlyDictionary<string, string>? addressFields = null,
|
||||||
|
string? driverDataType = null)
|
||||||
{
|
{
|
||||||
var address = fullName ?? "";
|
var address = fullName ?? "";
|
||||||
if (Is(driverType, DriverTypeNames.Mqtt))
|
if (Is(driverType, DriverTypeNames.Mqtt))
|
||||||
@@ -253,8 +267,17 @@ public static class RawBrowseCommitMapper
|
|||||||
// MTConnect: the DataItem id. The driver also accepts "address"/"dataItemId", but the typed
|
// MTConnect: the DataItem id. The driver also accepts "address"/"dataItemId", but the typed
|
||||||
// editor's canonical spelling is fullName — committing under the generic key below would open
|
// editor's canonical spelling is fullName — committing under the generic key below would open
|
||||||
// in that editor with an EMPTY id field and blank it on save.
|
// in that editor with an EMPTY id field and blank it on save.
|
||||||
|
//
|
||||||
|
// The coercion type is the leaf's own inferred DriverDataType (#560) — see the remarks above.
|
||||||
|
// A stated native "dataType" address field still wins where a browser supplies one, so this
|
||||||
|
// branch reads exactly like its AB CIP / TwinCAT / FOCAS siblings; the difference is only that
|
||||||
|
// MTConnect's fallback is the leaf's inference rather than a fixed model default.
|
||||||
if (Is(driverType, DriverTypeNames.MTConnect))
|
if (Is(driverType, DriverTypeNames.MTConnect))
|
||||||
return new MTConnectTagConfigModel { FullName = address }.ToJson();
|
return new MTConnectTagConfigModel
|
||||||
|
{
|
||||||
|
FullName = address,
|
||||||
|
DataType = NativeType(addressFields, ParseNativeType(driverDataType, DriverDataType.String)),
|
||||||
|
}.ToJson();
|
||||||
if (Is(driverType, DriverTypeNames.Galaxy))
|
if (Is(driverType, DriverTypeNames.Galaxy))
|
||||||
return WriteSingleKey("attributeRef", address);
|
return WriteSingleKey("attributeRef", address);
|
||||||
// Sql: a browsed leaf is a COLUMN, and a column name alone cannot address a tag — SqlTagConfigModel
|
// Sql: a browsed leaf is a COLUMN, and a column name alone cannot address a tag — SqlTagConfigModel
|
||||||
@@ -451,9 +474,27 @@ public static class RawBrowseCommitMapper
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static TEnum NativeType<TEnum>(IReadOnlyDictionary<string, string>? addressFields, TEnum fallback)
|
private static TEnum NativeType<TEnum>(IReadOnlyDictionary<string, string>? addressFields, TEnum fallback)
|
||||||
where TEnum : struct, Enum
|
where TEnum : struct, Enum
|
||||||
=> TryGetField(addressFields, "dataType") is { Length: > 0 } raw
|
=> ParseNativeType(TryGetField(addressFields, "dataType"), fallback);
|
||||||
&& !char.IsAsciiDigit(raw[0]) && raw[0] is not ('-' or '+')
|
|
||||||
&& Enum.TryParse<TEnum>(raw, ignoreCase: true, out var parsed)
|
/// <summary>
|
||||||
|
/// Parses one stated type NAME into a driver's data-type enum, keeping <paramref name="fallback"/>
|
||||||
|
/// for anything unusable — the rule <see cref="NativeType{TEnum}"/> applies to a stated address
|
||||||
|
/// field, split out so a type that arrives by another route (MTConnect's leaf-level
|
||||||
|
/// <see cref="DriverDataType"/>, Gitea #560) is judged by exactly the same one.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">The data-type enum the driver's tag config carries.</typeparam>
|
||||||
|
/// <param name="raw">The stated type name, or null/blank.</param>
|
||||||
|
/// <param name="fallback">The default to keep when the name is absent or unusable.</param>
|
||||||
|
/// <returns>The parsed type, or <paramref name="fallback"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// See <see cref="NativeType{TEnum}"/> for why an unparseable name keeps the default rather than
|
||||||
|
/// failing the commit, and why a numeric string is refused before parsing at all.
|
||||||
|
/// </remarks>
|
||||||
|
private static TEnum ParseNativeType<TEnum>(string? raw, TEnum fallback)
|
||||||
|
where TEnum : struct, Enum
|
||||||
|
=> raw?.Trim() is { Length: > 0 } name
|
||||||
|
&& !char.IsAsciiDigit(name[0]) && name[0] is not ('-' or '+')
|
||||||
|
&& Enum.TryParse<TEnum>(name, ignoreCase: true, out var parsed)
|
||||||
&& Enum.IsDefined(parsed)
|
&& Enum.IsDefined(parsed)
|
||||||
? parsed
|
? parsed
|
||||||
: fallback;
|
: fallback;
|
||||||
|
|||||||
@@ -248,6 +248,109 @@ public sealed class RawBrowseCommitMapperTests
|
|||||||
JsonNode.Parse(json)!.AsObject().ContainsKey("dataType").ShouldBeFalse();
|
JsonNode.Parse(json)!.AsObject().ContainsKey("dataType").ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- MTConnect: the commit must not retype the node it just typed (Gitea #560) ----------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A browse-committed MTConnect tag's OPC UA node type and its <c>TagConfig</c> coercion type
|
||||||
|
/// must BOTH come from the leaf's probe-inferred <see cref="DriverDataType"/> — the same defect
|
||||||
|
/// class as #536 arriving through a different door.
|
||||||
|
/// <para>
|
||||||
|
/// The pair is asserted together on purpose. The blob alone would pass against a mapper that
|
||||||
|
/// stopped typing the row, and the row alone was already right while the defect was live:
|
||||||
|
/// <c>MapLeaf</c> typed the node <c>Double</c> from the inference while <c>BuildTagConfig</c>
|
||||||
|
/// stamped the model's <c>String</c> default into the blob, and a non-blank authored type wins
|
||||||
|
/// the driver's coercion precedence — so every browsed SAMPLE published text into a numeric
|
||||||
|
/// node. Only the DISAGREEMENT is the bug, so only the agreement can pin the fix.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inferred">The <see cref="DriverDataType"/> the Agent's <c>/probe</c> inference reported.</param>
|
||||||
|
/// <param name="expectedNodeType">The OPC UA built-in name the raw tag row must carry.</param>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Float64", "Double")]
|
||||||
|
[InlineData("Int64", "Int64")]
|
||||||
|
[InlineData("Boolean", "Boolean")]
|
||||||
|
[InlineData("String", "String")] // an EVENT item — String is a real inference here, not a default
|
||||||
|
public void MapLeaf_types_an_MTConnect_node_and_its_coercion_alike(string inferred, string expectedNodeType)
|
||||||
|
{
|
||||||
|
var row = RawBrowseCommitMapper.MapLeaf(
|
||||||
|
driverType: DriverTypeNames.MTConnect,
|
||||||
|
fullName: "dev1_pos",
|
||||||
|
browseName: "position",
|
||||||
|
driverDataType: inferred,
|
||||||
|
defaultDataType: "Double",
|
||||||
|
groupPrefix: null,
|
||||||
|
folderPath: null,
|
||||||
|
createGroups: false);
|
||||||
|
|
||||||
|
row.Tag.DataType.ShouldBe(expectedNodeType);
|
||||||
|
JsonNode.Parse(row.Tag.TagConfig)!.AsObject()["dataType"]!.GetValue<string>().ShouldBe(inferred);
|
||||||
|
|
||||||
|
// …and the blob stays one the typed editor and the driver both accept, so the commit is not
|
||||||
|
// merely self-consistent but authorable.
|
||||||
|
var model = MTConnectTagConfigModel.FromJson(row.Tag.TagConfig);
|
||||||
|
model.FullName.ShouldBe("dev1_pos");
|
||||||
|
model.DataType.ShouldBe(Enum.Parse<DriverDataType>(inferred));
|
||||||
|
model.Validate().ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The inferred type is forwarded through <c>BuildTagConfig</c> itself, not patched on by the
|
||||||
|
/// caller — the same seam AB CIP / TwinCAT / FOCAS forward their browsed type through.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void BuildTagConfig_stamps_the_browsed_MTConnect_type()
|
||||||
|
{
|
||||||
|
var json = RawBrowseCommitMapper.BuildTagConfig(
|
||||||
|
DriverTypeNames.MTConnect, "dev1_pos", addressFields: null, driverDataType: "Float64");
|
||||||
|
|
||||||
|
var o = JsonNode.Parse(json)!.AsObject();
|
||||||
|
o["fullName"]!.GetValue<string>().ShouldBe("dev1_pos");
|
||||||
|
o["dataType"]!.GetValue<string>().ShouldBe("Float64");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The parity control for the theory above: a leaf the browser reported NO type for keeps the
|
||||||
|
/// model's own <c>String</c> default — the pre-fix behaviour, and the right answer for a
|
||||||
|
/// weakly-typed Agent. A "fix" that emitted a blank or absent <c>dataType</c> would satisfy
|
||||||
|
/// "not String by default" while leaving the blob unauthorable, so this is what stops the
|
||||||
|
/// forward being implemented as a deletion.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void MapLeaf_keeps_the_MTConnect_String_default_when_the_browser_states_no_type()
|
||||||
|
{
|
||||||
|
var row = RawBrowseCommitMapper.MapLeaf(
|
||||||
|
driverType: DriverTypeNames.MTConnect,
|
||||||
|
fullName: "dev1_exec",
|
||||||
|
browseName: "execution",
|
||||||
|
driverDataType: null,
|
||||||
|
defaultDataType: "String",
|
||||||
|
groupPrefix: null,
|
||||||
|
folderPath: null,
|
||||||
|
createGroups: false);
|
||||||
|
|
||||||
|
JsonNode.Parse(row.Tag.TagConfig)!.AsObject()["dataType"]!.GetValue<string>().ShouldBe("String");
|
||||||
|
MTConnectTagConfigModel.FromJson(row.Tag.TagConfig).Validate().ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A type name <see cref="DriverDataType"/> does not carry — an older browser, or a driver that
|
||||||
|
/// stamped something else — keeps the model default rather than failing the commit or writing a
|
||||||
|
/// name the runtime would reject. Mirrors the sibling drivers' fallback exactly, including the
|
||||||
|
/// numeric-string guard: <c>Enum.TryParse</c> accepts <c>"8"</c> for any enum and hands back
|
||||||
|
/// whatever member sits at that ordinal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stated">The unusable type name the browse leaf stated.</param>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("NotATypeThisDriverKnows")]
|
||||||
|
[InlineData("8")]
|
||||||
|
public void BuildTagConfig_falls_back_to_String_for_an_unusable_MTConnect_type(string stated)
|
||||||
|
{
|
||||||
|
var json = RawBrowseCommitMapper.BuildTagConfig(
|
||||||
|
DriverTypeNames.MTConnect, "dev1_pos", addressFields: null, driverDataType: stated);
|
||||||
|
|
||||||
|
JsonNode.Parse(json)!.AsObject()["dataType"]!.GetValue<string>().ShouldBe("String");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- BuildTagConfig: MQTT (the address is a DESCRIPTOR, not a single reference string) --------
|
// ---- BuildTagConfig: MQTT (the address is a DESCRIPTOR, not a single reference string) --------
|
||||||
|
|
||||||
/// <summary>The Sparkplug address a browse session states for a metric under a device.</summary>
|
/// <summary>The Sparkplug address a browse session states for a metric under a device.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user