994c0b3b19
MTConnectObservation.SampleRate was parsed per observation and never read; MTConnectTagDefinition.MtType/MtSubType/Units were carried (factory blob DTO, tags[] DTO, probe inference) and only MtCategory was ever read. Deleted. Stored configs and TagConfig blobs still carrying mtType/mtSubType/units keep loading (System.Text.Json skips unmapped members) - pinned by a new test. The AdminUI tag editor reads those keys straight off the blob and is unaffected. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
155 lines
6.4 KiB
C#
155 lines
6.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
|
|
|
|
/// <summary>
|
|
/// Task 2 coverage for <see cref="MTConnectDriverOptions"/> / <see cref="MTConnectTagDefinition"/>:
|
|
/// defaults are sane (non-zero timers, probe on) and the required-field shape (AgentUri) binds.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class MTConnectDriverOptionsTests
|
|
{
|
|
/// <summary>Default sample/heartbeat timers are non-zero and the probe is enabled by default.</summary>
|
|
[Fact]
|
|
public void Options_default_sample_and_heartbeat_are_sane()
|
|
{
|
|
var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" };
|
|
o.SampleIntervalMs.ShouldBeGreaterThan(0);
|
|
o.HeartbeatMs.ShouldBeGreaterThan(0);
|
|
o.Probe.Enabled.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>AgentUri is the only required field; DeviceName scope defaults to unset (agent-wide).</summary>
|
|
[Fact]
|
|
public void Options_agent_uri_is_required_device_name_defaults_to_null()
|
|
{
|
|
var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" };
|
|
o.AgentUri.ShouldBe("http://agent:5000");
|
|
o.DeviceName.ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>RequestTimeoutMs and SampleCount default to sane, non-zero, non-wedging values.</summary>
|
|
[Fact]
|
|
public void Options_request_timeout_and_sample_count_are_non_zero()
|
|
{
|
|
var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" };
|
|
o.RequestTimeoutMs.ShouldBeGreaterThan(0);
|
|
o.SampleCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
/// <summary>Probe sub-options mirror ModbusProbeOptions' shape: Enabled/Interval/Timeout, both non-zero.</summary>
|
|
[Fact]
|
|
public void Probe_defaults_have_non_zero_interval_and_timeout()
|
|
{
|
|
var probe = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }.Probe;
|
|
probe.Interval.ShouldBeGreaterThan(TimeSpan.Zero);
|
|
probe.Timeout.ShouldBeGreaterThan(TimeSpan.Zero);
|
|
}
|
|
|
|
/// <summary>Reconnect sub-options default to a bounded, non-degenerate geometric backoff.</summary>
|
|
[Fact]
|
|
public void Reconnect_defaults_bound_backoff_growth()
|
|
{
|
|
var reconnect = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }.Reconnect;
|
|
reconnect.MaxBackoffMs.ShouldBeGreaterThan(reconnect.MinBackoffMs);
|
|
reconnect.BackoffMultiplier.ShouldBeGreaterThan(1.0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write-back is <b>off unless authored</b>: the driver shipped read-only, and the agent's
|
|
/// PUT surface is itself off by default, so a deployment that never mentions <c>write</c>
|
|
/// must keep exactly today's behaviour.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Write_defaults_to_disabled()
|
|
{
|
|
var write = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }.Write;
|
|
|
|
write.ShouldNotBeNull();
|
|
write.Enabled.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="MTConnectDriverOptions.Tags"/> defaults to an <i>empty</i> collection, never
|
|
/// <c>null</c> — a null collection here is an operator-authorable NullReferenceException at
|
|
/// deploy, since a driver instance authored with no tags binds this default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Options_tags_default_to_empty_and_are_never_null()
|
|
{
|
|
var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" };
|
|
|
|
o.Tags.ShouldNotBeNull();
|
|
o.Tags.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A populated options object carries the authored tag definitions through unchanged —
|
|
/// the observation index (Task 8) coerces each present observation to <i>its tag's</i>
|
|
/// DriverDataType, so the definitions must reach the driver from the factory config.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Options_round_trip_the_authored_tag_definitions()
|
|
{
|
|
var pos = new MTConnectTagDefinition(
|
|
FullName: "dev1_pos",
|
|
DriverDataType: ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float64,
|
|
MtCategory: "SAMPLE");
|
|
var partCount = new MTConnectTagDefinition(
|
|
FullName: "dev1_partcount",
|
|
DriverDataType: ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int64,
|
|
MtCategory: "EVENT");
|
|
|
|
var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000", Tags = [pos, partCount] };
|
|
|
|
o.Tags.Count.ShouldBe(2);
|
|
o.Tags[0].ShouldBe(pos);
|
|
o.Tags[1].ShouldBe(partCount);
|
|
}
|
|
|
|
/// <summary>MTConnectTagDefinition round-trips its positional fields, including the category the write path reads.</summary>
|
|
[Fact]
|
|
public void TagDefinition_carries_dataItemId_and_mtconnect_category()
|
|
{
|
|
var tag = new MTConnectTagDefinition(
|
|
FullName: "dtop_2",
|
|
DriverDataType: ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float64,
|
|
MtCategory: "SAMPLE");
|
|
|
|
tag.FullName.ShouldBe("dtop_2");
|
|
tag.DriverDataType.ShouldBe(ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float64);
|
|
tag.IsArray.ShouldBeFalse();
|
|
tag.ArrayDim.ShouldBe(0);
|
|
tag.MtCategory.ShouldBe("SAMPLE");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 T-3: mtType / mtSubType / units were deleted from the definition as dead, but a stored
|
|
/// config that still carries them — in tags[] or in a raw tag's TagConfig blob — must keep loading.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Stored_configs_carrying_the_deleted_metadata_keys_still_load()
|
|
{
|
|
const string config = """
|
|
{
|
|
"agentUri": "http://agent:5000",
|
|
"tags": [ { "fullName": "dev1_pos", "driverDataType": "Float64", "mtCategory": "SAMPLE",
|
|
"mtType": "POSITION", "mtSubType": "ACTUAL", "units": "MILLIMETER" } ]
|
|
}
|
|
""";
|
|
|
|
var options = MTConnectDriver.ParseOptions("mt-t3", config);
|
|
|
|
options.Tags.ShouldHaveSingleItem().ShouldBe(new MTConnectTagDefinition(
|
|
"dev1_pos", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float64, MtCategory: "SAMPLE"));
|
|
|
|
MTConnectTagDefinitionFactory.TryFromTagConfig(
|
|
"""{ "dataItemId": "dev1_pos", "dataType": "Float64", "mtCategory": "SAMPLE", "mtType": "POSITION", "mtSubType": "ACTUAL", "units": "MILLIMETER" }""",
|
|
out var dataItemId, out var authored)
|
|
.ShouldBeTrue();
|
|
dataItemId.ShouldBe("dev1_pos");
|
|
authored.ShouldNotBeNull().MtCategory.ShouldBe("SAMPLE");
|
|
}
|
|
}
|