diff --git a/packages/pass-style/tools.js b/packages/pass-style/tools.js index 78fd22bf37..50fd8ca393 100644 --- a/packages/pass-style/tools.js +++ b/packages/pass-style/tools.js @@ -10,6 +10,8 @@ export { exampleBob, exampleCarol, arbString, + arbKeyLeaf, arbLeaf, + arbKey, arbPassable, } from './tools/arb-passable.js'; diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index 22549f5d83..5c6bcbdfbf 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -14,7 +14,7 @@ export const exampleCarol = Far('carol', {}); export const arbString = fc.oneof(fc.string(), fc.fullUnicodeString()); -export const arbLeaf = fc.oneof( +const keyableLeaves = [ fc.constantFrom(null, undefined, false, true), arbString, arbString.map(s => Symbol.for(s)), @@ -31,22 +31,43 @@ export const arbLeaf = fc.oneof( fc.constantFrom(-0, NaN, Infinity, -Infinity), fc.record({}), fc.constantFrom(exampleAlice, exampleBob, exampleCarol), +]; + +export const arbKeyLeaf = fc.oneof(...keyableLeaves); + +export const arbLeaf = fc.oneof( + ...keyableLeaves, arbString.map(s => Error(s)), // unresolved promise fc.constant(new Promise(() => {})), ); +const { keyDag } = fc.letrec(tie => { + return { + keyDag: fc.oneof( + { withCrossShrink: true }, + arbKeyLeaf, + fc.array(tie('keyDag')), + fc.dictionary( + arbString.filter(s => s !== 'then'), + tie('keyDag'), + ), + ), + }; +}); + 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 promise for a passable. + tie('arbDag').map(v => Promise.resolve(v)), // A tagged value, either of arbitrary type with arbitrary payload // or of known type with arbitrary or explicitly valid payload. // Ordered by increasing complexity. @@ -110,6 +131,11 @@ const { arbDag } = fc.letrec(tie => { }); /** - * A factory for arbitrary passables + * A factory for arbitrary keys. + */ +export const arbKey = keyDag.map(x => harden(x)); + +/** + * A factory for arbitrary passables. */ export const arbPassable = arbDag.map(x => harden(x)); diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 701e5470e5..e1d806d0a8 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -40,7 +40,9 @@ }, "devDependencies": { "@endo/init": "workspace:^", + "@endo/pass-style": "workspace:^", "@endo/ses-ava": "workspace:^", + "@fast-check/ava": "^1.1.5", "ava": "^6.1.3", "babel-eslint": "^10.1.0", "eslint": "^8.57.0", diff --git a/packages/patterns/test/copySet.test.js b/packages/patterns/test/copySet.test.js index b46e79aa36..4912f9c1e1 100644 --- a/packages/patterns/test/copySet.test.js +++ b/packages/patterns/test/copySet.test.js @@ -1,12 +1,21 @@ import test from '@endo/ses-ava/prepare-endo.js'; +import { fc } from '@fast-check/ava'; import { makeTagged, getTag, passStyleOf } from '@endo/marshal'; +import { + arbKey, + exampleAlice, + exampleBob, + exampleCarol, +} from '@endo/pass-style/tools.js'; +import { Fail, q } from '@endo/errors'; import { isCopySet, assertCopySet, makeCopySet, getCopySetKeys, } from '../src/keys/checkKey.js'; +import { keyEQ } from '../src/keys/compareKeys.js'; import { setIsSuperset, setIsDisjoint, @@ -85,7 +94,7 @@ test('key uniqueness', t => { // TODO: incorporate fast-check for property-based testing that construction // reverse rank sorts keys and validation rejects any other key order. -test('operations', t => { +test('operations on golden inputs', t => { const x = makeCopySet(['b', 'a', 'c']); const y = makeCopySet(['a', 'b']); const z = makeCopySet(['c', 'b']); @@ -104,6 +113,16 @@ test('operations', t => { t.assert(setIsDisjoint(xMy, y)); t.assert(setIsSuperset(x, y)); + const twoCohorts = [ + [exampleAlice, 'z'], + [exampleBob, 'z'], + [exampleCarol, 'a'], + ]; + t.assert( + setIsSuperset(makeCopySet(twoCohorts), makeCopySet(twoCohorts.slice(-1))), + 'superset with many items in one rank cohort (issue #2588)', + ); + t.assert(matches(x, yUz)); t.assert(matches(x, M.gt(y))); t.assert(matches(x, M.gt(z))); @@ -119,6 +138,23 @@ test('operations', t => { t.deepEqual(yIz, makeTagged('copySet', ['b'])); }); +test('setIsSuperset', async t => { + await fc.assert( + fc.property( + fc.uniqueArray(arbKey, { comparator: keyEQ }), + fc.infiniteStream(fc.boolean()), + (arr, keepSeq) => { + const sub = arr.filter(() => keepSeq.next().value); + setIsSuperset(makeCopySet(arr), makeCopySet(sub)) || + Fail`${q(sub)} of ${q(arr)}`; + }, + ), + ); + + // Ensure at least one ava assertion. + t.pass(); +}); + test('matching', t => { const copySet = makeCopySet(['z', 'c', 'b', 'a']); const missingKey = makeCopySet(['z', 'c', 'b']); diff --git a/yarn.lock b/yarn.lock index 27468a3e8a..6701d0ce66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -727,8 +727,10 @@ __metadata: "@endo/eventual-send": "workspace:^" "@endo/init": "workspace:^" "@endo/marshal": "workspace:^" + "@endo/pass-style": "workspace:^" "@endo/promise-kit": "workspace:^" "@endo/ses-ava": "workspace:^" + "@fast-check/ava": "npm:^1.1.5" ava: "npm:^6.1.3" babel-eslint: "npm:^10.1.0" eslint: "npm:^8.57.0"