Skip to content

Commit

Permalink
test(marshal): Refactor encodePassable tests and add failing inputs
Browse files Browse the repository at this point in the history
Ref #2588
  • Loading branch information
gibson042 committed Oct 19, 2024
1 parent 7a991aa commit de28fd8
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 114 deletions.
6 changes: 6 additions & 0 deletions packages/marshal/test/_marshal-test-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ export const unsortedSample = harden([
Symbol.for(''),
false,
exampleCarol,
[exampleCarol, 'm'],
[exampleAlice, 'a'],
[exampleBob, 'z'],
-0,
{},
[5, undefined],
Expand Down Expand Up @@ -350,6 +353,9 @@ export const sortedSample = harden([
[5, { foo: 4, bar: undefined }],
[5, null],
[5, undefined],
[exampleAlice, 'a'],
[exampleCarol, 'm'],
[exampleBob, 'z'],

false,
true,
Expand Down
235 changes: 121 additions & 114 deletions packages/marshal/test/encodePassable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,91 +5,67 @@ import test from '@endo/ses-ava/prepare-endo.js';
import { fc } from '@fast-check/ava';
import { Remotable } from '@endo/pass-style';
import { arbPassable } from '@endo/pass-style/tools.js';
import { Fail, q } from '@endo/errors';
import { assert, Fail, q, b } from '@endo/errors';

import {
makePassableKit,
makeEncodePassable,
makeDecodePassable,
} from '../src/encodePassable.js';
import { compareRank, makeComparatorKit } from '../src/rankOrder.js';
import { compareRank, makeFullOrderComparatorKit } from '../src/rankOrder.js';
import { unsortedSample } from './_marshal-test-data.js';

const buffers = {
__proto__: null,
r: [],
'?': [],
'!': [],
};
const resetBuffers = () => {
buffers.r = [];
buffers['?'] = [];
buffers['!'] = [];
};
const cursors = {
__proto__: null,
r: 0,
'?': 0,
'!': 0,
};
const resetCursors = () => {
cursors.r = 0;
cursors['?'] = 0;
cursors['!'] = 0;
};

const encodeThing = (prefix, r) => {
buffers[prefix].push(r);
// With this encoding, all things with the same prefix have the same rank
return prefix;
};

const decodeThing = (prefix, e) => {
prefix === e ||
Fail`expected encoding ${q(e)} to simply be the prefix ${q(prefix)}`;
(cursors[prefix] >= 0 && cursors[prefix] < buffers[prefix].length) ||
Fail`while decoding ${q(e)}, expected cursors[${q(prefix)}], i.e., ${q(
cursors[prefix],
)} <= ${q(buffers[prefix].length)}`;
const thing = buffers[prefix][cursors[prefix]];
cursors[prefix] += 1;
return thing;
};

const compareRemotables = (x, y) =>
compareRank(encodeThing('r', x), encodeThing('r', y));

const encodePassableInternal = makeEncodePassable({
encodeRemotable: r => encodeThing('r', r),
encodePromise: p => encodeThing('?', p),
encodeError: er => encodeThing('!', er),
});
const encodePassableInternal2 = makeEncodePassable({
encodeRemotable: r => encodeThing('r', r),
encodePromise: p => encodeThing('?', p),
encodeError: er => encodeThing('!', er),
format: 'compactOrdered',
});

const encodePassable = passable => {
resetBuffers();
return encodePassableInternal(passable);
};
const encodePassable2 = passable => {
resetBuffers();
return encodePassableInternal2(passable);
};
const statelessEncodePassableLegacy = makeEncodePassable();

const makeSimplePassableKit = ({ stateless = false } = {}) => {
let count = 0n;
const encodingFromVal = new Map();
const valFromEncoding = new Map();

const encodeSpecial = (prefix, val) => {
const foundEncoding = encodingFromVal.get(val);
if (foundEncoding) return foundEncoding;
count += 1n;
const template = statelessEncodePassableLegacy(count);
const encoding = prefix + template.substring(1);
encodingFromVal.set(val, encoding);
valFromEncoding.set(encoding, val);
return encoding;
};
const decodeSpecial = (prefix, e) => {
e.startsWith(prefix) ||
Fail`expected encoding ${q(e)} to start with ${b(prefix)}`;
const val =
valFromEncoding.get(e) || Fail`no object found while decoding ${q(e)}`;
return val;
};

const decodePassableInternal = makeDecodePassable({
decodeRemotable: e => decodeThing('r', e),
decodePromise: e => decodeThing('?', e),
decodeError: e => decodeThing('!', e),
});
const encoders = stateless
? {
encodeRemotable: r => 'r',
encodePromise: p => '?',
encodeError: err => '!',
}
: {
encodeRemotable: r => encodeSpecial('r', r),
encodePromise: p => encodeSpecial('?', p),
encodeError: err => encodeSpecial('!', err),
};
const encodePassableLegacy = makeEncodePassable({ ...encoders });
const encodePassableCompact = makeEncodePassable({
...encoders,
format: 'compactOrdered',
});
const decodePassable = makeDecodePassable({
decodeRemotable: e => decodeSpecial('r', e),
decodePromise: e => decodeSpecial('?', e),
decodeError: e => decodeSpecial('!', e),
});

const decodePassable = encoded => {
resetCursors();
return decodePassableInternal(encoded);
return { encodePassableLegacy, encodePassableCompact, decodePassable };
};
const pickLegacy = kit => kit.encodePassableLegacy;
const pickCompact = kit => kit.encodePassableCompact;

test('makePassableKit output shape', t => {
const kit = makePassableKit();
Expand Down Expand Up @@ -133,7 +109,7 @@ test(
(...args) => makePassableKit(...args).encodePassable,
);

const { comparator: compareFull } = makeComparatorKit(compareRemotables);
const { comparator: compareFull } = makeFullOrderComparatorKit();

const asNumber = new Float64Array(1);
const asBits = new BigUint64Array(asNumber.buffer);
Expand Down Expand Up @@ -171,6 +147,11 @@ const goldenPairs = harden([
]);

test('golden round trips', t => {
const {
encodePassableLegacy: encodePassable,
encodePassableCompact: encodePassable2,
decodePassable,
} = makeSimplePassableKit({ stateless: true });
for (const [k, e] of goldenPairs) {
t.is(encodePassable(k), e, 'does k encode as expected');
t.is(encodePassable2(k), `~${e}`, 'does k small-encode as expected');
Expand Down Expand Up @@ -258,13 +239,14 @@ test('capability encoding', t => {
});

test('compact string validity', t => {
t.notThrows(() => decodePassableInternal('~sa"z'));
t.notThrows(() => decodePassableInternal('~sa!!z'));
const { decodePassable } = makeSimplePassableKit({ stateless: true });
t.notThrows(() => decodePassable('~sa"z'));
t.notThrows(() => decodePassable('~sa!!z'));
const specialEscapes = ['!_', '!|', '_@', '__'];
for (const prefix of ['!', '_']) {
for (let cp = 0; cp <= 0x7f; cp += 1) {
const esc = `${prefix}${String.fromCodePoint(cp)}`;
const tryDecode = () => decodePassableInternal(`~sa${esc}z`);
const tryDecode = () => decodePassable(`~sa${esc}z`);
if (esc.match(/![!-@]/) || specialEscapes.includes(esc)) {
t.notThrows(tryDecode, `valid string escape: ${JSON.stringify(esc)}`);
} else {
Expand All @@ -276,7 +258,7 @@ test('compact string validity', t => {
}
}
t.throws(
() => decodePassableInternal(`~sa${prefix}`),
() => decodePassable(`~sa${prefix}`),
{ message: /invalid string escape/ },
`unterminated ${JSON.stringify(prefix)} escape`,
);
Expand All @@ -285,14 +267,15 @@ test('compact string validity', t => {
const ch = String.fromCodePoint(cp);
const uCode = cp.toString(16).padStart(4, '0').toUpperCase();
t.throws(
() => decodePassableInternal(`~sa${ch}z`),
() => decodePassable(`~sa${ch}z`),
{ message: /invalid string escape/ },
`disallowed string control character: U+${uCode} ${JSON.stringify(ch)}`,
);
}
});

test('compact custom encoding validity constraints', t => {
const { encodePassableCompact } = makeSimplePassableKit();
const encodings = new Map();
const dynamicEncoder = obj => encodings.get(obj);
const dynamicEncodePassable = makeEncodePassable({
Expand Down Expand Up @@ -332,11 +315,11 @@ test('compact custom encoding validity constraints', t => {
'custom encoding containing an invalid string escape is acceptable',
);
t.notThrows(
makeTryEncode(`${sigil}${encodePassableInternal2(harden([]))}`),
makeTryEncode(`${sigil}${encodePassableCompact(harden([]))}`),
'custom encoding containing an empty array is acceptable',
);
t.notThrows(
makeTryEncode(`${sigil}${encodePassableInternal2(harden(['foo', []]))}`),
makeTryEncode(`${sigil}${encodePassableCompact(harden(['foo', []]))}`),
'custom encoding containing a non-empty array is acceptable',
);

Expand All @@ -353,31 +336,36 @@ test('compact custom encoding validity constraints', t => {
}
});

const orderInvariants = (x, y) => {
const orderInvariants = (x, y, statelessEncodePassable) => {
const rankComp = compareRank(x, y);
const fullComp = compareFull(x, y);
if (rankComp !== 0) {
Object.is(rankComp, fullComp) ||
Fail`with rankComp ${rankComp}, expected matching fullComp: ${fullComp} for ${x} ${y}`;
Fail`with rankComp ${rankComp}, expected matching fullComp: ${fullComp} for ${x} vs. ${y}`;
}
if (fullComp === 0) {
Object.is(rankComp, 0) ||
Fail`with fullComp 0, expected matching rankComp: ${rankComp} for ${x} ${y}`;
Fail`with fullComp 0, expected matching rankComp: ${rankComp} for ${x} vs. ${y}`;
} else {
assert(fullComp !== 0);
rankComp === 0 ||
rankComp === fullComp ||
Fail`with fullComp ${fullComp}, expected 0 or matching rankComp: ${rankComp} for ${x} ${y}`;
Fail`with fullComp ${fullComp}, expected rankComp 0 or matching: ${rankComp} for ${x} vs. ${y}`;
}
const ex = encodePassable(x);
const ey = encodePassable(y);
const ex = statelessEncodePassable(x);
const ey = statelessEncodePassable(y);
const encComp = compareRank(ex, ey);
if (fullComp !== 0) {
Object.is(encComp, fullComp) ||
Fail`with fullComp ${fullComp}, expected matching encComp: ${encComp} for ${ex} ${ey}`;
encComp === 0 ||
encComp === fullComp ||
Fail`with fullComp ${fullComp}, expected matching stateless encComp: ${encComp} for ${x} as ${ex} vs. ${y} as ${ey}`;
}
};

const testRoundTrip = test.macro(async (t, encode) => {
const testRoundTrip = test.macro(async (t, pickEncode) => {
const kit = makeSimplePassableKit();
const encode = pickEncode(kit);
const { decodePassable } = kit;
await fc.assert(
fc.property(arbPassable, n => {
const en = encode(n);
Expand All @@ -388,39 +376,58 @@ const testRoundTrip = test.macro(async (t, encode) => {
}),
);
});
test('original encoding round-trips', testRoundTrip, encodePassable);
test('small encoding round-trips', testRoundTrip, encodePassable2);
test('original encoding round-trips', testRoundTrip, pickLegacy);
test('small encoding round-trips', testRoundTrip, pickCompact);

test('BigInt encoding comparison corresponds with numeric comparison', async t => {
const testBigInt = test.macro(async (t, pickEncode) => {
const kit = makeSimplePassableKit({ stateless: true });
const encodePassable = pickEncode(kit);
await fc.assert(
fc.property(fc.bigInt(), fc.bigInt(), (a, b) => {
const ea = encodePassable(a);
const eb = encodePassable(b);
t.is(a < b, ea < eb);
t.is(a > b, ea > eb);
fc.property(fc.bigInt(), fc.bigInt(), (x, y) => {
const ex = encodePassable(x);
const ey = encodePassable(y);
t.is(x < y, ex < ey);
t.is(x > y, ex > ey);
}),
);
});
test(
'original BigInt encoding comparison corresponds with numeric comparison',
testBigInt,
pickLegacy,
);
test(
'small BigInt encoding comparison corresponds with numeric comparison',
testBigInt,
pickCompact,
);

const testOrderInvariants = test.macro(async (t, pickEncode) => {
const kit = makeSimplePassableKit({ stateless: true });
const statelessEncodePassable = pickEncode(kit);

for (const x of unsortedSample) {
for (const y of unsortedSample) {
orderInvariants(x, y, statelessEncodePassable);
}
}

test('Passable encoding corresponds to rankOrder', async t => {
await fc.assert(
fc.property(arbPassable, arbPassable, (a, b) => {
return orderInvariants(a, b);
fc.property(arbPassable, arbPassable, (x, y) => {
return orderInvariants(x, y, statelessEncodePassable);
}),
);
// Ensure at least one ava assertion.
t.pass();
});

// The following is logically like the test above, but rather than relying on
// the heuristic generation of fuzzing test cases, it always checks everything
// in `sample`.
test('Also test against all enumerated in sample', t => {
for (let i = 0; i < unsortedSample.length; i += 1) {
for (let j = i; j < unsortedSample.length; j += 1) {
orderInvariants(unsortedSample[i], unsortedSample[j]);
}
}
// Ensure at least one ava assertion.
t.pass();
});
test(
'original passable encoding corresponds to rankOrder',
testOrderInvariants,
pickLegacy,
);
test(
'small passable encoding corresponds to rankOrder',
testOrderInvariants,
pickCompact,
);

0 comments on commit de28fd8

Please sign in to comment.