using Godosa.Core.Runs.Scripts; using Xunit; namespace Godosa.Core.Tests.Runs.Scripts; public class ScriptParserTests { private static ScriptFile Parse(string text) => ScriptParser.Parse("main.steps", text); private static string Errors(ScriptFile f) => string.Join(" | ", f.Errors.Select(e => e.ToString())); [Fact] public void Header_AndStatements() { var f = Parse(""" # comment format 1 mode player seed 7 vars ui/${res}.vars click 10,20 wait dialog-open 2s expect quest(1) >= accepted set x = "a b" include ch/one.steps call rest(8, inn) checkpoint done """); Assert.Equal("", Errors(f)); Assert.Equal((1, RunMode.Player, 7UL), (f.Format, f.Mode, f.Seed!.Value)); Assert.Equal(["ui/${res}.vars"], f.VarFiles); var v = Assert.IsType(f.Body[0]); Assert.Equal(("click", "10,20", 6), (v.Verb, v.Args, v.At.Line)); var w = Assert.IsType(f.Body[1]); Assert.Equal(("dialog-open", 120), (w.Condition, w.Timeout)); Assert.Equal("quest(1) >= accepted", Assert.IsType(f.Body[2]).Condition); var s = Assert.IsType(f.Body[3]); Assert.Equal(("x", "a b"), (s.Name, s.Value)); Assert.Equal("ch/one.steps", Assert.IsType(f.Body[4]).Path); var c = Assert.IsType(f.Body[5]); Assert.Equal("rest", c.Name); Assert.Equal(["8", "inn"], c.Args); Assert.Equal("done", Assert.IsType(f.Body[6]).Name); } [Fact] public void Blocks_NestByIndent() { var f = Parse(""" format 1 mode debug setup: cheat gold 5 step talk-virgil: click 1,2 try: key R def rest(hours): key R wait-frames ${hours} key Q """); Assert.Equal("", Errors(f)); var setup = Assert.IsType(f.Body[0]); Assert.Equal((BlockKind.Setup, 1), (setup.Kind, setup.Body.Count)); var step = Assert.IsType(f.Body[1]); Assert.Equal((BlockKind.Step, "talk-virgil", 2), (step.Kind, step.Label, step.Body.Count)); Assert.Equal(BlockKind.Try, Assert.IsType(step.Body[1]).Kind); var def = Assert.IsType(f.Body[2]); Assert.Equal(("rest", 2), (def.Name, def.Body.Count)); Assert.Equal(["hours"], def.Params); Assert.Equal("Q", Assert.IsType(f.Body[3]).Args); } [Theory] [InlineData("mode player\nclick 1,1", "main.steps:1: first line must be 'format N'")] [InlineData("format 9\nmode player", "main.steps:1: unknown format 9 (latest 1)")] [InlineData("format 1\nmode sneaky", "main.steps:2: mode must be player or debug")] [InlineData("format 1\nmode debug\nclick 1,1\nseed 3", "main.steps:4: header line after steps")] [InlineData("format 1\nmode debug\nwait dialog-open 0.01s", "main.steps:3: bad timeout '0.01s'")] [InlineData("format 1\nmode debug\nwait 2s", "main.steps:3: wait needs a condition and a timeout")] [InlineData("format 1\nmode debug\nstep a:\n click 1,1", "main.steps:4: indent not a multiple of 2")] [InlineData("format 1\nmode debug\nstep a:\n\tclick 1,1", "main.steps:4: tab in indentation")] [InlineData("format 1\nmode debug\nstep a:\nclick 1,1", "main.steps:3: empty block")] [InlineData("format 1\nmode debug\nloop:\n click 1,1", "main.steps:3: unknown block 'loop:'")] [InlineData("format 1\nmode debug\nclick 1,1\n key R", "main.steps:4: unexpected indent")] [InlineData("format 1\nmode debug\nlog \"abc", "main.steps:3: unterminated quote")] [InlineData("format 1\nmode debug\nset 9x = 1", "main.steps:3: bad set (set name = value)")] public void Errors_NameTheLine(string text, string error) => Assert.Contains(error, Errors(Parse(text))); // Review focus 5: CRLF, BOM and trailing spaces parse as LF. [Fact] public void CrlfBomTrailingSpaces_SameAsLf() { var lf = Parse("format 1\nmode debug\nstep a:\n click 1,2\n"); var crlf = Parse("\uFEFFformat 1 \r\nmode debug\r\nstep a: \r\n click 1,2 \r\n"); Assert.Equal("", Errors(crlf)); var a = Assert.IsType(lf.Body[0]); var b = Assert.IsType(crlf.Body[0]); Assert.Equal(a.Text, b.Text); Assert.Equal(((VerbNode)a.Body[0]).Text, ((VerbNode)b.Body[0]).Text); Assert.Equal(((VerbNode)a.Body[0]).At, ((VerbNode)b.Body[0]).At); } [Fact] public void ReportsEveryError_NotOnlyTheFirst() { var f = Parse("format 1\nmode debug\nwait x 2m\nset = 1\nloop:\n key R"); Assert.Equal(3, f.Errors.Count); } }