SSPC-Tester/SSPCTester.Tests/Udp5080Tests.cs

222 lines
7.2 KiB
C#
Raw Normal View History

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<FormatException>(() => 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_SendsVoltageAndCurrentLimitCommands()
{
var sync = new object();
var commands = new List<string>();
await using var server = await FakeScpiServer.StartAsync(command =>
{
lock (sync)
commands.Add(command);
return command switch
{
"*IDN?" => "UNI-T,UDP5080-100,AWPK225300032,1.03.0513",
"VOLT 12.345" => FakeScpiServer.NoResponse,
"CURR 1.25" => FakeScpiServer.NoResponse,
"OUTP?" => "1",
_ => "0"
};
});
var driver = CreateDriver(server.Port);
Assert.True(await driver.ConnectAsync());
await driver.SetVoltageAsync(12.345);
await driver.SetCurrentLimitAsync(1.25);
Assert.True(await driver.GetOutputStateAsync());
await WaitForCommandAsync("CURR 1.25");
string[] sentCommands;
lock (sync)
sentCommands = commands.ToArray();
Assert.Contains("VOLT 12.345", sentCommands);
Assert.Contains("CURR 1.25", sentCommands);
Assert.Contains("OUTP?", sentCommands);
async Task WaitForCommandAsync(string expected)
{
for (int i = 0; i < 20; i++)
{
lock (sync)
{
if (commands.Contains(expected))
return;
}
await Task.Delay(10);
}
}
}
[Theory]
[InlineData("ON", true)]
[InlineData("1", true)]
[InlineData("OFF", false)]
[InlineData("0", false)]
public void ParseSwitchState_AcceptsScpiValues(string response, bool expected) =>
Assert.Equal(expected, Udp5080.ParseSwitchState(response));
[Fact]
public async Task TcpDriver_MarksDisconnectedWhenRemoteCloses()
{
await using var server = await FakeScpiServer.StartAsync(_ => null);
var driver = CreateDriver(server.Port);
await Assert.ThrowsAsync<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<Udp5080>.Instance);
}
private sealed class FakeScpiServer : IAsyncDisposable
{
public const string NoResponse = "__NO_RESPONSE__";
private readonly TcpListener _listener;
private readonly Func<string, string?> _handler;
private readonly CancellationTokenSource _cts = new();
private readonly Task _loop;
private FakeScpiServer(TcpListener listener, Func<string, string?> handler)
{
_listener = listener;
_handler = handler;
Port = ((IPEndPoint)listener.LocalEndpoint).Port;
_loop = Task.Run(AcceptLoopAsync);
}
public int Port { get; }
public static Task<FakeScpiServer> StartAsync(Func<string, string?> 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];
var pending = "";
while (!_cts.IsCancellationRequested)
{
int read = await stream.ReadAsync(buffer, _cts.Token);
if (read <= 0) break;
pending += Encoding.ASCII.GetString(buffer, 0, read);
while (true)
{
int newline = pending.IndexOf('\n');
if (newline < 0)
break;
string command = pending[..newline].Trim();
pending = pending[(newline + 1)..];
if (string.IsNullOrEmpty(command))
continue;
string? response = _handler(command);
if (response is null) return;
if (response == NoResponse) continue;
byte[] payload = Encoding.ASCII.GetBytes(response + "\n");
await stream.WriteAsync(payload, _cts.Token);
await stream.FlushAsync(_cts.Token);
}
}
}
}
}
}