diff options
Diffstat (limited to 'src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs')
| -rw-r--r-- | src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs b/src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs new file mode 100644 index 0000000..f28f330 --- /dev/null +++ b/src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs @@ -0,0 +1,103 @@ +using Godosa.Core.Runs.Scripts; +using Xunit; + +namespace Godosa.Core.Tests.Runs.Scripts; + +public class ScriptLoaderTests +{ + private static ScriptProgram Load(Dictionary<string, string> files, Dictionary<string, string>? vars = null) => + ScriptLoader.Load("main.steps", new MemoryScriptSource(files), vars ?? []); + + private static string Errors(ScriptProgram p) => string.Join(" | ", p.Errors.Select(e => e.ToString())); + + private static string Ops(ScriptProgram p) => string.Join(" ", p.Code.Select(i => i.Op switch + { + Op.Verb => $"{i.A}", Op.ScopeBegin => i.LocalVars ? "{L" : "{", Op.ScopeEnd => "}", Op.LabelBegin => $"[{i.A}", + Op.LabelEnd => "]", Op.TryBegin => "try(", Op.TryEnd => ")", Op.SetupBegin => "setup(", Op.SetupEnd => ")", + _ => i.Op.ToString().ToLowerInvariant(), + })); + + [Fact] + public void Flattens_IncludesDefsBlocks() + { + var p = Load(new() + { + ["main.steps"] = "format 1\nmode player\nsetup:\n cheat gold\ndef rest(h):\n key R\nstep a:\n include ch/one.steps\n call rest(8)\ncheckpoint done", + ["ch/one.steps"] = "format 1\nclick 1,1\ninclude ../common.steps", + ["common.steps"] = "format 1\ntry:\n key Q", + }); + Assert.Equal("", Errors(p)); + Assert.Equal("setup( cheat ) [a { click { try( key ) } } {L key } ] checkpoint", Ops(p)); + var tryBegin = p.Code.Single(i => i.Op == Op.TryBegin); + Assert.Equal(Op.TryEnd, p.Code[tryBegin.Jump].Op); + var q = p.Code.Single(i => i.Op == Op.Verb && i.A == "key" && i.B == "Q"); + Assert.Equal("common.steps:3", q.At.ToString()); + Assert.Equal(["main.steps:8", "ch/one.steps:3"], q.Chain.Select(c => c.ToString())); + var bind = p.Code.Single(i => i.Op == Op.ScopeBegin && i.LocalVars); + Assert.Equal([new KeyValuePair<string, string>("h", "8")], bind.Bindings); + } + + [Fact] + public void VarsFiles_PathExpandedWithCommandLine_FileDoesNotOverrideIt() + { + var p = Load(new() + { + ["main.steps"] = "format 1\nmode player\nvars ui/${res}.vars\nclick ${ui.ok}", + ["ui/hd.vars"] = "ui.ok = 10,20\nres = sd", + }, new() { ["res"] = "hd" }); + Assert.Equal("", Errors(p)); + Assert.Equal(("10,20", "hd"), (p.Vars["ui.ok"], p.Vars["res"])); + Assert.Contains("res", p.Locked); + } + + [Theory] + [InlineData("format 1\nclick 1,1", "main.steps:1: top-level file needs 'mode'")] + [InlineData("format 1\nmode player\ninclude x.steps", "main.steps:3: include not found: x.steps")] + [InlineData("format 1\nmode player\ncall nope()", "main.steps:3: unknown def nope")] + [InlineData("format 1\nmode player\ndef d(a):\n key R\ncall d()", "main.steps:5: def d takes 1 arguments, got 0")] + [InlineData("format 1\nmode player\ndef d():\n call d()\ncall d()", "main.steps:4: recursive call d")] + [InlineData("format 1\nmode player\ndef d():\n key R\ndef d():\n key Q", "main.steps:5: def d already defined")] + [InlineData("format 1\nmode player\ncheckpoint a\ncheckpoint a", "main.steps:4: duplicate checkpoint a")] + [InlineData("format 1\nmode player\nclick ${nowhere}", "main.steps:3: unresolved variable ${nowhere}")] + [InlineData("format 1\nmode player\nkey R\nsetup:\n cheat x", "main.steps:4: setup must be the first block")] + [InlineData("format 1\nmode player\ntry:\n checkpoint c", "main.steps:4: checkpoint inside try")] + public void Reports(string main, string error) => Assert.Contains(error, Errors(Load(new() { ["main.steps"] = main }))); + + [Fact] + public void IncludeCycle_NamesThePath() => + Assert.Contains("b.steps:2: include cycle: main.steps → a.steps → b.steps → a.steps", Errors(Load(new() + { + ["main.steps"] = "format 1\nmode debug\ninclude a.steps", + ["a.steps"] = "format 1\ninclude b.steps", + ["b.steps"] = "format 1\ninclude a.steps", + }))); + + [Fact] + public void IncludedFile_ModeOrSetup_Rejected() + { + var p = Load(new() + { + ["main.steps"] = "format 1\nmode debug\ninclude a.steps", + ["a.steps"] = "format 1\nmode player\nsetup:\n cheat x", + }); + Assert.Contains("a.steps:2: mode only in the top-level file", Errors(p)); + Assert.Contains("a.steps:3: setup only in the top-level file", Errors(p)); + } + + [Fact] + public void LockedVariable_SetIsAnError() => + Assert.Contains("main.steps:3: variable res is set on the command line", + Errors(Load(new() { ["main.steps"] = "format 1\nmode debug\nset res = x" }, new() { ["res"] = "hd" }))); + + [Fact] + public void DefParamsAndEarlierSets_AreKnownVariables() => + Assert.Equal("", Errors(Load(new() + { + ["main.steps"] = "format 1\nmode debug\nset n = 3\ndef d(a):\n key ${a}\n wait-frames ${n}\ncall d(R)", + }))); + + [Fact] + public void IncludedFileKeepsItsOwnFormat() => + Assert.Equal(1, Load(new() { ["main.steps"] = "format 1\nmode debug\ninclude a.steps", ["a.steps"] = "format 1\nkey R" }) + .Code.Single(i => i.Op == Op.Verb).Format); +} |
