'use strict';
const { addCondition, explain } = require( '../report.js' );
/**
* @instance
* @memberOf conditions
* @method deepEqual
* @desc Compares two structures, outputs diff if differences found.
* @param {any} actual First structure
* @param {any} expected Structure to compare to
* @param {Object} [options]
* @param {number} options.max how many differences to output (default 5)
* @param {string} [description]
* @returns {undefined}
*/
addCondition( 'deepEqual',
{ args: 2, hasOptions: true }, deep );
function deep ( got, exp, options = {} ) {
if (!options.max)
options.max = 5;
options.diff = [];
_deep( got, exp, options );
if (!options.diff.length)
return 0;
const ret = [];
for (const item of options.diff) {
ret.push(
'at ' + item[0],
'- ' + (item[3] ? item[1] : explain( item[1], { depth: 2 } )),
'+ ' + (item[3] ? item[2] : explain( item[2], { depth: 2 } )),
);
}
return ret;
}
// result is stored in options.diff=[], return value is ignored
// if said diff exceeds max, return immediately & don't waste time
function _deep ( got, exp, options = {}, path = '$', seenL = new Map(), seenR = new Map() ) {
if (got === exp || options.max <= options.diff.length)
return;
if (typeof got !== typeof exp)
return options.diff.push( [path, got, exp] );
// recurse by expected value - consider it more predictable
if (typeof exp !== 'object' || exp === null ) {
// non-objects - so can't descend
// and comparison already done at the beginnning
return options.diff.push( [path, got, exp] );
}
// must detect loops before going down
const pathL = seenL.get(got);
const pathR = seenR.get(exp);
if (pathL || pathR) {
// Loop detected = only check topology
if (pathL === pathR)
return;
return options.diff.push( [
path + ' (circular)',
pathL ? 'Circular=' + pathL : explain(got, { depth: 2 }),
pathR ? 'Circular=' + pathR : explain(exp, { depth: 2 }),
true // don't stringify
]);
}
try {
seenL.set(got, path);
seenR.set(exp, path);
// compare object types
// (if a user is stupid enough to override constructor field, well the test
// would fail later anyway)
if (got.constructor !== exp.constructor)
return options.diff.push( [path, got, exp] );
// array
if (Array.isArray(exp)) {
if (!Array.isArray(got) || got.length !== exp.length)
return options.diff.push( [path, got, exp] );
for (let i = 0; i < exp.length; i++) {
_deep( got[i], exp[i], options, extendPath(path, i), seenL, seenR );
if (options.max <= options.diff.length)
break;
}
return;
}
// compare keys - +1 for exp, -1 for got, nonzero key at end means keys differ
// TODO better, faster way to do it?
const uniq = {};
Object.keys(exp).forEach( x => { uniq[x] = 1 } );
Object.keys(got).forEach( x => { uniq[x] = (uniq[x] || 0) - 1 } );
for (const x in uniq) {
if (uniq[x] !== 0)
return options.diff.push( [path, got, exp] );
}
// now typeof, object type, and object keys are the same.
// recurse.
for (const i in exp) {
_deep( got[i], exp[i], options, extendPath(path, i), seenL, seenR );
if (options.max <= options.diff.length)
break;
}
} finally {
seenL.delete(got);
seenR.delete(exp);
}
}
function extendPath (path, suffix) {
// array
if ( typeof suffix === 'number' )
return path + '[' + suffix + ']';
//
if ( suffix.match(/^[a-z_][a-z_0-9]*$/i) )
return path + '.' + suffix;
return path + '[' + JSON.stringify(suffix) + ']';
}