Skip to content

Commit

Permalink
improve test coverage for merge utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
sdumetz committed Jan 9, 2024
1 parent bf01ec0 commit ca8468a
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 126 deletions.
25 changes: 25 additions & 0 deletions source/server/utils/merge/apply.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

import {DELETE_KEY} from "./types.js";
import apply from "./apply.js";

describe("merge.apply()", function(){
it("merges a no-op", function(){
const a = {a:1};
expect(apply(a, {})).to.deep.equal(a);
});

it("merges a deleted key", function(){
expect(apply({a:1, b:2}, {b:DELETE_KEY})).to.deep.equal({a:1});
});

it("merges an updated key", function(){
expect(apply({a:1, b:2}, {b:3})).to.deep.equal({a:1, b:3});
});
it("merges nested objects", function(){
expect(apply({a:{b:1}, c:3}, {a:{b:2}})).to.deep.equal({a:{b:2}, c:3});
});
it("merges updated arrays", function(){
expect(apply({a:[1,2]}, {a:{0:1, 1:3}})).to.deep.equal({a:[1,3]});
});

});
78 changes: 78 additions & 0 deletions source/server/utils/merge/diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

import {DELETE_KEY} from "./types.js";

import diff from "./diff.js";

describe("merge.diff()", function(){
describe("handle native data types ", function(){
[ //All JSON-serializable data types with possible bounday values
1,
0,
"1",
"",
true,
false,
null,
{},
{a:1},
[],
].forEach(v=>{
const v_clone :typeof v = JSON.parse(JSON.stringify(v));
it(`${JSON.stringify(v)} deep equals itself`, function(){
expect(diff({v}, {v:v_clone})).to.deep.equal({});
});
it(`${JSON.stringify({v})} compares from {}`, function(){
expect(diff({}, {v})).to.deep.equal({v:v_clone});
});
it(`${JSON.stringify({v})} compares from {}`, function(){
expect(diff({v}, {})).to.deep.equal({v:DELETE_KEY});
});
});
it("special-case for null recursion", function(){
expect(diff({
v: {a:1},
}, {v: null}))
})
});

describe("arrays", function(){
//Arrays behave like objects with numeric keys, with some specific handling

it("handles an array equality", function(){
expect(diff({a:["foo"]}, {a:["foo"]})).to.deep.equal({});
});

it("handles nested objects equality", function(){
expect(diff({a:[{v: "foo"}]}, {a:[{v:"foo"}]})).to.deep.equal({});
});

it("handle array.push", function(){
expect(diff({a:[]}, {a:[1]})).to.deep.equal({a:{0:1}});
expect(diff({a:[1]}, {a:[1, 2]})).to.deep.equal({a:{1:2}});
});

it("handle Array.pop", function(){
expect(diff({a:[1]}, {a:[]})).to.deep.equal({a:{0: DELETE_KEY}});
expect(diff({a:[1, 2]}, {a:[1]})).to.deep.equal({a:{1: DELETE_KEY}});
});

it("handle Array.replace", function(){
expect(diff({a:["foo", "bar"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz"}});
});

it("handle Array.splice", function(){
// Might be improved to detect a splice and use special syntax to represent it
expect(diff({a:["foo", "bar", "baz"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz", 2: DELETE_KEY}});
});

it("handle deep changes", function(){
expect(diff({a:[{v:"foo"}, {v:"bar"}]}, {a:[{v:"foo"}, {v:"baz"}]})).to.deep.equal({a:{1:{v:"baz"}}});
});

it("throws when diffing an array with an object", function(){
expect(()=>diff({a:[]}, {a:{}})).to.throw("Can't diff an array with an object");
});


});
});
98 changes: 0 additions & 98 deletions source/server/utils/merge/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,104 +10,6 @@ import { IDocument, INode } from "../schema/document.js";
import {DELETE_KEY, apply, applyDoc, diff, diffDoc} from "./index.js";



describe("merge.diff()", function(){
describe("handle native data types ", function(){
[ //All JSON-serializable data types with possible bounday values
1,
0,
"1",
"",
true,
false,
null,
{},
{a:1},
[],
].forEach(v=>{
const v_clone :typeof v = JSON.parse(JSON.stringify(v));
it(`${JSON.stringify(v)} deep equals itself`, function(){
expect(diff({v}, {v:v_clone})).to.deep.equal({});
});
it(`${JSON.stringify({v})} compares from {}`, function(){
expect(diff({}, {v})).to.deep.equal({v:v_clone});
});
it(`${JSON.stringify({v})} compares from {}`, function(){
expect(diff({v}, {})).to.deep.equal({v:DELETE_KEY});
});
});
});

describe("arrays", function(){
//Arrays behave like objects with numeric keys, with some specific handling

it("handles an array equality", function(){
expect(diff({a:["foo"]}, {a:["foo"]})).to.deep.equal({});
});

it("handles nested objects equality", function(){
expect(diff({a:[{v: "foo"}]}, {a:[{v:"foo"}]})).to.deep.equal({});
});

it("handle array.push", function(){
expect(diff({a:[]}, {a:[1]})).to.deep.equal({a:{0:1}});
expect(diff({a:[1]}, {a:[1, 2]})).to.deep.equal({a:{1:2}});
});

it("handle Array.pop", function(){
expect(diff({a:[1]}, {a:[]})).to.deep.equal({a:{0: DELETE_KEY}});
expect(diff({a:[1, 2]}, {a:[1]})).to.deep.equal({a:{1: DELETE_KEY}});
});

it("handle Array.replace", function(){
expect(diff({a:["foo", "bar"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz"}});
});

it("handle Array.splice", function(){
// Might be improved to detect a splice and use special syntax to represent it
expect(diff({a:["foo", "bar", "baz"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz", 2: DELETE_KEY}});
});

it("handle deep changes", function(){
expect(diff({a:[{v:"foo"}, {v:"bar"}]}, {a:[{v:"foo"}, {v:"baz"}]})).to.deep.equal({a:{1:{v:"baz"}}});
});

it("throws when diffing an array with an object", function(){
expect(()=>diff({a:[]}, {a:{}})).to.throw("Can't diff an array with an object");
});


});
});


describe("merge.apply()", function(){
it("merges a no-op", function(){
const a = {a:1};
expect(apply(a, {})).to.deep.equal(a);
});

it("merges a deleted key", function(){
expect(apply({a:1, b:2}, {b:DELETE_KEY})).to.deep.equal({a:1});
});

it("merges an updated key", function(){
expect(apply({a:1, b:2}, {b:3})).to.deep.equal({a:1, b:3});
});
it("merges nested objects", function(){
expect(apply({a:{b:1}, c:3}, {a:{b:2}})).to.deep.equal({a:{b:2}, c:3});
});
it("merges updated arrays", function(){
expect(apply({a:[1,2]}, {a:{0:1, 1:3}})).to.deep.equal({a:[1,3]});
});

});

describe("merge.diffDoc()", function(){

})


describe("three-way merge", function(){
/*
* For consistency, tests should follow the following naming scheme:
Expand Down
83 changes: 83 additions & 0 deletions source/server/utils/merge/pointers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,62 @@ describe("(de)reference pointers", function(){
"nodes": [],
});
});

describe("throws", function(){
it("if doc has no scene", function(){
const doc = {...baseDoc, scenes: []};
expect(()=> toPointers(doc)).to.throw("Document has no valid scene");
});

it("if doc has an invalid scene index", function(){
const doc = {...baseDoc, scene: 1};
expect(()=> toPointers(doc as any)).to.throw("Document's scene #1 is invalid");
});

it("if scene has invalid node index", function(){
const doc = {...baseDoc, scenes:[{nodes: [1]}], nodes:[{name: "Camera"}]};
expect(()=> toPointers(doc as any)).to.throw(`Invalid node index 1 in scene #0`);
});

it("if node has invalid camera/light/model/meta index", function(){
"camera/light/model/meta".split("/").forEach(key=>{
const doc = {...baseDoc, scenes:[{nodes:[0]}], nodes:[{name: key.toUpperCase(), [key]: 0}]};
expect(()=> toPointers(doc as any)).to.throw(`Invalid ${key} index 0 in node #0`);
})
});

it("if scene has invalid setup index", function(){
const doc = {...baseDoc, scenes:[{setup: 0}]};
expect(()=> toPointers(doc as any)).to.throw(`Invalid setup #0 in scene #0`);
});

it("if scene has invalid meta index", function(){
const doc = {...baseDoc, scenes:[{meta: 0}]};
expect(()=> toPointers(doc as any)).to.throw(`Invalid meta #0 in scene #0`);
});
});
});

describe("fromPointers()", function(){

it("copies a scene's name and units", function(){
const deref = {asset: baseDoc.asset, scene:{
name: "My Scene",
units: "km",
}};
expect(fromPointers(deref as any).scene).to.deep.equal({
name: "My Scene",
units: "km"
});
});

it("auto-fills a scene's units", function(){
const deref = {asset: baseDoc.asset, scene:{
name: "My Scene",
}};
expect(fromPointers(deref as any).scene).to.have.property("units", "cm");
});

it("builds nodes reference arrays", function(){
const deref = {
asset: baseDoc.asset,
Expand Down Expand Up @@ -166,6 +218,37 @@ describe("(de)reference pointers", function(){
});
});

it("copies nodes matrix/translation/rotation/scale properties", function(){
const model = {
name: "Model",
model: {uri: "model.gltf"},
matrix:[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
translation:[0,0,0],
rotation: [0, 0, 0, 0],
scale: [1, 1, 1],
};

const deref = {
asset: baseDoc.asset,
scene: {
units: "m",
nodes:[model],
}
};

const doc:IDocument = fromPointers(deref as any);
//use JSON.parse(JSON.stringify()) to remove undefined values
expect(doc).to.deep.equal({
...baseDoc,
nodes: [
{name: "Model", model: 0},
],
models:[model],
scenes:[{ nodes: [0], units: "m" }],
});
});


it("builds metas from scene", function(){
const deref = {
asset: baseDoc.asset,
Expand Down
Loading

0 comments on commit ca8468a

Please sign in to comment.