From 5f31d547201db8176a8ae9cc3b673f0840fff705 Mon Sep 17 00:00:00 2001 From: Eugene Ostroukhov Date: Mon, 24 Jul 2017 09:23:04 -0700 Subject: [PATCH] inspector: rewrite inspector test helper Helper was rewritten to rely on promises instead of manually written queue and callbacks. This simplifies the code and makes it easier to maintain and extend. PR-URL: https://github.com/nodejs/node/pull/14797 Reviewed-By: Timothy Gu Reviewed-By: Colin Ihrig --- test/common/README.md | 9 + test/common/index.js | 42 + test/inspector/inspector-helper.js | 771 +++++++----------- test/inspector/test-break-when-eval.js | 68 ++ test/inspector/test-debug-brk-flag.js | 41 + test/inspector/test-debug-end.js | 46 ++ test/inspector/test-exception.js | 45 + .../test-inspector-break-when-eval.js | 128 --- test/inspector/test-inspector-debug-brk.js | 59 -- test/inspector/test-inspector-exception.js | 64 -- test/inspector/test-inspector-ip-detection.js | 51 -- .../test-inspector-stop-profile-after-done.js | 21 - test/inspector/test-inspector.js | 491 +++++------ test/inspector/test-ip-detection.js | 48 ++ test/inspector/test-not-blocked-on-idle.js | 28 +- test/inspector/test-off-no-session.js | 11 - .../test-off-with-session-then-on.js | 24 - ...r-port-cluster.js => test-port-cluster.js} | 0 ...o-cluster.js => test-port-zero-cluster.js} | 0 ...spector-port-zero.js => test-port-zero.js} | 0 .../inspector/test-stop-profile-after-done.js | 30 + ...stops-no-file.js => test-stops-no-file.js} | 0 22 files changed, 863 insertions(+), 1114 deletions(-) create mode 100644 test/inspector/test-break-when-eval.js create mode 100644 test/inspector/test-debug-brk-flag.js create mode 100644 test/inspector/test-debug-end.js create mode 100644 test/inspector/test-exception.js delete mode 100644 test/inspector/test-inspector-break-when-eval.js delete mode 100644 test/inspector/test-inspector-debug-brk.js delete mode 100644 test/inspector/test-inspector-exception.js delete mode 100644 test/inspector/test-inspector-ip-detection.js delete mode 100644 test/inspector/test-inspector-stop-profile-after-done.js create mode 100644 test/inspector/test-ip-detection.js delete mode 100644 test/inspector/test-off-no-session.js delete mode 100644 test/inspector/test-off-with-session-then-on.js rename test/inspector/{test-inspector-port-cluster.js => test-port-cluster.js} (100%) rename test/inspector/{test-inspector-port-zero-cluster.js => test-port-zero-cluster.js} (100%) rename test/inspector/{test-inspector-port-zero.js => test-port-zero.js} (100%) create mode 100644 test/inspector/test-stop-profile-after-done.js rename test/inspector/{test-inspector-stops-no-file.js => test-stops-no-file.js} (100%) 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..e9464c3679 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 sendEnd(socket) { - socket.write(Buffer.from([0x88, 0x80, 0x2D, 0x0E, 0x1E, 0xFA])); +function makeBufferingDataCallback(dataCallback) { + let buffer = Buffer.alloc(0); + return (data) => { + const newData = Buffer.concat([buffer, data]); + const str = newData.toString('utf8'); + const lines = str.replace(/\r/g, '').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 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,10 +74,9 @@ function parseWSFrame(buffer, handler) { bodyOffset = 10; } if (buffer.length < bodyOffset + dataLen) - return 0; + return { length: 0, message }; const jsonPayload = buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8'); - let message; try { message = JSON.parse(jsonPayload); } catch (e) { @@ -86,470 +85,326 @@ function parseWSFrame(buffer, handler) { } 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(); - })); -}; + 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; + } -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; - }); -} - -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