using Godosa.Core.Runs.Scripts; using Xunit; namespace Godosa.Core.Tests.Runs.Scripts; public class ScriptLoaderTests { private static ScriptProgram Load(Dictionary files, Dictionary? 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("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); }