Merge b626-mtconnect (batch #626+)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+61
@@ -0,0 +1,61 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #628 (decision b): an MTConnect driver instance binds every tag through one agent client, so a
|
||||
/// second <c>/raw</c> Device under it is a deploy error — while the normal single-Device layout stays valid.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DraftValidatorMTConnectDeviceTests
|
||||
{
|
||||
private const string Code = DraftValidator.MTConnectMultipleDevicesCode;
|
||||
|
||||
private static DriverInstance Driver(string id, string type) => new()
|
||||
{
|
||||
DriverInstanceId = id, ClusterId = "c", Name = id, DriverType = type, DriverConfig = "{}",
|
||||
};
|
||||
|
||||
private static Device Dev(string id, string driverId) => new()
|
||||
{
|
||||
DeviceId = id, DriverInstanceId = driverId, Name = id, DeviceConfig = "{}",
|
||||
};
|
||||
|
||||
private static DraftSnapshot Draft(DriverInstance[] drivers, params Device[] devices) => new()
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c", DriverInstances = drivers, Devices = devices,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Two_devices_under_one_MTConnect_driver_is_a_deploy_error()
|
||||
{
|
||||
var errors = DraftValidator.Validate(Draft([Driver("mt", "MTConnect")], Dev("d1", "mt"), Dev("d2", "mt")));
|
||||
|
||||
var e = errors.Where(x => x.Code == Code).ShouldHaveSingleItem();
|
||||
e.Context.ShouldBe("mt");
|
||||
e.Message.ShouldContain("one MTConnect driver per agent device");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_normal_single_device_layout_stays_valid()
|
||||
=> DraftValidator.Validate(Draft([Driver("mt", "MTConnect")], Dev("d1", "mt")))
|
||||
.ShouldNotContain(x => x.Code == Code);
|
||||
|
||||
[Fact]
|
||||
public void A_driver_with_no_device_is_not_this_rules_concern()
|
||||
=> DraftValidator.Validate(Draft([Driver("mt", "MTConnect")]))
|
||||
.ShouldNotContain(x => x.Code == Code);
|
||||
|
||||
[Fact]
|
||||
public void Several_devices_under_a_non_MTConnect_driver_are_unaffected_and_devices_are_counted_per_driver()
|
||||
{
|
||||
var errors = DraftValidator.Validate(Draft(
|
||||
[Driver("mb", "Modbus"), Driver("mt1", "MTConnect"), Driver("mt2", "MTConnect")],
|
||||
Dev("a", "mb"), Dev("b", "mb"), Dev("c", "mt1"), Dev("d", "mt2")));
|
||||
|
||||
errors.ShouldNotContain(x => x.Code == Code);
|
||||
}
|
||||
}
|
||||
+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>
|
||||
|
||||
@@ -328,4 +328,15 @@ public sealed class MTConnectTagConfigModelTests
|
||||
public void Warnings_is_a_noop_without_probe_metadata()
|
||||
=> MTConnectTagConfigModel.FromJson("{\"fullName\":\"x\",\"dataType\":\"Boolean\"}")
|
||||
.Warnings().ShouldBeEmpty();
|
||||
|
||||
[Fact]
|
||||
public void Warnings_accepts_a_float64_array_for_time_series_and_flags_other_array_types()
|
||||
{
|
||||
MTConnectTagConfigModel.FromJson("{\"fullName\":\"x\",\"dataType\":\"Float64\",\"isArray\":true}")
|
||||
.Warnings().ShouldBeEmpty();
|
||||
|
||||
var m = MTConnectTagConfigModel.FromJson("{\"fullName\":\"x\",\"dataType\":\"Int32\",\"isArray\":true}");
|
||||
m.Warnings().ShouldHaveSingleItem().ShouldContain("Float64");
|
||||
m.Validate().ShouldBeNull(); // advisory only
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +421,25 @@ public sealed class RawTreeServiceWaveBTests
|
||||
result.Error!.ShouldContain("already exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateDevice_refuses_a_second_device_under_an_MTConnect_driver_but_not_under_others()
|
||||
{
|
||||
var (service, dbName) = Seeded();
|
||||
var mt = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "mt-1", "MTConnect", "{}");
|
||||
mt.Ok.ShouldBeTrue();
|
||||
|
||||
// CreateDriver auto-creates Device1, so the very next device is the ambiguous second one (#628).
|
||||
var refused = await service.CreateDeviceAsync(mt.CreatedId!, "Device2", "{}");
|
||||
|
||||
refused.Ok.ShouldBeFalse();
|
||||
refused.Error!.ShouldContain("one MTConnect driver per agent device");
|
||||
|
||||
// Positive control: the same call against a non-MTConnect driver still succeeds.
|
||||
(await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device9", "{}")).Ok.ShouldBeTrue();
|
||||
using var db = RawTreeTestDb.CreateNamed(dbName);
|
||||
db.Devices.Count(d => d.DriverInstanceId == mt.CreatedId).ShouldBe(1);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- Merged probe config (Test connect)
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user