chore(testrunner): decouple UserCallback from location and timeout (#1557)

This will make it easier to change lifetimes of Test and Suite.
This commit is contained in:
Dmitry Gozman 2020-03-26 14:43:28 -07:00 committed by GitHub
parent 5d03be7ab1
commit aad82e00bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -33,38 +33,18 @@ const TerminatedError = new Error('Terminated');
const MAJOR_NODEJS_VERSION = parseInt(process.version.substring(1).split('.')[0], 10); const MAJOR_NODEJS_VERSION = parseInt(process.version.substring(1).split('.')[0], 10);
class UserCallback { function runUserCallback(callback, timeout, args) {
constructor(callback, timeout) { let terminateCallback;
this._callback = callback; let timeoutId;
this._terminatePromise = new Promise(resolve => { const promise = Promise.race([
this._terminateCallback = resolve; Promise.resolve().then(callback.bind(null, ...args)).then(() => null).catch(e => e),
}); new Promise(resolve => {
timeoutId = setTimeout(resolve.bind(null, TimeoutError), timeout);
this.timeout = timeout; }),
this.location = getCallerLocation(__filename); new Promise(resolve => terminateCallback = resolve),
} ]).catch(e => e).finally(() => clearTimeout(timeoutId));
const terminate = () => terminateCallback(TerminatedError);
async run(...args) { return { promise, terminate };
let timeoutId;
const timeoutPromise = new Promise(resolve => {
timeoutId = setTimeout(resolve.bind(null, TimeoutError), this.timeout);
});
try {
return await Promise.race([
Promise.resolve().then(this._callback.bind(null, ...args)).then(() => null).catch(e => e),
timeoutPromise,
this._terminatePromise
]);
} catch (e) {
return e;
} finally {
clearTimeout(timeoutId);
}
}
terminate() {
this._terminateCallback(TerminatedError);
}
} }
const TestMode = { const TestMode = {
@ -99,8 +79,8 @@ class Test {
this.fullName = (suite.fullName + ' ' + name).trim(); this.fullName = (suite.fullName + ' ' + name).trim();
this.declaredMode = declaredMode; this.declaredMode = declaredMode;
this.expectation = expectation; this.expectation = expectation;
this._userCallback = new UserCallback(callback, timeout); this._callback = callback;
this.location = this._userCallback.location; this.location = getCallerLocation(__filename);
this.timeout = timeout; this.timeout = timeout;
// Test results // Test results
@ -173,17 +153,17 @@ class TestWorker {
this._suiteStack = []; this._suiteStack = [];
this._terminating = false; this._terminating = false;
this._workerId = workerId; this._workerId = workerId;
this._runningTestCallback = null; this._runningTestTerminate = null;
this._runningHookCallback = null; this._runningHookTerminate = null;
this._runTests = []; this._runTests = [];
} }
terminate(terminateHooks) { terminate(terminateHooks) {
this._terminating = true; this._terminating = true;
if (this._runningTestCallback) if (this._runningTestTerminate)
this._runningTestCallback.terminate(); this._runningTestTerminate();
if (terminateHooks && this._runningHookCallback) if (terminateHooks && this._runningHookTerminate)
this._runningHookCallback.terminate(); this._runningHookTerminate();
} }
_markTerminated(test) { _markTerminated(test) {
@ -250,11 +230,12 @@ class TestWorker {
if (!test.error && !this._markTerminated(test)) { if (!test.error && !this._markTerminated(test)) {
await this._testPass._willStartTestBody(this, test); await this._testPass._willStartTestBody(this, test);
this._runningTestCallback = test._userCallback; const { promise, terminate } = runUserCallback(test._callback, test.timeout, [this._state, test]);
test.error = await test._userCallback.run(this._state, test); this._runningTestTerminate = terminate;
test.error = await promise;
this._runningTestTerminate = null;
if (test.error && test.error.stack) if (test.error && test.error.stack)
await this._testPass._runner._sourceMapSupport.rewriteStackTraceWithSourceMaps(test.error); await this._testPass._runner._sourceMapSupport.rewriteStackTraceWithSourceMaps(test.error);
this._runningTestCallback = null;
if (!test.error) if (!test.error)
test.result = TestResult.Ok; test.result = TestResult.Ok;
else if (test.error === TimeoutError) else if (test.error === TimeoutError)
@ -276,20 +257,22 @@ class TestWorker {
if (!hook) if (!hook)
return true; return true;
await this._testPass._willStartHook(this, suite, hook, hookName); await this._testPass._willStartHook(this, suite, hook.location, hookName);
this._runningHookCallback = hook; const timeout = this._testPass._runner._timeout;
let error = await hook.run(this._state, test); const { promise, terminate } = runUserCallback(hook.callback, timeout, [this._state, test]);
this._runningHookCallback = null; this._runningHookTerminate = terminate;
let error = await promise;
this._runningHookTerminate = null;
if (error) { if (error) {
const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`; const locationString = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`;
if (test.result !== TestResult.Terminated) { if (test.result !== TestResult.Terminated) {
// Prefer terminated result over any hook failures. // Prefer terminated result over any hook failures.
test.result = error === TerminatedError ? TestResult.Terminated : TestResult.Crashed; test.result = error === TerminatedError ? TestResult.Terminated : TestResult.Crashed;
} }
let message; let message;
if (error === TimeoutError) { if (error === TimeoutError) {
message = `${location} - Timeout Exceeded ${hook.timeout}ms while running "${hookName}" in suite "${suite.fullName}"`; message = `${locationString} - Timeout Exceeded ${timeout}ms while running "${hookName}" in suite "${suite.fullName}"`;
error = null; error = null;
} else if (error === TerminatedError) { } else if (error === TerminatedError) {
// Do not report termination details - it's just noise. // Do not report termination details - it's just noise.
@ -298,14 +281,14 @@ class TestWorker {
} else { } else {
if (error.stack) if (error.stack)
await this._testPass._runner._sourceMapSupport.rewriteStackTraceWithSourceMaps(error); await this._testPass._runner._sourceMapSupport.rewriteStackTraceWithSourceMaps(error);
message = `${location} - FAILED while running "${hookName}" in suite "${suite.fullName}": `; message = `${locationString} - FAILED while running "${hookName}" in suite "${suite.fullName}": `;
} }
await this._testPass._didFailHook(this, suite, hook, hookName, message, error); await this._testPass._didFailHook(this, suite, hook.location, hookName, message, error);
test.error = error; test.error = error;
return false; return false;
} }
await this._testPass._didCompleteHook(this, suite, hook, hookName); await this._testPass._didCompleteHook(this, suite, hook.location, hookName);
return true; return true;
} }
@ -435,19 +418,19 @@ class TestPass {
debug('testrunner:test')(`[${worker._workerId}] ${test.result.toUpperCase()} "${test.fullName}" (${test.location.fileName + ':' + test.location.lineNumber})`); debug('testrunner:test')(`[${worker._workerId}] ${test.result.toUpperCase()} "${test.fullName}" (${test.location.fileName + ':' + test.location.lineNumber})`);
} }
async _willStartHook(worker, suite, hook, hookName) { async _willStartHook(worker, suite, location, hookName) {
debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" started for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`); debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" started for "${suite.fullName}" (${location.fileName + ':' + location.lineNumber})`);
} }
async _didFailHook(worker, suite, hook, hookName, message, error) { async _didFailHook(worker, suite, location, hookName, message, error) {
debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" FAILED for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`); debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" FAILED for "${suite.fullName}" (${location.fileName + ':' + location.lineNumber})`);
if (message) if (message)
this._result.addError(message, error, worker); this._result.addError(message, error, worker);
this._result.setResult(TestResult.Crashed, message); this._result.setResult(TestResult.Crashed, message);
} }
async _didCompleteHook(worker, suite, hook, hookName) { async _didCompleteHook(worker, suite, location, hookName) {
debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" OK for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`); debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" OK for "${suite.fullName}" (${location.fileName + ':' + location.lineNumber})`);
} }
} }
@ -639,8 +622,8 @@ class TestRunner extends EventEmitter {
_addHook(hookName, callback) { _addHook(hookName, callback) {
assert(this._currentSuite[hookName] === null, `Only one ${hookName} hook available per suite`); assert(this._currentSuite[hookName] === null, `Only one ${hookName} hook available per suite`);
const hook = new UserCallback(callback, this._timeout); const location = getCallerLocation(__filename);
this._currentSuite[hookName] = hook; this._currentSuite[hookName] = { callback, location };
} }
async run(options = {}) { async run(options = {}) {