Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_runner: stringify AssertError expected and actual #47088

Merged
merged 1 commit into from Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 21 additions & 9 deletions lib/internal/test_runner/reporter/tap.js
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
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] ||= {};
cjihrig marked this conversation as resolved.
Show resolved Hide resolved
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
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
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
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
@@ -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
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'