Files
StarterNG/StarterNG.Tests/ScenerySyntaxTests.cs
maj00r 69aa075bfa 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.
2026-11-01 16:06:15 +01:00

383 lines
15 KiB
C#

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