using System.Text; using CliFx.Infrastructure; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests; /// /// Review 07 U7, shared half: arrays render their elements (not System.Int32[]), Ctrl-C prints /// Cancelled., and the subscribe change-line writer is serialised and never throws into the poll loop. /// [Trait("Category", "Unit")] public sealed class CliHelpersU7Tests { private sealed record Handle(string DiagnosticId) : ISubscriptionHandle; private static DataChangeEventArgs Change(string reference, object? value) => new(new Handle("h"), reference, new DataValueSnapshot(value, 0u, DateTime.UtcNow, DateTime.UtcNow)); /// An array value renders as its elements, each formatted like a scalar. [Fact] public void FormatValue_renders_array_elements() { SnapshotFormatter.FormatValue(new[] { 1, 2, 3 }).ShouldBe("[1, 2, 3]"); SnapshotFormatter.FormatValue(new[] { 1.5f, -2f }).ShouldBe("[1.5, -2]"); SnapshotFormatter.FormatValue(new[] { true, false }).ShouldBe("[true, false]"); SnapshotFormatter.FormatValue(new[] { "a", "b" }).ShouldBe("[\"a\", \"b\"]"); SnapshotFormatter.FormatValue(new List { 7 }).ShouldBe("[7]"); SnapshotFormatter.FormatValue(Array.Empty()).ShouldBe("[]"); } /// A string is still a scalar, not a sequence of chars (positive control for the array arm). [Fact] public void FormatValue_keeps_a_string_scalar() { SnapshotFormatter.FormatValue("abc").ShouldBe("\"abc\""); } /// Operator Ctrl-C prints Cancelled. instead of a stack trace. [Fact] public async Task RunCancellableAsync_operator_cancellation_prints_Cancelled() { using var console = new FakeInMemoryConsole(); using var cts = new CancellationTokenSource(); cts.Cancel(); await Should.NotThrowAsync(async () => await DriverCommandBase.RunCancellableAsync(console, cts.Token, () => Task.FromCanceled(cts.Token))); console.ReadErrorString().ShouldContain("Cancelled."); } /// A cancellation the operator did not request still propagates. [Fact] public async Task RunCancellableAsync_unrequested_cancellation_propagates() { using var console = new FakeInMemoryConsole(); await Should.ThrowAsync(async () => await DriverCommandBase.RunCancellableAsync(console, CancellationToken.None, () => throw new OperationCanceledException())); } /// A change line carries the reference, the formatted value and the status. [Fact] public void ChangeLineWriter_writes_one_formatted_line_per_change() { var output = new StringWriter(); var write = DriverCommandBase.CreateChangeLineWriter(output, new object()); write(this, Change("Tag:DInt", new[] { 4, 5 })); var line = output.ToString().TrimEnd(); line.ShouldContain("Tag:DInt = [4, 5] (0x00000000 (Good))"); } /// Concurrent poll-thread changes produce whole lines, never interleaved fragments. [Fact] public async Task ChangeLineWriter_serialises_concurrent_writes() { var output = new StringWriter(new StringBuilder()); var write = DriverCommandBase.CreateChangeLineWriter(output, new object()); await Task.WhenAll(Enumerable.Range(0, 8).Select(t => Task.Run(() => { for (var i = 0; i < 200; i++) write(this, Change($"T{t}", i)); }))); var lines = output.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); lines.Length.ShouldBe(1600); lines.ShouldAllBe(l => l.StartsWith('[') && l.EndsWith("(Good))")); } /// A failing console (closed pipe) is swallowed so it cannot fault the driver's poll loop. [Fact] public void ChangeLineWriter_swallows_a_console_write_failure() { var write = DriverCommandBase.CreateChangeLineWriter(new ThrowingWriter(), new object()); Should.NotThrow(() => write(this, Change("T", 1))); } private sealed class ThrowingWriter : TextWriter { public override Encoding Encoding => Encoding.UTF8; public override void Write(char value) => throw new IOException("pipe closed"); public override void WriteLine(string? value) => throw new IOException("pipe closed"); } }