fix(mtconnect): serve an empty string-typed value as Good "" (#824)
Owner decision 2026-11-11 (Java parity): a genuinely empty observation on a String/Reference or unauthored item is Good "" instead of BadNoCommunication. Non-string items (numeric, bool, DateTime, arrays) keep BadNoCommunication - the Agent supplied no value; BadTypeMismatch would wrongly blame the authored type. The UNAVAILABLE sentinel is unchanged, and a structured observation with no text stays BadNotSupported (structured check now precedes the empty check). Parity matrix, quality-mapping section and drivers rule updated. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
+91
-5
@@ -320,21 +320,107 @@ public sealed class MTConnectObservationIndexTests
|
||||
.ShouldBe(new DateTime(2026, 7, 24, 12, 0, 0, 500, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
/// <summary>An observation whose text is empty is "the Agent supplied no value" — never a Good empty string.</summary>
|
||||
/// <summary>
|
||||
/// lmxopcua#824 (Java parity): an empty element on a string-typed item is a Good empty string, not an
|
||||
/// absence. Whitespace-only text is the same (the parser trims it).
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Empty_observation_value_maps_to_BadNoCommunication(string value)
|
||||
[InlineData("", DriverDataType.String)]
|
||||
[InlineData(" ", DriverDataType.String)]
|
||||
[InlineData("", DriverDataType.Reference)]
|
||||
public void Empty_observation_value_on_a_string_item_is_a_Good_empty_string(string value, DriverDataType type)
|
||||
{
|
||||
var idx = new MTConnectObservationIndex([Tag("x", DriverDataType.String)]);
|
||||
var idx = new MTConnectObservationIndex([Tag("x", type)]);
|
||||
idx.Apply(Synthetic("x", value));
|
||||
|
||||
var snap = idx.Get("x");
|
||||
|
||||
snap.StatusCode.ShouldBe(Good);
|
||||
snap.Value.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>An unauthored item coerces as String, so its empty value is a Good empty string too (#824).</summary>
|
||||
[Fact]
|
||||
public void Empty_observation_value_on_an_unauthored_item_is_a_Good_empty_string()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex();
|
||||
idx.Apply(Synthetic("x", ""));
|
||||
|
||||
var snap = idx.Get("x");
|
||||
|
||||
snap.StatusCode.ShouldBe(Good);
|
||||
snap.Value.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #824: on a non-string item "" cannot be a value, so it stays BadNoCommunication (the Agent supplied
|
||||
/// no value) — not BadTypeMismatch, which would blame the tag's authored type.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(DriverDataType.Float64)]
|
||||
[InlineData(DriverDataType.Float32)]
|
||||
[InlineData(DriverDataType.Int32)]
|
||||
[InlineData(DriverDataType.Int64)]
|
||||
[InlineData(DriverDataType.Boolean)]
|
||||
[InlineData(DriverDataType.DateTime)]
|
||||
public void Empty_observation_value_on_a_non_string_item_stays_BadNoCommunication(DriverDataType type)
|
||||
{
|
||||
var idx = new MTConnectObservationIndex([Tag("x", type)]);
|
||||
idx.Apply(Synthetic("x", " "));
|
||||
|
||||
var snap = idx.Get("x");
|
||||
|
||||
snap.StatusCode.ShouldBe(BadNoCommunication);
|
||||
snap.Value.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #824 control: the UNAVAILABLE sentinel is unaffected — still BadNoCommunication on a String item, where an
|
||||
/// empty value is now Good.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Unavailable_on_a_string_item_is_still_BadNoCommunication()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex([Tag("x", DriverDataType.String)]);
|
||||
idx.Apply(Synthetic("x", "UNAVAILABLE"));
|
||||
|
||||
var snap = idx.Get("x");
|
||||
|
||||
snap.StatusCode.ShouldBe(BadNoCommunication);
|
||||
snap.Value.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>#824 end to end through the parser: an empty, a self-closing and a blank element are all Good "".</summary>
|
||||
[Theory]
|
||||
[InlineData("""<Program dataItemId="p" timestamp="2026-07-24T12:00:00Z" sequence="1"></Program>""")]
|
||||
[InlineData("""<Program dataItemId="p" timestamp="2026-07-24T12:00:00Z" sequence="1"/>""")]
|
||||
[InlineData("""<Program dataItemId="p" timestamp="2026-07-24T12:00:00Z" sequence="1"> </Program>""")]
|
||||
public void A_parsed_empty_event_on_a_string_item_is_a_Good_empty_string(string element)
|
||||
{
|
||||
var idx = new MTConnectObservationIndex([Tag("p", DriverDataType.String)]);
|
||||
idx.Apply(MTConnectStreamsParser.Parse($"""
|
||||
<MTConnectStreams>
|
||||
<Header instanceId="1" nextSequence="2" firstSequence="1"/>
|
||||
<Streams><DeviceStream><ComponentStream><Events>{element}</Events></ComponentStream></DeviceStream></Streams>
|
||||
</MTConnectStreams>
|
||||
"""));
|
||||
|
||||
var snap = idx.Get("p");
|
||||
|
||||
snap.StatusCode.ShouldBe(Good);
|
||||
snap.Value.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>A structured observation with no text stays BadNotSupported — it never becomes a Good "" (#824).</summary>
|
||||
[Fact]
|
||||
public void A_structured_observation_with_no_text_stays_BadNotSupported()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex([Tag("x", DriverDataType.String)]);
|
||||
idx.Apply(Synthetic("x", "", isStructured: true));
|
||||
|
||||
idx.Get("x").StatusCode.ShouldBe(BadNotSupported);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- unknown lookups
|
||||
|
||||
/// <summary>A dataItemId that is neither authored nor ever observed is unknown to this driver.</summary>
|
||||
|
||||
Reference in New Issue
Block a user