Keep scenery files intact on save and follow them on disk

Saves keep comments and every byte they do not change. Faults in a scenery's
text are found and described. Files that change while the starter runs are
picked up without a manual reload.
This commit is contained in:
maj00r
2026-11-01 16:06:15 +01:00
parent b6735fe345
commit 69aa075bfa
30 changed files with 3596 additions and 215 deletions

View File

@@ -0,0 +1,16 @@
using StarterNG.Application.Abstractions;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Infrastructure.Sceneries;
namespace StarterNG.Tests.Fakes;
/// <summary>The scenery reader over a fake clock and dice, for tests of what it makes of a text.</summary>
public static class TestScenery
{
/// <param name="now">The hour a scenery that names none starts at.</param>
public static SceneryParser Parser(DateTime? now = null) =>
new(new FixedClock(now ?? new DateTime(2026, 1, 1, 12, 0, 0)), new StubRandom());
public static SceneryRepository Repository(IFileSystem files) =>
new(files, new GamePaths("game"), TestLog.Over(files), Parser());
}

View File

@@ -72,4 +72,45 @@ public class GameLibraryTests
Assert.Contains("textures.txt", installation.Log.Text);
}
[Fact]
public void A_folder_of_the_rolling_stock_that_cannot_be_listed_costs_only_itself()
{
var files = new InMemoryFileSystem()
.WithFile(TestInstallation.At("dynamic", "aaa", "ep09", "textures.txt"), "ep09-1.mat=ep09.e3d,ep09")
.WithFile(TestInstallation.At("dynamic", "aaa", "ep09", "ep09.fiz"), "Param: M=2")
.WithFile(TestInstallation.At("dynamic", "pkp", "ep07", "textures.txt"), "ep07-135.mat=ep07.e3d,ep07")
.WithFile(TestInstallation.At("dynamic", "pkp", "ep07", "ep07.fiz"), "Param: M=1")
.WithFile(TestInstallation.At("scenery", "kato.scn"), "//$n Katowice")
.DenyListing(TestInstallation.At("dynamic", "aaa"));
var installation = new TestInstallation(files);
installation.Library.Load();
Assert.True(installation.Library.Loaded);
Assert.Equal("ep07-135", Assert.Single(installation.Library.Vehicles.Textures).Skinfile);
Assert.Equal("Katowice", Assert.Single(installation.Library.Sceneries).Name);
Assert.Contains("aaa", installation.Log.Text);
Assert.Equal(1, installation.Physics.For("pkp/ep07/", "ep07")!.Mass);
}
[Fact]
public void The_whole_rolling_stock_failing_still_leaves_the_sceneries()
{
var installation = Complete();
var library = new GameLibrary(new FailingVehicles(), installation.Sceneries, installation.MiniTextures,
installation.Physics, installation.Log);
library.Load();
Assert.True(library.Loaded);
Assert.Equal("Katowice", Assert.Single(library.Sceneries).Name);
Assert.Contains("no disk today", installation.Log.Text);
}
private sealed class FailingVehicles : StarterNG.Application.Abstractions.IVehicleRepository
{
public int Load(StarterNG.Domain.Vehicles.VehicleCatalog catalog) =>
throw new IOException("no disk today");
}
}

View File

@@ -1,13 +1,9 @@
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SceneryExportTests
{
private static SceneryParser Parser() =>
new(new FixedClock(new DateTime(2026, 1, 1, 12, 0, 0)), new StubRandom());
[Fact]
public void An_untouched_scenery_exports_its_trainsets_back_verbatim()
{
@@ -18,7 +14,7 @@ public class SceneryExportTests
endtrainset
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
@@ -27,6 +23,45 @@ public class SceneryExportTests
Assert.Contains("endtrainset", exported);
}
[Fact]
public void A_consist_emptied_stays_out_of_the_export()
{
const string scn = """
//$n Test
trainset pociag tor_1 10 0
node -1 -1 ep07-001 dynamic PKP/EP07 ep07 ep07.mmd 0 headdriver 0
endtrainset
FirstInit
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
scenery.Trainsets[0].RemoveVehicles();
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
Assert.DoesNotContain("trainset pociag", exported);
Assert.DoesNotContain("ep07-001", exported);
Assert.Contains("FirstInit", exported);
}
[Fact]
public void An_entry_that_could_not_be_read_goes_when_emptied_too()
{
const string scn = """
//$n Test
trainset pociag tor_1 10 0
node -1 -1 ep07-001 dynamic PKP/EP07 ep07 ep07.mmd 0 headdriver 0
endtrainset
FirstInit
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
scenery.Trainsets[0].Parsed = false;
scenery.Trainsets[0].RemoveVehicles();
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
Assert.DoesNotContain("ep07-001", exported);
}
[Fact]
public void Selected_optional_includes_are_written_back_before_FirstInit()
{
@@ -36,7 +71,7 @@ public class SceneryExportTests
endoptional
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
@@ -54,7 +89,7 @@ public class SceneryExportTests
endoptional
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
scenery.Includes[0].Selected = false;
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
@@ -71,7 +106,7 @@ public class SceneryExportTests
endoptional
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
scenery.HasCompanionTimetable = false;
Assert.Contains("include rozklad.inc",
@@ -95,7 +130,7 @@ public class SceneryExportTests
time 8:30 0 0 endtime
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
scenery.Weather.Day = 200;
scenery.Weather.Temperature = 25;
@@ -120,7 +155,7 @@ public class SceneryExportTests
endtrainset
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
Assert.True(scenery.Trainsets[0].Decor);
Assert.DoesNotContain("trainset ai_1",
@@ -136,11 +171,28 @@ public class SceneryExportTests
node -1 -1 wagon_1 dynamic PKP/111A 111a 111a.mmd none 0 nobody 0 enddynamic
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
Assert.Contains("wagon_1", exported);
Assert.Contains("enddynamic", exported);
}
[Theory]
[InlineData(0, "3 0 enddynamic")]
[InlineData(3, "3 3 pantstate enddynamic")]
public void A_load_type_is_written_only_after_a_count_above_zero(int count, string tail)
{
// After a 0 the simulator would read the type as a destination and take
// the next vehicle with it.
var car = new StarterNG.Classes.Dynamic
{
Name = "en57", DataFolder = "pkp/en57", SkinFile = "s", MmdFile = "m",
LoadType = StarterNG.Classes.Dynamic.PantState, LoadCount = count
};
car.Coupling.Flags = 3;
Assert.EndsWith(tail + "\n", car.ToTrainsetNode());
}
}

View File

@@ -0,0 +1,77 @@
using StarterNG.Domain.Sceneries;
namespace StarterNG.Tests;
public class SceneryFaultTextTests
{
private static string Wording(string key) => key switch
{
"SyntaxLost" => "lost: {0}",
"SyntaxUnmade" => "unmade: {0}",
"SyntaxMore" => "and {0} more",
_ => key + " {0} {1}"
};
private static string Format(int limit, params SceneryFault[] faults) =>
SceneryFaultText.Format(faults, limit, Wording);
[Fact]
public void No_faults_read_as_nothing() =>
Assert.Equal("", Format(10));
[Fact]
public void The_place_and_the_fault_stand_in_two_columns_as_wide_as_the_longest_place()
{
string text = Format(10,
new SceneryFault(SceneryFaultKind.VehicleIncomplete, "a.scn", 3, "v1", "x"),
new SceneryFault(SceneryFaultKind.NotANumber, "long.inc", 120, "v2", "y"));
Assert.Equal(
"a.scn:3 SyntaxVehicleIncomplete v1 x\n" +
"long.inc:120 SyntaxNotANumber v2 y",
text);
}
[Fact]
public void What_a_fault_has_no_subject_or_token_for_is_filled_in()
{
string text = Format(10, new SceneryFault(SceneryFaultKind.StrayEndTrainset, "a.scn", 3));
Assert.Equal("a.scn:3 SyntaxStrayEndTrainset ?", text);
}
[Fact]
public void What_a_fault_costs_stands_under_it_in_the_second_column()
{
string text = Format(10,
new SceneryFault(SceneryFaultKind.VehicleNotClosed, "a.scn", 3, "v1", "x") { Lost = "v2, v3", Unmade = "b" });
Assert.Equal(
"a.scn:3 SyntaxVehicleNotClosed v1 x\n" +
" lost: v2, v3\n" +
" unmade: b",
text);
}
[Fact]
public void Past_the_limit_the_faults_are_only_counted_and_do_not_widen_the_column()
{
string text = Format(1,
new SceneryFault(SceneryFaultKind.NotANumber, "a.scn", 3, "v1", "x"),
new SceneryFault(SceneryFaultKind.NotANumber, "a-longer-name.scn", 4, "v2", "y"),
new SceneryFault(SceneryFaultKind.NotANumber, "a.scn", 5, "v3", "z"));
Assert.Equal(
"a.scn:3 SyntaxNotANumber v1 x\n" +
"and 2 more",
text);
}
[Fact]
public void Exactly_as_many_as_the_limit_are_not_counted_as_more()
{
string text = Format(1, new SceneryFault(SceneryFaultKind.NotANumber, "a.scn", 3, "v1", "x"));
Assert.DoesNotContain("more", text);
}
}

View File

@@ -0,0 +1,376 @@
using StarterNG.Application;
using StarterNG.Domain.Sceneries;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SceneryFileWatchTests
{
private static readonly string Scn = TestInstallation.At("scenery", "kato.scn");
private static readonly string Other = TestInstallation.At("scenery", "lk4.scn");
private static readonly string Inc = TestInstallation.At("scenery", "auta.inc");
private const string Text =
"//$n Katowice\n" +
"FirstInit\n" +
"trainset a tor 0 0\n" +
"node -1 0 v1 dynamic d s m 0 headdriver 3 0 enddynamic\n" +
"endtrainset\n" +
"trainset b tor 0 0\n" +
"node -1 0 v2 dynamic d s m 0 headdriver 3 0 enddynamic\n" +
"endtrainset\n";
private static (TestInstallation Installation, SceneryFileWatch Watch, AppState State) Rig(InMemoryFileSystem files)
{
var installation = new TestInstallation(files);
installation.Library.ReloadSceneries();
var state = new AppState();
return (installation, new SceneryFileWatch(files, installation.Sceneries, installation.Library, state), state);
}
private static InMemoryFileSystem Folder() => new InMemoryFileSystem().WithFile(Scn, Text);
[Fact]
public void Files_as_they_were_read_are_no_change()
{
var (installation, watch, _) = Rig(Folder());
Assert.Empty(watch.Look(installation.Library.Sceneries));
}
[Fact]
public void A_file_changed_outside_is_read_anew()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var change = Assert.Single(watch.Look(installation.Library.Sceneries));
Assert.Same(current, change.Current);
Assert.Equal("Katowice Główne", change.Fresh!.Name);
}
[Fact]
public void An_include_that_changed_or_came_makes_the_scenery_read_anew()
{
var files = new InMemoryFileSystem().WithFile(Scn, Text + "include auta.inc end\n");
var (installation, watch, _) = Rig(files);
Assert.Empty(watch.Look(installation.Library.Sceneries));
files.WriteAllText(Inc, "// auta\n");
Assert.Single(watch.Look(installation.Library.Sceneries));
installation.Library.ReloadSceneries();
Assert.Empty(watch.Look(installation.Library.Sceneries));
files.WriteAllText(Inc, "// inne auta\n");
Assert.Single(watch.Look(installation.Library.Sceneries));
}
[Fact]
public void A_new_file_in_the_folder_comes_as_an_added_scenery()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
files.WriteAllText(Other, "//$n Lublin\n");
var change = Assert.Single(watch.Look(installation.Library.Sceneries));
Assert.Null(change.Current);
Assert.Equal("Lublin", change.Fresh!.Name);
}
[Fact]
public void A_file_is_gone_only_if_still_away_on_the_next_look()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
files.DeleteFile(Scn);
Assert.Empty(watch.Look(installation.Library.Sceneries));
var change = Assert.Single(watch.Look(installation.Library.Sceneries));
Assert.Same(current, change.Current);
Assert.Null(change.Fresh);
}
[Fact]
public void A_file_replaced_between_two_looks_is_not_gone()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
files.DeleteFile(Scn);
Assert.Empty(watch.Look(installation.Library.Sceneries));
files.WriteAllText(Scn, Text);
Assert.Empty(watch.Look(installation.Library.Sceneries));
}
[Fact]
public void A_file_that_will_not_load_is_tried_and_logged_once_until_it_can_be()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
files.WriteAllText(Other, "//$n Lublin\n");
files.FailReading(Other);
Assert.Empty(watch.Look(installation.Library.Sceneries));
string once = installation.Log.Text;
Assert.Contains("lk4.scn", once);
Assert.Empty(watch.Look(installation.Library.Sceneries));
Assert.Equal(once, installation.Log.Text);
files.AllowReading(Other);
Assert.Equal("Lublin", Assert.Single(watch.Look(installation.Library.Sceneries)).Fresh!.Name);
}
[Fact]
public void Its_own_save_is_no_change_from_outside()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var scenery = installation.Library.Sceneries.Single();
var trainset = scenery.Trainsets[0];
trainset.ReadyToGo = true;
var log = new FileDiagnosticsLog(files, installation.Clock, installation.Paths);
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, new SaveSceneryTrainset(files, log).Execute(scenery, trainset).Outcome);
Assert.False(watch.IsStale(scenery));
Assert.Empty(watch.Look(installation.Library.Sceneries));
}
[Fact]
public void A_consist_whose_text_stands_goes_over_with_its_edits()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
var edited = current.Trainsets[1];
edited.ReadyToGo = true;
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var fresh = watch.Look(installation.Library.Sceneries).Single().Fresh!;
Assert.Empty(SceneryFileWatch.CarryOver(current, fresh));
Assert.Same(edited, fresh.Trainsets[1]);
Assert.True(SceneryTrainsetText.IsModified(fresh.Trainsets[1]));
}
[Fact]
public void An_edited_consist_whose_text_changed_too_is_the_conflict()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
var edited = current.Trainsets[1];
edited.ReadyToGo = true;
files.WriteAllText(Scn, Text.Replace(" v2 ", " v22 "));
var fresh = watch.Look(installation.Library.Sceneries).Single().Fresh!;
Assert.Same(edited, Assert.Single(SceneryFileWatch.CarryOver(current, fresh)));
Assert.NotSame(edited, fresh.Trainsets[1]);
}
[Fact]
public void A_consist_changed_but_not_edited_just_gives_way()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
files.WriteAllText(Scn, Text.Replace(" v2 ", " v22 "));
var fresh = watch.Look(installation.Library.Sceneries).Single().Fresh!;
Assert.Empty(SceneryFileWatch.CarryOver(current, fresh));
Assert.Equal("v22", fresh.Trainsets[1].Vehicles.Single().Name);
}
[Fact]
public void The_weather_chosen_by_hand_stays_and_the_authored_one_follows_the_file()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
current.Weather.Day = 123;
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var fresh = watch.Look(installation.Library.Sceneries).Single().Fresh!;
SceneryFileWatch.CarryOver(current, fresh);
Assert.Equal(123, fresh.Weather.Day);
}
[Fact]
public void What_is_picked_follows_into_the_scenery_read_anew()
{
var files = Folder();
var (installation, watch, state) = Rig(files);
var current = installation.Library.Sceneries.Single();
var picked = current.Trainsets[1];
state.CurrentScenery = current;
state.CurrentTrainset = picked;
state.StartingVehicleName = "v2";
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var change = watch.Look(installation.Library.Sceneries).Single();
SceneryFileWatch.CarryOver(change.Current!, change.Fresh!);
watch.Apply(change);
Assert.Same(change.Fresh, installation.Library.Sceneries.Single());
Assert.Same(change.Fresh, state.CurrentScenery);
Assert.Same(picked, state.CurrentTrainset);
Assert.Equal("v2", state.StartingVehicleName);
}
[Fact]
public void A_picked_consist_changed_in_the_file_is_followed_by_its_name()
{
var files = Folder();
var (installation, watch, state) = Rig(files);
var current = installation.Library.Sceneries.Single();
var picked = current.Trainsets[1];
state.CurrentScenery = current;
state.CurrentTrainset = picked;
state.StartingVehicleName = "v2";
files.WriteAllText(Scn, Text.Replace(" v2 ", " v22 "));
var change = watch.Look(installation.Library.Sceneries).Single();
SceneryFileWatch.CarryOver(change.Current!, change.Fresh!);
watch.Apply(change);
Assert.Same(change.Fresh!.Trainsets[1], state.CurrentTrainset);
Assert.Null(state.StartingVehicleName);
}
[Fact]
public void A_change_to_a_scenery_reloaded_since_the_look_is_left()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var change = watch.Look(installation.Library.Sceneries).Single();
installation.Library.ReloadSceneries();
Assert.Equal(SceneryVerdict.Leave, watch.Judge(change).Verdict);
}
[Fact]
public void A_change_to_a_scenery_saved_from_here_since_the_look_is_left_and_nothing_carried_over()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var change = watch.Look(installation.Library.Sceneries).Single();
current.Sources = change.Fresh!.Sources;
Assert.Equal(SceneryVerdict.Leave, watch.Judge(change).Verdict);
Assert.NotSame(current.Trainsets[0], change.Fresh.Trainsets[0]);
}
[Fact]
public void A_scenery_new_in_the_folder_is_taken()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
files.WriteAllText(Other, "//$n Lublin\n");
var change = watch.Look(installation.Library.Sceneries).Single();
Assert.Equal(SceneryVerdict.Take, watch.Judge(change).Verdict);
}
[Fact]
public void A_scenery_new_in_the_folder_that_got_in_since_the_look_is_left()
{
var (installation, watch, _) = Rig(Folder());
var change = new SceneryChange(null, installation.Library.Sceneries.Single());
Assert.Equal(SceneryVerdict.Leave, watch.Judge(change).Verdict);
}
[Fact]
public void A_change_that_loses_no_edits_is_taken_with_the_edits_carried_over()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var edited = installation.Library.Sceneries.Single().Trainsets[1];
edited.ReadyToGo = true;
files.WriteAllText(Scn, Text.Replace("Katowice", "Katowice Główne"));
var change = watch.Look(installation.Library.Sceneries).Single();
var judgement = watch.Judge(change);
Assert.Equal(SceneryVerdict.Take, judgement.Verdict);
Assert.Empty(judgement.Lost);
Assert.Same(edited, change.Fresh!.Trainsets[1]);
}
[Fact]
public void A_change_that_loses_edits_is_asked_about_naming_them()
{
var files = Folder();
var (installation, watch, _) = Rig(files);
var current = installation.Library.Sceneries.Single();
var edited = current.Trainsets[1];
edited.ReadyToGo = true;
files.WriteAllText(Scn, Text.Replace(" v2 ", " v22 "));
var change = watch.Look(installation.Library.Sceneries).Single();
var judgement = watch.Judge(change);
Assert.Equal(SceneryVerdict.AskFirst, judgement.Verdict);
Assert.Same(edited, Assert.Single(judgement.Lost));
Assert.Same(current.Trainsets[0], change.Fresh!.Trainsets[0]);
}
[Fact]
public void A_scenery_gone_with_edits_in_it_stays_for_them_to_be_saved_elsewhere()
{
var (installation, watch, _) = Rig(Folder());
var current = installation.Library.Sceneries.Single();
current.Trainsets[1].ReadyToGo = true;
Assert.Equal(SceneryVerdict.Leave, watch.Judge(new SceneryChange(current, null)).Verdict);
}
[Fact]
public void A_scenery_gone_with_no_edits_in_it_is_taken_off()
{
var (installation, watch, _) = Rig(Folder());
var current = installation.Library.Sceneries.Single();
Assert.Equal(SceneryVerdict.Take, watch.Judge(new SceneryChange(current, null)).Verdict);
}
[Fact]
public void A_scenery_gone_takes_what_was_picked_with_it()
{
var files = Folder();
var (installation, watch, state) = Rig(files);
var current = installation.Library.Sceneries.Single();
state.CurrentScenery = current;
state.CurrentTrainset = current.Trainsets[0];
state.StartingVehicleName = "v1";
watch.Apply(new SceneryChange(current, null));
Assert.Empty(installation.Library.Sceneries);
Assert.Null(state.CurrentScenery);
Assert.Null(state.CurrentTrainset);
Assert.Null(state.StartingVehicleName);
}
}

View File

@@ -1,13 +1,10 @@
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Domain.Sceneries;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SceneryParserTests
{
private static SceneryParser Parser(DateTime? now = null) =>
new(new FixedClock(now ?? new DateTime(2026, 5, 4, 9, 15, 0)), new StubRandom());
[Fact]
public void Directives_become_the_scenery_headline()
{
@@ -19,7 +16,7 @@ public class SceneryParserTests
//$i images/kato.jpg
""";
var scenery = Parser().Parse("scenery/kato.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/kato.scn", scn);
Assert.Equal("Śląsk", scenery.Group);
Assert.Equal("Osobowy do Katowic", scenery.Name);
@@ -32,7 +29,7 @@ public class SceneryParserTests
[Fact]
public void The_a_directive_marks_a_scenery_archival()
{
var scenery = Parser().Parse("scenery/old.scn", "//$a\n//$n Stary\n");
var scenery = TestScenery.Parser().Parse("scenery/old.scn", "//$a\n//$n Stary\n");
Assert.True(scenery.Archival);
}
@@ -46,7 +43,7 @@ public class SceneryParserTests
//$f doc/notes.txt
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
Assert.Collection(scenery.Attachments,
first =>
@@ -76,7 +73,7 @@ public class SceneryParserTests
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
var include = Assert.Single(scenery.Includes);
Assert.Equal("zima.inc", include.FilePath);
@@ -95,7 +92,7 @@ public class SceneryParserTests
endoptional
""";
var include = Assert.Single(Parser().Parse("scenery/x.scn", scn).Includes);
var include = Assert.Single(TestScenery.Parser().Parse("scenery/x.scn", scn).Includes);
Assert.Equal(2, include.Kind);
Assert.False(include.Selected);
@@ -112,7 +109,7 @@ public class SceneryParserTests
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
var trainset = Assert.Single(scenery.Trainsets);
Assert.True(trainset.Parsed);
@@ -126,11 +123,11 @@ public class SceneryParserTests
public void Vehicles_outside_a_trainset_are_collected_as_loose()
{
const string scn = """
node -1 -1 wagon_1 dynamic PKP/111A 111a 111a.mmd none 0 nobody 0 enddynamic
node -1 -1 wagon_1 dynamic PKP/111A 111a 111a.mmd none 0 nobody 0 0 enddynamic
FirstInit
""";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
var vehicle = Assert.Single(scenery.LooseVehicles);
Assert.Equal("wagon_1", vehicle.Name);
@@ -142,13 +139,43 @@ public class SceneryParserTests
[Fact]
public void A_malformed_loose_vehicle_is_reported_and_left_in_place()
{
const string scn = "node x -1 wagon_1 dynamic PKP/111A 111a 111a.mmd none 0 nobody 0 enddynamic\n";
const string scn = "node x -1 wagon_1 dynamic PKP/111A 111a 111a.mmd none 0 nobody 0 0 enddynamic\n";
var scenery = Parser().Parse("scenery/x.scn", scn);
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
Assert.Empty(scenery.LooseVehicles);
Assert.Equal(new[] { "wagon_1" }, scenery.LooseVehicleNames);
Assert.Contains(scenery.Faults, fault => fault.Contains("wagon_1"));
var fault = Assert.Single(scenery.Faults);
Assert.Equal((SceneryFaultKind.NotANumber, "wagon_1", "x", 1), (fault.Kind, fault.Subject, fault.Token, fault.Line));
Assert.Contains("enddynamic", scenery.Template);
}
[Fact]
public void Directives_in_a_file_put_in_place_are_not_the_scenery_s_own()
{
const string included = """
//$n Auta
//$l Inne
//$d Opis aut
//$f doc/auta.pdf Auta
//$i auta.jpg
//$a
trainset auto road1 0 50
endtrainset
""";
string text = SceneryIncludes.Inline("//$d Opis scenerii\n//$f doc/mapa.png Mapa\nFirstInit\ninclude auta.scm end\n",
name => (name, included));
var scenery = TestScenery.Parser().Parse("scenery/x.scn", text);
Assert.Equal("Opis scenerii", scenery.Description);
Assert.Equal("doc/mapa.png", Assert.Single(scenery.Attachments).FilePath);
Assert.Null(scenery.Name);
Assert.Null(scenery.Group);
Assert.Null(scenery.ImageName);
Assert.False(scenery.Archival);
// The file's own trainset is still listed, and its text still written for a start.
Assert.Equal("auta.scm", Assert.Single(scenery.Trainsets).SourceFile);
Assert.Contains("//$d Opis aut", scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom()));
}
}

View File

@@ -0,0 +1,382 @@
using StarterNG.Domain.Sceneries;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class ScenerySyntaxTests
{
private static (SceneryFaultKind, string, int, string?, string?) Seen(SceneryFault fault) =>
(fault.Kind, fault.File, fault.Line, fault.Subject, fault.Token);
[Fact]
public void A_trainset_commented_out_is_no_trainset()
{
const string scn = """
/* trainset stary tor_1 0 0
node -1 0 st dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset */
// trainset dla gracza:
trainset pociag tor_1 10 0
node -1 0 ep07-001 dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic//koniec
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
var trainset = Assert.Single(scenery.Trainsets);
Assert.Equal("pociag", trainset.Name);
Assert.Single(trainset.Vehicles);
Assert.Empty(scenery.Faults);
Assert.Contains("trainset stary", scenery.Template);
}
[Fact]
public void A_trainset_without_its_end_is_reported_and_the_next_one_read_on_its_own()
{
const string scn = """
FirstInit
trainset pierwszy tor_1 10 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
trainset drugi tor_2 10 0
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
Assert.Equal((SceneryFaultKind.TrainsetNotClosed, "x.scn", 2, "pierwszy", null),
Seen(Assert.Single(scenery.Faults)));
var trainset = Assert.Single(scenery.Trainsets);
Assert.Equal("drugi", trainset.Name);
Assert.Equal("b", Assert.Single(trainset.Vehicles).Name);
// The unended one is the simulator's to read as written.
Assert.Contains("trainset pierwszy", scenery.Template);
}
[Fact]
public void A_vehicle_without_its_end_is_reported_yet_its_trainset_listed()
{
const string scn = """
trainset pociag tor_1 10 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3
node -1 0 b dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
var fault = Assert.Single(scenery.Faults);
Assert.Equal((SceneryFaultKind.VehicleNotClosed, "x.scn", 2, "a", null), Seen(fault));
// The simulator goes on to the next enddynamic, taking b for a's.
Assert.Equal("b", fault.Lost);
Assert.Equal(new[] { "a", "b" }, Assert.Single(scenery.Trainsets).Vehicles.Select(v => v.Name));
}
[Fact]
public void A_vehicle_with_no_load_count_takes_the_next_one_with_it()
{
const string scn = """
trainset pociag tor_1 10 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 enddynamic
node -1 0 b dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
// Its enddynamic read for the count, "node" for a destination, b's for its end.
var fault = Assert.Single(scenery.Faults);
Assert.Equal((SceneryFaultKind.LoadCountMissing, 2, "a", "b"), (fault.Kind, fault.Line, fault.Subject, fault.Lost));
Assert.Equal(new[] { "a", "b" }, Assert.Single(scenery.Trainsets).Vehicles.Select(v => v.Name));
}
[Fact]
public void A_load_with_no_type_runs_on_into_the_next_trainset()
{
const string scn = """
trainset pierwszy tor_1 10 0
node -1 0 a dynamic PKP/412Z 412z 412z 0 nobody 3 40 enddynamic
endtrainset
trainset drugi tor_2 10 0
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
node -1 0 c dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
// The second endtrainset is what ends the first trainset then.
var fault = Assert.Single(scenery.Faults);
Assert.Equal((SceneryFaultKind.LoadTypeMissing, 2, "a", "40", "endtrainset, trainset drugi, b"),
(fault.Kind, fault.Line, fault.Subject, fault.Token, fault.Lost));
Assert.Equal(new[] { "pierwszy", "drugi" }, scenery.Trainsets.Select(t => t.Name));
}
[Theory]
[InlineData("0 none", "none")]
[InlineData("0 pantstate", "pantstate")]
[InlineData("0 cel Katowice", null)]
[InlineData("3 pantstate", null)]
[InlineData("12 wegiel", null)]
public void One_word_after_the_load_is_taken_for_a_destination_named_enddynamic(string load, string? word)
{
string scn = $"""
trainset pociag tor_1 10 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 {load} enddynamic
node -1 0 b dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
endtrainset
""";
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults;
if (word is null)
Assert.Empty(faults);
else
Assert.Equal((SceneryFaultKind.WordBeforeEnd, word, "b"), (faults.Single().Kind, faults.Single().Token, faults.Single().Lost));
}
[Fact]
public void A_header_written_twice_is_told_as_such()
{
const string scn = """
trainset baltyk\mod\exe45151 chojnice301 40 0.1
//$o -Skład ozdobny
trainset baltyk\mod\exe45151 chojnice301 40 0.1
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
Assert.Equal((SceneryFaultKind.TrainsetHeaderRepeated, "x.scn", 1, "baltyk\\mod\\exe45151", null),
Seen(Assert.Single(scenery.Faults)));
Assert.Single(scenery.Trainsets);
}
[Fact]
public void A_trainset_whose_header_is_swallowed_is_told_at_the_fault_that_began_it()
{
const string scn = """
trainset pierwszy tor_1 10 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset
//trainset bez_naglowka tor_2 0 0
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset
trainset trzeci tor_3 0 0
node -1 0 c1 dynamic PKP/EU07 eu07 eu07 0 headdriver 3 0 enddynamic
node -1 0 c2 dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
node -1 0 c3 dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
endtrainset
trainset czwarty tor_4 0 0
node -1 0 d dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
// c2 with no trainset takes c3, and the endtrainset is left alone - all of
// it from b, where the fault is.
var fault = Assert.Single(scenery.Faults);
Assert.Equal((SceneryFaultKind.VehicleOutsideTrainset, 5, "endtrainset, trainset trzeci, c1", "trzeci"),
(fault.Kind, fault.Line, fault.Lost, fault.Unmade));
Assert.Equal(new[] { "pierwszy", "trzeci", "czwarty" }, scenery.Trainsets.Select(t => t.Name));
}
[Fact]
public void Text_where_a_number_belongs_is_named()
{
const string scn = """
trainset pociag tor_1 1O 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0,5 headdriver 3 0 enddynamic
endtrainset
""";
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults;
Assert.Equal(new[]
{
(SceneryFaultKind.NotANumber, "x.scn", 1, (string?)"pociag", (string?)"1O"),
(SceneryFaultKind.NotANumber, "x.scn", 2, "a", "0,5")
}, faults.Select(Seen));
}
[Fact]
public void A_track_name_split_by_a_space_is_named_whole()
{
// "Pawonków#L" with its "ó" lost to a space.
const string scn = """
trainset none Pawonk w#L 200.0 0.0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
endtrainset
""";
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults;
Assert.Equal((SceneryFaultKind.TrackNameSplit, "x.scn", 1, "none", "Pawonk w#L"), Seen(Assert.Single(faults)));
}
[Fact]
public void Vehicles_left_without_their_trainset_header_are_told_so()
{
const string scn = """
//trainset 2003r\64040 Staniszcze_Male_po#2 -50 0.1
node -1 0 EN57-1542ra dynamic pkp\en57_v2 EN57-1542ra EN57RA 0 headdriver 255 0 enddynamic
endtrainset
node -1 0 auto dynamic road\ford transit transit droga-b 0 headdriver 25.0 0 enddynamic
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
// On no track the simulator has, it drops the vehicle and goes on to the
// next enddynamic - past the endtrainset, to the car's.
var fault = Assert.Single(scenery.Faults);
Assert.Equal((SceneryFaultKind.VehicleOutsideTrainset, 2, "endtrainset, auto"), (fault.Kind, fault.Line, fault.Lost));
Assert.Equal("auto", Assert.Single(scenery.LooseVehicles).Name);
}
[Fact]
public void Structure_faults_are_found_where_the_simulator_reports_them()
{
const string scn = """
endtrainset
trainset pusty tor_1 10 0
endtrainset trainset pociag tor_1 10 0
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver enddynamic
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
reversed
endtrainset
include dekoracje.scm // bez end
include auta.scm end
/* bez końca
""";
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults.Select(f => (f.Kind, f.Line)).ToList();
Assert.Equal(new[]
{
(SceneryFaultKind.StrayEndTrainset, 1),
(SceneryFaultKind.VehicleIncomplete, 4),
(SceneryFaultKind.ReversedMisplaced, 6),
(SceneryFaultKind.IncludeNotClosed, 8),
(SceneryFaultKind.UnclosedComment, 10)
}, faults);
}
[Fact]
public void A_trainset_with_no_vehicles_is_listed_to_be_filled_in_the_depot()
{
const string scn = """
trainset td/rozklad tdo_n1734 0.0 0.1
//$o Skład gotowy na żeberku.
endtrainset
include koniec.scm
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn, _ => new IncludeLookup(true));
Assert.Empty(scenery.Faults);
Assert.Equal("td/rozklad", Assert.Single(scenery.Trainsets).Name);
}
[Fact]
public void Reversed_may_follow_the_assignments_ahead_of_the_vehicles()
{
const string scn = """
Trainset pociag tor_1 10 0
assignment pl "Pociąg" endassignment
reversed
Node -1 0 a dynamic PKP/EP07 ep07 ep07 0 HeadDriver 3 0 EndDynamic
EndTrainset
""";
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
Assert.Empty(scenery.Faults);
var trainset = Assert.Single(scenery.Trainsets);
Assert.True(trainset.Reversed);
Assert.Equal(StarterNG.Classes.eDriverType.Headdriver, Assert.Single(trainset.Vehicles).DriverType);
}
[Fact]
public void A_fault_in_an_included_file_is_placed_in_that_file()
{
string text = SceneryIncludes.Inline(
"FirstInit\ninclude tomaszewo/auta.scm end\ntrainset zly tor 0\nendtrainset\n",
name => (name, "// auta\ntrainset auto road1 0 50\nnode -1 0 a1 dynamic car car car x headdriver 3 0 enddynamic\nendtrainset"));
var faults = TestScenery.Parser().Parse("scenery/x.scn", text).Faults.Select(Seen);
Assert.Equal(new[]
{
(SceneryFaultKind.NotANumber, "tomaszewo/auta.scm", 3, (string?)"a1", (string?)"x"),
(SceneryFaultKind.TrainsetIncomplete, "x.scn", 3, "zly", null)
}, faults);
}
[Fact]
public void Missing_includes_are_reported_but_optional_ones_and_templates_are_not()
{
const string scn = """
include tory.scm end
include brak.scm end // komentarz
include budynek.inc (p1) 0 0 end
include $(p1)_dom.inc end
include opcja.scm end //$optional 2,Opcja
endoptional
""";
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn, name => new IncludeLookup(name is "tory.scm" or "budynek.inc")).Faults;
Assert.Equal((SceneryFaultKind.MissingInclude, "x.scn", 2, null, "brak.scm"), Seen(Assert.Single(faults)));
}
[Fact]
public void An_include_named_in_another_case_is_found_as_the_simulator_finds_it()
{
var files = new InMemoryFileSystem(caseSensitive: true)
.WithLegacyFile("game/scenery/x.scn", "FirstInit\ninclude Tomaszewo/Auta.SCM end\n")
.WithLegacyFile("game/scenery/tomaszewo/auta.scm",
"trainset auto road1 0 50\nnode -1 0 a1 dynamic car car car 0 headdriver 3 0 enddynamic\nendtrainset\n");
var repository = TestScenery.Repository(files);
var scenery = repository.Load("game/scenery/x.scn")!;
Assert.Empty(scenery.Faults);
Assert.Equal("tomaszewo/auta.scm", Assert.Single(scenery.Trainsets).SourceFile);
}
[Fact]
public void An_include_on_disk_only_in_other_letter_case_is_what_linux_misses()
{
// The simulator folds the name to lower case and opens it so.
var files = new InMemoryFileSystem(caseSensitive: true)
.WithLegacyFile("game/scenery/braniewo_szeroki.scn",
"include braniewo/base.ctr end\ninclude Braniewo/Tory.SCM end\n")
.WithFile("game/scenery/Braniewo/Base.ctr", "")
.WithFile("game/scenery/braniewo/tory.scm", "");
var repository = TestScenery.Repository(files);
var faults = repository.Load("game/scenery/braniewo_szeroki.scn")!.Faults;
Assert.Equal((SceneryFaultKind.IncludeLetterCase, "braniewo_szeroki.scn", 1, "Braniewo/Base.ctr", "braniewo/base.ctr"),
Seen(Assert.Single(faults)));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void A_missing_terrain_file_is_no_fault_while_the_scenery_has_its_own_sbt(bool sbt)
{
var files = new InMemoryFileSystem(caseSensitive: true)
.WithLegacyFile("game/scenery/Lk2_Sandbox.scn", "include nmt/nmt100_mazowieckie_ter.scm end\nFirstInit\n");
if (sbt)
files.WithFile("game/scenery/lk2_sandbox.sbt", "EU07");
var repository = TestScenery.Repository(files);
var scenery = repository.Load("game/scenery/Lk2_Sandbox.scn")!;
Assert.Equal(sbt, scenery.HasCompanionTimetable);
Assert.Equal(sbt ? 0 : 1, scenery.Faults.Count);
}
}

View File

@@ -0,0 +1,578 @@
using System.Reflection;
using System.Text;
using StarterNG.Application;
using StarterNG.Application.Abstractions;
using StarterNG.Classes;
using StarterNG.Domain.Sceneries;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SceneryTrainsetSaveTests
{
private const string Path = "scenery/test.scn";
private const string Scn =
"//$n Bałtyk\r\n" +
"FirstInit\r\n" +
"trainset pociag tor_1 200.0 0.0\r\n" +
"//$o -Opis składu\r\n" +
"node -1 0 ep07-001 dynamic PKP\\EP07 ep07-001 ep07 0 headdriver 3 0 enddynamic\r\n" +
"node -1 0 wagon-1 dynamic PKP\\111A b10-1 111a 0 nobody 3 0 enddynamic\r\n" +
"endtrainset\r\n" +
"// koniec pliku z polską literą: żółw\r\n";
private static Encoding Cp1250 => Encoding.GetEncoding(1250);
private static (InMemoryFileSystem Files, Scenery Scenery, SaveSceneryTrainset Save) Setup(string scn) =>
Setup(Cp1250.GetBytes(scn));
private static (InMemoryFileSystem Files, Scenery Scenery, SaveSceneryTrainset Save) Setup(byte[] bytes)
{
var files = new InMemoryFileSystem();
files.WriteAllBytes(Path, bytes);
var scenery = TestScenery.Parser().Parse(Path, LegacyText.Decode(bytes));
return (files, scenery, new SaveSceneryTrainset(files, TestLog.Over(files)));
}
[Fact]
public void Reversed_after_the_header_is_read_and_written_back()
{
var trainset = new Trainset(
"trainset a tor 0 0 reversed\n" +
"node -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\n" +
"endtrainset");
Assert.True(trainset.Reversed);
Assert.Single(trainset.Vehicles);
Assert.StartsWith("trainset a tor 0 0 reversed\n", trainset.GetTrainsetEntry());
}
[Fact]
public void A_trainset_without_reversed_is_not_turned()
{
var trainset = new Trainset("trainset a tor 0 0\nendtrainset");
Assert.False(trainset.Reversed);
Assert.DoesNotContain("reversed", trainset.GetTrainsetEntry());
}
[Fact]
public void An_untouched_trainset_is_not_modified_however_its_numbers_are_spelled()
{
var (_, scenery, _) = Setup(Scn);
Assert.False(SceneryTrainsetText.IsModified(scenery.Trainsets[0]));
}
[Fact]
public void The_head_stays_as_written_and_the_vehicles_are_rewritten()
{
var (_, scenery, _) = Setup(Scn.Replace("200.0 0.0\r\n", "200.0 0.0\r\nreversed\r\n"));
var trainset = scenery.Trainsets[0];
trainset.Vehicles.RemoveAt(1);
string entry = SceneryTrainsetText.Compose(trainset, "\r\n");
Assert.StartsWith("trainset pociag tor_1 200.0 0.0\r\nreversed\r\n//$o -Opis składu\r\nnode", entry);
Assert.Contains("ep07-001", entry);
Assert.DoesNotContain("wagon-1", entry);
Assert.EndsWith("enddynamic\r\nendtrainset", entry);
Assert.DoesNotContain("\r\r", entry);
Assert.DoesNotMatch("[^\r]\n", entry);
}
[Fact]
public void Only_the_speed_of_the_header_follows_the_ready_switch()
{
var (_, scenery, _) = Setup(Scn);
var trainset = scenery.Trainsets[0];
trainset.ReadyToGo = true;
string entry = SceneryTrainsetText.Compose(trainset, "\n");
Assert.StartsWith("trainset pociag tor_1 200.0 0.1\n", entry);
Assert.True(SceneryTrainsetText.IsModified(trainset));
}
[Fact]
public void Saving_rewrites_the_trainset_alone_and_keeps_the_original_once()
{
var (files, scenery, save) = Setup(Scn);
var trainset = scenery.Trainsets[0];
trainset.Vehicles.RemoveAt(1);
var (outcome, _) = save.Execute(scenery, trainset);
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, outcome);
string written = Cp1250.GetString(files.ReadAllBytes(Path));
Assert.StartsWith("//$n Bałtyk\r\nFirstInit\r\ntrainset pociag tor_1 200.0 0.0\r\n//$o -Opis składu\r\n", written);
Assert.DoesNotContain("wagon-1", written);
Assert.EndsWith("endtrainset\r\n// koniec pliku z polską literą: żółw\r\n", written);
Assert.Equal(Scn, Cp1250.GetString(files.ReadAllBytes(Path + SaveSceneryTrainset.BackupSuffix)));
Assert.False(SceneryTrainsetText.IsModified(trainset));
// A second save finds what the first one wrote; the backup stays the original.
trainset.Vehicles.Clear();
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, save.Execute(scenery, trainset).Outcome);
Assert.DoesNotContain("ep07-001", Cp1250.GetString(files.ReadAllBytes(Path)));
Assert.Equal(Scn, Cp1250.GetString(files.ReadAllBytes(Path + SaveSceneryTrainset.BackupSuffix)));
}
[Fact]
public void Of_two_trainsets_written_alike_the_right_one_is_saved()
{
const string block =
"trainset none tor_1 0 0\r\n" +
"node -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\r\n" +
"endtrainset\r\n";
var (files, scenery, save) = Setup(block + "// x\r\n" + block);
var second = scenery.Trainsets[1];
second.Vehicles.Clear();
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, save.Execute(scenery, second).Outcome);
string written = Cp1250.GetString(files.ReadAllBytes(Path));
Assert.StartsWith(block + "// x\r\ntrainset none tor_1 0 0\r\nendtrainset", written);
}
[Fact]
public void A_file_changed_meanwhile_is_left_alone()
{
var (files, scenery, save) = Setup(Scn);
var trainset = scenery.Trainsets[0];
trainset.Vehicles.Clear();
files.WithLegacyFile(Path, Scn.Replace("wagon-1", "wagon-2"));
Assert.Equal(SceneryTrainsetSaveOutcome.NotFound, save.Execute(scenery, trainset).Outcome);
Assert.Contains("wagon-2", Cp1250.GetString(files.ReadAllBytes(Path)));
Assert.False(files.FileExists(Path + SaveSceneryTrainset.BackupSuffix));
}
private const string Cars =
"trainset none road1 -20 50\r\n" +
"node -1 0 auto_1 dynamic road\\man man nl293 0 headdriver 0 0 enddynamic\r\n" +
"endtrainset\r\n";
private const string Traffic =
"node -1 0 nl293_a dynamic road\\man man nl293 \t\tryp_rd110 \t0 headdriver 50 0 enddynamic\r\n";
private const string WithIncludes =
"include tory.scm end\r\n" +
"FirstInit\r\n" +
"trainset pociag tor_1 0 0\r\n" +
"node -1 0 ep07-001 dynamic PKP\\EP07 ep07-001 ep07 0 headdriver 3 0 enddynamic\r\n" +
"endtrainset\r\n" +
"include tomaszewo/auta.scm end //ruch uliczny\r\n" +
"include tomaszewo/eventy.ctr end\r\n" +
"/* include tomaszewo/auta.scm end */\r\n" +
"include tomaszewo/auta.scm parametr end\r\n";
private static (string File, string Text)? Files(string name) => name switch
{
"tory.scm" => (name, Cars),
"tomaszewo/auta.scm" => (name, Cars + Traffic),
"tomaszewo/eventy.ctr" => (name, "event e1 multiple 0 none endevent\r\n"),
_ => null
};
[Fact]
public void Includes_after_first_init_that_hold_trainsets_are_put_in_place()
{
string text = SceneryIncludes.Inline(WithIncludes, Files);
// Before FirstInit is the scenery itself; without trainsets there is nothing to list.
Assert.StartsWith("include tory.scm end\r\n", text);
Assert.Contains("include tomaszewo/eventy.ctr end", text);
Assert.Contains(SceneryIncludes.BeginMarker + "tomaszewo/auta.scm\r\ntrainset none road1", text);
// Commented out, or with parameters to put in: left to the simulator.
Assert.Contains("/* include tomaszewo/auta.scm end */", text);
Assert.Contains("include tomaszewo/auta.scm parametr end", text);
Assert.Single(SceneryIncludes.Ranges(text));
}
[Fact]
public void A_trainset_from_an_include_is_listed_with_its_file_and_exported_in_place()
{
var scenery = TestScenery.Parser().Parse(Path, SceneryIncludes.Inline(WithIncludes, Files));
Assert.Equal(2, scenery.Trainsets.Count);
Assert.Null(scenery.Trainsets[0].SourceFile);
Assert.Equal("tomaszewo/auta.scm", scenery.Trainsets[1].SourceFile);
// Loose road traffic in the file stays put, as written; only its name is known.
Assert.Empty(scenery.LooseVehicles);
Assert.Contains("nl293_a", scenery.LooseVehicleNames);
string exported = scenery.BuildExportContent(skipDecorTrainsets: false, new StubRandom());
Assert.Contains("auto_1", exported);
Assert.Contains(Traffic, exported);
Assert.True(exported.IndexOf(Traffic, StringComparison.Ordinal) > exported.IndexOf("FirstInit", StringComparison.Ordinal));
Assert.DoesNotContain("include tomaszewo/auta.scm end //", exported);
}
[Fact]
public void A_trainset_from_an_include_is_saved_into_that_file()
{
var files = new InMemoryFileSystem()
.WithLegacyFile(Path, WithIncludes)
.WithLegacyFile("scenery/tomaszewo/auta.scm", Cars + "// koniec\r\n");
var scenery = TestScenery.Parser().Parse(Path, SceneryIncludes.Inline(WithIncludes, Files));
var log = TestLog.Over(files);
var car = scenery.Trainsets[1];
car.Vehicles.Clear();
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, new SaveSceneryTrainset(files, log).Execute(scenery, car).Outcome);
Assert.Equal("trainset none road1 -20 50\r\nendtrainset\r\n// koniec\r\n",
Cp1250.GetString(files.ReadAllBytes("scenery/tomaszewo/auta.scm")));
Assert.Equal(WithIncludes, Cp1250.GetString(files.ReadAllBytes(Path)));
Assert.True(files.FileExists("scenery/tomaszewo/auta.scm" + SaveSceneryTrainset.BackupSuffix));
}
[Fact]
public void A_utf8_file_stays_utf8_with_its_mark()
{
var (files, scenery, save) = Setup(new byte[] { 0xEF, 0xBB, 0xBF }.Concat(Encoding.UTF8.GetBytes(Scn)).ToArray());
scenery.Trainsets[0].Vehicles.RemoveAt(1);
save.Execute(scenery, scenery.Trainsets[0]);
byte[] bytes = files.ReadAllBytes(Path);
Assert.Equal(new byte[] { 0xEF, 0xBB, 0xBF }, bytes[..3]);
Assert.Contains("żółw", Encoding.UTF8.GetString(bytes[3..]));
}
private const string V1 = "node -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic";
private const string V2 = "node -1 0 v2 dynamic d s m 0 nobody 3 0 enddynamic";
private const string V3 = "node -1 0 v3 dynamic d s m 0 nobody 3 0 enddynamic";
private static string SavedText(string scn, Action<Trainset>? change = null)
{
var (files, scenery, save) = Setup(scn);
change?.Invoke(scenery.Trainsets[0]);
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, save.Execute(scenery, scenery.Trainsets[0]).Outcome);
return Cp1250.GetString(files.ReadAllBytes(Path));
}
private static IEnumerable<string> VehicleNames(string scn) =>
TestScenery.Parser().Parse(Path, scn).Trainsets.Single().Vehicles.Select(v => v.Name);
private static void Edit(Trainset trainset) => trainset.Vehicles[0].Coupling.Flags = 5;
[Fact]
public void A_vehicle_commented_out_stays_in_its_comment()
{
string scn = $"FirstInit\ntrainset a t 0 0\n/*\n{V1}\n*/\n{V2}\nendtrainset\n" +
"node -1 0 poza dynamic d s m none 0 nobody 0 0 enddynamic\n";
Assert.Equal(scn, SavedText(scn));
string edited = SavedText(scn, Edit);
Assert.Contains($"/*\n{V1}\n*/\n", edited);
var scenery = TestScenery.Parser().Parse(Path, edited);
Assert.Equal(new[] { "v2" }, scenery.Trainsets.Single().Vehicles.Select(v => v.Name));
Assert.Equal(new[] { "poza" }, scenery.LooseVehicleNames);
}
[Theory]
[InlineData("trainset a t 0 0\nNode -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\nendtrainset\n")]
[InlineData("trainset a t 0 0\nNODE -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\nendtrainset\n")]
[InlineData("trainset a t 0 0 node -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\nendtrainset\n")]
[InlineData("trainset a t 0 0 /*x*/node -1 0 v1 dynamic d s m 0 nobody 3 0 enddynamic\nendtrainset\n")]
public void A_vehicle_is_found_however_it_is_written_and_not_written_twice(string scn)
{
Assert.Equal(scn, SavedText(scn));
string edited = SavedText(scn, Edit);
var vehicle = Assert.Single(TestScenery.Parser().Parse(Path, edited).Trainsets.Single().Vehicles);
Assert.Equal(5, vehicle.Coupling.Flags);
}
[Fact]
public void A_vehicle_named_as_a_keyword_is_no_keyword()
{
string scn = "trainset node node 0 0\nnode -1 0 node dynamic d s m 0 nobody 3 0 enddynamic\nendtrainset\n";
Assert.Equal(scn, SavedText(scn));
Assert.Contains("trainset node node 0 0.1\n", SavedText(scn, t => t.ReadyToGo = true));
}
private const string Commented =
"trainset a t 0 0\n" +
V1 + "\n" +
"// o v2\n" +
V2 + " // przy v2\n" +
"/* o v3 */\n" +
V3 + "\n" +
"// koniec\n" +
"EndTrainset\n";
[Fact]
public void A_save_with_no_change_leaves_the_vehicles_and_comments_as_they_were()
{
Assert.Equal(Commented, SavedText(Commented));
// Nor is anything but the speed touched when only the speed changes.
Assert.Equal(Commented.Replace("t 0 0\n", "t 0 0.1\n"), SavedText(Commented, t => t.ReadyToGo = true));
}
[Fact]
public void Comments_stay_where_they_are_when_a_vehicle_is_edited()
{
string edited = SavedText(Commented, t => t.Vehicles[1].Coupling.Flags = 5);
Assert.Equal(Commented.Replace(V2, V2.Replace("nobody 3", "nobody 5")), edited);
}
[Fact]
public void Comments_stay_when_a_vehicle_is_removed()
{
Assert.Equal(
"trainset a t 0 0\n" + V1 + "\n// o v2\n// przy v2\n/* o v3 */\n" + V3 + "\n// koniec\nEndTrainset\n",
SavedText(Commented, t => t.Vehicles.RemoveAt(1)));
Assert.Equal(
"trainset a t 0 0\n// o v2\n// przy v2\n/* o v3 */\n// koniec\nEndTrainset\n",
SavedText(Commented, t => t.Vehicles.Clear()));
}
[Fact]
public void Comments_stay_when_a_vehicle_is_added()
{
var added = new Dynamic { Name = "nowy", DataFolder = "d", SkinFile = "s", MmdFile = "m" };
Assert.Equal(
Commented.Replace(V3, added.ToTrainsetNode() + V3),
SavedText(Commented, t => t.Vehicles.Insert(2, added)));
Assert.Equal(
Commented.Replace("EndTrainset", added.ToTrainsetNode() + "EndTrainset"),
SavedText(Commented, t => t.Vehicles.Add(added)));
}
[Fact]
public void Comments_stay_when_vehicles_change_places_and_a_moved_one_stays_as_written()
{
string moved = "node 500 0 v3 dynamic d s m /*w srodku*/ 0 nobody 3 0 enddynamic";
string scn = Commented.Replace(V3, moved);
string saved = SavedText(scn, t => t.Vehicles.Reverse());
Assert.Equal(new[] { "v3", "v2", "v1" }, VehicleNames(saved));
foreach (string comment in new[] { "// o v2\n", " // przy v2\n", "/* o v3 */\n", "// koniec\n" })
Assert.Contains(comment, saved);
Assert.Contains(moved, saved);
}
[Theory]
[InlineData("trainset a /*tor*/ t 100 0\nendtrainset\n", "trainset a /*tor*/ t 100 0.1\nendtrainset\n")]
[InlineData("trainset a t /*x*/ 100/*y*/ 0.0 reversed\nendtrainset\n", "trainset a t /*x*/ 100/*y*/ 0.1 reversed\nendtrainset\n")]
[InlineData("Trainset a // tor\n t\n 100\n 0.0\nendtrainset\n", "Trainset a // tor\n t\n 100\n 0.1\nendtrainset\n")]
public void The_speed_is_found_past_a_comment_in_the_header(string scn, string expected)
{
Assert.Equal(expected, SavedText(scn, t => t.ReadyToGo = true));
}
[Fact]
public void An_optional_include_after_first_init_stays_an_option()
{
const string scn = "FirstInit\ninclude zima.inc end //$optional 1, Zima\nx\nendoptional\n";
string content = $"trainset a t 0 0\n{V1}\nendtrainset\n";
string text = SceneryIncludes.Inline(scn, name => (name, content));
Assert.Equal(scn, text);
var scenery = TestScenery.Parser().Parse(Path, text);
Assert.Equal("Zima", Assert.Single(scenery.Includes).Desc);
Assert.Empty(scenery.Trainsets);
}
/// <summary>A disk that gives out on whatever is being written; what it does to the file is <see cref="PhysicalFileSystemTests"/>' business.</summary>
public class FailingDisk : DispatchProxy
{
public InMemoryFileSystem Files = null!;
protected override object? Invoke(MethodInfo? method, object?[]? args) =>
method!.Name == nameof(IFileSystem.WriteAllBytesAtomic)
? throw new IOException("the disk gave out")
: method.Invoke(Files, args);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void A_write_that_fails_leaves_the_file_and_its_backup_whole(bool backedUp)
{
var (files, scenery, _) = Setup(Scn);
string backup = Path + SaveSceneryTrainset.BackupSuffix;
if (backedUp)
files.WithLegacyFile(backup, Scn);
var disk = DispatchProxy.Create<IFileSystem, FailingDisk>();
((FailingDisk)(object)disk).Files = files;
var log = TestLog.Over();
var trainset = scenery.Trainsets[0];
string before = trainset.RawEntry;
trainset.Vehicles.RemoveAt(1);
var (outcome, detail) = new SaveSceneryTrainset(disk, log).Execute(scenery, trainset);
Assert.Equal((SceneryTrainsetSaveOutcome.Failed, "the disk gave out"), (outcome, detail));
Assert.Equal(Scn, Cp1250.GetString(files.ReadAllBytes(Path)));
Assert.Equal(backedUp, files.FileExists(backup));
Assert.Equal(new[] { Path }.Concat(backedUp ? new[] { backup } : Array.Empty<string>()),
files.GetFiles("scenery"));
Assert.Equal(before, trainset.RawEntry);
}
private static (string Scn, SaveSceneryTrainset Save, Scenery Scenery, string Folder) OnDisk(string text)
{
string folder = Directory.CreateTempSubdirectory("starter-save-").FullName;
string scn = System.IO.Path.Combine(folder, "test.scn");
var files = new PhysicalFileSystem();
files.WriteAllText(scn, text, LegacyText.CodePage1250);
var log = TestLog.Over();
return (scn, new SaveSceneryTrainset(files, log), TestScenery.Parser().Parse(scn, text), folder);
}
[Fact]
public void On_disk_the_new_text_is_written_beside_the_file_and_moved_over_it()
{
var (scn, save, scenery, folder) = OnDisk(Scn);
try
{
var trainset = scenery.Trainsets[0];
trainset.Vehicles.RemoveAt(1);
// Nothing can be written beside it: the file is not touched either.
string temp = scn + PhysicalFileSystem.TempSuffix;
Directory.CreateDirectory(temp);
Assert.Equal(SceneryTrainsetSaveOutcome.Failed, save.Execute(scenery, trainset).Outcome);
Assert.Equal(Scn, Cp1250.GetString(File.ReadAllBytes(scn)));
Assert.True(File.Exists(scn + SaveSceneryTrainset.BackupSuffix));
Directory.Delete(temp);
Assert.Equal((SceneryTrainsetSaveOutcome.Saved, (string?)null), save.Execute(scenery, trainset));
Assert.DoesNotContain("wagon-1", Cp1250.GetString(File.ReadAllBytes(scn)));
Assert.Equal(Scn, Cp1250.GetString(File.ReadAllBytes(scn + SaveSceneryTrainset.BackupSuffix)));
Assert.Equal(
new[] { "test.scn", "test.scn" + SaveSceneryTrainset.BackupSuffix },
Directory.GetFiles(folder, "test.scn*").Select(file => System.IO.Path.GetFileName(file)).Order());
}
finally
{
Directory.Delete(folder, recursive: true);
}
}
[Fact]
public void On_disk_a_file_that_cannot_be_replaced_stays_and_nothing_is_left_beside_it()
{
// Only Windows keeps a file in use from being moved over.
if (!OperatingSystem.IsWindows())
return;
var (scn, save, scenery, folder) = OnDisk(Scn);
try
{
scenery.Trainsets[0].Vehicles.RemoveAt(1);
using (new FileStream(scn, FileMode.Open, FileAccess.Read, FileShare.Read))
Assert.Equal(SceneryTrainsetSaveOutcome.Failed, save.Execute(scenery, scenery.Trainsets[0]).Outcome);
Assert.Equal(Scn, Cp1250.GetString(File.ReadAllBytes(scn)));
Assert.False(File.Exists(scn + PhysicalFileSystem.TempSuffix));
}
finally
{
Directory.Delete(folder, recursive: true);
}
}
[Fact]
public void On_disk_a_link_stays_a_link_and_its_file_gets_the_text()
{
var (scn, save, scenery, folder) = OnDisk(Scn);
try
{
string target = scn + ".wlasciwy";
File.Move(scn, target);
try
{
File.CreateSymbolicLink(scn, target);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Windows lets only some users make links.
return;
}
scenery.Trainsets[0].Vehicles.RemoveAt(1);
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, save.Execute(scenery, scenery.Trainsets[0]).Outcome);
Assert.NotNull(new FileInfo(scn).LinkTarget);
Assert.DoesNotContain("wagon-1", Cp1250.GetString(File.ReadAllBytes(target)));
}
finally
{
Directory.Delete(folder, recursive: true);
}
}
private static byte[] Bytes(params object[] parts) =>
parts.SelectMany(part => part as byte[] ?? Encoding.ASCII.GetBytes((string)part)).ToArray();
[Fact]
public void Bytes_behind_a_mark_that_are_not_utf8_stay_as_they_were()
{
byte[] mark = { 0xEF, 0xBB, 0xBF };
byte[] polish = { 0xB3, 0xF3, 0xBF }; // "łóż" in code page 1250
string trainset = $"trainset a t 0 0\n{V1}\nendtrainset\n";
var (files, scenery, save) = Setup(Bytes(mark, "// ", polish, "\n", trainset, "// ", polish, "\n"));
scenery.Trainsets[0].Vehicles.Clear();
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, save.Execute(scenery, scenery.Trainsets[0]).Outcome);
Assert.Equal(Bytes(mark, "// ", polish, "\ntrainset a t 0 0\nendtrainset\n// ", polish, "\n"), files.ReadAllBytes(Path));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Every_byte_outside_the_vehicles_stays_as_it_was(bool marked)
{
byte[] mark = marked ? new byte[] { 0xEF, 0xBB, 0xBF } : Array.Empty<byte>();
byte[] all = Enumerable.Range(0, 256).Select(b => (byte)b).ToArray();
string trainset = $"trainset a t 0 0\n{V1}\nendtrainset\n";
var (files, scenery, save) = Setup(Bytes(mark, trainset, all));
scenery.Trainsets[0].Vehicles.Clear();
Assert.Equal(SceneryTrainsetSaveOutcome.Saved, save.Execute(scenery, scenery.Trainsets[0]).Outcome);
Assert.Equal(Bytes(mark, "trainset a t 0 0\nendtrainset\n", all), files.ReadAllBytes(Path));
}
[Theory]
// Its own line ends, in a file with the other kind elsewhere.
[InlineData("// x\r\ntrainset a t 0 0\n" + V1 + "\nendtrainset\n// y\r\n")]
// Both kinds in the trainset.
[InlineData("trainset a t 0 0\r\n// x\n" + V1 + "\r\nendtrainset\r\n")]
public void Mixed_line_ends_stay_as_they_were(string scn)
{
Assert.Equal(scn, SavedText(scn));
}
[Fact]
public void What_the_starter_writes_takes_the_line_ends_of_the_trainset()
{
string scn = "// x\r\ntrainset a t 0 0\n" + V1 + "\nendtrainset\n// y\r\n";
Assert.Equal(
"// x\r\ntrainset a t 0 0\n" + V2 + "\nendtrainset\n// y\r\n",
SavedText(scn, t => t.Vehicles[0].Name = "v2"));
}
}

View File

@@ -1,13 +1,10 @@
using StarterNG.Domain.Sceneries;
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SceneryWeatherTests
{
private static SceneryParser Parser(DateTime now) => new(new FixedClock(now), new StubRandom());
[Fact]
public void Authored_weather_is_read_from_the_config_block()
{
@@ -21,7 +18,7 @@ public class SceneryWeatherTests
time 8:30 0 0 endtime
""";
var weather = Parser(new DateTime(2026, 1, 1)).Parse("scenery/x.scn", scn).Weather;
var weather = TestScenery.Parser(new DateTime(2026, 1, 1)).Parse("scenery/x.scn", scn).Weather;
Assert.True(weather.IsAuthored);
Assert.Equal(172, weather.Day);
@@ -35,7 +32,7 @@ public class SceneryWeatherTests
[Fact]
public void A_scenery_without_a_time_starts_at_the_players_clock()
{
var weather = Parser(new DateTime(2026, 5, 4, 17, 42, 0)).Parse("scenery/x.scn", "//$n Test").Weather;
var weather = TestScenery.Parser(new DateTime(2026, 5, 4, 17, 42, 0)).Parse("scenery/x.scn", "//$n Test").Weather;
Assert.Equal("17:42", weather.ScenarioTimeOverride);
Assert.False(weather.IsAuthored);
@@ -44,7 +41,7 @@ public class SceneryWeatherTests
[Fact]
public void Freshly_parsed_weather_counts_as_unchanged()
{
var weather = Parser(new DateTime(2026, 1, 1)).Parse("scenery/x.scn", "movelight 100\n").Weather;
var weather = TestScenery.Parser(new DateTime(2026, 1, 1)).Parse("scenery/x.scn", "movelight 100\n").Weather;
Assert.False(weather.Changed);
}
@@ -52,7 +49,7 @@ public class SceneryWeatherTests
[Fact]
public void Editing_then_restoring_returns_to_the_authored_values()
{
var weather = Parser(new DateTime(2026, 1, 1)).Parse("scenery/x.scn", "movelight 100\n").Weather;
var weather = TestScenery.Parser(new DateTime(2026, 1, 1)).Parse("scenery/x.scn", "movelight 100\n").Weather;
weather.Day = 5;
weather.Temperature = -12;
@@ -84,4 +81,168 @@ public class SceneryWeatherTests
Assert.Equal(-1.5, weather.EffectiveOvercast(new StubRandom()));
Assert.Equal(0.42, new SceneryWeather { Overcast = 0.42 }.EffectiveOvercast(new StubRandom()));
}
private static string Rewritten(string scn) =>
SceneryWeatherWriter.Rewrite(scn, new SceneryWeather(), new StubRandom());
private static string Code(string text) => SceneryCode.Mask(text, out _);
/// <summary>The block the writer puts at the top of the text, and the rest.</summary>
private static (string Written, string Others) Split(string rewritten)
{
const string end = "endtime\r\n";
int at = rewritten.IndexOf(end, StringComparison.Ordinal) + end.Length;
return (rewritten[..at], rewritten[at..]);
}
[Fact]
public void A_config_commented_out_stays_off_and_the_one_after_it_is_kept()
{
// As linia61_osobowy2.scn has them.
const string commented = "//config pause yes endconfig //yes: zatrzymanie symulacji zaraz po wczytaniu, jeśli start jako jadący";
string scn = $"""
sky skydome_clouds.t3d endsky
atmo 0 0 0 1000 2000 0.341 0.408 0.471 -1.2 endatmo
time 6:43 06:07 21:02 endtime //w Lublińcu 7:30
{commented}
config hiddenevents 1 endconfig
include linia61/czestochowa.scm end
""";
var (written, rest) = Split(Rewritten(scn));
Assert.Contains("hiddenevents 1\r\n", written);
Assert.DoesNotContain("pause", written);
Assert.Contains(commented, rest);
Assert.Contains("//w Lublińcu 7:30", rest);
Assert.Contains("sky skydome_clouds.t3d endsky", rest);
Assert.Contains("include linia61/czestochowa.scm end", rest);
Assert.DoesNotContain("hiddenevents", rest);
foreach (string word in new[] { "atmo", "time", "pause" })
Assert.DoesNotContain(word, Code(rest));
}
[Fact]
public void A_keyword_in_a_comment_does_not_cut_up_to_the_next_block()
{
const string scn = """
// atmo: tu byla mgla
include a.scm end
{{0}}
/* config */
include b.scm end
{{1}}
// time 8 tu nie
include c.scm end
atmo 0 0 0 10 2000 0 0 0 0 endatmo
config hiddenevents 1 endconfig
time 8:30 0 0 endtime
""";
var (written, rest) = Split(Rewritten(scn));
Assert.Contains("hiddenevents 1", written);
foreach (string kept in new[]
{
"// atmo: tu byla mgla", "include a.scm end", "{{0}}", "/* config */",
"include b.scm end", "{{1}}", "// time 8 tu nie", "include c.scm end"
})
Assert.Contains(kept, rest);
foreach (string gone in new[] { "endatmo", "endconfig", "endtime" })
Assert.DoesNotContain(gone, rest);
}
[Theory]
[InlineData("/* config a 1 endconfig */")]
[InlineData("// config a 1 endconfig")]
[InlineData("/* atmo 0 0 0 1 2 0 0 0 0 endatmo */")]
[InlineData("// atmo 0 0 0 1 2 0 0 0 0 endatmo")]
[InlineData("/* time 8:30 0 0 endtime */")]
[InlineData("// time 8:30 0 0 endtime")]
[InlineData("// movelight 100")]
[InlineData("/*\nmovelight 100\n*/")]
[InlineData("// scenario.weather.temperature 5")]
[InlineData("/* scenario.time.override 6:00 */")]
public void What_is_commented_out_stays_as_it_is(string comment)
{
var (written, rest) = Split(Rewritten($"x\n{comment}\ny\n"));
Assert.Equal($"x\n{comment}\ny\n", rest);
Assert.DoesNotContain("a 1", written);
}
[Fact]
public void A_comment_in_a_block_stays_when_the_block_goes()
{
const string scn = "atmo 0 0 0 // mgla\n10 2000 0 0 0 /* zachmurzenie */ 0 endatmo\n" +
"config\n// uwaga\nmovelight 5 // lato\nhiddenevents 1\nendconfig\ninclude a.scm end\n";
var (written, rest) = Split(Rewritten(scn));
foreach (string comment in new[] { "// mgla\n", "/* zachmurzenie */", "// uwaga\n", "// lato\n" })
Assert.Contains(comment, rest);
foreach (string word in new[] { "atmo", "2000", "config", "movelight", "hiddenevents" })
Assert.DoesNotContain(word, Code(rest));
Assert.Contains("hiddenevents 1\r\n", written);
Assert.Contains("include a.scm end", rest);
}
[Fact]
public void Settings_of_every_config_are_carried_and_only_the_starters_own_are_replaced()
{
const string scn = "config movelight 5 hiddenevents 1 endconfig\nCONFIG\nscenario.weather.temperature 3 pause no\nENDCONFIG\n";
var (written, rest) = Split(Rewritten(scn));
Assert.Contains("hiddenevents 1\r\n", written);
Assert.Contains("pause no\r\n", written);
Assert.DoesNotContain("movelight 5", written);
Assert.DoesNotContain("temperature 3", written);
Assert.Equal("", rest.Trim());
}
[Fact]
public void A_word_in_an_include_s_path_is_no_keyword()
{
const string scn = """
include atmo/lasy.scm end
include time/8.scm end
include config/a.scm end
atmo 0 0 0 10 2000 0 0 0 0 endatmo
config hiddenevents 1 endconfig
""";
var (_, rest) = Split(Rewritten(scn));
Assert.Contains("include atmo/lasy.scm end", rest);
Assert.Contains("include time/8.scm end", rest);
Assert.Contains("include config/a.scm end", rest);
Assert.DoesNotContain("endatmo", rest);
}
[Fact]
public void Weather_in_comments_is_not_read()
{
const string scn = """
// movelight 7
// scenario.weather.temperature 40
// scenario.time.override 3:33
/* time 4:44 0 0 endtime */
// atmo 5 mgla w l61
include l61/a.scm end
movelight 100
atmo 0 0 0 50 900 0 0 0 0.7 endatmo
""";
var weather = TestScenery.Parser(new DateTime(2026, 5, 4, 17, 42, 0)).Parse("scenery/x.scn", scn).Weather;
Assert.Equal(100, weather.Day);
Assert.NotEqual(40, weather.Temperature);
Assert.Equal("17:42", weather.ScenarioTimeOverride);
Assert.Equal(50, weather.FogStart);
Assert.Equal(0.7, weather.Overcast);
}
}