Skip to content

Commit

Permalink
POC for unique test/hook/suite identifiers
Browse files Browse the repository at this point in the history
- fixed an issue in rollup config causing weird problems
- changed how we define `global.Mocha` in `browser-entry.js` (tangential)
- removed `object.assign` polyfill (tangential)
- renamed misnamed `buffered-runner.spec.js` to `parallel-buffered-runner.spec.js`
  • Loading branch information
boneskull committed Aug 18, 2020
1 parent 2b8a1ff commit de5bdfc
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 87 deletions.
4 changes: 2 additions & 2 deletions browser-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ Mocha.process = process;
* Expose mocha.
*/

mocha.Mocha = Mocha;
mocha.mocha = mocha;
global.Mocha = Mocha;
global.mocha = mocha;

// this allows test/acceptance/required-tokens.js to pass; thus,
// you can now do `const describe = require('mocha').describe` in a
Expand Down
23 changes: 14 additions & 9 deletions lib/hook.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

var Runnable = require('./runnable');
var inherits = require('./utils').inherits;
const {inherits, constants} = require('./utils');

/**
* Expose `Hook`.
Expand Down Expand Up @@ -63,16 +63,21 @@ Hook.prototype.serialize = function serialize() {
return {
$$isPending: this.isPending(),
$$titlePath: this.titlePath(),
ctx: {
currentTest: {
title: this.ctx && this.ctx.currentTest && this.ctx.currentTest.title
}
},
ctx:
this.ctx && this.ctx.currentTest
? {
currentTest: {
title: this.ctx.currentTest.title,
[constants.MOCHA_ID_PROP_NAME]: this.ctx.currentTest.id
}
}
: {},
parent: {
root: this.parent.root,
title: this.parent.title
[constants.MOCHA_ID_PROP_NAME]: this.parent.id
},
title: this.title,
type: this.type
type: this.type,
[constants.MOCHA_ID_PROP_NAME]: this.id,
__mocha_partial__: true
};
};
4 changes: 2 additions & 2 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ exports.Test = require('./test');
* suite with
* @param {boolean} [options.isWorker] - Should be `true` if `Mocha` process is running in a worker process.
*/
function Mocha(options) {
options = utils.assign({}, mocharc, options || {});
function Mocha(options = {}) {
options = {...mocharc, ...options};
this.files = [];
this.options = options;
// root suite
Expand Down
106 changes: 92 additions & 14 deletions lib/nodejs/parallel-buffered-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const {EVENT_RUN_BEGIN, EVENT_RUN_END} = Runner.constants;
const debug = require('debug')('mocha:parallel:parallel-buffered-runner');
const {BufferedWorkerPool} = require('./buffered-worker-pool');
const {setInterval, clearInterval} = global;
const {createMap} = require('../utils');
const {createMap, constants} = require('../utils');
const {MOCHA_ID_PROP_NAME} = constants;

/**
* Outputs a debug statement with worker stats
Expand Down Expand Up @@ -76,6 +77,9 @@ class ParallelBufferedRunner extends Runner {
}
});

this._linkPartialObjects = false;
this._linkedObjectMap = new Map();

this.once(Runner.constants.EVENT_RUN_END, () => {
this._state = COMPLETE;
});
Expand All @@ -88,10 +92,60 @@ class ParallelBufferedRunner extends Runner {
* @returns {FileRunner} Mapping function
*/
_createFileRunner(pool, options) {
/**
* Emits event and sets `BAILING` state, if necessary.
* @param {Object} event - Event having `eventName`, maybe `data` and maybe `error`
* @param {number} failureCount - Failure count
*/
const emitEvent = (event, failureCount) => {
this.emit(event.eventName, event.data, event.error);
if (
this._state !== BAILING &&
event.data &&
event.data._bail &&
(failureCount || event.error)
) {
debug('run(): nonzero failure count & found bail flag');
// we need to let the events complete for this file, as the worker
// should run any cleanup hooks
this._state = BAILING;
}
};

/**
* Given an event, recursively find any objects in its data that have ID's, and create object references to already-seen objects.
* @param {Object} event - Event having `eventName`, maybe `data` and maybe `error`
*/
const linkEvent = event => {
const stack = [{parent: event, prop: 'data'}];
while (stack.length) {
const {parent, prop} = stack.pop();
const obj = parent[prop];
let newObj;
if (obj && typeof obj === 'object' && obj[MOCHA_ID_PROP_NAME]) {
const id = obj[MOCHA_ID_PROP_NAME];
newObj = this._linkedObjectMap.has(id)
? Object.assign(this._linkedObjectMap.get(id), obj)
: obj;
this._linkedObjectMap.set(id, newObj);
parent[prop] = newObj;
} else {
newObj = obj;
}
Object.keys(newObj).forEach(key => {
const value = obj[key];
if (value && typeof value === 'object' && value[MOCHA_ID_PROP_NAME]) {
stack.push({obj: value, parent: newObj, prop: key});
}
});
}
};

return async file => {
debug('run(): enqueueing test file %s', file);
try {
const {failureCount, events} = await pool.run(file, options);

if (this._state === BAILED) {
// short-circuit after a graceful bail. if this happens,
// some other worker has bailed.
Expand All @@ -107,20 +161,18 @@ class ParallelBufferedRunner extends Runner {
);
this.failures += failureCount; // can this ever be non-numeric?
let event = events.shift();
while (event) {
this.emit(event.eventName, event.data, event.error);
if (
this._state !== BAILING &&
event.data &&
event.data._bail &&
(failureCount || event.error)
) {
debug('run(): nonzero failure count & found bail flag');
// we need to let the events complete for this file, as the worker
// should run any cleanup hooks
this._state = BAILING;

if (this._linkPartialObjects) {
while (event) {
linkEvent(event);
emitEvent(event, failureCount);
event = events.shift();
}
} else {
while (event) {
emitEvent(event, failureCount);
event = events.shift();
}
event = events.shift();
}
if (this._state === BAILING) {
debug('run(): terminating pool due to "bail" flag');
Expand Down Expand Up @@ -275,6 +327,32 @@ class ParallelBufferedRunner extends Runner {
})();
return this;
}

/**
* Toggle partial object linking behavior; used for building object references from
* unique ID's.
* @param {boolean} [value] - If `true`, enable partial object linking, otherwise disable
* @returns {Runner}
* @chainable
* @public
* @example
* // this reporter needs proper object references when run in parallel mode
* class MyReporter() {
* constructor(runner) {
* this.runner.linkPartialObjects(true)
* .on(EVENT_SUITE_BEGIN, suite => {
// this Suite may be the same object...
* })
* .on(EVENT_TEST_BEGIN, test => {
* // ...as the `test.parent` property
* });
* }
* }
*/
linkPartialObjects(value) {
this._linkPartialObjects = Boolean(value);
return super.linkPartialObjects(value);
}
}

module.exports = ParallelBufferedRunner;
Expand Down
6 changes: 6 additions & 0 deletions lib/runnable.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ function Runnable(title, fn) {
this._timeout = 2000;
this._slow = 75;
this._retries = -1;
utils.assignNewMochaID(this);
Object.defineProperty(this, 'id', {
get() {
return utils.getMochaID(this);
}
});
this.reset();
}

Expand Down
27 changes: 27 additions & 0 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,33 @@ Runner.prototype.run = function(fn, opts) {
return this;
};

/**
* Toggle partial object linking behavior; used for building object references from
* unique ID's. Does nothing in serial mode, because the object references already exist.
* Subclasses can implement this (e.g., `ParallelBufferedRunner`)
* @abstract
* @param {boolean} [value] - If `true`, enable partial object linking, otherwise disable
* @returns {Runner}
* @chainable
* @public
* @example
* // this reporter needs proper object references when run in parallel mode
* class MyReporter() {
* constructor(runner) {
* this.runner.linkPartialObjects(true)
* .on(EVENT_SUITE_BEGIN, suite => {
// this Suite may be the same object...
* })
* .on(EVENT_TEST_BEGIN, test => {
* // ...as the `test.parent` property
* });
* }
* }
*/
Runner.prototype.linkPartialObjects = function(value) {
return this;
};

/**
* Cleanly abort execution.
*
Expand Down
14 changes: 13 additions & 1 deletion lib/suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var milliseconds = require('ms');
var errors = require('./errors');
var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError;

const {MOCHA_ID_PROP_NAME} = utils.constants;

/**
* Expose `Suite`.
*/
Expand Down Expand Up @@ -74,6 +76,14 @@ function Suite(title, parentContext, isRoot) {
this._bail = false;
this._onlyTests = [];
this._onlySuites = [];
utils.assignNewMochaID(this);

Object.defineProperty(this, 'id', {
get() {
return utils.getMochaID(this);
}
});

this.reset();

this.on('newListener', function(event) {
Expand Down Expand Up @@ -579,7 +589,9 @@ Suite.prototype.serialize = function serialize() {
$$fullTitle: this.fullTitle(),
$$isPending: this.isPending(),
root: this.root,
title: this.title
title: this.title,
id: this.id,
parent: this.parent ? {[MOCHA_ID_PROP_NAME]: this.parent.id} : null
};
};

Expand Down
8 changes: 6 additions & 2 deletions lib/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ var errors = require('./errors');
var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError;
var isString = utils.isString;

const {MOCHA_ID_PROP_NAME} = utils.constants;

module.exports = Test;

/**
Expand Down Expand Up @@ -98,12 +100,14 @@ Test.prototype.serialize = function serialize() {
duration: this.duration,
err: this.err,
parent: {
$$fullTitle: this.parent.fullTitle()
$$fullTitle: this.parent.fullTitle(),
[MOCHA_ID_PROP_NAME]: this.parent.id
},
speed: this.speed,
state: this.state,
title: this.title,
type: this.type,
file: this.file
file: this.file,
[MOCHA_ID_PROP_NAME]: this.id
};
};
33 changes: 31 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
* Module dependencies.
*/

const {nanoid} = require('nanoid/non-secure');
var path = require('path');
var util = require('util');
var he = require('he');

var assign = (exports.assign = require('object.assign').getPolyfill());
const MOCHA_ID_PROP_NAME = '__mocha_id__';

/**
* Inherit the prototype methods from one constructor into another.
Expand Down Expand Up @@ -575,7 +576,7 @@ exports.noop = function() {};
* @returns {Object} An object with no prototype, having `...obj` properties
*/
exports.createMap = function(obj) {
return assign.apply(
return Object.assign.apply(
null,
[Object.create(null)].concat(Array.prototype.slice.call(arguments))
);
Expand Down Expand Up @@ -645,3 +646,31 @@ exports.cwd = function cwd() {
exports.isBrowser = function isBrowser() {
return Boolean(process.browser);
};

exports.constants = exports.defineConstants({
MOCHA_ID_PROP_NAME
});

/**
* Creates a new unique identifier
* @returns {string} Unique identifier
*/
exports.uniqueID = () => nanoid();

exports.assignNewMochaID = obj => {
const id = exports.uniqueID();
Object.defineProperty(obj, MOCHA_ID_PROP_NAME, {
get() {
return id;
}
});
return obj;
};

/**
* Retrieves a Mocha ID from an object, if present.
* @param {*} [obj] - Object
* @returns {string|void}
*/
exports.getMochaID = obj =>
obj && typeof obj === 'object' ? obj[MOCHA_ID_PROP_NAME] : undefined;
Loading

0 comments on commit de5bdfc

Please sign in to comment.