using StarterNG.Classes; using StarterNG.Infrastructure.Adapters; using StarterNG.Tests.Fakes; namespace StarterNG.Tests; // Over the fake disk throughout: PresetStore itself is the player's real file. public class PresetFileTests { private const string PresetsPath = "/home/user/.config/MaSzyna/starter/userpresets.json"; private const string ThreePresets = """ { "Presets": [ { "Name": "a", "Entry": "trainset a" }, { "Name": "b", "Entry": "trainset b" }, { "Name": "c", "Entry": "trainset c" } ] } """; private static (PresetFile Presets, InMemoryFileSystem Files, FileDiagnosticsLog Log) Rig(string? held = null) { var files = new InMemoryFileSystem(); if (held is not null) files.WithFile(PresetsPath, held); var log = TestLog.Over(files); return (new PresetFile(files, log, PresetsPath), files, log); } [Fact] public void A_saved_preset_joins_the_ones_in_the_file() { var (presets, files, _) = Rig(ThreePresets); presets.Save("d", "trainset d"); Assert.Equal(new[] { "a", "b", "c", "d" }, presets.Presets().Select(p => p.Name)); Assert.False(files.FileExists(PresetsPath + ".unreadable")); } [Fact] public void Saving_under_a_name_there_replaces_that_one() { var (presets, _, _) = Rig(ThreePresets); presets.Save("B", "trainset other"); Assert.Equal(new[] { "a", "B", "c" }, presets.Presets().Select(p => p.Name)); } [Fact] public void A_first_preset_needs_no_file_to_be_there() { var (presets, files, log) = Rig(); presets.Save("a", "trainset a"); Assert.Equal("a", Assert.Single(presets.Presets()).Name); Assert.False(files.FileExists(PresetsPath + ".unreadable")); Assert.Equal(string.Empty, log.Text); } [Theory] [InlineData("""{ "Presets": [ { "Name": "a", "Entry": "trainset a" }, { "Na""")] [InlineData("")] [InlineData("null")] [InlineData("""{ "Presets": null }""")] public void A_file_cut_short_goes_aside_before_a_save_replaces_it(string held) { var (presets, files, log) = Rig(held); presets.Save("new", "trainset new"); Assert.Equal(held, files.ReadAllText(PresetsPath + ".unreadable")); Assert.Equal("new", Assert.Single(presets.Presets()).Name); Assert.Contains(PresetsPath + ".unreadable", log.Text); } [Fact] public void An_earlier_copy_set_aside_is_not_overwritten() { var (presets, files, _) = Rig("{ cut"); files.WithFile(PresetsPath + ".unreadable", "older"); presets.Save("new", "trainset new"); Assert.Equal("older", files.ReadAllText(PresetsPath + ".unreadable")); Assert.Equal("{ cut", files.ReadAllText(PresetsPath + ".unreadable-2")); } [Fact] public void A_locked_file_is_left_alone_when_no_copy_of_it_can_be_made() { var (presets, files, log) = Rig(ThreePresets); files.FailReading(PresetsPath); presets.Save("new", "trainset new"); files.AllowReading(PresetsPath); Assert.Equal(ThreePresets, files.ReadAllText(PresetsPath)); Assert.False(files.FileExists(PresetsPath + ".unreadable")); Assert.Contains("saving", log.Text); } [Fact] public void A_file_unlocked_between_the_read_and_the_write_is_still_kept() { // The list in hand is the empty one from the read that failed: the file has // to be kept whatever it looks like by the time it would be replaced. var (presets, files, _) = Rig(ThreePresets); var unlocking = new UnlockingLog(files, PresetsPath); var racing = new PresetFile(files, unlocking, PresetsPath); files.FailReading(PresetsPath); racing.Save("new", "trainset new"); Assert.Equal(ThreePresets, files.ReadAllText(PresetsPath + ".unreadable")); Assert.Equal("new", Assert.Single(presets.Presets()).Name); } [Fact] public void Deleting_from_an_unreadable_file_leaves_it_as_it_is() { var (presets, files, _) = Rig("{ cut"); presets.Delete("a"); Assert.Equal("{ cut", files.ReadAllText(PresetsPath)); Assert.False(files.FileExists(PresetsPath + ".unreadable")); } [Fact] public void An_unreadable_file_shows_as_no_presets_and_is_logged() { var (presets, _, log) = Rig("{ cut"); Assert.Empty(presets.Presets()); Assert.Contains("reading", log.Text); } /// Lets the file go the moment its read has failed and been logged. private sealed class UnlockingLog : StarterNG.Application.Abstractions.IDiagnosticsLog { private readonly InMemoryFileSystem _files; private readonly string _path; public UnlockingLog(InMemoryFileSystem files, string path) { _files = files; _path = path; } public string LogPath => ""; public string Text => ""; public void Log(string message) => _files.AllowReading(_path); public void Log(string context, Exception exception) => _files.AllowReading(_path); public void Log(IEnumerable messages) => _files.AllowReading(_path); public void Clear() { } public void Flush() { } } }