| // 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; |
| InspectorTest._commandsForLogging = new Set(); |
| InspectorTest._sessions = new Set(); |
| |
| InspectorTest.log = utils.print.bind(utils); |
| InspectorTest.quitImmediately = utils.quit.bind(utils); |
| |
| InspectorTest.logProtocolCommandCalls = function(command) { |
| InspectorTest._commandsForLogging.add(command); |
| } |
| |
| InspectorTest.completeTest = function() { |
| var promises = []; |
| for (var session of InspectorTest._sessions) |
| promises.push(session.Protocol.Debugger.disable()); |
| Promise.all(promises).then(() => utils.quit()); |
| } |
| |
| 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); |
| } |
| |
| InspectorTest.startDumpingProtocolMessages = function() { |
| InspectorTest._dumpInspectorProtocolMessages = true; |
| } |
| |
| InspectorTest.logMessage = function(originalMessage) { |
| const nonStableFields = new Set([ |
| 'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId', |
| 'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId', |
| 'formatterObjectId', 'debuggerId', 'bodyGetterId' |
| ]); |
| const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields))); |
| if (message.id) |
| message.id = '<messageId>'; |
| |
| InspectorTest.logObject(message); |
| return originalMessage; |
| |
| 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; |
| } |
| if (name === 'parentId') |
| return { id: '<id>' }; |
| if (val && val[stableIdSymbol]) |
| return '<StablectObjectId>'; |
| return val; |
| } |
| } |
| |
| InspectorTest.logObject = function(object, title) { |
| var lines = []; |
| |
| function dumpValue(value, prefix, prefixWithName) { |
| 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, " ")); |
| } |
| } |
| |
| function dumpProperties(object, prefix, firstLinePrefix) { |
| 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 + "}"); |
| } |
| |
| function dumpItems(object, prefix, firstLinePrefix) { |
| 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 + "]"); |
| } |
| |
| dumpValue(object, "", title || ""); |
| InspectorTest.log(lines.join("\n")); |
| } |
| |
| 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; |
| } |
| |
| 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; |
| } |
| |
| InspectorTest.ContextGroup = class { |
| constructor() { |
| this.id = utils.createContextGroup(); |
| } |
| |
| createContext(name) { |
| utils.createContext(this.id, name || ''); |
| } |
| |
| schedulePauseOnNextStatement(reason, details) { |
| utils.schedulePauseOnNextStatement(this.id, reason, details); |
| } |
| |
| cancelPauseOnNextStatement() { |
| utils.cancelPauseOnNextStatement(this.id); |
| } |
| |
| addScript(string, lineOffset, columnOffset, url) { |
| utils.compileAndRunWithOrigin(this.id, string, url || '', lineOffset || 0, columnOffset || 0, false); |
| } |
| |
| 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); |
| } |
| |
| addModule(string, url, lineOffset, columnOffset) { |
| utils.compileAndRunWithOrigin(this.id, string, url, lineOffset || 0, columnOffset || 0, true); |
| } |
| |
| loadScript(fileName) { |
| this.addScript(utils.read(fileName)); |
| } |
| |
| connect() { |
| return new InspectorTest.Session(this); |
| } |
| |
| reset() { |
| utils.resetContextGroup(this.id); |
| } |
| |
| setupInjectedScriptEnvironment(session) { |
| let scriptSource = ''; |
| 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", |
| "description","formatAccessorsAsProperties","isOwn","name", |
| "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"]; |
| scriptSource += `(function installSettersAndGetters() { |
| let defineProperty = Object.defineProperty; |
| let ObjectPrototype = Object.prototype; |
| let ArrayPrototype = Array.prototype; |
| defineProperty(ArrayPrototype, 0, { |
| set() { debugger; throw 42; }, get() { debugger; throw 42; }, |
| __proto__: null |
| });`, |
| scriptSource += getters.map(getter => ` |
| defineProperty(ObjectPrototype, '${getter}', { |
| set() { debugger; throw 42; }, get() { debugger; throw 42; }, |
| __proto__: null |
| }); |
| `).join('\n') + '})();'; |
| this.addScript(scriptSource); |
| |
| if (session) { |
| InspectorTest.log('WARNING: setupInjectedScriptEnvironment with debug flag for debugging only and should not be landed.'); |
| session.setupScriptMap(); |
| session.Protocol.Debugger.enable(); |
| session.Protocol.Debugger.onPaused(message => { |
| let callFrames = message.params.callFrames; |
| session.logSourceLocations(callFrames.map(frame => frame.location)); |
| }) |
| } |
| } |
| }; |
| |
| 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)); |
| } |
| |
| disconnect() { |
| InspectorTest._sessions.delete(this); |
| utils.disconnectSession(this.id); |
| } |
| |
| reconnect() { |
| var state = utils.disconnectSession(this.id); |
| this.id = utils.connectSession(this.contextGroup.id, state, this._dispatchMessage.bind(this)); |
| } |
| |
| async addInspectedObject(serializable) { |
| return this.Protocol.Runtime.evaluate({expression: `inspector.addInspectedObject(${this.id}, ${JSON.stringify(serializable)})`}); |
| } |
| |
| sendRawCommand(requestId, command, handler) { |
| if (InspectorTest._dumpInspectorProtocolMessages) |
| utils.print("frontend: " + command); |
| this._dispatchTable.set(requestId, handler); |
| utils.sendMessageToBackend(this.id, command); |
| } |
| |
| setupScriptMap() { |
| if (this._scriptMap) |
| return; |
| this._scriptMap = new Map(); |
| } |
| |
| logCallFrames(callFrames) { |
| for (var frame of callFrames) { |
| var functionName = frame.functionName || '(anonymous)'; |
| var scriptId = frame.location ? frame.location.scriptId : frame.scriptId; |
| var url = frame.url ? frame.url : this._scriptMap.get(scriptId).url; |
| var lineNumber = frame.location ? frame.location.lineNumber : frame.lineNumber; |
| var columnNumber = frame.location ? frame.location.columnNumber : frame.columnNumber; |
| InspectorTest.log(`${functionName} (${url}:${lineNumber}:${columnNumber})`); |
| } |
| } |
| |
| 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) { |
| var scriptId = location.scriptId; |
| if (!this._scriptMap || !this._scriptMap.has(scriptId)) { |
| InspectorTest.log("setupScriptMap should be called before Protocol.Debugger.enable."); |
| InspectorTest.completeTest(); |
| } |
| var script = await this.getScriptWithSource(scriptId, forceSourceRequest); |
| |
| if (script.bytecode) { |
| if (location.lineNumber != 0) { |
| InspectorTest.log('Unexpected wasm line number: ' + location.lineNumber); |
| } |
| 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}`); |
| } else { |
| 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))); |
| } |
| |
| async logBreakLocations(inputLocations) { |
| let locations = inputLocations.slice(); |
| let scriptId = locations[0].scriptId; |
| let script = await this.getScriptWithSource(scriptId); |
| 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'); |
| return inputLocations; |
| |
| function locationMark(type) { |
| if (type === 'return') return '|R|'; |
| if (type === 'call') return '|C|'; |
| if (type === 'debuggerStatement') return '|D|'; |
| return '|_|'; |
| } |
| } |
| |
| 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(', ')}*/`; |
| } |
| } |
| |
| logAsyncStackTrace(asyncStackTrace) { |
| while (asyncStackTrace) { |
| InspectorTest.log(`-- ${asyncStackTrace.description || '<empty>'} --`); |
| this.logCallFrames(asyncStackTrace.callFrames); |
| if (asyncStackTrace.parentId) InspectorTest.log(' <external stack>'); |
| asyncStackTrace = asyncStackTrace.parent; |
| } |
| } |
| |
| _sendCommandPromise(method, params) { |
| if (typeof params !== 'object') |
| utils.print(`WARNING: non-object params passed to invocation of method ${method}`); |
| 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]) |
| return numOfEvents => this._waitForEventPromise( |
| `${agentName}.${eventName}`, numOfEvents || 1); |
| return listener => this._eventHandlers.set(`${agentName}.${eventName}`, listener); |
| } |
| })}); |
| } |
| |
| _dispatchMessage(messageString) { |
| var messageObject = JSON.parse(messageString); |
| if (InspectorTest._dumpInspectorProtocolMessages) |
| utils.print("backend: " + JSON.stringify(messageObject)); |
| 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(); |
| } |
| 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(); |
| } |
| }; |
| |
| _waitForEventPromise(eventName, numOfEvents) { |
| let events = []; |
| return new Promise(fulfill => { |
| this._eventHandlers.set(eventName, result => { |
| --numOfEvents; |
| events.push(result); |
| if (numOfEvents === 0) { |
| delete this._eventHandlers.delete(eventName); |
| fulfill(events.length > 1 ? events : events[0]); |
| } |
| }); |
| }); |
| } |
| }; |
| |
| InspectorTest.runTestSuite = function(testSuite) { |
| function nextTest() { |
| if (!testSuite.length) { |
| InspectorTest.completeTest(); |
| return; |
| } |
| var fun = testSuite.shift(); |
| InspectorTest.log("\nRunning test: " + fun.name); |
| fun(nextTest); |
| } |
| nextTest(); |
| } |
| |
| InspectorTest.runAsyncTestSuite = async function(testSuite) { |
| const selected = testSuite.filter(test => test.name.startsWith('f_')); |
| if (selected.length) |
| testSuite = selected; |
| for (var test of testSuite) { |
| InspectorTest.log("\nRunning test: " + test.name); |
| try { |
| await test(); |
| } catch (e) { |
| utils.print(e.stack); |
| } |
| } |
| InspectorTest.completeTest(); |
| } |
| |
| 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); |
| } |
| } |