using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.Tests; /// /// Driver.FOCAS.Cli-002: the FOCAS subscribe command — a near-verbatim copy /// of the Modbus subscribe command — must /// (a) serialise writes from the OnDataChange handler (raised from the /// driver's PollGroupEngine background thread) with a lock, so the /// "Subscribed to ..." banner write from the CliFx main thread cannot interleave /// with the first poll-driven change line; and /// (b) carry the explanatory comment that documents why OnDataChange uses /// console.Output.WriteLine (synchronous, on a driver background thread) /// instead of System.Console or the async WriteLineAsync. The /// rationale is non-obvious to a reader and the Modbus copy carries it; the FOCAS /// copy must too. /// [Trait("Category", "Unit")] public sealed class SubscribeCommandConsoleHandlerTests { private static string ReadSubscribeSource() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) dir = dir.Parent; dir.ShouldNotBeNull(); return File.ReadAllText(Path.Combine( dir!.FullName, "src", "Drivers", "Cli", "ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli", "Commands", "SubscribeCommand.cs")); } [Fact] public void SubscribeCommand_explains_why_OnDataChange_uses_console_Output_synchronously() { var source = ReadSubscribeSource(); // The comment must reference the CliFx console abstraction so future copy-pastes // do not lose the rationale. source.ShouldContain("CliFx console"); source.ShouldContain("IConsole"); } [Fact] public void SubscribeCommand_serialises_console_writes_with_a_lock() { var source = ReadSubscribeSource(); // Both the banner write and the OnDataChange handler must share a writeLock so the // banner from the CliFx invocation thread cannot interleave with the first // poll-driven change line from the driver tick thread. source.ShouldContain("writeLock"); source.ShouldContain("lock (writeLock)"); } }