mudb
Version:
Real-time database for multiplayer games
892 lines (813 loc) • 26.6 kB
text/typescript
import * as tape from 'tape';
import {
MuWriteStream,
MuReadStream,
} from '../../stream';
import {
MuSchema,
MuBoolean,
MuASCII,
MuFixedASCII,
MuUTF8,
MuFloat32,
MuFloat64,
MuInt8,
MuInt16,
MuInt32,
MuUint8,
MuUint16,
MuUint32,
MuVarint,
MuRelativeVarint,
MuQuantizedFloat,
MuArray,
MuOption,
MuSortedArray,
MuStruct,
MuUnion,
MuBytes,
MuDictionary,
MuVector,
MuDate,
MuJSON,
} from '../index';
import { MuString, MuStringType } from '../_string';
import { MuNumber } from '../_number';
import {
randBool,
randFloat32,
randFloat64,
randInt8,
randInt16,
randInt32,
randUint8,
randUint16,
randUint32,
} from '../../util/random';
function createTest<T> (
t:tape.Test,
schema:MuSchema<T>,
) : (base:MuSchema<T>['identity'], target:MuSchema<T>['identity']) => void {
return (base, target) => {
const out = new MuWriteStream(1);
if (schema.diff(base, target, out)) {
t.notDeepEqual(base, target, 'diff() implied values are not identical');
t.true(out.offset > 0, 'at least one byte should be written to stream');
const inp = new MuReadStream(out.bytes());
t.deepEqual(schema.patch(base, inp), target, 'patched value should be identical to target');
t.equal(inp.offset, inp.length, 'patch() should consume all bytes on stream');
} else {
t.deepEqual(base, target, 'diff() implied values are identical');
t.equal(out.offset, 0, 'no bytes should be written to stream');
}
};
}
function randArray () {
const a = new Array(Math.random() * 10 | 0);
for (let i = 0; i < a.length; ++i) {
a[i] = randFloat32();
}
return a;
}
function randDict () {
const d = {};
let code = 97 + Math.random() * 6 | 0;
for (let i = Math.random() * 6 | 0; i > 0; --i) {
d[String.fromCharCode(code++)] = randFloat32();
}
return d;
}
const compare = (a, b) => a - b;
(<any>tape).onFailure(() => {
process.exit(1);
});
tape('de/serializing boolean', (t) => {
const bool = new MuBoolean();
const test = createTest(t, bool);
test(true, true);
test(false, false);
test(true, false);
test(false, true);
t.end();
});
tape('de/serializing string', (t) => {
function createTestPair (
t_:tape.Test,
schema:MuString<MuStringType>,
) : (a:string, b:string) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test('', a);
test('', b);
};
}
t.test('ascii', (st) => {
const ascii = new MuASCII();
const testPair = createTestPair(st, ascii);
testPair('', ' ');
testPair('a', 'b');
testPair('a', '<a href="https://github.com/mikolalysenko/mudb/">mudb</a>');
const codePoints = new Array(0x80);
for (let i = 0; i < codePoints.length; ++i) {
codePoints[i] = i;
}
testPair(
String.fromCharCode.apply(null, codePoints),
String.fromCharCode.apply(null, codePoints.reverse()),
);
st.end();
});
t.test('fixed-ascii', (st) => {
const fixedAscii = new MuFixedASCII(32);
const test = createTest(st, fixedAscii);
test('https://github.com/mikolalysenko', 'https://github.com/mikolalysenko');
test('e42dfecf821ebdfce692c7692b18d2b1', 'https://github.com/mikolalysenko');
test('https://github.com/mikolalysenko', 'e42dfecf821ebdfce692c7692b18d2b1');
st.end();
});
t.test('utf8', (st) => {
const utf8 = new MuUTF8();
const testPair = createTestPair(st, utf8);
testPair('', ' ');
testPair('<a href="https://github.com/mikolalysenko/mudb/">mudb</a>', 'Iñtërnâtiônàlizætiøn☃💩');
testPair('<a href="https://github.com/mikolalysenko/mudb/">mudb</a>', '💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩');
let bigText = '啊啊啊';
for (let i = 0; i < 16; ++i) {
bigText += bigText;
}
testPair('<a href="https://github.com/mikolalysenko/mudb/">mudb</a>', bigText);
st.end();
});
t.end();
});
tape('de/serializing number', (t) => {
function createTestPair (
t_:tape.Test,
schema:MuNumber<any>,
) : (a:number, b:number) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
};
}
const testFloat32 = createTestPair(t, new MuFloat32());
testFloat32(-3.4028234663852886e+38, 3.4028234663852886e+38);
const testFloat64 = createTestPair(t, new MuFloat64());
testFloat64(-1.7976931348623157e+308, 1.7976931348623157e+308);
const testInt8 = createTestPair(t, new MuInt8());
testInt8(-0x80, 0x7f);
const testInt16 = createTestPair(t, new MuInt16());
testInt16(-0x8000, 0x7fff);
const testInt32 = createTestPair(t, new MuInt32());
testInt32(-0x80000000, 0x7fffffff);
const testUint8 = createTestPair(t, new MuUint8());
testUint8(0, 0xff);
const testUint16 = createTestPair(t, new MuUint16());
testUint16(0, 0xffff);
const testUint32 = createTestPair(t, new MuUint32());
testUint32(0, 0xffffffff);
t.end();
});
tape('de/serializing varint', (t) => {
const sample = [ 1, 64, 128, 256, 1 << 14, 1 << 21, 1 << 28, 1 << 31 >>> 0 ];
for (let i = sample.length - 1; i >= 0; --i) {
const x = sample[i];
sample.push(x - 1);
sample.push(x + 1);
sample.push(x + (x * Math.random() | 0) >>> 0);
}
sample.push(0xffffffff);
const testVarint = createTest(t, new MuVarint());
for (let i = 0; i < sample.length; ++i) {
const x = sample[i];
testVarint(0, x);
testVarint(x, x);
}
t.end();
});
tape('de/serializing rvarint', (t) => {
const sample:number[] = [];
for (let i = 0; i < 30; ++i) {
sample.push(1 << i);
sample.push(-1 << i);
}
for (let i = sample.length - 1; i >= 0; --i) {
const x = sample[i];
sample.push(x - 1);
sample.push(x + 1);
sample.push(x + (x * Math.random() | 0) >> 0);
}
sample.push(1 << 31);
const testRelativeVarint = createTest(t, new MuRelativeVarint());
for (let i = 0; i < sample.length; ++i) {
const x = sample[i];
testRelativeVarint(0, x);
testRelativeVarint(x, x);
}
testRelativeVarint(0x80000000, 0);
testRelativeVarint(0x80000001, 1);
testRelativeVarint(0x100000000, 0x80000000);
t.end();
});
tape('de/serializing array', (t) => {
function createTestPair (
t_:tape.Test,
schema:MuArray<any>,
) : (a:any[], b:any[]) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test([], a);
test([], b);
};
}
function randNestedArray () {
const na = new Array(Math.random() * 10 | 0);
for (let i = 0; i < na.length; ++i) {
na[i] = randArray();
}
return na;
}
t.test('simple array', (st) => {
const array = new MuArray(new MuFloat32(), Infinity);
const testPair = createTestPair(st, array);
testPair([0], [1]);
testPair([0, 1], [1, 1]);
testPair([0, 1], [0, 2]);
testPair([0, 1], [0.5, 1.5]);
testPair([0], [0, 1]);
testPair([0, 1], [1, 2, 3]);
for (let i = 0; i < 1000; ++i) {
testPair(randArray(), randArray());
}
st.end();
});
t.test('nested array', (st) => {
const array = new MuArray(
new MuArray(new MuFloat32(), Infinity),
Infinity,
);
const testPair = createTestPair(st, array);
testPair([[]], [[], []]);
for (let i = 0; i < 1000; ++i) {
testPair(randNestedArray(), randNestedArray());
}
st.end();
});
t.end();
});
tape('de/serializing sorted array', (t) => {
function createTestPair<T, Schema extends MuSchema<T>> (
t_:tape.Test,
schema:MuSortedArray<Schema>,
) : (a:T[], b:T[]) => void {
const test = createTest(t_, schema);
return (a, b) => {
a.sort(compare);
b.sort(compare);
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test([], a);
test([], b);
test(a, []);
test(b, []);
};
}
const sortedArray = new MuSortedArray(new MuFloat32(), Infinity);
const testPair = createTestPair(t, sortedArray);
testPair([], []);
testPair([0], [1]);
testPair([0, 1], [1, 1]);
testPair([0, 1], [0, 2]);
testPair([0, 1], [0.5, 1.5]);
testPair([0], [0, 1]);
testPair([0, 1], [1, 2, 3]);
for (let i = 0; i < 1000; ++i) {
testPair(randArray(), randArray());
}
const structSchema = new MuStruct({
id: new MuUint32(0),
garbage: new MuArray(new MuFloat64(10), Infinity),
poop: new MuFloat32(0),
});
function randomStruct () {
const s = structSchema.alloc();
s.id = (Math.random() * 1000) | 0;
s.garbage.length = 0;
const ngarbage = Math.random() * 10;
for (let i = 0; i < ngarbage; ++i) {
s.garbage.push(Math.random());
}
s.poop = (Math.random() * 10) | 0;
return s;
}
const arraySchema = new MuSortedArray(structSchema, Infinity, (a, b) => a.id - b.id);
function randomArray () {
const x = arraySchema.alloc();
const n = (Math.random() * 100) | 0;
for (let i = 0; i < n; ++i) {
x.push(randomStruct());
}
x.sort(arraySchema.compare);
return x;
}
const testStruct = createTestPair(t, arraySchema);
testStruct([], []);
for (let i = 0; i < 100; ++i) {
const x = randomArray();
const y = randomArray();
testStruct(x, y);
arraySchema.assign(x, []);
arraySchema.free(y);
}
t.end();
});
tape('de/serializing struct', (t) => {
function createTestPair<T extends {[prop:string]:MuSchema<any>}> (
t_:tape.Test,
schema:MuStruct<T>,
) : (a:MuStruct<T>['identity'], b:MuStruct<T>['identity']) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test(schema.alloc(), a);
test(schema.alloc(), b);
};
}
const struct = new MuStruct({
struct: new MuStruct({
ascii: new MuASCII(),
fixed: new MuFixedASCII(8),
utf8: new MuUTF8(),
bool: new MuBoolean(),
float32: new MuFloat32(),
float64: new MuFloat64(),
int8: new MuInt8(),
int16: new MuInt16(),
int32: new MuInt32(),
uint8: new MuUint8(),
uint16: new MuUint16(),
uint32: new MuUint32(),
varint: new MuVarint(),
rvarint: new MuRelativeVarint(),
array: new MuArray(new MuFloat32(), Infinity),
sorted: new MuSortedArray(new MuFloat32(), Infinity),
dict: new MuDictionary(new MuFloat32(), Infinity),
vector: new MuVector(new MuFloat32(), 3),
}),
});
const strings = [
' ',
'<a href="https://github.com/mikolalysenko/mudb/">mudb</a>',
'Iñtërnâtiônàlizætiøn☃💩',
];
function createStruct () {
const s = struct.alloc();
s.struct.ascii = Math.random().toString(36);
s.struct.fixed = Math.random().toString(36).substring(2, 10);
s.struct.utf8 = strings[Math.random() * 3 | 0];
s.struct.bool = randBool();
s.struct.float32 = randFloat32();
s.struct.float64 = randFloat64();
s.struct.int8 = randInt8();
s.struct.int16 = randInt16();
s.struct.int32 = randInt32();
s.struct.uint8 = randUint8();
s.struct.uint16 = randUint16();
s.struct.uint32 = randUint32();
s.struct.varint = randUint32();
s.struct.rvarint = randInt16();
s.struct.array = randArray();
s.struct.sorted = randArray().sort(compare);
s.struct.dict = randDict();
s.struct.vector = randVec(3);
return s;
}
const testPair = createTestPair(t, struct);
for (let i = 0; i < 2000; ++i) {
testPair(createStruct(), createStruct());
}
t.end();
});
tape('struct of quantized floats', (t) => {
function createTest_<T> (
t_:tape.Test,
schema:MuSchema<T>,
) : (base:MuSchema<T>['identity'], target:MuSchema<T>['identity']) => void {
return (base, target) => {
const out = new MuWriteStream(1);
if (schema.diff(base, target, out)) {
t_.notDeepEqual(base, target, 'diff() implied values are not identical');
t_.true(out.offset > 0, 'at least one byte should be written to stream');
const inp = new MuReadStream(out.bytes());
t_.true(schema.equal(schema.patch(base, inp), target), 'patched value should be identical to target');
t_.equal(inp.offset, inp.length, 'patch() should consume all bytes on stream');
} else {
t_.deepEqual(base, target, 'diff() implied values are identical');
t_.equal(out.offset, 0, 'no bytes should be written to stream');
}
};
}
function createTestPair<T extends {[prop:string]:MuSchema<any>}> (
t_:tape.Test,
schema:MuStruct<T>,
) : (a:MuStruct<T>['identity'], b:MuStruct<T>['identity']) => void {
const test = createTest_(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test(schema.alloc(), a);
test(schema.alloc(), b);
};
}
function createStruct (schema:MuStruct<any>) {
const s = schema.alloc();
Object.keys(s).forEach((k) => {
s[k] = schema.muData[k].clone(randFloat32());
});
return s;
}
const structSchema = new MuStruct({
lowPrecision: new MuQuantizedFloat(1 / 16),
mediumPrecision: new MuQuantizedFloat(1 / 256),
highPrecision: new MuQuantizedFloat(1 / 4096),
});
const testPair = createTestPair(t, structSchema);
for (let i = 0; i < 200; i++) {
testPair(createStruct(structSchema), createStruct(structSchema));
}
t.end();
});
tape('de/serializing struct of booleans', (t) => {
function createTestPair<T extends {[prop:string]:MuSchema<any>}> (
t_:tape.Test,
schema:MuStruct<T>,
) : (a:MuStruct<T>['identity'], b:MuStruct<T>['identity']) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test(schema.alloc(), a);
test(schema.alloc(), b);
};
}
function createStruct (schema) {
const s = schema.alloc();
Object.keys(s).forEach((key) => {
s[key] = randBool();
});
return s;
}
const struct1 = new MuStruct({
a: new MuBoolean(),
});
const struct2 = new MuStruct({
a: new MuBoolean(),
b: new MuBoolean(),
});
const struct8 = new MuStruct({
a: new MuBoolean(),
b: new MuBoolean(),
c: new MuBoolean(),
d: new MuBoolean(),
e: new MuBoolean(),
f: new MuBoolean(),
g: new MuBoolean(),
h: new MuBoolean(),
});
const struct9 = new MuStruct({
a: new MuBoolean(),
b: new MuBoolean(),
c: new MuBoolean(),
d: new MuBoolean(),
e: new MuBoolean(),
f: new MuBoolean(),
g: new MuBoolean(),
h: new MuBoolean(),
i: new MuBoolean(),
});
const shape:{[key:string]:MuBoolean} = {};
for (let i = 0; i < 1000; ++i) {
shape[i] = new MuBoolean();
}
const struct = new MuStruct(shape);
const testPair1 = createTestPair(t, struct1);
testPair1({ a: true }, { a: false });
const testPair2 = createTestPair(t, struct2);
testPair2({ a: false, b: true }, { a: true, b: false });
testPair2({ a: false, b: true }, { a: true, b: true });
testPair2({ a: true, b: false }, { a: true, b: true });
const testPair8 = createTestPair(t, struct8);
for (let i = 0; i < 1000; ++i) {
testPair8(createStruct(struct8), createStruct(struct8));
}
const testPair9 = createTestPair(t, struct9);
for (let i = 0; i < 1000; ++i) {
testPair9(createStruct(struct9), createStruct(struct9));
}
const testPair = createTestPair(t, struct);
for (let i = 0; i < 1000; ++i) {
testPair(createStruct(struct), createStruct(struct));
}
t.end();
});
tape('de/serializing union', (t) => {
function createTestPair<T extends {[prop:string]:MuSchema<any>}> (
t_:tape.Test,
schema:MuUnion<T>,
) : (a:MuUnion<T>['identity'], b:MuUnion<T>['identity']) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
};
}
const spec = {
b: new MuBoolean(),
u: new MuUTF8(),
f: new MuFloat32(),
a: new MuArray(new MuFloat32(), Infinity),
sa: new MuSortedArray(new MuFloat32(), Infinity),
v: new MuVector(new MuFloat32(), 16),
d: new MuDictionary(new MuFloat32(), Infinity),
};
const tags = Object.keys(spec) as (keyof typeof spec)[];
const numTags = tags.length;
const strings = [
'',
'<a href="https://github.com/mikolalysenko/mudb/">mudb</a>',
'Iñtërnâtiônàlizætiøn☃💩',
];
function randUnionCase () {
const type = tags[Math.random() * numTags | 0];
let data;
switch (type) {
case 'b':
data = randBool();
break;
case 'u':
data = strings[Math.random() * strings.length | 0];
break;
case 'f':
data = randFloat32();
break;
case 'a':
data = randArray();
break;
case 'sa':
data = randArray().sort(compare);
break;
case 'v':
data = randVec(16);
break;
case 'd':
data = randDict();
break;
}
return {
type,
data,
};
}
const union = new MuUnion(spec);
const testPair = createTestPair(t, union);
for (let i = 0; i < 1000; ++i) {
testPair(randUnionCase(), randUnionCase());
}
t.end();
});
tape('de/serializing bytes', (t) => {
function createTestPair (
t_:tape.Test,
schema:MuBytes,
) : (a:Uint8Array, b:Uint8Array) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, b);
test(b, a);
};
}
function randUint8Array () {
const a = new Uint8Array(Math.ceil(Math.random() * 100));
for (let i = 0; i < a.length; ++i) {
a[i] = randUint8();
}
return a;
}
const bytes = new MuBytes();
const testPair = createTestPair(t, bytes);
testPair(new Uint8Array([]), randUint8Array());
for (let i = 0; i < 1000; ++i) {
const a = randUint8Array();
const b = randUint8Array();
if (!bytes.equal(a, b)) {
testPair(a, b);
}
}
t.end();
});
tape('de/serializing dictionary', (t) => {
function createTestPair<T extends MuSchema<any>> (
t_:tape.Test,
schema:MuDictionary<T>,
) : (a:MuDictionary<T>['identity'], b:MuDictionary<T>['identity']) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test({}, a);
test({}, b);
};
}
function randNestedDict () {
const nd = {};
let code = 97 + Math.random() * 6 | 0;
for (let i = Math.random() * 6 | 0; i > 0; --i) {
nd[String.fromCharCode(code++)] = randDict();
}
return nd;
}
t.test('simple dictionary', (st) => {
const dictionary = new MuDictionary(new MuFloat32(), Infinity);
const testPair = createTestPair(st, dictionary);
testPair({f: 0}, {f: 0.5});
testPair({f: 0, g: 0.5}, {f: 0, g: 1});
testPair({f: 0, g: 0.5}, {f: 1, g: 0.5});
testPair({f: 0, g: 0.5}, {f: 1, g: 1.5});
testPair({f: 0}, {g: 0});
testPair({f: 0}, {g: 0.5});
testPair({f: 0, g: 0.5}, {g: 1, h: 1.5});
testPair({f: 0, g: 0.5}, {h: 1, i: 1.5});
testPair({f: 0}, {f: 0, g: 0.5});
testPair({f: 0}, {f: 0.5, g: 1});
for (let i = 0; i < 1000; ++i) {
testPair(randDict(), randDict());
}
st.end();
});
t.test('nested dictionary', (st) => {
const dictionary = new MuDictionary(
new MuDictionary(new MuFloat32(), Infinity),
Infinity,
);
const testPair = createTestPair(st, dictionary);
testPair({a: {a: 0}}, {a: {b: 0.5}});
testPair({a: {a: 0}, b: {a: 0}}, {a: {a: 0}, b: {b: 0.5}});
testPair({a: {a: 0}, b: {a: 0}}, {a: {b: 0.5}, b: {a: 0}});
testPair({a: {a: 0}, b: {a: 0}}, {a: {b: 0.5}, b: {b: 0.5}});
testPair({a: {a: 0}}, {b: {a: 0}});
testPair({a: {a: 0}}, {b: {b: 0}});
testPair({a: {a: 0}, b: {a: 0}}, {b: {b: 0.5}, c: {a: 0}});
testPair({a: {a: 0}, b: {a: 0}}, {c: {a: 0}, d: {a: 0}});
testPair({a: {a: 0}}, {a: {b: 0.5}, b: {a: 0}});
testPair({a: {a: 0}}, {b: {a: 0.5}, c: {a: 0.5}});
for (let i = 0; i < 1000; ++i) {
testPair(randNestedDict(), randNestedDict());
}
st.end();
});
t.end();
});
function randVec<D extends number> (dimension:D) : MuVector<'float32', D>['identity'] {
const v = new MuVector(new MuFloat32(), dimension).alloc();
for (let i = 0; i < v.length; ++i) {
v[i] = randFloat32();
}
return v;
}
tape('de/serializing vector', (t) => {
function createTestPair<Schema extends MuVector<any, number>> (
t_:tape.Test,
schema:Schema,
) : (a:Schema['identity'], b:Schema['identity']) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, a);
test(b, b);
test(a, b);
test(b, a);
test(schema.alloc(), a);
test(schema.alloc(), b);
};
}
t.test('vec0', (st) => {
const vector = new MuVector(new MuFloat32(), 0);
const test = createTest(st, vector);
const zeroA = vector.alloc();
const zeroB = vector.alloc();
test(zeroA, zeroB);
st.end();
});
t.test('vec1', (st) => {
const vector = new MuVector(new MuFloat32(), 1);
const testPair = createTestPair(st, vector);
for (let i = 0; i < 10; ++i) {
testPair(randVec(1), randVec(1));
}
st.end();
});
t.test('vec2', (st) => {
const vector = new MuVector(new MuFloat32(), 2);
const testPair = createTestPair(st, vector);
for (let i = 0; i < 100; ++i) {
testPair(randVec(2), randVec(2));
}
st.end();
});
t.test('vec3', (st) => {
const vector = new MuVector(new MuFloat32(), 3);
const testPair = createTestPair(st, vector);
for (let i = 0; i < 1000; ++i) {
testPair(randVec(3), randVec(3));
}
st.end();
});
t.test('vec10000', (st) => {
const vector = new MuVector(new MuFloat32(), 10000);
const testPair = createTestPair(st, vector);
for (let i = 0; i < 10; ++i) {
testPair(randVec(10000), randVec(10000));
}
st.end();
});
t.end();
});
tape('de/serializing date', (t) => {
const date = new MuDate();
const test = createTest(t, date);
const d1 = date.alloc();
const d2 = date.alloc();
d2.setTime(0);
test(d1, d1);
test(d2, d2);
test(d1, d2);
test(d2, d1);
t.end();
});
tape('de/serializing json', (t) => {
function createTestPair (
t_:tape.Test,
schema:MuJSON,
) : (a:object, b:object) => void {
const test = createTest(t_, schema);
return (a, b) => {
test(a, b);
test(b, a);
};
}
const json = new MuJSON();
const testPair = createTestPair(t, json);
testPair({}, {a:0.5, b:false, c:'', d:[]});
testPair([], [1e9, true, 'Iñtërnâtiônàlizætiøn☃💩', {}]);
t.end();
});
tape('de/serializing option', (t) => {
const op = new MuOption(new MuFloat32()); // optional primitive
const of = new MuOption(new MuStruct({op: op})); // optional functor
const testPrimitive = createTest(t, op);
testPrimitive(undefined, undefined);
testPrimitive(undefined, 4);
testPrimitive(4, undefined);
testPrimitive(undefined, 0);
testPrimitive(0, undefined);
testPrimitive(4, 4);
testPrimitive(3, 4);
const testFunctor = createTest(t, of);
testFunctor(undefined, undefined);
testFunctor({op: undefined}, undefined);
testFunctor(undefined, {op: undefined});
testFunctor({op: 4}, undefined);
testFunctor(undefined, {op: 4});
testFunctor({op: 4}, {op: 3});
testFunctor({op: 4}, {op: 4});
testFunctor({op: 0}, {op: 4});
testFunctor({op: 4}, {op: 0});
testFunctor({op: undefined}, {op: 0});
testFunctor({op: 0}, {op: undefined});
testFunctor(undefined, {op: 0});
testFunctor({op: 0}, undefined);
t.end();
});