protocol-test.js 19.1 KB
Newer Older
1 2 3 4 5 6
// 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.

InspectorTest = {};
InspectorTest._dumpInspectorProtocolMessages = false;
7
InspectorTest._commandsForLogging = new Set();
8
InspectorTest._sessions = new Set();
9

10 11 12 13 14
InspectorTest.log = utils.print.bind(utils);
InspectorTest.quitImmediately = utils.quit.bind(utils);

InspectorTest.logProtocolCommandCalls = function(command) {
  InspectorTest._commandsForLogging.add(command);
15 16
}

17 18 19 20 21
InspectorTest.completeTest = function() {
  var promises = [];
  for (var session of InspectorTest._sessions)
    promises.push(session.Protocol.Debugger.disable());
  Promise.all(promises).then(() => utils.quit());
22
}
23

24 25 26 27 28 29
InspectorTest.waitForPendingTasks = function() {
  var promises = [];
  for (var session of InspectorTest._sessions)
    promises.push(session.Protocol.Runtime.evaluate({ expression: "new Promise(r => setTimeout(r, 0))//# sourceURL=wait-for-pending-tasks.js", awaitPromise: true }));
  return Promise.all(promises);
}
30

31 32 33
InspectorTest.startDumpingProtocolMessages = function() {
  InspectorTest._dumpInspectorProtocolMessages = true;
}
34

35
InspectorTest.logMessage = function(originalMessage) {
36 37 38
  const nonStableFields = new Set([
    'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId',
    'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId',
39
    'formatterObjectId', 'debuggerId', 'bodyGetterId', 'uniqueId'
40
  ]);
41 42 43
  const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields)));
  if (message.id)
    message.id = '<messageId>';
44

45
  InspectorTest.logObject(message);
46
  return originalMessage;
47 48 49 50 51 52 53 54 55

  function replacer(stableIdSymbol, nonStableFields, name, val) {
    if (nonStableFields.has(name))
      return `<${name}>`;
    if (name === 'internalProperties') {
      const stableId = val.find(prop => prop.name === '[[StableObjectId]]');
      if (stableId)
        stableId.value[stableIdSymbol] = true;
    }
56 57
    if (name === 'parentId')
      return { id: '<id>' };
58 59 60 61
    if (val && val[stableIdSymbol])
      return '<StablectObjectId>';
    return val;
  }
62
}
63

64
InspectorTest.logObject = function(object, title) {
65 66
  var lines = [];

67
  function dumpValue(value, prefix, prefixWithName) {
68 69 70 71 72 73 74 75 76 77
    if (typeof value === "object" && value !== null) {
      if (value instanceof Array)
        dumpItems(value, prefix, prefixWithName);
      else
        dumpProperties(value, prefix, prefixWithName);
    } else {
      lines.push(prefixWithName + String(value).replace(/\n/g, " "));
    }
  }

78
  function dumpProperties(object, prefix, firstLinePrefix) {
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
    prefix = prefix || "";
    firstLinePrefix = firstLinePrefix || prefix;
    lines.push(firstLinePrefix + "{");

    var propertyNames = Object.keys(object);
    propertyNames.sort();
    for (var i = 0; i < propertyNames.length; ++i) {
      var name = propertyNames[i];
      if (!object.hasOwnProperty(name))
        continue;
      var prefixWithName = "    " + prefix + name + " : ";
      dumpValue(object[name], "    " + prefix, prefixWithName);
    }
    lines.push(prefix + "}");
  }

95
  function dumpItems(object, prefix, firstLinePrefix) {
96 97 98 99 100 101 102 103
    prefix = prefix || "";
    firstLinePrefix = firstLinePrefix || prefix;
    lines.push(firstLinePrefix + "[");
    for (var i = 0; i < object.length; ++i)
      dumpValue(object[i], "    " + prefix, "    " + prefix + "[" + i + "] : ");
    lines.push(prefix + "]");
  }

104
  dumpValue(object, "", title || "");
105 106 107
  InspectorTest.log(lines.join("\n"));
}

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
InspectorTest.decodeBase64 = function(base64) {
  const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

  const paddingLength = base64.match(/=*$/)[0].length;
  const bytesLength = base64.length * 0.75 - paddingLength;

  let bytes = new Uint8Array(bytesLength);

  for (let i = 0, p = 0; i < base64.length; i += 4, p += 3) {
    let bits = 0;
    for (let j = 0; j < 4; j++) {
      bits <<= 6;
      const c = base64[i + j];
      if (c !== '=') bits |= LOOKUP.indexOf(c);
    }
    for (let j = p + 2; j >= p; j--) {
      if (j < bytesLength) bytes[j] = bits;
      bits >>= 8;
    }
  }

  return bytes;
}

132 133 134 135 136 137 138 139
InspectorTest.trimErrorMessage = function(message) {
  if (!message.error || !message.error.data)
    return message;
  message.error.data = message.error.data.replace(/at position \d+/,
                                                  'at <some position>');
  return message;
}

140 141 142
InspectorTest.ContextGroup = class {
  constructor() {
    this.id = utils.createContextGroup();
143 144
  }

145 146 147 148
  createContext(name) {
    utils.createContext(this.id, name || '');
  }

149 150
  schedulePauseOnNextStatement(reason, details) {
    utils.schedulePauseOnNextStatement(this.id, reason, details);
151
  }
152 153 154

  cancelPauseOnNextStatement() {
    utils.cancelPauseOnNextStatement(this.id);
155
  }
156 157 158

  addScript(string, lineOffset, columnOffset, url) {
    utils.compileAndRunWithOrigin(this.id, string, url || '', lineOffset || 0, columnOffset || 0, false);
159 160
  }

161 162 163 164 165 166
  addInlineScript(string, url) {
    const match = (new Error().stack).split('\n')[2].match(/([0-9]+):([0-9]+)/);
    this.addScript(
        string, match[1] * 1, match[1] * 1 + '.addInlineScript('.length, url);
  }

167 168 169
  addModule(string, url, lineOffset, columnOffset) {
    utils.compileAndRunWithOrigin(this.id, string, url, lineOffset || 0, columnOffset || 0, true);
  }
170

171 172 173 174 175 176 177 178
  loadScript(fileName) {
    this.addScript(utils.read(fileName));
  }

  connect() {
    return new InspectorTest.Session(this);
  }

179 180 181 182
  reset() {
    utils.resetContextGroup(this.id);
  }

183
  setupInjectedScriptEnvironment(session) {
184
    let scriptSource = '';
185 186 187 188 189 190
    let getters = ["length","internalConstructorName","subtype","getProperty",
        "objectHasOwnProperty","nullifyPrototype","primitiveTypes",
        "closureTypes","prototype","all","RemoteObject","bind",
        "PropertyDescriptor","object","get","set","value","configurable",
        "enumerable","symbol","getPrototypeOf","nativeAccessorDescriptor",
        "isBuiltin","hasGetter","hasSetter","getOwnPropertyDescriptor",
191
        "description","isOwn","name",
192 193 194 195 196 197 198 199 200
        "typedArrayProperties","keys","getOwnPropertyNames",
        "getOwnPropertySymbols","isPrimitiveValue","com","toLowerCase",
        "ELEMENT","trim","replace","DOCUMENT","size","byteLength","toString",
        "stack","substr","message","indexOf","key","type","unserializableValue",
        "objectId","className","preview","proxyTargetValue","customPreview",
        "CustomPreview","resolve","then","console","error","header","hasBody",
        "stringify","ObjectPreview","ObjectPreviewType","properties",
        "ObjectPreviewSubtype","getInternalProperties","wasThrown","indexes",
        "overflow","valuePreview","entries"];
201 202
    scriptSource += `(function installSettersAndGetters() {
        let defineProperty = Object.defineProperty;
203 204 205 206 207 208
        let ObjectPrototype = Object.prototype;
        let ArrayPrototype = Array.prototype;
        defineProperty(ArrayPrototype, 0, {
          set() { debugger; throw 42; }, get() { debugger; throw 42; },
          __proto__: null
        });`,
209
        scriptSource += getters.map(getter => `
210 211 212 213 214 215 216
        defineProperty(ObjectPrototype, '${getter}', {
          set() { debugger; throw 42; }, get() { debugger; throw 42; },
          __proto__: null
        });
        `).join('\n') + '})();';
    this.addScript(scriptSource);

217
    if (session) {
218 219
      InspectorTest.log('WARNING: setupInjectedScriptEnvironment with debug flag for debugging only and should not be landed.');
      session.setupScriptMap();
220
      session.Protocol.Debugger.enable();
221 222 223 224
      session.Protocol.Debugger.onPaused(message => {
        let callFrames = message.params.callFrames;
        session.logSourceLocations(callFrames.map(frame => frame.location));
      })
225 226
    }
  }
227 228 229 230 231 232 233 234 235 236 237 238
};

InspectorTest.Session = class {
  constructor(contextGroup) {
    this.contextGroup = contextGroup;
    this._dispatchTable = new Map();
    this._eventHandlers = new Map();
    this._requestId = 0;
    this.Protocol = this._setupProtocol();
    InspectorTest._sessions.add(this);
    this.id = utils.connectSession(contextGroup.id, '', this._dispatchMessage.bind(this));
  }
239

240 241 242 243
  disconnect() {
    InspectorTest._sessions.delete(this);
    utils.disconnectSession(this.id);
  }
244

245 246 247 248
  reconnect() {
    var state = utils.disconnectSession(this.id);
    this.id = utils.connectSession(this.contextGroup.id, state, this._dispatchMessage.bind(this));
  }
249 250 251 252

  async addInspectedObject(serializable) {
    return this.Protocol.Runtime.evaluate({expression: `inspector.addInspectedObject(${this.id}, ${JSON.stringify(serializable)})`});
  }
253

254 255 256 257 258 259
  sendRawCommand(requestId, command, handler) {
    if (InspectorTest._dumpInspectorProtocolMessages)
      utils.print("frontend: " + command);
    this._dispatchTable.set(requestId, handler);
    utils.sendMessageToBackend(this.id, command);
  }
260

261 262 263 264 265
  setupScriptMap() {
    if (this._scriptMap)
      return;
    this._scriptMap = new Map();
  }
266

267 268 269 270 271
  getCallFrameUrl(frame) {
    const {scriptId} = frame.location ? frame.location : frame;
    return (this._scriptMap.get(scriptId) ?? frame).url;
  }

272 273 274
  logCallFrames(callFrames) {
    for (var frame of callFrames) {
      var functionName = frame.functionName || '(anonymous)';
275
      var url = this.getCallFrameUrl(frame);
276 277 278 279
      var lineNumber = frame.location ? frame.location.lineNumber : frame.lineNumber;
      var columnNumber = frame.location ? frame.location.columnNumber : frame.columnNumber;
      InspectorTest.log(`${functionName} (${url}:${lineNumber}:${columnNumber})`);
    }
280 281
  }

282 283 284 285 286 287 288 289 290 291 292 293 294
  async getScriptWithSource(scriptId, forceSourceRequest) {
    var script = this._scriptMap.get(scriptId);
    if (forceSourceRequest || !(script.scriptSource || script.bytecode)) {
      var message = await this.Protocol.Debugger.getScriptSource({ scriptId });
      script.scriptSource = message.result.scriptSource;
      if (message.result.bytecode) {
        script.bytecode = InspectorTest.decodeBase64(message.result.bytecode);
      }
    }
    return script;
  }

  async logSourceLocation(location, forceSourceRequest) {
295 296 297 298 299
    var scriptId = location.scriptId;
    if (!this._scriptMap || !this._scriptMap.has(scriptId)) {
      InspectorTest.log("setupScriptMap should be called before Protocol.Debugger.enable.");
      InspectorTest.completeTest();
    }
300
    var script = await this.getScriptWithSource(scriptId, forceSourceRequest);
301

302 303 304 305
    if (script.bytecode) {
      if (location.lineNumber != 0) {
        InspectorTest.log('Unexpected wasm line number: ' + location.lineNumber);
      }
306 307 308 309 310 311 312 313
      let wasm_opcode = script.bytecode[location.columnNumber];
      let opcode_str = wasm_opcode.toString(16);
      if (opcode_str.length % 2) opcode_str = `0${opcode_str}`;
      if (InspectorTest.getWasmOpcodeName) {
        opcode_str += ` (${InspectorTest.getWasmOpcodeName(wasm_opcode)})`;
      }
      InspectorTest.log(`Script ${script.url} byte offset ${
          location.columnNumber}: Wasm opcode 0x${opcode_str}`);
314
    } else {
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
      var lines = script.scriptSource.split('\n');
      var line = lines[location.lineNumber];
      line = line.slice(0, location.columnNumber) + '#' + (line.slice(location.columnNumber) || '');
      lines[location.lineNumber] = line;
      lines = lines.filter(line => line.indexOf('//# sourceURL=') === -1);
      InspectorTest.log(lines.slice(Math.max(location.lineNumber - 1, 0), location.lineNumber + 2).join('\n'));
      InspectorTest.log('');
    }
  }

  logSourceLocations(locations) {
    if (locations.length == 0) return Promise.resolve();
    return this.logSourceLocation(locations[0]).then(() => this.logSourceLocations(locations.splice(1)));
  }

330 331
  async logBreakLocations(inputLocations) {
    let locations = inputLocations.slice();
332
    let scriptId = locations[0].scriptId;
333
    let script = await this.getScriptWithSource(scriptId);
334 335 336 337 338 339 340 341 342 343 344 345
    let lines = script.scriptSource.split('\n');
    locations = locations.sort((loc1, loc2) => {
      if (loc2.lineNumber !== loc1.lineNumber) return loc2.lineNumber - loc1.lineNumber;
      return loc2.columnNumber - loc1.columnNumber;
    });
    for (let location of locations) {
      let line = lines[location.lineNumber];
      line = line.slice(0, location.columnNumber) + locationMark(location.type) + line.slice(location.columnNumber);
      lines[location.lineNumber] = line;
    }
    lines = lines.filter(line => line.indexOf('//# sourceURL=') === -1);
    InspectorTest.log(lines.join('\n') + '\n');
346
    return inputLocations;
347 348 349 350 351 352 353 354 355

    function locationMark(type) {
      if (type === 'return') return '|R|';
      if (type === 'call') return '|C|';
      if (type === 'debuggerStatement') return '|D|';
      return '|_|';
    }
  }

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  async logTypeProfile(typeProfile, source) {
    let entries = typeProfile.entries;

    // Sort in reverse order so we can replace entries without invalidating
    // the other offsets.
    entries = entries.sort((a, b) => b.offset - a.offset);

    for (let entry of entries) {
      source = source.slice(0, entry.offset) + typeAnnotation(entry.types) +
        source.slice(entry.offset);
    }
    InspectorTest.log(source);
    return typeProfile;

    function typeAnnotation(types) {
      return `/*${types.map(t => t.name).join(', ')}*/`;
    }
  }

375 376
  logAsyncStackTrace(asyncStackTrace) {
    while (asyncStackTrace) {
377
      InspectorTest.log(`-- ${asyncStackTrace.description || '<empty>'} --`);
378
      this.logCallFrames(asyncStackTrace.callFrames);
379
      if (asyncStackTrace.parentId) InspectorTest.log('  <external stack>');
380 381 382 383 384
      asyncStackTrace = asyncStackTrace.parent;
    }
  }

  _sendCommandPromise(method, params) {
385 386
    if (typeof params !== 'object')
      utils.print(`WARNING: non-object params passed to invocation of method ${method}`);
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    if (InspectorTest._commandsForLogging.has(method))
      utils.print(method + ' called');
    var requestId = ++this._requestId;
    var messageObject = { "id": requestId, "method": method, "params": params };
    return new Promise(fulfill => this.sendRawCommand(requestId, JSON.stringify(messageObject), fulfill));
  }

  _setupProtocol() {
    return new Proxy({}, { get: (target, agentName, receiver) => new Proxy({}, {
      get: (target, methodName, receiver) => {
        const eventPattern = /^on(ce)?([A-Z][A-Za-z0-9]+)/;
        var match = eventPattern.exec(methodName);
        if (!match)
          return args => this._sendCommandPromise(`${agentName}.${methodName}`, args || {});
        var eventName = match[2];
        eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
        if (match[1])
404 405
          return numOfEvents => this._waitForEventPromise(
                     `${agentName}.${eventName}`, numOfEvents || 1);
406 407 408 409 410 411 412 413 414
        return listener => this._eventHandlers.set(`${agentName}.${eventName}`, listener);
      }
    })});
  }

  _dispatchMessage(messageString) {
    var messageObject = JSON.parse(messageString);
    if (InspectorTest._dumpInspectorProtocolMessages)
      utils.print("backend: " + JSON.stringify(messageObject));
415 416 417 418 419 420
    const kMethodNotFound = -32601;
    if (messageObject.error && messageObject.error.code === kMethodNotFound) {
      InspectorTest.log(`Error: Called non-existent method. ${
          messageObject.error.message} code: ${messageObject.error.code}`);
      InspectorTest.completeTest();
    }
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
    try {
      var messageId = messageObject["id"];
      if (typeof messageId === "number") {
        var handler = this._dispatchTable.get(messageId);
        if (handler) {
          handler(messageObject);
          this._dispatchTable.delete(messageId);
        }
      } else {
        var eventName = messageObject["method"];
        var eventHandler = this._eventHandlers.get(eventName);
        if (this._scriptMap && eventName === "Debugger.scriptParsed")
          this._scriptMap.set(messageObject.params.scriptId, JSON.parse(JSON.stringify(messageObject.params)));
        if (eventName === "Debugger.scriptParsed" && messageObject.params.url === "wait-for-pending-tasks.js")
          return;
        if (eventHandler)
          eventHandler(messageObject);
      }
    } catch (e) {
      InspectorTest.log("Exception when dispatching message: " + e + "\n" + e.stack + "\n message = " + JSON.stringify(messageObject, null, 2));
      InspectorTest.completeTest();
    }
  };

445 446
  _waitForEventPromise(eventName, numOfEvents) {
    let events = [];
447 448
    return new Promise(fulfill => {
      this._eventHandlers.set(eventName, result => {
449 450 451 452 453 454
        --numOfEvents;
        events.push(result);
        if (numOfEvents === 0) {
          delete this._eventHandlers.delete(eventName);
          fulfill(events.length > 1 ? events : events[0]);
        }
455 456 457 458
      });
    });
  }
};
459

460 461
InspectorTest.runTestSuite = function(testSuite) {
  function nextTest() {
462 463 464 465 466 467 468 469 470 471
    if (!testSuite.length) {
      InspectorTest.completeTest();
      return;
    }
    var fun = testSuite.shift();
    InspectorTest.log("\nRunning test: " + fun.name);
    fun(nextTest);
  }
  nextTest();
}
472

473
InspectorTest.runAsyncTestSuite = async function(testSuite) {
474 475 476
  const selected = testSuite.filter(test => test.name.startsWith('f_'));
  if (selected.length)
    testSuite = selected;
477 478
  for (var test of testSuite) {
    InspectorTest.log("\nRunning test: " + test.name);
479 480 481 482 483
    try {
      await test();
    } catch (e) {
      utils.print(e.stack);
    }
484 485 486 487
  }
  InspectorTest.completeTest();
}

488 489 490 491 492 493 494 495
InspectorTest.start = function(description) {
  try {
    InspectorTest.log(description);
    var contextGroup = new InspectorTest.ContextGroup();
    var session = contextGroup.connect();
    return { session: session, contextGroup: contextGroup, Protocol: session.Protocol };
  } catch (e) {
    utils.print(e.stack);
496 497
  }
}
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521

/**
 * Two helper functions for the tests in `debugger/restart-frame/*`.
 */

InspectorTest.evaluateAndWaitForPause = async (expression) => {
  const pausedPromise = Protocol.Debugger.oncePaused();
  const evaluatePromise = Protocol.Runtime.evaluate({ expression });

  const { params: { callFrames } } = await pausedPromise;
  InspectorTest.log('Paused at (after evaluation):');
  await session.logSourceLocation(callFrames[0].location);

  // Ignore the last frame, it's always an anonymous empty frame for the
  // Runtime#evaluate call.
  InspectorTest.log('Pause stack:');
  for (const frame of callFrames.slice(0, -1)) {
    InspectorTest.log(`  ${frame.functionName}:${frame.location.lineNumber} (canBeRestarted = ${frame.canBeRestarted ?? false})`);
  }
  InspectorTest.log('');

  return { callFrames, evaluatePromise };
};

522 523
// TODO(crbug.com/1303521): Remove `quitOnFailure` once no longer needed.
InspectorTest.restartFrameAndWaitForPause = async (callFrames, index, quitOnFailure = true) => {
524 525 526 527 528 529 530 531
  const pausedPromise = Protocol.Debugger.oncePaused();
  const frame = callFrames[index];

  InspectorTest.log(`Restarting function "${frame.functionName}" ...`);
  const response = await Protocol.Debugger.restartFrame({ callFrameId: frame.callFrameId, mode: 'StepInto' });
  if (response.error) {
    InspectorTest.log(`Failed to restart function "${frame.functionName}":`);
    InspectorTest.logMessage(response.error);
532 533 534
    if (quitOnFailure) {
      InspectorTest.completeTest();
    }
535 536 537 538 539 540 541 542 543
    return;
  }

  const { params: { callFrames: pausedCallFrames } } = await pausedPromise;
  InspectorTest.log('Paused at (after restart):');
  await session.logSourceLocation(pausedCallFrames[0].location);

  return callFrames;
};