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);
}
}
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);
}
}