aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs')
-rw-r--r--src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs224
1 files changed, 224 insertions, 0 deletions
diff --git a/src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs b/src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs
new file mode 100644
index 0000000..0738f7b
--- /dev/null
+++ b/src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs
@@ -0,0 +1,224 @@
+using Godosa.Core.Runs.Scripts;
+using Xunit;
+
+namespace Godosa.Core.Tests.Runs.Scripts;
+
+public class ScriptRunnerTests
+{
+ internal static (RunResult Result, List<string> Output) Run(string main, FakeGame game, ScriptRunOptions? options = null,
+ Dictionary<string, string>? files = null, Dictionary<string, string>? vars = null)
+ {
+ var all = new Dictionary<string, string>(files ?? []) { ["main.steps"] = main };
+ var program = ScriptLoader.Load("main.steps", new MemoryScriptSource(all), vars ?? []);
+ var output = new List<string>();
+ var runner = new ScriptRunner(program, game, options ?? new ScriptRunOptions(), output.Add);
+ return (runner.Run(cap: 10_000), output);
+ }
+
+ [Fact]
+ public void Click_DownAtStepFrameUpNextFrame()
+ {
+ var g = new FakeGame();
+ var (r, _) = Run("format 1\nmode player\nclick 100,200\nkey R", g);
+ Assert.Equal((RunOutcome.Passed, 0), (r.Outcome, r.ExitCode));
+ // Hand-derived: frame 0 (game.Frame before first Advance) move+down, frame 1 up; key starts on frame 2: down, frame 3 up.
+ Assert.Equal(
+ [
+ (0L, new InputEvent(InputKind.Move, 100, 200)), (0L, new InputEvent(InputKind.Down, 100, 200)),
+ (1L, new InputEvent(InputKind.Up, 100, 200)),
+ (2L, new InputEvent(InputKind.KeyDown, Key: "R")), (3L, new InputEvent(InputKind.KeyUp, Key: "R")),
+ ], g.Sent);
+ Assert.Equal(4, r.Frames);
+ }
+
+ // A window host ends its batch of game frames when a step completes, so the next step resolves on a fresh draw.
+ [Fact]
+ public void StepsDone_CountsCompletedSteps()
+ {
+ var program = ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = "format 1\nmode player\nclick 100,200\nkey R" }), new Dictionary<string, string>());
+ var runner = new ScriptRunner(program, new FakeGame(), new ScriptRunOptions(), _ => { });
+ Assert.Equal(0, runner.StepsDone);
+ runner.Run(cap: 10_000);
+ Assert.Equal(2, runner.StepsDone);
+ }
+
+ // A window host skips the poll of a frame drawn later (a batch frame that closed a panel: a click resolved then
+ // would aim on the draw from before): the frame counts, the step is polled on the next one.
+ [Fact]
+ public void AfterFrame_NoPoll_CountsTheFrameOnly()
+ {
+ var program = ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = "format 1\nmode player\nwait-frames 1" }), new Dictionary<string, string>());
+ var runner = new ScriptRunner(program, new FakeGame(), new ScriptRunOptions(), _ => { });
+ runner.BeforeFrame();
+ runner.AfterFrame(poll: false);
+ Assert.Equal((1, 0), (runner.FrameCount, runner.StepsDone));
+ runner.BeforeFrame();
+ runner.AfterFrame();
+ Assert.Equal((2, 1), (runner.FrameCount, runner.StepsDone));
+ }
+
+ [Fact]
+ public void Locator_AimsTheClick_OffScreenFaults()
+ {
+ var g = new FakeGame();
+ g.Objects["Virgil"] = (640, 360);
+ var (ok, _) = Run("format 1\nmode player\nclick @obj(Virgil)", g);
+ Assert.Equal(RunOutcome.Passed, ok.Outcome);
+ Assert.Contains((0L, new InputEvent(InputKind.Down, 640, 360)), g.Sent);
+ var (bad, output) = Run("format 1\nmode player\nclick @obj(Bates)", new FakeGame());
+ Assert.Equal((RunOutcome.Failed, 1), (bad.Outcome, bad.ExitCode));
+ Assert.Contains(output, l => l == "FAIL main.steps:3 click @obj(Bates): locator @obj(Bates): no object Bates on screen");
+ }
+
+ [Fact]
+ public void Wait_HoldsUntilConditionOrTimesOut()
+ {
+ var g = new FakeGame();
+ g.ProbeFns["ready"] = _ => RunValue.Of(g.Frame >= 5);
+ var (r, _) = Run("format 1\nmode player\nwait ready 10\nkey A", g);
+ Assert.Equal(RunOutcome.Passed, r.Outcome);
+ Assert.Equal(5L, g.Sent[0].Frame); // condition true after the 5th advance → key starts at frame 5
+
+ var g2 = new FakeGame();
+ g2.ProbeFns["ready"] = _ => RunValue.Of(false);
+ g2.ProbeFns["quest"] = _ => RunValue.Of(1);
+ var (t, output) = Run("format 1\nmode player\nwait ready and quest(3) > 1 4", g2);
+ Assert.Equal(RunOutcome.Failed, t.Outcome);
+ Assert.Equal(4, t.Frames);
+ Assert.Contains(output, l => l == "FAIL main.steps:3 wait ready and quest(3) > 1 4: timeout after 4 frames: ready and quest(3) > 1 (ready=0 quest(3)=1)");
+ }
+
+ [Fact]
+ public void Wait_AlreadyTrue_TakesNoFrame()
+ {
+ var g = new FakeGame();
+ g.ProbeFns["ready"] = _ => RunValue.Of(true);
+ var (r, _) = Run("format 1\nmode player\nwait ready 10\nkey A", g);
+ Assert.Equal(0L, g.Sent[0].Frame);
+ Assert.Equal(2, r.Frames);
+ }
+
+ [Fact]
+ public void Expect_FailsWithValues()
+ {
+ var g = new FakeGame();
+ g.ProbeFns["quest"] = _ => RunValue.Of(1);
+ var (r, output) = Run("format 1\nmode player\nexpect quest(1) >= accepted", g);
+ Assert.Equal(RunOutcome.Failed, r.Outcome);
+ Assert.Contains(output, l => l == "FAIL main.steps:3 expect quest(1) >= accepted: expect failed: quest(1) >= accepted (quest(1)=1)");
+ }
+
+ [Fact]
+ public void Action_PlanThenPollUntilDone()
+ {
+ var g = new FakeGame();
+ var talk = new FakeAction(VerbKind.Player, args => new ActionStart(new InputPlan([new PlannedInput(0, new InputEvent(InputKind.Down, 5, 6)), new PlannedInput(1, new InputEvent(InputKind.Up, 5, 6))])),
+ polls => polls >= 3 ? StepStatus.Done : StepStatus.Running, 1, 1);
+ g.ActionTable["talk"] = talk;
+ var (r, _) = Run("format 1\nmode player\ntalk @obj(x)\nkey Z", g);
+ Assert.Equal(RunOutcome.Passed, r.Outcome);
+ Assert.Equal(["@obj(x)"], talk.Begun.Single());
+ // Down sent f0, Up f1; polls run after advances to 2, 3, 4 (Done on the 3rd) → key starts at frame 4.
+ Assert.Equal(4L, g.Sent.Single(s => s.Event.Kind == InputKind.KeyDown).Frame);
+ }
+
+ [Fact]
+ public void Action_ArgsGetVariablesExpanded_LocatorsLeftToTheGame()
+ {
+ var g = new FakeGame();
+ var a = new FakeAction(VerbKind.Debug, _ => new ActionStart(Status: StepStatus.Done), min: 1, max: 3);
+ g.ActionTable["cheat"] = a;
+ Run("format 1\nmode debug\nset n = 50\ncheat gold ${n}", g);
+ Assert.Equal(["gold", "50"], a.Begun.Single());
+ }
+
+ [Fact]
+ public void Action_FaultStatusFailsTheRun()
+ {
+ var g = new FakeGame();
+ g.ActionTable["walk-to"] = new FakeAction(VerbKind.Player, _ => new ActionStart(), polls => polls == 2 ? StepStatus.Fault("stuck at 3,4") : StepStatus.Running, 1, 1);
+ var (r, output) = Run("format 1\nmode player\nwalk-to 3,4", g);
+ Assert.Equal((RunOutcome.Failed, 2L), (r.Outcome, r.Frames));
+ Assert.Contains(output, l => l == "FAIL main.steps:3 walk-to 3,4: stuck at 3,4");
+ }
+
+ // Review focus 2: a hook that never finishes hits the step cap.
+ [Fact]
+ public void StepCap_StopsAHangingAction()
+ {
+ var g = new FakeGame();
+ g.ActionTable["walk-to"] = new FakeAction(VerbKind.Player, _ => new ActionStart(), _ => StepStatus.Running, 1, 1);
+ var (r, output) = Run("format 1\nmode player\nwalk-to 3,4", g, new ScriptRunOptions { StepCap = 50 });
+ Assert.Equal((RunOutcome.Failed, 50L), (r.Outcome, r.Frames));
+ Assert.Contains(output, l => l == "FAIL main.steps:3 walk-to 3,4: step exceeded 50 frames");
+ }
+
+ [Fact]
+ public void GameHookException_IsAFailureNotACrash()
+ {
+ var g = new FakeGame();
+ g.ActionTable["talk"] = new FakeAction(VerbKind.Player, _ => throw new InvalidOperationException("boom"), min: 1, max: 1);
+ var (r, output) = Run("format 1\nmode player\ntalk x", g);
+ Assert.Equal(RunOutcome.Failed, r.Outcome);
+ Assert.Contains(output, l => l == "FAIL main.steps:3 talk x: InvalidOperationException: boom");
+ }
+
+ [Fact]
+ public void InvalidScript_Exit2_NoFrames()
+ {
+ var g = new FakeGame();
+ var (r, output) = Run("format 1\nmode player\ndance", g);
+ Assert.Equal((RunOutcome.Invalid, 2, 0L), (r.Outcome, r.ExitCode, r.Frames));
+ Assert.Contains(output, l => l == "main.steps:3: unknown verb dance");
+ Assert.Equal(0, g.Frame);
+ }
+
+ [Fact]
+ public void Rng_CallsTheGameHook_NoFrame()
+ {
+ var g = new FakeGame();
+ var (r, _) = Run("format 1\nmode player\nrng seed 9\nrng next hit 1", g);
+ Assert.Equal((RunOutcome.Passed, 0L), (r.Outcome, r.Frames));
+ Assert.Equal(["seed 9", "next hit 1"], g.RngImpl!.Calls);
+ }
+
+ [Fact]
+ public void WaitFrames_CountsFrames()
+ {
+ var g = new FakeGame();
+ var (r, _) = Run("format 1\nmode player\nwait-frames 7\nwait-frames 0\nwait-frames 0.5s", g);
+ Assert.Equal(37, r.Frames);
+ }
+
+ [Theory]
+ [InlineData("dclick 1,2", "Move@0 Down@0 Up@1 Down@2 Up@3")]
+ [InlineData("rclick 1,2", "Move@0 Down(R)@0 Up(R)@1")]
+ [InlineData("drag 0,0 30,60", "Move(0,0)@0 Down@0 Move(10,20)@1 Move(20,40)@2 Move(30,60)@3 Up@4")]
+ [InlineData("hold W 3", "KeyDown(W)@0 KeyUp(W)@3")]
+ [InlineData("chord Ctrl+S", "KeyDown(Ctrl)@0 KeyDown(S)@0 KeyUp(S)@1 KeyUp(Ctrl)@1")]
+ [InlineData("type \"hi there\"", "Text(hi there)@0")]
+ [InlineData("wheel -2 5,5", "Move@0 Wheel(-2)@0")]
+ public void InputPlans_Shapes(string line, string expected)
+ {
+ var g = new FakeGame();
+ Run($"format 1\nmode player\n{line}", g);
+ string Show((long Frame, InputEvent E) s) => s.E.Kind switch
+ {
+ InputKind.Move when line.StartsWith("drag") => $"Move({s.E.X},{s.E.Y})@{s.Frame}",
+ InputKind.Down or InputKind.Up when s.E.Button == MouseButton.Right => $"{s.E.Kind}(R)@{s.Frame}",
+ InputKind.KeyDown or InputKind.KeyUp => $"{s.E.Kind}({s.E.Key})@{s.Frame}",
+ InputKind.Text => $"Text({s.E.Text})@{s.Frame}",
+ InputKind.Wheel => $"Wheel({s.E.Delta})@{s.Frame}",
+ _ => $"{s.E.Kind}@{s.Frame}",
+ };
+ Assert.Equal(expected, string.Join(" ", g.Sent.Select(Show)));
+ }
+
+ [Fact]
+ public void BadPosition_Faults()
+ {
+ var (r, output) = Run("format 1\nmode player\nclick 10;20", new FakeGame());
+ Assert.Equal(RunOutcome.Failed, r.Outcome);
+ Assert.Contains(output, l => l == "FAIL main.steps:3 click 10;20: bad position '10;20'");
+ }
+}