mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 21:19:50 +01:00
0161ad0baf
`NodeRuntime.waitingForDebugger` is a new Inspector Protocol event that will fire when the process being inspected is waiting for the debugger (for example, when `inspector.waitForDebugger()` is called). This allows inspecting processes to know when the inspected process is waiting for a `Runtime.runIfWaitingForDebugger` message to resume execution. It allows tooling to resume execution of the inspected process as soon as it deems necessary, without having to guess if the inspected process is waiting or not, making the workflow more deterministic. With a more deterministic workflow, it is possible to update Node.js core tests to avoid race conditions that can cause flakiness. Therefore, tests were also changed as following: * Remove no-op Runtime.runIfWaitingForDebugger from tests that don't need it * Use NodeRuntime.waitingForDebugger in all tests that need Runtime.runIfWaitingForDebugger, to ensure order of operations is predictable and correct * Simplify test-inspector-multisession-ws There might be value in adding `NodeWorker.waitingForDebugger` in a future patch, but as of right now, no Node.js core inspector tests using worker threads are not failing due to race conditions. Fixes: https://github.com/nodejs/node/issues/34730 PR-URL: https://github.com/nodejs/node/pull/51560 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
320 lines
10 KiB
JavaScript
320 lines
10 KiB
JavaScript
'use strict';
|
|
const common = require('../common');
|
|
|
|
common.skipIfInspectorDisabled();
|
|
|
|
const assert = require('assert');
|
|
const { NodeInstance } = require('../common/inspector-helper.js');
|
|
|
|
function checkListResponse(response) {
|
|
const expectedLength = 1;
|
|
assert.strictEqual(
|
|
response.length,
|
|
expectedLength,
|
|
`Expected response length ${response.length} to be ${expectedLength}.`
|
|
);
|
|
assert.ok(response[0].devtoolsFrontendUrl);
|
|
assert.ok(
|
|
/ws:\/\/localhost:\d+\/[0-9A-Fa-f]{8}-/
|
|
.test(response[0].webSocketDebuggerUrl),
|
|
response[0].webSocketDebuggerUrl);
|
|
}
|
|
|
|
function checkVersion(response) {
|
|
assert.ok(response);
|
|
const expected = {
|
|
'Browser': `node.js/${process.version}`,
|
|
'Protocol-Version': '1.1',
|
|
};
|
|
assert.strictEqual(JSON.stringify(response),
|
|
JSON.stringify(expected));
|
|
}
|
|
|
|
function checkBadPath(err) {
|
|
assert(err instanceof SyntaxError);
|
|
assert.match(err.message, /Unexpected token/);
|
|
assert.match(err.body, /WebSockets request was expected/);
|
|
}
|
|
|
|
function checkException(message) {
|
|
assert.strictEqual(message.exceptionDetails, undefined);
|
|
}
|
|
|
|
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(
|
|
actual.value.value,
|
|
value,
|
|
`Expected scope values to be ${actual.value.value} instead of ${value}.`
|
|
);
|
|
unmatched.delete(actual.name);
|
|
}
|
|
}
|
|
if (unmatched.size)
|
|
assert.fail(Array.from(unmatched.values()));
|
|
}
|
|
|
|
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({ method: 'NodeRuntime.enable' });
|
|
await session.waitForNotification('NodeRuntime.waitingForDebugger');
|
|
await session.send(commands);
|
|
await session.send({ method: 'NodeRuntime.disable' });
|
|
await session.waitForBreakOnLine(0, session.scriptURL());
|
|
}
|
|
|
|
async function testBreakpoint(session) {
|
|
console.log('[test]', 'Setting a breakpoint and verifying it is hit');
|
|
const commands = [
|
|
{ 'method': 'Debugger.setBreakpointByUrl',
|
|
'params': { 'lineNumber': 5,
|
|
'url': session.scriptURL(),
|
|
'columnNumber': 0,
|
|
'condition': '' } },
|
|
{ 'method': 'Debugger.resume' },
|
|
];
|
|
await session.send(commands);
|
|
const { scriptSource } = await session.send({
|
|
'method': 'Debugger.getScriptSource',
|
|
'params': { 'scriptId': session.mainScriptId },
|
|
});
|
|
assert(scriptSource && (scriptSource.includes(session.script())),
|
|
`Script source is wrong: ${scriptSource}`);
|
|
|
|
await session.waitForConsoleOutput('log', ['A message', 5]);
|
|
const paused = await session.waitForBreakOnLine(5, session.scriptURL());
|
|
const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId;
|
|
|
|
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 });
|
|
|
|
let { result } = await session.send({
|
|
'method': 'Debugger.evaluateOnCallFrame', 'params': {
|
|
'callFrameId': session.pausedDetails().callFrames[0].callFrameId,
|
|
'expression': 'k + t',
|
|
'objectGroup': 'console',
|
|
'includeCommandLineAPI': true,
|
|
'silent': false,
|
|
'returnByValue': false,
|
|
'generatePreview': true
|
|
}
|
|
});
|
|
const expectedEvaluation = 1002;
|
|
assert.strictEqual(
|
|
result.value,
|
|
expectedEvaluation,
|
|
`Expected evaluation to be ${expectedEvaluation}, got ${result.value}.`
|
|
);
|
|
|
|
result = (await session.send({
|
|
'method': 'Runtime.evaluate', 'params': {
|
|
'expression': '5 * 5'
|
|
}
|
|
})).result;
|
|
const expectedResult = 25;
|
|
assert.strictEqual(
|
|
result.value,
|
|
expectedResult,
|
|
`Expected Runtime.evaluate to be ${expectedResult}, got ${result.value}.`
|
|
);
|
|
}
|
|
|
|
async function testI18NCharacters(session) {
|
|
console.log('[test]', 'Verify sending and receiving UTF8 characters');
|
|
const chars = 'טֶ字и';
|
|
session.send({
|
|
'method': 'Debugger.evaluateOnCallFrame', 'params': {
|
|
'callFrameId': session.pausedDetails().callFrames[0].callFrameId,
|
|
'expression': `console.log("${chars}")`,
|
|
'objectGroup': 'console',
|
|
'includeCommandLineAPI': true,
|
|
'silent': false,
|
|
'returnByValue': false,
|
|
'generatePreview': true
|
|
}
|
|
});
|
|
await session.waitForConsoleOutput('log', [chars]);
|
|
}
|
|
|
|
async function testCommandLineAPI(session) {
|
|
const testModulePath = require.resolve('../fixtures/empty.js');
|
|
const testModuleStr = JSON.stringify(testModulePath);
|
|
const printAModulePath = require.resolve('../fixtures/printA.js');
|
|
const printAModuleStr = JSON.stringify(printAModulePath);
|
|
const printBModulePath = require.resolve('../fixtures/printB.js');
|
|
const printBModuleStr = JSON.stringify(printBModulePath);
|
|
|
|
// 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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
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
|
|
}
|
|
});
|
|
checkException(result);
|
|
assert.deepStrictEqual(JSON.parse(result.result.value), {
|
|
parentsEqual: true,
|
|
parentId: '<inspector console>'
|
|
});
|
|
// The `require` in the module shadows the command line API's `require`
|
|
result = await session.send(
|
|
{
|
|
'method': 'Debugger.evaluateOnCallFrame', 'params': {
|
|
'callFrameId': session.pausedDetails().callFrames[0].callFrameId,
|
|
'expression': `(
|
|
require(${printBModuleStr}),
|
|
require.cache[${printBModuleStr}].parent.id
|
|
)`,
|
|
'includeCommandLineAPI': true
|
|
}
|
|
});
|
|
checkException(result);
|
|
assert.notStrictEqual(result.result.value,
|
|
'<inspector console>');
|
|
}
|
|
|
|
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();
|
|
await testBreakpointOnStart(session);
|
|
await testBreakpoint(session);
|
|
await testI18NCharacters(session);
|
|
await testCommandLineAPI(session);
|
|
await session.runToCompletion();
|
|
const expectedExitCode = 55;
|
|
const { exitCode } = await child.expectShutdown();
|
|
assert.strictEqual(
|
|
exitCode,
|
|
expectedExitCode,
|
|
`Expected exit code to be ${expectedExitCode} but got ${expectedExitCode}.`
|
|
);
|
|
}
|
|
|
|
runTest().then(common.mustCall());
|