aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs
blob: f28f33075d0583957b2cee54065aadd90a6da33b (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
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);
}