feat(mtconnect): serve TIME_SERIES observations as variable-length double[] (#627)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+85
-19
@@ -30,6 +30,7 @@ public sealed class MTConnectObservationIndexTests
|
||||
private const uint BadOutOfRange = 0x803C0000u;
|
||||
private const uint BadNotSupported = 0x803D0000u;
|
||||
private const uint BadTypeMismatch = 0x80740000u;
|
||||
private const uint BadDecodingError = 0x80070000u;
|
||||
|
||||
/// <summary>Every dataItemId both /current fixtures report.</summary>
|
||||
private static readonly string[] AllFixtureDataItemIds =
|
||||
@@ -48,7 +49,7 @@ public sealed class MTConnectObservationIndexTests
|
||||
|
||||
/// <summary>Wraps one synthetic observation in the minimal legal streams result.</summary>
|
||||
private static MTConnectStreamsResult Synthetic(
|
||||
string dataItemId, string value, DateTime? timestampUtc = null, bool isStructured = false) =>
|
||||
string dataItemId, string value, DateTime? timestampUtc = null, bool isStructured = false, int? sampleCount = null) =>
|
||||
new(
|
||||
InstanceId: 1L,
|
||||
NextSequence: 2L,
|
||||
@@ -57,7 +58,8 @@ public sealed class MTConnectObservationIndexTests
|
||||
dataItemId,
|
||||
value,
|
||||
timestampUtc ?? new DateTime(2026, 7, 24, 12, 0, 0, DateTimeKind.Utc),
|
||||
isStructured)]);
|
||||
isStructured,
|
||||
sampleCount)]);
|
||||
|
||||
private static MTConnectTagDefinition Tag(string id, DriverDataType type) => new(id, type);
|
||||
|
||||
@@ -412,13 +414,9 @@ public sealed class MTConnectObservationIndexTests
|
||||
|
||||
// -------------------------------------------------------------------------------- TIME_SERIES
|
||||
|
||||
/// <summary>
|
||||
/// P1 defers TIME_SERIES array materialization to P1.5. An array-authored tag therefore
|
||||
/// reports BadNotSupported rather than pretending: the vector is real data this build
|
||||
/// cannot represent, which is neither a type mismatch nor a comms failure.
|
||||
/// </summary>
|
||||
/// <summary>#627: a Float64 array tag serves the whole vector as <c>double[]</c>, Good.</summary>
|
||||
[Fact]
|
||||
public void Time_series_authored_as_an_array_reports_BadNotSupported()
|
||||
public void Time_series_authored_as_a_float64_array_parses_to_double_array()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("dev1_vibration_ts", DriverDataType.Float64, IsArray: true, ArrayDim: 10)]);
|
||||
@@ -426,25 +424,93 @@ public sealed class MTConnectObservationIndexTests
|
||||
|
||||
var snap = idx.Get("dev1_vibration_ts");
|
||||
|
||||
snap.StatusCode.ShouldBe(BadNotSupported);
|
||||
snap.StatusCode.ShouldBe(Good);
|
||||
snap.Value.ShouldBeOfType<double[]>()
|
||||
.ShouldBe([1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]);
|
||||
}
|
||||
|
||||
/// <summary>The variable-length shape (no declared ArrayDim) is served the same way.</summary>
|
||||
[Fact]
|
||||
public void Time_series_without_a_declared_length_is_served_variable_length()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("ts", DriverDataType.Float64, IsArray: true)]);
|
||||
idx.Apply(Synthetic("ts", "1 2.5\n 3"));
|
||||
|
||||
var snap = idx.Get("ts");
|
||||
|
||||
snap.StatusCode.ShouldBe(Good);
|
||||
snap.Value.ShouldBeOfType<double[]>().ShouldBe([1.0, 2.5, 3.0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Time_series_count_mismatching_the_observations_sampleCount_is_Bad()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("ts", DriverDataType.Float64, IsArray: true)]);
|
||||
idx.Apply(Synthetic("ts", "1 2 3", sampleCount: 4));
|
||||
|
||||
var snap = idx.Get("ts");
|
||||
|
||||
snap.StatusCode.ShouldBe(BadDecodingError);
|
||||
snap.Value.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The falsifiable half of the deferral: the vector must never be silently reduced to its
|
||||
/// first element. A Good 1.1 here would be a wrong value an operator would trust.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Time_series_vector_is_never_silently_parsed_as_its_first_element()
|
||||
public void Time_series_count_mismatching_a_fixed_ArrayDim_is_Bad()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("dev1_vibration_ts", DriverDataType.Float64, IsArray: true, ArrayDim: 10)]);
|
||||
[new MTConnectTagDefinition("ts", DriverDataType.Float64, IsArray: true, ArrayDim: 5)]);
|
||||
idx.Apply(Synthetic("ts", "1 2 3"));
|
||||
|
||||
idx.Get("ts").StatusCode.ShouldBe(BadDecodingError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("1 2 abc")]
|
||||
[InlineData("1 NaN 3")]
|
||||
[InlineData("1 UNAVAILABLE 3")]
|
||||
public void Time_series_with_an_unusable_token_is_Bad_never_a_partial_vector(string text)
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("ts", DriverDataType.Float64, IsArray: true)]);
|
||||
idx.Apply(Synthetic("ts", text));
|
||||
|
||||
var snap = idx.Get("ts");
|
||||
|
||||
snap.StatusCode.ShouldNotBe(Good);
|
||||
snap.Value.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_time_series_reports_no_communication()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("ts", DriverDataType.Float64, IsArray: true)]);
|
||||
idx.Apply(Synthetic("ts", " "));
|
||||
|
||||
idx.Get("ts").StatusCode.ShouldBe(BadNoCommunication);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_non_float64_array_tag_stays_BadNotSupported()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("dev1_vibration_ts", DriverDataType.Int32, IsArray: true)]);
|
||||
idx.Apply(ParseFixture(CurrentFixture));
|
||||
|
||||
var snap = idx.Get("dev1_vibration_ts");
|
||||
idx.Get("dev1_vibration_ts").StatusCode.ShouldBe(BadNotSupported);
|
||||
}
|
||||
|
||||
snap.Value.ShouldNotBe(1.1);
|
||||
snap.StatusCode.ShouldNotBe(Good);
|
||||
/// <summary>The parser carries the observation's own sampleCount / sampleRate attributes.</summary>
|
||||
[Fact]
|
||||
public void Parser_carries_time_series_sampleCount_and_sampleRate()
|
||||
{
|
||||
var obs = ParseFixture(CurrentFixture).Observations.Single(o => o.DataItemId == "dev1_vibration_ts");
|
||||
|
||||
obs.SampleCount.ShouldBe(10);
|
||||
obs.SampleRate.ShouldBe(100.0);
|
||||
ParseFixture(CurrentFixture).Observations.Single(o => o.DataItemId == "dev1_pos").SampleCount.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -452,7 +518,7 @@ public sealed class MTConnectObservationIndexTests
|
||||
/// representable state and must report it rather than the deferral code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Unavailable_wins_over_the_unsupported_array_shape()
|
||||
public void Unavailable_array_tag_reports_no_communication()
|
||||
{
|
||||
var idx = new MTConnectObservationIndex(
|
||||
[new MTConnectTagDefinition("dev1_vibration_ts", DriverDataType.Float64, IsArray: true, ArrayDim: 10)]);
|
||||
|
||||
@@ -219,15 +219,16 @@ public sealed class MTConnectReadTests
|
||||
res[0].SourceTimestampUtc.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>A TIME_SERIES vector is real data this build cannot represent (P1.5 deferral).</summary>
|
||||
/// <summary>A probe-declared TIME_SERIES item is read end to end as a <c>double[]</c> (#627).</summary>
|
||||
[Fact]
|
||||
public async Task Read_of_a_time_series_ref_reports_BadNotSupported()
|
||||
public async Task Read_of_a_time_series_ref_returns_the_double_vector()
|
||||
{
|
||||
var (driver, _) = await InitializedDriverAsync();
|
||||
|
||||
var res = await driver.ReadAsync(["dev1_vibration_ts"], TestContext.Current.CancellationToken);
|
||||
|
||||
res[0].StatusCode.ShouldBe(BadNotSupported);
|
||||
res[0].StatusCode.ShouldBe(0u);
|
||||
res[0].Value.ShouldBeOfType<double[]>().Length.ShouldBe(10);
|
||||
}
|
||||
|
||||
/// <summary>A blank reference is data, not a programming error — it codes Bad, it does not throw.</summary>
|
||||
|
||||
Reference in New Issue
Block a user