using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using SSPCTester.Devices.Config; using SSPCTester.Devices.Drivers.Real; namespace SSPCTester.Tests; public sealed class Udp5080Tests { [Theory] [InlineData("48.125", 48.125)] [InlineData("+1.25E+01 V", 12.5)] [InlineData("VOLT: 48.000 V", 48.0)] public void ParseNumber_ExtractsFirstFiniteNumber(string response, double expected) { Assert.Equal(expected, Udp5080.ParseNumber(response), 6); } [Fact] public void ParseNumber_RejectsInvalidResponse() { Assert.Throws(() => Udp5080.ParseNumber("NO DATA")); } [Fact] public async Task TcpDriver_QueriesIdentityAndMeasurements() { await using var server = await FakeScpiServer.StartAsync(command => command switch { "*IDN?" => "UNI-T,UDP5080-100,AWPK225300032,1.03.0513", "MEASure:VOLTage?" => "48.125 V", "MEASure:CURRent?" => "+1.25E+00 A", _ => "0" }); var driver = CreateDriver(server.Port); Assert.True(await driver.ConnectAsync()); Assert.Equal("UNI-T,UDP5080-100,AWPK225300032,1.03.0513", await driver.QueryIdentityAsync()); var reading = await driver.ReadAsync(); Assert.Equal(48.125, reading.volts, 6); Assert.Equal(1.25, reading.amps, 6); Assert.True(driver.IsConnected); } [Fact] public async Task TcpDriver_MarksDisconnectedWhenRemoteCloses() { await using var server = await FakeScpiServer.StartAsync(_ => null); var driver = CreateDriver(server.Port); await Assert.ThrowsAsync(() => driver.ConnectAsync()); Assert.False(driver.IsConnected); } [Fact] public async Task TcpDriver_RejectsUnexpectedIdentity() { await using var server = await FakeScpiServer.StartAsync(command => command switch { "*IDN?" => "SOME,OTHER-DEVICE,SN,1.0", _ => "0" }); var driver = CreateDriver(server.Port); var ex = await Assert.ThrowsAsync(() => driver.ConnectAsync()); Assert.Contains("Unexpected UDP5080 identity", ex.Message); Assert.False(driver.IsConnected); } private static Udp5080 CreateDriver(int port) { var options = Options.Create(new DeviceOptions { PowerSupply = new PowerSupplyOptions { Host = IPAddress.Loopback.ToString(), Port = port, TimeoutSeconds = 1, IdnCommand = "*IDN?", VoltageCommand = "MEASure:VOLTage?", CurrentCommand = "MEASure:CURRent?" } }); return new Udp5080(options, NullLogger.Instance); } private sealed class FakeScpiServer : IAsyncDisposable { private readonly TcpListener _listener; private readonly Func _handler; private readonly CancellationTokenSource _cts = new(); private readonly Task _loop; private FakeScpiServer(TcpListener listener, Func handler) { _listener = listener; _handler = handler; Port = ((IPEndPoint)listener.LocalEndpoint).Port; _loop = Task.Run(AcceptLoopAsync); } public int Port { get; } public static Task StartAsync(Func handler) { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); return Task.FromResult(new FakeScpiServer(listener, handler)); } public async ValueTask DisposeAsync() { _cts.Cancel(); _listener.Stop(); try { await _loop; } catch { } _cts.Dispose(); } private async Task AcceptLoopAsync() { while (!_cts.IsCancellationRequested) { using var client = await _listener.AcceptTcpClientAsync(_cts.Token); await using var stream = client.GetStream(); var buffer = new byte[1024]; while (!_cts.IsCancellationRequested) { int read = await stream.ReadAsync(buffer, _cts.Token); if (read <= 0) break; string command = Encoding.ASCII.GetString(buffer, 0, read).Trim(); string? response = _handler(command); if (response is null) break; byte[] payload = Encoding.ASCII.GetBytes(response + "\n"); await stream.WriteAsync(payload, _cts.Token); await stream.FlushAsync(_cts.Token); } } } } }