Implement TreeProcessor to solve some issues with running the suite

- execute beforeAll/afterAll once per suite instead of once per child
  when running focused specs/suites Fixes #773
- refuse to execute an order if it would cause a suite with a beforeAll
  or afterAll to be re-entered after leaving once
- report children of an xdescribe similarly to how they would be
  reported if they were themselves x'd out Fixes #774
- only process the tree once instead of figuring it out again at each
  level

[finishes #87545620]
Fixes #776
This commit is contained in:
slackersoft
2015-02-27 11:16:53 -08:00
committed by Chris Amavisca and Gregg Van Hove
parent 0c68cc4afc
commit 715de7aa38
11 changed files with 1289 additions and 503 deletions

View File

@@ -100,27 +100,21 @@ getJasmineRequireObj().Env = function(j$) {
delete runnableResources[id];
};
var beforeAndAfterFns = function(suite, runnablesExplictlySet) {
var beforeAndAfterFns = function(suite) {
return function() {
var befores = [],
afters = [],
beforeAlls = [],
afterAlls = [];
afters = [];
while(suite) {
befores = befores.concat(suite.beforeFns);
afters = afters.concat(suite.afterFns);
if (runnablesExplictlySet()) {
beforeAlls = beforeAlls.concat(suite.beforeAllFns);
afterAlls = afterAlls.concat(suite.afterAllFns);
}
suite = suite.parentSuite;
}
return {
befores: beforeAlls.reverse().concat(befores.reverse()),
afters: afters.concat(afterAlls)
befores: befores.reverse(),
afters: afters
};
};
};
@@ -190,26 +184,40 @@ getJasmineRequireObj().Env = function(j$) {
};
this.execute = function(runnablesToRun) {
if(runnablesToRun) {
runnablesExplictlySet = true;
} else if (focusedRunnables.length) {
runnablesExplictlySet = true;
runnablesToRun = focusedRunnables;
} else {
runnablesToRun = [topSuite.id];
if(!runnablesToRun) {
if (focusedRunnables.length) {
runnablesToRun = focusedRunnables;
} else {
runnablesToRun = [topSuite.id];
}
}
var processor = new j$.TreeProcessor({
tree: topSuite,
runnableIds: runnablesToRun,
queueRunnerFactory: queueRunnerFactory,
nodeStart: function(suite) {
currentlyExecutingSuites.push(suite);
defaultResourcesForRunnable(suite.id, suite.parentSuite.id);
reporter.suiteStarted(suite.result);
},
nodeComplete: function(suite, result) {
if (!suite.disabled) {
clearResourcesForRunnable(suite.id);
}
currentlyExecutingSuites.pop();
reporter.suiteDone(result);
}
});
var allFns = [];
for(var i = 0; i < runnablesToRun.length; i++) {
var runnable = runnableLookupTable[runnablesToRun[i]];
allFns.push((function(runnable) { return { fn: function(done) { runnable.execute(done); } }; })(runnable));
if(!processor.processTree().valid) {
throw new Error('Invalid order: would cause a beforeAll or afterAll to be run multiple times');
}
reporter.jasmineStarted({
totalSpecsDefined: totalSpecsDefined
});
queueRunnerFactory({queueableFns: allFns, onComplete: reporter.jasmineDone});
processor.execute(reporter.jasmineDone);
};
this.addReporter = function(reporterToAdd) {
@@ -233,28 +241,12 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSuiteId(),
description: description,
parentSuite: currentDeclarationSuite,
queueRunner: queueRunnerFactory,
onStart: suiteStarted,
expectationFactory: expectationFactory,
expectationResultFactory: expectationResultFactory,
runnablesExplictlySetGetter: runnablesExplictlySetGetter,
resultCallback: function(attrs) {
if (!suite.disabled) {
clearResourcesForRunnable(suite.id);
}
currentlyExecutingSuites.pop();
reporter.suiteDone(attrs);
}
expectationResultFactory: expectationResultFactory
});
runnableLookupTable[suite.id] = suite;
return suite;
function suiteStarted(suite) {
currentlyExecutingSuites.push(suite);
defaultResourcesForRunnable(suite.id, suite.parentSuite.id);
reporter.suiteStarted(suite.result);
}
};
this.describe = function(description, specDefinitions) {
@@ -326,17 +318,11 @@ getJasmineRequireObj().Env = function(j$) {
}
}
var runnablesExplictlySet = false;
var runnablesExplictlySetGetter = function(){
return runnablesExplictlySet;
};
var specFactory = function(description, fn, suite, timeout) {
totalSpecsDefined++;
var spec = new j$.Spec({
id: getNextSpecId(),
beforeAndAfterFns: beforeAndAfterFns(suite, runnablesExplictlySetGetter),
beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory,
resultCallback: specResultCallback,
getSpecName: function(spec) {

View File

@@ -40,13 +40,13 @@ getJasmineRequireObj().Spec = function(j$) {
return this.expectationFactory(actual, this);
};
Spec.prototype.execute = function(onComplete) {
Spec.prototype.execute = function(onComplete, enabled) {
var self = this;
this.onStart(this);
if (this.markedPending || this.disabled) {
complete();
if (!this.isExecutable() || enabled === false) {
complete(enabled);
return;
}
@@ -60,8 +60,8 @@ getJasmineRequireObj().Spec = function(j$) {
userContext: this.userContext()
});
function complete() {
self.result.status = self.status();
function complete(enabledAgain) {
self.result.status = self.status(enabledAgain);
self.resultCallback(self.result);
if (onComplete) {
@@ -96,8 +96,13 @@ getJasmineRequireObj().Spec = function(j$) {
}
};
Spec.prototype.status = function() {
if (this.disabled) {
Spec.prototype.getResult = function() {
this.result.status = this.status();
return this.result;
};
Spec.prototype.status = function(enabled) {
if (this.disabled || enabled === false) {
return 'disabled';
}

View File

@@ -4,18 +4,13 @@ getJasmineRequireObj().Suite = function() {
this.id = attrs.id;
this.parentSuite = attrs.parentSuite;
this.description = attrs.description;
this.onStart = attrs.onStart || function() {};
this.resultCallback = attrs.resultCallback || function() {};
this.clearStack = attrs.clearStack || function(fn) {fn();};
this.expectationFactory = attrs.expectationFactory;
this.expectationResultFactory = attrs.expectationResultFactory;
this.runnablesExplictlySetGetter = attrs.runnablesExplictlySetGetter || function() {};
this.beforeFns = [];
this.afterFns = [];
this.beforeAllFns = [];
this.afterAllFns = [];
this.queueRunner = attrs.queueRunner || function() {};
this.disabled = false;
this.children = [];
@@ -78,51 +73,17 @@ getJasmineRequireObj().Suite = function() {
}
};
Suite.prototype.execute = function(onComplete) {
var self = this;
this.onStart(this);
if (this.disabled) {
complete();
return;
}
var allFns = [];
for (var i = 0; i < this.children.length; i++) {
allFns.push(wrapChildAsAsync(this.children[i]));
}
if (this.isExecutable()) {
allFns = this.beforeAllFns.concat(allFns);
allFns = allFns.concat(this.afterAllFns);
}
this.queueRunner({
queueableFns: allFns,
onComplete: complete,
userContext: this.sharedUserContext(),
onException: function() { self.onException.apply(self, arguments); }
});
function complete() {
self.result.status = self.status();
self.resultCallback(self.result);
if (onComplete) {
onComplete();
}
}
function wrapChildAsAsync(child) {
return { fn: function(done) { child.execute(done); } };
}
Suite.prototype.isExecutable = function() {
return !this.disabled;
};
Suite.prototype.isExecutable = function() {
var runnablesExplicitlySet = this.runnablesExplictlySetGetter();
return !runnablesExplicitlySet && hasExecutableChild(this.children);
Suite.prototype.canBeReentered = function() {
return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0;
};
Suite.prototype.getResult = function() {
this.result.status = this.status();
return this.result;
};
Suite.prototype.sharedUserContext = function() {
@@ -175,17 +136,6 @@ getJasmineRequireObj().Suite = function() {
return !args[0];
}
function hasExecutableChild(children) {
var foundActive = false;
for (var i = 0; i < children.length; i++) {
if (children[i].isExecutable()) {
foundActive = true;
break;
}
}
return foundActive;
}
function clone(obj) {
var clonedObj = {};
for (var prop in obj) {

203
src/core/TreeProcessor.js Normal file
View File

@@ -0,0 +1,203 @@
getJasmineRequireObj().TreeProcessor = function() {
function TreeProcessor(attrs) {
var tree = attrs.tree,
runnableIds = attrs.runnableIds,
queueRunnerFactory = attrs.queueRunnerFactory,
nodeStart = attrs.nodeStart || function() {},
nodeComplete = attrs.nodeComplete || function() {},
stats = { valid: true },
processed = false,
defaultMin = Infinity,
defaultMax = 1 - Infinity;
this.processTree = function() {
processNode(tree, false);
processed = true;
return stats;
};
this.execute = function(done) {
if (!processed) {
this.processTree();
}
if (!stats.valid) {
throw 'invalid order';
}
var childFns = wrapChildren(tree, 0);
queueRunnerFactory({
queueableFns: childFns,
onException: function() {
tree.onException.apply(tree, arguments);
},
onComplete: done
});
};
function runnableIndex(id) {
for (var i = 0; i < runnableIds.length; i++) {
if (runnableIds[i] === id) {
return i;
}
}
}
function processNode(node, parentEnabled) {
var executableIndex = runnableIndex(node.id);
if (executableIndex !== undefined) {
parentEnabled = true;
}
parentEnabled = parentEnabled && node.isExecutable();
if (!node.children) {
stats[node.id] = {
executable: parentEnabled && node.isExecutable(),
segments: [{
index: 0,
owner: node,
nodes: [node],
min: startingMin(executableIndex),
max: startingMax(executableIndex)
}]
};
} else {
var hasExecutableChild = false;
for (var i = 0; i < node.children.length; i++) {
var child = node.children[i];
processNode(child, parentEnabled);
if (!stats.valid) {
return;
}
var childStats = stats[child.id];
hasExecutableChild = hasExecutableChild || childStats.executable;
}
stats[node.id] = {
executable: hasExecutableChild
};
segmentChildren(node, stats[node.id], executableIndex);
if (!node.canBeReentered() && stats[node.id].segments.length > 1) {
stats = { valid: false };
}
}
}
function startingMin(executableIndex) {
return executableIndex === undefined ? defaultMin : executableIndex;
}
function startingMax(executableIndex) {
return executableIndex === undefined ? defaultMax : executableIndex;
}
function segmentChildren(node, nodeStats, executableIndex) {
var currentSegment = { index: 0, owner: node, nodes: [], min: startingMin(executableIndex), max: startingMax(executableIndex) },
result = [currentSegment],
lastMax = defaultMax,
orderedChildSegments = orderChildSegments(node.children);
function isSegmentBoundary(minIndex) {
return lastMax !== defaultMax && minIndex !== defaultMin && lastMax < minIndex - 1;
}
for (var i = 0; i < orderedChildSegments.length; i++) {
var childSegment = orderedChildSegments[i],
maxIndex = childSegment.max,
minIndex = childSegment.min;
if (isSegmentBoundary(minIndex)) {
currentSegment = {index: result.length, owner: node, nodes: [], min: defaultMin, max: defaultMin};
result.push(currentSegment);
}
currentSegment.nodes.push(childSegment);
currentSegment.min = Math.min(currentSegment.min, minIndex);
currentSegment.max = Math.max(currentSegment.max, maxIndex);
lastMax = maxIndex;
}
nodeStats.segments = result;
}
function orderChildSegments(array) {
var result = [];
for (var i = 0; i < array.length; i++) {
var child = array[i],
segments = stats[child.id].segments;
for (var j = 0; j < segments.length; j++) {
result.push(segments[j]);
}
}
result.sort(function(a, b) {
if (a.min === null) {
return b.min === null ? 0 : 1;
}
if (b.min === null) {
return -1;
}
return a.min - b.min;
});
return result;
}
function executeNode(node, segmentNumber) {
if (node.children) {
return {
fn: function(done) {
nodeStart(node);
queueRunnerFactory({
onComplete: function() {
nodeComplete(node, node.getResult());
done();
},
queueableFns: wrapChildren(node, segmentNumber),
userContext: node.sharedUserContext(),
onException: function() {
node.onException.apply(node, arguments);
}
});
}
};
} else {
return {
fn: function(done) { node.execute(done, stats[node.id].executable); }
};
}
}
function wrapChildren(node, segmentNumber) {
var result = [],
segmentChildren = stats[node.id].segments[segmentNumber].nodes;
for (var i = 0; i < segmentChildren.length; i++) {
result.push(executeNode(segmentChildren[i].owner, segmentChildren[i].index));
}
if (!stats[node.id].executable) {
return result;
}
return node.beforeAllFns.concat(result).concat(node.afterAllFns);
}
}
return TreeProcessor;
};

View File

@@ -43,6 +43,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) {
j$.StringMatching = jRequire.StringMatching(j$);
j$.Suite = jRequire.Suite();
j$.Timer = jRequire.Timer();
j$.TreeProcessor = jRequire.TreeProcessor();
j$.version = jRequire.version();
j$.matchers = jRequire.requireMatchers(jRequire, j$);