deep-copy-diagnostics
Version:
Ultra-fast definitive testing for deep copies and deep copy algorithms
363 lines (320 loc) • 15.1 kB
JavaScript
const {dtype$} = require('type-quickly');
(function()
{
let wmX
let wmY
const stopPropagation = true
let primitivePrototypes
let checkPropertyDescriptors
module.exports.deepEquivalent = deepEquivalent
function deepEquivalent(x, y, primitiveClasses)
{
wmX = new WeakMap()
wmY = new WeakMap()
primitivePrototypes = null
checkPropertyDescriptors = true
if(primitiveClasses) {
primitivePrototypes = new Set()
for (const klass of primitiveClasses.values())
primitivePrototypes.add(klass.prototype)
}
deepEquivalent.message = 'Source and target are deeply equivalent'
return deepEquivalent$(x,y,'')
}
function failure(x, y, path, errorNumber, message)
{
deepEquivalent.pair = {source:x, target:y};
if(path === '') {
deepEquivalent.message = `Error #${errorNumber}
Error-path = top,
source-dtype = ${dtype$(x)}, target-dtype = ${dtype$(y)}
${message}`
}
else deepEquivalent.message = ` Error# ${errorNumber}
Error-path = ${path.substring(1)},
source-dtype = ${dtype$(x)}, target-dtype = ${dtype$(y)}
${message}`
return false
}
function getterSetterBody(func)
{
let n = 0;
const string = func.toString();
while(string[++n] !== '{');
return string.substring(n);
}
function deepEquivalent$(x,y,path)
{
if(x === null || y === null) return x === y
const tx = typeof x
if(tx !== 'function' && tx !== 'object')
{
if(x !== x)
{
if(y===y) return failure(x,y,path,1, `source is NaN but target is not`)
}
else if(x === 0 && !(y === 0 & 1/x === 1/y))
return failure(x,y,path,2, 'source is -0, 0, or +0 but target is not equal to it')
else if(x !== y) return failure(x,y,path,3,
`source is ${tx==='symbol'?'symbol':x}, but target is not equal to it`)
return stopPropagation
}
const ty = typeof y
if(ty !== 'function' && ty !== 'object')
{
if(y !== y)
{
if(x===x) return failure(x,y,path,4, `target is NaN but target is not`)
}
else if(y === 0 && !(x === 0 & 1/x === 1/y))
return failure(x,y,path,5, 'target is -0, 0, or +0 but source is not')
else if(x !== y) return failure(x,y,path,6,
`target is ${ty==='symbol'?'symbol':y}, but source is not equal to it`)
return stopPropagation
}
if(wmX.has(x))
{
const xSeenPath = wmX.get(x)
if(!wmY.has(y)) return failure(x,y,path,7,
`source${path} = source${xSeenPath} but target${path} != target${xSeenPath}`)
const ySeenPath = wmY.get(y)
if(xSeenPath !== ySeenPath) return failure(x,y,path,8,
`source${path} = source${xSeenPath} but target${path} = target${ySeenPath}`)
return stopPropagation
}
if(wmY.has(y))
{
const ySeenPath = wmY.get(y)
return failure(x,y,path,9,
`target${path} = target${ySeenPath} but source${path} != target${ySeenPath}`)
}
wmX.set(x, path)
wmY.set(y, path)
const protoX = Object.getPrototypeOf(x)
if(primitivePrototypes && primitivePrototypes.has(protoX)) return x === y
if(x === y)
{
return failure(x,y,path,10, "source and target are reference equal but shouldn't be")
}
const ntX = Object.prototype.toString.call(x).charAt(8)
const ntY = Object.prototype.toString.call(y).charAt(8)
if(ntX !== ntY) return failure(x,y,path,11,
'source and target have different native types')
const protoY = Object.getPrototypeOf(y)
if(protoX !== protoY) return failure(x,y,path,12, 'source and target have different internal prototypes')
if(ntX !== 'O' && protoX !== null && protoX[check] && !protoX[check](x,y,path)) return false
const xProps = Object.getOwnPropertyNames(x);
const yProps = Object.getOwnPropertyNames(y);
const xLength = xProps.length;
const yLength = yProps.length;
if(xLength !== yLength) return failure(x,y,path,13,
'source and target have a different number of properties')
for(const p of xProps)
{
if(!Object.prototype.hasOwnProperty.call(y, p)) return failure(x,y,path,14,
`source has property ${p} but target doesn't`)
if(!checkPropertyDescriptors)
{
if(!deepEquivalent$(x[p],y[p],path + '.' + p)) return false
break
}
const pdX = Object.getOwnPropertyDescriptor(x,p)
const pdY = Object.getOwnPropertyDescriptor(y,p)
if(!pdX.get && !pdX.set)
{
if(pdY.get || pdY.set) return failure(x,y,path,15,
`property ${p} a getter/setter in target but not in source`)
if(!deepEquivalent$(x[p],y[p],path + '.' + p)) return false
if(pdX.writable !== pdY.writable) return failure(x,y,path,16,
`property "${p}" writability in source is ${pdX.writable} but in target is ${pdY.writable}`)
}
if(pdX.get)
{
if(!pdY.get) return failure(x,y,path,17,
`property ${p} is a getter in source but not in target`)
if(getterSetterBody(pdX.get) !== getterSetterBody(pdY.get)) return failure(x,y,path,18,
`getter property ${p} has different bodies in source and target`)
}
if(pdX.set)
{
if(!pdY.set) return failure(x,y,path,19,
`property ${p} is a setter in source but not in target`)
if(getterSetterBody(pdX.set) !== getterSetterBody(pdY.set)) return failure(x,y,path,20,
`setter property ${p} has different bodies in source and target`)
}
if(pdX.enumerable !== pdY.enumerable) return failure(x,y,path,21,
`property ${p} enumerability in source is ${pdX.enumerable} but in target is ${pdY.enumerable}`)
if(pdX.configurable !== pdY.configurable) return failure(x,y,path,22,
`property ${p} configurability in source is ${pdX.configurable} but in target is ${pdY.configurable}`)
}
return true
}
const check = Symbol('check')
Boolean.prototype[check] = function(x,y,path)
{
if(x.valueOf() !== y.valueOf()) return failure(x,y,path,23,
`source ${x.constructor.name} is ${x.valueOf()} but target ${x.constructor.name} is ${y.valueOf()}`)
return true
}
Number.prototype[check] = Boolean.prototype[check]
String.prototype[check] = Boolean.prototype[check]
Date.prototype[check] = Boolean.prototype[check]
RegExp.prototype[check] = function(x,y,path)
{
if(x.toString() !== y.toString()) return failure(x,y,path,24,
`source RegExp is ${x.toString()} but target RegExp is ${y.toString()}`)
return true
}
Function.prototype[check] = function(x,y,path)
{
if(x.toString() !== y.toString()) return failure(x,y,path,25,
`source and target Functions have different toStrings()`)
return true
}
WeakSet.prototype[check] = function(x,y, path)
{
let xIsEdge = false
let yIsEdge = false
try{WeakSet.prototype.has.call(x,"value")}catch(e){xIsEdge = true}
try{WeakSet.prototype.has.call(y,"value")}catch(e){yIsEdge = true}
if(xIsEdge)
{
if(!yIsEdge) return failure(x,y,path,26, 'source is WeakSet[Object] but target is WeakSet')
if(x===y) return failure(x,y,path,27, // #27 preempted by #10
'source and target WeakSet[Object]s are reference equal')
return true
}
if(x !== y) return failure(x,y,path,28,`source is WeakSet but target not reference equal to it`)
return true
}
WeakMap.prototype[check] = function(x,y, path)
{
let xIsEdge = false
let yIsEdge = false
try{WeakMap.prototype.has.call(x,"key")}catch(e){xIsEdge = true}
try{WeakMap.prototype.has.call(y,"key")}catch(e){yIsEdge = true}
if(xIsEdge)
{
if(!yIsEdge) return failure(x,y,path,29, 'source is WeakMap[Object] but target is WeakMap')
if(x===y) return failure(x,y,path,30,
'source and target WeakMap[Object]s are reference equal')
return true
}
if(x !== y) return failure(x,y,path,31,`source is WeakMap but target not reference equal to it`)
return true
}
Set.prototype[check] = function(x,y,path)
{
let xIsEdge = false
let yIsEdge = false
try{Set.prototype.has.call(x,"key")}catch(e){xIsEdge = true}
try{Set.prototype.has.call(y,"key")}catch(e){yIsEdge = true}
if(xIsEdge)
{
if(!yIsEdge) return failure(x,y,path,32, 'source is Set[Object] but target is Set')
if(x===y) return failure(x,y,path,33,
'source and target Set[Object]s are reference equal')
return true
}
if(yIsEdge) return failure(x,y,path,34,`source and target d-types differ`)
const xKeyIterator = x.keys();
const yKeyIterator = y.keys();
let n, p, q;
for(n = 0, p = xKeyIterator.next(), q = yKeyIterator.next(); !p.done && !q.done;
p = xKeyIterator.next(), q = yKeyIterator.next(), n++)
{
if(!deepEquivalent$(p.value, q.value, `${path}[setKey[${n}]]`)) return false
}
if(!p.done) return failure(x,y,path,35,'source Set has more members than target Set')
if(!q.done) return failure(x,y,path,36, 'source Set has fewer members than target Set')
return true
}
Map.prototype[check] = function(x,y,path)
{
let xIsEdge = false
let yIsEdge = false
try{Map.prototype.has.call(x, {})}catch(e){xIsEdge = true}
try{Map.prototype.has.call(y,{})}catch(e){yIsEdge = true}
if(xIsEdge)
{
if(!yIsEdge) return failure(x,y,path,37, 'source is Map[Object] but target is Map')
if(x===y) return failure(x,y,path,38, 'source and target Map[Object]s are reference equal')
return true
}
if(yIsEdge) return failure(x,y,path,39,`source and target d-types differ`)
const xKeyIterator = x.keys();
const yKeyIterator = y.keys();
let n, p, q;
for(n = 0, p = xKeyIterator.next(), q = yKeyIterator.next(); !p.done && !q.done;
p = xKeyIterator.next(), q = yKeyIterator.next(), n++)
{
if(!deepEquivalent$(p.value, q.value, `${path}[mapKey[${n}]]`)) return false
if(!deepEquivalent$(x.get(p.value), y.get(q.value), `${path}[mapValue[${n}]]`)) return false
}
if(!p.done) return failure(x,y,path,40,'source Map has more key-value pairs than target Map')
if(!q.done) return failure(x,y,path,41, 'source Map has fewer key-value pairs than target Map')
return true
}
ArrayBuffer.prototype[check] = function(x,y,path)
{
let xIsEdge = false
let yIsEdge = false
try{ArrayBuffer.prototype.slice.call(x,0,0)}catch(e){xIsEdge = true}
try{ArrayBuffer.prototype.slice.call(y,0,0)}catch(e){yIsEdge = true}
if(xIsEdge)
{
if(!yIsEdge) return failure(x,y,path,42, 'source is ArrayBuffer[Object] but target is ArrayBuffer')
if(x===y) return failure(x,y,path,43,
'source and target ArrayBuffer[Object]s are reference equal')
return true
}
if(yIsEdge) return failure(x,y,path,44,`source and target d-types differ`)
if(x.byteLength !== y.byteLength) return failure(x,y,path,45,
`source.byteLength = ${x.byteLength} but target.byteLength = ${y.byteLength}`)
const xView = new Uint8Array(x)
const yView = new Uint8Array(y)
for(let i = 0; i < xView.byteLength; i++) {
if(xView[i] !== yView[i]) return failure(x,y,path,46,
`source[${i}] = ${xView[i]} but target[${i}] = ${yView[i]}`)
}
return true
}
DataView.prototype[check] = function(x,y,path)
{
let xIsEdge = false
let yIsEdge = false
try{DataView.prototype.getUint8.call(x,0)}catch(e){xIsEdge = true}
try{DataView.prototype.getUint8.call(y,0)}catch(e){yIsEdge = true}
if(xIsEdge)
{
if(!yIsEdge) return failure(x,y,path,47, 'source is DataView[Object] but target is DataView')
if(x===y) return failure(x,y,path,48,
'source and target DataView[Object]s are reference equal')
return true
}
if(yIsEdge) return failure(x,y,path,49,`source and target d-types differ`)
if(!deepEquivalent$(x.buffer, y.buffer, `${path}.buffer`)) return false
if(x.byteOffset !== y.byteOffset) return failure(x,y,path,50,
`source.byteOffset = ${x.byteOffset} but target.byteOffset = ${y.byteOffset}`)
if(x.byteLength !== y.byteLength) return failure(x,y,path,51,
`source.byteLength = ${x.byteLength} but target.byteLength = ${y.byteLength}`)
return true
}
Uint8Array.prototype[check] = function(x,y,path)
{
if(!deepEquivalent$(x.buffer, y.buffer, `${path}.buffer`)) return false
if(x.byteOffset !== y.byteOffset) return failure(x,y,path,50,
`source.byteOffset = ${x.byteOffset} but target.byteOffset = ${y.byteOffset}`)
if(x.byteLength !== y.byteLength) return failure(x,y,path,51,
`source.byteLength = ${x.byteLength} but target.byteLength = ${y.byteLength}`)
return true
}
Uint8ClampedArray.prototype[check] = Uint8Array.prototype[check]
Int8Array.prototype[check] = Uint8Array.prototype[check]
Uint16Array.prototype[check] = Uint8Array.prototype[check]
Int16Array.prototype[check] = Uint8Array.prototype[check]
Uint32Array.prototype[check] = Uint8Array.prototype[check]
Int32Array.prototype[check] = Uint8Array.prototype[check]
Float32Array.prototype[check] = Uint8Array.prototype[check]
Float64Array.prototype[check] = Uint8Array.prototype[check]
})();