From fc2860adc10b1d3d5963c81ba87c78aeaf0106a4 Mon Sep 17 00:00:00 2001 From: Zachary Mulgrew Date: Sun, 14 Feb 2021 20:26:38 +0000 Subject: [PATCH] Reporter: Handle acyclical duplicates Handles the case where an object is references more than once in an object graph but is acyclic. Also accounts for cyclical references. --- lib/reporters/TapReporter.js | 50 +++++++++++++++++++++++----------- test/fixtures/unit.js | 53 ++++++++++++++++++++++++++++++------ test/unit/tap-reporter.js | 11 ++++++-- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/lib/reporters/TapReporter.js b/lib/reporters/TapReporter.js index a7a575b..2957398 100644 --- a/lib/reporters/TapReporter.js +++ b/lib/reporters/TapReporter.js @@ -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 { diff --git a/test/fixtures/unit.js b/test/fixtures/unit.js index b8b1337..088cf4e 100644 --- a/test/fixtures/unit.js +++ b/test/fixtures/unit.js @@ -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 = { @@ -203,7 +218,7 @@ module.exports = { actual : [] expected: expected ...`, - actualCircular: copyErrors({ + actualCyclical: copyErrors({ name: 'Failing', suiteName: undefined, fullName: ['Failing'], @@ -211,16 +226,38 @@ module.exports = { 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 ...`, @@ -259,7 +296,7 @@ module.exports = { errors: [{ passed: false, actual: 'actual', - expected: createCircular() + expected: createCyclical() }], assertions: null }), diff --git a/test/unit/tap-reporter.js b/test/unit/tap-reporter.js index 07fcaaa..b8d6679 100644 --- a/test/unit/tap-reporter.js +++ b/test/unit/tap-reporter.js @@ -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 => {