using Jint.Runtime.Debugger; namespace Jint.Tests.Runtime.Debugger; public class StepModeTests { /// /// Helper method to keep tests independent of line numbers, columns or other arbitrary assertions on /// the current statement. Steps through script with StepMode.Into until it reaches literal statement /// (or directive) 'source'. Then counts the steps needed to reach 'target' using the indicated StepMode. /// /// Script used as basis for test /// StepMode to use from source to target /// Number of steps from source to target private static int StepsFromSourceToTarget(string script, StepMode stepMode) { var engine = new Engine(options => options .DebugMode() .InitialStepMode(StepMode.Into) .DebuggerStatementHandling(DebuggerStatementHandling.Script)); int steps = 0; bool sourceReached = false; bool targetReached = false; engine.Debugger.Step += (sender, info) => { if (sourceReached) { steps++; if (info.ReachedLiteral("target")) { // Stop stepping targetReached = true; return StepMode.None; } return stepMode; } else if (info.ReachedLiteral("source")) { sourceReached = true; return stepMode; } return StepMode.Into; }; engine.Execute(script); // Make sure we actually reached the target Assert.True(targetReached); return steps; } [Fact] public void StepsIntoRegularFunctionCall() { var script = @" 'source'; test(); // first step function test() { 'target'; // second step }"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into)); } [Fact] public void StepsOverRegularFunctionCall() { var script = @" 'source'; test(); 'target'; function test() { 'dummy'; }"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void StepsOutOfRegularFunctionCall() { var script = @" test(); 'target'; function test() { 'source'; 'dummy'; }"; Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out)); } [Fact] public void StepsIntoMemberFunctionCall() { var script = @" const obj = { test() { 'target'; // second step } }; 'source'; obj.test(); // first step"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into)); } [Fact] public void StepsOverMemberFunctionCall() { var script = @" const obj = { test() { 'dummy'; } }; 'source'; obj.test(); 'target';"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void StepsOutOfMemberFunctionCall() { var script = @" const obj = { test() { 'source'; 'dummy'; } }; obj.test(); 'target';"; Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out)); } [Fact] public void StepsIntoCallExpression() { var script = @" function test() { 'target'; // second step return 42; } 'source'; const x = test(); // first step"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into)); } [Fact] public void StepsOverCallExpression() { var script = @" function test() { 'dummy'; return 42; } 'source'; const x = test(); 'target';"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void StepsOutOfCallExpression() { var script = @" function test() { 'source'; 'dummy'; return 42; } const x = test(); 'target';"; Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out)); } [Fact] public void StepsIntoGetAccessor() { var script = @" const obj = { get test() { 'target'; // second step return 144; } }; 'source'; const x = obj.test; // first step"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into)); } [Fact] public void StepsOverGetAccessor() { var script = @" const obj = { get test() { return 144; } }; 'source'; const x = obj.test; 'target';"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void StepsOutOfGetAccessor() { var script = @" const obj = { get test() { 'source'; 'dummy'; return 144; } }; const x = obj.test; 'target';"; Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out)); } [Fact] public void StepsIntoSetAccessor() { var script = @" const obj = { set test(value) { 'target'; // second step this.value = value; } }; 'source'; obj.test = 37; // first step"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into)); } [Fact] public void StepsOverSetAccessor() { var script = @" const obj = { set test(value) { this.value = value; } }; 'source'; obj.test = 37; 'target';"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void StepsOutOfSetAccessor() { var script = @" const obj = { set test(value) { 'source'; 'dummy'; this.value = value; } }; obj.test = 37; 'target';"; Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out)); } [Fact] public void ReturnPointIsAStep() { var script = @" function test() { 'source'; } test(); 'target';"; Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void ReturnStatementIsAStep() { var script = @" function test() { 'source'; return 'result'; } test(); 'target';"; Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Over)); } [Fact] public void StepOutOnlyStepsOutOneStackLevel() { var script = @" function test() { 'dummy'; test2(); 'target'; } function test2() { 'source'; 'dummy'; 'dummy'; } test();"; var engine = new Engine(options => options.DebugMode()); int step = 0; engine.Debugger.Step += (sender, info) => { switch (step) { case 0: if (info.ReachedLiteral("source")) { step++; return StepMode.Out; } break; case 1: Assert.True(info.ReachedLiteral("target")); step++; break; } return StepMode.Into; }; engine.Execute(script); } [Fact] public void StepOverDoesSinglestepAfterBreakpoint() { string script = @" test(); function test() { 'dummy'; debugger; 'target'; }"; var engine = new Engine(options => options .DebugMode() .DebuggerStatementHandling(DebuggerStatementHandling.Script)); bool stepping = false; engine.Debugger.Break += (sender, info) => { stepping = true; return StepMode.Over; }; engine.Debugger.Step += (sender, info) => { if (stepping) { Assert.True(info.ReachedLiteral("target")); } return StepMode.None; }; engine.Execute(script); } [Fact] public void StepNotTriggeredWhenRunning() { string script = @" test(); function test() { 'dummy'; 'dummy'; }"; var engine = new Engine(options => options .DebugMode() .InitialStepMode(StepMode.Into)); int stepCount = 0; engine.Debugger.Step += (sender, info) => { stepCount++; // Start running after first step return StepMode.None; }; engine.Execute(script); Assert.Equal(1, stepCount); } [Fact] public void SkipIsTriggeredWhenRunning() { string script = @" 'step'; 'skip'; 'skip'; debugger; 'step'; 'step'; "; var engine = new Engine(options => options .DebugMode() .DebuggerStatementHandling(DebuggerStatementHandling.Script) .InitialStepMode(StepMode.Into)); int stepCount = 0; int skipCount = 0; engine.Debugger.Step += (sender, info) => { Assert.True(TestHelpers.IsLiteral(info.CurrentNode, "step")); stepCount++; // Start running after first step return stepCount == 1 ? StepMode.None : StepMode.Into; }; engine.Debugger.Skip += (sender, info) => { Assert.True(TestHelpers.IsLiteral(info.CurrentNode, "skip")); skipCount++; return StepMode.None; }; engine.Debugger.Break += (sender, info) => { // Back to stepping after debugger statement return StepMode.Into; }; engine.Execute(script); Assert.Equal(2, skipCount); Assert.Equal(3, stepCount); } }