-
Notifications
You must be signed in to change notification settings - Fork 30k
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: refactor spec reporter to generator function #50177
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,8 @@ | |
SafeMap, | ||
StringPrototypeRepeat, | ||
} = primordials; | ||
|
||
const assert = require('assert'); | ||
const Transform = require('internal/streams/transform'); | ||
const { inspectWithNoCustomRetry } = require('internal/errors'); | ||
const { green, blue, red, white, gray, shouldColorize } = require('internal/util/colors'); | ||
const { kSubtestsFailed } = require('internal/test_runner/test'); | ||
|
@@ -27,6 +27,7 @@ | |
'test:pass': green, | ||
'test:diagnostic': blue, | ||
}; | ||
|
||
const symbols = { | ||
'__proto__': null, | ||
'test:fail': '\u2716 ', | ||
|
@@ -36,27 +37,25 @@ | |
'arrow:right': '\u25B6 ', | ||
'hyphen:minus': '\uFE63 ', | ||
}; | ||
class SpecReporter extends Transform { | ||
#stack = []; | ||
#reported = []; | ||
#indentMemo = new SafeMap(); | ||
#failedTests = []; | ||
#cwd = process.cwd(); | ||
|
||
constructor() { | ||
super({ __proto__: null, writableObjectMode: true }); | ||
} | ||
|
||
#indent(nesting) { | ||
let value = this.#indentMemo.get(nesting); | ||
module.exports = async function *specReporter(source) { | ||
let stack = []; | ||
let reported = []; | ||
let indentMemo = new SafeMap(); | ||
let failedTests = []; | ||
let cwd = process.cwd(); | ||
|
||
function indent(nesting) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this means all of these functions are defined anew every time this reporter is called, instead of just once at module startup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these functions require closure variables There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then if you're switching to a non-class-based model, the functions should take everything as arguments, so they can be defined at module level. |
||
let value = indentMemo.get(nesting); | ||
if (value === undefined) { | ||
value = StringPrototypeRepeat(' ', nesting); | ||
this.#indentMemo.set(nesting, value); | ||
indentMemo.set(nesting, value); | ||
} | ||
|
||
return value; | ||
} | ||
#formatError(error, indent) { | ||
|
||
function formatError(error, indent) { | ||
if (!error) return ''; | ||
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error; | ||
const message = ArrayPrototypeJoin( | ||
|
@@ -66,7 +65,8 @@ | |
), `\n${indent} `); | ||
return `\n${indent} ${message}\n`; | ||
} | ||
#formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) { | ||
|
||
function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) { | ||
let color = colors[type] ?? white; | ||
let symbol = symbols[type] ?? ' '; | ||
const { skip, todo } = data; | ||
|
@@ -82,77 +82,74 @@ | |
// If this test has had children - it was already reported, so slightly modify the output | ||
return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n`; | ||
} | ||
const error = this.#formatError(data.details?.error, indent); | ||
const error = formatError(data.details?.error, indent); | ||
if (skip !== undefined) { | ||
color = gray; | ||
symbol = symbols['hyphen:minus']; | ||
} | ||
return `${prefix}${indent}${color}${symbol}${title}${white}${error}`; | ||
} | ||
#handleTestReportEvent(type, data) { | ||
const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event | ||
|
||
function handleTestReportEvent(type, data) { | ||
const subtest = ArrayPrototypeShift(stack); // This is the matching `test:start` event | ||
if (subtest) { | ||
assert(subtest.type === 'test:start'); | ||
assert(subtest.data.nesting === data.nesting); | ||
assert(subtest.data.name === data.name); | ||
} | ||
let prefix = ''; | ||
while (this.#stack.length) { | ||
while (stack.length) { | ||
// Report all the parent `test:start` events | ||
const parent = ArrayPrototypePop(this.#stack); | ||
const parent = ArrayPrototypePop(stack); | ||
assert(parent.type === 'test:start'); | ||
const msg = parent.data; | ||
ArrayPrototypeUnshift(this.#reported, msg); | ||
prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`; | ||
ArrayPrototypeUnshift(reported, msg); | ||
prefix += `${indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`; | ||
} | ||
let hasChildren = false; | ||
if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) { | ||
ArrayPrototypeShift(this.#reported); | ||
if (reported[0] && reported[0].nesting === data.nesting && reported[0].name === data.name) { | ||
ArrayPrototypeShift(reported); | ||
hasChildren = true; | ||
} | ||
const indent = this.#indent(data.nesting); | ||
return `${this.#formatTestReport(type, data, prefix, indent, hasChildren)}\n`; | ||
return `${formatTestReport(type, data, prefix, indent(data.nesting), hasChildren)}\n`; | ||
} | ||
#handleEvent({ type, data }) { | ||
|
||
function handleEvent({ type, data }) { | ||
switch (type) { | ||
case 'test:fail': | ||
if (data.details?.error?.failureType !== kSubtestsFailed) { | ||
ArrayPrototypePush(this.#failedTests, data); | ||
ArrayPrototypePush(failedTests, data); | ||
} | ||
return this.#handleTestReportEvent(type, data); | ||
return handleTestReportEvent(type, data); | ||
case 'test:pass': | ||
return this.#handleTestReportEvent(type, data); | ||
return handleTestReportEvent(type, data); | ||
case 'test:start': | ||
ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type }); | ||
ArrayPrototypeUnshift(stack, { __proto__: null, data, type }); | ||
break; | ||
case 'test:stderr': | ||
case 'test:stdout': | ||
return data.message; | ||
case 'test:diagnostic': | ||
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`; | ||
return `${colors[type]}${indent(data.nesting)}${symbols[type]}${data.message}${white}\n`; | ||
case 'test:coverage': | ||
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue, true); | ||
return getCoverageReport(indent(data.nesting), data.summary, symbols['test:coverage'], blue, true); | ||
} | ||
} | ||
_transform({ type, data }, encoding, callback) { | ||
callback(null, this.#handleEvent({ __proto__: null, type, data })); | ||
|
||
for await (const event of source) { | ||
yield handleEvent(event); | ||
} | ||
_flush(callback) { | ||
if (this.#failedTests.length === 0) { | ||
callback(null, ''); | ||
return; | ||
} | ||
|
||
if (failedTests.length !== 0) { | ||
const results = [`\n${colors['test:fail']}${symbols['test:fail']}failing tests:${white}\n`]; | ||
for (let i = 0; i < this.#failedTests.length; i++) { | ||
const test = this.#failedTests[i]; | ||
const relPath = relative(this.#cwd, test.file); | ||
const formattedErr = this.#formatTestReport('test:fail', test); | ||
for (let i = 0; i < failedTests.length; i++) { | ||
const test = failedTests[i]; | ||
const relPath = relative(cwd, test.file); | ||
const formattedErr = formatTestReport('test:fail', test); | ||
const location = `test at ${relPath}:${test.line}:${test.column}`; | ||
|
||
ArrayPrototypePush(results, location, formattedErr); | ||
} | ||
callback(null, ArrayPrototypeJoin(results, '\n')); | ||
yield ArrayPrototypeJoin(results, '\n'); | ||
} | ||
} | ||
|
||
module.exports = SpecReporter; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would still need to be a transform stream, API wise (can be from a generator)