namespace XFESystem.Browser.Tests; public sealed class PreparedScriptCacheTests { private static string Padded(string source) => source + "\n/*" + new string(' ', 1_024) + "*/"; [Fact] public void PackagedSyntaxReusesAcrossFreshEnginesWithoutSharingGlobalsOrResults() { var cache = new PreparedScriptCache(); string source = Padded("globalThis.counter=(globalThis.counter||0)+1;globalThis.created={value:counter};counter;"); var first = new JavaScriptRuntime(null, cache); Assert.Equal("1", first.EvaluatePackaged(source, "packaged/shared.js").Value); Assert.Equal("2", first.EvaluatePackaged(source, "packaged/shared.js").Value); Assert.Equal(1, first.PackagedScriptPreparations); Assert.Equal(1, first.PackagedScriptCacheHits); var second = new JavaScriptRuntime(null, cache); Assert.Equal("1", second.EvaluatePackaged(source, "packaged/shared.js").Value); Assert.Equal(1, second.PackagedScriptCacheHits); Assert.Equal("99", first.Evaluate("created.value=99").Value); Assert.Equal("1", second.Evaluate("created.value").Value); } [Fact] public void ChangedSourceAtSameAddressIsReparsedAndOldSyntaxIsReplaced() { var cache = new PreparedScriptCache(); var runtime = new JavaScriptRuntime(null, cache); Assert.Equal("41", runtime.EvaluatePackaged(Padded("41"), "packaged/same.js").Value); Assert.Equal("42", runtime.EvaluatePackaged(Padded("42"), "packaged/same.js").Value); Assert.Equal(0, runtime.PackagedScriptCacheHits); Assert.Equal(2, runtime.PackagedScriptPreparations); Assert.Equal(1, cache.Snapshot.Entries); } [Fact] public void OrdinaryEvaluationDoesNotPopulateOrConsumePackagedCache() { var cache = new PreparedScriptCache(); var runtime = new JavaScriptRuntime(null, cache); string source = Padded("6*7"); Assert.Equal("42", runtime.Evaluate(source).Value); Assert.Equal("42", runtime.Evaluate(source).Value); Assert.Equal((0, 0, 0), cache.Snapshot); Assert.Equal(0, runtime.PackagedScriptCacheHits); Assert.Equal(0, runtime.PackagedScriptPreparations); } [Fact] public void WarmSyntaxStillChecksSourceSizeAndStatementBudgetOfReceivingEngine() { var cache = new PreparedScriptCache(); string source = Padded("for(let i=0;i<1000;i++){};42"); Assert.Equal("42", new JavaScriptRuntime(null, cache).EvaluatePackaged(source, "packaged/loop.js").Value); var limited = new JavaScriptRuntime(new() { MaximumStatements = 100 }, cache); var failed = limited.EvaluatePackaged(source, "packaged/loop.js"); Assert.False(failed.Success); Assert.True(failed.BudgetExceeded); Assert.Equal(1, limited.PackagedScriptCacheHits); var small = new JavaScriptRuntime(new() { MaximumScriptCharacters = 100 }, cache); failed = small.EvaluatePackaged(source, "packaged/loop.js"); Assert.False(failed.Success); Assert.True(failed.BudgetExceeded); Assert.Equal(0, small.PackagedScriptCacheHits); Assert.Equal("42", limited.Evaluate("6*7").Value); } [Fact] public void WarmSyntaxStillChecksTimeoutAndDisabledStringCompilation() { var cache = new PreparedScriptCache(); string source = Padded("for(let i=0;i<500000;i++){};42"); var generous = new JavaScriptRuntime(new() { MaximumStatements = 100_000_000, ScriptTimeout = TimeSpan.FromSeconds(15) }, cache); Assert.Equal("42", generous.EvaluatePackaged(source, "packaged/timeout.js").Value); var limited = new JavaScriptRuntime(new() { MaximumStatements = 100_000_000, ScriptTimeout = TimeSpan.FromMilliseconds(1) }, cache); var failed = limited.EvaluatePackaged(source, "packaged/timeout.js"); Assert.False(failed.Success); Assert.True(failed.BudgetExceeded); Assert.Equal(1, limited.PackagedScriptCacheHits); source = Padded("eval('42')"); Assert.False(generous.EvaluatePackaged(source, "packaged/eval.js").Success); Assert.False(generous.EvaluatePackaged(source, "packaged/eval.js").Success); } [Fact] public void SyntaxFailureIsNotCachedAndReceivingEngineRecovers() { var cache = new PreparedScriptCache(); var runtime = new JavaScriptRuntime(null, cache); Assert.False(runtime.EvaluatePackaged(Padded("function {"), "packaged/bad.js").Success); Assert.Equal((0, 0, 0), cache.Snapshot); Assert.Equal("42", runtime.EvaluatePackaged(Padded("42"), "packaged/bad.js").Value); Assert.Equal(1, cache.Snapshot.Entries); } [Fact] public void LeastRecentSyntaxIsEvictedWithinEntryCharacterAndNodeBudgets() { var cache = new PreparedScriptCache(maximumEntries: 2, maximumCharacters: 3_000, maximumNodes: 100); string source = Padded("42"); cache.GetOrPrepare(source, "a", out bool reused); Assert.False(reused); cache.GetOrPrepare(source, "b", out reused); Assert.False(reused); cache.GetOrPrepare(source, "a", out reused); Assert.True(reused); cache.GetOrPrepare(source, "c", out reused); Assert.False(reused); cache.GetOrPrepare(source, "a", out reused); Assert.True(reused); cache.GetOrPrepare(source, "b", out reused); Assert.False(reused); Assert.InRange(cache.Snapshot.Entries, 0, 2); Assert.InRange(cache.Snapshot.Characters, 0, 3_000); Assert.InRange(cache.Snapshot.Nodes, 0, 100); } [Fact] public void OversizedSyntaxExecutesWithoutBeingRetainedByCache() { var cache = new PreparedScriptCache(maximumCharacters: 10, maximumNodes: 1); var runtime = new JavaScriptRuntime(null, cache); Assert.Equal("42", runtime.EvaluatePackaged(Padded("42"), "large").Value); Assert.Equal("42", runtime.EvaluatePackaged(Padded("42"), "large").Value); Assert.Equal((0, 0, 0), cache.Snapshot); Assert.Equal(0, runtime.PackagedScriptCacheHits); } [Fact] public async Task PreparedClosuresRegularExpressionsAndBoundCallsStayLocalAcrossConcurrentEngines() { var cache = new PreparedScriptCache(); string source = Padded("var expression=/x/g;expression.test('xx');var local={count:0};var increment=function(){return ++this.count}.bind(local);var values=[increment(),increment(),expression.lastIndex];values.join(',');"); var results = await Task.WhenAll(Enumerable.Range(0, 12).Select(_ => Task.Run(() => { var runtime = new JavaScriptRuntime(null, cache); return runtime.EvaluatePackaged(source, "packaged/concurrent.js"); }))); Assert.All(results, result => { Assert.True(result.Success, result.Error); Assert.Equal("1,2,1", result.Value); }); Assert.Equal(1, cache.Snapshot.Entries); } [Fact] public void WarmSyntaxHonorsMemoryAndRecursionLimitsOfEachReceivingEngine() { var cache = new PreparedScriptCache(); var generous = new JavaScriptRuntime(new() { MaximumStatements = 1_000_000, MaximumScriptMemoryBytes = 64_000_000 }, cache); string source = Padded("var items=[];for(let i=0;i<20000;i++)items.push({value:i});items.length"); Assert.Equal("20000", generous.EvaluatePackaged(source, "packaged/memory.js").Value); var memory = new JavaScriptRuntime(new() { MaximumStatements = 1_000_000, MaximumScriptMemoryBytes = 100_000 }, cache); var failed = memory.EvaluatePackaged(source, "packaged/memory.js"); Assert.False(failed.Success); Assert.True(failed.BudgetExceeded); Assert.Equal(1, memory.PackagedScriptCacheHits); source = Padded("function nested(n){return n?nested(n-1):42};nested(20)"); Assert.Equal("42", generous.EvaluatePackaged(source, "packaged/recursion.js").Value); var recursion = new JavaScriptRuntime(new() { MaximumRecursionDepth = 4 }, cache); failed = recursion.EvaluatePackaged(source, "packaged/recursion.js"); Assert.False(failed.Success); Assert.True(failed.BudgetExceeded); Assert.Equal(1, recursion.PackagedScriptCacheHits); } [Fact] public void NodeBudgetIndependentlyRefusesRetainingAnOtherwiseSmallSource() { var cache = new PreparedScriptCache(maximumCharacters: 8_192, maximumNodes: 2); var runtime = new JavaScriptRuntime(null, cache); Assert.Equal("42", runtime.EvaluatePackaged(Padded("6*7"), "node-bound").Value); Assert.Equal("42", runtime.EvaluatePackaged(Padded("6*7"), "node-bound").Value); Assert.Equal((0, 0, 0), cache.Snapshot); Assert.Equal(0, runtime.PackagedScriptCacheHits); } }