Skip to content

Commit

Permalink
test_runner: stringify AssertError expected and actual
Browse files Browse the repository at this point in the history
PR-URL: #47088
Fixes: #47075
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
  • Loading branch information
MoLow authored Apr 4, 2023
1 parent 0361978 commit 102540a
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 29 deletions.
30 changes: 21 additions & 9 deletions lib/internal/test_runner/reporter/tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
ObjectEntries,
RegExpPrototypeSymbolReplace,
SafeMap,
SafeSet,
StringPrototypeReplaceAll,
StringPrototypeSplit,
StringPrototypeRepeat,
Expand Down Expand Up @@ -79,7 +80,7 @@ function reportDetails(nesting, data = kEmptyObject) {

details += jsToYaml(_indent, 'duration_ms', duration_ms);
details += jsToYaml(_indent, 'type', data.type);
details += jsToYaml(_indent, null, error);
details += jsToYaml(_indent, null, error, new SafeSet());
details += `${_indent} ...\n`;
return details;
}
Expand Down Expand Up @@ -109,7 +110,7 @@ function tapEscape(input) {
return result;
}

function jsToYaml(indent, name, value) {
function jsToYaml(indent, name, value, seen) {
if (value === null || value === undefined) {
return '';
}
Expand All @@ -136,18 +137,29 @@ function jsToYaml(indent, name, value) {
return str;
}

seen.add(value);
const entries = ObjectEntries(value);
const isErrorObj = isError(value);
let result = '';
let propsIndent = indent;

if (name != null) {
result += `${indent} ${name}:\n`;
propsIndent += ' ';
}

for (let i = 0; i < entries.length; i++) {
const { 0: key, 1: value } = entries[i];

if (isErrorObj && (key === 'cause' || key === 'code')) {
continue;
}
if (seen.has(value)) {
result += `${propsIndent} ${key}: <Circular>\n`;
continue;
}

result += jsToYaml(indent, key, value);
result += jsToYaml(propsIndent, key, value, seen);
}

if (isErrorObj) {
Expand Down Expand Up @@ -189,20 +201,20 @@ function jsToYaml(indent, name, value) {
}
}

result += jsToYaml(indent, 'error', errMsg);
result += jsToYaml(indent, 'error', errMsg, seen);

if (errCode) {
result += jsToYaml(indent, 'code', errCode);
result += jsToYaml(indent, 'code', errCode, seen);
}
if (errName && errName !== 'Error') {
result += jsToYaml(indent, 'name', errName);
result += jsToYaml(indent, 'name', errName, seen);
}

if (errIsAssertion) {
result += jsToYaml(indent, 'expected', errExpected);
result += jsToYaml(indent, 'actual', errActual);
result += jsToYaml(indent, 'expected', errExpected, seen);
result += jsToYaml(indent, 'actual', errActual, seen);
if (errOperator) {
result += jsToYaml(indent, 'operator', errOperator);
result += jsToYaml(indent, 'operator', errOperator, seen);
}
}

Expand Down
27 changes: 19 additions & 8 deletions lib/internal/test_runner/yaml_to_js.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const {
StringPrototypeSubstring,
} = primordials;

const kYamlKeyRegex = /^(\s+)?(\w+):(\s)+([>|][-+])?(.*)$/;
const kYamlKeyRegex = /^(\s+)?(\w+):(\s)*([>|][-+])?(.*)$/;
const kStackDelimiter = ' at ';

function reConstructError(parsedYaml) {
Expand Down Expand Up @@ -91,28 +91,39 @@ function YAMLToJs(lines) {
return undefined;
}
const result = { __proto__: null };
let context = { __proto__: null, object: result, indent: 0, currentKey: null };
let isInYamlBlock = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (isInYamlBlock && !StringPrototypeStartsWith(line, StringPrototypeRepeat(' ', isInYamlBlock.indent))) {
result[isInYamlBlock.key] = isInYamlBlock.key === 'stack' ?
result[isInYamlBlock.key] : ArrayPrototypeJoin(result[isInYamlBlock.key], '\n');
context.object[isInYamlBlock.key] = isInYamlBlock.key === 'stack' ?
context.object[isInYamlBlock.key] : ArrayPrototypeJoin(context.object[isInYamlBlock.key], '\n');
isInYamlBlock = false;
}
if (isInYamlBlock) {
const blockLine = StringPrototypeSubstring(line, isInYamlBlock.indent);
ArrayPrototypePush(result[isInYamlBlock.key], blockLine);
ArrayPrototypePush(context.object[isInYamlBlock.key], blockLine);
continue;
}
const match = RegExpPrototypeExec(kYamlKeyRegex, line);
if (match !== null) {
const { 1: leadingSpaces, 2: key, 4: block, 5: value } = match;
const indent = leadingSpaces?.length ?? 0;
if (block) {
isInYamlBlock = { key, indent: (leadingSpaces?.length ?? 0) + 2 };
result[key] = [];
} else {
result[key] = getYamlValue(value);
isInYamlBlock = { key, indent: indent + 2 };
context.object[key] = [];
continue;
}

if (indent > context.indent) {
context.object[context.currentKey] ||= {};
context = { __proto__: null, parent: context, object: context.object[context.currentKey], indent };
} else if (indent < context.indent) {
context = context.parent;
}

context.currentKey = key;
context.object[key] = getYamlValue(value);
}
}
return reConstructError(result);
Expand Down
14 changes: 14 additions & 0 deletions test/message/test_runner_output.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,17 @@ test('unfinished test with unhandledRejection', async () => {
setImmediate(() => {
throw new Error('uncaught from outside of a test');
});

test('assertion errors display actual and expected properly', async () => {
// Make sure the assert module is handled.
const circular = { bar: 2 };
circular.c = circular;
const tmpLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 1;
try {
assert.deepEqual({ foo: 1, bar: 1 }, circular); // eslint-disable-line no-restricted-properties
} catch (err) {
Error.stackTraceLimit = tmpLimit;
throw err;
}
});
39 changes: 35 additions & 4 deletions test/message/test_runner_output.out
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,39 @@ not ok 64 - unfinished test with unhandledRejection
*
*
...
# Subtest: assertion errors display actual and expected properly
not ok 65 - assertion errors display actual and expected properly
---
duration_ms: *
failureType: 'testCodeFailure'
error: |-
Expected values to be loosely deep-equal:

{
bar: 1,
foo: 1
}

should loosely deep-equal

<ref *1> {
bar: 2,
c: [Circular *1]
}
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
bar: 2
c: <Circular>
actual:
foo: 1
bar: 1
operator: 'deepEqual'
stack: |-
*
...
# Subtest: invalid subtest fail
not ok 65 - invalid subtest fail
not ok 66 - invalid subtest fail
---
duration_ms: *
failureType: 'parentAlreadyFinished'
Expand All @@ -634,18 +665,18 @@ not ok 65 - invalid subtest fail
stack: |-
*
...
1..65
1..66
# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.
# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
# tests 79
# tests 80
# suites 0
# pass 37
# fail 24
# fail 25
# cancelled 3
# skipped 10
# todo 5
Expand Down
39 changes: 35 additions & 4 deletions test/message/test_runner_output_cli.out
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,39 @@ not ok 64 - unfinished test with unhandledRejection
*
*
...
# Subtest: assertion errors display actual and expected properly
not ok 65 - assertion errors display actual and expected properly
---
duration_ms: *
failureType: 'testCodeFailure'
error: |-
Expected values to be loosely deep-equal:

{
bar: 1,
foo: 1
}

should loosely deep-equal

<ref *1> {
bar: 2,
c: [Circular *1]
}
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
bar: 2
c: '<Circular>'
actual:
foo: 1
bar: 1
operator: 'deepEqual'
stack: |-
*
...
# Subtest: invalid subtest fail
not ok 65 - invalid subtest fail
not ok 66 - invalid subtest fail
---
duration_ms: *
failureType: 'parentAlreadyFinished'
Expand All @@ -641,11 +672,11 @@ not ok 65 - invalid subtest fail
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
1..65
# tests 79
1..66
# tests 80
# suites 0
# pass 37
# fail 24
# fail 25
# cancelled 3
# skipped 10
# todo 5
Expand Down
3 changes: 2 additions & 1 deletion test/message/test_runner_output_dot_reporter.out
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
..XX...X..XXX.X.....
XXX.....X..X...X....
.........X...XXX.XX.
.....XXXXXXX...XXXX
.....XXXXXXX...XXXXX

50 changes: 47 additions & 3 deletions test/message/test_runner_output_spec_reporter.out
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
callback called twice in different ticks (*ms)
callback called twice in future tick (*ms)
Error [ERR_TEST_FAILURE]: callback invoked multiple times
*
* {
failureType: 'multipleCallbackInvocations',
cause: 'callback invoked multiple times',
code: 'ERR_TEST_FAILURE'
Expand Down Expand Up @@ -265,6 +265,28 @@
*
*

assertion errors display actual and expected properly (*ms)
AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal:

{
bar: 1,
foo: 1
}

should loosely deep-equal

<ref *1> {
bar: 2,
c: [Circular *1]
}
* {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [Object],
expected: [Object],
operator: 'deepEqual'
}

invalid subtest fail (*ms)
'test could not be started because its parent finished'

Expand All @@ -275,10 +297,10 @@
Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
tests 79
tests 80
suites 0
pass 37
fail 24
fail 25
cancelled 3
skipped 10
todo 5
Expand Down Expand Up @@ -490,5 +512,27 @@
*
*

assertion errors display actual and expected properly (*ms)
AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal:

{
bar: 1,
foo: 1
}

should loosely deep-equal

<ref *1> {
bar: 2,
c: [Circular *1]
}
* {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [Object],
expected: [Object],
operator: 'deepEqual'
}

invalid subtest fail (*ms)
'test could not be started because its parent finished'

0 comments on commit 102540a

Please sign in to comment.