Files
jasmine/src/core/TreeRunner.js
Steve Gravrock f12f4395f0 Redesigned moudule system
* Top level private APIs (e.g. jasmine.private.whatever) are no longer
  exposed
* jasmineRequire is no longer exposed
* core is self-booting
* Globals are automatically created in browsers. (They can subsequently
  be removed by user code if desired.)
* Globals are *not* automatically created in Node. An installGlobals
  function is exported instead. The jasmine package calls installGlobals
  unless configured not to do so.
* In Node, the same instance is returned each time jasmine-core is
  imported. A reset function is exported. It effectively resets all state
  by discarding the env and creating a new one. This allows mulitple
  sequential runs within the same process to be independent of each
  other, but does not allow multiple concurrent runs. (That probably never
  worked anyway.)

Fixes #2094
2026-02-15 20:16:45 -08:00

292 lines
8.6 KiB
JavaScript

getJasmineRequireObj().TreeRunner = function(j$, private$) {
'use strict';
class TreeRunner {
#executionTree;
#setTimeout;
#globalErrors;
#runableResources;
#reportDispatcher;
#runQueue;
#getConfig;
#currentRunableTracker;
#hasFailures;
constructor(attrs) {
this.#executionTree = attrs.executionTree;
this.#globalErrors = attrs.globalErrors;
this.#setTimeout = attrs.setTimeout || setTimeout.bind(globalThis);
this.#runableResources = attrs.runableResources;
this.#reportDispatcher = attrs.reportDispatcher;
this.#runQueue = attrs.runQueue;
this.#getConfig = attrs.getConfig;
this.#currentRunableTracker = attrs.currentRunableTracker;
}
async execute() {
this.#hasFailures = false;
await new Promise(resolve => {
this.#executeSuite(this.#executionTree.topSuite, resolve);
});
return { hasFailures: this.#hasFailures };
}
#wrapNodes(nodes) {
return nodes.map(node => {
return {
fn: done => {
if (node.suite) {
this.#executeSuite(node.suite, done);
} else {
this._executeSpec(node.spec, done);
}
}
};
});
}
// Only exposed for testing.
_executeSpec(spec, specOverallDone) {
const onStart = next => {
this.#currentRunableTracker.setCurrentSpec(spec);
this.#runableResources.initForRunable(
spec.id,
spec.parentSuiteId || this.#executionTree.topSuite.id
);
this.#reportDispatcher.specStarted(spec.startedEvent()).then(next);
};
const resultCallback = (result, next) => {
this.#specComplete(spec).then(next);
};
const queueableFns = this.#specQueueableFns(
spec,
onStart,
resultCallback
);
this.#runQueue({
isLeaf: true,
queueableFns,
onException: e => spec.handleException(e),
onMultipleDone: () => {
// Issue an erorr. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
spec.onLateError(
new Error(
'An asynchronous spec, beforeEach, or afterEach function called its ' +
"'done' callback more than once.\n(in spec: " +
spec.getFullName() +
')'
)
);
},
onComplete: () => {
if (spec.status() === 'failed') {
specOverallDone(new private$.StopExecutionError('spec failed'));
} else {
specOverallDone();
}
},
userContext: spec.userContext(),
runnableName: spec.getFullName.bind(spec),
SkipPolicy: private$.CompleteOnFirstErrorSkipPolicy
});
}
#specQueueableFns(spec, onStart, resultCallback) {
const config = this.#getConfig();
const excluded = this.#executionTree.isExcluded(spec);
const ba = spec.beforeAndAfterFns();
let fns = [...ba.befores, spec.queueableFn, ...ba.afters];
if (spec.markedPending || excluded === true) {
fns = [];
}
const start = {
fn(done) {
spec.executionStarted();
onStart(done);
}
};
const complete = {
fn(done) {
spec.executionFinished(excluded, config.failSpecWithNoExpectations);
resultCallback(spec.doneEvent(), done);
},
type: 'specCleanup'
};
fns.unshift(start);
if (config.detectLateRejectionHandling) {
fns.push(this.#lateUnhandledRejectionChecker());
}
fns.push(complete);
return fns;
}
#executeSuite(suite, done) {
const isTopSuite = suite === this.#executionTree.topSuite;
const isExcluded = this.#executionTree.isExcluded(suite);
let befores = [];
let afters = [];
if (suite.beforeAllFns.length > 0 && !isExcluded) {
befores = [...suite.beforeAllFns];
if (this.#getConfig().detectLateRejectionHandling) {
befores.push(this.#lateUnhandledRejectionChecker());
}
}
if (suite.afterAllFns.length > 0 && !isExcluded) {
afters = [...suite.afterAllFns];
if (this.#getConfig().detectLateRejectionHandling) {
afters.push(this.#lateUnhandledRejectionChecker());
}
}
const children = isTopSuite
? this.#executionTree.childrenOfTopSuite()
: this.#executionTree.childrenOfSuite(suite);
const queueableFns = [
...befores,
...this.#wrapNodes(children),
...afters
];
if (!isTopSuite) {
queueableFns.unshift({
fn: next => {
this.#suiteSegmentStart(suite, next);
}
});
}
this.#runQueue({
onComplete: maybeError => {
this.#suiteSegmentComplete(suite, () => {
done(maybeError);
});
},
queueableFns,
userContext: suite.sharedUserContext(),
onException: function() {
suite.handleException.apply(suite, arguments);
},
onMultipleDone: suite.onMultipleDone
? suite.onMultipleDone.bind(suite)
: null,
SkipPolicy: this.#suiteSkipPolicy()
});
}
// Returns a queueable fn that reports any still-unhandled rejections in
// cases where detectLateRejectionHandling is enabled. Should only be called
// when detectLateRejectionHandling is enabled, because the setTimeout
// imposes a significant performance penalty in suites with lots of fast
// specs.
#lateUnhandledRejectionChecker() {
const globalErrors = this.#globalErrors;
return {
fn: done => {
// setTimeout is necessary to trigger rejectionhandled events
this.#setTimeout(function() {
globalErrors.reportUnhandledRejections();
done();
});
}
};
}
#suiteSegmentStart(suite, next) {
this.#currentRunableTracker.pushSuite(suite);
this.#runableResources.initForRunable(suite.id, suite.parentSuite.id);
this.#reportDispatcher.suiteStarted(suite.startedEvent()).then(next);
suite.startTimer();
}
#suiteSegmentComplete(suite, next) {
suite.endTimer();
const isTopSuite = suite === this.#executionTree.topSuite;
if (!isTopSuite) {
if (suite !== this.#currentRunableTracker.currentSuite()) {
throw new Error('Tried to complete the wrong suite');
}
// suite.cleanupBeforeAfter() is conditional because calling it on the
// top suite breaks parallel mode. The top suite is reentered every time
// a runner runs another file, so its before and after fns need to be
// preserved.
suite.cleanupBeforeAfter();
this.#runableResources.clearForRunable(suite.id);
this.#currentRunableTracker.popSuite();
if (suite.doneEvent().status === 'failed') {
this.#hasFailures = true;
}
}
const finish = isTopSuite
? next
: () => this.#reportSuiteDone(suite, next);
if (suite.hadBeforeAllFailure) {
this.#reportChildrenOfBeforeAllFailure(suite).then(finish);
} else {
finish();
}
}
#reportSuiteDone(suite, next) {
suite.reportedDone = true;
this.#reportDispatcher.suiteDone(suite.doneEvent()).then(next);
}
async #specComplete(spec) {
this.#runableResources.clearForRunable(spec.id);
this.#currentRunableTracker.setCurrentSpec(null);
if (spec.status() === 'failed') {
this.#hasFailures = true;
}
await this.#reportSpecDone(spec);
}
async #reportSpecDone(spec) {
spec.reportedDone = true;
await this.#reportDispatcher.specDone(spec.doneEvent());
}
async #reportChildrenOfBeforeAllFailure(suite) {
for (const child of suite.children) {
if (child instanceof private$.Suite) {
await this.#reportDispatcher.suiteStarted(child.startedEvent());
await this.#reportChildrenOfBeforeAllFailure(child);
await this.#reportDispatcher.suiteDone(child.doneEvent());
} else {
/* a spec */
await this.#reportDispatcher.specStarted(child.startedEvent());
child.hadBeforeAllFailure();
await this.#reportSpecDone(child);
}
}
}
#suiteSkipPolicy() {
if (this.#getConfig().stopOnSpecFailure) {
return private$.CompleteOnFirstErrorSkipPolicy;
} else {
return private$.SkipAfterBeforeAllErrorPolicy;
}
}
}
return TreeRunner;
};