using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Modbus; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests; /// /// Gitea #516 — used to serve the options its CONSTRUCTOR /// captured and never look at the driverConfigJson it was handed, so an operator's config edit /// was silently discarded while the deployment still sealed green. /// This test passes a CHANGED config on purpose. Every pre-existing reinit test in this /// suite passes "{}" — exactly the input the guarded re-parser treats as "keep the constructor /// options" — so those tests were blind to the defect by construction. /// [Trait("Category", "Unit")] public sealed class ModbusReinitConfigAdoptionTests { /// The transport factory receives the options the driver actually decided to use, so it is a /// direct read of which config won — no log-string matching. [Fact] public async Task Reinitialize_with_a_changed_host_rebuilds_the_transport_against_the_new_host() { var seen = new List(); var options = ModbusDriverFactoryExtensions.ParseOptions("m1", """{"host":"10.0.0.1","port":502}"""); var driver = new ModbusDriver(options, "m1", opts => { seen.Add($"{opts.Host}:{opts.Port}"); return new NeverConnectingTransport(); }); try { await driver.InitializeAsync("""{"host":"10.0.0.1","port":502}""", CancellationToken.None); } catch { /* connect failure is expected and irrelevant */ } try { await driver.ReinitializeAsync("""{"host":"10.0.0.99","port":1502}""", CancellationToken.None); } catch { /* ditto */ } seen.Count.ShouldBeGreaterThanOrEqualTo(2); seen[^1].ShouldBe("10.0.0.99:1502", "ModbusDriver kept its constructor-supplied host after a reinitialize with a changed config (#516)"); } /// A placeholder / empty document must still keep the constructor options — the guard exists so /// the many lifecycle tests that pass "{}" keep meaning what they meant. [Fact] public async Task Reinitialize_with_an_empty_document_keeps_the_constructor_options() { var seen = new List(); var options = ModbusDriverFactoryExtensions.ParseOptions("m1", """{"host":"10.0.0.1","port":502}"""); var driver = new ModbusDriver(options, "m1", opts => { seen.Add($"{opts.Host}:{opts.Port}"); return new NeverConnectingTransport(); }); try { await driver.ReinitializeAsync("{}", CancellationToken.None); } catch { /* connect failure is expected */ } seen.ShouldAllBe(s => s == "10.0.0.1:502"); } private sealed class NeverConnectingTransport : IModbusTransport { public bool IsConnected => false; public Task ConnectAsync(CancellationToken cancellationToken) => throw new IOException("test transport never connects"); public Task DisconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task SendAsync(byte unitId, byte[] pdu, CancellationToken cancellationToken) => throw new IOException("test transport never connects"); public void Dispose() { } public ValueTask DisposeAsync() => ValueTask.CompletedTask; } }