149 lines
4.8 KiB
C#
149 lines
4.8 KiB
C#
|
|
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_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
|
||
|
|
{
|
||
|
|
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];
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|