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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
using Godosa.Core.Runs;
using Godosa.Core.Runs.Scripts;
using Xunit;
using static Godosa.Core.Tests.Runs.Scripts.ScriptRunnerTests;
namespace Godosa.Core.Tests.Runs.Scripts;
/// <summary>Final-review findings (lib core): each test failed before its fix.</summary>
public sealed class ReviewFixTests : IDisposable
{
private readonly string _dir = Path.Combine(AppContext.BaseDirectory, "rs-tmp", Guid.NewGuid().ToString("N"));
public void Dispose()
{
if (Directory.Exists(_dir))
Directory.Delete(_dir, recursive: true);
}
private const string Script = "format 1\nmode player\nkey A\ncheckpoint one\nkey B\ncheckpoint two\nkey C";
[Fact]
public void NoArgVerb_AndEmptySet_Run()
{
var g = new FakeGame();
g.ActionTable["close-panel"] = new FakeAction(VerbKind.Player, _ => new ActionStart(Status: StepStatus.Done), min: 0, max: 0);
var (r, output) = Run("format 1\nmode player\nset x =\nclose-panel\nlog \"[${x}]\"", g);
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.Contains("[]", output);
}
// A probe value (dialog text) with quotes inside a quoted argument stays text: the line is split into tokens first.
[Fact]
public void QuotedArgument_ValueWithQuotes_StaysOneToken()
{
var (r, output) = Run("format 1\nmode player\nset x = \"\\\"Hi,\\\" he said\"\nlog \"R: ${x}\"", new FakeGame());
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.Contains("R: \"Hi,\" he said", output);
}
[Fact]
public void SubstitutedValues_AreNotRescanned()
{
var values = new Dictionary<string, string> { ["d"] = "$", ["e"] = "{x}", ["raw"] = "${y}" };
Assert.Equal("${x}", VarExpander.Expand("${d}${e}", n => values.GetValueOrDefault(n), null));
Assert.Equal("say ${y}!", VarExpander.Expand("say ${raw}!", n => values.GetValueOrDefault(n), null));
}
[Theory]
[InlineData("log Done:", "log", "Done:")]
[InlineData("type name:", "type", "name:")]
public void LineEndingInColon_IsAVerbUnlessABlockKeyword(string line, string verb, string args)
{
var f = ScriptParser.Parse("main.steps", $"format 1\nmode debug\n{line}");
Assert.Empty(f.Errors);
var v = Assert.IsType<VerbNode>(f.Body.Single());
Assert.Equal((verb, args), (v.Verb, v.Args));
}
[Fact]
public void ThrowingInputRouter_FailsTheRun()
{
var g = new FakeGame { OnSend = _ => throw new InvalidOperationException("router down") };
var (r, output) = Run("format 1\nmode player\nkey R", g);
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(output, l => l == "FAIL main.steps:3 key R: InvalidOperationException: router down");
}
[Fact]
public void CorruptCacheEntry_IsAMiss()
{
Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir });
foreach (var f in Directory.GetFiles(_dir, "*.snap"))
File.WriteAllBytes(f, [.. "GRS1"u8, 3, 0, 0, 0, (byte)'{', (byte)'x', (byte)'!']);
var (r, output) = Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir });
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.DoesNotContain(output, l => l.StartsWith("resumed"));
}
// A game state the snapshot can't hold (an open dialog) → checkpoint not cached, run unharmed.
[Fact]
public void RefusedSnapshot_CheckpointNotCached()
{
var g = new FakeGame { Refusal = "dialog open" };
var output = new List<string>();
var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string>
{
["main.steps"] = Script,
}), new Dictionary<string, string>()), g, new ScriptRunOptions { CacheDir = _dir }, output.Add);
Assert.Equal(RunOutcome.Passed, runner.Run(1000).Outcome);
Assert.Equal(["checkpoint one not cached: dialog open", "checkpoint two not cached: dialog open"], runner.Report!.Warnings);
Assert.False(Directory.Exists(_dir) && Directory.GetFiles(_dir, "*.snap", SearchOption.AllDirectories).Length > 0);
}
[Fact]
public void FailingRestore_FailsTheRun()
{
Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir });
var (r, output) = Run(Script, new FakeGame { RestoreThrows = true }, new ScriptRunOptions { CacheDir = _dir });
Assert.Equal((RunOutcome.Failed, 1), (r.Outcome, r.ExitCode));
Assert.Contains(output, l => l.Contains("IOException: restore broke"));
}
[Fact]
public void UnwritableCacheAndReportDirs_WarnInsteadOfThrowing()
{
Directory.CreateDirectory(_dir);
var file = Path.Combine(_dir, "file");
File.WriteAllText(file, "x");
var g = new FakeGame();
g.ProbeFns["no"] = _ => RunValue.Of(false);
var output = new List<string>();
var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string>
{
["main.steps"] = "format 1\nmode player\ncheckpoint one\nexpect no",
}), new Dictionary<string, string>()), g, new ScriptRunOptions { CacheDir = Path.Combine(file, "cache"), ReportDir = Path.Combine(file, "report") }, output.Add);
var r = runner.Run(1000);
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(runner.Report!.Warnings, w => w.StartsWith("checkpoint cache not written:"));
Assert.Contains(output, l => l.StartsWith("warning: report not written:"));
}
[Fact]
public void Until_UnknownName_Invalid()
{
var (r, output) = Run(Script, new FakeGame(), new ScriptRunOptions { Until = "nowhere" });
Assert.Equal((RunOutcome.Invalid, 2), (r.Outcome, r.ExitCode));
Assert.Contains("unknown label or checkpoint nowhere", output);
}
[Fact]
public void Until_ResumesOnlyBeforeIt()
{
Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir }); // caches one and two
var output = new List<string>();
var r = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = Script }),
new Dictionary<string, string>()), new FakeGame(), new ScriptRunOptions { CacheDir = _dir, Until = "two" }, output.Add).Run(1000);
Assert.Contains("resumed at one (cache) f=2", output);
Assert.Equal((RunOutcome.Stopped, 4L), (r.Outcome, r.Frames));
}
[Fact]
public void ScriptSeedHeader_SeedsTheGame_CommandLineWins()
{
var g = new FakeGame();
var output = new List<string>();
var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }),
new Dictionary<string, string>()), g, new ScriptRunOptions(), output.Add);
runner.Run(1000);
Assert.Equal(["seed 7"], g.RngImpl!.Calls);
Assert.Equal(7UL, runner.Report!.Seed);
var g2 = new FakeGame();
var runner2 = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }),
new Dictionary<string, string>()), g2, new ScriptRunOptions { Seed = 9 }, output.Add);
runner2.Run(1000);
Assert.Equal(["seed 9"], g2.RngImpl!.Calls);
}
}
|