From 84e293d2ff3441fd38779a40b58be6d3eddde325 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 9 Nov 2026 14:17:50 -0500 Subject: [PATCH] ci: Modbus framing test lost its FC16 PDU to a List race with the probe loop Hosted run 2243 (dc0a24f2, job 3882) failed in ModbusWritePduFramingTests.FC16_pdu_is_exactly_header_plus_byte_count with "Sequence contains no matching element" at transport.Sent.Single(fc == 0x10), one line after the write had already returned status Good: the driver sent the FC16 PDU, but the record lost it. The fixture left Probe at its default (Enabled = true), so InitializeAsync started ProbeLoopAsync on a pool thread, which sends an FC03 at once. Both that send and the write's send did List.Add on the capturing transport concurrently; List is not thread-safe and one Add can overwrite the other. - Disable the probe in this fixture, as the sibling capturing-transport fixtures (ModbusProtocolOptionsTests, ModbusCoalescingDefaultsTests) do. - Record PDUs in a ConcurrentQueue so a background send can never drop one. - Dispose the driver (await using) so no loop outlives the test. Test-only; not related to the #729/#727 changes (2243 predates them, and the run that includes them, 2260, passed Driver.Modbus.Tests 1055/1055). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV --- .../ModbusWritePduFramingTests.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusWritePduFramingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusWritePduFramingTests.cs index bc125c3f..5744cca5 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusWritePduFramingTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusWritePduFramingTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; @@ -16,16 +17,20 @@ public sealed class ModbusWritePduFramingTests { private const string Ref = "arr"; - /// Records every PDU; answers reads with zeros and multi-writes with the standard echo. + /// + /// Records every PDU; answers reads with zeros and multi-writes with the standard echo. The record is a + /// concurrent queue: a plain List lost the FC16 PDU on hosted CI (run 2243, "Sequence contains no + /// matching element") when the default-on probe loop's FC03 Add raced the write's Add. + /// private sealed class CapturingTransport : IModbusTransport { - public readonly List Sent = []; + public readonly ConcurrentQueue Sent = new(); public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask; public Task SendAsync(byte unitId, byte[] pdu, CancellationToken ct) { - Sent.Add(pdu); + Sent.Enqueue(pdu); var qty = (pdu[3] << 8) | pdu[4]; return Task.FromResult(pdu[0] switch { @@ -42,10 +47,13 @@ public sealed class ModbusWritePduFramingTests private static async Task WriteAndCaptureAsync(string tagConfig, object value, byte functionCode, bool fc23 = false) { var transport = new CapturingTransport(); - var drv = new ModbusDriver( + // The probe is off: these tests assert on the write's own PDU, and a background probe loop sending FC03 + // concurrently from a pool thread is noise at best (and was a lost-PDU race against the old List record). + await using var drv = new ModbusDriver( new ModbusDriverOptions { Host = "fake", RawTags = [new RawTagEntry(Ref, tagConfig, false)], UseFC23ForMultiRegisterWrites = fc23, + Probe = new ModbusProbeOptions { Enabled = false }, }, "modbus-framing", _ => transport); await drv.InitializeAsync("{}", CancellationToken.None);