v8/test/debugger/test-api.js
Benedikt Meurer 3740764cca [debug][cleanup] Use consistent StepInto and StepOver naming.
In the Chrome DevTools Protocol, the step actions are named StepOut,
StepOver, and StepInto, but internally we used StepOut, StepNext, and
StepIn instead. This change adjusts the naming to be consistent.

Bug: chromium:901814, chromium:1162229
Change-Id: Id3502a1b0a4aadd94734ec3d1fef73c1782fa220
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2928510
Reviewed-by: Yang Guo <yangguo@chromium.org>
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74877}
2021-06-01 11:26:57 +00:00

802 lines
24 KiB
JavaScript

// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
"use strict";
// If true, prints all messages sent and received by inspector.
const printProtocolMessages = false;
// The active wrapper instance.
let activeWrapper = undefined;
// Receiver function called by inspector, delegating to active wrapper.
function receive(message) {
activeWrapper.receiveMessage(message);
}
class DebugWrapper {
constructor() {
// Message dictionary storing {id, message} pairs.
this.receivedMessages = new Map();
// Each message dispatched by the Debug wrapper is assigned a unique number
// using nextMessageId.
this.nextMessageId = 0;
// The listener method called on certain events.
this.listener = undefined;
// Debug events which can occur in the V8 JavaScript engine.
this.DebugEvent = { Break: 1,
Exception: 2,
AfterCompile: 3,
CompileError: 4,
OOM: 5,
};
// The different types of steps.
this.StepAction = { StepOut: 0,
StepOver: 1,
StepInto: 2,
};
// A copy of the scope types from runtime-debug.cc.
// NOTE: these constants should be backward-compatible, so
// add new ones to the end of this list.
this.ScopeType = { Global: 0,
Local: 1,
With: 2,
Closure: 3,
Catch: 4,
Block: 5,
Script: 6,
Eval: 7,
Module: 8
};
// Types of exceptions that can be broken upon.
this.ExceptionBreak = { Caught : 0,
Uncaught: 1 };
// Store the current script id so we can skip corresponding break events.
this.thisScriptId = %FunctionGetScriptId(receive);
// Stores all set breakpoints.
this.breakpoints = new Set();
// Register as the active wrapper.
assertTrue(activeWrapper === undefined);
activeWrapper = this;
}
enable() { this.sendMessageForMethodChecked("Debugger.enable"); }
disable() { this.sendMessageForMethodChecked("Debugger.disable"); }
setListener(listener) { this.listener = listener; }
stepOver() { this.sendMessageForMethodChecked("Debugger.stepOver"); }
stepInto() { this.sendMessageForMethodChecked("Debugger.stepInto"); }
stepOut() { this.sendMessageForMethodChecked("Debugger.stepOut"); }
setBreakOnException() {
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : "all" });
}
clearBreakOnException() {
const newState = this.isBreakOnUncaughtException() ? "uncaught" : "none";
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : newState });
}
isBreakOnException() {
return !!%IsBreakOnException(this.ExceptionBreak.Caught);
};
setBreakOnUncaughtException() {
const newState = this.isBreakOnException() ? "all" : "uncaught";
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : newState });
}
clearBreakOnUncaughtException() {
const newState = this.isBreakOnException() ? "all" : "none";
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : newState });
}
isBreakOnUncaughtException() {
return !!%IsBreakOnException(this.ExceptionBreak.Uncaught);
};
clearStepping() { %ClearStepping(); };
// Returns the resulting breakpoint id.
setBreakPoint(func, opt_line, opt_column, opt_condition) {
assertTrue(%IsFunction(func));
assertFalse(%FunctionIsAPIFunction(func));
const scriptid = %FunctionGetScriptId(func);
assertTrue(scriptid != -1);
const offset = %FunctionGetScriptSourcePosition(func);
const loc =
%ScriptLocationFromLine2(scriptid, opt_line, opt_column, offset);
return this.setBreakPointAtLocation(scriptid, loc, opt_condition);
}
clearBreakPoint(breakpoint) {
assertTrue(this.breakpoints.has(breakpoint));
const breakid = breakpoint.id;
const {msgid, msg} = this.createMessage(
"Debugger.removeBreakpoint", { breakpointId : breakid });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
this.breakpoints.delete(breakid);
}
clearAllBreakPoints() {
for (let breakpoint of this.breakpoints) {
this.clearBreakPoint(breakpoint);
}
this.breakpoints.clear();
}
showBreakPoints(f) {
if (!%IsFunction(f)) throw new Error("Not passed a Function");
const source = %FunctionGetSourceCode(f);
const offset = %FunctionGetScriptSourcePosition(f);
const locations = %GetBreakLocations(f);
if (!locations) return source;
locations.sort(function(x, y) { return x - y; });
let result = "";
let prev_pos = 0;
let pos;
for (var i = 0; i < locations.length; i++) {
pos = locations[i] - offset;
result += source.slice(prev_pos, pos);
result += "[B" + i + "]";
prev_pos = pos;
}
pos = source.length;
result += source.substring(prev_pos, pos);
return result;
}
debuggerFlags() {
return { breakPointsActive :
{ setValue : (enabled) => this.setBreakPointsActive(enabled) }
};
}
// Returns the script source. The return value is the script source for the
// script in which the function is defined.
scriptSource(func) {
return %FunctionGetScriptSource(func);
};
setBreakPointsActive(enabled) {
const {msgid, msg} = this.createMessage(
"Debugger.setBreakpointsActive", { active : enabled });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
generatorScopeCount(gen) {
return %GetGeneratorScopeCount(gen);
}
generatorScope(gen, index) {
// These indexes correspond definitions in debug-scopes.h.
const kScopeDetailsTypeIndex = 0;
const kScopeDetailsObjectIndex = 1;
const details = %GetGeneratorScopeDetails(gen, index);
function scopeObjectProperties() {
const obj = details[kScopeDetailsObjectIndex];
return Object.keys(obj).map((k, v) => v);
}
function setScopeVariableValue(name, value) {
const res = %SetGeneratorScopeVariableValue(gen, index, name, value);
if (!res) throw new Error("Failed to set variable '" + name + "' value");
}
const scopeObject =
{ value : () => details[kScopeDetailsObjectIndex],
property : (prop) => details[kScopeDetailsObjectIndex][prop],
properties : scopeObjectProperties,
propertyNames : () => Object.keys(details[kScopeDetailsObjectIndex])
.map((key, _) => key),
};
return { scopeType : () => details[kScopeDetailsTypeIndex],
scopeIndex : () => index,
scopeObject : () => scopeObject,
setVariableValue : setScopeVariableValue,
}
}
generatorScopes(gen) {
const count = %GetGeneratorScopeCount(gen);
const scopes = [];
for (let i = 0; i < count; i++) {
scopes.push(this.generatorScope(gen, i));
}
return scopes;
}
// --- Internal methods. -----------------------------------------------------
getNextMessageId() {
return this.nextMessageId++;
}
createMessage(method, params) {
const id = this.getNextMessageId();
const msg = JSON.stringify({
id: id,
method: method,
params: params,
});
return { msgid : id, msg: msg };
}
receiveMessage(message) {
const parsedMessage = JSON.parse(message);
if (printProtocolMessages) {
print(JSON.stringify(parsedMessage, undefined, 1));
}
if (parsedMessage.id !== undefined) {
this.receivedMessages.set(parsedMessage.id, parsedMessage);
}
this.dispatchMessage(parsedMessage);
}
sendMessage(message) {
if (printProtocolMessages) print(message);
send(message);
}
sendMessageForMethodChecked(method, params) {
const {msgid, msg} = this.createMessage(method, params);
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
takeReplyChecked(msgid) {
const reply = this.receivedMessages.get(msgid);
assertTrue(reply !== undefined);
this.receivedMessages.delete(msgid);
return reply;
}
setBreakPointAtLocation(scriptid, loc, opt_condition) {
const params = { location :
{ scriptId : scriptid.toString(),
lineNumber : loc.line,
columnNumber : loc.column,
},
condition : opt_condition,
};
const {msgid, msg} = this.createMessage("Debugger.setBreakpoint", params);
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
const result = reply.result;
assertTrue(result !== undefined);
const breakid = result.breakpointId;
assertTrue(breakid !== undefined);
const breakpoint = { id : result.breakpointId }
this.breakpoints.add(breakpoint);
return breakpoint;
}
execStatePrepareStep(action) {
switch(action) {
case this.StepAction.StepOut: this.stepOut(); break;
case this.StepAction.StepOver: this.stepOver(); break;
case this.StepAction.StepInto: this.stepInto(); break;
default: %AbortJS("Unsupported StepAction"); break;
}
}
execStateScopeType(type) {
switch (type) {
case "global": return this.ScopeType.Global;
case "local": return this.ScopeType.Local;
case "with": return this.ScopeType.With;
case "closure": return this.ScopeType.Closure;
case "catch": return this.ScopeType.Catch;
case "block": return this.ScopeType.Block;
case "script": return this.ScopeType.Script;
case "eval": return this.ScopeType.Eval;
case "module": return this.ScopeType.Module;
default: %AbortJS("Unexpected scope type");
}
}
execStateScopeObjectProperty(serialized_scope, prop) {
let found = null;
for (let i = 0; i < serialized_scope.length; i++) {
const elem = serialized_scope[i];
if (elem.name == prop) {
found = elem;
break;
}
}
if (found == null) return { isUndefined : () => true };
const val = { value : () => found.value.value };
// Not undefined in the sense that we did find a property, even though
// the value can be 'undefined'.
return { value : () => val,
isUndefined : () => false,
};
}
// Returns an array of property descriptors of the scope object.
// This is in contrast to the original API, which simply passed object
// mirrors.
execStateScopeObject(obj) {
const serialized_scope = this.getProperties(obj.objectId);
const scope = this.propertiesToObject(serialized_scope);
return { value : () => scope,
property : (prop) =>
this.execStateScopeObjectProperty(serialized_scope, prop),
properties : () => serialized_scope.map(elem => elem.value),
propertyNames : () => serialized_scope.map(elem => elem.name)
};
}
setVariableValue(frame, scope_index, name, value) {
const frameid = frame.callFrameId;
const {msgid, msg} = this.createMessage(
"Debugger.setVariableValue",
{ callFrameId : frameid,
scopeNumber : scope_index,
variableName : name,
newValue : { value : value }
});
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
if (reply.error) {
throw new Error("Failed to set variable '" + name + "' value");
}
}
execStateScope(frame, scope_index) {
const scope = frame.scopeChain[scope_index];
return { scopeType : () => this.execStateScopeType(scope.type),
scopeIndex : () => scope_index,
frameIndex : () => frame.callFrameId,
scopeObject : () => this.execStateScopeObject(scope.object),
setVariableValue :
(name, value) => this.setVariableValue(frame, scope_index,
name, value),
};
}
// Takes a list of properties as produced by getProperties and turns them
// into an object.
propertiesToObject(props) {
const obj = {}
props.forEach((elem) => {
const key = elem.name;
let value;
if (elem.value) {
// Some properties (e.g. with getters/setters) don't have a value.
switch (elem.value.type) {
case "undefined": value = undefined; break;
default: value = elem.value.value; break;
}
}
obj[key] = value;
})
return obj;
}
getProperties(objectId) {
const {msgid, msg} = this.createMessage(
"Runtime.getProperties", { objectId : objectId, ownProperties: true });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
return reply.result.result;
}
getLocalScopeDetails(frame) {
const scopes = frame.scopeChain;
for (let i = 0; i < scopes.length; i++) {
const scope = scopes[i]
if (scope.type == "local") {
return this.getProperties(scope.object.objectId);
}
}
return undefined;
}
execStateFrameLocalCount(frame) {
const scope_details = this.getLocalScopeDetails(frame);
return scope_details ? scope_details.length : 0;
}
execStateFrameLocalName(frame, index) {
const scope_details = this.getLocalScopeDetails(frame);
if (index < 0 || index >= scope_details.length) return undefined;
return scope_details[index].name;
}
execStateFrameLocalValue(frame, index) {
const scope_details = this.getLocalScopeDetails(frame);
if (index < 0 || index >= scope_details.length) return undefined;
const local = scope_details[index];
let localValue;
switch (local.value.type) {
case "undefined": localValue = undefined; break;
default: localValue = local.value.value; break;
}
return { value : () => localValue };
}
reconstructValue(objectId) {
const {msgid, msg} = this.createMessage(
"Runtime.getProperties", { objectId : objectId, ownProperties: true });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
for (const internalProperty of reply.result.internalProperties) {
if (internalProperty.name === '[[PrimitiveValue]]') {
return Object(internalProperty.value.value);
}
}
throw new Error('Remote object is not a value wrapper');
}
reconstructRemoteObject(obj) {
let value = obj.value;
let isUndefined = false;
switch (obj.type) {
case "object": {
switch (obj.subtype) {
case "error": {
const desc = obj.description;
switch (obj.className) {
case "EvalError": throw new EvalError(desc);
case "RangeError": throw new RangeError(desc);
case "ReferenceError": throw new ReferenceError(desc);
case "SyntaxError": throw new SyntaxError(desc);
case "TypeError": throw new TypeError(desc);
case "URIError": throw new URIError(desc);
default: throw new Error(desc);
}
break;
}
case "array": {
const array = [];
const props = this.propertiesToObject(
this.getProperties(obj.objectId));
for (let i = 0; i < props.length; i++) {
array[i] = props[i];
}
value = array;
break;
}
case "null": {
value = null;
break;
}
default: {
switch (obj.className) {
case "global":
value = Function('return this')();
break;
case "Number":
case "String":
case "Boolean":
value = this.reconstructValue(obj.objectId);
break;
default:
value = this.propertiesToObject(
this.getProperties(obj.objectId));
break;
}
break;
}
}
break;
}
case "undefined": {
value = undefined;
isUndefined = true;
break;
}
case "number": {
if (obj.description === "NaN") {
value = NaN;
}
break;
}
case "bigint": {
assertEquals("n", obj.unserializableValue.charAt(
obj.unserializableValue.length - 1));
value = eval(obj.unserializableValue);
break;
}
case "string":
case "boolean": {
break;
}
case "function": {
value = obj.description;
}
default: {
break;
}
}
return { value : () => value,
isUndefined : () => isUndefined,
type : () => obj.type,
className : () => obj.className
};
}
evaluateOnCallFrame(frame, expr, throw_on_side_effect = false) {
const frameid = frame.callFrameId;
const {msgid, msg} = this.createMessage(
"Debugger.evaluateOnCallFrame",
{ callFrameId : frameid,
expression : expr,
throwOnSideEffect : throw_on_side_effect,
});
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
const result = reply.result.result;
return this.reconstructRemoteObject(result);
}
frameReceiver(frame) {
return this.reconstructRemoteObject(frame.this);
}
frameReturnValue(frame) {
return this.reconstructRemoteObject(frame.returnValue);
}
execStateFrameRestart(frame) {
const frameid = frame.callFrameId;
const {msgid, msg} = this.createMessage(
"Debugger.restartFrame", { callFrameId : frameid });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
execStateFrame(frame) {
const scriptid = parseInt(frame.location.scriptId);
const line = frame.location.lineNumber;
const column = frame.location.columnNumber;
const loc = %ScriptLocationFromLine2(scriptid, line, column, 0);
const func = { name : () => frame.functionName };
const index = +frame.callFrameId.split(".")[2];
function allScopes() {
const scopes = [];
for (let i = 0; i < frame.scopeChain.length; i++) {
scopes.push(this.execStateScope(frame, i));
}
return scopes;
}
return { sourceColumn : () => column,
sourceLine : () => line + 1,
sourceLineText : () => loc.sourceText,
sourcePosition : () => loc.position,
evaluate : (expr, throw_on_side_effect) =>
this.evaluateOnCallFrame(frame, expr, throw_on_side_effect),
functionName : () => frame.functionName,
func : () => func,
index : () => index,
localCount : () => this.execStateFrameLocalCount(frame),
localName : (ix) => this.execStateFrameLocalName(frame, ix),
localValue: (ix) => this.execStateFrameLocalValue(frame, ix),
receiver : () => this.frameReceiver(frame),
restart : () => this.execStateFrameRestart(frame),
returnValue : () => this.frameReturnValue(frame),
scopeCount : () => frame.scopeChain.length,
scope : (index) => this.execStateScope(frame, index),
allScopes : allScopes.bind(this)
};
}
evaluateGlobal(expr, throw_on_side_effect) {
const {msgid, msg} = this.createMessage(
"Runtime.evaluate", { expression : expr, throwOnSideEffect: throw_on_side_effect });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
const result = reply.result.result;
return this.reconstructRemoteObject(result);
}
evaluateGlobalREPL(expr) {
return %RuntimeEvaluateREPL(expr).then(value => {
return value[".repl_result"];
});
}
eventDataException(params) {
switch (params.data.type) {
case "string": {
return params.data.value;
}
case "object": {
const props = this.getProperties(params.data.objectId);
return this.propertiesToObject(props);
}
default: {
return undefined;
}
}
}
eventDataScriptSource(id) {
const {msgid, msg} = this.createMessage(
"Debugger.getScriptSource", { scriptId : String(id) });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
return reply.result.scriptSource;
}
eventDataScriptSetSource(id, src) {
const {msgid, msg} = this.createMessage(
"Debugger.setScriptSource", { scriptId : id, scriptSource : src });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
eventDataScript(params) {
const id = parseInt(params.scriptId);
const name = params.url ? params.url : undefined;
return { id : () => id,
name : () => name,
source : () => this.eventDataScriptSource(params.scriptId),
setSource : (src) => this.eventDataScriptSetSource(id, src)
};
}
// --- Message handlers. -----------------------------------------------------
dispatchMessage(message) {
const method = message.method;
if (method == "Debugger.paused") {
this.handleDebuggerPaused(message);
} else if (method == "Debugger.scriptParsed") {
this.handleDebuggerScriptParsed(message);
} else if (method == "Debugger.scriptFailedToParse") {
this.handleDebuggerScriptFailedToParse(message);
}
}
handleDebuggerPaused(message) {
const params = message.params;
var debugEvent;
switch (params.reason) {
case "exception":
case "promiseRejection":
debugEvent = this.DebugEvent.Exception;
break;
case "OOM":
debugEvent = this.DebugEvent.OOM;
break;
case "other":
debugEvent = this.DebugEvent.Break;
break;
case "ambiguous":
case "XHR":
case "DOM":
case "EventListener":
case "assert":
case "debugCommand":
case "CSPViolation":
assertUnreachable();
default:
assertUnreachable();
}
if (!params.callFrames[0]) return;
// Skip break events in this file.
if (params.callFrames[0].location.scriptId == this.thisScriptId) return;
// TODO(jgruber): Arguments as needed.
let execState = { frames : params.callFrames,
prepareStep : this.execStatePrepareStep.bind(this),
evaluateGlobal :
(expr) => this.evaluateGlobal(expr),
frame : (index) => this.execStateFrame(
index ? params.callFrames[index]
: params.callFrames[0]),
frameCount : () => params.callFrames.length
};
let eventData = this.execStateFrame(params.callFrames[0]);
if (debugEvent == this.DebugEvent.Exception) {
eventData.uncaught = () => params.data.uncaught;
eventData.exception = () => this.eventDataException(params);
}
this.invokeListener(debugEvent, execState, eventData);
}
handleDebuggerScriptParsed(message) {
const params = message.params;
let eventData = { scriptId : params.scriptId,
script : () => this.eventDataScript(params),
eventType : this.DebugEvent.AfterCompile
}
// TODO(jgruber): Arguments as needed. Still completely missing exec_state,
// and eventData used to contain the script mirror instead of its id.
this.invokeListener(this.DebugEvent.AfterCompile, undefined, eventData,
undefined);
}
handleDebuggerScriptFailedToParse(message) {
const params = message.params;
let eventData = { scriptId : params.scriptId,
script : () => this.eventDataScript(params),
eventType : this.DebugEvent.CompileError
}
// TODO(jgruber): Arguments as needed. Still completely missing exec_state,
// and eventData used to contain the script mirror instead of its id.
this.invokeListener(this.DebugEvent.CompileError, undefined, eventData,
undefined);
}
invokeListener(event, exec_state, event_data, data) {
if (this.listener) {
this.listener(event, exec_state, event_data, data);
}
}
}
// Simulate the debug object generated by --expose-debug-as debug.
var debug = { instance : undefined };
Object.defineProperty(debug, 'Debug', { get: function() {
if (!debug.instance) {
debug.instance = new DebugWrapper();
debug.instance.enable();
}
return debug.instance;
}});
Object.defineProperty(debug, 'ScopeType', { get: function() {
const instance = debug.Debug;
return instance.ScopeType;
}});