Browse Source

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 <timothygu99@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
v6
Eugene Ostroukhov 8 years ago
parent
commit
5f31d54720
  1. 9
      test/common/README.md
  2. 42
      test/common/index.js
  3. 717
      test/inspector/inspector-helper.js
  4. 68
      test/inspector/test-break-when-eval.js
  5. 41
      test/inspector/test-debug-brk-flag.js
  6. 46
      test/inspector/test-debug-end.js
  7. 45
      test/inspector/test-exception.js
  8. 128
      test/inspector/test-inspector-break-when-eval.js
  9. 59
      test/inspector/test-inspector-debug-brk.js
  10. 64
      test/inspector/test-inspector-exception.js
  11. 51
      test/inspector/test-inspector-ip-detection.js
  12. 21
      test/inspector/test-inspector-stop-profile-after-done.js
  13. 311
      test/inspector/test-inspector.js
  14. 48
      test/inspector/test-ip-detection.js
  15. 28
      test/inspector/test-not-blocked-on-idle.js
  16. 11
      test/inspector/test-off-no-session.js
  17. 24
      test/inspector/test-off-with-session-then-on.js
  18. 0
      test/inspector/test-port-cluster.js
  19. 0
      test/inspector/test-port-zero-cluster.js
  20. 0
      test/inspector/test-port-zero.js
  21. 30
      test/inspector/test-stop-profile-after-done.js
  22. 0
      test/inspector/test-stops-no-file.js

9
test/common/README.md

@ -99,6 +99,15 @@ Tests whether `name` and `expected` are part of a raised warning.
Checks if `pathname` exists Checks if `pathname` exists
### fires(promise, [error], [timeoutMs])
* promise [&lt;Promise]
* error [&lt;String] default = 'timeout'
* timeoutMs [&lt;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 ### fixturesDir
* return [&lt;String>] * return [&lt;String>]

42
test/common/index.js

@ -814,6 +814,32 @@ function restoreWritable(name) {
delete process[name].writeTimes; 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.hijackStdout = hijackStdWritable.bind(null, 'stdout');
exports.hijackStderr = hijackStdWritable.bind(null, 'stderr'); exports.hijackStderr = hijackStdWritable.bind(null, 'stderr');
exports.restoreStdout = restoreWritable.bind(null, 'stdout'); exports.restoreStdout = restoreWritable.bind(null, 'stdout');
@ -827,3 +853,19 @@ exports.firstInvalidFD = function firstInvalidFD() {
} catch (e) {} } catch (e) {}
return fd; 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
]);
};

717
test/inspector/inspector-helper.js

@ -4,61 +4,61 @@ const assert = require('assert');
const fs = require('fs'); const fs = require('fs');
const http = require('http'); const http = require('http');
const path = require('path'); const path = require('path');
const spawn = require('child_process').spawn; const { spawn } = require('child_process');
const url = require('url'); const url = require('url');
const _MAINSCRIPT = path.join(common.fixturesDir, 'loop.js');
const DEBUG = false; const DEBUG = false;
const TIMEOUT = 15 * 1000; 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) { function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) {
const msg = JSON.parse(JSON.stringify(message)); // Clone! const args = [].concat(inspectorFlags);
msg['id'] = id; if (scriptContents) {
if (DEBUG) args.push('-e', scriptContents);
console.log('[sent]', JSON.stringify(msg)); } else {
const messageBuf = Buffer.from(JSON.stringify(msg)); args.push(scriptFile);
}
const child = spawn(process.execPath, args);
const wsHeaderBuf = Buffer.allocUnsafe(16); const handler = tearDown.bind(null, child);
wsHeaderBuf.writeUInt8(0x81, 0); process.on('exit', handler);
let byte2 = 0x80; process.on('uncaughtException', handler);
const bodyLen = messageBuf.length; process.on('unhandledRejection', handler);
process.on('SIGINT', handler);
let maskOffset = 2; return child;
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);
for (let i = 0; i < messageBuf.length; i++) function makeBufferingDataCallback(dataCallback) {
messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); let buffer = Buffer.alloc(0);
socket.write( return (data) => {
Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]), const newData = Buffer.concat([buffer, data]);
callback); 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 sendEnd(socket) { function tearDown(child, err) {
socket.write(Buffer.from([0x88, 0x80, 0x2D, 0x0E, 0x1E, 0xFA])); 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 // Protocol described in https://tools.ietf.org/html/rfc6455#section-5
let message = null;
if (buffer.length < 2) if (buffer.length < 2)
return 0; return { length: 0, message };
if (buffer[0] === 0x88 && buffer[1] === 0x00) { if (buffer[0] === 0x88 && buffer[1] === 0x00) {
handler(null); return { length: 2, message, closed: true };
return 2;
} }
assert.strictEqual(0x81, buffer[0]); assert.strictEqual(0x81, buffer[0]);
let dataLen = 0x7F & buffer[1]; let dataLen = 0x7F & buffer[1];
@ -74,10 +74,9 @@ function parseWSFrame(buffer, handler) {
bodyOffset = 10; bodyOffset = 10;
} }
if (buffer.length < bodyOffset + dataLen) if (buffer.length < bodyOffset + dataLen)
return 0; return { length: 0, message };
const jsonPayload = const jsonPayload =
buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8'); buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8');
let message;
try { try {
message = JSON.parse(jsonPayload); message = JSON.parse(jsonPayload);
} catch (e) { } catch (e) {
@ -86,368 +85,285 @@ function parseWSFrame(buffer, handler) {
} }
if (DEBUG) if (DEBUG)
console.log('[received]', JSON.stringify(message)); console.log('[received]', JSON.stringify(message));
handler(message); return { length: bodyOffset + dataLen, message };
return bodyOffset + dataLen;
} }
function tearDown(child, err) { function formatWSFrame(message) {
child.kill(); const messageBuf = Buffer.from(JSON.stringify(message));
if (err instanceof Error) {
console.error(err.stack);
process.exit(1);
}
}
function checkHttpResponse(host, port, path, callback, errorcb) { const wsHeaderBuf = Buffer.allocUnsafe(16);
const req = http.get({ host, port, path }, function(res) { wsHeaderBuf.writeUInt8(0x81, 0);
let response = ''; let byte2 = 0x80;
res.setEncoding('utf8'); const bodyLen = messageBuf.length;
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 maskOffset = 2;
let buffer = Buffer.alloc(0); if (bodyLen < 126) {
return (data) => { byte2 = 0x80 + bodyLen;
const newData = Buffer.concat([buffer, data]); } else if (bodyLen < 65536) {
const str = newData.toString('utf8'); byte2 = 0xFE;
const lines = str.split('\n'); wsHeaderBuf.writeUInt16BE(bodyLen, 2);
if (str.endsWith('\n')) maskOffset = 4;
buffer = Buffer.alloc(0); } else {
else byte2 = 0xFF;
buffer = Buffer.from(lines.pop(), 'utf8'); wsHeaderBuf.writeUInt32BE(bodyLen, 2);
for (const line of lines) wsHeaderBuf.writeUInt32BE(0, 6);
dataCallback(line); maskOffset = 10;
};
} }
wsHeaderBuf.writeUInt8(byte2, 1);
wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset);
function timeout(message, multiplicator) { for (let i = 0; i < messageBuf.length; i++)
return setTimeout(common.mustNotCall(message), messageBuf[i] = messageBuf[i] ^ (1 << (i % 4));
TIMEOUT * (multiplicator || 1));
}
function TestSession(socket, harness) { return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]);
this.mainScriptPath = harness.mainScriptPath; }
this.mainScriptId = null;
this.harness_ = harness; class InspectorSession {
this.socket_ = socket; constructor(socket, instance) {
this.expectClose_ = false; this._instance = instance;
this.scripts_ = {}; this._socket = socket;
this.messagefilter_ = null; this._nextId = 1;
this.responseCheckers_ = {}; this._commandResponsePromises = new Map();
this.lastId_ = 0; this._unprocessedNotifications = [];
this.messages_ = {}; this._notificationCallback = null;
this.expectedId_ = 1; this._scriptsIdsByUrl = new Map();
this.lastMessageResponseCallback_ = null;
this.closeCallback_ = null;
let buffer = Buffer.alloc(0); let buffer = Buffer.alloc(0);
socket.on('data', (data) => { socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data]); buffer = Buffer.concat([buffer, data]);
let consumed;
do { do {
consumed = parseWSFrame(buffer, this.processMessage_.bind(this)); const { length, message, closed } = parseWSFrame(buffer);
if (consumed) if (!length)
buffer = buffer.slice(consumed); break;
} while (consumed);
}).on('close', () => { if (closed) {
assert(this.expectClose_, 'Socket closed prematurely'); socket.write(Buffer.from([0x88, 0x00])); // WS close frame
this.closeCallback_ && this.closeCallback_(); }
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) { waitForServerDisconnect() {
return this.scripts_[id]; return this._terminationPromise;
}; }
TestSession.prototype.processMessage_ = function(message) { disconnect() {
if (message === null) { this._socket.destroy();
sendEnd(this.socket_);
return;
} }
const method = message['method']; _onMessage(message) {
if (method === 'Debugger.scriptParsed') { 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 {
if (message.method === 'Debugger.scriptParsed') {
const script = message['params']; const script = message['params'];
const scriptId = script['scriptId']; const scriptId = script['scriptId'];
const url = script['url']; const url = script['url'];
this.scripts_[scriptId] = url; this._scriptsIdsByUrl.set(scriptId, url);
if (url === mainScript) if (url === _MAINSCRIPT)
this.mainScriptId = scriptId; 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;
}
}
};
TestSession.prototype.sendAll_ = function(commands, callback) { if (this._notificationCallback) {
if (!commands.length) { // In case callback needs to install another
callback(); const callback = this._notificationCallback;
} else { this._notificationCallback = null;
let id = ++this.lastId_; callback(message);
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 { } else {
id += 100000; this._unprocessedNotifications.push(message);
this.lastId_--;
} }
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);
});
});
};
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');
} }
});
});
};
TestSession.prototype.createCallbackWithTimeout_ = function(message) { _sendMessage(message) {
const promise = new Promise((resolve) => { const msg = JSON.parse(JSON.stringify(message)); // Clone!
this.enqueue((callback) => { msg['id'] = this._nextId++;
const timeoutId = timeout(message); if (DEBUG)
resolve(() => { console.log('[sent]', JSON.stringify(msg));
clearTimeout(timeoutId);
callback();
});
});
});
return () => promise.then((callback) => callback());
};
TestSession.prototype.expectMessages = function(expects) { const responsePromise = new Promise((resolve, reject) => {
if (!(expects instanceof Array)) expects = [ expects ]; this._commandResponsePromises.set(msg['id'], { resolve, reject });
});
const callback = this.createCallbackWithTimeout_( return new Promise(
`Matching response was not received:\n${expects[0]}`); (resolve) => this._socket.write(formatWSFrame(msg), resolve))
this.messagefilter_ = (message) => { .then(() => responsePromise);
if (expects[0](message))
expects.shift();
if (!expects.length) {
this.messagefilter_ = null;
callback();
} }
};
return this;
};
TestSession.prototype.expectStderrOutput = function(regexp) { send(commands) {
this.harness_.addStderrFilter( if (Array.isArray(commands)) {
regexp, // Multiple commands means the response does not matter. There might even
this.createCallbackWithTimeout_(`Timed out waiting for ${regexp}`)); // never be a response.
return this; return Promise
}; .all(commands.map((command) => this._sendMessage(command)))
.then(() => {});
} else {
return this._sendMessage(commands);
}
}
TestSession.prototype.runNext_ = function() { waitForNotification(methodOrPredicate, description) {
if (this.task_) { const desc = description || methodOrPredicate;
setImmediate(() => { const message = `Timed out waiting for matching notification (${desc}))`;
this.task_(() => { return common.fires(
this.task_ = this.task_.next_; this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT);
this.runNext_();
});
});
} }
};
TestSession.prototype.enqueue = function(task) { async _asyncWaitForNotification(methodOrPredicate) {
if (!this.task_) { function matchMethod(notification) {
this.task_ = task; return notification.method === methodOrPredicate;
this.runNext_(); }
const predicate =
typeof methodOrPredicate === 'string' ? matchMethod : methodOrPredicate;
let notification = null;
do {
if (this._unprocessedNotifications.length) {
notification = this._unprocessedNotifications.shift();
} else { } else {
let t = this.task_; notification = await new Promise(
while (t.next_) (resolve) => this._notificationCallback = resolve);
t = t.next_; }
t.next_ = task; } while (!predicate(notification));
return notification;
} }
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();
}));
};
_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;
}
}
function Harness(port, childProcess) { waitForBreakOnLine(line, url) {
this.port = port; return this
this.mainScriptPath = mainScript; .waitForNotification(
this.stderrFilters_ = []; (notification) =>
this.process_ = childProcess; this._isBreakOnLineNotification(notification, line, url),
this.result_ = {}; `break on ${url}:${line}`)
this.running_ = true; .then((notification) =>
notification.params.callFrames[0].scopeChain[0].object.objectId);
}
childProcess.stdout.on('data', makeBufferingDataCallback( _matchesConsoleOutputNotification(notification, type, values) {
(line) => console.log('[out]', line))); 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;
}
}
}
waitForConsoleOutput(type, values) {
const desc = `Console output matching ${JSON.stringify(values)}`;
return this.waitForNotification(
(notification) => this._matchesConsoleOutputNotification(notification,
type, values),
desc);
}
childProcess.stderr.on('data', makeBufferingDataCallback((message) => { async runToCompletion() {
const pending = []; console.log('[test]', 'Verify node waits for the frontend to disconnect');
console.log('[err]', message); await this.send({ 'method': 'Debugger.resume' });
for (const filter of this.stderrFilters_) await this.waitForNotification((notification) => {
if (!filter(message)) pending.push(filter); return notification.method === 'Runtime.executionContextDestroyed' &&
this.stderrFilters_ = pending; notification.params.executionContextId === 1;
}));
childProcess.on('exit', (code, signal) => {
this.result_ = { code, signal };
this.running_ = false;
}); });
while ((await this._instance.nextStderrString()) !==
'Waiting for the debugger to disconnect...');
await this.disconnect();
} }
Harness.prototype.addStderrFilter = function(regexp, callback) {
this.stderrFilters_.push((message) => {
if (message.match(regexp)) {
callback();
return true;
} }
});
};
Harness.prototype.assertStillAlive = function() { class NodeInstance {
assert.strictEqual(this.running_, true, constructor(inspectorFlags = ['--inspect-brk=0'],
`Child died: ${JSON.stringify(this.result_)}`); 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)));
Harness.prototype.run_ = function() { this._process.stderr.on('data', makeBufferingDataCallback(
setImmediate(() => { (message) => this.onStderrLine(message)));
if (!this.task_[EXPECT_ALIVE_SYMBOL])
this.assertStillAlive(); this._shutdownPromise = new Promise((resolve) => {
this.task_(() => { this._process.once('exit', (exitCode, signal) => {
this.task_ = this.task_.next_; resolve({ exitCode, signal });
if (this.task_) this._running = false;
this.run_();
}); });
}); });
}; }
Harness.prototype.enqueue_ = function(task, expectAlive) { onStderrLine(line) {
task[EXPECT_ALIVE_SYMBOL] = !!expectAlive; console.log('[err]', line);
if (!this.task_) { if (this._portCallback) {
this.task_ = task; const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/);
this.run_(); if (matches)
this._portCallback(matches[1]);
this._portCallback = null;
}
if (this._stderrLineCallback) {
this._stderrLineCallback(line);
this._stderrLineCallback = null;
} else { } else {
let chain = this.task_; this._unprocessedStderrLines.push(line);
while (chain.next_)
chain = chain.next_;
chain.next_ = task;
} }
return this;
};
Harness.prototype.testHttpResponse = function(host, path, check, errorcb) {
return this.enqueue_((doneCallback) => {
function wrap(callback) {
if (callback) {
return function() {
callback(...arguments);
doneCallback();
};
} }
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;
} }
checkHttpResponse(host, this.port, path, wrap(check), wrap(errorcb));
}); });
}; }
Harness.prototype.wsHandshake = function(devtoolsUrl, tests, readyCallback) { wsHandshake(devtoolsUrl) {
return this.portPromise.then((port) => new Promise((resolve) => {
http.get({ http.get({
port: this.port, port,
path: url.parse(devtoolsUrl).path, path: url.parse(devtoolsUrl).path,
headers: { headers: {
'Connection': 'Upgrade', 'Connection': 'Upgrade',
@ -456,100 +372,39 @@ Harness.prototype.wsHandshake = function(devtoolsUrl, tests, readyCallback) {
'Sec-WebSocket-Key': 'key==' 'Sec-WebSocket-Key': 'key=='
} }
}).on('upgrade', (message, socket) => { }).on('upgrade', (message, socket) => {
const session = new TestSession(socket, this); resolve(new InspectorSession(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')); }).on('response', common.mustNotCall('Upgrade was not received'));
}; }));
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);
});
});
};
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();
});
} else {
assert.strictEqual(errorCode, this.result_.code);
callback();
} }
}, true);
};
Harness.prototype.kill = function() {
return this.enqueue_((callback) => {
this.process_.kill();
callback();
});
};
exports.startNodeForInspectorTest = function(callback, async connectInspectorSession() {
inspectorFlags = ['--inspect-brk'], console.log('[test]', 'Connecting to a child Node process');
scriptContents = '', const response = await this.httpGet(null, '/json/list');
scriptFile = mainScript) { const url = response[0]['webSocketDebuggerUrl'];
const args = [].concat(inspectorFlags); return await this.wsHandshake(url);
if (scriptContents) {
args.push('-e', scriptContents);
} else {
args.push(scriptFile);
} }
const child = spawn(process.execPath, args); expectShutdown() {
return this._shutdownPromise;
const timeoutId = timeout('Child process did not start properly', 4); }
let found = false;
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));
});
child.stderr.on('data', dataCallback);
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); kill() {
process.on('uncaughtException', handler); this._process.kill();
process.on('SIGINT', handler); }
}; }
exports.mainScriptSource = function() { function readMainScriptSource() {
return fs.readFileSync(mainScript, 'utf8'); return fs.readFileSync(_MAINSCRIPT, 'utf8');
}; }
exports.markMessageNoResponse = function(message) { module.exports = {
message[DONT_EXPECT_RESPONSE_SYMBOL] = true; mainScriptPath: _MAINSCRIPT,
readMainScriptSource,
NodeInstance
}; };

68
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();

41
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();

46
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();

45
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();

128
test/inspector/test-inspector-break-when-eval.js

@ -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);

59
test/inspector/test-inspector-debug-brk.js

@ -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']);

64
test/inspector/test-inspector-exception.js

@ -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);

51
test/inspector/test-inspector-ip-detection.js

@ -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');

21
test/inspector/test-inspector-stop-profile-after-done.js

@ -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;');

311
test/inspector/test-inspector.js

@ -4,12 +4,11 @@ const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
const assert = require('assert'); const assert = require('assert');
const helper = require('./inspector-helper.js'); const { mainScriptPath,
readMainScriptSource,
NodeInstance } = require('./inspector-helper.js');
let scopeId; function checkListResponse(response) {
function checkListResponse(err, response) {
assert.ifError(err);
assert.strictEqual(1, response.length); assert.strictEqual(1, response.length);
assert.ok(response[0]['devtoolsFrontendUrl']); assert.ok(response[0]['devtoolsFrontendUrl']);
assert.ok( assert.ok(
@ -17,8 +16,7 @@ function checkListResponse(err, response) {
.test(response[0]['webSocketDebuggerUrl'])); .test(response[0]['webSocketDebuggerUrl']));
} }
function checkVersion(err, response) { function checkVersion(response) {
assert.ifError(err);
assert.ok(response); assert.ok(response);
const expected = { const expected = {
'Browser': `node.js/${process.version}`, 'Browser': `node.js/${process.version}`,
@ -28,10 +26,10 @@ function checkVersion(err, response) {
JSON.stringify(expected)); JSON.stringify(expected));
} }
function checkBadPath(err, response) { function checkBadPath(err) {
assert(err instanceof SyntaxError); assert(err instanceof SyntaxError);
assert(/Unexpected token/.test(err.message)); assert(/Unexpected token/.test(err.message), err.message);
assert(/WebSockets request was expected/.test(err.response)); assert(/WebSockets request was expected/.test(err.body), err.body);
} }
function checkException(message) { function checkException(message) {
@ -39,69 +37,26 @@ function checkException(message) {
'An exception occurred during execution'); 'An exception occurred during execution');
} }
function expectMainScriptSource(result) { function assertNoUrlsWhileConnected(response) {
const expected = helper.mainScriptSource(); assert.strictEqual(1, response.length);
const source = result['scriptSource']; assert.ok(!response[0].hasOwnProperty('devtoolsFrontendUrl'));
assert(source && (source.includes(expected)), assert.ok(!response[0].hasOwnProperty('webSocketDebuggerUrl'));
`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 setupExpectScopeValues(expected) { function assertScopeValues({ result }, expected) {
return function(result) { const unmatched = new Set(Object.keys(expected));
for (const actual of result['result']) { for (const actual of result) {
const value = expected[actual['name']]; const value = expected[actual['name']];
if (value) if (value) {
assert.strictEqual(value, actual['value']['value']); assert.strictEqual(value, actual['value']['value']);
unmatched.delete(actual['name']);
} }
};
} }
if (unmatched.size)
function setupExpectValue(value) { assert.fail(Array.from(unmatched.values()));
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]', console.log('[test]',
'Verifying debugger stops on start (--inspect-brk option)'); 'Verifying debugger stops on start (--inspect-brk option)');
const commands = [ const commands = [
@ -119,40 +74,34 @@ function testBreakpointOnStart(session) {
{ 'method': 'Runtime.runIfWaitingForDebugger' } { 'method': 'Runtime.runIfWaitingForDebugger' }
]; ];
session await session.send(commands);
.sendInspectorCommands(commands) await session.waitForBreakOnLine(0, mainScriptPath);
.expectMessages(setupExpectBreakOnLine(0, session.mainScriptPath, session));
} }
function testSetBreakpointAndResume(session) { async function testBreakpoint(session) {
console.log('[test]', 'Setting a breakpoint and verifying it is hit'); console.log('[test]', 'Setting a breakpoint and verifying it is hit');
const commands = [ const commands = [
{ 'method': 'Debugger.setBreakpointByUrl', { 'method': 'Debugger.setBreakpointByUrl',
'params': { 'lineNumber': 5, 'params': { 'lineNumber': 5,
'url': session.mainScriptPath, 'url': mainScriptPath,
'columnNumber': 0, 'columnNumber': 0,
'condition': '' 'condition': ''
} }
}, },
{ 'method': 'Debugger.resume' }, { 'method': 'Debugger.resume' },
[ { 'method': 'Debugger.getScriptSource',
'params': { 'scriptId': session.mainScriptId } },
expectMainScriptSource ],
]; ];
session await session.send(commands);
.sendInspectorCommands(commands) const { scriptSource } = await session.send({
.expectMessages([ 'method': 'Debugger.getScriptSource',
setupExpectConsoleOutput('log', ['A message', 5]), 'params': { 'scriptId': session.mainScriptId } });
setupExpectBreakOnLine(5, session.mainScriptPath, assert(scriptSource && (scriptSource.includes(readMainScriptSource())),
session, (id) => scopeId = id), `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'); console.log('[test]', 'Verify we can read current application state');
session.sendInspectorCommands([ const response = await session.send({
[
{
'method': 'Runtime.getProperties', 'method': 'Runtime.getProperties',
'params': { 'params': {
'objectId': scopeId, 'objectId': scopeId,
@ -160,10 +109,10 @@ function testInspectScope(session) {
'accessorPropertiesOnly': false, 'accessorPropertiesOnly': false,
'generatePreview': true 'generatePreview': true
} }
}, setupExpectScopeValues({ t: 1001, k: 1 }) });
], assertScopeValues(response, { t: 1001, k: 1 });
[
{ let { result } = await session.send({
'method': 'Debugger.evaluateOnCallFrame', 'params': { 'method': 'Debugger.evaluateOnCallFrame', 'params': {
'callFrameId': '{"ordinal":0,"injectedScriptId":1}', 'callFrameId': '{"ordinal":0,"injectedScriptId":1}',
'expression': 'k + t', 'expression': 'k + t',
@ -173,32 +122,22 @@ function testInspectScope(session) {
'returnByValue': false, 'returnByValue': false,
'generatePreview': true 'generatePreview': true
} }
}, setupExpectValue(1002) });
],
[ assert.strictEqual(1002, result['value']);
{
result = (await session.send({
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': '5 * 5' 'expression': '5 * 5'
} }
}, (message) => assert.strictEqual(25, message['result']['value']) })).result;
], assert.strictEqual(25, result['value']);
]);
}
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'));
});
} }
function testI18NCharacters(session) { async function testI18NCharacters(session) {
console.log('[test]', 'Verify sending and receiving UTF8 characters'); console.log('[test]', 'Verify sending and receiving UTF8 characters');
const chars = 'טֶ字и'; const chars = 'טֶ字и';
session.sendInspectorCommands([ session.send({
{
'method': 'Debugger.evaluateOnCallFrame', 'params': { 'method': 'Debugger.evaluateOnCallFrame', 'params': {
'callFrameId': '{"ordinal":0,"injectedScriptId":1}', 'callFrameId': '{"ordinal":0,"injectedScriptId":1}',
'expression': `console.log("${chars}")`, 'expression': `console.log("${chars}")`,
@ -208,32 +147,31 @@ function testI18NCharacters(session) {
'returnByValue': false, 'returnByValue': false,
'generatePreview': true 'generatePreview': true
} }
} });
]).expectMessages([ await session.waitForConsoleOutput('log', [chars]);
setupExpectConsoleOutput('log', [chars]),
]);
} }
function testCommandLineAPI(session) { async function testCommandLineAPI(session) {
const testModulePath = require.resolve('../fixtures/empty.js'); const testModulePath = require.resolve('../fixtures/empty.js');
const testModuleStr = JSON.stringify(testModulePath); const testModuleStr = JSON.stringify(testModulePath);
const printAModulePath = require.resolve('../fixtures/printA.js'); const printAModulePath = require.resolve('../fixtures/printA.js');
const printAModuleStr = JSON.stringify(printAModulePath); const printAModuleStr = JSON.stringify(printAModulePath);
const printBModulePath = require.resolve('../fixtures/printB.js'); const printBModulePath = require.resolve('../fixtures/printB.js');
const printBModuleStr = JSON.stringify(printBModulePath); const printBModuleStr = JSON.stringify(printBModulePath);
session.sendInspectorCommands([
[ // we can use `require` outside of a callframe with require in scope // we can use `require` outside of a callframe with require in scope
let result = await session.send(
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': 'typeof require("fs").readFile === "function"', 'expression': 'typeof require("fs").readFile === "function"',
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.strictEqual(message['result']['value'], true); assert.strictEqual(result['result']['value'], true);
}
], // the global require has the same properties as a normal `require`
[ // the global require has the same properties as a normal `require` result = await session.send(
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': [ 'expression': [
@ -243,12 +181,11 @@ function testCommandLineAPI(session) {
].join(' && '), ].join(' && '),
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.strictEqual(message['result']['value'], true); assert.strictEqual(result['result']['value'], true);
} // `require` twice returns the same value
], result = await session.send(
[ // `require` twice returns the same value
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
// 1. We require the same module twice // 1. We require the same module twice
@ -260,12 +197,11 @@ function testCommandLineAPI(session) {
) === require(${testModuleStr})`, ) === require(${testModuleStr})`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.strictEqual(message['result']['value'], true); assert.strictEqual(result['result']['value'], true);
} // after require the module appears in require.cache
], result = await session.send(
[ // after require the module appears in require.cache
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': `JSON.stringify( 'expression': `JSON.stringify(
@ -273,46 +209,42 @@ function testCommandLineAPI(session) {
)`, )`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.deepStrictEqual(JSON.parse(message['result']['value']), assert.deepStrictEqual(JSON.parse(result['result']['value']),
{ old: 'yes' }); { old: 'yes' });
} // remove module from require.cache
], result = await session.send(
[ // remove module from require.cache
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': `delete require.cache[${testModuleStr}]`, 'expression': `delete require.cache[${testModuleStr}]`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.strictEqual(message['result']['value'], true); assert.strictEqual(result['result']['value'], true);
} // require again, should get fresh (empty) exports
], result = await session.send(
[ // require again, should get fresh (empty) exports
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': `JSON.stringify(require(${testModuleStr}))`, 'expression': `JSON.stringify(require(${testModuleStr}))`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); assert.deepStrictEqual(JSON.parse(result['result']['value']), {});
} // require 2nd module, exports an empty object
], result = await session.send(
[ // require 2nd module, exports an empty object
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': `JSON.stringify(require(${printAModuleStr}))`, 'expression': `JSON.stringify(require(${printAModuleStr}))`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); assert.deepStrictEqual(JSON.parse(result['result']['value']), {});
} // both modules end up with the same module.parent
], result = await session.send(
[ // both modules end up with the same module.parent
{ {
'method': 'Runtime.evaluate', 'params': { 'method': 'Runtime.evaluate', 'params': {
'expression': `JSON.stringify({ 'expression': `JSON.stringify({
@ -323,15 +255,14 @@ function testCommandLineAPI(session) {
})`, })`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.deepStrictEqual(JSON.parse(message['result']['value']), { assert.deepStrictEqual(JSON.parse(result['result']['value']), {
parentsEqual: true, parentsEqual: true,
parentId: '<inspector console>' parentId: '<inspector console>'
}); });
} // the `require` in the module shadows the command line API's `require`
], result = await session.send(
[ // the `require` in the module shadows the command line API's `require`
{ {
'method': 'Debugger.evaluateOnCallFrame', 'params': { 'method': 'Debugger.evaluateOnCallFrame', 'params': {
'callFrameId': '{"ordinal":0,"injectedScriptId":1}', 'callFrameId': '{"ordinal":0,"injectedScriptId":1}',
@ -341,40 +272,32 @@ function testCommandLineAPI(session) {
)`, )`,
'includeCommandLineAPI': true 'includeCommandLineAPI': true
} }
}, (message) => { });
checkException(message); checkException(result);
assert.notStrictEqual(message['result']['value'], assert.notStrictEqual(result['result']['value'],
'<inspector console>'); '<inspector console>');
} }
],
]);
}
function testWaitsForFrontendDisconnect(session, harness) { async function runTest() {
console.log('[test]', 'Verify node waits for the frontend to disconnect'); const child = new NodeInstance();
session.sendInspectorCommands({ 'method': 'Debugger.resume' }) checkListResponse(await child.httpGet(null, '/json'));
.expectMessages(setupExpectContextDestroyed(1)) checkListResponse(await child.httpGet(null, '/json/list'));
.expectStderrOutput('Waiting for the debugger to disconnect...') checkVersion(await child.httpGet(null, '/json/version'));
.disconnect(true);
} 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) { const session = await child.connectInspectorSession();
harness assertNoUrlsWhileConnected(await child.httpGet(null, '/json/list'));
.testHttpResponse(null, '/json', checkListResponse) await testBreakpointOnStart(session);
.testHttpResponse(null, '/json/list', checkListResponse) await testBreakpoint(session);
.testHttpResponse(null, '/json/version', checkVersion) await testI18NCharacters(session);
.testHttpResponse(null, '/json/activate', checkBadPath) await testCommandLineAPI(session);
.testHttpResponse(null, '/json/activate/boom', checkBadPath) await session.runToCompletion();
.testHttpResponse(null, '/json/badpath', checkBadPath) assert.strictEqual(55, (await child.expectShutdown()).exitCode);
.runFrontendSession([
testNoUrlsWhenConnected,
testBreakpointOnStart,
testSetBreakpointAndResume,
testInspectScope,
testI18NCharacters,
testCommandLineAPI,
testWaitsForFrontendDisconnect
]).expectShutDown(55);
} }
helper.startNodeForInspectorTest(runTests); common.crashOnUnhandledRejection();
runTest();

48
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();

28
test/inspector/test-not-blocked-on-idle.js

@ -1,21 +1,21 @@
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
const helper = require('./inspector-helper.js'); const { NodeInstance } = require('./inspector-helper.js');
function shouldShutDown(session) { async function runTests() {
session const script = 'setInterval(() => {debugger;}, 60000);';
.sendInspectorCommands([ const node = new NodeInstance('--inspect=0', script);
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.pause' },
])
.disconnect(true);
}
function runTests(harness) {
// 1 second wait to make sure the inferior began running the 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);'; common.crashOnUnhandledRejection();
helper.startNodeForInspectorTest(runTests, '--inspect', script); runTests();

11
test/inspector/test-off-no-session.js

@ -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);');

24
test/inspector/test-off-with-session-then-on.js

@ -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);

0
test/inspector/test-inspector-port-cluster.js → test/inspector/test-port-cluster.js

0
test/inspector/test-inspector-port-zero-cluster.js → test/inspector/test-port-zero-cluster.js

0
test/inspector/test-inspector-port-zero.js → test/inspector/test-port-zero.js

30
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();

0
test/inspector/test-inspector-stops-no-file.js → test/inspector/test-stops-no-file.js

Loading…
Cancel
Save