Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debugger improvements 4b #1113

Merged
merged 27 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c000817
Added Parsed event to engine
Jither Dec 4, 2021
98fffaa
Split Global scope into Global (OER) and Script (DER) in debugger
Jither Dec 4, 2021
860bf0b
New BreakPoint implementation
Jither Dec 4, 2021
7af6c24
Let Scope resolve BindingObject
Jither Dec 4, 2021
4f571e1
Slight cleanup
Jither Dec 5, 2021
613c410
BreakLocation to record
Jither Dec 5, 2021
99ca6a9
Sealed classes
Jither Dec 5, 2021
281a614
Fixed inspection of uninitialized block scoped bindings
Jither Dec 6, 2021
b72c854
Member order
Jither Dec 9, 2021
5a13506
Silently catch errors in breakpoint conditions
Jither Mar 6, 2022
2252d61
Distinguish between breakpoints and debugger statements
Jither Mar 6, 2022
fd15360
DebugHandler-specific script evaluation
Jither Mar 7, 2022
d52430a
Fix: DebugHandler reentry when evaluating conditional breakpoints
Jither Mar 8, 2022
a8c0db3
Stepping: Skip block statements, include loop expressions
Jither Mar 10, 2022
4b4d632
Include relevant BreakPoint in DebugInformation
Jither Mar 12, 2022
e9babba
DebugHandler Support for evaluating (preparsed) AST
Jither Mar 12, 2022
7ba7a41
More logical Break/Step events
Jither Mar 13, 2022
745594c
Cleanup
Jither Mar 13, 2022
3c8961c
Don't react to debugger statement when stepping
Jither Mar 17, 2022
855729c
DebugHandler.CurrentLocation
Jither Mar 18, 2022
0cb99b7
Merge branch 'dev' into debugger-improvements-4b
Jither Mar 19, 2022
8b8ec53
Evaluation context guard for DebugHandler Evaluate
Jither Mar 22, 2022
391157c
Tests for new DebugHandler functionality
Jither Mar 22, 2022
cc81716
Removed Engine.Parsed event for now
Jither Mar 22, 2022
f0f1006
Merge branch 'dev' into debugger-improvements-4b
Jither Mar 22, 2022
d76df0d
Changes after review
Jither Mar 23, 2022
dfee6dc
Minor variable renaming for test clarity
Jither Mar 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 295 additions & 11 deletions Jint.Tests/Runtime/Debugger/BreakPointTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,117 @@
using Esprima;
using Jint.Runtime.Debugger;
using System.Linq;
using Xunit;

namespace Jint.Tests.Runtime.Debugger
{
public class BreakPointTests
{
[Fact]
[Fact]
public void BreakLocationsCompareEqualityByValue()
{
var loc1 = new BreakLocation(42, 23);
var loc2 = new BreakLocation(42, 23);
var loc3 = new BreakLocation(17, 7);

Assert.Equal(loc1, loc2);
Assert.True(loc1 == loc2);
Assert.True(loc2 != loc3);
Assert.False(loc1 != loc2);
Assert.False(loc2 == loc3);
}

[Fact]
public void BreakLocationsWithSourceCompareEqualityByValue()
{
var loc1 = new BreakLocation("script1", 42, 23);
var loc2 = new BreakLocation("script1", 42, 23);
var loc3 = new BreakLocation("script2", 42, 23);

Assert.Equal(loc1, loc2);
Assert.True(loc1 == loc2);
Assert.True(loc2 != loc3);
Assert.False(loc1 != loc2);
Assert.False(loc2 == loc3);
}

[Fact]
public void BreakLocationsOptionalSourceEqualityComparer()
{
var script1 = new BreakLocation("script1", 42, 23);
var script2 = new BreakLocation("script2", 42, 23);
var script2b = new BreakLocation("script2", 44, 23);
var any = new BreakLocation(null, 42, 23);

var comparer = new OptionalSourceBreakLocationEqualityComparer();
Assert.True(comparer.Equals(script1, any));
Assert.True(comparer.Equals(script2, any));
Assert.False(comparer.Equals(script1, script2));
Assert.False(comparer.Equals(script2, script2b));
Assert.Equal(comparer.GetHashCode(script1), comparer.GetHashCode(any));
Assert.Equal(comparer.GetHashCode(script1), comparer.GetHashCode(script2));
Assert.NotEqual(comparer.GetHashCode(script2), comparer.GetHashCode(script2b));
}

[Fact]
public void BreakPointReplacesPreviousBreakPoint()
{
var engine = new Engine(options => options.DebugMode());

engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5, "i === 1"));
Assert.Collection(engine.DebugHandler.BreakPoints,
breakPoint =>
{
Assert.Equal(4, breakPoint.Location.Line);
Assert.Equal(5, breakPoint.Location.Column);
Assert.Equal("i === 1", breakPoint.Condition);
});

engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5));
Assert.Collection(engine.DebugHandler.BreakPoints,
breakPoint =>
{
Assert.Equal(4, breakPoint.Location.Line);
Assert.Equal(5, breakPoint.Location.Column);
Assert.Equal(null, breakPoint.Condition);
});
}

[Fact]
public void BreakPointRemovesBasedOnLocationEquality()
{
var engine = new Engine(options => options.DebugMode());

engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5, "i === 1"));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(5, 6, "j === 2"));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(10, 7, "x > 5"));
Assert.Equal(3, engine.DebugHandler.BreakPoints.Count);

engine.DebugHandler.BreakPoints.RemoveAt(new BreakLocation(null, 4, 5));
engine.DebugHandler.BreakPoints.RemoveAt(new BreakLocation(null, 10, 7));

Assert.Collection(engine.DebugHandler.BreakPoints,
breakPoint =>
{
Assert.Equal(5, breakPoint.Location.Line);
Assert.Equal(6, breakPoint.Location.Column);
Assert.Equal("j === 2", breakPoint.Condition);
});
}

[Fact]
public void BreakPointContainsBasedOnLocationEquality()
{
var engine = new Engine(options => options.DebugMode());

engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5, "i === 1"));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(5, 6, "j === 2"));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(10, 7, "x > 5"));
Assert.True(engine.DebugHandler.BreakPoints.Contains(new BreakLocation(null, 5, 6)));
Assert.False(engine.DebugHandler.BreakPoints.Contains(new BreakLocation(null, 8, 9)));
}

[Fact]
public void BreakPointBreaksAtPosition()
{
string script = @"let x = 1, y = 2;
Expand All @@ -20,13 +125,13 @@ public void BreakPointBreaksAtPosition()
bool didBreak = false;
engine.DebugHandler.Break += (sender, info) =>
{
Assert.Equal(4, info.CurrentStatement.Location.Start.Line);
Assert.Equal(5, info.CurrentStatement.Location.Start.Column);
Assert.Equal(4, info.Location.Start.Line);
Assert.Equal(5, info.Location.Start.Column);
didBreak = true;
return StepMode.None;
};

engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 5));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5));
engine.Execute(script);
Assert.True(didBreak);
}
Expand All @@ -50,14 +155,14 @@ public void BreakPointBreaksInCorrectSource()

var engine = new Engine(options => { options.DebugMode(); });

engine.DebugHandler.BreakPoints.Add(new BreakPoint("script2", 3, 0));
engine.DebugHandler.BreakPoints.Set(new BreakPoint("script2", 3, 0));

bool didBreak = false;
engine.DebugHandler.Break += (sender, info) =>
{
Assert.Equal("script2", info.CurrentStatement.Location.Source);
Assert.Equal(3, info.CurrentStatement.Location.Start.Line);
Assert.Equal(0, info.CurrentStatement.Location.Start.Column);
Assert.Equal("script2", info.Location.Source);
Assert.Equal(3, info.Location.Start.Line);
Assert.Equal(0, info.Location.Start.Column);
didBreak = true;
return StepMode.None;
};
Expand Down Expand Up @@ -99,6 +204,81 @@ public void DebuggerStatementTriggersBreak()
Assert.True(didBreak);
}

[Fact]
public void DebuggerStatementDoesNotTriggerBreakWhenStepping()
{
string script = @"'dummy';
debugger;
'dummy';";

var engine = new Engine(options => options
.DebugMode()
.DebuggerStatementHandling(DebuggerStatementHandling.Script)
.InitialStepMode(StepMode.Into));

bool didBreak = false;
int stepCount = 0;
engine.DebugHandler.Break += (sender, info) =>
{
didBreak = true;
return StepMode.None;
};

engine.DebugHandler.Step += (sender, info) =>
{
stepCount++;
return StepMode.Into;
};

engine.Execute(script);
Assert.Equal(3, stepCount);
Assert.False(didBreak);
}

[Fact]
public void BreakPointDoesNotTriggerBreakWhenStepping()
{
string script = @"
'first breakpoint';
'dummy';
'second breakpoint';";

var engine = new Engine(options => options
.DebugMode()
.InitialStepMode(StepMode.Into));

bool didStep = true;
bool didBreak = true;

engine.DebugHandler.BreakPoints.Set(new BreakPoint(2, 0));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 0));

engine.DebugHandler.Break += (sender, info) =>
{
didBreak = true;
// first breakpoint shouldn't cause us to get here, because we're stepping,
// but when we reach the second, we're running:
Assert.True(TestHelpers.ReachedLiteral(info, "second breakpoint"));
return StepMode.None;
};

engine.DebugHandler.Step += (sender, info) =>
{
didStep = true;
if (TestHelpers.ReachedLiteral(info, "first breakpoint"))
{
// Run from here
return StepMode.None;
}
return StepMode.Into;
};

engine.Execute(script);

Assert.True(didStep);
Assert.True(didBreak);
}

[Fact(Skip = "Non-source breakpoint is triggered before Statement, while debugger statement is now triggered by ExecuteInternal")]
public void DebuggerStatementAndBreakpointTriggerSingleBreak()
{
Expand All @@ -110,7 +290,7 @@ public void DebuggerStatementAndBreakpointTriggerSingleBreak()
.DebugMode()
.DebuggerStatementHandling(DebuggerStatementHandling.Script));

engine.DebugHandler.BreakPoints.Add(new BreakPoint(2, 0));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(2, 0));

int breakTriggered = 0;
engine.DebugHandler.Break += (sender, info) =>
Expand Down Expand Up @@ -138,8 +318,8 @@ public void BreakpointOverridesStepOut()

var engine = new Engine(options => options.DebugMode());

engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 0));
engine.DebugHandler.BreakPoints.Add(new BreakPoint(6, 0));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 0));
engine.DebugHandler.BreakPoints.Set(new BreakPoint(6, 0));

int step = 0;
engine.DebugHandler.Break += (sender, info) =>
Expand All @@ -160,5 +340,109 @@ public void BreakpointOverridesStepOut()

Assert.Equal(2, step);
}

[Fact]
public void ErrorInConditionalBreakpointLeavesCallStackAlone()
{
string script = @"
function foo()
{
let x = 0;
'before breakpoint';
'breakpoint 1 here';
'breakpoint 2 here';
'after breakpoint';
}

foo();
";
var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));

int stepsReached = 0;
int breakpointsReached = 0;

// This breakpoint will be hit:
engine.DebugHandler.BreakPoints.Set(new BreakPoint(6, 0, "x == 0"));
// This condition is an error (y is not defined). DebugHandler will
// treat it as an unmatched breakpoint:
engine.DebugHandler.BreakPoints.Set(new BreakPoint(7, 0, "y == 0"));

engine.DebugHandler.Step += (sender, info) =>
{
if (info.ReachedLiteral("before breakpoint"))
{
Assert.Equal(1, engine.CallStack.Count);
stepsReached++;
return StepMode.None;
}
else if (info.ReachedLiteral("after breakpoint"))
{
Assert.Equal(1, engine.CallStack.Count);
stepsReached++;
return StepMode.None;
}
return StepMode.Into;
};

engine.DebugHandler.Break += (sender, info) =>
{
breakpointsReached++;
return StepMode.Into;
};

engine.Execute(script);

Assert.Equal(1, breakpointsReached);
Assert.Equal(2, stepsReached);
}

private class SimpleHitConditionBreakPoint : BreakPoint
{
public SimpleHitConditionBreakPoint(int line, int column, string condition = null,
int? hitCondition = null) : base(line, column, condition)
{
HitCondition = hitCondition;
}

public int HitCount { get; set; }
public int? HitCondition { get; set; }
}

[Fact]
public void BreakPointCanBeExtended()
{
// More of a documentation than a required test, this shows the usefulness of BreakPoint being
// extensible - as a test, at least it ensures that it is.
var script = @"
for (let i = 0; i < 10; i++)
{
'breakpoint';
}
";
var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.None));

engine.DebugHandler.BreakPoints.Set(
new SimpleHitConditionBreakPoint(4, 4, condition: null, hitCondition: 5));

int numberOfBreaks = 0;
engine.DebugHandler.Break += (sender, info) =>
{
Assert.True(info.ReachedLiteral("breakpoint"));
var extendedBreakPoint = Assert.IsType<SimpleHitConditionBreakPoint>(info.BreakPoint);
extendedBreakPoint.HitCount++;
if (extendedBreakPoint.HitCount == extendedBreakPoint.HitCondition)
{
// Here is where we would normally pause the execution.
// the breakpoint is hit for the fifth time, when i is 4 (off by one)
Assert.Equal(4, info.CurrentScopeChain[0].GetBindingValue("i").AsInteger());
numberOfBreaks++;
}
return StepMode.None;
};

engine.Execute(script);

Assert.Equal(1, numberOfBreaks);
}
}
}
4 changes: 2 additions & 2 deletions Jint.Tests/Runtime/Debugger/CallStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function foo()

foo();";

var engine = new Engine(options => options.DebugMode());
var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));

bool atReturn = false;
bool didCheckReturn = false;
Expand All @@ -121,7 +121,7 @@ function foo()
atReturn = false;
}

if (info.CurrentStatement is ReturnStatement)
if (info.CurrentNode is ReturnStatement)
{
// Step one further, and we should have the return value
atReturn = true;
Expand Down
4 changes: 2 additions & 2 deletions Jint.Tests/Runtime/Debugger/DebugHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ public void AvoidsPauseRecursion()
// reentrance in a multithreaded environment (e.g. using ManualResetEvent(Slim)) would cause
// a deadlock.
string script = @"
const obj = { get name() { 'fail'; return 'Smith'; } };
var obj = { get name() { 'fail'; return 'Smith'; } };
'target';
";

var engine = new Engine(options => options.DebugMode());
var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));

bool didPropertyAccess = false;

Expand Down
Loading