UNPKG

deep-copy-diagnostics

Version:

Ultra-fast definitive testing for deep copies and deep copy algorithms

363 lines (320 loc) 15.1 kB
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] })();