-
Notifications
You must be signed in to change notification settings - Fork 1
Testing
Special testing infrastructure and utilities in Asynkron.JsEngine.
File: src/Asynkron.JsEngine/Execution/ExecutionPlanPrinter.cs
Pretty-prints IR execution plans for debugging and reasoning about generated IR code.
var planOutput = ExecutionPlanPrinter.Print(plan.Instructions, plan.EntryPoint);
output.WriteLine(planOutput);Example Output:
=== Execution Plan (15 instructions, entry: 0) ===
→ [ 0] PUSH_ENV (bindings: [i], scopeId: 2, slots: 1, pool: true) → [1]
[ 1] VAR let i = 0 → [2]
[ 2] BRANCH (i < 10) ? [3] : [5]
[ 3] YIELD i → [4]
[ 4] JUMP → [2]
[ 5] POP_ENV (scopeId: 2, pool: true) → [6]
[ 6] RETURN undefined
=== End ===
var formatted = ExecutionPlanPrinter.FormatInstruction(instruction);
// "BRANCH (i < 10) ? [3] : [5]"| Instruction | Format |
|---|---|
| Jump | JUMP → [5] |
| Branch | BRANCH (x < 10) ? [3] : [7] |
| Variable | VAR let i = 0 → [2] |
| PushEnv | PUSH_ENV (bindings: [i, j], scopeId: 2, slots: 2, pool: true) → [1] |
| PopEnv | POP_ENV (scopeId: 2, pool: true) → [6] |
| Return | RETURN value → [8] |
| Yield | YIELD expr → [4] |
| YieldStar | YIELD* iterable (result → x) → [5] |
| IterInit | ITER_INIT expr (slot: $iter, kind: Of) → [2] |
| IterMoveNext | ITER_MOVE_NEXT (iter: $iter, value: item) body: [3], done: [7] |
| EnterTry | ENTER_TRY (handler: [5], finally: [8]) → [1] |
| BinaryOp | BINOP (a + b) → result → [3] |
| Increment |
++i → [2] or i++ → [2]
|
File: src/Asynkron.JsEngine/Execution/ExecutionPlanDiagnostics.cs
Thread-safe diagnostics for monitoring execution plan building.
var funcDecl = (FunctionDeclaration)program.Body[0];
var planOutput = ExecutionPlanDiagnostics.PrintPlan(funcDecl.Function);
if (planOutput.StartsWith("No execution plan"))
{
output.WriteLine($"Plan failed: {planOutput}");
}
else
{
output.WriteLine(planOutput);
}// Reset before test
ExecutionPlanDiagnostics.Reset();
// Execute code
await engine.Evaluate(script);
// Check results
var (attempts, succeeded, failed) = ExecutionPlanDiagnostics.Snapshot();
Assert.True(attempts >= 1);
Assert.True(succeeded >= 1);
Assert.Equal(0, failed);
Assert.Null(ExecutionPlanDiagnostics.LastFailureReason);
Assert.Null(ExecutionPlanDiagnostics.LastFunctionDescription);| Property | Description |
|---|---|
LastFailureReason |
Why the last plan build failed |
LastFunctionDescription |
Which function failed (name or source location) |
Enable TRACE_IR_EXECUTION to compile trace methods:
dotnet build -p:DefineConstants=TRACE_IR_EXECUTIONThis enables:
-
TraceInstruction()- Log each instruction during execution -
TraceDefine()- Log variable definitions -
TraceLookup()- Log variable lookups with resolution status -
TraceEnvChange()- Log environment push/pop operations
// Logged during execution:
// [0] PUSH_ENV (bindings: [i], scopeId: 2) [env:A1B2C3D4 scope: 2 d:1]
// [1] VAR let i = 0 [env:A1B2C3D4 scope: 2 d:1]
// │· DEFINE let 'i' = 0 [env:A1B2C3D4 scope:2]await using var engine = new JsEngine(new JsEngineOptions
{
DebugMode = true,
Logger = logger
});Enables:
- IR trace logging (via
Logger) - Slot hit/miss logging
- Pool state logging
- Additional runtime validation
File: tests/Asynkron.JsEngine.Tests.Helpers/AstTestHelpers.cs
Utilities for parsing and traversing typed ASTs.
flowchart TB
subgraph Traversal["AST Traversal"]
Root((Program)) --> F1((FunctionDecl))
F1 --> Block((BlockStmt))
Block --> For((ForStmt))
For --> Body((Body))
Body --> If((IfStmt))
end
subgraph Helpers["AstTestHelpers"]
FindFirst["FindFirst<T>()"]
Walk["Walk()"]
Parse["ParseAndAnalyze()"]
end
Helpers --> Traversal
var pipeline = AstTestHelpers.ParseAndAnalyze(@"
function outer(seed) {
let baseValue = seed;
function inner(flag) {
if (flag) {
let baseValue = seed + 100;
return baseValue;
}
return baseValue;
}
return inner;
}
");
// Access parsed, analyzed, and CPS-transformed programs
var analyzed = pipeline.Analyzed;// Find first node of type
var forStatement = AstTestHelpers.FindFirst<ForStatement>(program);
// Find with predicate
var innerFunc = AstTestHelpers.FindFirst<FunctionExpression>(
outerFunc.Body,
f => f.Name?.Name == "inner");
// Find specific node types
var ifStatement = AstTestHelpers.FindFirst<IfStatement>(innerFunc.Body);
var thenBlock = Assert.IsType<BlockStatement>(ifStatement.Then);// Depth-first traversal
var allFunctions = AstTestHelpers.Walk(program, includeSelf: true)
.OfType<FunctionDeclaration>()
.ToList();
var allIdentifiers = AstTestHelpers.Walk(program)
.OfType<IdentifierExpression>()
.Where(id => id.Name.Name == "i")
.ToList();var funcDecl = (FunctionDeclaration)program.Body[0];
var cache = ((IAstCacheable<ExecutionPlanCache>)funcDecl.Function).GetOrCreateCache();
if (cache.Succeeded)
{
var plan = cache.Plan!;
output.WriteLine($"Instructions: {plan.Instructions.Length}");
output.WriteLine($"Entry: {plan.EntryPoint}");
output.WriteLine($"FlatSlotCount: {plan.FlatSlotCount}");
output.WriteLine($"RootScopeId: {plan.RootScopeId}");
// Inspect specific instructions
var branches = plan.Instructions.OfType<BranchInstruction>();
var yields = plan.Instructions.OfType<YieldInstruction>();
}
else
{
output.WriteLine($"Plan failed: {cache.FailureReason}");
}var blockStmt = AstTestHelpers.FindFirst<BlockStatement>(program);
var hoistPlan = ((IAstCacheable<HoistPlan>)blockStmt).GetOrCreateCache();
Assert.True(hoistPlan.NeedsEnvironment,
"Block must allocate lexical environment for let/const");var forStmt = AstTestHelpers.FindFirst<ForStatement>(program);
var loopPlan = ((IAstCacheable<LoopPlan>)forStmt).GetOrCreateCache();
// Check per-iteration bindings (for closures)
Assert.Equal(["i", "s"], loopPlan.PerIterationBindings.Select(b => b.Name));var cache = ((IAstCacheable<ExecutionPlanCache>)funcDecl.Function).GetOrCreateCache();
var plan = cache.Plan!;
// Find branch instruction and inspect its condition
var branchInstr = plan.Instructions.OfType<BranchInstruction>().First();
var condition = branchInstr.Condition as BinaryExpression;
var leftId = condition.Left as IdentifierExpression;
// Verify identifier has been assigned a slot
Assert.True(leftId.SlotIndex >= 0, "Should have slot index");
Assert.True(leftId.FlatSlotId >= 0, "Should have flat slot ID");
Assert.True(leftId.ScopeId >= 0, "Should have scope ID");File: tests/Asynkron.JsEngine.Tests.Helpers/TestLogger.cs
Custom ILogger that captures logs for assertion.
var logger = new TestLogger(
output, // xUnit ITestOutputHelper (optional)
"TestName", // Logger name
maxLogCount: 0, // 0 = unlimited (or set limit for infinite loop detection)
minLogLevel: LogLevel.Information
);
await using var engine = new JsEngine(new JsEngineOptions
{
Logger = logger,
DebugMode = true
});await engine.Evaluate(code);
// Get all captured logs
var records = logger.Collector.Snapshot();
// Check latest log
var latest = logger.Collector.LatestRecord;
// Assert on logs
foreach (var record in records)
{
output.WriteLine($"[{record.Level}] {record.Message}");
if (record.Exception is not null)
{
output.WriteLine($" Exception: {record.Exception}");
}
}
// Search for specific patterns
Assert.Contains(records, r => r.Message.Contains("SlotHit: i"));
Assert.DoesNotContain(records, r => r.Message.Contains("SlotMiss"));File: tests/Asynkron.JsEngine.Tests.Helpers/TestEngineFactory.cs
Factory for creating pre-configured test engines.
// With explicit logger
var engine = TestEngineFactory.CreateDebugEngine(logger);
// Auto-attach logger if JSENGINE_TRACE_REALM env var is set
var engine = TestEngineFactory.CreateDebugEngine(loggerName: "MyTest");File: tests/Asynkron.JsEngine.Tests/InternalTestBase.cs
Base class with built-in logger support.
public class MyTests : InternalTestBase
{
public MyTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task MyTest()
{
await using var engine = CreateEngine(); // Has logger + DebugMode
await engine.Evaluate(code);
// Access captured logs
var logs = CurrentLogger?.Collector.Snapshot();
}
}Capture execution context at specific points using __debug().
let x = 42;
let y = 'hello';
__debug(); // Capture state hereawait using var engine = CreateDebugEngine();
await engine.Evaluate(source);
var debugMessage = await engine.DebugMessages().ReadAsync();
// Access captured data
var variables = debugMessage.Variables; // IReadOnlyDictionary<string, object?>
var callStack = debugMessage.CallStack; // IReadOnlyList<CallStackFrame>
var envChain = debugMessage.EnvironmentChain; // IReadOnlyList<EnvironmentInfo>
Assert.Equal(42d, variables["x"]);
Assert.Equal("hello", variables["y"]);foreach (var env in debugMessage.EnvironmentChain)
{
output.WriteLine($"ScopeId={env.ScopeId} Slots={env.SlotCount}");
foreach (var (name, value) in env.DictionaryVariables)
{
output.WriteLine($" {name} = {value}");
}
foreach (var (slot, value) in env.SlotVariables)
{
output.WriteLine($" [slot {slot}] = {value}");
}
}# Enable IR execution tracing
dotnet build -p:DefineConstants=TRACE_IR_EXECUTION
# Disable object pooling (for baseline debugging)
dotnet build -p:DefineConstants=DISABLE_POOLING
# Disable aggressive inlining (for accurate profiler call stacks)
dotnet build -p:DefineConstants=NO_INLINING| Variable | Effect |
|---|---|
JSENGINE_DEBUG_POOL_GUARDS=true |
Enable runtime pool guards |
JSENGINE_TRACE_REALM=true |
Auto-attach logger in TestEngineFactory |
[Fact]
public async Task CompleteTestWorkflow()
{
// 1. Setup with TestLogger
var logger = new TestLogger(output, "Complete", minLogLevel: LogLevel.Debug);
await using var engine = new JsEngine(new JsEngineOptions
{
Logger = logger,
DebugMode = true
});
// 2. Reset diagnostics
ExecutionPlanDiagnostics.Reset();
// 3. Parse and analyze AST
var program = engine.ParseProgram(@"
function run() {
let sum = 0;
for (let i = 0; i < 10; i++) {
sum += i;
}
return sum;
}
");
// 4. Find specific AST nodes
var funcDecl = AstTestHelpers.FindFirst<FunctionDeclaration>(program);
var forStmt = AstTestHelpers.FindFirst<ForStatement>(funcDecl.Function.Body);
// 5. Verify AST cache state
var cache = ((IAstCacheable<ExecutionPlanCache>)funcDecl.Function).GetOrCreateCache();
Assert.True(cache.Succeeded, $"Plan failed: {cache.FailureReason}");
// 6. Print IR plan
var planOutput = ExecutionPlanDiagnostics.PrintPlan(funcDecl.Function);
output.WriteLine("=== Execution Plan ===");
output.WriteLine(planOutput);
// 7. Verify instruction types
var plan = cache.Plan!;
Assert.Contains(plan.Instructions, i => i is PushEnvironmentInstruction);
Assert.Contains(plan.Instructions, i => i is IncrementSlotInstruction);
Assert.Contains(plan.Instructions, i => i is BranchInstruction);
// 8. Execute and verify
await engine.Evaluate(program);
var result = await engine.Evaluate("run()");
Assert.Equal(45d, result.AsDouble());
// 9. Check diagnostics
var (attempts, succeeded, failed) = ExecutionPlanDiagnostics.Snapshot();
Assert.Equal(0, failed);
// 10. Inspect logs
var logs = logger.Collector.Snapshot();
output.WriteLine($"Captured {logs.Length} log entries");
}- Debugging - Pool guards, assertions
- IR Execution - Instruction types and handlers
- JsEnvironment & Slots - Slot verification
- Architecture Overview - Execution model