Skip to content

Commit

Permalink
Reporter: Handle acyclical duplicates
Browse files Browse the repository at this point in the history
Handles the case where an object is references more than once
in an object graph but is acyclic. Also accounts for cyclical
references.
  • Loading branch information
zackthehuman authored Feb 14, 2021
1 parent b833973 commit fc2860a
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 27 deletions.
50 changes: 34 additions & 16 deletions lib/reporters/TapReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,28 +119,46 @@ function prettyYamlValue (value, indent = 4) {
}

// Handle null, boolean, array, and object
return JSON.stringify(value, createCycleSafeReplacer(), 2);
return JSON.stringify(decycledShallowClone(value), null, 2);
}

/**
* Creates a replacer function to use with JSON.stringify that
* safely handles cyclical references. When a cycle is detected
* it will be represented as "[Circular]".
* Creates a shallow clone of an object where cycles have
* been replaced with "[Circular]".
*/
function createCycleSafeReplacer () {
const seen = new Set();

return function cycleSafeReplacer (_key, value) {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
function decycledShallowClone (object, ancestors = []) {
if (ancestors.includes(object)) {
return '[Circular]';
}

seen.add(value);
}
let clone;

const type = Object.prototype.toString
.call(object)
.replace(/^\[.+\s(.+?)]$/, '$1')
.toLowerCase();

switch (type) {
case 'array':
ancestors.push(object);
clone = object.map(function (element) {
return decycledShallowClone(element, ancestors);
});
ancestors.pop(object);
break;
case 'object':
ancestors.push(object);
clone = {};
Object.keys(object).forEach(function (key) {
clone[key] = decycledShallowClone(object[key], ancestors);
});
ancestors.pop(object);
break;
default:
clone = object;
}

return value;
};
return clone;
}

module.exports = class TapReporter {
Expand Down
53 changes: 45 additions & 8 deletions test/fixtures/unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@ function copyErrors (testEnd) {
return testEnd;
}

function createCircular () {
const circular = {};
circular.cycle = circular;
return circular;
/**
* Creates an object that has a cyclical reference.
*/
function createCyclical () {
const cyclical = {};
cyclical.cycle = cyclical;
return cyclical;
}

/**
* Creates an object that references another object more
* than once in an acyclical way.
*/
function createDuplicateAcyclical () {
const duplicate = {};
return {
a: duplicate,
b: duplicate
};
}

module.exports = {
Expand Down Expand Up @@ -203,24 +218,46 @@ module.exports = {
actual : []
expected: expected
...`,
actualCircular: copyErrors({
actualCyclical: copyErrors({
name: 'Failing',
suiteName: undefined,
fullName: ['Failing'],
status: 'failed',
runtime: 0,
errors: [{
passed: false,
actual: createCircular(),
actual: createCyclical(),
expected: 'expected'
}],
assertions: null
}),
actualCircularTap: ` ---
actualCyclicalTap: ` ---
message: failed
severity: failed
actual : {
"cycle": "[Circular]"
}
expected: expected
...`,
actualDuplicateAcyclic: copyErrors({
name: 'Failing',
suiteName: undefined,
fullName: ['Failing'],
status: 'failed',
runtime: 0,
errors: [{
passed: false,
actual: createDuplicateAcyclical(),
expected: 'expected'
}],
assertions: null
}),
actualDuplicateAcyclicTap: ` ---
message: failed
severity: failed
actual : {
"a": {},
"b": {}
}
expected: expected
...`,
Expand Down Expand Up @@ -259,7 +296,7 @@ module.exports = {
errors: [{
passed: false,
actual: 'actual',
expected: createCircular()
expected: createCyclical()
}],
assertions: null
}),
Expand Down
11 changes: 8 additions & 3 deletions test/unit/tap-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,14 @@ QUnit.module('TapReporter', hooks => {
assert.equal(spy.args[1][0], data.actualArrayTap);
});

test('output actual assertion value of a circular structure', assert => {
emitter.emit('testEnd', data.actualCircular);
assert.equal(spy.args[1][0], data.actualCircularTap);
test('output actual assertion value of a cyclical structure', assert => {
emitter.emit('testEnd', data.actualCyclical);
assert.equal(spy.args[1][0], data.actualCyclicalTap);
});

test('output actual assertion value of an acyclical structure', assert => {
emitter.emit('testEnd', data.actualDuplicateAcyclic);
assert.equal(spy.args[1][0], data.actualDuplicateAcyclicTap);
});

test('output expected assertion of undefined', assert => {
Expand Down

0 comments on commit fc2860a

Please sign in to comment.