Skip to content

Commit

Permalink
WRLGS-11: Test stderrUtils
Browse files Browse the repository at this point in the history
  • Loading branch information
BourgoisMickael committed Jun 26, 2024
1 parent 2ba70ea commit 17da689
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 0 deletions.
17 changes: 17 additions & 0 deletions tests/unit/fixtures/stderrUtils/catchStderr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env node
// Convert string args into primitive value
const fromStr = (str, primitive) => (str === `${primitive}` ? primitive : str);
const date = fromStr(process.argv[2], undefined);
const exitCode = fromStr(fromStr(process.argv[3], null), undefined);

const { stderrUtils } = require('../../../../index');

stderrUtils.catchAndTimestampStderr(
date ? () => date : undefined,
exitCode,
);

process.emitWarning('TestWarningMessage');
// This will print warning after printing error before exit
throw new Error('TestingError');

23 changes: 23 additions & 0 deletions tests/unit/fixtures/stderrUtils/catchUncaughtException.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node
// Convert string args into primitive value
const fromStr = (str, primitive) => (str === `${primitive}` ? primitive : str);
const date = fromStr(process.argv[2], undefined);
const exitCode = fromStr(fromStr(process.argv[3], null), undefined);
const promise = fromStr(process.argv[4], true);

const { stderrUtils } = require('../../../../index');

stderrUtils.catchAndTimestampUncaughtException(
date ? () => date : undefined,
exitCode,
);

// Executed if process does not exit, process is in undefined behavior (bad)
// eslint-disable-next-line no-console
setTimeout(() => console.log('EXECUTED AFTER UNCAUGHT EXCEPTION'), 1);

if (promise === true) {
Promise.reject();
} else {
throw new Error('TestingError');
}
38 changes: 38 additions & 0 deletions tests/unit/fixtures/stderrUtils/catchWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env node
// Convert string args into primitive value
const fromStr = (str, primitive) => (str === `${primitive}` ? primitive : str);
const date = fromStr(process.argv[2], undefined);
const name = fromStr(process.argv[3], undefined);
const code = fromStr(process.argv[4], undefined);
const detail = fromStr(process.argv[5], undefined);

const { stderrUtils } = require('../../../../index');

stderrUtils.catchAndTimestampWarning(
date ? () => date : undefined,
);

const warning = new Error('TestWarningMessage');

if (name) warning.name = name;
if (code) warning.code = code;
if (detail) warning.detail = detail;

process.emitWarning(warning);

/*
Examples:
(node:203831) Error: TestWarningMessage
at Object.<anonymous> (catchWarning.js:15:17)
...
at node:internal/main/run_main_module:22:47
Above Warning Date: 2024-06-26T16:32:55.505Z
(node:205151) [TEST01] CUSTOM: TestWarningMessage
at Object.<anonymous> (catchWarning.js:15:17)
...
at node:internal/main/run_main_module:22:47
Some additional detail
Above Warning Date: Tue, 31 Dec 2024 10:20:30 GMT
*/
273 changes: 273 additions & 0 deletions tests/unit/stderrUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
const assert = require('assert');
const { execFile } = require('child_process');

const stderrUtils = require('../../lib/stderrUtils');

/** Simple regex for ISO YYYY-MM-DDThh:mm:ss.sssZ */
// eslint-disable-next-line max-len
const defaultDateRegex = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)/;

// eslint-disable-next-line valid-jsdoc
/** another format: Tue, 31 Dec 2024 10:20:30 GMT */
const customDate = () => new Date('2024-12-31T10:20:30.444Z').toUTCString();

describe('stderrUtils', () => {
const errStackRegex = /Error: TestingError\n(?:.*\sat\s.*\n)+/;

describe('defaultTimestamp', () => {
it('should match ISO format', () => {
assert.match(stderrUtils.defaultTimestamp(), defaultDateRegex);
});
});

describe('printErrorWithTimestamp', () => {
let stderrText;
const originalStderrWrite = process.stderr.write;
const mockedStderrWrite = text => { stderrText = text; return true; };
const err = new Error('TestingError');
const origin = 'uncaughtException';

beforeEach(() => {
stderrText = undefined;
process.stderr.write = mockedStderrWrite;
});

afterEach(() => {
process.stderr.write = originalStderrWrite;
stderrText = undefined;
});

it(
'should write to stderr with current date, origin and stacktrace',
() => {
const written = stderrUtils
.printErrorWithTimestamp(err, origin);

assert.strictEqual(written, true);
const [firstLine, errStack] = stderrText.split(':\n');
const [errDate, errOrigin] = firstLine.split(': ');

assert.match(errDate, defaultDateRegex);
assert.strictEqual(errOrigin, origin);
assert.strictEqual(errStack, `${err.stack}\n`);
},
);

it(
'should write to stderr with custom date, origin and stacktrace',
() => {
const written = stderrUtils
.printErrorWithTimestamp(err, origin, customDate());

assert.strictEqual(written, true);
const [firstLine, errStack] = stderrText.split(':\n');
const [errDate, errOrigin] = firstLine.split(': ');

assert.strictEqual(errDate, customDate());
assert.strictEqual(errOrigin, origin);
assert.strictEqual(errStack, `${err.stack}\n`);
},
);
});

const execOptions = {
cwd: __dirname,
timeout: 100, // stop subprocess after 100ms
};

// Execute in another process to notice the process exit
// Therefore, looks more like a functional test

describe('catchAndTimestampUncaughtException', () => {
[
{ desc: 'with default date' },
{ desc: 'with custom date', date: customDate() },
{ desc: 'with custom exitCode 42', exitCode: 42 },
{ desc: 'without exit on uncaught exception', exitCode: null },
{ desc: 'for unhandled promise', promise: true },
].forEach(({
desc, date, exitCode, promise,
}) => describe(desc, () => {
let err;
let stdout;
let stderr;
let errStack;
let errDate;
let errOrigin;

before('run process catchUncaughtException', done => {
execFile(
'./fixtures/stderrUtils/catchUncaughtException.js',
[`${date}`, `${exitCode}`, `${promise}`],
execOptions,
(subErr, subStdout, subStderr) => {
err = subErr;
stdout = subStdout;
stderr = subStderr;
let firstLine;
[firstLine, errStack] = stderr.split(':\n');
[errDate, errOrigin] = firstLine.split(': ');
done();
},
);
});

if (exitCode === null) {
it('should not be an error (or timeout)',
() => assert.ifError(err));
it('should have stdout (printed after uncaught exception)',
() => assert.match(stdout,
/^.*EXECUTED AFTER UNCAUGHT EXCEPTION(?:.|\n)*$/));
} else {
it('should be an error',
() => assert.ok(err));
it(`should have exitCode ${exitCode || 1}`,
() => assert.strictEqual(err.code, exitCode || 1));
it('should have empty stdout',
() => assert.strictEqual(stdout, ''));
}

it('should have stderr',
() => assert.ok(stderr));
it('should have date in stderr first line',
() => (date
? assert.strictEqual(errDate, date)
: assert.match(errDate, defaultDateRegex)));

it('should have origin in stderr first line',
() => (promise === true
? assert.strictEqual(errOrigin, 'unhandledRejection')
: assert.strictEqual(errOrigin, 'uncaughtException')));

if (!promise) {
it('should have stack trace on stderr',
() => assert.match(errStack, errStackRegex));
}
}));
});

describe('catchAndTimestampWarning (also tests node onWarning)', () => {
[
{ desc: 'with default date' },
{ desc: 'with custom date', date: customDate() },
{ desc: 'with deprecation warning', name: 'DeprecationWarning' },
{
desc: 'with custom warning',
name: 'CUSTOM',
code: 'TEST01',
detail: 'Some additional detail',
},
].forEach(({
desc, date, name, code, detail,
}) => describe(desc, () => {
let err;
let stdout;
let stderr;

before('run process catchWarning', done => {
execFile(
'./fixtures/stderrUtils/catchWarning.js',
[`${date}`, `${name}`, `${code}`, `${detail}`],
execOptions,
(subErr, subStdout, subStderr) => {
err = subErr;
stdout = subStdout;
stderr = subStderr;
done();
},
);
});

it('should not be an error (or timeout)',
() => assert.ifError(err));
it('should have empty stdout',
() => assert.strictEqual(stdout, ''));
it('should have stderr',
() => assert.ok(stderr));
it('should have message on stderr first line, then stack trace',
() => assert.match(stderr,
/^.*TestWarningMessage\n(?:\s+at\s.*\n)+/));

if (code) {
it('should have code on stderr first line',
() => assert.match(stderr, new RegExp(`^.*[${code}]`)));
}

if (name) {
it('should have name on stderr first line',
() => assert.match(stderr, new RegExp(`^.*${name}:`)));
}

if (detail) {
it('should have detail on stderr',
() => assert.match(stderr, new RegExp(`.*${detail}.*`)));
}

it(`should have ${date ? 'custom' : 'default'} date on stderr`,
() => assert.match(stderr, new RegExp(
`\nAbove Warning Date: ${
date || defaultDateRegex.source}\n`,
)));
}));
});

describe('catchAndTimestampStderr', () => {
[
{ desc: 'with default date' },
{ desc: 'with custom date', date: customDate() },
{ desc: 'with exit code', exitCode: 42 },

].forEach(({
desc, date, exitCode,
}) => describe(desc, () => {
let err;
let stdout;
let stderr;

before('run process catchStderr', done => {
execFile(
'./fixtures/stderrUtils/catchStderr.js',
[`${date}`, `${exitCode}`],
execOptions,
(subErr, subStdout, subStderr) => {
err = subErr;
stdout = subStdout;
stderr = subStderr;
done();
},
);
});

it('should be an error',
() => assert.ok(err));
it(`should have exitCode ${exitCode || 1}`,
() => assert.strictEqual(err.code, exitCode || 1));
it('should have empty stdout',
() => assert.strictEqual(stdout, ''));

it('should have stderr',
() => assert.ok(stderr));

// 2024-06-26T15:04:55.364Z: uncaughtException:
// Error: TestingError
// at Object.<anonymous> (catchStderr.js:16:7)
// at node:internal/main/run_main_module:22:47
it('should have error date, origin and stacktrace in stderr',
() => assert.match(stderr,
new RegExp(`${date || defaultDateRegex.source
}: uncaughtException:\n${errStackRegex.source}`)));

// (node:171245) Warning: TestWarningMessage
// at Object.<anonymous> (catchStderr.js:14:9)
// at node:internal/main/run_main_module:22:47
// Above Warning Date: 2024-06-26T15:04:55.365Z
it('should have warning with stacktrace in stderr', () => {
const trace = 'Warning: TestWarningMessage\n(?:\\s+at\\s.*\n)+';
const detail = `(?:.|\n)*?(?<=\n)Above Warning Date: ${
date || defaultDateRegex.source}\n`;
assert.match(stderr,
new RegExp(`${trace}${detail}`));
});
}));
});
});

0 comments on commit 17da689

Please sign in to comment.