diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs | |
| download | godosa-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.cs | 224 |
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'"); + } +} |
