384 lines
13 KiB
C#
384 lines
13 KiB
C#
using Exchanger.Core.Configuration;
|
|
using Exchanger.Core.TechnicalConfiguration;
|
|
using NUnit.Framework;
|
|
|
|
namespace Exchanger.Tests;
|
|
|
|
[TestFixture]
|
|
public sealed class TechnicalSettingsTests
|
|
{
|
|
private string _directory = null!;
|
|
private string _defaultPath = null!;
|
|
private string _userPath = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_directory = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"Exchanger.TechnicalSettingsTests",
|
|
Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_directory);
|
|
_defaultPath = Path.Combine(_directory, "default.toml");
|
|
_userPath = Path.Combine(_directory, "user", "technical_settings.toml");
|
|
File.WriteAllText(_defaultPath, ValidToml);
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
if (Directory.Exists(_directory))
|
|
{
|
|
Directory.Delete(_directory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Load_FirstRunCreatesReadableUserCopyAndUsesIt()
|
|
{
|
|
var service = new TechnicalSettingsService(_defaultPath, _userPath);
|
|
|
|
TechnicalSettings settings = service.Load();
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(File.Exists(_userPath), Is.True);
|
|
Assert.That(File.ReadAllText(_userPath), Is.EqualTo(ValidToml));
|
|
Assert.That(settings.Controller.Port, Is.EqualTo(ValidControllerPort));
|
|
Assert.That(settings.Controller.UseMock, Is.False);
|
|
Assert.That(settings.Controller.ReconnectIntervalSeconds, Is.EqualTo(5));
|
|
Assert.That(settings.Hopper1.StockSensorInput, Is.EqualTo(1));
|
|
Assert.That(settings.Hopper1.DispenseSensorInput, Is.EqualTo(24));
|
|
Assert.That(settings.Hopper2.StockSensorInput, Is.EqualTo(4));
|
|
Assert.That(settings.Hopper2.DispenseSensorInput, Is.EqualTo(25));
|
|
Assert.That(settings.Service.ButtonInput, Is.EqualTo(8));
|
|
Assert.That(settings.Theme?.Enabled, Is.True);
|
|
Assert.That(settings.Theme?.Directory, Is.EqualTo("user://themes"));
|
|
Assert.That(settings.Theme?.FileName, Is.EqualTo("night-theme"));
|
|
Assert.That(settings.Theme?.BuildPackagePath(), Is.EqualTo("user://themes/night-theme.pck"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Load_InvalidUserFileIsPreservedAndFallsBackToSafeDefault()
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(_userPath)!);
|
|
File.WriteAllText(_userPath, "[controller]\nuse_mock = false\n");
|
|
var warnings = new List<string>();
|
|
var service = new TechnicalSettingsService(_defaultPath, _userPath, warnings.Add);
|
|
|
|
TechnicalSettings settings = service.Load();
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(settings.Controller.Port, Is.EqualTo(ValidControllerPort));
|
|
Assert.That(File.ReadAllText(_userPath), Is.EqualTo("[controller]\nuse_mock = false\n"));
|
|
Assert.That(warnings, Has.Count.EqualTo(1));
|
|
Assert.That(warnings[0], Does.Contain("не применена"));
|
|
Assert.That(warnings[0], Does.Contain("безопасный профиль"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_MissingRequiredValueIsRejected()
|
|
{
|
|
string content = ValidToml.Replace("button_input = 8\n", string.Empty, StringComparison.Ordinal);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(content, out _, out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.False);
|
|
Assert.That(issues, Has.Some.Contains("service.button_input"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_DuplicateActiveInputIsRejected()
|
|
{
|
|
string content = ValidToml.Replace("button_input = 8", "button_input = 1", StringComparison.Ordinal);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(content, out _, out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.False);
|
|
Assert.That(issues, Has.Some.Contains("IN1 одновременно назначен"));
|
|
}
|
|
}
|
|
|
|
[TestCase(true, true, "DualHopperAuto")]
|
|
[TestCase(true, false, "Hopper1Auto")]
|
|
[TestCase(false, true, "Hopper2Auto")]
|
|
[TestCase(false, false, "Unsupported")]
|
|
public void ResolveDispenseMode_UsesEnabledHoppers(bool hopper1, bool hopper2, string expected)
|
|
{
|
|
Assert.That(TechnicalSettings.ResolveDispenseMode(hopper1, hopper2), Is.EqualTo(expected));
|
|
}
|
|
|
|
[TestCase("DualHopperAuto", true, true)]
|
|
[TestCase("Hopper1Auto", true, false)]
|
|
[TestCase("hopper2auto", false, true)]
|
|
[TestCase("Unsupported", false, false)]
|
|
[TestCase(null, false, false)]
|
|
public void ResolveConfiguredHoppers_UsesDispenseProfile(
|
|
string? dispenseMode,
|
|
bool hopper1,
|
|
bool hopper2)
|
|
{
|
|
var configured = TechnicalSettings.ResolveConfiguredHoppers(dispenseMode);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(configured.Hopper1Enabled, Is.EqualTo(hopper1));
|
|
Assert.That(configured.Hopper2Enabled, Is.EqualTo(hopper2));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_DisabledHopperRemovesItsInputsAndSelectsSecondHopper()
|
|
{
|
|
Assert.That(
|
|
TechnicalSettingsService.TryParse(
|
|
ValidToml.Replace(
|
|
"[hopper_1]\nenabled = true",
|
|
"[hopper_1]\nenabled = false",
|
|
StringComparison.Ordinal),
|
|
out TechnicalSettings? technical,
|
|
out IReadOnlyList<string> issues),
|
|
Is.True,
|
|
string.Join(" ", issues));
|
|
var hardware = new HardwareSettings();
|
|
|
|
technical!.ApplyTo(hardware);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(hardware.UseMock, Is.False);
|
|
Assert.That(hardware.PortName, Is.EqualTo(ValidControllerPort));
|
|
Assert.That(hardware.DispenseMode, Is.EqualTo("Hopper2Auto"));
|
|
Assert.That(hardware.StockSensorInputIndex, Is.EqualTo(-1));
|
|
Assert.That(hardware.Hopper1DispenseSensorInputIndex, Is.EqualTo(-1));
|
|
Assert.That(hardware.Hopper2StockSensorInputIndex, Is.EqualTo(4));
|
|
Assert.That(hardware.Hopper2DispenseSensorInputIndex, Is.EqualTo(25));
|
|
Assert.That(hardware.ServiceButtonInputIndex, Is.EqualTo(8));
|
|
Assert.That(hardware.InputPollMilliseconds, Is.EqualTo(100));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_ThemeBuildsPckPathFromDirectoryAndExtensionlessFileName()
|
|
{
|
|
Assert.That(
|
|
TechnicalSettingsService.TryParse(
|
|
ValidToml,
|
|
out TechnicalSettings? technical,
|
|
out IReadOnlyList<string> issues),
|
|
Is.True,
|
|
string.Join(" ", issues));
|
|
var themes = new ThemeSettings
|
|
{
|
|
ExternalDlcEnabled = false,
|
|
ExternalDlcPath = "user://old/theme.pck",
|
|
};
|
|
|
|
technical!.ApplyTo(themes);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(themes.ExternalDlcEnabled, Is.True);
|
|
Assert.That(themes.ExternalDlcPath, Is.EqualTo("user://themes/night-theme.pck"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_ThemeSectionIsOptionalForExistingVersionOneFiles()
|
|
{
|
|
string content = ValidToml.Replace(
|
|
"[theme]\nenabled = true\ndirectory = \"user://themes\"\nfile_name = \"night-theme\"\n\n",
|
|
string.Empty,
|
|
StringComparison.Ordinal);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(
|
|
content,
|
|
out TechnicalSettings? technical,
|
|
out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.True, string.Join(" ", issues));
|
|
Assert.That(technical?.Theme, Is.Null);
|
|
}
|
|
}
|
|
|
|
[TestCase("night-theme.pck", "без расширения")]
|
|
[TestCase("folder/night-theme", "недопустимое имя")]
|
|
public void TryParse_InvalidThemeFileNameIsRejected(string fileName, string expectedIssue)
|
|
{
|
|
string content = ValidToml.Replace(
|
|
"file_name = \"night-theme\"",
|
|
$"file_name = \"{fileName}\"",
|
|
StringComparison.Ordinal);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(content, out _, out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.False);
|
|
Assert.That(issues, Has.Some.Contains(expectedIssue));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_RelativeThemeDirectoryIsRejected()
|
|
{
|
|
string content = ValidToml.Replace(
|
|
"directory = \"user://themes\"",
|
|
"directory = \"themes\"",
|
|
StringComparison.Ordinal);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(content, out _, out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.False);
|
|
Assert.That(issues, Has.Some.Contains("theme.directory"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThemePackagePath_PreservesUserRootPrefix()
|
|
{
|
|
var theme = new ThemeTechnicalSettings(true, "user://", "space-pixel");
|
|
|
|
Assert.That(theme.BuildPackagePath(), Is.EqualTo("user://space-pixel.pck"));
|
|
}
|
|
|
|
[Test]
|
|
[Platform("Win")]
|
|
public void TryParse_WindowsLiteralThemeDirectoryPreservesBackslashes()
|
|
{
|
|
string content = ValidToml.Replace(
|
|
"directory = \"user://themes\"",
|
|
@"directory = 'D:\Exchanger\themes'",
|
|
StringComparison.Ordinal);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(
|
|
content,
|
|
out TechnicalSettings? technical,
|
|
out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.True, string.Join(" ", issues));
|
|
Assert.That(technical?.Theme?.Directory, Is.EqualTo(@"D:\Exchanger\themes"));
|
|
Assert.That(
|
|
technical?.Theme?.BuildPackagePath(),
|
|
Is.EqualTo(@"D:\Exchanger\themes\night-theme.pck"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void RepositoryTemplate_IsValidAndSafeByDefault()
|
|
{
|
|
string path = Path.Combine(ProjectPaths.FindRoot(), "config", "technical_settings.toml");
|
|
string content = File.ReadAllText(path);
|
|
|
|
bool valid = TechnicalSettingsService.TryParse(
|
|
content,
|
|
out TechnicalSettings? settings,
|
|
out IReadOnlyList<string> issues);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(valid, Is.True, string.Join(" ", issues));
|
|
Assert.That(settings!.Controller.UseMock, Is.True);
|
|
Assert.That(settings.Controller.ReconnectIntervalSeconds, Is.EqualTo(30));
|
|
Assert.That(settings.Hopper1.Enabled, Is.True);
|
|
Assert.That(settings.Hopper2.Enabled, Is.True);
|
|
Assert.That(settings.Theme?.Enabled, Is.True);
|
|
Assert.That(settings.Theme?.BuildPackagePath(), Is.EqualTo("user://theme_dlc/active_theme.pck"));
|
|
Assert.That(settings.Validate(), Is.Empty);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_ControllerPortUsesRequestedPlatformRules()
|
|
{
|
|
Assert.That(
|
|
TechnicalSettingsService.TryParse(
|
|
ValidToml,
|
|
out TechnicalSettings? currentSettings,
|
|
out IReadOnlyList<string> parseIssues),
|
|
Is.True,
|
|
string.Join(" ", parseIssues));
|
|
var linuxSettings = new TechnicalSettings
|
|
{
|
|
Version = currentSettings!.Version,
|
|
Controller = currentSettings.Controller with
|
|
{
|
|
UseMock = false,
|
|
Port = "/dev/ttyUSB0",
|
|
},
|
|
Hopper1 = currentSettings.Hopper1,
|
|
Hopper2 = currentSettings.Hopper2,
|
|
Service = currentSettings.Service,
|
|
Polling = currentSettings.Polling,
|
|
Theme = currentSettings.Theme,
|
|
};
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(linuxSettings.Validate(SerialPortPlatform.Linux), Is.Empty);
|
|
Assert.That(
|
|
linuxSettings.Validate(SerialPortPlatform.Windows),
|
|
Has.Some.Contains("controller.port"));
|
|
}
|
|
}
|
|
|
|
private static string ValidControllerPort => OperatingSystem.IsLinux() ? "/dev/ttyUSB7" : "COM7";
|
|
|
|
private static string ValidToml => ValidTomlTemplate.Replace("{PORT}", ValidControllerPort, StringComparison.Ordinal);
|
|
|
|
private const string ValidTomlTemplate = """
|
|
version = 1
|
|
|
|
[theme]
|
|
enabled = true
|
|
directory = "user://themes"
|
|
file_name = "night-theme"
|
|
|
|
[controller]
|
|
use_mock = false
|
|
port = "{PORT}"
|
|
baud_rate = 115200
|
|
reconnect_interval_seconds = 5
|
|
handshake_timeout_ms = 2000
|
|
frame_timeout_ms = 100
|
|
max_frame_length = 1024
|
|
cashless_keep_alive_seconds = 120
|
|
|
|
[hopper_1]
|
|
enabled = true
|
|
stock_sensor_input = 1
|
|
stock_low_active_high = false
|
|
dispense_sensor_input = 24
|
|
dispense_pulse_active_high = false
|
|
|
|
[hopper_2]
|
|
enabled = true
|
|
stock_sensor_input = 4
|
|
stock_low_active_high = false
|
|
dispense_sensor_input = 25
|
|
dispense_pulse_active_high = false
|
|
|
|
[service]
|
|
button_input = 8
|
|
button_active_high = false
|
|
|
|
[polling]
|
|
input_poll_ms = 100
|
|
stock_poll_seconds = 5
|
|
""";
|
|
}
|