264 lines
12 KiB
C#
264 lines
12 KiB
C#
using Exchanger.Hardware.Abstractions;
|
||
using Exchanger.Hardware.PeripheryController;
|
||
using NUnit.Framework;
|
||
|
||
namespace Exchanger.HardwareTests;
|
||
|
||
[TestFixture]
|
||
[NonParallelizable]
|
||
public sealed class BoardInteractiveTests
|
||
{
|
||
[Test]
|
||
[Category("HardwareInteractive")]
|
||
public void ServiceButton_PublishesTypedEvent()
|
||
{
|
||
BoardTestConfiguration configuration = RequireInteractive();
|
||
using var session = new BoardTestSession(configuration);
|
||
session.ConnectAndAuthorize();
|
||
bool requested = false;
|
||
session.Controller.ServiceRequested += () => requested = true;
|
||
|
||
TestContext.Progress.WriteLine("Нажмите и отпустите сервисную кнопку, подключённую к IN8.");
|
||
session.WaitUntil(
|
||
() => requested,
|
||
configuration.InteractionTimeout,
|
||
"За отведённое время active-low фронт IN8/SERVICE не получен.");
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwareInteractive")]
|
||
public void CoinAcceptor_ReportsAndAcknowledgesExpectedAmount()
|
||
{
|
||
BoardTestConfiguration configuration = RequireInteractive();
|
||
int expectedRubles = BoardTestConfiguration.RequireOptionalPositiveInt(
|
||
"EXCHANGER_BOARD_COIN_RUBLES",
|
||
9999,
|
||
"Монетоприёмник");
|
||
using var session = new BoardTestSession(configuration, PeripheryDispenseMode.Hopper1Auto);
|
||
session.ConnectAndAuthorize();
|
||
int balance = 0;
|
||
session.Controller.CashBalanceChanged += value => balance = value;
|
||
|
||
session.Controller.BeginCashAcceptance();
|
||
try
|
||
{
|
||
TestContext.Progress.WriteLine($"Внесите одну тестовую монету номиналом {expectedRubles} ₽.");
|
||
session.WaitForMessage(
|
||
message => message.Kind == PeripheryMessageKind.MoneyCoins
|
||
&& message.NumericValue == expectedRubles,
|
||
configuration.InteractionTimeout,
|
||
"Не получен MONEY:COINS с ожидаемым номиналом.");
|
||
Assert.That(balance, Is.EqualTo(expectedRubles));
|
||
}
|
||
finally
|
||
{
|
||
session.Controller.EndCashAcceptance();
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwareInteractive")]
|
||
public void BillAcceptor_ReportsAndAcknowledgesExpectedAmount()
|
||
{
|
||
BoardTestConfiguration configuration = RequireInteractive();
|
||
int expectedRubles = BoardTestConfiguration.RequireOptionalPositiveInt(
|
||
"EXCHANGER_BOARD_BILL_RUBLES",
|
||
9999,
|
||
"Купюроприёмник");
|
||
using var session = new BoardTestSession(configuration, PeripheryDispenseMode.Hopper1Auto);
|
||
session.ConnectAndAuthorize();
|
||
int balance = 0;
|
||
session.Controller.CashBalanceChanged += value => balance = value;
|
||
|
||
session.Controller.BeginCashAcceptance();
|
||
try
|
||
{
|
||
TestContext.Progress.WriteLine($"Внесите одну тестовую купюру номиналом {expectedRubles} ₽.");
|
||
session.WaitForMessage(
|
||
message => message.Kind == PeripheryMessageKind.MoneyBill
|
||
&& message.NumericValue == expectedRubles,
|
||
configuration.InteractionTimeout,
|
||
"Не получен MONEY:BILL с ожидаемым номиналом.");
|
||
Assert.That(balance, Is.EqualTo(expectedRubles));
|
||
}
|
||
finally
|
||
{
|
||
session.Controller.EndCashAcceptance();
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwareInteractive")]
|
||
public void StockSensor_ReportsConfiguredBinaryLevel()
|
||
{
|
||
BoardTestConfiguration configuration = RequireInteractive();
|
||
if (configuration.StockSensorInputIndex < 0)
|
||
{
|
||
Assert.Ignore("Датчик запаса не проверяется: EXCHANGER_BOARD_STOCK_INPUT не задан.");
|
||
}
|
||
|
||
MachineStockLevel expected = BoardTestConfiguration.RequireOptionalExpectedStockLevel();
|
||
using var session = new BoardTestSession(configuration);
|
||
session.ConnectAndAuthorize();
|
||
|
||
TestContext.Progress.WriteLine($"Датчик должен находиться в состоянии {expected}; отправляется команда 91.");
|
||
session.SendAndWaitForMessage(
|
||
PeripheryProtocolCommands.QueryDigitalInputs,
|
||
message => message.Kind == PeripheryMessageKind.DigitalInputs,
|
||
"Не получен документированный кадр цифровых входов.");
|
||
session.PumpOnce();
|
||
|
||
Assert.That(session.Controller.StockLevel, Is.EqualTo(expected));
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwarePayment")]
|
||
public void CashlessTerminal_ApprovesConfiguredRealPayment()
|
||
{
|
||
BoardTestConfiguration configuration = BoardTestConfiguration.RequireSafeTests();
|
||
BoardTestConfiguration.RequireConfirmation(
|
||
"EXCHANGER_BOARD_PAYMENT_CONFIRM",
|
||
"I_ACCEPT_REAL_PAYMENT",
|
||
"Тест может провести реальную банковскую операцию.");
|
||
int amountRubles = BoardTestConfiguration.RequirePositiveInt("EXCHANGER_BOARD_CARD_RUBLES", 9999);
|
||
using var session = new BoardTestSession(configuration, PeripheryDispenseMode.Hopper1Auto);
|
||
session.ConnectAndAuthorize();
|
||
MachineCardResult? result = null;
|
||
session.Controller.CardPaymentFinished += value => result = value;
|
||
|
||
TestContext.Progress.WriteLine($"На терминале будет запрошена реальная оплата {amountRubles} ₽. Завершите её тестовой картой.");
|
||
session.Controller.BeginCardPayment(amountRubles);
|
||
session.WaitUntil(
|
||
() => result.HasValue,
|
||
configuration.InteractionTimeout,
|
||
"Терминал не вернул поддерживаемый итог карточной оплаты.");
|
||
|
||
Assert.That(result, Is.EqualTo(MachineCardResult.Approved));
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwareDispense")]
|
||
public void Hopper1DirectMotor_ReportsFirmwarePulse()
|
||
{
|
||
BoardTestConfiguration configuration = BoardTestConfiguration.RequireSafeTests();
|
||
BoardTestConfiguration.RequireConfirmation(
|
||
"EXCHANGER_BOARD_DISPENSE_CONFIRM",
|
||
"I_ACCEPT_TOKEN_DISPENSE",
|
||
"Тест напрямую включит первый хоппер командой 301 и обязательно выключит его командой 300.");
|
||
using var session = new BoardTestSession(configuration);
|
||
session.ConnectAndAuthorize();
|
||
int pulseCount = 0;
|
||
|
||
void ObservePulse(string frame)
|
||
{
|
||
PeripheryMessage message = PeripheryProtocolParser.Parse(frame);
|
||
if (message.Kind == PeripheryMessageKind.Hopper1Pulse)
|
||
{
|
||
pulseCount++;
|
||
}
|
||
}
|
||
|
||
session.Port.OnDataReceived += ObservePulse;
|
||
try
|
||
{
|
||
TestContext.Progress.WriteLine("→ 301; ожидание первого HOPPER1 PULSE=1.");
|
||
session.Port.SendCommand(PeripheryProtocolCommands.EnableHopper1Motor);
|
||
session.WaitUntil(
|
||
() => pulseCount > 0,
|
||
TimeSpan.FromSeconds(5),
|
||
"После 301 плата не прислала HOPPER1 PULSE=1.");
|
||
}
|
||
finally
|
||
{
|
||
TestContext.Progress.WriteLine("→ 300; двигатель первого хоппера выключен.");
|
||
if (session.Port.IsConnected)
|
||
{
|
||
session.Port.SendCommand(PeripheryProtocolCommands.DisableHopper1Motor);
|
||
session.DrainAndClear(TimeSpan.FromMilliseconds(250));
|
||
}
|
||
|
||
session.Port.OnDataReceived -= ObservePulse;
|
||
}
|
||
|
||
Assert.That(pulseCount, Is.GreaterThanOrEqualTo(1));
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwareDispense")]
|
||
public void Hopper_DispensesExactlyConfiguredTokenCount()
|
||
{
|
||
BoardTestConfiguration configuration = BoardTestConfiguration.RequireSafeTests();
|
||
BoardTestConfiguration.RequireConfirmation(
|
||
"EXCHANGER_BOARD_DISPENSE_CONFIRM",
|
||
"I_ACCEPT_TOKEN_DISPENSE",
|
||
"Тест физически запустит хоппер.");
|
||
int tokenCount = BoardTestConfiguration.RequirePositiveInt("EXCHANGER_BOARD_DISPENSE_COUNT", 10);
|
||
using var session = new BoardTestSession(configuration, PeripheryDispenseMode.Hopper1Auto);
|
||
session.ConnectAndAuthorize();
|
||
(bool Success, int Count, string? Error)? result = null;
|
||
session.Controller.DispenseFinished += (success, count, error) => result = (success, count, error);
|
||
|
||
TestContext.Progress.WriteLine($"Запускается физическая выдача {tokenCount} жетонов. Освободите лоток.");
|
||
session.Controller.DispenseTokens(tokenCount);
|
||
session.WaitUntil(
|
||
() => result.HasValue,
|
||
configuration.InteractionTimeout,
|
||
"Хоппер не прислал финальное подтверждение выдачи.");
|
||
|
||
using (Assert.EnterMultipleScope())
|
||
{
|
||
Assert.That(result!.Value.Success, Is.True, result.Value.Error);
|
||
Assert.That(result.Value.Count, Is.EqualTo(tokenCount));
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
[Category("HardwareCable")]
|
||
public void UsbCableRemoval_IsDetectedAndBoardCanReauthorizeAfterReconnect()
|
||
{
|
||
BoardTestConfiguration configuration = BoardTestConfiguration.RequireSafeTests();
|
||
BoardTestConfiguration.RequireConfirmation(
|
||
"EXCHANGER_BOARD_CABLE_CONFIRM",
|
||
"I_WILL_UNPLUG_USB",
|
||
"Тест ожидает ручное отключение и повторное подключение USB-кабеля.");
|
||
using var session = new BoardTestSession(configuration);
|
||
session.ConnectAndAuthorize();
|
||
|
||
TestContext.Progress.WriteLine("Отключите USB-кабель контроллера.");
|
||
session.WaitUntil(
|
||
() => session.Controller.ConnectionState == MachineConnectionState.Disconnected,
|
||
configuration.InteractionTimeout,
|
||
"Отключение USB не было обнаружено.");
|
||
|
||
TestContext.Progress.WriteLine("Подключите USB-кабель обратно; тест периодически откроет тот же COM-порт.");
|
||
DateTime reconnectDeadline = DateTime.UtcNow + configuration.InteractionTimeout;
|
||
while (DateTime.UtcNow < reconnectDeadline && !session.Controller.IsReady)
|
||
{
|
||
if (!session.Port.IsConnected)
|
||
{
|
||
session.Port.Connect(configuration.PortName, configuration.BaudRate);
|
||
}
|
||
|
||
session.PumpOnce();
|
||
Thread.Sleep(500);
|
||
}
|
||
|
||
using (Assert.EnterMultipleScope())
|
||
{
|
||
Assert.That(session.Controller.IsReady, Is.True, "После возврата USB контроллер не прошёл повторную авторизацию.");
|
||
Assert.That(session.ConnectionStates, Does.Contain(MachineConnectionState.Disconnected));
|
||
Assert.That(session.ConnectionStates.Count(state => state == MachineConnectionState.Ready), Is.GreaterThanOrEqualTo(2));
|
||
}
|
||
}
|
||
|
||
private static BoardTestConfiguration RequireInteractive()
|
||
{
|
||
BoardTestConfiguration configuration = BoardTestConfiguration.RequireSafeTests();
|
||
BoardTestConfiguration.RequireConfirmation(
|
||
"EXCHANGER_BOARD_INTERACTIVE_CONFIRM",
|
||
"I_AM_READY",
|
||
"Тест требует ручного действия с периферией.");
|
||
return configuration;
|
||
}
|
||
}
|