diff --git a/test/common/README.md b/test/common/README.md index aa3fcb3d4a..59b02cf52a 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -99,6 +99,15 @@ Tests whether `name` and `expected` are part of a raised warning. Checks if `pathname` exists +### fires(promise, [error], [timeoutMs]) +* promise [<Promise] +* error [<String] default = 'timeout' +* timeoutMs [<Number] default = 100 + +Returns a new promise that will propagate `promise` resolution or rejection if +that happens within the `timeoutMs` timespan, or rejects with `error` as +a reason otherwise. + ### fixturesDir * return [<String>] diff --git a/test/common/index.js b/test/common/index.js index 54742319d2..2564b227fe 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -814,6 +814,32 @@ function restoreWritable(name) { delete process[name].writeTimes; } +function onResolvedOrRejected(promise, callback) { + return promise.then((result) => { + callback(); + return result; + }, (error) => { + callback(); + throw error; + }); +} + +function timeoutPromise(error, timeoutMs) { + let clearCallback = null; + let done = false; + const promise = onResolvedOrRejected(new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(error), timeoutMs); + clearCallback = () => { + if (done) + return; + clearTimeout(timeout); + resolve(); + }; + }), () => done = true); + promise.clear = clearCallback; + return promise; +} + exports.hijackStdout = hijackStdWritable.bind(null, 'stdout'); exports.hijackStderr = hijackStdWritable.bind(null, 'stderr'); exports.restoreStdout = restoreWritable.bind(null, 'stdout'); @@ -827,3 +853,19 @@ exports.firstInvalidFD = function firstInvalidFD() { } catch (e) {} return fd; }; + +exports.fires = function fires(promise, error, timeoutMs) { + if (!timeoutMs && util.isNumber(error)) { + timeoutMs = error; + error = null; + } + if (!error) + error = 'timeout'; + if (!timeoutMs) + timeoutMs = 100; + const timeout = timeoutPromise(error, timeoutMs); + return Promise.race([ + onResolvedOrRejected(promise, () => timeout.clear()), + timeout + ]); +}; diff --git a/test/inspector/inspector-helper.js b/test/inspector/inspector-helper.js index 2f45e21c5b..b316f04aea 100644 --- a/test/inspector/inspector-helper.js +++ b/test/inspector/inspector-helper.js @@ -4,61 +4,61 @@ const assert = require('assert'); const fs = require('fs'); const http = require('http'); const path = require('path'); -const spawn = require('child_process').spawn; +const { spawn } = require('child_process'); const url = require('url'); +const _MAINSCRIPT = path.join(common.fixturesDir, 'loop.js'); const DEBUG = false; const TIMEOUT = 15 * 1000; -const EXPECT_ALIVE_SYMBOL = Symbol('isAlive'); -const DONT_EXPECT_RESPONSE_SYMBOL = Symbol('dontExpectResponse'); -const mainScript = path.join(common.fixturesDir, 'loop.js'); -function send(socket, message, id, callback) { - const msg = JSON.parse(JSON.stringify(message)); // Clone! - msg['id'] = id; - if (DEBUG) - console.log('[sent]', JSON.stringify(msg)); - const messageBuf = Buffer.from(JSON.stringify(msg)); - - const wsHeaderBuf = Buffer.allocUnsafe(16); - wsHeaderBuf.writeUInt8(0x81, 0); - let byte2 = 0x80; - const bodyLen = messageBuf.length; - - let maskOffset = 2; - if (bodyLen < 126) { - byte2 = 0x80 + bodyLen; - } else if (bodyLen < 65536) { - byte2 = 0xFE; - wsHeaderBuf.writeUInt16BE(bodyLen, 2); - maskOffset = 4; +function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { + const args = [].concat(inspectorFlags); + if (scriptContents) { + args.push('-e', scriptContents); } else { - byte2 = 0xFF; - wsHeaderBuf.writeUInt32BE(bodyLen, 2); - wsHeaderBuf.writeUInt32BE(0, 6); - maskOffset = 10; + args.push(scriptFile); } - wsHeaderBuf.writeUInt8(byte2, 1); - wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset); + const child = spawn(process.execPath, args); - for (let i = 0; i < messageBuf.length; i++) - messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); - socket.write( - Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]), - callback); + const handler = tearDown.bind(null, child); + process.on('exit', handler); + process.on('uncaughtException', handler); + process.on('unhandledRejection', handler); + process.on('SIGINT', handler); + + return child; +} + +function makeBufferingDataCallback(dataCallback) { + let buffer = Buffer.alloc(0); + return (data) => { + const newData = Buffer.concat([buffer, data]); + const str = newData.toString('utf8'); + const lines = str.split('\n'); + if (str.endsWith('\n')) + buffer = Buffer.alloc(0); + else + buffer = Buffer.from(lines.pop(), 'utf8'); + for (const line of lines) + dataCallback(line); + }; } -function sendEnd(socket) { - socket.write(Buffer.from([0x88, 0x80, 0x2D, 0x0E, 0x1E, 0xFA])); +function tearDown(child, err) { + child.kill(); + if (err) { + console.error(err); + process.exit(1); + } } -function parseWSFrame(buffer, handler) { +function parseWSFrame(buffer) { // Protocol described in https://tools.ietf.org/html/rfc6455#section-5 + let message = null; if (buffer.length < 2) - return 0; + return { length: 0, message }; if (buffer[0] === 0x88 && buffer[1] === 0x00) { - handler(null); - return 2; + return { length: 2, message, closed: true }; } assert.strictEqual(0x81, buffer[0]); let dataLen = 0x7F & buffer[1]; @@ -74,482 +74,331 @@ function parseWSFrame(buffer, handler) { bodyOffset = 10; } if (buffer.length < bodyOffset + dataLen) - return 0; - const jsonPayload = - buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8'); - let message; - try { - message = JSON.parse(jsonPayload); - } catch (e) { - console.error(`JSON.parse() failed for: ${jsonPayload}`); - throw e; - } + return { length: 0, message }; + message = JSON.parse( + buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8')); if (DEBUG) console.log('[received]', JSON.stringify(message)); - handler(message); - return bodyOffset + dataLen; + return { length: bodyOffset + dataLen, message }; } -function tearDown(child, err) { - child.kill(); - if (err instanceof Error) { - console.error(err.stack); - process.exit(1); - } -} +function formatWSFrame(message) { + const messageBuf = Buffer.from(JSON.stringify(message)); -function checkHttpResponse(host, port, path, callback, errorcb) { - const req = http.get({ host, port, path }, function(res) { - let response = ''; - res.setEncoding('utf8'); - res - .on('data', (data) => response += data.toString()) - .on('end', () => { - let err = null; - let json = undefined; - try { - json = JSON.parse(response); - } catch (e) { - err = e; - err.response = response; - } - callback(err, json); - }); - }); - if (errorcb) - req.on('error', errorcb); -} - -function makeBufferingDataCallback(dataCallback) { - let buffer = Buffer.alloc(0); - return (data) => { - const newData = Buffer.concat([buffer, data]); - const str = newData.toString('utf8'); - const lines = str.split('\n'); - if (str.endsWith('\n')) - buffer = Buffer.alloc(0); - else - buffer = Buffer.from(lines.pop(), 'utf8'); - for (const line of lines) - dataCallback(line); - }; -} + const wsHeaderBuf = Buffer.allocUnsafe(16); + wsHeaderBuf.writeUInt8(0x81, 0); + let byte2 = 0x80; + const bodyLen = messageBuf.length; -function timeout(message, multiplicator) { - return setTimeout(common.mustNotCall(message), - TIMEOUT * (multiplicator || 1)); -} + let maskOffset = 2; + if (bodyLen < 126) { + byte2 = 0x80 + bodyLen; + } else if (bodyLen < 65536) { + byte2 = 0xFE; + wsHeaderBuf.writeUInt16BE(bodyLen, 2); + maskOffset = 4; + } else { + byte2 = 0xFF; + wsHeaderBuf.writeUInt32BE(bodyLen, 2); + wsHeaderBuf.writeUInt32BE(0, 6); + maskOffset = 10; + } + wsHeaderBuf.writeUInt8(byte2, 1); + wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset); -function TestSession(socket, harness) { - this.mainScriptPath = harness.mainScriptPath; - this.mainScriptId = null; - - this.harness_ = harness; - this.socket_ = socket; - this.expectClose_ = false; - this.scripts_ = {}; - this.messagefilter_ = null; - this.responseCheckers_ = {}; - this.lastId_ = 0; - this.messages_ = {}; - this.expectedId_ = 1; - this.lastMessageResponseCallback_ = null; - this.closeCallback_ = null; + for (let i = 0; i < messageBuf.length; i++) + messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); - let buffer = Buffer.alloc(0); - socket.on('data', (data) => { - buffer = Buffer.concat([buffer, data]); - let consumed; - do { - consumed = parseWSFrame(buffer, this.processMessage_.bind(this)); - if (consumed) - buffer = buffer.slice(consumed); - } while (consumed); - }).on('close', () => { - assert(this.expectClose_, 'Socket closed prematurely'); - this.closeCallback_ && this.closeCallback_(); - }); + return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]); } -TestSession.prototype.scriptUrlForId = function(id) { - return this.scripts_[id]; -}; - -TestSession.prototype.processMessage_ = function(message) { - if (message === null) { - sendEnd(this.socket_); - return; +class InspectorSession { + constructor(socket, instance) { + this._instance = instance; + this._socket = socket; + this._nextId = 1; + this._commandResponsePromises = new Map(); + this._unprocessedNotifications = []; + this._notificationCallback = null; + this._scriptsIdsByUrl = new Map(); + + let buffer = Buffer.alloc(0); + socket.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + do { + const { length, message, closed } = parseWSFrame(buffer); + if (!length) + break; + + if (closed) { + socket.write(Buffer.from([0x88, 0x00])); // WS close frame + } + buffer = buffer.slice(length); + if (message) + this._onMessage(message); + } while (true); + }); + this._terminationPromise = new Promise((resolve) => { + socket.once('close', resolve); + }); } - const method = message['method']; - if (method === 'Debugger.scriptParsed') { - const script = message['params']; - const scriptId = script['scriptId']; - const url = script['url']; - this.scripts_[scriptId] = url; - if (url === mainScript) - this.mainScriptId = scriptId; + waitForServerDisconnect() { + return this._terminationPromise; } - this.messagefilter_ && this.messagefilter_(message); - const id = message['id']; - if (id) { - this.expectedId_++; - if (this.responseCheckers_[id]) { - const messageJSON = JSON.stringify(message); - const idJSON = JSON.stringify(this.messages_[id]); - assert(message['result'], `${messageJSON} (response to ${idJSON})`); - this.responseCheckers_[id](message['result']); - delete this.responseCheckers_[id]; - } - const messageJSON = JSON.stringify(message); - const idJSON = JSON.stringify(this.messages_[id]); - assert(!message['error'], `${messageJSON} (replying to ${idJSON})`); - delete this.messages_[id]; - if (id === this.lastId_) { - this.lastMessageResponseCallback_ && this.lastMessageResponseCallback_(); - this.lastMessageResponseCallback_ = null; - } + + disconnect() { + this._socket.destroy(); } -}; -TestSession.prototype.sendAll_ = function(commands, callback) { - if (!commands.length) { - callback(); - } else { - let id = ++this.lastId_; - let command = commands[0]; - if (command instanceof Array) { - this.responseCheckers_[id] = command[1]; - command = command[0]; - } - if (command instanceof Function) - command = command(); - if (!command[DONT_EXPECT_RESPONSE_SYMBOL]) { - this.messages_[id] = command; + _onMessage(message) { + if (message.id) { + const { resolve, reject } = this._commandResponsePromises.get(message.id); + this._commandResponsePromises.delete(message.id); + if (message.result) + resolve(message.result); + else + reject(message.error); } else { - id += 100000; - this.lastId_--; + if (message.method === 'Debugger.scriptParsed') { + const script = message['params']; + const scriptId = script['scriptId']; + const url = script['url']; + this._scriptsIdsByUrl.set(scriptId, url); + if (url === _MAINSCRIPT) + this.mainScriptId = scriptId; + } + + if (this._notificationCallback) { + // In case callback needs to install another + const callback = this._notificationCallback; + this._notificationCallback = null; + callback(message); + } else { + this._unprocessedNotifications.push(message); + } } - send(this.socket_, command, id, - () => this.sendAll_(commands.slice(1), callback)); } -}; -TestSession.prototype.sendInspectorCommands = function(commands) { - if (!(commands instanceof Array)) - commands = [commands]; - return this.enqueue((callback) => { - let timeoutId = null; - this.lastMessageResponseCallback_ = () => { - timeoutId && clearTimeout(timeoutId); - callback(); - }; - this.sendAll_(commands, () => { - timeoutId = setTimeout(() => { - assert.fail(`Messages without response: ${ - Object.keys(this.messages_).join(', ')}`); - }, TIMEOUT); - }); - }); -}; + _sendMessage(message) { + const msg = JSON.parse(JSON.stringify(message)); // Clone! + msg['id'] = this._nextId++; + if (DEBUG) + console.log('[sent]', JSON.stringify(msg)); -TestSession.prototype.sendCommandsAndExpectClose = function(commands) { - if (!(commands instanceof Array)) - commands = [commands]; - return this.enqueue((callback) => { - let timeoutId = null; - let done = false; - this.expectClose_ = true; - this.closeCallback_ = function() { - if (timeoutId) - clearTimeout(timeoutId); - done = true; - callback(); - }; - this.sendAll_(commands, () => { - if (!done) { - timeoutId = timeout('Session still open'); - } + const responsePromise = new Promise((resolve, reject) => { + this._commandResponsePromises.set(msg['id'], { resolve, reject }); }); - }); -}; -TestSession.prototype.createCallbackWithTimeout_ = function(message) { - const promise = new Promise((resolve) => { - this.enqueue((callback) => { - const timeoutId = timeout(message); - resolve(() => { - clearTimeout(timeoutId); - callback(); - }); - }); - }); - return () => promise.then((callback) => callback()); -}; + return new Promise( + (resolve) => this._socket.write(formatWSFrame(msg), resolve)) + .then(() => responsePromise); + } -TestSession.prototype.expectMessages = function(expects) { - if (!(expects instanceof Array)) expects = [ expects ]; - - const callback = this.createCallbackWithTimeout_( - `Matching response was not received:\n${expects[0]}`); - this.messagefilter_ = (message) => { - if (expects[0](message)) - expects.shift(); - if (!expects.length) { - this.messagefilter_ = null; - callback(); + send(commands) { + if (Array.isArray(commands)) { + // Multiple commands means the response does not matter. There might even + // never be a response. + return Promise + .all(commands.map((command) => this._sendMessage(command))) + .then(() => {}); + } else { + return this._sendMessage(commands); } - }; - return this; -}; - -TestSession.prototype.expectStderrOutput = function(regexp) { - this.harness_.addStderrFilter( - regexp, - this.createCallbackWithTimeout_(`Timed out waiting for ${regexp}`)); - return this; -}; - -TestSession.prototype.runNext_ = function() { - if (this.task_) { - setImmediate(() => { - this.task_(() => { - this.task_ = this.task_.next_; - this.runNext_(); - }); - }); } -}; -TestSession.prototype.enqueue = function(task) { - if (!this.task_) { - this.task_ = task; - this.runNext_(); - } else { - let t = this.task_; - while (t.next_) - t = t.next_; - t.next_ = task; + waitForNotification(methodOrPredicate, description) { + const desc = description || methodOrPredicate; + const message = `Timed out waiting for matching notification (${desc}))`; + return common.fires( + this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT); } - return this; -}; - -TestSession.prototype.disconnect = function(childDone) { - return this.enqueue((callback) => { - this.expectClose_ = true; - this.socket_.destroy(); - console.log('[test]', 'Connection terminated'); - callback(); - }, childDone); -}; - -TestSession.prototype.expectClose = function() { - return this.enqueue((callback) => { - this.expectClose_ = true; - callback(); - }); -}; - -TestSession.prototype.assertClosed = function() { - return this.enqueue((callback) => { - assert.strictEqual(this.closed_, true); - callback(); - }); -}; - -TestSession.prototype.testHttpResponse = function(path, check) { - return this.enqueue((callback) => - checkHttpResponse(null, this.harness_.port, path, (err, response) => { - check.call(this, err, response); - callback(); - })); -}; - -function Harness(port, childProcess) { - this.port = port; - this.mainScriptPath = mainScript; - this.stderrFilters_ = []; - this.process_ = childProcess; - this.result_ = {}; - this.running_ = true; - - childProcess.stdout.on('data', makeBufferingDataCallback( - (line) => console.log('[out]', line))); - - - childProcess.stderr.on('data', makeBufferingDataCallback((message) => { - const pending = []; - console.log('[err]', message); - for (const filter of this.stderrFilters_) - if (!filter(message)) pending.push(filter); - this.stderrFilters_ = pending; - })); - childProcess.on('exit', (code, signal) => { - this.result_ = { code, signal }; - this.running_ = false; - }); -} + async _asyncWaitForNotification(methodOrPredicate) { + function matchMethod(notification) { + return notification.method === methodOrPredicate; + } + const predicate = + typeof methodOrPredicate === 'string' ? matchMethod : methodOrPredicate; + let notification = null; + do { + if (this._unprocessedNotifications.length) { + notification = this._unprocessedNotifications.shift(); + } else { + notification = await new Promise( + (resolve) => this._notificationCallback = resolve); + } + } while (!predicate(notification)); + return notification; + } -Harness.prototype.addStderrFilter = function(regexp, callback) { - this.stderrFilters_.push((message) => { - if (message.match(regexp)) { - callback(); + _isBreakOnLineNotification(message, line, url) { + if ('Debugger.paused' === message['method']) { + const callFrame = message['params']['callFrames'][0]; + const location = callFrame['location']; + assert.strictEqual(url, this._scriptsIdsByUrl.get(location['scriptId'])); + assert.strictEqual(line, location['lineNumber']); return true; } - }); -}; - -Harness.prototype.assertStillAlive = function() { - assert.strictEqual(this.running_, true, - `Child died: ${JSON.stringify(this.result_)}`); -}; - -Harness.prototype.run_ = function() { - setImmediate(() => { - if (!this.task_[EXPECT_ALIVE_SYMBOL]) - this.assertStillAlive(); - this.task_(() => { - this.task_ = this.task_.next_; - if (this.task_) - this.run_(); - }); - }); -}; + } -Harness.prototype.enqueue_ = function(task, expectAlive) { - task[EXPECT_ALIVE_SYMBOL] = !!expectAlive; - if (!this.task_) { - this.task_ = task; - this.run_(); - } else { - let chain = this.task_; - while (chain.next_) - chain = chain.next_; - chain.next_ = task; + waitForBreakOnLine(line, url) { + return this + .waitForNotification( + (notification) => + this._isBreakOnLineNotification(notification, line, url), + `break on ${url}:${line}`) + .then((notification) => + notification.params.callFrames[0].scopeChain[0].object.objectId); } - return this; -}; -Harness.prototype.testHttpResponse = function(host, path, check, errorcb) { - return this.enqueue_((doneCallback) => { - function wrap(callback) { - if (callback) { - return function() { - callback(...arguments); - doneCallback(); - }; + _matchesConsoleOutputNotification(notification, type, values) { + if (!Array.isArray(values)) + values = [ values ]; + if ('Runtime.consoleAPICalled' === notification['method']) { + const params = notification['params']; + if (params['type'] === type) { + let i = 0; + for (const value of params['args']) { + if (value['value'] !== values[i++]) + return false; + } + return i === values.length; } } - checkHttpResponse(host, this.port, path, wrap(check), wrap(errorcb)); - }); -}; + } -Harness.prototype.wsHandshake = function(devtoolsUrl, tests, readyCallback) { - http.get({ - port: this.port, - path: url.parse(devtoolsUrl).path, - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Key': 'key==' - } - }).on('upgrade', (message, socket) => { - const session = new TestSession(socket, this); - if (!(tests instanceof Array)) - tests = [tests]; - function enqueue(tests) { - session.enqueue((sessionCb) => { - if (tests.length) { - tests[0](session); - session.enqueue((cb2) => { - enqueue(tests.slice(1)); - cb2(); - }); - } else { - readyCallback(); - } - sessionCb(); - }); - } - enqueue(tests); - }).on('response', common.mustNotCall('Upgrade was not received')); -}; + waitForConsoleOutput(type, values) { + const desc = `Console output matching ${JSON.stringify(values)}`; + return this.waitForNotification( + (notification) => this._matchesConsoleOutputNotification(notification, + type, values), + desc); + } -Harness.prototype.runFrontendSession = function(tests) { - return this.enqueue_((callback) => { - checkHttpResponse(null, this.port, '/json/list', (err, response) => { - assert.ifError(err); - this.wsHandshake(response[0]['webSocketDebuggerUrl'], tests, callback); + async runToCompletion() { + console.log('[test]', 'Verify node waits for the frontend to disconnect'); + await this.send({ 'method': 'Debugger.resume' }); + await this.waitForNotification((notification) => { + return notification.method === 'Runtime.executionContextDestroyed' && + notification.params.executionContextId === 1; }); - }); -}; + while ((await this._instance.nextStderrString()) !== + 'Waiting for the debugger to disconnect...'); + await this.disconnect(); + } +} -Harness.prototype.expectShutDown = function(errorCode) { - this.enqueue_((callback) => { - if (this.running_) { - const timeoutId = timeout('Have not terminated'); - this.process_.on('exit', (code, signal) => { - clearTimeout(timeoutId); - assert.strictEqual(errorCode, code, JSON.stringify({ code, signal })); - callback(); +class NodeInstance { + constructor(inspectorFlags = ['--inspect-brk=0'], + scriptContents = '', + scriptFile = _MAINSCRIPT) { + this._portCallback = null; + this.portPromise = new Promise((resolve) => this._portCallback = resolve); + this._process = spawnChildProcess(inspectorFlags, scriptContents, + scriptFile); + this._running = true; + this._stderrLineCallback = null; + this._unprocessedStderrLines = []; + + this._process.stdout.on('data', makeBufferingDataCallback( + (line) => console.log('[out]', line))); + + this._process.stderr.on('data', makeBufferingDataCallback( + (message) => this.onStderrLine(message))); + + this._shutdownPromise = new Promise((resolve) => { + this._process.once('exit', (exitCode, signal) => { + resolve({ exitCode, signal }); + this._running = false; }); + }); + } + + onStderrLine(line) { + console.log('[err]', line); + if (this._portCallback) { + const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/); + if (matches) + this._portCallback(matches[1]); + this._portCallback = null; + } + if (this._stderrLineCallback) { + this._stderrLineCallback(line); + this._stderrLineCallback = null; } else { - assert.strictEqual(errorCode, this.result_.code); - callback(); + this._unprocessedStderrLines.push(line); } - }, true); -}; - -Harness.prototype.kill = function() { - return this.enqueue_((callback) => { - this.process_.kill(); - callback(); - }); -}; - -exports.startNodeForInspectorTest = function(callback, - inspectorFlags = ['--inspect-brk'], - scriptContents = '', - scriptFile = mainScript) { - const args = [].concat(inspectorFlags); - if (scriptContents) { - args.push('-e', scriptContents); - } else { - args.push(scriptFile); } - const child = spawn(process.execPath, args); - - const timeoutId = timeout('Child process did not start properly', 4); + httpGet(host, path) { + console.log('[test]', `Testing ${path}`); + return this.portPromise.then((port) => new Promise((resolve, reject) => { + const req = http.get({ host, port, path }, (res) => { + let response = ''; + res.setEncoding('utf8'); + res + .on('data', (data) => response += data.toString()) + .on('end', () => { + resolve(response); + }); + }); + req.on('error', reject); + })).then((response) => { + try { + return JSON.parse(response); + } catch (e) { + e.body = response; + throw e; + } + }); + } - let found = false; + wsHandshake(devtoolsUrl) { + return this.portPromise.then((port) => new Promise((resolve) => { + http.get({ + port, + path: url.parse(devtoolsUrl).path, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Key': 'key==' + } + }).on('upgrade', (message, socket) => { + resolve(new InspectorSession(socket, this)); + }).on('response', common.mustNotCall('Upgrade was not received')); + })); + } - const dataCallback = makeBufferingDataCallback((text) => { - clearTimeout(timeoutId); - console.log('[err]', text); - if (found) return; - const match = text.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/); - found = true; - child.stderr.removeListener('data', dataCallback); - assert.ok(match, text); - callback(new Harness(match[1], child)); - }); + async connectInspectorSession() { + console.log('[test]', 'Connecting to a child Node process'); + const response = await this.httpGet(null, '/json/list'); + const url = response[0]['webSocketDebuggerUrl']; + return await this.wsHandshake(url); + } - child.stderr.on('data', dataCallback); + expectShutdown() { + return this._shutdownPromise; + } - const handler = tearDown.bind(null, child); + nextStderrString() { + if (this._unprocessedStderrLines.length) + return Promise.resolve(this._unprocessedStderrLines.shift()); + return new Promise((resolve) => this._stderrLineCallback = resolve); + } - process.on('exit', handler); - process.on('uncaughtException', handler); - process.on('SIGINT', handler); -}; + kill() { + this._process.kill(); + } +} -exports.mainScriptSource = function() { - return fs.readFileSync(mainScript, 'utf8'); -}; +function readMainScriptSource() { + return fs.readFileSync(_MAINSCRIPT, 'utf8'); +} -exports.markMessageNoResponse = function(message) { - message[DONT_EXPECT_RESPONSE_SYMBOL] = true; +module.exports = { + mainScriptPath: _MAINSCRIPT, + readMainScriptSource, + NodeInstance }; diff --git a/test/inspector/test-break-when-eval.js b/test/inspector/test-break-when-eval.js new file mode 100644 index 0000000000..ddd8220bb9 --- /dev/null +++ b/test/inspector/test-break-when-eval.js @@ -0,0 +1,68 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +const assert = require('assert'); +const { NodeInstance } = require('./inspector-helper.js'); +const path = require('path'); + +const script = path.join(path.dirname(module.filename), 'global-function.js'); + +async function setupDebugger(session) { + console.log('[test]', 'Setting up a debugger'); + const commands = [ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 0 } }, + { 'method': 'Runtime.runIfWaitingForDebugger' }, + ]; + session.send(commands); + await session.waitForNotification('Runtime.consoleAPICalled'); +} + +async function breakOnLine(session) { + console.log('[test]', 'Breaking in the code'); + const commands = [ + { 'method': 'Debugger.setBreakpointByUrl', + 'params': { 'lineNumber': 9, + 'url': script, + 'columnNumber': 0, + 'condition': '' + } + }, + { 'method': 'Runtime.evaluate', + 'params': { 'expression': 'sum()', + 'objectGroup': 'console', + 'includeCommandLineAPI': true, + 'silent': false, + 'contextId': 1, + 'returnByValue': false, + 'generatePreview': true, + 'userGesture': true, + 'awaitPromise': false + } + } + ]; + session.send(commands); + await session.waitForBreakOnLine(9, script); +} + +async function stepOverConsoleStatement(session) { + console.log('[test]', 'Step over console statement and test output'); + session.send({ 'method': 'Debugger.stepOver' }); + await session.waitForConsoleOutput('log', [0, 3]); + await session.waitForNotification('Debugger.paused'); +} + +async function runTests() { + const child = new NodeInstance(['--inspect=0'], undefined, script); + const session = await child.connectInspectorSession(); + await setupDebugger(session); + await breakOnLine(session); + await stepOverConsoleStatement(session); + await session.runToCompletion(); + assert.strictEqual(0, (await child.expectShutdown()).exitCode); +} + +common.crashOnUnhandledRejection(); +runTests(); diff --git a/test/inspector/test-debug-brk-flag.js b/test/inspector/test-debug-brk-flag.js new file mode 100644 index 0000000000..f0a4d97602 --- /dev/null +++ b/test/inspector/test-debug-brk-flag.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { mainScriptPath, + NodeInstance } = require('./inspector-helper.js'); + +async function testBreakpointOnStart(session) { + const commands = [ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setPauseOnExceptions', + 'params': { 'state': 'none' } }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 0 } }, + { 'method': 'Profiler.enable' }, + { 'method': 'Profiler.setSamplingInterval', + 'params': { 'interval': 100 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]; + + session.send(commands); + await session.waitForBreakOnLine(0, mainScriptPath); +} + +async function runTests() { + const child = new NodeInstance(['--inspect', '--debug-brk']); + const session = await child.connectInspectorSession(); + + await testBreakpointOnStart(session); + await session.runToCompletion(); + + assert.strictEqual(55, (await child.expectShutdown()).exitCode); +} + +common.crashOnUnhandledRejection(); +runTests(); diff --git a/test/inspector/test-debug-end.js b/test/inspector/test-debug-end.js new file mode 100644 index 0000000000..57ce019083 --- /dev/null +++ b/test/inspector/test-debug-end.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +const { strictEqual } = require('assert'); +const { NodeInstance } = require('./inspector-helper.js'); + +async function testNoServerNoCrash() { + console.log('Test there\'s no crash stopping server that was not started'); + const instance = new NodeInstance([], + `process._debugEnd(); + process.exit(42);`); + strictEqual(42, (await instance.expectShutdown()).exitCode); +} + +async function testNoSessionNoCrash() { + console.log('Test there\'s no crash stopping server without connecting'); + const instance = new NodeInstance('--inspect=0', + 'process._debugEnd();process.exit(42);'); + strictEqual(42, (await instance.expectShutdown()).exitCode); +} + +async function testSessionNoCrash() { + console.log('Test there\'s no crash stopping server after connecting'); + const script = `process._debugEnd(); + process._debugProcess(process.pid); + setTimeout(() => { + console.log("Done"); + process.exit(42); + });`; + + const instance = new NodeInstance('--inspect-brk=0', script); + const session = await instance.connectInspectorSession(); + await session.send({ 'method': 'Runtime.runIfWaitingForDebugger' }); + await session.waitForServerDisconnect(); + strictEqual(42, (await instance.expectShutdown()).exitCode); +} + +async function runTest() { + await testNoServerNoCrash(); + await testNoSessionNoCrash(); + await testSessionNoCrash(); +} + +common.crashOnUnhandledRejection(); + +runTest(); diff --git a/test/inspector/test-exception.js b/test/inspector/test-exception.js new file mode 100644 index 0000000000..ca3994c0a0 --- /dev/null +++ b/test/inspector/test-exception.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { NodeInstance } = require('./inspector-helper.js'); +const path = require('path'); + +const script = path.join(common.fixturesDir, 'throws_error.js'); + +async function testBreakpointOnStart(session) { + console.log('[test]', + 'Verifying debugger stops on start (--inspect-brk option)'); + const commands = [ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setPauseOnExceptions', + 'params': { 'state': 'none' } }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 0 } }, + { 'method': 'Profiler.enable' }, + { 'method': 'Profiler.setSamplingInterval', + 'params': { 'interval': 100 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]; + + await session.send(commands); + await session.waitForBreakOnLine(0, script); +} + + +async function runTest() { + const child = new NodeInstance(undefined, undefined, script); + const session = await child.connectInspectorSession(); + await testBreakpointOnStart(session); + await session.runToCompletion(); + assert.strictEqual(1, (await child.expectShutdown()).exitCode); +} + +common.crashOnUnhandledRejection(); + +runTest(); diff --git a/test/inspector/test-inspector-break-when-eval.js b/test/inspector/test-inspector-break-when-eval.js deleted file mode 100644 index 957d7ea4d2..0000000000 --- a/test/inspector/test-inspector-break-when-eval.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; -const common = require('../common'); -common.skipIfInspectorDisabled(); -const assert = require('assert'); -const helper = require('./inspector-helper.js'); -const path = require('path'); - -const script = path.join(path.dirname(module.filename), 'global-function.js'); - - -function setupExpectBreakOnLine(line, url, session) { - return function(message) { - if ('Debugger.paused' === message['method']) { - const callFrame = message['params']['callFrames'][0]; - const location = callFrame['location']; - assert.strictEqual(url, session.scriptUrlForId(location['scriptId'])); - assert.strictEqual(line, location['lineNumber']); - return true; - } - }; -} - -function setupExpectConsoleOutputAndBreak(type, values) { - if (!(values instanceof Array)) - values = [ values ]; - let consoleLog = false; - function matchConsoleLog(message) { - if ('Runtime.consoleAPICalled' === message['method']) { - const params = message['params']; - if (params['type'] === type) { - let i = 0; - for (const value of params['args']) { - if (value['value'] !== values[i++]) - return false; - } - return i === values.length; - } - } - } - - return function(message) { - if (consoleLog) - return message['method'] === 'Debugger.paused'; - consoleLog = matchConsoleLog(message); - return false; - }; -} - -function setupExpectContextDestroyed(id) { - return function(message) { - if ('Runtime.executionContextDestroyed' === message['method']) - return message['params']['executionContextId'] === id; - }; -} - -function setupDebugger(session) { - console.log('[test]', 'Setting up a debugger'); - const commands = [ - { 'method': 'Runtime.enable' }, - { 'method': 'Debugger.enable' }, - { 'method': 'Debugger.setAsyncCallStackDepth', - 'params': { 'maxDepth': 0 } }, - { 'method': 'Runtime.runIfWaitingForDebugger' }, - ]; - - session - .sendInspectorCommands(commands) - .expectMessages((message) => 'Runtime.consoleAPICalled' === message.method); -} - -function breakOnLine(session) { - console.log('[test]', 'Breaking in the code'); - const commands = [ - { 'method': 'Debugger.setBreakpointByUrl', - 'params': { 'lineNumber': 9, - 'url': script, - 'columnNumber': 0, - 'condition': '' - } - }, - { 'method': 'Runtime.evaluate', - 'params': { 'expression': 'sum()', - 'objectGroup': 'console', - 'includeCommandLineAPI': true, - 'silent': false, - 'contextId': 1, - 'returnByValue': false, - 'generatePreview': true, - 'userGesture': true, - 'awaitPromise': false - } - } - ]; - helper.markMessageNoResponse(commands[1]); - session - .sendInspectorCommands(commands) - .expectMessages(setupExpectBreakOnLine(9, script, session)); -} - -function stepOverConsoleStatement(session) { - console.log('[test]', 'Step over console statement and test output'); - session - .sendInspectorCommands({ 'method': 'Debugger.stepOver' }) - .expectMessages(setupExpectConsoleOutputAndBreak('log', [0, 3])); -} - -function testWaitsForFrontendDisconnect(session, harness) { - console.log('[test]', 'Verify node waits for the frontend to disconnect'); - session.sendInspectorCommands({ 'method': 'Debugger.resume' }) - .expectMessages(setupExpectContextDestroyed(1)) - .expectStderrOutput('Waiting for the debugger to disconnect...') - .disconnect(true); -} - -function runTests(harness) { - harness - .runFrontendSession([ - setupDebugger, - breakOnLine, - stepOverConsoleStatement, - testWaitsForFrontendDisconnect - ]).expectShutDown(0); -} - -helper.startNodeForInspectorTest(runTests, - ['--inspect'], - undefined, - script); diff --git a/test/inspector/test-inspector-debug-brk.js b/test/inspector/test-inspector-debug-brk.js deleted file mode 100644 index 1d7af9e318..0000000000 --- a/test/inspector/test-inspector-debug-brk.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; -const common = require('../common'); - -common.skipIfInspectorDisabled(); - -const assert = require('assert'); -const helper = require('./inspector-helper.js'); - -function setupExpectBreakOnLine(line, url, session, scopeIdCallback) { - return function(message) { - if ('Debugger.paused' === message['method']) { - const callFrame = message['params']['callFrames'][0]; - const location = callFrame['location']; - assert.strictEqual(url, session.scriptUrlForId(location['scriptId'])); - assert.strictEqual(line, location['lineNumber']); - scopeIdCallback && - scopeIdCallback(callFrame['scopeChain'][0]['object']['objectId']); - return true; - } - }; -} - -function testBreakpointOnStart(session) { - const commands = [ - { 'method': 'Runtime.enable' }, - { 'method': 'Debugger.enable' }, - { 'method': 'Debugger.setPauseOnExceptions', - 'params': { 'state': 'none' } }, - { 'method': 'Debugger.setAsyncCallStackDepth', - 'params': { 'maxDepth': 0 } }, - { 'method': 'Profiler.enable' }, - { 'method': 'Profiler.setSamplingInterval', - 'params': { 'interval': 100 } }, - { 'method': 'Debugger.setBlackboxPatterns', - 'params': { 'patterns': [] } }, - { 'method': 'Runtime.runIfWaitingForDebugger' } - ]; - - session - .sendInspectorCommands(commands) - .expectMessages(setupExpectBreakOnLine(0, session.mainScriptPath, session)); -} - -function testWaitsForFrontendDisconnect(session, harness) { - console.log('[test]', 'Verify node waits for the frontend to disconnect'); - session.sendInspectorCommands({ 'method': 'Debugger.resume' }) - .expectStderrOutput('Waiting for the debugger to disconnect...') - .disconnect(true); -} - -function runTests(harness) { - harness - .runFrontendSession([ - testBreakpointOnStart, - testWaitsForFrontendDisconnect - ]).expectShutDown(55); -} - -helper.startNodeForInspectorTest(runTests, ['--inspect', '--debug-brk']); diff --git a/test/inspector/test-inspector-exception.js b/test/inspector/test-inspector-exception.js deleted file mode 100644 index 2c6432c65d..0000000000 --- a/test/inspector/test-inspector-exception.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; -const common = require('../common'); - -common.skipIfInspectorDisabled(); - -const assert = require('assert'); -const helper = require('./inspector-helper.js'); -const path = require('path'); - -const script = path.join(common.fixturesDir, 'throws_error.js'); - - -function setupExpectBreakOnLine(line, url, session) { - return function(message) { - if ('Debugger.paused' === message['method']) { - const callFrame = message['params']['callFrames'][0]; - const location = callFrame['location']; - assert.strictEqual(url, session.scriptUrlForId(location['scriptId'])); - assert.strictEqual(line, location['lineNumber']); - return true; - } - }; -} - -function testBreakpointOnStart(session) { - const commands = [ - { 'method': 'Runtime.enable' }, - { 'method': 'Debugger.enable' }, - { 'method': 'Debugger.setPauseOnExceptions', - 'params': { 'state': 'none' } }, - { 'method': 'Debugger.setAsyncCallStackDepth', - 'params': { 'maxDepth': 0 } }, - { 'method': 'Profiler.enable' }, - { 'method': 'Profiler.setSamplingInterval', - 'params': { 'interval': 100 } }, - { 'method': 'Debugger.setBlackboxPatterns', - 'params': { 'patterns': [] } }, - { 'method': 'Runtime.runIfWaitingForDebugger' } - ]; - - session - .sendInspectorCommands(commands) - .expectMessages(setupExpectBreakOnLine(0, script, session)); -} - -function testWaitsForFrontendDisconnect(session, harness) { - console.log('[test]', 'Verify node waits for the frontend to disconnect'); - session.sendInspectorCommands({ 'method': 'Debugger.resume' }) - .expectStderrOutput('Waiting for the debugger to disconnect...') - .disconnect(true); -} - -function runTests(harness) { - harness - .runFrontendSession([ - testBreakpointOnStart, - testWaitsForFrontendDisconnect - ]).expectShutDown(1); -} - -helper.startNodeForInspectorTest(runTests, - undefined, - undefined, - script); diff --git a/test/inspector/test-inspector-ip-detection.js b/test/inspector/test-inspector-ip-detection.js deleted file mode 100644 index be5e34a977..0000000000 --- a/test/inspector/test-inspector-ip-detection.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; -const common = require('../common'); - -common.skipIfInspectorDisabled(); - -const assert = require('assert'); -const helper = require('./inspector-helper.js'); -const os = require('os'); - -const ip = pickIPv4Address(); - -if (!ip) - common.skip('No IP address found'); - -function checkListResponse(instance, err, response) { - assert.ifError(err); - const res = response[0]; - const wsUrl = res['webSocketDebuggerUrl']; - assert.ok(wsUrl); - const match = wsUrl.match(/^ws:\/\/(.*):9229\/(.*)/); - assert.strictEqual(ip, match[1]); - assert.strictEqual(res['id'], match[2]); - assert.strictEqual(ip, res['devtoolsFrontendUrl'].match(/.*ws=(.*):9229/)[1]); - instance.childInstanceDone = true; -} - -function checkError(instance, error) { - // Some OSes will not allow us to connect - if (error.code === 'EHOSTUNREACH') { - common.printSkipMessage('Unable to connect to self'); - } else { - throw error; - } - instance.childInstanceDone = true; -} - -function runTests(instance) { - instance - .testHttpResponse(ip, '/json/list', checkListResponse.bind(null, instance), - checkError.bind(null, instance)) - .kill(); -} - -function pickIPv4Address() { - for (const i of [].concat(...Object.values(os.networkInterfaces()))) { - if (i.family === 'IPv4' && i.address !== '127.0.0.1') - return i.address; - } -} - -helper.startNodeForInspectorTest(runTests, '--inspect-brk=0.0.0.0'); diff --git a/test/inspector/test-inspector-stop-profile-after-done.js b/test/inspector/test-inspector-stop-profile-after-done.js deleted file mode 100644 index db43e4ae79..0000000000 --- a/test/inspector/test-inspector-stop-profile-after-done.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; -const common = require('../common'); -common.skipIfInspectorDisabled(); -const helper = require('./inspector-helper.js'); - -function test(session) { - session.sendInspectorCommands([ - { 'method': 'Runtime.runIfWaitingForDebugger' }, - { 'method': 'Profiler.setSamplingInterval', 'params': { 'interval': 100 } }, - { 'method': 'Profiler.enable' }, - { 'method': 'Profiler.start' }]); - session.expectStderrOutput('Waiting for the debugger to disconnect...'); - session.sendInspectorCommands({ 'method': 'Profiler.stop' }); - session.disconnect(true); -} - -function runTests(harness) { - harness.runFrontendSession([test]).expectShutDown(0); -} - -helper.startNodeForInspectorTest(runTests, ['--inspect-brk'], 'let a = 2;'); diff --git a/test/inspector/test-inspector.js b/test/inspector/test-inspector.js index f933232553..3139940451 100644 --- a/test/inspector/test-inspector.js +++ b/test/inspector/test-inspector.js @@ -4,12 +4,11 @@ const common = require('../common'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const helper = require('./inspector-helper.js'); +const { mainScriptPath, + readMainScriptSource, + NodeInstance } = require('./inspector-helper.js'); -let scopeId; - -function checkListResponse(err, response) { - assert.ifError(err); +function checkListResponse(response) { assert.strictEqual(1, response.length); assert.ok(response[0]['devtoolsFrontendUrl']); assert.ok( @@ -17,8 +16,7 @@ function checkListResponse(err, response) { .test(response[0]['webSocketDebuggerUrl'])); } -function checkVersion(err, response) { - assert.ifError(err); +function checkVersion(response) { assert.ok(response); const expected = { 'Browser': `node.js/${process.version}`, @@ -28,10 +26,10 @@ function checkVersion(err, response) { JSON.stringify(expected)); } -function checkBadPath(err, response) { +function checkBadPath(err) { assert(err instanceof SyntaxError); - assert(/Unexpected token/.test(err.message)); - assert(/WebSockets request was expected/.test(err.response)); + assert(/Unexpected token/.test(err.message), err.message); + assert(/WebSockets request was expected/.test(err.body), err.body); } function checkException(message) { @@ -39,69 +37,26 @@ function checkException(message) { 'An exception occurred during execution'); } -function expectMainScriptSource(result) { - const expected = helper.mainScriptSource(); - const source = result['scriptSource']; - assert(source && (source.includes(expected)), - `Script source is wrong: ${source}`); -} - -function setupExpectBreakOnLine(line, url, session, scopeIdCallback) { - return function(message) { - if ('Debugger.paused' === message['method']) { - const callFrame = message['params']['callFrames'][0]; - const location = callFrame['location']; - assert.strictEqual(url, session.scriptUrlForId(location['scriptId'])); - assert.strictEqual(line, location['lineNumber']); - scopeIdCallback && - scopeIdCallback(callFrame['scopeChain'][0]['object']['objectId']); - return true; - } - }; -} - -function setupExpectConsoleOutput(type, values) { - if (!(values instanceof Array)) - values = [ values ]; - return function(message) { - if ('Runtime.consoleAPICalled' === message['method']) { - const params = message['params']; - if (params['type'] === type) { - let i = 0; - for (const value of params['args']) { - if (value['value'] !== values[i++]) - return false; - } - return i === values.length; - } - } - }; +function assertNoUrlsWhileConnected(response) { + assert.strictEqual(1, response.length); + assert.ok(!response[0].hasOwnProperty('devtoolsFrontendUrl')); + assert.ok(!response[0].hasOwnProperty('webSocketDebuggerUrl')); } -function setupExpectScopeValues(expected) { - return function(result) { - for (const actual of result['result']) { - const value = expected[actual['name']]; - if (value) - assert.strictEqual(value, actual['value']['value']); +function assertScopeValues({ result }, expected) { + const unmatched = new Set(Object.keys(expected)); + for (const actual of result) { + const value = expected[actual['name']]; + if (value) { + assert.strictEqual(value, actual['value']['value']); + unmatched.delete(actual['name']); } - }; + } + if (unmatched.size) + assert.fail(Array.from(unmatched.values())); } -function setupExpectValue(value) { - return function(result) { - assert.strictEqual(value, result['result']['value']); - }; -} - -function setupExpectContextDestroyed(id) { - return function(message) { - if ('Runtime.executionContextDestroyed' === message['method']) - return message['params']['executionContextId'] === id; - }; -} - -function testBreakpointOnStart(session) { +async function testBreakpointOnStart(session) { console.log('[test]', 'Verifying debugger stops on start (--inspect-brk option)'); const commands = [ @@ -119,262 +74,230 @@ function testBreakpointOnStart(session) { { 'method': 'Runtime.runIfWaitingForDebugger' } ]; - session - .sendInspectorCommands(commands) - .expectMessages(setupExpectBreakOnLine(0, session.mainScriptPath, session)); + await session.send(commands); + await session.waitForBreakOnLine(0, mainScriptPath); } -function testSetBreakpointAndResume(session) { +async function testBreakpoint(session) { console.log('[test]', 'Setting a breakpoint and verifying it is hit'); const commands = [ { 'method': 'Debugger.setBreakpointByUrl', 'params': { 'lineNumber': 5, - 'url': session.mainScriptPath, + 'url': mainScriptPath, 'columnNumber': 0, 'condition': '' } }, { 'method': 'Debugger.resume' }, - [ { 'method': 'Debugger.getScriptSource', - 'params': { 'scriptId': session.mainScriptId } }, - expectMainScriptSource ], ]; - session - .sendInspectorCommands(commands) - .expectMessages([ - setupExpectConsoleOutput('log', ['A message', 5]), - setupExpectBreakOnLine(5, session.mainScriptPath, - session, (id) => scopeId = id), - ]); -} + await session.send(commands); + const { scriptSource } = await session.send({ + 'method': 'Debugger.getScriptSource', + 'params': { 'scriptId': session.mainScriptId } }); + assert(scriptSource && (scriptSource.includes(readMainScriptSource())), + `Script source is wrong: ${scriptSource}`); + + await session.waitForConsoleOutput('log', ['A message', 5]); + const scopeId = await session.waitForBreakOnLine(5, mainScriptPath); -function testInspectScope(session) { console.log('[test]', 'Verify we can read current application state'); - session.sendInspectorCommands([ - [ - { - 'method': 'Runtime.getProperties', - 'params': { - 'objectId': scopeId, - 'ownProperties': false, - 'accessorPropertiesOnly': false, - 'generatePreview': true - } - }, setupExpectScopeValues({ t: 1001, k: 1 }) - ], - [ - { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': 'k + t', - 'objectGroup': 'console', - 'includeCommandLineAPI': true, - 'silent': false, - 'returnByValue': false, - 'generatePreview': true - } - }, setupExpectValue(1002) - ], - [ - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': '5 * 5' - } - }, (message) => assert.strictEqual(25, message['result']['value']) - ], - ]); -} + const response = await session.send({ + 'method': 'Runtime.getProperties', + 'params': { + 'objectId': scopeId, + 'ownProperties': false, + 'accessorPropertiesOnly': false, + 'generatePreview': true + } + }); + assertScopeValues(response, { t: 1001, k: 1 }); -function testNoUrlsWhenConnected(session) { - session.testHttpResponse('/json/list', (err, response) => { - assert.ifError(err); - assert.strictEqual(1, response.length); - assert.ok(!response[0].hasOwnProperty('devtoolsFrontendUrl')); - assert.ok(!response[0].hasOwnProperty('webSocketDebuggerUrl')); + let { result } = await session.send({ + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': 'k + t', + 'objectGroup': 'console', + 'includeCommandLineAPI': true, + 'silent': false, + 'returnByValue': false, + 'generatePreview': true + } }); + + assert.strictEqual(1002, result['value']); + + result = (await session.send({ + 'method': 'Runtime.evaluate', 'params': { + 'expression': '5 * 5' + } + })).result; + assert.strictEqual(25, result['value']); } -function testI18NCharacters(session) { +async function testI18NCharacters(session) { console.log('[test]', 'Verify sending and receiving UTF8 characters'); const chars = 'טֶ字и'; - session.sendInspectorCommands([ - { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': `console.log("${chars}")`, - 'objectGroup': 'console', - 'includeCommandLineAPI': true, - 'silent': false, - 'returnByValue': false, - 'generatePreview': true - } + session.send({ + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `console.log("${chars}")`, + 'objectGroup': 'console', + 'includeCommandLineAPI': true, + 'silent': false, + 'returnByValue': false, + 'generatePreview': true } - ]).expectMessages([ - setupExpectConsoleOutput('log', [chars]), - ]); + }); + await session.waitForConsoleOutput('log', [chars]); } -function testCommandLineAPI(session) { +async function testCommandLineAPI(session) { const testModulePath = require.resolve('../fixtures/empty.js'); const testModuleStr = JSON.stringify(testModulePath); const printAModulePath = require.resolve('../fixtures/printA.js'); const printAModuleStr = JSON.stringify(printAModulePath); const printBModulePath = require.resolve('../fixtures/printB.js'); const printBModuleStr = JSON.stringify(printBModulePath); - session.sendInspectorCommands([ - [ // we can use `require` outside of a callframe with require in scope - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': 'typeof require("fs").readFile === "function"', - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.strictEqual(message['result']['value'], true); + + // we can use `require` outside of a callframe with require in scope + let result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': 'typeof require("fs").readFile === "function"', + 'includeCommandLineAPI': true } - ], - [ // the global require has the same properties as a normal `require` - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': [ - 'typeof require.resolve === "function"', - 'typeof require.extensions === "object"', - 'typeof require.cache === "object"' - ].join(' && '), - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.strictEqual(message['result']['value'], true); + }); + checkException(result); + assert.strictEqual(result['result']['value'], true); + + // the global require has the same properties as a normal `require` + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': [ + 'typeof require.resolve === "function"', + 'typeof require.extensions === "object"', + 'typeof require.cache === "object"' + ].join(' && '), + 'includeCommandLineAPI': true } - ], - [ // `require` twice returns the same value - { - 'method': 'Runtime.evaluate', 'params': { - // 1. We require the same module twice - // 2. We mutate the exports so we can compare it later on - 'expression': ` - Object.assign( - require(${testModuleStr}), - { old: 'yes' } - ) === require(${testModuleStr})`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.strictEqual(message['result']['value'], true); + }); + checkException(result); + assert.strictEqual(result['result']['value'], true); + // `require` twice returns the same value + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + // 1. We require the same module twice + // 2. We mutate the exports so we can compare it later on + 'expression': ` + Object.assign( + require(${testModuleStr}), + { old: 'yes' } + ) === require(${testModuleStr})`, + 'includeCommandLineAPI': true } - ], - [ // after require the module appears in require.cache - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': `JSON.stringify( - require.cache[${testModuleStr}].exports - )`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.deepStrictEqual(JSON.parse(message['result']['value']), - { old: 'yes' }); + }); + checkException(result); + assert.strictEqual(result['result']['value'], true); + // after require the module appears in require.cache + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify( + require.cache[${testModuleStr}].exports + )`, + 'includeCommandLineAPI': true } - ], - [ // remove module from require.cache - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': `delete require.cache[${testModuleStr}]`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.strictEqual(message['result']['value'], true); + }); + checkException(result); + assert.deepStrictEqual(JSON.parse(result['result']['value']), + { old: 'yes' }); + // remove module from require.cache + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `delete require.cache[${testModuleStr}]`, + 'includeCommandLineAPI': true } - ], - [ // require again, should get fresh (empty) exports - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': `JSON.stringify(require(${testModuleStr}))`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); + }); + checkException(result); + assert.strictEqual(result['result']['value'], true); + // require again, should get fresh (empty) exports + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify(require(${testModuleStr}))`, + 'includeCommandLineAPI': true } - ], - [ // require 2nd module, exports an empty object - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': `JSON.stringify(require(${printAModuleStr}))`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); + }); + checkException(result); + assert.deepStrictEqual(JSON.parse(result['result']['value']), {}); + // require 2nd module, exports an empty object + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify(require(${printAModuleStr}))`, + 'includeCommandLineAPI': true } - ], - [ // both modules end up with the same module.parent - { - 'method': 'Runtime.evaluate', 'params': { - 'expression': `JSON.stringify({ - parentsEqual: - require.cache[${testModuleStr}].parent === - require.cache[${printAModuleStr}].parent, - parentId: require.cache[${testModuleStr}].parent.id, - })`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.deepStrictEqual(JSON.parse(message['result']['value']), { - parentsEqual: true, - parentId: '' - }); + }); + checkException(result); + assert.deepStrictEqual(JSON.parse(result['result']['value']), {}); + // both modules end up with the same module.parent + result = await session.send( + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify({ + parentsEqual: + require.cache[${testModuleStr}].parent === + require.cache[${printAModuleStr}].parent, + parentId: require.cache[${testModuleStr}].parent.id, + })`, + 'includeCommandLineAPI': true } - ], - [ // the `require` in the module shadows the command line API's `require` - { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': `( - require(${printBModuleStr}), - require.cache[${printBModuleStr}].parent.id - )`, - 'includeCommandLineAPI': true - } - }, (message) => { - checkException(message); - assert.notStrictEqual(message['result']['value'], - ''); + }); + checkException(result); + assert.deepStrictEqual(JSON.parse(result['result']['value']), { + parentsEqual: true, + parentId: '' + }); + // the `require` in the module shadows the command line API's `require` + result = await session.send( + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `( + require(${printBModuleStr}), + require.cache[${printBModuleStr}].parent.id + )`, + 'includeCommandLineAPI': true } - ], - ]); + }); + checkException(result); + assert.notStrictEqual(result['result']['value'], + ''); } -function testWaitsForFrontendDisconnect(session, harness) { - console.log('[test]', 'Verify node waits for the frontend to disconnect'); - session.sendInspectorCommands({ 'method': 'Debugger.resume' }) - .expectMessages(setupExpectContextDestroyed(1)) - .expectStderrOutput('Waiting for the debugger to disconnect...') - .disconnect(true); -} +async function runTest() { + const child = new NodeInstance(); + checkListResponse(await child.httpGet(null, '/json')); + checkListResponse(await child.httpGet(null, '/json/list')); + checkVersion(await child.httpGet(null, '/json/version')); + + await child.httpGet(null, '/json/activate').catch(checkBadPath); + await child.httpGet(null, '/json/activate/boom').catch(checkBadPath); + await child.httpGet(null, '/json/badpath').catch(checkBadPath); -function runTests(harness) { - harness - .testHttpResponse(null, '/json', checkListResponse) - .testHttpResponse(null, '/json/list', checkListResponse) - .testHttpResponse(null, '/json/version', checkVersion) - .testHttpResponse(null, '/json/activate', checkBadPath) - .testHttpResponse(null, '/json/activate/boom', checkBadPath) - .testHttpResponse(null, '/json/badpath', checkBadPath) - .runFrontendSession([ - testNoUrlsWhenConnected, - testBreakpointOnStart, - testSetBreakpointAndResume, - testInspectScope, - testI18NCharacters, - testCommandLineAPI, - testWaitsForFrontendDisconnect - ]).expectShutDown(55); + const session = await child.connectInspectorSession(); + assertNoUrlsWhileConnected(await child.httpGet(null, '/json/list')); + await testBreakpointOnStart(session); + await testBreakpoint(session); + await testI18NCharacters(session); + await testCommandLineAPI(session); + await session.runToCompletion(); + assert.strictEqual(55, (await child.expectShutdown()).exitCode); } -helper.startNodeForInspectorTest(runTests); +common.crashOnUnhandledRejection(); + +runTest(); diff --git a/test/inspector/test-ip-detection.js b/test/inspector/test-ip-detection.js new file mode 100644 index 0000000000..5a6a116144 --- /dev/null +++ b/test/inspector/test-ip-detection.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { NodeInstance } = require('./inspector-helper.js'); +const os = require('os'); + +const ip = pickIPv4Address(); + +if (!ip) + common.skip('No IP address found'); + +function checkIpAddress(ip, response) { + const res = response[0]; + const wsUrl = res['webSocketDebuggerUrl']; + assert.ok(wsUrl); + const match = wsUrl.match(/^ws:\/\/(.*):\d+\/(.*)/); + assert.strictEqual(ip, match[1]); + assert.strictEqual(res['id'], match[2]); + assert.strictEqual(ip, res['devtoolsFrontendUrl'].match(/.*ws=(.*):\d+/)[1]); +} + +function pickIPv4Address() { + for (const i of [].concat(...Object.values(os.networkInterfaces()))) { + if (i.family === 'IPv4' && i.address !== '127.0.0.1') + return i.address; + } +} + +async function test() { + const instance = new NodeInstance('--inspect-brk=0.0.0.0:0'); + try { + checkIpAddress(ip, await instance.httpGet(ip, '/json/list')); + } catch (error) { + if (error.code === 'EHOSTUNREACH') { + common.printSkipMessage('Unable to connect to self'); + } else { + throw error; + } + } + instance.kill(); +} + +common.crashOnUnhandledRejection(); + +test(); diff --git a/test/inspector/test-not-blocked-on-idle.js b/test/inspector/test-not-blocked-on-idle.js index 1573e875cd..8684d6f314 100644 --- a/test/inspector/test-not-blocked-on-idle.js +++ b/test/inspector/test-not-blocked-on-idle.js @@ -1,21 +1,21 @@ 'use strict'; const common = require('../common'); common.skipIfInspectorDisabled(); -const helper = require('./inspector-helper.js'); +const { NodeInstance } = require('./inspector-helper.js'); -function shouldShutDown(session) { - session - .sendInspectorCommands([ - { 'method': 'Debugger.enable' }, - { 'method': 'Debugger.pause' }, - ]) - .disconnect(true); -} - -function runTests(harness) { +async function runTests() { + const script = 'setInterval(() => {debugger;}, 60000);'; + const node = new NodeInstance('--inspect=0', script); // 1 second wait to make sure the inferior began running the script - setTimeout(() => harness.runFrontendSession([shouldShutDown]).kill(), 1000); + await new Promise((resolve) => setTimeout(() => resolve(), 1000)); + const session = await node.connectInspectorSession(); + await session.send([ + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.pause' } + ]); + session.disconnect(); + node.kill(); } -const script = 'setInterval(() => {debugger;}, 60000);'; -helper.startNodeForInspectorTest(runTests, '--inspect', script); +common.crashOnUnhandledRejection(); +runTests(); diff --git a/test/inspector/test-off-no-session.js b/test/inspector/test-off-no-session.js deleted file mode 100644 index 2ec54f3651..0000000000 --- a/test/inspector/test-off-no-session.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -const common = require('../common'); -common.skipIfInspectorDisabled(); -const helper = require('./inspector-helper.js'); - -function testStop(harness) { - harness.expectShutDown(42); -} - -helper.startNodeForInspectorTest(testStop, '--inspect', - 'process._debugEnd();process.exit(42);'); diff --git a/test/inspector/test-off-with-session-then-on.js b/test/inspector/test-off-with-session-then-on.js deleted file mode 100644 index bd6455699d..0000000000 --- a/test/inspector/test-off-with-session-then-on.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; -const common = require('../common'); -common.skipIfInspectorDisabled(); -const helper = require('./inspector-helper.js'); - -function testResume(session) { - session.sendCommandsAndExpectClose([ - { 'method': 'Runtime.runIfWaitingForDebugger' } - ]); -} - -function testDisconnectSession(harness) { - harness - .runFrontendSession([ - testResume, - ]).expectShutDown(42); -} - -const script = 'process._debugEnd();' + - 'process._debugProcess(process.pid);' + - 'setTimeout(() => {console.log("Done");process.exit(42)});'; - -helper.startNodeForInspectorTest(testDisconnectSession, '--inspect-brk', - script); diff --git a/test/inspector/test-inspector-port-cluster.js b/test/inspector/test-port-cluster.js similarity index 100% rename from test/inspector/test-inspector-port-cluster.js rename to test/inspector/test-port-cluster.js diff --git a/test/inspector/test-inspector-port-zero-cluster.js b/test/inspector/test-port-zero-cluster.js similarity index 100% rename from test/inspector/test-inspector-port-zero-cluster.js rename to test/inspector/test-port-zero-cluster.js diff --git a/test/inspector/test-inspector-port-zero.js b/test/inspector/test-port-zero.js similarity index 100% rename from test/inspector/test-inspector-port-zero.js rename to test/inspector/test-port-zero.js diff --git a/test/inspector/test-stop-profile-after-done.js b/test/inspector/test-stop-profile-after-done.js new file mode 100644 index 0000000000..314c429d46 --- /dev/null +++ b/test/inspector/test-stop-profile-after-done.js @@ -0,0 +1,30 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +const assert = require('assert'); +const { NodeInstance } = require('./inspector-helper.js'); + +async function runTests() { + const child = new NodeInstance(['--inspect=0'], + `let c = 0; + const interval = setInterval(() => { + console.log(new Object()); + if (c++ === 10) + clearInterval(interval); + }, 10);`); + const session = await child.connectInspectorSession(); + + session.send([ + { 'method': 'Profiler.setSamplingInterval', 'params': { 'interval': 100 } }, + { 'method': 'Profiler.enable' }, + { 'method': 'Runtime.runIfWaitingForDebugger' }, + { 'method': 'Profiler.start' }]); + while (await child.nextStderrString() !== + 'Waiting for the debugger to disconnect...'); + await session.send({ 'method': 'Profiler.stop' }); + session.disconnect(); + assert.strictEqual(0, (await child.expectShutdown()).exitCode); +} + +common.crashOnUnhandledRejection(); +runTests(); diff --git a/test/inspector/test-inspector-stops-no-file.js b/test/inspector/test-stops-no-file.js similarity index 100% rename from test/inspector/test-inspector-stops-no-file.js rename to test/inspector/test-stops-no-file.js