exchanger/tests/Exchanger.HardwareTests/BoardInteractiveTests.cs

264 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}