// 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', 'uniqueId' ]); const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields))); if (message.id) message.id = ''; 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: '' }; if (val && val[stableIdSymbol]) return ''; 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 '); 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","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(); } getCallFrameUrl(frame) { const {scriptId} = frame.location ? frame.location : frame; return (this._scriptMap.get(scriptId) ?? frame).url; } logCallFrames(callFrames) { for (var frame of callFrames) { var functionName = frame.functionName || '(anonymous)'; var url = this.getCallFrameUrl(frame); 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 || ''} --`); this.logCallFrames(asyncStackTrace.callFrames); if (asyncStackTrace.parentId) InspectorTest.log(' '); 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); } } /** * 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 }; }; // TODO(crbug.com/1303521): Remove `quitOnFailure` once no longer needed. InspectorTest.restartFrameAndWaitForPause = async (callFrames, index, quitOnFailure = true) => { 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); if (quitOnFailure) { InspectorTest.completeTest(); } return; } const { params: { callFrames: pausedCallFrames } } = await pausedPromise; InspectorTest.log('Paused at (after restart):'); await session.logSourceLocation(pausedCallFrames[0].location); return callFrames; };