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;
}
}