diff --git a/packages/marshal/package.json b/packages/marshal/package.json index 8a90431b8a..8cf934abbf 100644 --- a/packages/marshal/package.json +++ b/packages/marshal/package.json @@ -39,13 +39,13 @@ "@endo/eventual-send": "^0.16.9", "@endo/nat": "^4.1.24", "@endo/pass-style": "^0.1.0", - "@endo/promise-kit": "^0.2.53" + "@endo/promise-kit": "^0.2.53", + "@fast-check/ava": "^1.0.1" }, "devDependencies": { "@endo/init": "^0.5.53", "@endo/lockdown": "^0.1.25", "@endo/ses-ava": "^0.2.37", - "@fast-check/ava": "^1.0.1", "ava": "^5.1.0", "c8": "^7.7.3" }, diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index 9d0599cf3a..7e0a3d84f9 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -293,17 +293,16 @@ const decodeTagged = (encoded, decodePassable) => { */ /** - * @param {EncodeOptions} encodeOptions - * encodeOptions is actually optional, but not marked as such to work around - * https://github.com/microsoft/TypeScript/issues/50286 - * + * @param {EncodeOptions} [encodeOptions] * @returns {(passable: Passable) => string} */ -export const makeEncodePassable = ({ - encodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`, - encodePromise = (prom, _) => Fail`promise unexpected: ${prom}`, - encodeError = (err, _) => Fail`error unexpected: ${err}`, -} = {}) => { +export const makeEncodePassable = (encodeOptions = {}) => { + const { + encodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`, + encodePromise = (prom, _) => Fail`promise unexpected: ${prom}`, + encodeError = (err, _) => Fail`error unexpected: ${err}`, + } = encodeOptions; + const encodePassable = passable => { if (isErrorLike(passable)) { return encodeError(passable, encodePassable); @@ -387,18 +386,13 @@ harden(makeEncodePassable); * @param {DecodeOptions} [decodeOptions] * @returns {(encoded: string) => Passable} */ -// `yarn lint` complains here but not for equivalent code in agoric-sdk. -// Also, vscode does not complain. Hence we're using at-ts-ignore rather than -// at-ts-expect-error. Using at-ts-ignore should also generate a complaint -// that we should be using at-expect-error, where we would normally need -// to suppress that error as well. However, perhaps that second error currently -// happens only in agoric-sdk, but not yet in endo. TODO figure out and fix. -// @ts-ignore -export const makeDecodePassable = ({ - decodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`, - decodePromise = (prom, _) => Fail`promise unexpected: ${prom}`, - decodeError = (err, _) => Fail`error unexpected: ${err}`, -} = {}) => { +export const makeDecodePassable = (decodeOptions = {}) => { + const { + decodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`, + decodePromise = (prom, _) => Fail`promise unexpected: ${prom}`, + decodeError = (err, _) => Fail`error unexpected: ${err}`, + } = decodeOptions; + const decodePassable = encoded => { switch (encoded.charAt(0)) { case 'v': { diff --git a/packages/marshal/src/encodeToCapData.js b/packages/marshal/src/encodeToCapData.js index b2ba597fcd..c4185fd2cf 100644 --- a/packages/marshal/src/encodeToCapData.js +++ b/packages/marshal/src/encodeToCapData.js @@ -82,17 +82,16 @@ const dontEncodePromiseToCapData = prom => Fail`promise unexpected: ${prom}`; const dontEncodeErrorToCapData = err => Fail`error object unexpected: ${err}`; /** - * @param {EncodeToCapDataOptions} encodeOptions - * encodeOptions is actually optional, but not marked as such to work around - * https://github.com/microsoft/TypeScript/issues/50286 - * + * @param {EncodeToCapDataOptions} [encodeOptions] * @returns {(passable: Passable) => Encoding} */ -export const makeEncodeToCapData = ({ - encodeRemotableToCapData = dontEncodeRemotableToCapData, - encodePromiseToCapData = dontEncodePromiseToCapData, - encodeErrorToCapData = dontEncodeErrorToCapData, -} = {}) => { +export const makeEncodeToCapData = (encodeOptions = {}) => { + const { + encodeRemotableToCapData = dontEncodeRemotableToCapData, + encodePromiseToCapData = dontEncodePromiseToCapData, + encodeErrorToCapData = dontEncodeErrorToCapData, + } = encodeOptions; + /** * Must encode `val` into plain JSON data *canonically*, such that * `JSON.stringify(encode(v1)) === JSON.stringify(encode(v1))`. For most @@ -296,14 +295,16 @@ const dontDecodeErrorFromCapData = errorEncoding => * API where these can reliably differ. * See https://github.com/Agoric/agoric-sdk/issues/4334 * - * @param {DecodeOptions} decodeOptions + * @param {DecodeOptions} [decodeOptions] * @returns {(encoded: Encoding) => Passable} */ -export const makeDecodeFromCapData = ({ - decodeRemotableFromCapData = dontDecodeRemotableOrPromiseFromCapData, - decodePromiseFromCapData = dontDecodeRemotableOrPromiseFromCapData, - decodeErrorFromCapData = dontDecodeErrorFromCapData, -} = {}) => { +export const makeDecodeFromCapData = (decodeOptions = {}) => { + const { + decodeRemotableFromCapData = dontDecodeRemotableOrPromiseFromCapData, + decodePromiseFromCapData = dontDecodeRemotableOrPromiseFromCapData, + decodeErrorFromCapData = dontDecodeErrorFromCapData, + } = decodeOptions; + decodeRemotableFromCapData === decodePromiseFromCapData || Fail`An implementation restriction for now: If either decodeRemotableFromCapData or decodePromiseFromCapData is provided, both must be provided and they must be the same: ${q( decodeRemotableFromCapData, diff --git a/packages/marshal/src/encodeToSmallcaps.js b/packages/marshal/src/encodeToSmallcaps.js index a3234aa1da..01ce4bf1fc 100644 --- a/packages/marshal/src/encodeToSmallcaps.js +++ b/packages/marshal/src/encodeToSmallcaps.js @@ -111,17 +111,19 @@ const dontEncodeErrorToSmallcaps = err => Fail`error object unexpected: ${q(err)}`; /** - * @param {EncodeToSmallcapsOptions} encodeOptions + * @param {EncodeToSmallcapsOptions} [encodeOptions] * encodeOptions is actually optional, but not marked as such to work around * https://github.com/microsoft/TypeScript/issues/50286 * * @returns {(passable: Passable) => SmallcapsEncoding} */ -export const makeEncodeToSmallcaps = ({ - encodeRemotableToSmallcaps = dontEncodeRemotableToSmallcaps, - encodePromiseToSmallcaps = dontEncodePromiseToSmallcaps, - encodeErrorToSmallcaps = dontEncodeErrorToSmallcaps, -} = {}) => { +export const makeEncodeToSmallcaps = (encodeOptions = {}) => { + const { + encodeRemotableToSmallcaps = dontEncodeRemotableToSmallcaps, + encodePromiseToSmallcaps = dontEncodePromiseToSmallcaps, + encodeErrorToSmallcaps = dontEncodeErrorToSmallcaps, + } = encodeOptions; + const assertEncodedError = encoding => { (typeof encoding === 'object' && hasOwnPropertyOf(encoding, '#error')) || Fail`internal: Error encoding must have "#error" property: ${q( @@ -316,14 +318,16 @@ const dontDecodeErrorFromSmallcaps = encoding => Fail`error unexpected: ${q(encoding)}`; /** - * @param {DecodeFromSmallcapsOptions} decodeOptions + * @param {DecodeFromSmallcapsOptions} [decodeOptions] * @returns {(encoded: SmallcapsEncoding) => Passable} */ -export const makeDecodeFromSmallcaps = ({ - decodeRemotableFromSmallcaps = dontDecodeRemotableFromSmallcaps, - decodePromiseFromSmallcaps = dontDecodePromiseFromSmallcaps, - decodeErrorFromSmallcaps = dontDecodeErrorFromSmallcaps, -} = {}) => { +export const makeDecodeFromSmallcaps = (decodeOptions = {}) => { + const { + decodeRemotableFromSmallcaps = dontDecodeRemotableFromSmallcaps, + decodePromiseFromSmallcaps = dontDecodePromiseFromSmallcaps, + decodeErrorFromSmallcaps = dontDecodeErrorFromSmallcaps, + } = decodeOptions; + /** * `decodeFromSmallcaps` may rely on `encoding` being the result of a * plain call to JSON.parse. However, it *cannot* rely on `encoding` diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index 6f864fcd1e..d3616fd7ac 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -114,9 +114,11 @@ const passStyleRanks = /** @type {PassStyleRanksRecord} */ ( return trivialComparator(leftPrefixes, rightPrefixes); }) .map(([passStyle, prefixes], index) => { - // Cover all strings that start with any character in `prefixes`. - // `prefixes` is already sorted, so that's + // Cover all strings that start with any character in `prefixes`, + // verifying that it is sorted so that is // all s such that prefixes.at(0) ≤ s < successor(prefixes.at(-1)). + prefixes === [...prefixes].sort().join('') || + Fail`unsorted prefixes for passStyle ${q(passStyle)}: ${q(prefixes)}`; const cover = [ prefixes.charAt(0), String.fromCharCode(prefixes.charCodeAt(prefixes.length - 1) + 1), diff --git a/packages/marshal/test/test-encodePassable.js b/packages/marshal/test/test-encodePassable.js index 57e39d66c0..b40a2b921a 100644 --- a/packages/marshal/test/test-encodePassable.js +++ b/packages/marshal/test/test-encodePassable.js @@ -11,35 +11,75 @@ import { } from '../src/encodePassable.js'; import { compareRank, makeComparatorKit } from '../src/rankOrder.js'; import { sample } from './test-rankOrder.js'; +import { arbPassable } from '../tools/arb-passable.js'; -const { Fail } = assert; +const { Fail, quote: q } = assert; -const r2e = new Map(); -const e2r = []; +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 encodeRemotable = r => { - if (r2e.has(r)) { - return r2e.get(r); - } - const result = `r${e2r.length}`; - r2e.set(r, result); - e2r.push(r); - return result; +const encodeThing = (prefix, r) => { + buffers[prefix].push(r); + // With this encoding, all things with the same prefix have the same rank + return prefix; }; -const decodeRemotable = e => { - e.startsWith('r') || Fail`unexpected encoding ${e}`; - const i = Number(BigInt(e.substring(1))); - assert(i >= 0 && i < e2r.length); - return e2r[i]; +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(encodeRemotable(x), encodeRemotable(y)); + compareRank(encodeThing('r', x), encodeThing('r', y)); -const encodeKey = makeEncodePassable({ encodeRemotable }); +const encodePassableInternal = makeEncodePassable({ + encodeRemotable: r => encodeThing('r', r), + encodePromise: p => encodeThing('?', p), + encodeError: er => encodeThing('!', er), +}); -const decodeKey = makeDecodePassable({ decodeRemotable }); +const encodePassable = passable => { + resetBuffers(); + return encodePassableInternal(passable); +}; + +const decodePassableInternal = makeDecodePassable({ + decodeRemotable: e => decodeThing('r', e), + decodePromise: e => decodeThing('?', e), + decodeError: e => decodeThing('!', e), +}); + +const decodePassable = encoded => { + resetCursors(); + return decodePassableInternal(encoded); +}; const { comparator: compareFull } = makeComparatorKit(compareRemotables); @@ -80,40 +120,46 @@ const goldenPairs = harden([ test('golden round trips', t => { for (const [k, e] of goldenPairs) { - t.is(encodeKey(k), e, 'does k encode as expected'); - t.is(decodeKey(e), k, 'does the key round trip through the encoding'); + t.is(encodePassable(k), e, 'does k encode as expected'); + t.is(decodePassable(e), k, 'does the key round trip through the encoding'); } // Not round trips - t.is(encodeKey(-0), 'f8000000000000000'); - t.is(decodeKey('f0000000000000000'), NaN); + t.is(encodePassable(-0), 'f8000000000000000'); + t.is(decodePassable('f0000000000000000'), NaN); }); -const orderInvariants = (t, x, y) => { +const orderInvariants = (x, y) => { const rankComp = compareRank(x, y); const fullComp = compareFull(x, y); if (rankComp !== 0) { - t.is(rankComp, fullComp); + Object.is(rankComp, fullComp) || + Fail`with rankComp ${rankComp}, expected matching fullComp: ${fullComp} for ${x} ${y}`; } if (fullComp === 0) { - t.is(rankComp, 0); + Object.is(rankComp, 0) || + Fail`with fullComp 0, expected matching rankComp: ${rankComp} for ${x} ${y}`; } else { - t.assert(rankComp === 0 || rankComp === fullComp); + rankComp === 0 || + rankComp === fullComp || + Fail`with fullComp ${fullComp}, expected 0 or matching rankComp: ${rankComp} for ${x} ${y}`; } -}; - -test('order invariants', t => { - for (let i = 0; i < sample.length; i += 1) { - for (let j = i; j < sample.length; j += 1) { - orderInvariants(t, sample[i], sample[j]); - } + const ex = encodePassable(x); + const ey = encodePassable(y); + const encComp = compareRank(ex, ey); + if (fullComp !== 0) { + Object.is(encComp, fullComp) || + Fail`with fullComp ${fullComp}, expected matching encComp: ${encComp} for ${ex} ${ey}`; } -}); +}; -test('BigInt values round-trip', async t => { +test('Passables round-trip', async t => { await fc.assert( - fc.property(fc.bigInt(), n => { - const rt = decodeKey(encodeKey(n)); - return t.is(rt, n); + fc.property(arbPassable, n => { + const en = encodePassable(n); + const rt = decodePassable(en); + const er = encodePassable(rt); + t.is(en, er); + t.is(compareFull(n, rt), 0); }), ); }); @@ -121,9 +167,33 @@ test('BigInt values round-trip', async t => { test('BigInt encoding comparison corresponds with numeric comparison', async t => { await fc.assert( fc.property(fc.bigInt(), fc.bigInt(), (a, b) => { - const ea = encodeKey(a); - const eb = encodeKey(b); - return t.is(a < b, ea < eb) && t.is(a > b, ea > eb); + const ea = encodePassable(a); + const eb = encodePassable(b); + t.is(a < b, ea < eb); + t.is(a > b, ea > eb); + }), + ); +}); + +test('Passable encoding corresponds to rankOrder', async t => { + await fc.assert( + fc.property(arbPassable, arbPassable, (a, b) => { + return orderInvariants(a, b); }), ); + // 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 < sample.length; i += 1) { + for (let j = i; j < sample.length; j += 1) { + orderInvariants(sample[i], sample[j]); + } + } + // Ensure at least one ava assertion. + t.pass(); }); diff --git a/packages/marshal/test/test-rankOrder.js b/packages/marshal/test/test-rankOrder.js index b93abcdfbd..207659ea03 100644 --- a/packages/marshal/test/test-rankOrder.js +++ b/packages/marshal/test/test-rankOrder.js @@ -4,7 +4,7 @@ import { test } from './prepare-test-env-ava.js'; // eslint-disable-next-line import/no-extraneous-dependencies import { fc } from '@fast-check/ava'; -import { makeTagged, Far } from '@endo/pass-style'; +import { makeTagged } from '@endo/pass-style'; import { FullRankCover, @@ -16,73 +16,18 @@ import { assertRankSorted, } from '../src/rankOrder.js'; -const { quote: q } = assert; - -/** - * The only elements with identity. Everything else should be equal - * by contents. - */ -const alice = Far('alice', {}); -const bob = Far('bob', {}); -const carol = Far('carol', {}); +import { + arbPassable, + exampleAlice, + exampleBob, + exampleCarol, +} from '../tools/arb-passable.js'; -/** - * A factory for arbitrary passables - */ -const { passable } = fc.letrec(tie => { - return { - passable: tie('dag').map(x => harden(x)), - dag: fc.oneof( - { depthFactor: 0.5, withCrossShrink: true }, - // a tagged value whose payload is an array of [key, leaf] pairs - // where each key is unique within the payload - // XXX can the payload be generalized further? - fc - .record({ - type: fc.constantFrom('copyMap', 'copySet', 'nonsense'), - payload: fc - .uniqueArray(fc.fullUnicodeString(), { maxLength: 3 }) - .chain(k => { - return fc.tuple(fc.constant(k), tie('leaf')); - }), - }) - .map(({ type, payload }) => makeTagged(type, payload)), - fc.array(tie('dag'), { maxLength: 3 }), - fc.dictionary( - fc.fullUnicodeString().filter(s => s !== 'then'), - tie('dag'), - { maxKeys: 3 }, - ), - tie('dag').map(v => Promise.resolve(v)), - tie('leaf'), - ), - leaf: fc.oneof( - fc.record({}), - fc.fullUnicodeString(), - fc.fullUnicodeString().map(s => Symbol.for(s)), - fc.fullUnicodeString().map(s => new Error(s)), - // primordial symbols and registered lookalikes - fc.constantFrom( - ...Object.getOwnPropertyNames(Symbol).flatMap(k => { - const v = Symbol[k]; - if (typeof v !== 'symbol') return []; - return [v, Symbol.for(k), Symbol.for(`@@${k}`)]; - }), - ), - fc.bigInt(), - fc.integer(), - fc.constantFrom(-0, NaN, Infinity, -Infinity), - fc.constantFrom(null, undefined, false, true), - fc.constantFrom(alice, bob, carol), - // unresolved promise - fc.constant(new Promise(() => {})), - ), - }; -}); +const { quote: q } = assert; test('compareRank is reflexive', async t => { await fc.assert( - fc.property(passable, x => { + fc.property(arbPassable, x => { return t.is(compareRank(x, x), 0); }), ); @@ -90,7 +35,7 @@ test('compareRank is reflexive', async t => { test('compareRank totally orders ranks', async t => { await fc.assert( - fc.property(passable, passable, (a, b) => { + fc.property(arbPassable, arbPassable, (a, b) => { const ab = compareRank(a, b); const ba = compareRank(b, a); if (ab === 0) { @@ -105,15 +50,12 @@ test('compareRank totally orders ranks', async t => { ); }); -// TODO Had to remove key-level cases from the test-encodePassable.js as -// migrated to endo. As a result, some of the tests here are broken. -// Fix. -test.skip('compareRank is transitive', async t => { +test('compareRank is transitive', async t => { await fc.assert( fc.property( // operate on a set of three passables covering at least two ranks fc - .uniqueArray(passable, { minLength: 3, maxLength: 3 }) + .uniqueArray(arbPassable, { minLength: 3, maxLength: 3 }) .filter( ([a, b, c]) => compareRank(a, b) !== 0 || compareRank(a, c) !== 0, ), @@ -122,39 +64,44 @@ test.skip('compareRank is transitive', async t => { assertRankSorted(sorted, compareRank); const [a, b, c] = sorted; const failures = []; - let result; - let resultOk; - result = compareRank(a, b); - resultOk = t.true(result <= 0, 'a <= b'); - if (!resultOk) { - failures.push(`Expected <= 0: ${result} from ${q(a)} vs. ${q(b)}`); - } - result = compareRank(a, c); - resultOk = t.true(result <= 0, 'a <= c'); - if (!resultOk) { - failures.push(`Expected <= 0: ${result} from ${q(a)} vs. ${q(c)}`); - } - result = compareRank(b, c); - resultOk = t.true(result <= 0, 'b <= c'); - if (!resultOk) { - failures.push(`Expected <= 0: ${result} from ${q(b)} vs. ${q(c)}`); - } - result = compareRank(c, b); - resultOk = t.true(result >= 0, 'c >= b'); - if (!resultOk) { - failures.push(`Expected >= 0: ${result} from ${q(c)} vs. ${q(b)}`); - } - result = compareRank(c, a); - resultOk = t.true(result >= 0, 'c >= a'); - if (!resultOk) { - failures.push(`Expected >= 0: ${result} from ${q(c)} vs. ${q(a)}`); - } - result = compareRank(b, a); - resultOk = t.true(result >= 0, 'b >= a'); - if (!resultOk) { - failures.push(`Expected >= 0: ${result} from ${q(b)} vs. ${q(a)}`); - } + const testCompare = (outcome, message, failure) => { + t.true(outcome, message); + if (!outcome) { + failures.push(failure); + } + }; + + testCompare( + compareRank(a, b) <= 0, + 'a <= b', + `Expected <= 0: ${q(a)} vs. ${q(b)}`, + ); + testCompare( + compareRank(a, c) <= 0, + 'a <= c', + `Expected <= 0: ${q(a)} vs. ${q(c)}`, + ); + testCompare( + compareRank(b, c) <= 0, + 'b <= c', + `Expected <= 0: ${q(b)} vs. ${q(c)}`, + ); + testCompare( + compareRank(c, b) >= 0, + 'c >= b', + `Expected >= 0: ${q(c)} vs. ${q(b)}`, + ); + testCompare( + compareRank(c, a) >= 0, + 'c >= a', + `Expected >= 0: ${q(c)} vs. ${q(a)}`, + ); + testCompare( + compareRank(b, a) >= 0, + 'b >= a', + `Expected >= 0: ${q(b)} vs. ${q(a)}`, + ); return t.deepEqual(failures, []); }, @@ -178,7 +125,7 @@ export const sample = harden([ 2, null, [5, { foo: 4, bar: null }], - bob, + exampleBob, 0, makeTagged('copySet', [ ['a', 4], @@ -189,7 +136,7 @@ export const sample = harden([ undefined, -Infinity, [5], - alice, + exampleAlice, [], Symbol.for('foo'), new Error('not erroneous'), @@ -197,7 +144,7 @@ export const sample = harden([ [5, { bar: 5 }], Symbol.for(''), false, - carol, + exampleCarol, -0, {}, [5, undefined], @@ -219,6 +166,12 @@ export const sample = harden([ [5, { foo: 4, bar: undefined }], Promise.resolve('fulfillment'), [5, { foo: 4 }], + // The promises should be of the same rank, in which case + // the singleton array should be earlier. But if the encoded + // gives the earlier promise an earlier encoding (as it used to), + // then the encoded forms will not be order preserving. + [Promise.resolve(null), 'x'], + [Promise.resolve(null)], ]); const rejectedP = Promise.reject(new Error('broken')); @@ -261,6 +214,8 @@ const sortedSample = harden([ // Lexicographic records by reverse sorted property name, then by values // in that order. [], + [Promise.resolve(null)], + [Promise.resolve(null), 'x'], [5], [5, { bar: 5 }], [5, { foo: 4 }], @@ -287,9 +242,9 @@ const sortedSample = harden([ // All remotables are tied for the same rank and the sort is stable, // so their relative order is preserved - bob, - alice, - carol, + exampleBob, + exampleAlice, + exampleCarol, // Lexicographic strings. Shorter beats longer. // TODO Probe UTF-16 vs Unicode vs UTF-8 (Moddable) ordering. @@ -318,7 +273,8 @@ test('compare and sort by rank', t => { ); }); -const rangeSample = harden([ +// Unused in that it is used only in a skipped test +const unusedRangeSample = harden([ {}, // 0 -- prefix are earlier, so empty is earliest { bar: null }, // 1 { bar: undefined }, // 2 -- records with same names grouped together @@ -338,7 +294,9 @@ const rangeSample = harden([ ]); /** @type {[RankCover, IndexCover][]} */ -const queries = harden([ +// @ts-expect-error Stale from when RankCover was a pair of extreme values +// rather than a pair of strings to be compared to passable encodings. +const brokenQueries = harden([ [ [['c'], ['c']], // first > last implies absent. @@ -368,9 +326,9 @@ const queries = harden([ // adding composite key handling to the durable store implementation) will need // to re-enable and (likely) update this test. test.skip('range queries', t => { - t.assert(isRankSorted(rangeSample, compareRank)); - for (const [rankCover, indexRange] of queries) { - const range = getIndexCover(rangeSample, compareRank, rankCover); + t.assert(isRankSorted(unusedRangeSample, compareRank)); + for (const [rankCover, indexRange] of brokenQueries) { + const range = getIndexCover(unusedRangeSample, compareRank, rankCover); t.is(range[0], indexRange[0]); t.is(range[1], indexRange[1]); } diff --git a/packages/marshal/tools/arb-passable.js b/packages/marshal/tools/arb-passable.js new file mode 100644 index 0000000000..5be6f13762 --- /dev/null +++ b/packages/marshal/tools/arb-passable.js @@ -0,0 +1,109 @@ +// @ts-check +import '../src/types.js'; +import { fc } from '@fast-check/ava'; +import { Far, makeTagged } from '@endo/pass-style'; + +/** + * The only elements with identity. Everything else should be equal + * by contents. + */ +export const exampleAlice = Far('alice', {}); +export const exampleBob = Far('bob', {}); +export const exampleCarol = Far('carol', {}); + +export const arbString = fc.oneof(fc.string(), fc.fullUnicodeString()); + +export const arbLeaf = fc.oneof( + fc.constantFrom(null, undefined, false, true), + arbString, + arbString.map(s => Symbol.for(s)), + // primordial symbols and registered lookalikes + fc.constantFrom( + ...Object.getOwnPropertyNames(Symbol).flatMap(k => { + const v = Symbol[k]; + if (typeof v !== 'symbol') return []; + return [v, Symbol.for(k), Symbol.for(`@@${k}`)]; + }), + ), + fc.bigInt(), + fc.integer(), + fc.constantFrom(-0, NaN, Infinity, -Infinity), + fc.record({}), + fc.constantFrom(exampleAlice, exampleBob, exampleCarol), + arbString.map(s => new Error(s)), + // unresolved promise + fc.constant(new Promise(() => {})), +); + +const { arbDag } = fc.letrec(tie => { + return { + arbDag: fc.oneof( + { withCrossShrink: true }, + arbLeaf, + tie('arbDag').map(v => Promise.resolve(v)), + fc.array(tie('arbDag')), + fc.dictionary( + arbString.filter(s => s !== 'then'), + tie('arbDag'), + ), + // A tagged value, either of arbitrary type with arbitrary payload + // or of known type with arbitrary or explicitly valid payload. + // Ordered by increasing complexity. + fc + .oneof( + fc.record({ type: arbString, payload: tie('arbDag') }), + fc.record({ + type: fc.constantFrom('copySet'), + payload: fc.oneof( + tie('arbDag'), + // copySet valid payload is an array of unique passables. + // TODO: A valid copySet payload must be a reverse sorted array, + // so we should generate some of those as well. + fc.uniqueArray(tie('arbDag')), + ), + }), + fc.record({ + type: fc.constantFrom('copyBag'), + payload: fc.oneof( + tie('arbDag'), + // copyBag valid payload is an array of [passable, count] tuples + // in which each passable is unique. + // TODO: A valid copyBag payload must be a reverse sorted array, + // so we should generate some of those as well. + fc.uniqueArray(fc.tuple(tie('arbDag'), fc.bigInt()), { + selector: entry => entry[0], + }), + ), + }), + fc.record({ + type: fc.constantFrom('copyMap'), + payload: fc.oneof( + tie('arbDag'), + // copyMap valid payload is a + // `{ keys: Passable[], values: Passable[]}` + // record in which keys are unique and both arrays have the + // same length. + // TODO: In a valid copyMap payload, the keys must be a + // reverse sorted array, so we should generate some of + // those as well. + fc + .uniqueArray( + fc.record({ key: tie('arbDag'), value: tie('arbDag') }), + { selector: entry => entry.key }, + ) + .map(entries => ({ + keys: entries.map(({ key }) => key), + values: entries.map(({ value }) => value), + })), + ), + }), + ) + .map(({ type, payload }) => makeTagged(type, payload)), + ), + }; +}); + +/** + * A factory for arbitrary passables + */ +export const arbPassable = arbDag.map(x => harden(x));