Merge review R5 fixes: MTConnect session publish + generation bump atomic, superseded pumps joined on stop; MQTT writes encode the birth's own Sparkplug datatype (unsupported birth types refused BadNotSupported); AbLegacy dead init join removed + Dispose join test; MQTT QoS1 and Postgres key-normalisation documented
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -51,4 +51,34 @@ public sealed class AbLegacyShutdownJoinTests
|
||||
|
||||
factory.Tags["S:0"].Disposed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>The synchronous <c>Dispose()</c> path joins the parked loop too (it used to cancel and dispose under it).</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_WaitsForProbeLoopParkedInInit_ThenProbeRuntimeIsDisposed()
|
||||
{
|
||||
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var factory = new FakeAbLegacyTagFactory
|
||||
{
|
||||
Customise = p => new ParkedInitTag(p, entered, release),
|
||||
};
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)],
|
||||
Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(50), ProbeAddress = "S:0" },
|
||||
}, "drv-join-dispose", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
var dispose = Task.Run(drv.Dispose);
|
||||
|
||||
await Task.Delay(300);
|
||||
dispose.IsCompleted.ShouldBeFalse("Dispose must join the probe loop parked in runtime init");
|
||||
|
||||
// Positive control: releasing the parked call lets Dispose finish, with the probe runtime released.
|
||||
release.SetResult();
|
||||
await dispose.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
factory.Tags["S:0"].Disposed.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -35,6 +35,7 @@ public sealed class MTConnectPumpGenerationTests
|
||||
"mt-gen",
|
||||
_ => clients.Dequeue());
|
||||
await driver.InitializeAsync("{}", Ct);
|
||||
Task[] retired = [];
|
||||
|
||||
var blockOnce = 0;
|
||||
var blocking = new ManualResetEventSlim(false);
|
||||
@@ -75,6 +76,11 @@ public sealed class MTConnectPumpGenerationTests
|
||||
blocking.Set();
|
||||
await restart.WaitAsync(Watchdog, Ct);
|
||||
|
||||
// The pump retired by the restart is retained so shutdown can join it (it used to be dropped
|
||||
// un-awaited and could write health after the new session was Healthy).
|
||||
retired = driver.SupersededPumpTasks;
|
||||
retired.ShouldNotBeEmpty();
|
||||
|
||||
// The restart installed a new client; the standing subscription must be streaming from IT.
|
||||
await clientB.WaitForSampleCallsAsync(1).WaitAsync(Watchdog, Ct);
|
||||
clientA.IsDisposed.ShouldBeTrue();
|
||||
@@ -84,6 +90,10 @@ public sealed class MTConnectPumpGenerationTests
|
||||
blocking.Set();
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
// Shutdown joined every retired pump and forgot them.
|
||||
retired.ShouldAllBe(t => t.IsCompleted);
|
||||
driver.SupersededPumpTasks.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -287,6 +287,56 @@ public sealed class MqttWriteEncoderTests
|
||||
MqttMode.SparkplugB, Sparkplug("Filler1", DriverDataType.Reference), "ref", out _)
|
||||
.ShouldBe(MqttWriteEncodeError.UnsupportedDataType);
|
||||
|
||||
// ---- Birth-typed writes keep the birth's OWN Sparkplug datatype (review R5 MINOR-4) ----------
|
||||
|
||||
private static Payload.Types.Metric EncodeBirth(TahuDataType birth, object value)
|
||||
{
|
||||
var error = MqttWriteEncoder.TryEncode(
|
||||
MqttMode.SparkplugB, Sparkplug("Filler1", DriverDataType.Float64) with { DataTypeAuthored = false }, value,
|
||||
out var encoded, birth.ToDriverDataType(), birth);
|
||||
error.ShouldBe(MqttWriteEncodeError.None);
|
||||
return Payload.Parser.ParseFrom(encoded.Payload).Metrics.Single();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TahuDataType.Int8, (sbyte)-5)]
|
||||
[InlineData(TahuDataType.Uint8, (byte)200)]
|
||||
[InlineData(TahuDataType.Text, "hello")]
|
||||
[InlineData(TahuDataType.Uuid, "123e4567-e89b-12d3-a456-426614174000")]
|
||||
public void A_birth_typed_write_is_commanded_with_the_births_own_datatype(TahuDataType birth, object value)
|
||||
=> ((TahuDataType)EncodeBirth(birth, value).Datatype).ShouldBe(birth);
|
||||
|
||||
[Fact]
|
||||
public void A_birth_typed_bytes_write_carries_bytes_with_the_bytes_datatype()
|
||||
{
|
||||
var metric = EncodeBirth(TahuDataType.Bytes, new byte[] { 1, 2, 3 });
|
||||
((TahuDataType)metric.Datatype).ShouldBe(TahuDataType.Bytes);
|
||||
metric.BytesValue.ToByteArray().ShouldBe([1, 2, 3]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TahuDataType.Int32Array)]
|
||||
[InlineData(TahuDataType.DoubleArray)]
|
||||
[InlineData(TahuDataType.DataSet)]
|
||||
[InlineData(TahuDataType.Template)]
|
||||
public void A_birth_type_with_no_command_encoding_is_refused_never_sent_as_another_type(TahuDataType birth)
|
||||
{
|
||||
MqttWriteEncoder.TryEncode(
|
||||
MqttMode.SparkplugB, Sparkplug("Filler1") with { DataTypeAuthored = false }, 1, out var encoded,
|
||||
birth.ToDriverDataType(), birth)
|
||||
.ShouldBe(MqttWriteEncodeError.UnsupportedDataType);
|
||||
encoded.Payload.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_authored_type_still_wins_over_the_birth_type()
|
||||
{
|
||||
var def = Sparkplug("Filler1", DriverDataType.Int32) with { DataTypeAuthored = true };
|
||||
MqttWriteEncoder.TryEncode(MqttMode.SparkplugB, def, 7, out var encoded, null, TahuDataType.Text)
|
||||
.ShouldBe(MqttWriteEncodeError.None);
|
||||
((TahuDataType)Payload.Parser.ParseFrom(encoded.Payload).Metrics.Single().Datatype).ShouldBe(TahuDataType.Int32);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_mode_decides_the_shape_not_the_blob()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user