From 39066900773e7857faf2d02e7ed51b71d219d97d Mon Sep 17 00:00:00 2001 From: godosa Date: Tue, 6 Oct 2026 23:39:36 +0200 Subject: godosa-engine: initial public history --- .../Runs/Scripts/ReviewFixTests.cs | 157 +++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ReviewFixTests.cs (limited to 'src/Godosa.Core.Tests/Runs/Scripts/ReviewFixTests.cs') diff --git a/src/Godosa.Core.Tests/Runs/Scripts/ReviewFixTests.cs b/src/Godosa.Core.Tests/Runs/Scripts/ReviewFixTests.cs new file mode 100644 index 0000000..1b7257b --- /dev/null +++ b/src/Godosa.Core.Tests/Runs/Scripts/ReviewFixTests.cs @@ -0,0 +1,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; + +/// Final-review findings (lib core): each test failed before its fix. +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 { ["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(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(); + var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary + { + ["main.steps"] = Script, + }), new Dictionary()), 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(); + var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary + { + ["main.steps"] = "format 1\nmode player\ncheckpoint one\nexpect no", + }), new Dictionary()), 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(); + var r = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = Script }), + new Dictionary()), 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(); + var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }), + new Dictionary()), 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 { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }), + new Dictionary()), g2, new ScriptRunOptions { Seed = 9 }, output.Add); + runner2.Run(1000); + Assert.Equal(["seed 9"], g2.RngImpl!.Calls); + } +} -- cgit