Split Settings into model, serializer, paths and store

698 lines did all of it at once. The mutable CaptureFromUi callback becomes an
ISettingsCapture the settings view implements.
This commit is contained in:
maj00r
2026-09-04 21:13:06 +02:00
parent 8e5fe01104
commit 3c502f630d
30 changed files with 1371 additions and 779 deletions

View File

@@ -0,0 +1,82 @@
using StarterNG.Domain.Settings;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class ExecutableLocatorTests
{
private const string Elf = "\u007fELF binary";
private const string PortableExecutable = "MZ binary";
[Fact]
public void The_canonical_binary_is_preferred_over_other_candidates()
{
var files = new InMemoryFileSystem()
.WithExecutable(TestInstallation.At("eu07-dev"), Elf)
.WithExecutable(TestInstallation.At("eu07"), Elf);
var installation = new TestInstallation(files);
string resolved = installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.None, problem);
Assert.EndsWith("eu07", resolved);
}
[Fact]
public void A_binary_for_the_other_platform_is_reported_as_such()
{
var files = new InMemoryFileSystem().WithExecutable(TestInstallation.At("eu07"), PortableExecutable);
var installation = new TestInstallation(files);
installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.WrongPlatform, problem);
}
[Fact]
public void A_binary_without_the_execute_bit_is_reported_as_such()
{
var files = new InMemoryFileSystem().WithFile(TestInstallation.At("eu07"), Elf);
var installation = new TestInstallation(files);
installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.NotExecutable, problem);
}
[Fact]
public void An_empty_installation_reports_the_canonical_name_as_missing()
{
var installation = new TestInstallation();
string resolved = installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.NotFound, problem);
Assert.Equal("eu07", resolved);
}
[Fact]
public void A_manually_chosen_path_is_used_as_given()
{
var files = new InMemoryFileSystem().WithExecutable(TestInstallation.At("eu07-custom"), Elf);
var installation = new TestInstallation(files);
var settings = new SimulatorSettings { SelectExeAutomatically = false, ExecutablePath = "eu07-custom" };
string resolved = installation.Executables.Resolve(settings, out var problem);
Assert.Equal("eu07-custom", resolved);
Assert.Equal(ExeProblem.None, problem);
}
[Fact]
public void Data_files_beside_the_binary_are_never_offered_as_candidates()
{
var files = new InMemoryFileSystem()
.WithFile(TestInstallation.At("eu07.ini"), "width 1280")
.WithFile(TestInstallation.At("eu07.log"), "log")
.WithExecutable(TestInstallation.At("eu07"), Elf);
var installation = new TestInstallation(files);
Assert.Equal(new[] { "eu07" }, installation.Executables.ListCandidates());
}
}

View File

@@ -0,0 +1,34 @@
using StarterNG.Application.Abstractions;
namespace StarterNG.Tests.Fakes;
/// <summary>A host machine described inline: platform, home folder, env vars.</summary>
public sealed class FakeEnvironment : IEnvironment
{
private readonly Dictionary<string, string> _variables = new(StringComparer.Ordinal);
public bool IsWindows { get; set; }
public bool IsLinux { get; set; } = true;
public bool IsMacOS { get; set; }
public string BaseDirectory { get; set; } = Path.Combine("game");
public string? GetVariable(string name) => _variables.TryGetValue(name, out var value) ? value : null;
public FakeEnvironment With(string name, string value)
{
_variables[name] = value;
return this;
}
/// <summary>Switches the fake to Windows, with an APPDATA folder.</summary>
public FakeEnvironment AsWindows(string appData = "C:/Users/kolejarz/AppData/Roaming")
{
IsWindows = true;
IsLinux = false;
IsMacOS = false;
return With("APPDATA", appData);
}
}

View File

@@ -15,6 +15,7 @@ public sealed class InMemoryFileSystem : IFileSystem
{
private readonly Dictionary<string, byte[]> _files = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _directories = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _executable = new(StringComparer.OrdinalIgnoreCase);
public DateTime LastWriteTimeUtc { get; set; } = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@@ -115,6 +116,16 @@ public sealed class InMemoryFileSystem : IFileSystem
public DateTime GetLastWriteTimeUtc(string path) => LastWriteTimeUtc;
/// <summary>Files added through <see cref="WithExecutable"/> carry the execute bit.</summary>
public bool IsExecutable(string path) => _executable.Contains(Normalize(path));
public InMemoryFileSystem WithExecutable(string path, string contents = "\u007fELF")
{
WithFile(path, contents);
_executable.Add(Normalize(path));
return this;
}
private byte[] Bytes(string path) =>
_files.TryGetValue(Normalize(path), out var bytes)
? bytes

View File

@@ -1,6 +1,7 @@
using StarterNG.Application;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Infrastructure.Settings;
using StarterNG.Infrastructure.Vehicles;
namespace StarterNG.Tests.Fakes;
@@ -28,6 +29,11 @@ public sealed class TestInstallation
Sceneries = new SceneryRepository(Files, Paths, Log, new SceneryParser(Clock, Random));
MissingAssets = new MissingAssetScanner(Files, Paths);
Library = new GameLibrary(Vehicles, Sceneries, MiniTextures, Physics, Log);
Environment = new FakeEnvironment();
SettingsPaths = new SettingsPaths(Environment, Paths);
Executables = new ExecutableLocator(Files, Paths, Environment, Log);
SettingsStore = new SettingsStore(Files, Clock, Log, SettingsPaths, new SettingsSerializer(), Executables);
}
public InMemoryFileSystem Files { get; }
@@ -42,6 +48,10 @@ public sealed class TestInstallation
public SceneryRepository Sceneries { get; }
public MissingAssetScanner MissingAssets { get; }
public GameLibrary Library { get; }
public FakeEnvironment Environment { get; }
public SettingsPaths SettingsPaths { get; }
public ExecutableLocator Executables { get; }
public SettingsStore SettingsStore { get; }
/// <summary>Path of a file inside the fake installation.</summary>
public static string At(params string[] segments) => Path.Combine(new[] { Root }.Concat(segments).ToArray());

View File

@@ -12,7 +12,7 @@ public class InstallationCheckTests
var installation = new TestInstallation();
var strings = installation.Strings.With("FaultNoDir", "missing {0}");
var check = new InstallationCheck(installation.Paths, installation.Files, strings,
installation.Physics, installation.Library);
installation.Physics, installation.Library, installation.SettingsStore);
var faults = check.Run();
@@ -35,7 +35,7 @@ public class InstallationCheckTests
var strings = installation.Strings.With("FaultNoDir", "missing {0}");
var faults = new InstallationCheck(installation.Paths, files, strings,
installation.Physics, installation.Library).Run();
installation.Physics, installation.Library, installation.SettingsStore).Run();
Assert.DoesNotContain(faults, fault => fault.StartsWith("missing "));
Assert.DoesNotContain("FaultNoWeights", faults);

View File

@@ -0,0 +1,131 @@
using StarterNG.Domain.Settings;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SettingsStoreTests
{
private const string UserIni = "/home/kolejarz/.config/MaSzyna/eu07.ini";
private static TestInstallation WithHome(InMemoryFileSystem? files = null)
{
var installation = new TestInstallation(files);
installation.Environment.With("HOME", "/home/kolejarz");
return installation;
}
[Fact]
public void Settings_are_read_from_the_users_ini()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "width 1920\nheight 1080\nfullscreen yes\nlang pl\n"));
installation.SettingsStore.Load();
var settings = installation.SettingsStore.Settings;
Assert.Equal(1920, settings.Width);
Assert.Equal(1080, settings.Height);
Assert.True(settings.Fullscreen);
Assert.Equal("Polski", settings.Language);
Assert.True(settings.LanguageWasSet);
Assert.Equal(UserIni, installation.SettingsStore.LoadedFrom);
}
[Fact]
public void A_first_run_falls_back_to_the_ini_shipped_with_the_installation()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(TestInstallation.At("eu07.ini"), "width 800\n"));
installation.SettingsStore.Load();
Assert.Equal(800, installation.SettingsStore.Settings.Width);
Assert.Equal(TestInstallation.At("eu07.ini"), installation.SettingsStore.LoadedFrom);
// Saving still goes to the user's own file, never back into the installation.
Assert.Equal(UserIni, installation.SettingsStore.SavePath);
}
[Fact]
public void With_no_ini_at_all_the_defaults_stand()
{
var installation = WithHome();
installation.SettingsStore.Load();
Assert.Equal(1280, installation.SettingsStore.Settings.Width);
Assert.False(installation.SettingsStore.Settings.LanguageWasSet);
}
[Fact]
public void A_save_round_trips_the_settings()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Settings.Width = 2560;
installation.SettingsStore.Settings.FieldOfView = 60;
installation.SettingsStore.Save();
var reopened = WithHome(installation.Files);
reopened.SettingsStore.Load();
Assert.Equal(2560, reopened.SettingsStore.Settings.Width);
Assert.Equal(60, reopened.SettingsStore.Settings.FieldOfView);
}
[Fact]
public void Keys_the_starter_does_not_manage_survive_a_round_trip()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "width 1280\nsomethingexotic 42\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("somethingexotic 42", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_registered_screen_is_asked_for_its_edits_before_a_save()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.RegisterCapture(new WidthCapture(3840));
installation.SettingsStore.CaptureAndSave();
Assert.Equal(3840, installation.SettingsStore.Settings.Width);
}
[Fact]
public void An_ini_written_by_someone_else_is_noticed()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "width 1280\n"));
installation.SettingsStore.Load();
Assert.False(installation.SettingsStore.IsConfigNewerOnDisk());
installation.Files.LastWriteTimeUtc = installation.Files.LastWriteTimeUtc.AddMinutes(5);
Assert.True(installation.SettingsStore.IsConfigNewerOnDisk());
}
[Fact]
public void On_windows_the_ini_lives_under_appdata()
{
var installation = new TestInstallation();
installation.Environment.AsWindows();
Assert.Equal(Path.Combine("C:/Users/kolejarz/AppData/Roaming", "MaSzyna", "eu07.ini"),
installation.SettingsPaths.UserConfigPath());
}
/// <summary>Stands in for the settings screen holding an unsaved edit.</summary>
private sealed class WidthCapture : StarterNG.Application.ISettingsCapture
{
private readonly int _width;
public WidthCapture(int width) => _width = width;
public void CaptureInto(SimulatorSettings settings) => settings.Width = _width;
}
}