From a6973a38117de2e1b7681647160e55c8f9ad4050 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 11 Aug 2017 21:37:19 +0200 Subject: [PATCH] Revert "inspector: rewrite inspector test helper" This reverts commit 2296b677fb1e2ea71e86a899b028ba9e817e86ad. That commit was landed without a green CI and is failing on Windows. Ref: https://github.com/nodejs/node/pull/14460 PR-URL: https://github.com/nodejs/node/pull/14777 Reviewed-By: Refael Ackermann --- test/common/README.md | 9 - test/common/index.js | 42 - test/inspector/inspector-helper.js | 781 +++++++++++------- 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 ++ ...ster.js => test-inspector-port-cluster.js} | 0 ...js => test-inspector-port-zero-cluster.js} | 0 ...rt-zero.js => test-inspector-port-zero.js} | 0 .../test-inspector-stop-profile-after-done.js | 21 + ...ile.js => test-inspector-stops-no-file.js} | 0 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 + .../inspector/test-stop-profile-after-done.js | 30 - 22 files changed, 1122 insertions(+), 865 deletions(-) delete mode 100644 test/inspector/test-break-when-eval.js delete mode 100644 test/inspector/test-debug-brk-flag.js delete mode 100644 test/inspector/test-debug-end.js delete mode 100644 test/inspector/test-exception.js create mode 100644 test/inspector/test-inspector-break-when-eval.js create mode 100644 test/inspector/test-inspector-debug-brk.js create mode 100644 test/inspector/test-inspector-exception.js create mode 100644 test/inspector/test-inspector-ip-detection.js rename test/inspector/{test-port-cluster.js => test-inspector-port-cluster.js} (100%) rename test/inspector/{test-port-zero-cluster.js => test-inspector-port-zero-cluster.js} (100%) rename test/inspector/{test-port-zero.js => test-inspector-port-zero.js} (100%) create mode 100644 test/inspector/test-inspector-stop-profile-after-done.js rename test/inspector/{test-stops-no-file.js => test-inspector-stops-no-file.js} (100%) delete mode 100644 test/inspector/test-ip-detection.js create mode 100644 test/inspector/test-off-no-session.js create mode 100644 test/inspector/test-off-with-session-then-on.js delete mode 100644 test/inspector/test-stop-profile-after-done.js diff --git a/test/common/README.md b/test/common/README.md index 59b02cf52a9a48..aa3fcb3d4a41b5 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -99,15 +99,6 @@ 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 2564b227fe3efd..54742319d2abc6 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -814,32 +814,6 @@ 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'); @@ -853,19 +827,3 @@ 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 b316f04aea0ca3..2f45e21c5b0665 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'); +const spawn = require('child_process').spawn; 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 spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { - const args = [].concat(inspectorFlags); - if (scriptContents) { - args.push('-e', scriptContents); - } else { - args.push(scriptFile); - } - const child = spawn(process.execPath, args); +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 handler = tearDown.bind(null, child); - process.on('exit', handler); - process.on('uncaughtException', handler); - process.on('unhandledRejection', handler); - process.on('SIGINT', handler); + const wsHeaderBuf = Buffer.allocUnsafe(16); + wsHeaderBuf.writeUInt8(0x81, 0); + let byte2 = 0x80; + const bodyLen = messageBuf.length; - return child; -} + 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 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); - }; + 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); } -function tearDown(child, err) { - child.kill(); - if (err) { - console.error(err); - process.exit(1); - } +function sendEnd(socket) { + socket.write(Buffer.from([0x88, 0x80, 0x2D, 0x0E, 0x1E, 0xFA])); } -function parseWSFrame(buffer) { +function parseWSFrame(buffer, handler) { // Protocol described in https://tools.ietf.org/html/rfc6455#section-5 - let message = null; if (buffer.length < 2) - return { length: 0, message }; + return 0; if (buffer[0] === 0x88 && buffer[1] === 0x00) { - return { length: 2, message, closed: true }; + handler(null); + return 2; } assert.strictEqual(0x81, buffer[0]); let dataLen = 0x7F & buffer[1]; @@ -74,331 +74,482 @@ function parseWSFrame(buffer) { bodyOffset = 10; } if (buffer.length < bodyOffset + dataLen) - return { length: 0, message }; - message = JSON.parse( - buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8')); + 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; + } if (DEBUG) console.log('[received]', JSON.stringify(message)); - return { length: bodyOffset + dataLen, message }; + handler(message); + return bodyOffset + dataLen; } -function formatWSFrame(message) { - const messageBuf = Buffer.from(JSON.stringify(message)); +function tearDown(child, err) { + child.kill(); + if (err instanceof Error) { + console.error(err.stack); + process.exit(1); + } +} - const wsHeaderBuf = Buffer.allocUnsafe(16); - wsHeaderBuf.writeUInt8(0x81, 0); - let byte2 = 0x80; - const bodyLen = messageBuf.length; +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); +} - 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 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); + }; +} - for (let i = 0; i < messageBuf.length; i++) - messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); +function timeout(message, multiplicator) { + return setTimeout(common.mustNotCall(message), + TIMEOUT * (multiplicator || 1)); +} - return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]); +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; + + 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_(); + }); } -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); - }); - } +TestSession.prototype.scriptUrlForId = function(id) { + return this.scripts_[id]; +}; - waitForServerDisconnect() { - return this._terminationPromise; +TestSession.prototype.processMessage_ = function(message) { + if (message === null) { + sendEnd(this.socket_); + return; } - disconnect() { - this._socket.destroy(); + 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; } + 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; + } + } +}; - _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); +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; } else { - 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); - } + id += 100000; + this.lastId_--; } + send(this.socket_, command, id, + () => this.sendAll_(commands.slice(1), callback)); } +}; - _sendMessage(message) { - const msg = JSON.parse(JSON.stringify(message)); // Clone! - msg['id'] = this._nextId++; - if (DEBUG) - console.log('[sent]', JSON.stringify(msg)); +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); + }); + }); +}; - const responsePromise = new Promise((resolve, reject) => { - this._commandResponsePromises.set(msg['id'], { resolve, reject }); +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'); + } }); + }); +}; - return new Promise( - (resolve) => this._socket.write(formatWSFrame(msg), resolve)) - .then(() => responsePromise); - } +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()); +}; - 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); +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(); } - } + }; + return this; +}; + +TestSession.prototype.expectStderrOutput = function(regexp) { + this.harness_.addStderrFilter( + regexp, + this.createCallbackWithTimeout_(`Timed out waiting for ${regexp}`)); + return this; +}; - waitForNotification(methodOrPredicate, description) { - const desc = description || methodOrPredicate; - const message = `Timed out waiting for matching notification (${desc}))`; - return common.fires( - this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT); +TestSession.prototype.runNext_ = function() { + if (this.task_) { + setImmediate(() => { + this.task_(() => { + this.task_ = this.task_.next_; + this.runNext_(); + }); + }); } +}; - 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; +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; } + 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; + }); +} - _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']); +Harness.prototype.addStderrFilter = function(regexp, callback) { + this.stderrFilters_.push((message) => { + if (message.match(regexp)) { + callback(); return true; } - } + }); +}; + +Harness.prototype.assertStillAlive = function() { + assert.strictEqual(this.running_, true, + `Child died: ${JSON.stringify(this.result_)}`); +}; - 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); +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; } + return this; +}; - _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; +Harness.prototype.testHttpResponse = function(host, path, check, errorcb) { + return this.enqueue_((doneCallback) => { + function wrap(callback) { + if (callback) { + return function() { + callback(...arguments); + doneCallback(); + }; } } - } + checkHttpResponse(host, this.port, path, wrap(check), wrap(errorcb)); + }); +}; - waitForConsoleOutput(type, values) { - const desc = `Console output matching ${JSON.stringify(values)}`; - return this.waitForNotification( - (notification) => this._matchesConsoleOutputNotification(notification, - type, values), - desc); - } +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')); +}; - 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; +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); }); - while ((await this._instance.nextStderrString()) !== - 'Waiting for the debugger to disconnect...'); - await this.disconnect(); - } -} + }); +}; -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; +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(); }); - }); - } - - 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 { - this._unprocessedStderrLines.push(line); + assert.strictEqual(errorCode, this.result_.code); + callback(); } - } + }, true); +}; - 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; - } - }); - } +Harness.prototype.kill = function() { + return this.enqueue_((callback) => { + this.process_.kill(); + callback(); + }); +}; - 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')); - })); +exports.startNodeForInspectorTest = function(callback, + inspectorFlags = ['--inspect-brk'], + scriptContents = '', + scriptFile = mainScript) { + const args = [].concat(inspectorFlags); + if (scriptContents) { + args.push('-e', scriptContents); + } else { + args.push(scriptFile); } - 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); - } + const child = spawn(process.execPath, args); - expectShutdown() { - return this._shutdownPromise; - } + const timeoutId = timeout('Child process did not start properly', 4); - nextStderrString() { - if (this._unprocessedStderrLines.length) - return Promise.resolve(this._unprocessedStderrLines.shift()); - return new Promise((resolve) => this._stderrLineCallback = resolve); - } + let found = false; - kill() { - this._process.kill(); - } -} + 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)); + }); -function readMainScriptSource() { - return fs.readFileSync(_MAINSCRIPT, 'utf8'); -} + child.stderr.on('data', dataCallback); + + const handler = tearDown.bind(null, child); + + process.on('exit', handler); + process.on('uncaughtException', handler); + process.on('SIGINT', handler); +}; + +exports.mainScriptSource = function() { + return fs.readFileSync(mainScript, 'utf8'); +}; -module.exports = { - mainScriptPath: _MAINSCRIPT, - readMainScriptSource, - NodeInstance +exports.markMessageNoResponse = function(message) { + message[DONT_EXPECT_RESPONSE_SYMBOL] = true; }; diff --git a/test/inspector/test-break-when-eval.js b/test/inspector/test-break-when-eval.js deleted file mode 100644 index ddd8220bb92e5f..00000000000000 --- a/test/inspector/test-break-when-eval.js +++ /dev/null @@ -1,68 +0,0 @@ -'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 deleted file mode 100644 index f0a4d976028f09..00000000000000 --- a/test/inspector/test-debug-brk-flag.js +++ /dev/null @@ -1,41 +0,0 @@ -'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 deleted file mode 100644 index 57ce0190838504..00000000000000 --- a/test/inspector/test-debug-end.js +++ /dev/null @@ -1,46 +0,0 @@ -'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 deleted file mode 100644 index ca3994c0a0005f..00000000000000 --- a/test/inspector/test-exception.js +++ /dev/null @@ -1,45 +0,0 @@ -'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 new file mode 100644 index 00000000000000..957d7ea4d2f2f5 --- /dev/null +++ b/test/inspector/test-inspector-break-when-eval.js @@ -0,0 +1,128 @@ +'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 new file mode 100644 index 00000000000000..1d7af9e318ab8d --- /dev/null +++ b/test/inspector/test-inspector-debug-brk.js @@ -0,0 +1,59 @@ +'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 new file mode 100644 index 00000000000000..2c6432c65d059f --- /dev/null +++ b/test/inspector/test-inspector-exception.js @@ -0,0 +1,64 @@ +'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 new file mode 100644 index 00000000000000..be5e34a977eb84 --- /dev/null +++ b/test/inspector/test-inspector-ip-detection.js @@ -0,0 +1,51 @@ +'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-port-cluster.js b/test/inspector/test-inspector-port-cluster.js similarity index 100% rename from test/inspector/test-port-cluster.js rename to test/inspector/test-inspector-port-cluster.js diff --git a/test/inspector/test-port-zero-cluster.js b/test/inspector/test-inspector-port-zero-cluster.js similarity index 100% rename from test/inspector/test-port-zero-cluster.js rename to test/inspector/test-inspector-port-zero-cluster.js diff --git a/test/inspector/test-port-zero.js b/test/inspector/test-inspector-port-zero.js similarity index 100% rename from test/inspector/test-port-zero.js rename to test/inspector/test-inspector-port-zero.js diff --git a/test/inspector/test-inspector-stop-profile-after-done.js b/test/inspector/test-inspector-stop-profile-after-done.js new file mode 100644 index 00000000000000..db43e4ae79c4bd --- /dev/null +++ b/test/inspector/test-inspector-stop-profile-after-done.js @@ -0,0 +1,21 @@ +'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-stops-no-file.js b/test/inspector/test-inspector-stops-no-file.js similarity index 100% rename from test/inspector/test-stops-no-file.js rename to test/inspector/test-inspector-stops-no-file.js diff --git a/test/inspector/test-inspector.js b/test/inspector/test-inspector.js index 3139940451a515..f933232553587e 100644 --- a/test/inspector/test-inspector.js +++ b/test/inspector/test-inspector.js @@ -4,11 +4,12 @@ const common = require('../common'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const { mainScriptPath, - readMainScriptSource, - NodeInstance } = require('./inspector-helper.js'); +const helper = require('./inspector-helper.js'); -function checkListResponse(response) { +let scopeId; + +function checkListResponse(err, response) { + assert.ifError(err); assert.strictEqual(1, response.length); assert.ok(response[0]['devtoolsFrontendUrl']); assert.ok( @@ -16,7 +17,8 @@ function checkListResponse(response) { .test(response[0]['webSocketDebuggerUrl'])); } -function checkVersion(response) { +function checkVersion(err, response) { + assert.ifError(err); assert.ok(response); const expected = { 'Browser': `node.js/${process.version}`, @@ -26,10 +28,10 @@ function checkVersion(response) { JSON.stringify(expected)); } -function checkBadPath(err) { +function checkBadPath(err, response) { assert(err instanceof SyntaxError); - assert(/Unexpected token/.test(err.message), err.message); - assert(/WebSockets request was expected/.test(err.body), err.body); + assert(/Unexpected token/.test(err.message)); + assert(/WebSockets request was expected/.test(err.response)); } function checkException(message) { @@ -37,26 +39,69 @@ function checkException(message) { 'An exception occurred during execution'); } -function assertNoUrlsWhileConnected(response) { - assert.strictEqual(1, response.length); - assert.ok(!response[0].hasOwnProperty('devtoolsFrontendUrl')); - assert.ok(!response[0].hasOwnProperty('webSocketDebuggerUrl')); +function expectMainScriptSource(result) { + const expected = helper.mainScriptSource(); + const source = result['scriptSource']; + assert(source && (source.includes(expected)), + `Script source is wrong: ${source}`); } -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']); +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; } - } - if (unmatched.size) - assert.fail(Array.from(unmatched.values())); + }; } -async function testBreakpointOnStart(session) { +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 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 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) { console.log('[test]', 'Verifying debugger stops on start (--inspect-brk option)'); const commands = [ @@ -74,230 +119,262 @@ async function testBreakpointOnStart(session) { { 'method': 'Runtime.runIfWaitingForDebugger' } ]; - await session.send(commands); - await session.waitForBreakOnLine(0, mainScriptPath); + session + .sendInspectorCommands(commands) + .expectMessages(setupExpectBreakOnLine(0, session.mainScriptPath, session)); } -async function testBreakpoint(session) { +function testSetBreakpointAndResume(session) { console.log('[test]', 'Setting a breakpoint and verifying it is hit'); const commands = [ { 'method': 'Debugger.setBreakpointByUrl', 'params': { 'lineNumber': 5, - 'url': mainScriptPath, + 'url': session.mainScriptPath, 'columnNumber': 0, 'condition': '' } }, { 'method': 'Debugger.resume' }, + [ { 'method': 'Debugger.getScriptSource', + 'params': { 'scriptId': session.mainScriptId } }, + expectMainScriptSource ], ]; - 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); + session + .sendInspectorCommands(commands) + .expectMessages([ + setupExpectConsoleOutput('log', ['A message', 5]), + setupExpectBreakOnLine(5, session.mainScriptPath, + session, (id) => scopeId = id), + ]); +} +function testInspectScope(session) { console.log('[test]', 'Verify we can read current application state'); - const response = await session.send({ - 'method': 'Runtime.getProperties', - 'params': { - 'objectId': scopeId, - 'ownProperties': false, - 'accessorPropertiesOnly': false, - 'generatePreview': true - } - }); - assertScopeValues(response, { t: 1001, k: 1 }); + 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']) + ], + ]); +} - 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 - } +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')); }); - - assert.strictEqual(1002, result['value']); - - result = (await session.send({ - 'method': 'Runtime.evaluate', 'params': { - 'expression': '5 * 5' - } - })).result; - assert.strictEqual(25, result['value']); } -async function testI18NCharacters(session) { +function testI18NCharacters(session) { console.log('[test]', 'Verify sending and receiving UTF8 characters'); const chars = 'טֶ字и'; - 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 + 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 + } } - }); - await session.waitForConsoleOutput('log', [chars]); + ]).expectMessages([ + setupExpectConsoleOutput('log', [chars]), + ]); } -async function testCommandLineAPI(session) { +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); - - // 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 + 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); } - }); - 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 + ], + [ // 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); - // `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 + ], + [ // `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); - // 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 + ], + [ // 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.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 + ], + [ // 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.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 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.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 + ], + [ // 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']), {}); - // 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 + ], + [ // 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']), { - 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 + ], + [ // 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.notStrictEqual(result['result']['value'], - ''); + ], + ]); } -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); - - 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); +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); } -common.crashOnUnhandledRejection(); +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); +} -runTest(); +helper.startNodeForInspectorTest(runTests); diff --git a/test/inspector/test-ip-detection.js b/test/inspector/test-ip-detection.js deleted file mode 100644 index 5a6a116144e7d2..00000000000000 --- a/test/inspector/test-ip-detection.js +++ /dev/null @@ -1,48 +0,0 @@ -'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 8684d6f3143387..1573e875cd4f9c 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 { NodeInstance } = require('./inspector-helper.js'); +const helper = require('./inspector-helper.js'); -async function runTests() { - const script = 'setInterval(() => {debugger;}, 60000);'; - const node = new NodeInstance('--inspect=0', script); +function shouldShutDown(session) { + session + .sendInspectorCommands([ + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.pause' }, + ]) + .disconnect(true); +} + +function runTests(harness) { // 1 second wait to make sure the inferior began running the script - 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(); + setTimeout(() => harness.runFrontendSession([shouldShutDown]).kill(), 1000); } -common.crashOnUnhandledRejection(); -runTests(); +const script = 'setInterval(() => {debugger;}, 60000);'; +helper.startNodeForInspectorTest(runTests, '--inspect', script); diff --git a/test/inspector/test-off-no-session.js b/test/inspector/test-off-no-session.js new file mode 100644 index 00000000000000..2ec54f3651e268 --- /dev/null +++ b/test/inspector/test-off-no-session.js @@ -0,0 +1,11 @@ +'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 new file mode 100644 index 00000000000000..bd6455699d8dc0 --- /dev/null +++ b/test/inspector/test-off-with-session-then-on.js @@ -0,0 +1,24 @@ +'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-stop-profile-after-done.js b/test/inspector/test-stop-profile-after-done.js deleted file mode 100644 index 314c429d461723..00000000000000 --- a/test/inspector/test-stop-profile-after-done.js +++ /dev/null @@ -1,30 +0,0 @@ -'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();