mirror of
https://github.com/dart-lang/sdk
synced 2024-11-05 18:22:09 +00:00
Change how debugger handles source breakpoints
- Identify a source breakpoint by a script and a token position rather than a function object. - When a new breakpoint is set, iterate over all compiled functions to see whether code breakpoints must be set. This change fixes issues with functions that get compiled several times, for example as implicit closures or a copies of mixin functions or local closures of mixing functions. We did not detect all duplicates of functions. Eliminated recently introduced const modifiers in SourceBreakpoint class. They tend to be in the way because several accessor methods of SourceBreakpoint compute data lazily and modify the object as a side effect. Review URL: https://codereview.chromium.org//117133005 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@31372 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
parent
8f125a4cff
commit
15e8f121dd
10 changed files with 445 additions and 247 deletions
|
@ -12,6 +12,8 @@ cc/SNPrint_BadArgs: Skip
|
|||
cc/Debug_StackTraceDump1: Skip
|
||||
cc/Debug_StackTraceDump2: Skip
|
||||
|
||||
cc/Debug_StepInto: Fail
|
||||
|
||||
# Flaky on buildbot. Issue 5133 and 10409.
|
||||
cc/Sleep: Pass, Fail
|
||||
|
||||
|
|
|
@ -50,17 +50,19 @@ class RemoteObjectCache : public ZoneAllocated {
|
|||
|
||||
|
||||
SourceBreakpoint::SourceBreakpoint(intptr_t id,
|
||||
const Function& func,
|
||||
const Script& script,
|
||||
intptr_t token_pos)
|
||||
: id_(id),
|
||||
function_(func.raw()),
|
||||
script_(script.raw()),
|
||||
token_pos_(token_pos),
|
||||
line_number_(-1),
|
||||
is_resolved_(false),
|
||||
is_enabled_(false),
|
||||
next_(NULL) {
|
||||
ASSERT(!func.IsNull());
|
||||
ASSERT((func.token_pos() <= token_pos_) &&
|
||||
(token_pos_ <= func.end_token_pos()));
|
||||
next_(NULL),
|
||||
function_(Function::null()),
|
||||
line_number_(-1) {
|
||||
ASSERT(id_ > 0);
|
||||
ASSERT(!script.IsNull());
|
||||
ASSERT(token_pos_ >= 0);
|
||||
}
|
||||
|
||||
|
||||
|
@ -76,51 +78,59 @@ void SourceBreakpoint::Disable() {
|
|||
}
|
||||
|
||||
|
||||
RawScript* SourceBreakpoint::SourceCode() {
|
||||
const Function& func = Function::Handle(function_);
|
||||
return func.script();
|
||||
void SourceBreakpoint::SetResolved(const Function& func, intptr_t token_pos) {
|
||||
ASSERT(func.script() == script_);
|
||||
ASSERT((func.token_pos() <= token_pos) &&
|
||||
(token_pos <= func.end_token_pos()));
|
||||
function_ = func.raw();
|
||||
token_pos_ = token_pos;
|
||||
line_number_ = -1; // Recalcualte lazily.
|
||||
is_resolved_ = true;
|
||||
}
|
||||
|
||||
|
||||
// TODO(hausner): Get rid of library parameter. A source breakpoint location
|
||||
// does not imply a library, since the same source code can be included
|
||||
// in more than one library, e.g. the text location of mixin functions.
|
||||
void SourceBreakpoint::GetCodeLocation(
|
||||
Library* lib,
|
||||
Script* script,
|
||||
intptr_t* pos) const {
|
||||
const Function& func = Function::Handle(function_);
|
||||
const Class& cls = Class::Handle(func.origin());
|
||||
*lib = cls.library();
|
||||
*script = func.script();
|
||||
*pos = token_pos();
|
||||
intptr_t* pos) {
|
||||
*script = this->script();
|
||||
*pos = token_pos_;
|
||||
if (IsResolved()) {
|
||||
const Function& func = Function::Handle(function_);
|
||||
ASSERT(!func.IsNull());
|
||||
const Class& cls = Class::Handle(func.origin());
|
||||
*lib = cls.library();
|
||||
} else {
|
||||
*lib = Library::null();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RawString* SourceBreakpoint::SourceUrl() {
|
||||
const Script& script = Script::Handle(SourceCode());
|
||||
return script.url();
|
||||
return Script::Handle(script()).url();
|
||||
}
|
||||
|
||||
|
||||
intptr_t SourceBreakpoint::LineNumber() {
|
||||
// Compute line number lazily since it causes scanning of the script.
|
||||
if (line_number_ < 0) {
|
||||
const Script& script = Script::Handle(SourceCode());
|
||||
const Script& script = Script::Handle(this->script());
|
||||
script.GetTokenLocation(token_pos_, &line_number_, NULL);
|
||||
}
|
||||
return line_number_;
|
||||
}
|
||||
|
||||
|
||||
void SourceBreakpoint::set_function(const Function& func) {
|
||||
function_ = func.raw();
|
||||
}
|
||||
|
||||
|
||||
void SourceBreakpoint::VisitObjectPointers(ObjectPointerVisitor* visitor) {
|
||||
visitor->VisitPointer(reinterpret_cast<RawObject**>(&script_));
|
||||
visitor->VisitPointer(reinterpret_cast<RawObject**>(&function_));
|
||||
}
|
||||
|
||||
|
||||
void SourceBreakpoint::PrintToJSONStream(JSONStream* stream) const {
|
||||
void SourceBreakpoint::PrintToJSONStream(JSONStream* stream) {
|
||||
Isolate* isolate = Isolate::Current();
|
||||
|
||||
JSONObject jsobj(stream);
|
||||
|
@ -128,9 +138,7 @@ void SourceBreakpoint::PrintToJSONStream(JSONStream* stream) const {
|
|||
|
||||
jsobj.AddProperty("id", id());
|
||||
jsobj.AddProperty("enabled", IsEnabled());
|
||||
|
||||
const Function& func = Function::Handle(function());
|
||||
jsobj.AddProperty("resolved", func.HasCode());
|
||||
jsobj.AddProperty("resolved", IsResolved());
|
||||
|
||||
Library& library = Library::Handle(isolate);
|
||||
Script& script = Script::Handle(isolate);
|
||||
|
@ -139,7 +147,6 @@ void SourceBreakpoint::PrintToJSONStream(JSONStream* stream) const {
|
|||
{
|
||||
JSONObject location(&jsobj, "location");
|
||||
location.AddProperty("type", "Location");
|
||||
location.AddProperty("libId", library.index());
|
||||
|
||||
const String& url = String::Handle(script.url());
|
||||
location.AddProperty("script", url.ToCString());
|
||||
|
@ -221,13 +228,30 @@ const char* Debugger::QualifiedFunctionName(const Function& func) {
|
|||
}
|
||||
|
||||
|
||||
// Returns true if function contains the token position in the given script.
|
||||
static bool FunctionContains(const Function& func,
|
||||
const Script& script,
|
||||
intptr_t token_pos) {
|
||||
if ((func.token_pos() <= token_pos) && (token_pos <= func.end_token_pos())) {
|
||||
// Check script equality second because it allocates
|
||||
// handles as a side effect.
|
||||
return func.script() == script.raw();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool Debugger::HasBreakpoint(const Function& func) {
|
||||
if (!func.HasCode()) {
|
||||
// If the function is not compiled yet, just check whether there
|
||||
// is a user-defined latent breakpoint.
|
||||
// is a user-defined breakpoint that falls into the token
|
||||
// range of the function. This may be a false positive: the breakpoint
|
||||
// might be inside a local closure.
|
||||
Script& script = Script::Handle(isolate_);
|
||||
SourceBreakpoint* sbpt = src_breakpoints_;
|
||||
while (sbpt != NULL) {
|
||||
if (func.raw() == sbpt->function()) {
|
||||
script = sbpt->script();
|
||||
if (FunctionContains(func, script, sbpt->token_pos())) {
|
||||
return true;
|
||||
}
|
||||
sbpt = sbpt->next_;
|
||||
|
@ -1285,54 +1309,47 @@ void Debugger::SignalExceptionThrown(const Instance& exc) {
|
|||
}
|
||||
|
||||
|
||||
// Given a function and a token position range, return the best fit
|
||||
// token position to set a breakpoint.
|
||||
// If multiple possible breakpoint positions are within the given range,
|
||||
// the one with the lowest machine code address is picked.
|
||||
// If no possible breakpoint location exists in the given range, the closest
|
||||
// token position after the range is returned.
|
||||
// Given a function and a token position, return the best fit
|
||||
// token position to set a breakpoint. The best fit is the safe point
|
||||
// with the lowest compiled code address that follows the requsted
|
||||
// token position.
|
||||
intptr_t Debugger::ResolveBreakpointPos(const Function& func,
|
||||
intptr_t first_token_pos,
|
||||
intptr_t last_token_pos) {
|
||||
intptr_t requested_token_pos) {
|
||||
ASSERT(func.HasCode());
|
||||
ASSERT(!func.HasOptimizedCode());
|
||||
Code& code = Code::Handle(func.unoptimized_code());
|
||||
ASSERT(!code.IsNull());
|
||||
PcDescriptors& desc = PcDescriptors::Handle(code.pc_descriptors());
|
||||
intptr_t best_fit_index = -1;
|
||||
intptr_t best_fit = INT_MAX;
|
||||
intptr_t best_fit_pos = INT_MAX;
|
||||
uword lowest_pc = kUwordMax;
|
||||
intptr_t lowest_pc_index = -1;
|
||||
for (intptr_t i = 0; i < desc.Length(); i++) {
|
||||
intptr_t desc_token_pos = desc.TokenPos(i);
|
||||
ASSERT(desc_token_pos >= 0);
|
||||
if (desc_token_pos < first_token_pos) {
|
||||
// This descriptor is before the given range.
|
||||
if (desc_token_pos < requested_token_pos) {
|
||||
// This descriptor is before the first acceptable token position.
|
||||
continue;
|
||||
}
|
||||
if (IsSafePoint(desc.DescriptorKind(i))) {
|
||||
if ((desc_token_pos - first_token_pos) < best_fit) {
|
||||
// So far, this descriptor has the closest token position to the
|
||||
// beginning of the range.
|
||||
best_fit = desc_token_pos - first_token_pos;
|
||||
ASSERT(best_fit >= 0);
|
||||
if (desc_token_pos < best_fit_pos) {
|
||||
// So far, this descriptor has the lowest token position after
|
||||
// the first acceptable token position.
|
||||
best_fit_pos = desc_token_pos;
|
||||
best_fit_index = i;
|
||||
}
|
||||
if ((first_token_pos <= desc_token_pos) &&
|
||||
(desc_token_pos <= last_token_pos) &&
|
||||
(desc.PC(i) < lowest_pc)) {
|
||||
// This descriptor is within the token position range and so
|
||||
// far has the lowest code address.
|
||||
if (desc.PC(i) < lowest_pc) {
|
||||
// This descriptor so far has the lowest code address.
|
||||
lowest_pc = desc.PC(i);
|
||||
lowest_pc_index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lowest_pc_index >= 0) {
|
||||
// We found the the pc descriptor within the given token range that
|
||||
// has the lowest execution address. This is the first possible
|
||||
// breakpoint on the line. We use this instead of the nearest
|
||||
// PC descriptor measured in token index distance.
|
||||
// We found the pc descriptor that has the lowest execution address.
|
||||
// This is the first possible breakpoint after the requested token
|
||||
// position. We use this instead of the nearest PC descriptor
|
||||
// measured in token index distance.
|
||||
best_fit_index = lowest_pc_index;
|
||||
}
|
||||
if (best_fit_index >= 0) {
|
||||
|
@ -1343,15 +1360,16 @@ intptr_t Debugger::ResolveBreakpointPos(const Function& func,
|
|||
|
||||
|
||||
void Debugger::MakeCodeBreakpointsAt(const Function& func,
|
||||
intptr_t token_pos,
|
||||
SourceBreakpoint* bpt) {
|
||||
ASSERT((bpt != NULL) && bpt->IsResolved());
|
||||
ASSERT(!func.HasOptimizedCode());
|
||||
Code& code = Code::Handle(func.unoptimized_code());
|
||||
ASSERT(!code.IsNull());
|
||||
PcDescriptors& desc = PcDescriptors::Handle(code.pc_descriptors());
|
||||
for (intptr_t i = 0; i < desc.Length(); i++) {
|
||||
intptr_t desc_token_pos = desc.TokenPos(i);
|
||||
if ((desc_token_pos == token_pos) && IsSafePoint(desc.DescriptorKind(i))) {
|
||||
if ((desc_token_pos == bpt->token_pos_) &&
|
||||
IsSafePoint(desc.DescriptorKind(i))) {
|
||||
CodeBreakpoint* code_bpt = GetCodeBreakpoint(desc.PC(i));
|
||||
if (code_bpt == NULL) {
|
||||
// No code breakpoint for this code exists; create one.
|
||||
|
@ -1359,83 +1377,207 @@ void Debugger::MakeCodeBreakpointsAt(const Function& func,
|
|||
RegisterCodeBreakpoint(code_bpt);
|
||||
}
|
||||
code_bpt->set_src_bpt(bpt);
|
||||
if (bpt->IsEnabled()) {
|
||||
code_bpt->Enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SourceBreakpoint* Debugger::SetBreakpoint(const Function& target_function,
|
||||
intptr_t first_token_pos,
|
||||
intptr_t last_token_pos) {
|
||||
if ((last_token_pos < target_function.token_pos()) ||
|
||||
(target_function.end_token_pos() < first_token_pos)) {
|
||||
// The given token position is not within the target function.
|
||||
void Debugger::FindEquivalentFunctions(const Script& script,
|
||||
intptr_t start_pos,
|
||||
intptr_t end_pos,
|
||||
GrowableObjectArray* function_list) {
|
||||
Class& cls = Class::Handle(isolate_);
|
||||
Array& functions = Array::Handle(isolate_);
|
||||
GrowableObjectArray& closures = GrowableObjectArray::Handle(isolate_);
|
||||
Function& function = Function::Handle(isolate_);
|
||||
|
||||
const ClassTable& class_table = *isolate_->class_table();
|
||||
const intptr_t num_classes = class_table.NumCids();
|
||||
for (intptr_t i = 1; i < num_classes; i++) {
|
||||
if (class_table.HasValidClassAt(i)) {
|
||||
cls = class_table.At(i);
|
||||
// Note: we need to check the functions of this class even if
|
||||
// the class is defined in a differenct 'script'. There could
|
||||
// be mixin functions from the given script in this class.
|
||||
functions = cls.functions();
|
||||
if (!functions.IsNull()) {
|
||||
const intptr_t num_functions = functions.Length();
|
||||
for (intptr_t pos = 0; pos < num_functions; pos++) {
|
||||
function ^= functions.At(pos);
|
||||
ASSERT(!function.IsNull());
|
||||
// Check token position first to avoid unnecessary calls
|
||||
// to script() which allocates handles.
|
||||
if ((function.token_pos() == start_pos)
|
||||
&& (function.end_token_pos() == end_pos)
|
||||
&& (function.script() == script.raw())) {
|
||||
function_list->Add(function);
|
||||
if (function.HasImplicitClosureFunction()) {
|
||||
function = function.ImplicitClosureFunction();
|
||||
function_list->Add(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closures = cls.closures();
|
||||
if (!closures.IsNull()) {
|
||||
const intptr_t num_closures = closures.Length();
|
||||
for (intptr_t pos = 0; pos < num_closures; pos++) {
|
||||
function ^= closures.At(pos);
|
||||
ASSERT(!function.IsNull());
|
||||
if ((function.token_pos() == start_pos)
|
||||
&& (function.end_token_pos() == end_pos)
|
||||
&& (function.script() == script.raw())) {
|
||||
function_list->Add(function);
|
||||
if (function.HasImplicitClosureFunction()) {
|
||||
function = function.ImplicitClosureFunction();
|
||||
function_list->Add(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void SelectBestFit(Function* best_fit, Function* func) {
|
||||
if (best_fit->IsNull()) {
|
||||
*best_fit = func->raw();
|
||||
}
|
||||
if (func->token_pos() > best_fit->token_pos()) {
|
||||
if (func->end_token_pos() <= best_fit->end_token_pos()) {
|
||||
// func is contained within best_fit. Select it even if it
|
||||
// has not been compiled yet.
|
||||
*best_fit = func->raw();
|
||||
if (func->HasImplicitClosureFunction()) {
|
||||
*func = func->ImplicitClosureFunction();
|
||||
if (func->HasCode()) {
|
||||
*best_fit = func->raw();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ((func->token_pos() == best_fit->token_pos())
|
||||
&& (func->end_token_pos() == best_fit->end_token_pos())
|
||||
&& func->HasCode()) {
|
||||
// If func covers the same range, it is considered a better fit if
|
||||
// it has been compiled.
|
||||
*best_fit = func->raw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RawFunction* Debugger::FindBestFit(const Script& script,
|
||||
intptr_t token_pos) {
|
||||
Class& cls = Class::Handle(isolate_);
|
||||
Array& functions = Array::Handle(isolate_);
|
||||
GrowableObjectArray& closures = GrowableObjectArray::Handle(isolate_);
|
||||
Function& function = Function::Handle(isolate_);
|
||||
Function& best_fit = Function::Handle(isolate_);
|
||||
|
||||
const ClassTable& class_table = *isolate_->class_table();
|
||||
const intptr_t num_classes = class_table.NumCids();
|
||||
for (intptr_t i = 1; i < num_classes; i++) {
|
||||
if (class_table.HasValidClassAt(i)) {
|
||||
cls = class_table.At(i);
|
||||
// Note: we need to check the functions of this class even if
|
||||
// the class is defined in a differenct 'script'. There could
|
||||
// be mixin functions from the given script in this class.
|
||||
functions = cls.functions();
|
||||
if (!functions.IsNull()) {
|
||||
const intptr_t num_functions = functions.Length();
|
||||
for (intptr_t pos = 0; pos < num_functions; pos++) {
|
||||
function ^= functions.At(pos);
|
||||
ASSERT(!function.IsNull());
|
||||
if (FunctionContains(function, script, token_pos)) {
|
||||
SelectBestFit(&best_fit, &function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closures = cls.closures();
|
||||
if (!closures.IsNull()) {
|
||||
const intptr_t num_closures = closures.Length();
|
||||
for (intptr_t pos = 0; pos < num_closures; pos++) {
|
||||
function ^= closures.At(pos);
|
||||
ASSERT(!function.IsNull());
|
||||
if (FunctionContains(function, script, token_pos)) {
|
||||
SelectBestFit(&best_fit, &function);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return best_fit.raw();
|
||||
}
|
||||
|
||||
|
||||
SourceBreakpoint* Debugger::SetBreakpoint(const Script& script,
|
||||
intptr_t token_pos) {
|
||||
Function& func = Function::Handle(isolate_);
|
||||
func = FindBestFit(script, token_pos);
|
||||
if (func.IsNull()) {
|
||||
return NULL;
|
||||
}
|
||||
intptr_t breakpoint_pos = -1;
|
||||
Function& closure = Function::Handle(isolate_);
|
||||
if (target_function.HasImplicitClosureFunction()) {
|
||||
// There is a closurized version of this function.
|
||||
closure = target_function.ImplicitClosureFunction();
|
||||
}
|
||||
// Determine actual breakpoint location if the function or an
|
||||
// implicit closure of the function has been compiled already.
|
||||
if (target_function.HasCode()) {
|
||||
if (!func.IsNull() && func.HasCode()) {
|
||||
// A function containing this breakpoint location has already
|
||||
// been compiled. We can resolve the breakpoint now.
|
||||
DeoptimizeWorld();
|
||||
ASSERT(!target_function.HasOptimizedCode());
|
||||
breakpoint_pos =
|
||||
ResolveBreakpointPos(target_function, first_token_pos, last_token_pos);
|
||||
} else if (!closure.IsNull() && closure.HasCode()) {
|
||||
DeoptimizeWorld();
|
||||
ASSERT(!closure.HasOptimizedCode());
|
||||
breakpoint_pos =
|
||||
ResolveBreakpointPos(closure, first_token_pos, last_token_pos);
|
||||
} else {
|
||||
// This function has not been compiled yet. Set a pending
|
||||
// breakpoint to be resolved later.
|
||||
SourceBreakpoint* source_bpt =
|
||||
GetSourceBreakpoint(target_function, first_token_pos);
|
||||
if (source_bpt != NULL) {
|
||||
// A pending source breakpoint for this uncompiled location
|
||||
// already exists.
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("Pending breakpoint for uncompiled function"
|
||||
" '%s' at line %" Pd " already exists\n",
|
||||
target_function.ToFullyQualifiedCString(),
|
||||
source_bpt->LineNumber());
|
||||
intptr_t breakpoint_pos = ResolveBreakpointPos(func, token_pos);
|
||||
if (breakpoint_pos >= 0) {
|
||||
SourceBreakpoint* bpt = GetSourceBreakpoint(script, breakpoint_pos);
|
||||
if (bpt != NULL) {
|
||||
// A source breakpoint for this location already exists.
|
||||
return bpt;
|
||||
}
|
||||
return source_bpt;
|
||||
bpt = new SourceBreakpoint(nextId(), script, token_pos);
|
||||
bpt->SetResolved(func, breakpoint_pos);
|
||||
RegisterSourceBreakpoint(bpt);
|
||||
// There may be more than one function object for a given function
|
||||
// in source code. There may be implicit closure functions, and
|
||||
// there may be copies of mixin functions. Collect all functions whose
|
||||
// source code range matches exactly the best fit function we
|
||||
// found.
|
||||
GrowableObjectArray& functions =
|
||||
GrowableObjectArray::Handle(GrowableObjectArray::New());
|
||||
FindEquivalentFunctions(script,
|
||||
func.token_pos(),
|
||||
func.end_token_pos(),
|
||||
&functions);
|
||||
const intptr_t num_functions = functions.Length();
|
||||
// We must have found at least one function: func.
|
||||
ASSERT(num_functions > 0);
|
||||
// Create code breakpoints for all compiled functions we found.
|
||||
for (intptr_t i = 0; i < num_functions; i++) {
|
||||
func ^= functions.At(i);
|
||||
if (func.HasCode()) {
|
||||
MakeCodeBreakpointsAt(func, bpt);
|
||||
}
|
||||
}
|
||||
bpt->Enable();
|
||||
SignalBpResolved(bpt);
|
||||
return bpt;
|
||||
}
|
||||
source_bpt =
|
||||
new SourceBreakpoint(nextId(), target_function, first_token_pos);
|
||||
RegisterSourceBreakpoint(source_bpt);
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("Registering pending breakpoint for "
|
||||
"uncompiled function '%s' at line %" Pd "\n",
|
||||
target_function.ToFullyQualifiedCString(),
|
||||
source_bpt->LineNumber());
|
||||
}
|
||||
source_bpt->Enable();
|
||||
return source_bpt;
|
||||
}
|
||||
ASSERT(breakpoint_pos != -1);
|
||||
SourceBreakpoint* source_bpt =
|
||||
GetSourceBreakpoint(target_function, breakpoint_pos);
|
||||
if (source_bpt != NULL) {
|
||||
// A source breakpoint for this location already exists.
|
||||
return source_bpt;
|
||||
// There is no compiled function at this token position.
|
||||
// Register an unresolved breakpoint.
|
||||
if (FLAG_verbose_debug && !func.IsNull()) {
|
||||
intptr_t line_number;
|
||||
script.GetTokenLocation(token_pos, &line_number, NULL);
|
||||
OS::Print("Registering pending breakpoint for "
|
||||
"uncompiled function '%s' at line %" Pd "\n",
|
||||
func.ToFullyQualifiedCString(),
|
||||
line_number);
|
||||
}
|
||||
source_bpt = new SourceBreakpoint(nextId(), target_function, breakpoint_pos);
|
||||
RegisterSourceBreakpoint(source_bpt);
|
||||
if (target_function.HasCode()) {
|
||||
MakeCodeBreakpointsAt(target_function, breakpoint_pos, source_bpt);
|
||||
SourceBreakpoint* bpt = GetSourceBreakpoint(script, token_pos);
|
||||
if (bpt == NULL) {
|
||||
bpt = new SourceBreakpoint(nextId(), script, token_pos);
|
||||
}
|
||||
if (!closure.IsNull() && closure.HasCode()) {
|
||||
MakeCodeBreakpointsAt(closure, breakpoint_pos, source_bpt);
|
||||
}
|
||||
source_bpt->Enable();
|
||||
SignalBpResolved(source_bpt);
|
||||
return source_bpt;
|
||||
RegisterSourceBreakpoint(bpt);
|
||||
bpt->Enable();
|
||||
return bpt;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1469,9 +1611,8 @@ void Debugger::OneTimeBreakAtEntry(const Function& target_function) {
|
|||
SourceBreakpoint* Debugger::SetBreakpointAtEntry(
|
||||
const Function& target_function) {
|
||||
ASSERT(!target_function.IsNull());
|
||||
return SetBreakpoint(target_function,
|
||||
target_function.token_pos(),
|
||||
target_function.end_token_pos());
|
||||
const Script& script = Script::Handle(target_function.script());
|
||||
return SetBreakpoint(script, target_function.token_pos());
|
||||
}
|
||||
|
||||
|
||||
|
@ -1505,33 +1646,25 @@ SourceBreakpoint* Debugger::SetBreakpointAtLine(const String& script_url,
|
|||
}
|
||||
return NULL;
|
||||
} else if (last_token_idx < 0) {
|
||||
// Line does not contain any tokens. first_token_index is the first
|
||||
// token after the given line. We check whether that token is
|
||||
// part of a function.
|
||||
last_token_idx = first_token_idx;
|
||||
}
|
||||
|
||||
Function& func = Function::Handle(isolate_);
|
||||
while (first_token_idx <= last_token_idx) {
|
||||
func = lib.LookupFunctionInScript(script, first_token_idx);
|
||||
if (!func.IsNull()) {
|
||||
break;
|
||||
}
|
||||
first_token_idx++;
|
||||
}
|
||||
if (func.IsNull()) {
|
||||
// Line does not contain any tokens.
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("No executable code at line %" Pd " in '%s'\n",
|
||||
line_number, script_url.ToCString());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (last_token_idx < 0) {
|
||||
// The token at first_token_index is past the requested source line.
|
||||
// Set the breakpoint at the closest position after that line.
|
||||
last_token_idx = func.end_token_pos();
|
||||
|
||||
SourceBreakpoint* bpt = NULL;
|
||||
ASSERT(first_token_idx <= last_token_idx);
|
||||
while ((bpt == NULL) && (first_token_idx <= last_token_idx)) {
|
||||
bpt = SetBreakpoint(script, first_token_idx);
|
||||
first_token_idx++;
|
||||
}
|
||||
return SetBreakpoint(func, first_token_idx, last_token_idx);
|
||||
if ((bpt == NULL) && FLAG_verbose_debug) {
|
||||
OS::Print("No executable code at line %" Pd " in '%s'\n",
|
||||
line_number, script_url.ToCString());
|
||||
}
|
||||
return bpt;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1923,37 +2056,35 @@ void Debugger::Initialize(Isolate* isolate) {
|
|||
}
|
||||
|
||||
|
||||
static RawFunction* GetOriginalFunction(const Function& func) {
|
||||
if (func.IsClosureFunction()) {
|
||||
// Local functions (closures) in mixin functions do not have
|
||||
// an original function they were cloned from.
|
||||
// Note: this is a problem when a breakpoint is set in a mixed-in
|
||||
// closure. The breakpoint is linked to the closure attached to the
|
||||
// mixin application class, not the mixin class. When the same
|
||||
// closure is compiled for another mixin application class, we
|
||||
// don't find the breakpoint since we'll be looking in the
|
||||
// mixin class.
|
||||
return func.raw();
|
||||
// Return innermost closure contained in 'function' that contains
|
||||
// the given token position.
|
||||
RawFunction* Debugger::FindInnermostClosure(const Function& function,
|
||||
intptr_t token_pos) {
|
||||
const Class& owner = Class::Handle(isolate_, function.Owner());
|
||||
if (owner.closures() == GrowableObjectArray::null()) {
|
||||
return Function::null();
|
||||
}
|
||||
const Class& origin_class = Class::Handle(func.origin());
|
||||
if (origin_class.is_patch()) {
|
||||
// Patched functions from patch classes are removed from the
|
||||
// function array of the patch class, so we will not find an
|
||||
// original function object.
|
||||
return func.raw();
|
||||
}
|
||||
const Array& functions = Array::Handle(origin_class.functions());
|
||||
Object& orig_func = Object::Handle();
|
||||
for (intptr_t i = 0; i < functions.Length(); i++) {
|
||||
orig_func = functions.At(i);
|
||||
// Function names are symbols, so we can compare the raw pointers.
|
||||
if (func.name() == Function::Cast(orig_func).name()) {
|
||||
return Function::Cast(orig_func).raw();
|
||||
// Note that we need to check that the closure is in the same
|
||||
// script as the outer function. We could have closures originating
|
||||
// in mixin classes whose source code is contained in a different
|
||||
// script.
|
||||
const Script& outer_origin = Script::Handle(isolate_, function.script());
|
||||
const GrowableObjectArray& closures =
|
||||
GrowableObjectArray::Handle(isolate_, owner.closures());
|
||||
const intptr_t num_closures = closures.Length();
|
||||
Function& closure = Function::Handle(isolate_);
|
||||
Function& best_fit = Function::Handle(isolate_);
|
||||
for (intptr_t i = 0; i < num_closures; i++) {
|
||||
closure ^= closures.At(i);
|
||||
if ((function.token_pos() < closure.token_pos()) &&
|
||||
(closure.end_token_pos() < function.end_token_pos()) &&
|
||||
(closure.token_pos() <= token_pos) &&
|
||||
(token_pos <= closure.end_token_pos()) &&
|
||||
(closure.script() == outer_origin.raw())) {
|
||||
SelectBestFit(&best_fit, &closure);
|
||||
}
|
||||
}
|
||||
// Uncommon case: not a mixin function.
|
||||
ASSERT(!Class::Handle(func.Owner()).IsMixinApplication());
|
||||
return func.raw();
|
||||
return best_fit.raw();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1962,56 +2093,67 @@ void Debugger::NotifyCompilation(const Function& func) {
|
|||
// Return with minimal overhead if there are no breakpoints.
|
||||
return;
|
||||
}
|
||||
Function& lookup_function = Function::Handle(func.raw());
|
||||
if (func.IsImplicitClosureFunction()) {
|
||||
// If the newly compiled function is a an implicit closure (a closure that
|
||||
// was formed by assigning a static or instance method to a function
|
||||
// object), we need to use the closure's parent function to see whether
|
||||
// there are any breakpoints. The parent function is the actual method on
|
||||
// which the user sets breakpoints.
|
||||
lookup_function = func.parent_function();
|
||||
ASSERT(!lookup_function.IsNull());
|
||||
}
|
||||
if (lookup_function.Owner() != lookup_function.origin()) {
|
||||
// This is a cloned function from a mixin class. If a breakpoint
|
||||
// was set in this function, it is registered using the function
|
||||
// of the origin class.
|
||||
lookup_function = GetOriginalFunction(lookup_function);
|
||||
}
|
||||
SourceBreakpoint* bpt = src_breakpoints_;
|
||||
while (bpt != NULL) {
|
||||
if (lookup_function.raw() == bpt->function()) {
|
||||
// Check if the breakpoint is inside a closure or local function
|
||||
// within the newly compiled function. Note that we must look
|
||||
// in the owner class of the function that just got compiled
|
||||
// (i.e. func), not the owner class of the function we use to
|
||||
// record the breakpoint (lookup_function).
|
||||
Class& owner = Class::Handle(func.Owner());
|
||||
Function& closure =
|
||||
Function::Handle(owner.LookupClosureFunction(bpt->token_pos()));
|
||||
if (!closure.IsNull() && (closure.raw() != lookup_function.raw())) {
|
||||
// Iterate over all source breakpoints to check whether breakpoints
|
||||
// need to be set in the newly compiled function.
|
||||
Script& script = Script::Handle(isolate_);
|
||||
for (SourceBreakpoint* bpt = src_breakpoints_;
|
||||
bpt != NULL;
|
||||
bpt = bpt->next()) {
|
||||
script = bpt->script();
|
||||
if (FunctionContains(func, script, bpt->token_pos())) {
|
||||
Function& inner_function = Function::Handle(isolate_);
|
||||
inner_function = FindInnermostClosure(func, bpt->token_pos());
|
||||
if (!inner_function.IsNull()) {
|
||||
// The local function of a function we just compiled cannot
|
||||
// be compiled already.
|
||||
ASSERT(!inner_function.HasCode());
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("Resetting pending breakpoint to function %s\n",
|
||||
closure.ToFullyQualifiedCString());
|
||||
OS::Print("Pending BP remains unresolved in inner function '%s'\n",
|
||||
inner_function.ToFullyQualifiedCString());
|
||||
}
|
||||
bpt->set_function(closure);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO(hausner): What should we do if function is optimized?
|
||||
// Can we deoptimize the function?
|
||||
ASSERT(!func.HasOptimizedCode());
|
||||
|
||||
// There is no local function within func that contains the
|
||||
// breakpoint token position. Resolve the breakpoint if necessary
|
||||
// and set the code breakpoints.
|
||||
if (!bpt->IsResolved()) {
|
||||
// Resolve source breakpoint in the newly compiled function.
|
||||
intptr_t bp_pos = ResolveBreakpointPos(func, bpt->token_pos());
|
||||
if (bp_pos < 0) {
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("Failed resolving breakpoint for function '%s'\n",
|
||||
String::Handle(func.name()).ToCString());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
intptr_t requested_pos = bpt->token_pos();
|
||||
bpt->SetResolved(func, bp_pos);
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("Enable pending breakpoint for function '%s'\n",
|
||||
String::Handle(lookup_function.name()).ToCString());
|
||||
OS::Print("Resolved BP %" Pd " to pos %" Pd ", line %" Pd ", "
|
||||
"function '%s' (requested pos %" Pd ")\n",
|
||||
bpt->id(),
|
||||
bpt->token_pos(),
|
||||
bpt->LineNumber(),
|
||||
func.ToFullyQualifiedCString(),
|
||||
requested_pos);
|
||||
}
|
||||
const Script& script= Script::Handle(func.script());
|
||||
intptr_t first_pos, last_pos;
|
||||
script.TokenRangeAtLine(bpt->LineNumber(), &first_pos, &last_pos);
|
||||
intptr_t bp_pos =
|
||||
ResolveBreakpointPos(func, bpt->token_pos(), last_pos);
|
||||
bpt->set_token_pos(bp_pos);
|
||||
MakeCodeBreakpointsAt(func, bp_pos, bpt);
|
||||
SignalBpResolved(bpt);
|
||||
}
|
||||
bpt->Enable(); // Enables the code breakpoint as well.
|
||||
ASSERT(bpt->IsResolved());
|
||||
if (FLAG_verbose_debug) {
|
||||
OS::Print("Setting breakpoint %" Pd " at line %" Pd " for %s '%s'\n",
|
||||
bpt->id(),
|
||||
bpt->LineNumber(),
|
||||
func.IsClosureFunction() ? "closure" : "function",
|
||||
String::Handle(func.name()).ToCString());
|
||||
}
|
||||
MakeCodeBreakpointsAt(func, bpt);
|
||||
}
|
||||
bpt = bpt->next();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2105,12 +2247,11 @@ void Debugger::RemoveInternalBreakpoints() {
|
|||
}
|
||||
|
||||
|
||||
SourceBreakpoint* Debugger::GetSourceBreakpoint(const Function& func,
|
||||
SourceBreakpoint* Debugger::GetSourceBreakpoint(const Script& script,
|
||||
intptr_t token_pos) {
|
||||
SourceBreakpoint* bpt = src_breakpoints_;
|
||||
while (bpt != NULL) {
|
||||
if ((bpt->function() == func.raw()) &&
|
||||
(bpt->token_pos() == token_pos)) {
|
||||
if ((bpt->script_ == script.raw()) && (bpt->token_pos_ == token_pos)) {
|
||||
return bpt;
|
||||
}
|
||||
bpt = bpt->next();
|
||||
|
|
|
@ -27,42 +27,43 @@ class StackFrame;
|
|||
// SourceBreakpoint.
|
||||
class SourceBreakpoint {
|
||||
public:
|
||||
SourceBreakpoint(intptr_t id, const Function& func, intptr_t token_pos);
|
||||
SourceBreakpoint(intptr_t id, const Script& script, intptr_t token_pos);
|
||||
|
||||
RawFunction* function() const { return function_; }
|
||||
intptr_t token_pos() const { return token_pos_; }
|
||||
void set_token_pos(intptr_t value) { token_pos_ = value; }
|
||||
intptr_t id() const { return id_; }
|
||||
|
||||
RawScript* SourceCode();
|
||||
RawScript* script() { return script_; }
|
||||
RawString* SourceUrl();
|
||||
intptr_t LineNumber();
|
||||
|
||||
void GetCodeLocation(Library* lib,
|
||||
Script* script,
|
||||
intptr_t* token_pos) const;
|
||||
void GetCodeLocation(Library* lib, Script* script, intptr_t* token_pos);
|
||||
|
||||
void Enable();
|
||||
void Disable();
|
||||
bool IsEnabled() const { return is_enabled_; }
|
||||
bool IsResolved() { return is_resolved_; }
|
||||
|
||||
void PrintToJSONStream(JSONStream* stream) const;
|
||||
void PrintToJSONStream(JSONStream* stream);
|
||||
|
||||
private:
|
||||
void VisitObjectPointers(ObjectPointerVisitor* visitor);
|
||||
|
||||
void set_function(const Function& func);
|
||||
void SetResolved(const Function& func, intptr_t token_pos);
|
||||
void set_next(SourceBreakpoint* value) { next_ = value; }
|
||||
SourceBreakpoint* next() const { return this->next_; }
|
||||
|
||||
const intptr_t id_;
|
||||
RawFunction* function_;
|
||||
RawScript* script_;
|
||||
intptr_t token_pos_;
|
||||
intptr_t line_number_;
|
||||
bool is_resolved_;
|
||||
bool is_enabled_;
|
||||
|
||||
SourceBreakpoint* next_;
|
||||
|
||||
// Valid for resolved breakpoints:
|
||||
RawFunction* function_;
|
||||
intptr_t line_number_;
|
||||
|
||||
friend class Debugger;
|
||||
DISALLOW_COPY_AND_ASSIGN(SourceBreakpoint);
|
||||
};
|
||||
|
@ -378,11 +379,18 @@ class Debugger {
|
|||
kSingleStep
|
||||
};
|
||||
|
||||
void FindEquivalentFunctions(const Script& script,
|
||||
intptr_t start_pos,
|
||||
intptr_t end_pos,
|
||||
GrowableObjectArray* function_list);
|
||||
RawFunction* FindBestFit(const Script& script, intptr_t token_pos);
|
||||
RawFunction* FindInnermostClosure(const Function& function,
|
||||
intptr_t token_pos);
|
||||
intptr_t ResolveBreakpointPos(const Function& func,
|
||||
intptr_t first_token_pos,
|
||||
intptr_t last_token_pos);
|
||||
intptr_t requested_token_pos);
|
||||
void DeoptimizeWorld();
|
||||
void InstrumentForStepping(const Function& target_function);
|
||||
SourceBreakpoint* SetBreakpoint(const Script& script, intptr_t token_pos);
|
||||
SourceBreakpoint* SetBreakpoint(const Function& target_function,
|
||||
intptr_t first_token_pos,
|
||||
intptr_t last_token_pos);
|
||||
|
@ -390,10 +398,9 @@ class Debugger {
|
|||
void UnlinkCodeBreakpoints(SourceBreakpoint* src_bpt);
|
||||
void RegisterSourceBreakpoint(SourceBreakpoint* bpt);
|
||||
void RegisterCodeBreakpoint(CodeBreakpoint* bpt);
|
||||
SourceBreakpoint* GetSourceBreakpoint(const Function& func,
|
||||
SourceBreakpoint* GetSourceBreakpoint(const Script& script,
|
||||
intptr_t token_pos);
|
||||
void MakeCodeBreakpointsAt(const Function& func,
|
||||
intptr_t token_pos,
|
||||
SourceBreakpoint* bpt);
|
||||
// Returns NULL if no breakpoint exists for the given address.
|
||||
CodeBreakpoint* GetCodeBreakpoint(uword breakpoint_address);
|
||||
|
|
|
@ -44,11 +44,11 @@ TEST_CASE(Debugger_PrintBreakpointsToJSONArray) {
|
|||
EXPECT_STREQ(
|
||||
"[{\"type\":\"Breakpoint\",\"id\":2,"
|
||||
"\"enabled\":true,\"resolved\":false,"
|
||||
"\"location\":{\"type\":\"Location\",\"libId\":12,"
|
||||
"\"location\":{\"type\":\"Location\","
|
||||
"\"script\":\"dart:test-lib\",\"tokenPos\":14}},"
|
||||
"{\"type\":\"Breakpoint\",\"id\":1,"
|
||||
"\"enabled\":true,\"resolved\":false,"
|
||||
"\"location\":{\"type\":\"Location\",\"libId\":12,"
|
||||
"\"location\":{\"type\":\"Location\","
|
||||
"\"script\":\"dart:test-lib\",\"tokenPos\":5}}]",
|
||||
js.ToCString());
|
||||
}
|
||||
|
|
|
@ -133,7 +133,7 @@ void JSONStream::PrintValue(const Field& field,
|
|||
}
|
||||
|
||||
|
||||
void JSONStream::PrintValue(const SourceBreakpoint* bpt) {
|
||||
void JSONStream::PrintValue(SourceBreakpoint* bpt) {
|
||||
PrintCommaIfNeeded();
|
||||
bpt->PrintToJSONStream(this);
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ class JSONStream : ValueObject {
|
|||
void PrintfValue(const char* format, ...) PRINTF_ATTRIBUTE(2, 3);
|
||||
void PrintValue(const Object& o, bool ref = true);
|
||||
void PrintValue(const Field& f, const Instance& instance, bool ref = true);
|
||||
void PrintValue(const SourceBreakpoint* bpt);
|
||||
void PrintValue(SourceBreakpoint* bpt);
|
||||
|
||||
void PrintPropertyBool(const char* name, bool b);
|
||||
void PrintProperty(const char* name, intptr_t i);
|
||||
|
@ -158,7 +158,7 @@ class JSONArray : public ValueObject {
|
|||
bool ref = true) const {
|
||||
stream_->PrintValue(field, instance, ref);
|
||||
}
|
||||
void AddValue(const SourceBreakpoint* bpt) const {
|
||||
void AddValue(SourceBreakpoint* bpt) const {
|
||||
stream_->PrintValue(bpt);
|
||||
}
|
||||
void AddValueF(const char* format, ...) const PRINTF_ATTRIBUTE(2, 3);
|
||||
|
|
|
@ -139,7 +139,7 @@ TEST_CASE(Service_DebugBreakpoints) {
|
|||
"{\"type\":\"BreakpointList\",\"breakpoints\":[{"
|
||||
"\"type\":\"Breakpoint\",\"id\":1,\"enabled\":true,"
|
||||
"\"resolved\":false,"
|
||||
"\"location\":{\"type\":\"Location\",\"libId\":12,"
|
||||
"\"location\":{\"type\":\"Location\","
|
||||
"\"script\":\"dart:test-lib\",\"tokenPos\":5}}]}",
|
||||
handler.msg());
|
||||
|
||||
|
@ -150,7 +150,7 @@ TEST_CASE(Service_DebugBreakpoints) {
|
|||
EXPECT_STREQ(
|
||||
"{\"type\":\"Breakpoint\",\"id\":1,\"enabled\":true,"
|
||||
"\"resolved\":false,"
|
||||
"\"location\":{\"type\":\"Location\",\"libId\":12,"
|
||||
"\"location\":{\"type\":\"Location\","
|
||||
"\"script\":\"dart:test-lib\",\"tokenPos\":5}}",
|
||||
handler.msg());
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ main(List<String> arguments) {
|
|||
}
|
||||
|
||||
bar() {
|
||||
|
||||
// Attempt to set breakpoint in this empty line does not
|
||||
// result in a resolved breakpoint.
|
||||
print("bar");
|
||||
}
|
||||
|
||||
|
@ -24,12 +25,10 @@ var testScript = [
|
|||
SetBreakpoint(16),
|
||||
Resume(),
|
||||
MatchFrame(0, "main"),
|
||||
Resume(),
|
||||
MatchFrame(0, "bar"),
|
||||
Resume(),
|
||||
Resume(), // Next breakpoint expected in main, when bar() returns.
|
||||
MatchFrame(0, "main"),
|
||||
// Only two breakpoint resolved events expected.
|
||||
ExpectEvent("breakpointResolved", {"breakpointId": 1}),
|
||||
ExpectEvent("breakpointResolved", {"breakpointId": 2}),
|
||||
ExpectEvent("breakpointResolved", {"breakpointId": 3}),
|
||||
Resume(),
|
||||
];
|
||||
|
|
|
@ -471,7 +471,7 @@ class Debugger {
|
|||
}
|
||||
|
||||
// Send next debugger command in the script, if a response
|
||||
// form the last command has been received and processed.
|
||||
// from the last command has been received and processed.
|
||||
void sendNextCommand() {
|
||||
while (script.isNextEventMatcher) {
|
||||
EventMatcher matcher = script.currentEntry;
|
||||
|
@ -610,6 +610,9 @@ bool RunScript(List script, List<String> arguments) {
|
|||
|
||||
// Port number 0 means debug target picks a free port dynamically.
|
||||
var targetOpts = [ "--debug:0" ];
|
||||
if (arguments.contains("--verbose")) {
|
||||
targetOpts.add("--verbose_debug");
|
||||
}
|
||||
targetOpts.add(Platform.script.toFilePath());
|
||||
targetOpts.add("--debuggee");
|
||||
print('args: ${targetOpts.join(" ")}');
|
||||
|
|
46
tests/standalone/debugger/local_function_test.dart
Normal file
46
tests/standalone/debugger/local_function_test.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// This test forks a second vm process that runs this dart script as
|
||||
// a debug target.
|
||||
// Run this test with option --wire to see the json messages sent
|
||||
// between the processes.
|
||||
// Run this test with option --verbose to see the stdout and stderr output
|
||||
// of the debug target process.
|
||||
|
||||
import "debug_lib.dart";
|
||||
|
||||
// Check that if a breakpoint is requested on the line containing the
|
||||
// one-liner with a local function below, the breakpoint gets set in
|
||||
// the outer function.
|
||||
foo(x) => (n) => x * n;
|
||||
|
||||
// Check that if a breakpoint is requested on the line "return n*x"
|
||||
// below, the breakpoint is set in the local function (closure).
|
||||
bar(x) {
|
||||
return (n) {
|
||||
return n * x;
|
||||
};
|
||||
}
|
||||
|
||||
main(List<String> arguments) {
|
||||
if (RunScript(testScript, arguments)) return;
|
||||
print("Hello from debuggee");
|
||||
var f = foo(10); // Hits breakpoint.
|
||||
print(f(5));
|
||||
var b = bar(10);
|
||||
print(b(10)); // Hits breakpoint.
|
||||
}
|
||||
|
||||
// Expected debugger events and commands.
|
||||
var testScript = [
|
||||
MatchFrame(0, "main"), // Top frame in trace is function "main".
|
||||
SetBreakpoint(17), // Set breakpoint in function foo.
|
||||
SetBreakpoint(23), // Set breakpoint in local function inside bar.
|
||||
Resume(),
|
||||
MatchFrames(["foo", "main"]),
|
||||
Resume(),
|
||||
MatchFrames(["<anonymous closure>", "main"]),
|
||||
Resume()
|
||||
];
|
Loading…
Reference in a new issue