aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Runs/Scripts/ScriptParserTests.cs
blob: 352ff03aa335c2b4d9ed8c998378472ab61a9998 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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<VerbNode>(f.Body[0]);
        Assert.Equal(("click", "10,20", 6), (v.Verb, v.Args, v.At.Line));
        var w = Assert.IsType<WaitNode>(f.Body[1]);
        Assert.Equal(("dialog-open", 120), (w.Condition, w.Timeout));
        Assert.Equal("quest(1) >= accepted", Assert.IsType<ExpectNode>(f.Body[2]).Condition);
        var s = Assert.IsType<SetNode>(f.Body[3]);
        Assert.Equal(("x", "a b"), (s.Name, s.Value));
        Assert.Equal("ch/one.steps", Assert.IsType<IncludeNode>(f.Body[4]).Path);
        var c = Assert.IsType<CallNode>(f.Body[5]);
        Assert.Equal("rest", c.Name);
        Assert.Equal(["8", "inn"], c.Args);
        Assert.Equal("done", Assert.IsType<CheckpointNode>(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<BlockNode>(f.Body[0]);
        Assert.Equal((BlockKind.Setup, 1), (setup.Kind, setup.Body.Count));
        var step = Assert.IsType<BlockNode>(f.Body[1]);
        Assert.Equal((BlockKind.Step, "talk-virgil", 2), (step.Kind, step.Label, step.Body.Count));
        Assert.Equal(BlockKind.Try, Assert.IsType<BlockNode>(step.Body[1]).Kind);
        var def = Assert.IsType<DefNode>(f.Body[2]);
        Assert.Equal(("rest", 2), (def.Name, def.Body.Count));
        Assert.Equal(["hours"], def.Params);
        Assert.Equal("Q", Assert.IsType<VerbNode>(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<BlockNode>(lf.Body[0]);
        var b = Assert.IsType<BlockNode>(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);
    }
}