Skip to content

Commit

Permalink
test: add node-report tests
Browse files Browse the repository at this point in the history
One test per each API, so that additional tests in future are modular.
test/common/report.js contain common functions  that tests leverage.

PR-URL: #22712
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michael Dawson <Michael_Dawson@ca.ibm.com>
Reviewed-By: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
  • Loading branch information
LakshmiSwethaG authored and targos committed Jan 24, 2019
1 parent 4f38106 commit 0800f91
Show file tree
Hide file tree
Showing 13 changed files with 484 additions and 1 deletion.
6 changes: 6 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2227,6 +2227,12 @@ size.
This `Error` is thrown when a read is attempted on a TTY `WriteStream`,
such as `process.stdout.on('data')`.

<a id="ERR_SYNTHETIC"></a>
#### ERR_SYNTHETIC

An artifical error object used to capture call stack when diagnostic report
is produced.


[`'uncaughtException'`]: process.html#process_event_uncaughtexception
[`--force-fips`]: cli.html#cli_force_fips
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,6 @@ E('ERR_INSPECTOR_CLOSED', 'Session was closed', Error);
E('ERR_INSPECTOR_NOT_AVAILABLE', 'Inspector is not available', Error);
E('ERR_INSPECTOR_NOT_CONNECTED', 'Session is not connected', Error);
E('ERR_INVALID_ADDRESS_FAMILY', 'Invalid address family: %s', RangeError);
E('ERR_SYNTHETIC', 'JavaScript Callstack: %s', Error);
E('ERR_INVALID_ARG_TYPE',
(name, expected, actual) => {
assert(typeof name === 'string', "'name' must be a string");
Expand Down Expand Up @@ -924,6 +923,7 @@ E('ERR_STREAM_UNSHIFT_AFTER_END_EVENT',
'stream.unshift() after end event', Error);
E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error);
E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error);
E('ERR_SYNTHETIC', 'JavaScript Callstack: %s', Error);
E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError);
E('ERR_TLS_CERT_ALTNAME_INVALID',
'Hostname/IP does not match certificate\'s altnames: %s', Error);
Expand Down
7 changes: 7 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,12 @@ function skipIfInspectorDisabled() {
}
}

function skipIfReportDisabled() {
if (!process.config.variables.node_report) {
skip('Node Report is disabled');
}
}

function skipIf32Bits() {
if (bits < 64) {
skip('The tested feature is not available in 32bit builds');
Expand Down Expand Up @@ -762,6 +768,7 @@ module.exports = {
skipIf32Bits,
skipIfEslintMissing,
skipIfInspectorDisabled,
skipIfReportDisabled,
skipIfWorker,

get localhostIPv6() { return '::1'; },
Expand Down
43 changes: 43 additions & 0 deletions test/common/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';
require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');

const REPORT_SECTIONS = [ 'header',
'javascriptStack',
'nativeStack',
'javascriptHeap',
'libuv',
'environmentVariables',
'sharedObjects' ];

let tmppath = '';

exports.findReports = (pid, path) => {
// Default filenames are of the form
// report.<date>.<time>.<pid>.<seq>.json
tmppath = path;
const format = '^report\\.\\d+\\.\\d+\\.' + pid + '\\.\\d+\\.json$';
const filePattern = new RegExp(format);
const files = fs.readdirSync(path);
return files.filter((file) => filePattern.test(file));
};

exports.validate = (report, options) => {
const jtmp = path.join(tmppath, report);
fs.readFile(jtmp, (err, data) => {
this.validateContent(data, options);
});
};


exports.validateContent = function validateContent(data, options) {
const report = JSON.parse(data);
const comp = Object.keys(report);

// Check all sections are present
REPORT_SECTIONS.forEach((section) => {
assert.ok(comp.includes(section));
});
};
28 changes: 28 additions & 0 deletions test/node-report/test-api-getreport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

// Testcase for returning report as a string via API call
const common = require('../common');
common.skipIfReportDisabled();
const assert = require('assert');
if (process.argv[2] === 'child') {
console.log(process.report.getReport());
} else {
const helper = require('../common/report.js');
const spawnSync = require('child_process').spawnSync;
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

const args = ['--experimental-report', __filename, 'child'];
const child = spawnSync(process.execPath, args, { cwd: tmpdir.path });
const report_msg = 'Found report files';
const std_msg = 'Found messages on stderr';
assert.ok(child.stderr.toString().includes(
`(node:${child.pid}) ExperimentalWarning: report is an` +
' experimental feature. This feature could change at any time'), std_msg);
const reportFiles = helper.findReports(child.pid, tmpdir.path);
assert.deepStrictEqual(reportFiles, [], report_msg);
helper.validateContent(child.stdout, { pid: child.pid,
commandline: process.execPath +
' ' + args.join(' ')
});
}
30 changes: 30 additions & 0 deletions test/node-report/test-api-nohooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

// Testcase to produce report via API call, using the no-hooks/no-signal
// interface - i.e. require('node-report/api')
const common = require('../common');
common.skipIfReportDisabled();
if (process.argv[2] === 'child') {
process.report.triggerReport();
} else {
const helper = require('../common/report.js');
const spawn = require('child_process').spawn;
const assert = require('assert');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

const child = spawn(process.execPath, ['--experimental-report',
__filename, 'child'],
{ cwd: tmpdir.path });
child.on('exit', common.mustCall((code) => {
const report_msg = 'No reports found';
const process_msg = 'Process exited unexpectedly';
assert.strictEqual(code, 0, process_msg + ':' + code);
const reports = helper.findReports(child.pid, tmpdir.path);
assert.strictEqual(reports.length, 1, report_msg);
const report = reports[0];
helper.validate(report, { pid: child.pid,
commandline: child.spawnargs.join(' ')
});
}));
}
33 changes: 33 additions & 0 deletions test/node-report/test-api-pass-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

// Testcase for passing an error object to the API call.
const common = require('../common');
common.skipIfReportDisabled();
const assert = require('assert');
if (process.argv[2] === 'child') {
try {
throw new Error('Testing error handling');
} catch {
process.report.triggerReport();
}
} else {
const helper = require('../common/report.js');
const spawn = require('child_process').spawn;
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const child = spawn(process.execPath, ['--experimental-report',
__filename, 'child'],
{ cwd: tmpdir.path });
child.on('exit', common.mustCall((code) => {
const report_msg = 'No reports found';
const process_msg = 'Process exited unexpectedly';
assert.strictEqual(code, 0, process_msg);
const reports = helper.findReports(child.pid, tmpdir.path);
assert.strictEqual(reports.length, 1, report_msg);
const report = reports[0];
helper.validate(report, { pid: child.pid,
commandline: child.spawnargs.join(' '),
expectedException: 'Testing error handling',
});
}));
}
133 changes: 133 additions & 0 deletions test/node-report/test-api-uvhandles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use strict';

// Testcase to check reporting of uv handles.
const common = require('../common');
common.skipIfReportDisabled();
if (process.argv[2] === 'child') {
// Exit on loss of parent process
const exit = () => process.exit(2);
process.on('disconnect', exit);

const fs = require('fs');
const http = require('http');
const spawn = require('child_process').spawn;

// Watching files should result in fs_event/fs_poll uv handles.
let watcher;
try {
watcher = fs.watch(__filename);
} catch {
// fs.watch() unavailable
}
fs.watchFile(__filename, () => {});

// Child should exist when this returns as child_process.pid must be set.
const child_process = spawn(process.execPath,
['-e', "process.stdin.on('data', (x) => " +
'console.log(x.toString()));']);

const timeout = setInterval(() => {}, 1000);
// Make sure the timer doesn't keep the test alive and let
// us check we detect unref'd handles correctly.
timeout.unref();

// Datagram socket for udp uv handles.
const dgram = require('dgram');
const udp_socket = dgram.createSocket('udp4');
udp_socket.bind({});

// Simple server/connection to create tcp uv handles.
const server = http.createServer((req, res) => {
req.on('end', () => {
// Generate the report while the connection is active.
console.log(process.report.getReport());
child_process.kill();

res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end();

// Tidy up to allow process to exit cleanly.
server.close(() => {
if (watcher) watcher.close();
fs.unwatchFile(__filename);
udp_socket.close();
process.removeListener('disconnect', exit);
});
});
req.resume();
});
server.listen(() => {
const data = { pid: child_process.pid,
tcp_address: server.address(),
udp_address: udp_socket.address(),
skip_fs_watch: (watcher === undefined ?
'fs.watch() unavailable' :
false) };
process.send(data);
http.get({ port: server.address().port });
});
} else {
const helper = require('../common/report.js');
const fork = require('child_process').fork;
const assert = require('assert');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const options = { encoding: 'utf8', silent: true, cwd: tmpdir.path };
const child = fork('--experimental-report', [__filename, 'child'], options);
let stderr = '';
child.stderr.on('data', (chunk) => { stderr += chunk; });
let stdout = '';
const std_msg = 'Found messages in stderr unexpectedly: ';
const report_msg = 'Report files were written: unexpectedly';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.on('exit', common.mustCall((code, signal) => {
assert.deepStrictEqual(code, 0, 'Process exited unexpectedly with code' +
`${code}`);
assert.deepStrictEqual(signal, null, 'Process should have exited cleanly,' +
` but did not: ${signal}`);
assert.ok(stderr.match(
'(node:.*) ExperimentalWarning: report is an experimental' +
' feature. This feature could change at any time'),
std_msg);

const reports = helper.findReports(child.pid, tmpdir.path);
assert.deepStrictEqual(reports, [], report_msg, reports);

const report = JSON.parse(stdout);
let fs = 0;
let poll = 0;
let process = 0;
let timer = 0;
let pipe = 0;
let tcp = 0;
let udp = 0;
const fs_msg = 'fs_event not found';
const poll_msg = 'poll_event not found';
const process_msg = 'process event not found';
const timer_msg = 'timer event not found';
const pipe_msg = 'pipe event not found';
const tcp_msg = 'tcp event not found';
const udp_msg = 'udp event not found';
for (const entry in report.libuv) {
if (report.libuv[entry].type === 'fs_event') fs = 1;
else if (report.libuv[entry].type === 'fs_poll') poll = 1;
else if (report.libuv[entry].type === 'process') process = 1;
else if (report.libuv[entry].type === 'timer') timer = 1;
else if (report.libuv[entry].type === 'pipe') pipe = 1;
else if (report.libuv[entry].type === 'tcp') tcp = 1;
else if (report.libuv[entry].type === 'udp') udp = 1;
}
assert.deepStrictEqual(fs, 1, fs_msg);
assert.deepStrictEqual(poll, 1, poll_msg);
assert.deepStrictEqual(process, 1, process_msg);
assert.deepStrictEqual(timer, 1, timer_msg);
assert.deepStrictEqual(pipe, 1, pipe_msg);
assert.deepStrictEqual(tcp, 1, tcp_msg);
assert.deepStrictEqual(udp, 1, udp_msg);

// Common report tests.
helper.validateContent(stdout, { pid: child.pid,
commandline: child.spawnargs.join(' ')
});
}));
}
29 changes: 29 additions & 0 deletions test/node-report/test-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

// Testcase to produce report via API call
const common = require('../common');
common.skipIfReportDisabled();
if (process.argv[2] === 'child') {
process.report.triggerReport();
} else {
const helper = require('../common/report.js');
const spawn = require('child_process').spawn;
const assert = require('assert');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

const child = spawn(process.execPath,
['--experimental-report', __filename, 'child'],
{ cwd: tmpdir.path });
child.on('exit', common.mustCall((code) => {
const report_msg = 'No reports found';
const process_msg = 'Process exited unexpectedly';
assert.strictEqual(code, 0, process_msg + ':' + code);
const reports = helper.findReports(child.pid, tmpdir.path);
assert.strictEqual(reports.length, 1, report_msg);
const report = reports[0];
helper.validate(report, { pid: child.pid,
commandline: child.spawnargs.join(' ')
});
}));
}
44 changes: 44 additions & 0 deletions test/node-report/test-exception.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

// Testcase to produce report on uncaught exception
const common = require('../common');
common.skipIfReportDisabled();
if (process.argv[2] === 'child') {
function myException(request, response) {
const m = '*** test-exception.js: throwing uncaught Error';
throw new Error(m);
}

myException();

} else {
const helper = require('../common/report.js');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const spawn = require('child_process').spawn;
const assert = require('assert');

const child = spawn(process.execPath,
['--experimental-report',
'--diagnostic-report-uncaught-exception',
__filename, 'child'],
{ cwd: tmpdir.path });
// Capture stderr output from the child process
let stderr = '';
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
child.on('exit', common.mustCall((code) => {
const report_msg = 'No reports found';
const process_msg = 'Process exited unexpectedly';
assert.strictEqual(code, 1, process_msg + ':' + code);
assert.ok(new RegExp('myException').test(stderr),
'Check for expected stack trace frame in stderr');
const reports = helper.findReports(child.pid, tmpdir.path);
assert.strictEqual(reports.length, 1, report_msg);
const report = reports[0];
helper.validate(report, { pid: child.pid,
commandline: child.spawnargs.join(' ')
});
}));
}
Loading

0 comments on commit 0800f91

Please sign in to comment.