Skip to content

Commit

Permalink
fix: arb passable tools (#6296)
Browse files Browse the repository at this point in the history
* fix: arb tools

* fix: better comments
  • Loading branch information
erights authored Oct 19, 2022
1 parent 5e5ae42 commit d91ee8c
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 80 deletions.
3 changes: 2 additions & 1 deletion packages/store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"@agoric/internal": "^0.2.1",
"@endo/eventual-send": "^0.16.5",
"@endo/marshal": "^0.7.5",
"@endo/promise-kit": "^0.2.49"
"@endo/promise-kit": "^0.2.49",
"@fast-check/ava": "^1.0.1"
},
"devDependencies": {
"@agoric/swingset-vat": "^0.30.2",
Expand Down
106 changes: 27 additions & 79 deletions packages/store/test/test-rankOrder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-nocheck
// @ts-check
import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js';
import { Far, makeTagged } from '@endo/marshal';
import { makeTagged } from '@endo/marshal';
import { fc } from '@fast-check/ava';
import {
FullRankCover,
Expand All @@ -11,82 +11,26 @@ import {
getPassStyleCover,
assertRankSorted,
} from '../src/patterns/rankOrder.js';
import {
arbPassable,
exampleAlice,
exampleBob,
exampleCarol,
} from '../tools/arb-passable.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', {});

/**
* 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(() => {})),
),
};
});

test('compareRank is reflexive', async t => {
await fc.assert(
fc.property(passable, x => {
fc.property(arbPassable, x => {
return t.is(compareRank(x, x), 0);
}),
);
});

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) {
Expand All @@ -104,9 +48,10 @@ test('compareRank totally orders ranks', 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
// operate on a set of three distinct 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,
),
Expand Down Expand Up @@ -176,7 +121,7 @@ export const sample = harden([
2,
null,
[5, { foo: 4, bar: null }],
bob,
exampleBob,
0,
makeTagged('copySet', [
['a', 4],
Expand All @@ -187,15 +132,15 @@ export const sample = harden([
undefined,
-Infinity,
[5],
alice,
exampleAlice,
[],
Symbol.for('foo'),
new Error('not erroneous'),
Symbol.for('@@foo'),
[5, { bar: 5 }],
Symbol.for(''),
false,
carol,
exampleCarol,
-0,
{},
[5, undefined],
Expand Down Expand Up @@ -285,9 +230,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.
Expand Down Expand Up @@ -316,7 +261,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
Expand All @@ -336,7 +282,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.
Expand Down Expand Up @@ -366,9 +314,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]);
}
Expand Down
109 changes: 109 additions & 0 deletions packages/store/tools/arb-passable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// @ts-check
import { Far, makeTagged } from '@endo/marshal';
import { fc } from '@fast-check/ava';
import '../src/types.js';

/**
* 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));

0 comments on commit d91ee8c

Please sign in to comment.