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; using SSPCTester.Devices.Interfaces; namespace SSPCTester.Tests; public sealed class It87xxLoadTests { [Fact] public async Task CombinedQuery_ReadsVoltageCurrentAndPower() { var sync = new object(); var commands = new List(); await using var server = await FakeScpiServer.StartAsync(command => { lock (sync) commands.Add(command); return command switch { "*IDN?" => "ITECH,IT8702P,SN,1.0", "SYST:REM" => FakeScpiServer.NoResponse, "CHAN 1" => FakeScpiServer.NoResponse, "FETC:VOLT?;:FETC:CURR?;:FETC:POW?" => "2.971700E+00;1.040000E+00;3.090000E+00", "SYST:LOC" => FakeScpiServer.NoResponse, _ => "0" }; }); var driver = CreateDriver(server.Port, LoadQueryMode.Combined); Assert.True(await driver.ConnectAsync()); var reading = await driver.ReadPowerAsync(); await driver.DisconnectAsync(); Assert.Equal(2.9717, reading.Volts, 6); Assert.Equal(1.04, reading.Amps, 6); Assert.Equal(3.09, reading.Watts, 6); Assert.Contains("*IDN?", commands); Assert.Contains("SYST:REM", commands); Assert.Contains("CHAN 1", commands); } [Fact] public async Task SeparateQuery_ReadsVoltageCurrentAndPower() { await using var server = await FakeScpiServer.StartAsync(command => command switch { "*IDN?" => "ITECH,IT8702P,SN,1.0", "SYST:REM" => FakeScpiServer.NoResponse, "CHAN 1" => FakeScpiServer.NoResponse, "FETC:VOLT?" => "2.5", "FETC:CURR?" => "1.2", "FETC:POW?" => "3.0", "SYST:LOC" => FakeScpiServer.NoResponse, _ => "0" }); var driver = CreateDriver(server.Port, LoadQueryMode.Separate); Assert.True(await driver.ConnectAsync()); var reading = await driver.ReadPowerAsync(); Assert.Equal(2.5, reading.Volts, 6); Assert.Equal(1.2, reading.Amps, 6); Assert.Equal(3.0, reading.Watts, 6); } [Fact] public async Task ControlCommands_SetCurrentAndReset() { var sync = new object(); var commands = new List(); await using var server = await FakeScpiServer.StartAsync(command => { lock (sync) commands.Add(command); return command switch { "*IDN?" => "ITECH,IT8702P,SN,1.0", "SYST:REM" => FakeScpiServer.NoResponse, "CHAN 1" => FakeScpiServer.NoResponse, "CURR 1.25" => FakeScpiServer.NoResponse, "INP?" => "0", "*RST" => FakeScpiServer.NoResponse, "SYST:LOC" => FakeScpiServer.NoResponse, _ => "0" }; }); var driver = CreateDriver(server.Port, LoadQueryMode.Combined); Assert.True(await driver.ConnectAsync()); await driver.SetModeAsync(LoadMode.CC); await driver.SetValueAsync(1.25); Assert.False(await driver.GetInputStateAsync()); await driver.ResetAsync(); await driver.DisconnectAsync(); await WaitForCommandAsync("SYST:LOC"); string[] sentCommands; lock (sync) sentCommands = commands.ToArray(); Assert.Contains("CURR 1.25", sentCommands); Assert.Contains("INP?", sentCommands); Assert.Contains("*RST", sentCommands); Assert.Equal(2, sentCommands.Count(x => x == "SYST:REM")); Assert.Equal(2, sentCommands.Count(x => x == "CHAN 1")); Assert.DoesNotContain("MODE CC", 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, It87xxLoad.ParseSwitchState(response)); [Fact] public async Task QueryFailure_MarksDisconnected() { await using var server = await FakeScpiServer.StartAsync(command => command switch { "*IDN?" => "ITECH,IT8702P,SN,1.0", "SYST:REM" => FakeScpiServer.NoResponse, "CHAN 1" => FakeScpiServer.NoResponse, "FETC:VOLT?;:FETC:CURR?;:FETC:POW?" => FakeScpiServer.CloseConnection, _ => "0" }); var driver = CreateDriver(server.Port, LoadQueryMode.Combined); Assert.True(await driver.ConnectAsync()); await Assert.ThrowsAsync(() => driver.ReadPowerAsync()); Assert.False(driver.IsConnected); } private static It87xxLoad CreateDriver(int port, LoadQueryMode queryMode) { var options = Options.Create(new DeviceOptions { Load = new LoadOptions { Host = IPAddress.Loopback.ToString(), Port = port, TimeoutSeconds = 1, Channel = 1, UseFetch = true, QueryMode = queryMode } }); return new It87xxLoad(options, NullLogger.Instance); } private sealed class FakeScpiServer : IAsyncDisposable { public const string NoResponse = "__NO_RESPONSE__"; public const string CloseConnection = "__CLOSE_CONNECTION__"; 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]; 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 == CloseConnection) return; if (response == NoResponse) continue; byte[] payload = Encoding.ASCII.GetBytes(response + "\n"); await stream.WriteAsync(payload, _cts.Token); await stream.FlushAsync(_cts.Token); } } } } } }