deep-equal-diagnostics

Fully featured and robust diagnostic testing for deep equality and deep copies

Usage no npm install needed!

<script type="module">
  import deepEqualDiagnostics from 'https://cdn.skypack.dev/deep-equal-diagnostics';
</script>

README

Introduction to deep-equal-diagnostics

Diagnostics are provided for determining whether deep copy algorithms work correctly. In this regard, the built-in diagnostics assert.deepEqual() and assert.deepStrictEqual() fail on multiple fronts, as their specifications give ample reasons why they shouldn't be used. See the Problems with assert.deepEqual() section.

The deepEqual(x,y) and deepEqualSeparate(x,y) exports are the most feature rich tests you can find. They catch absolutely every possible way in which x and y may not be deep copies of each other. They take into account internal prototypes, null objects (*), circular references, duplicate references, functions, getters/setters, property descriptors, and frozen/sealed/extensible states. If they were intended to be fast, features would have been removed.

(*) Null objects are objects that do not derive from Object.prototype.

Exports

export description
deepEqual(x,y) tests for deep equality
deepEqualSeparate(x,y) tests for deep but separate equality. equivalent to deepEqual(x,y,true).

Deep but separate equality

The first export may take a third boolean parameter separate, as in deepEqual(x,y,separate). This parameter defaults to false, but if set to true then a test for deep but separate equality is performed: See the Definition of deep equality section. If x and y are deep copies of each other, then they are necessarily deeply but separately equal. Separateness means that corresponding elements in the trees of x and y can be the same only if they are primitives, WeakMaps, or WeakSets. Otherwise, corresponding objects can never be the same.

Installation and Usage

npm install deep-equal-diagnostics
const {deepEqual, deepEqualSeparate} = require('deep-equal-diagnostics');

if(deepEqual(x,y))
{   
    // statement that x and y are deeply equal
    console.log(deepEqual.message); 
    
    // x and y might not be deep copies of each other: 
    // for that test use deepEqualSeparate(x,y) instead.
}
else
{
    // reason why x and y are not deeply equal.
    console.log(deepEqual.message); 
}

if(deepEqualSeparate(x,y))
{
    // statement that x and y are deeply but separately equal
    console.log(deepEqual.message); 
    
    // x and y pass as being deep copies of each other
}
else
{
    // reason why x and y are not deeply but separately equal.
    console.log(deepEqual.message); 
}

Definition of deep equality

The elements x and y are deeply equal if the elements in their trees are in a one-one correspondence that meets the following conditions.

  1. x corresponds to y
  2. Corresponding elements must have the same type (*).
  3. Corresponding objects must have the same internal prototype.
  4. Corresponding primitives, WeakSets, and WeakMaps must have the same value.
  5. In the separate case, corresponding objects are never equal (except for WeakSets and WeakMaps).
  6. Corresponding objects must have the same frozen/sealed/extensible states.
  7. If p and q are corresponding objects
    1. if either p.a or q.a exist then they correspond, and moreover, the property descriptors must be the same.
    2. if p is a Set (we know by 2 that q is a Set) then insertion order defines the one-one correspondence between their members.
    3. if p is a Map (we know by 2 that q is a Map) then insertion order defines the one-one correspondences between their keys and their values.

You could obtain a better theoretical definition, by saying that insertion order does not matter for Sets and Maps. However, the stated definition is the practical one. For example, when copying a Set, you will read its members in insertion order, and then copy them in insertion order: You certainly won't jumble the insertion order up. So a deep equal test for determining whether x and y are deep copies will test the members/keys/values of Sets and Maps in insertion order.

(*) As determined by the dtype(x) function of the type-robustly package.

Testing

In the folder node_modules/deep-equal-diagnostics there is a tests folder with a single test.js file.

Problems with assert.deepEqual(x,y)

The following enumerated features are by design. They may have their place, but they have no place for testing deep copies: If x and y pass deep equality by this method, you have no idea as to whether x and y are truly deep copies of each other. The mistakes one might make in deep copying get a pass by assert.deepEqual(x,y) and assert.deepStrictEqual(x,y).

  1. Doesn't distinguish between getter and value properties.
    1. x = {a:1} and y = {get a(){return 1;} are reported as deep equals.
  2. Doesn't handle circular references.
    1. The algorithm tests for circular references and stops if found. The algorithm could just as easily keep going.
  3. Doesn't test internal prototypes(*).
  4. Doesn't test for separateness, which is a necessity in deep copies.

(*) Since internal prototypes are not tested, assert.deepEqual(x,y) is a misnomer and should instead be called something like assert.deeplyParallel(x,y).

Documentation of assert.deepEqual(x,y) incorrectly states that Map keys and Set items are compared unordered. That this is in fact an error of official documentation is proven by the following code.

try
{
    const x = new Set([{a:1},{b:2}]);
    const y = new Set([{b:1},{a:2}]);
    assert.deepStrictEqual(x,y);
    
    // The following line does not get executed
    
    console.log('x and y are deeply equal'); 
    
    // It would have been executed if insertion 
    // order into the Set was the same. So
    // comparison is in fact ordered, which 
    // contradicts the documentation.
}
catch(e)
{
    console.log(e.message); // gets executed
}

The larger picture and library

Also see the ReadMe.md files of the type-robustly, and deep-copy-system packages.

  • The type-robustly package (is used by all following packages)
    • The type(x) function is its main export.
  • The deep-copy-system package
    • aphelion = deep copy programmer defined ES5 and ES6 classes.
  • The serialize-nicely package
    • aphelion = serialize/deserialize programmer defined ES5 and ES6 classes.
  • The deep-equal-diagnostics package
    • Gives reason why two objects are not deep copies of each other or states that they are deep equals.
  • The deep-copy-quickly package is written for efficiency when the following
    don't matter
    • copying circular and duplicate references (very expensive)
    • copying property descriptors
    • copying frozen/sealed/extensible states

Robust deep copying means EVERYTHING IS COPIED: circular references, duplicate references, functions, getters/setters, property descriptors, frozen/sealed/extensible states. You can't achieve robust deep copying without a robust typing mechanism. Also, to test whether a robust deep copy algorithm succeeded, you need a robust deep equal test that again takes into account circular references, duplicate references, functions, getters/setters, property descriptors, and frozen/sealed/extensible states.

Version History

1.0.0 published 11-8-2021

1.0.1 published 11-8-2021

Forgot to add keywords in 1.0.0. So, did so in 1.0.1

1.0.2 published 11-9-2021

As a newbie have problems getting started. In the ReadMe.md file a header was repeated twice. May have accidentally left test code in deepEqualDiagnostics.js by mistake.