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);
}
|