From b78bd5ae72215f295e1bfffd444a0b72b809b543 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 27 Feb 2025 23:52:25 -0500 Subject: [PATCH 1/7] test(internal): Accommodate property-based testing attempts to delete an Array's `length` Ref #10807 --- packages/internal/test/utils.test.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index 22eb0dd3553..afc1e061ee1 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -177,8 +177,13 @@ const { (expectation, prop) => { if (!isObject(expectation.specimen)) expectation.specimen = {}; if (!isObject(expectation.permit)) expectation.permit = { [prop]: true }; - delete expectation.specimen[prop]; - expectation.problem = 'specimen missing key'; + try { + // The "length" property of an array is not configurable, so accept + // failure as an option. + delete expectation.specimen[prop]; + expectation.problem = 'specimen missing key'; + // eslint-disable-next-line no-empty + } catch (err) {} return expectation; }, ).filter( From bda059d4efdd3ea086117ea7b0e6f4d4cbdfaab8 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 28 Feb 2025 11:24:10 -0500 Subject: [PATCH 2/7] chore(internal): Remove unused dependency --- packages/internal/package.json | 1 - yarn.lock | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/packages/internal/package.json b/packages/internal/package.json index 555771f14a9..65ebb7b7ab8 100755 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -38,7 +38,6 @@ "@endo/exo": "^1.5.8", "@endo/init": "^1.1.8", "ava": "^5.3.0", - "fast-check": "^3.23.2", "@fast-check/ava": "^2.0.1", "tsd": "^0.31.1" }, diff --git a/yarn.lock b/yarn.lock index 0dbdfd25508..f6452b76673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6396,13 +6396,6 @@ fast-check@^3.0.0: dependencies: pure-rand "^5.0.1" -fast-check@^3.23.2: - version "3.23.2" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-3.23.2.tgz#0129f1eb7e4f500f58e8290edc83c670e4a574a2" - integrity sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A== - dependencies: - pure-rand "^6.1.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -10391,11 +10384,6 @@ pure-rand@^5.0.1: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-5.0.1.tgz#97a287b4b4960b2a3448c0932bf28f2405cac51d" integrity sha512-ksWccjmXOHU2gJBnH0cK1lSYdvSZ0zLoCMSz/nTGh6hDvCSgcRxDyIcOBD6KNxFz3xhMPm/T267Tbe2JRymKEQ== -pure-rand@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" - integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== - q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" From fb023521769313badb3d7ca42dffcf59b827de59 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 28 Feb 2025 11:39:08 -0500 Subject: [PATCH 3/7] chore(internal): Improve comments and typing for `attenuate` testing --- packages/internal/test/utils.test.js | 65 ++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index afc1e061ee1..55bf2f775b2 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -15,6 +15,7 @@ import { } from '../src/ses-utils.js'; /** @import {Permit, Attenuated} from '../src/types.js'; */ +/** @import {Arbitrary} from 'fast-check'; */ const { ownKeys } = Reflect; const defineDataProperty = (obj, key, value) => @@ -77,12 +78,29 @@ const arbPermitLeaf = fc.oneof(fastShrink, fc.constant(true), arbString); * specimen: T; * permit: P; * attenuation: Attenuated; - * problem?: string; + * problem: + * | undefined + * | 'bad permit' + * | 'bad specimen' + * | 'specimen missing key'; * }} AttenuateExpectation */ +/** + * An Arbitrary for generating `attenuate` test cases, including those in which + * either or both of the permit and specimen may be invalid in some way. + * + * @template {AttenuateExpectation} T + * @template B + * @param {Arbitrary} arbRecursive a self-reference for recursion + * @param {Arbitrary} [arbBad] for generating a "bad" value. Required by + * `makeBad`. + * @param {(testCase: T, badValue: B) => T} [makeBad] derive a "bad" test case + * from a generic one and a value from `arrBad` + * @returns {Arbitrary} + */ const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { - // An arbitrary value with no attenuation. + /** Test case for an arbitrary value with no attenuation. */ const base = fc .tuple(arbShallow, arbPermitLeaf) .map(([specimen, permit]) => ({ @@ -91,15 +109,15 @@ const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { attenuation: specimen, problem: undefined, })); + if (makeBad && !arbBad) throw Error('arbBad is required with makeBad'); + // eslint-disable-next-line no-self-assign + arbBad = /** @type {Arbitrary} */ (arbBad); const badBase = - makeBad && fc.tuple(base, arbBad).map(args => makeBad(...args)); - return fc.oneof( - fastShrink, - ...(makeBad ? [badBase, base] : [base]), - // An object with string-keyed properties that are subject to attenuation, - // or else a specimen and permit, either or both of which may be invalid in - // some way. + makeBad && + fc.tuple(base, arbBad).map((testCase, bad) => makeBad(testCase, bad)); + + const recurse = /** @type {Arbitrary} */ ( fc .uniqueArray( fc.record({ @@ -120,14 +138,14 @@ const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { let problem; for (const propRecord of propRecords) { const { name, skip, corruption } = propRecord; - let { subExpectation } = - /** @type {{ subExpectation: AttenuateExpectation }} */ ( - propRecord - ); + let { subExpectation } = /** @type {{ subExpectation: T }} */ ( + propRecord + ); defineDataProperty(specimen, name, subExpectation.specimen); if (skip) continue; problem ||= subExpectation.problem; if (!problem && corruption) { + // @ts-expect-error TS2722 makeBad is not undefined here subExpectation = makeBad(subExpectation, corruption); defineDataProperty(specimen, name, subExpectation.specimen); problem = subExpectation.problem; @@ -136,7 +154,13 @@ const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { defineDataProperty(attenuation, name, subExpectation.attenuation); } return { specimen, permit, attenuation, problem }; - }), + }) + ); + + return fc.oneof( + fastShrink, + .../** @type {Arbitrary[]} */ (makeBad ? [badBase, base] : [base]), + recurse, ); }; const { @@ -145,7 +169,10 @@ const { badSpecimen: arbBadSpecimen, specimenMissingKey: arbSpecimenMissingKey, } = fc.letrec(tie => ({ + // Happy-path test cases. expectation: makeArbExpectation(tie('expectation')), + + // Test cases in which the permit is invalid. badPermit: makeArbExpectation( tie('badPermit'), arbShallow.filter(x => x !== true && typeof x !== 'string' && !isObject(x)), @@ -158,6 +185,8 @@ const { expectation => !!(/** @type {AttenuateExpectation} */ (expectation).problem), ), + + // Test cases in which the permit is an object but the specimen is not. badSpecimen: makeArbExpectation( tie('badSpecimen'), fc.oneof(arbPrimitive, arbFunction), @@ -171,16 +200,18 @@ const { expectation => !!(/** @type {AttenuateExpectation} */ (expectation).problem), ), + + // Test cases in which the specimen is missing a permit property. specimenMissingKey: makeArbExpectation( tie('specimenMissingKey'), arbString, (expectation, prop) => { if (!isObject(expectation.specimen)) expectation.specimen = {}; if (!isObject(expectation.permit)) expectation.permit = { [prop]: true }; + // Some properties are not configurable (e.g., an array's `length`), so + // accept failure as an option. try { - // The "length" property of an array is not configurable, so accept - // failure as an option. - delete expectation.specimen[prop]; + delete (/** @type {any} */ (expectation.specimen)[prop]); expectation.problem = 'specimen missing key'; // eslint-disable-next-line no-empty } catch (err) {} From bac55bf82e91ca7d625c8b1db324e0263460d9e8 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 28 Feb 2025 11:48:05 -0500 Subject: [PATCH 4/7] test(internal): Isolate `attenuate` testing into a lexical block --- packages/internal/test/utils.test.js | 463 ++++++++++++++------------- 1 file changed, 235 insertions(+), 228 deletions(-) diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index 55bf2f775b2..dd88c980a31 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -70,242 +70,249 @@ const { value: arbShallow } = fc.letrec(tie => ({ })); // #region attenuate -const arbPermitLeaf = fc.oneof(fastShrink, fc.constant(true), arbString); -/** - * @template [T=unknown] - * @template {Permit} [P=Permit] - * @typedef {{ - * specimen: T; - * permit: P; - * attenuation: Attenuated; - * problem: - * | undefined - * | 'bad permit' - * | 'bad specimen' - * | 'specimen missing key'; - * }} AttenuateExpectation - */ - -/** - * An Arbitrary for generating `attenuate` test cases, including those in which - * either or both of the permit and specimen may be invalid in some way. - * - * @template {AttenuateExpectation} T - * @template B - * @param {Arbitrary} arbRecursive a self-reference for recursion - * @param {Arbitrary} [arbBad] for generating a "bad" value. Required by - * `makeBad`. - * @param {(testCase: T, badValue: B) => T} [makeBad] derive a "bad" test case - * from a generic one and a value from `arrBad` - * @returns {Arbitrary} - */ -const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { - /** Test case for an arbitrary value with no attenuation. */ - const base = fc - .tuple(arbShallow, arbPermitLeaf) - .map(([specimen, permit]) => ({ - specimen, - permit, - attenuation: specimen, - problem: undefined, - })); - - if (makeBad && !arbBad) throw Error('arbBad is required with makeBad'); - // eslint-disable-next-line no-self-assign - arbBad = /** @type {Arbitrary} */ (arbBad); - const badBase = - makeBad && - fc.tuple(base, arbBad).map((testCase, bad) => makeBad(testCase, bad)); - - const recurse = /** @type {Arbitrary} */ ( - fc - .uniqueArray( - fc.record({ - name: arbString, - subExpectation: arbRecursive, - skip: fc.boolean(), - corruption: arbBad ? fc.oneof(arbBad, arbUndefined) : arbUndefined, - }), - { - selector: propRecord => propRecord.name, - maxLength: 5, - }, - ) - .map(propRecords => { - const specimen = {}; - const permit = {}; - const attenuation = {}; - let problem; - for (const propRecord of propRecords) { - const { name, skip, corruption } = propRecord; - let { subExpectation } = /** @type {{ subExpectation: T }} */ ( - propRecord - ); - defineDataProperty(specimen, name, subExpectation.specimen); - if (skip) continue; - problem ||= subExpectation.problem; - if (!problem && corruption) { - // @ts-expect-error TS2722 makeBad is not undefined here - subExpectation = makeBad(subExpectation, corruption); +{ + const arbPermitLeaf = fc.oneof(fastShrink, fc.constant(true), arbString); + /** + * @template [T=unknown] + * @template {Permit} [P=Permit] + * @typedef {{ + * specimen: T; + * permit: P; + * attenuation: Attenuated; + * problem: + * | undefined + * | 'bad permit' + * | 'bad specimen' + * | 'specimen missing key'; + * }} AttenuateExpectation + */ + + /** + * An Arbitrary for generating `attenuate` test cases, including those in + * which either or both of the permit and specimen may be invalid in some + * way. + * + * @template {AttenuateExpectation} T + * @template B + * @param {Arbitrary} arbRecursive a self-reference for recursion + * @param {Arbitrary} [arbBad] for generating a "bad" value. Required by + * `makeBad`. + * @param {(testCase: T, badValue: B) => T} [makeBad] derive a "bad" test case + * from a generic one and a value from `arrBad` + * @returns {Arbitrary} + */ + const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { + /** Test case for an arbitrary value with no attenuation. */ + const base = fc + .tuple(arbShallow, arbPermitLeaf) + .map(([specimen, permit]) => ({ + specimen, + permit, + attenuation: specimen, + problem: undefined, + })); + + if (makeBad && !arbBad) throw Error('arbBad is required with makeBad'); + // eslint-disable-next-line no-self-assign + arbBad = /** @type {Arbitrary} */ (arbBad); + const badBase = + makeBad && + fc.tuple(base, arbBad).map((testCase, bad) => makeBad(testCase, bad)); + + const recurse = /** @type {Arbitrary} */ ( + fc + .uniqueArray( + fc.record({ + name: arbString, + subExpectation: arbRecursive, + skip: fc.boolean(), + corruption: arbBad ? fc.oneof(arbBad, arbUndefined) : arbUndefined, + }), + { + selector: propRecord => propRecord.name, + maxLength: 5, + }, + ) + .map(propRecords => { + const specimen = {}; + const permit = {}; + const attenuation = {}; + let problem; + for (const propRecord of propRecords) { + const { name, skip, corruption } = propRecord; + let { subExpectation } = /** @type {{ subExpectation: T }} */ ( + propRecord + ); defineDataProperty(specimen, name, subExpectation.specimen); - problem = subExpectation.problem; + if (skip) continue; + problem ||= subExpectation.problem; + if (!problem && corruption) { + // @ts-expect-error TS2722 makeBad is not undefined here + subExpectation = makeBad(subExpectation, corruption); + defineDataProperty(specimen, name, subExpectation.specimen); + problem = subExpectation.problem; + } + defineDataProperty(permit, name, subExpectation.permit); + defineDataProperty(attenuation, name, subExpectation.attenuation); } - defineDataProperty(permit, name, subExpectation.permit); - defineDataProperty(attenuation, name, subExpectation.attenuation); + return { specimen, permit, attenuation, problem }; + }) + ); + + return fc.oneof( + fastShrink, + .../** @type {Arbitrary[]} */ (makeBad ? [badBase, base] : [base]), + recurse, + ); + }; + const { + expectation: arbExpectation, + badPermit: arbBadPermit, + badSpecimen: arbBadSpecimen, + specimenMissingKey: arbSpecimenMissingKey, + } = fc.letrec(tie => ({ + // Happy-path test cases. + expectation: makeArbExpectation(tie('expectation')), + + // Test cases in which the permit is invalid. + badPermit: makeArbExpectation( + tie('badPermit'), + arbShallow.filter( + x => x !== true && typeof x !== 'string' && !isObject(x), + ), + (expectation, badPermit) => { + expectation.permit = badPermit; + expectation.problem = 'bad permit'; + return expectation; + }, + ).filter( + expectation => + !!(/** @type {AttenuateExpectation} */ (expectation).problem), + ), + + // Test cases in which the permit is an object but the specimen is not. + badSpecimen: makeArbExpectation( + tie('badSpecimen'), + fc.oneof(arbPrimitive, arbFunction), + (expectation, badSpecimen) => { + if (!isObject(expectation.permit)) expectation.permit = {}; + expectation.specimen = badSpecimen; + expectation.problem = 'bad specimen'; + return expectation; + }, + ).filter( + expectation => + !!(/** @type {AttenuateExpectation} */ (expectation).problem), + ), + + // Test cases in which the specimen is missing a permit property. + specimenMissingKey: makeArbExpectation( + tie('specimenMissingKey'), + arbString, + (expectation, prop) => { + if (!isObject(expectation.specimen)) expectation.specimen = {}; + if (!isObject(expectation.permit)) { + expectation.permit = { [prop]: true }; } - return { specimen, permit, attenuation, problem }; - }) + // Some properties are not configurable (e.g., an array's `length`), so + // accept failure as an option. + try { + delete (/** @type {any} */ (expectation.specimen)[prop]); + expectation.problem = 'specimen missing key'; + // eslint-disable-next-line no-empty + } catch (err) {} + return expectation; + }, + ).filter( + expectation => + !!(/** @type {AttenuateExpectation} */ (expectation).problem), + ), + })); + testProp( + 'attenuate', + /** @type {any} */ ([arbExpectation]), + // @ts-expect-error TS2345 function signature + async (t, { specimen, permit, attenuation }) => { + const actualAttenuation = attenuate(specimen, permit); + t.deepEqual(actualAttenuation, attenuation); + }, ); - - return fc.oneof( - fastShrink, - .../** @type {Arbitrary[]} */ (makeBad ? [badBase, base] : [base]), - recurse, + testProp( + 'attenuate - transform', + /** @type {any} */ ([ + arbExpectation.filter(({ specimen }) => isObject(specimen)), + ]), + // @ts-expect-error TS2345 function signature + async (t, { specimen, permit }) => { + const tag = Symbol('transformed'); + + let mutationCallCount = 0; + const mutatedAttenuation = attenuate(specimen, permit, obj => { + mutationCallCount += 1; + obj[tag] = true; + return obj; + }); + let mutationOk = true; + (function visit(subObj, subPermit) { + if (subPermit === true || typeof subPermit === 'string') return; + mutationOk &&= subObj[tag]; + const allKeys = [...ownKeys(subObj), ...ownKeys(subPermit)]; + for (const k of new Set(allKeys)) { + if (k === tag) continue; + visit(subObj[k], subPermit[k]); + } + })(mutatedAttenuation, permit); + if (!mutationOk) t.log({ specimen, permit, mutatedAttenuation }); + t.true(mutationOk, 'mutation must visit all attenuations'); + + let replacementCallCount = 0; + const replacedAttenuation = attenuate(specimen, permit, _obj => { + replacementCallCount += 1; + return /** @type {any} */ ({ [tag]: replacementCallCount }); + }); + t.is(mutationCallCount, replacementCallCount); + if (mutationCallCount > 0) { + const replacementKeys = ownKeys(replacedAttenuation); + t.true(replacementKeys.includes(tag)); + t.is(replacementKeys.length, 1); + t.is(replacedAttenuation[tag], replacementCallCount); + } + }, ); -}; -const { - expectation: arbExpectation, - badPermit: arbBadPermit, - badSpecimen: arbBadSpecimen, - specimenMissingKey: arbSpecimenMissingKey, -} = fc.letrec(tie => ({ - // Happy-path test cases. - expectation: makeArbExpectation(tie('expectation')), - - // Test cases in which the permit is invalid. - badPermit: makeArbExpectation( - tie('badPermit'), - arbShallow.filter(x => x !== true && typeof x !== 'string' && !isObject(x)), - (expectation, badPermit) => { - expectation.permit = badPermit; - expectation.problem = 'bad permit'; - return expectation; + testProp( + 'attenuate - bad permit', + /** @type {any} */ ([arbBadPermit]), + // @ts-expect-error TS2345 function signature + async (t, { specimen, permit, problem: _problem }) => { + // t.log({ specimen, permit, problem }); + t.throws(() => attenuate(specimen, permit), { + message: /^invalid permit\b/, + }); }, - ).filter( - expectation => - !!(/** @type {AttenuateExpectation} */ (expectation).problem), - ), - - // Test cases in which the permit is an object but the specimen is not. - badSpecimen: makeArbExpectation( - tie('badSpecimen'), - fc.oneof(arbPrimitive, arbFunction), - (expectation, badSpecimen) => { - if (!isObject(expectation.permit)) expectation.permit = {}; - expectation.specimen = badSpecimen; - expectation.problem = 'bad specimen'; - return expectation; + ); + testProp( + 'attenuate - bad specimen', + /** @type {any} */ ([arbBadSpecimen]), + // @ts-expect-error TS2345 function signature + async (t, { specimen, permit, problem: _problem }) => { + // t.log({ specimen, permit, problem }); + t.throws(() => attenuate(specimen, permit), { + message: /^specimen( at path .*)? must be an object for permit\b/, + }); }, - ).filter( - expectation => - !!(/** @type {AttenuateExpectation} */ (expectation).problem), - ), - - // Test cases in which the specimen is missing a permit property. - specimenMissingKey: makeArbExpectation( - tie('specimenMissingKey'), - arbString, - (expectation, prop) => { - if (!isObject(expectation.specimen)) expectation.specimen = {}; - if (!isObject(expectation.permit)) expectation.permit = { [prop]: true }; - // Some properties are not configurable (e.g., an array's `length`), so - // accept failure as an option. - try { - delete (/** @type {any} */ (expectation.specimen)[prop]); - expectation.problem = 'specimen missing key'; - // eslint-disable-next-line no-empty - } catch (err) {} - return expectation; + ); + testProp( + 'attenuate - specimen missing key', + /** @type {any} */ ([arbSpecimenMissingKey]), + // @ts-expect-error TS2345 function signature + async (t, { specimen, permit, problem: _problem }) => { + // t.log({ specimen, permit, problem }); + t.throws(() => attenuate(specimen, permit), { + message: /^specimen is missing path /, + }); }, - ).filter( - expectation => - !!(/** @type {AttenuateExpectation} */ (expectation).problem), - ), -})); -testProp( - 'attenuate', - /** @type {any} */ ([arbExpectation]), - // @ts-expect-error TS2345 function signature - async (t, { specimen, permit, attenuation }) => { - const actualAttenuation = attenuate(specimen, permit); - t.deepEqual(actualAttenuation, attenuation); - }, -); -testProp( - 'attenuate - transform', - /** @type {any} */ ([ - arbExpectation.filter(({ specimen }) => isObject(specimen)), - ]), - // @ts-expect-error TS2345 function signature - async (t, { specimen, permit }) => { - const tag = Symbol('transformed'); - - let mutationCallCount = 0; - const mutatedAttenuation = attenuate(specimen, permit, obj => { - mutationCallCount += 1; - obj[tag] = true; - return obj; - }); - let mutationOk = true; - (function visit(subObj, subPermit) { - if (subPermit === true || typeof subPermit === 'string') return; - mutationOk &&= subObj[tag]; - const allKeys = [...ownKeys(subObj), ...ownKeys(subPermit)]; - for (const k of new Set(allKeys)) { - if (k === tag) continue; - visit(subObj[k], subPermit[k]); - } - })(mutatedAttenuation, permit); - if (!mutationOk) t.log({ specimen, permit, mutatedAttenuation }); - t.true(mutationOk, 'mutation must visit all attenuations'); - - let replacementCallCount = 0; - const replacedAttenuation = attenuate(specimen, permit, _obj => { - replacementCallCount += 1; - return /** @type {any} */ ({ [tag]: replacementCallCount }); - }); - t.is(mutationCallCount, replacementCallCount); - if (mutationCallCount > 0) { - const replacementKeys = ownKeys(replacedAttenuation); - t.true(replacementKeys.includes(tag)); - t.is(replacementKeys.length, 1); - t.is(replacedAttenuation[tag], replacementCallCount); - } - }, -); -testProp( - 'attenuate - bad permit', - /** @type {any} */ ([arbBadPermit]), - // @ts-expect-error TS2345 function signature - async (t, { specimen, permit, problem: _problem }) => { - // t.log({ specimen, permit, problem }); - t.throws(() => attenuate(specimen, permit), { - message: /^invalid permit\b/, - }); - }, -); -testProp( - 'attenuate - bad specimen', - /** @type {any} */ ([arbBadSpecimen]), - // @ts-expect-error TS2345 function signature - async (t, { specimen, permit, problem: _problem }) => { - // t.log({ specimen, permit, problem }); - t.throws(() => attenuate(specimen, permit), { - message: /^specimen( at path .*)? must be an object for permit\b/, - }); - }, -); -testProp( - 'attenuate - specimen missing key', - /** @type {any} */ ([arbSpecimenMissingKey]), - // @ts-expect-error TS2345 function signature - async (t, { specimen, permit, problem: _problem }) => { - // t.log({ specimen, permit, problem }); - t.throws(() => attenuate(specimen, permit), { - message: /^specimen is missing path /, - }); - }, -); + ); +} // #endregion test('deeplyFulfilledObject', async t => { From f403eaabed4f172da3e44a2d7c2dcac298b346aa Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 28 Feb 2025 11:59:25 -0500 Subject: [PATCH 5/7] test(internal): Clarify the nature of a predicate --- packages/internal/test/utils.test.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index dd88c980a31..9e4f55a0293 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -25,7 +25,15 @@ const defineDataProperty = (obj, key, value) => enumerable: true, writable: true, }); -const isObject = x => x !== null && typeof x === 'object'; + +/** + * A predicate for matching non-null non-function objects. Note that this + * category includes arrays and other built-in exotic objects. + * + * @param {unknown} x + * @returns {x is (Array | Record)} + */ +const hasObjectType = x => x !== null && typeof x === 'object'; const fastShrink = { withCrossShrink: true }; const arbUndefined = fc.constant(undefined); @@ -178,7 +186,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ badPermit: makeArbExpectation( tie('badPermit'), arbShallow.filter( - x => x !== true && typeof x !== 'string' && !isObject(x), + x => x !== true && typeof x !== 'string' && !hasObjectType(x), ), (expectation, badPermit) => { expectation.permit = badPermit; @@ -195,7 +203,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ tie('badSpecimen'), fc.oneof(arbPrimitive, arbFunction), (expectation, badSpecimen) => { - if (!isObject(expectation.permit)) expectation.permit = {}; + if (!hasObjectType(expectation.permit)) expectation.permit = {}; expectation.specimen = badSpecimen; expectation.problem = 'bad specimen'; return expectation; @@ -210,8 +218,8 @@ const { value: arbShallow } = fc.letrec(tie => ({ tie('specimenMissingKey'), arbString, (expectation, prop) => { - if (!isObject(expectation.specimen)) expectation.specimen = {}; - if (!isObject(expectation.permit)) { + if (!hasObjectType(expectation.specimen)) expectation.specimen = {}; + if (!hasObjectType(expectation.permit)) { expectation.permit = { [prop]: true }; } // Some properties are not configurable (e.g., an array's `length`), so @@ -240,7 +248,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ testProp( 'attenuate - transform', /** @type {any} */ ([ - arbExpectation.filter(({ specimen }) => isObject(specimen)), + arbExpectation.filter(({ specimen }) => hasObjectType(specimen)), ]), // @ts-expect-error TS2345 function signature async (t, { specimen, permit }) => { From f829272af1725c9d71a47e30dc950f92f899f38c Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 28 Feb 2025 11:59:25 -0500 Subject: [PATCH 6/7] test(internal): Use "test case" rather than the more generic "expectation" --- packages/internal/test/utils.test.js | 87 ++++++++++++++-------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index 9e4f55a0293..819929c0a2a 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -92,7 +92,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ * | 'bad permit' * | 'bad specimen' * | 'specimen missing key'; - * }} AttenuateExpectation + * }} AttenuateTestCase */ /** @@ -100,7 +100,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ * which either or both of the permit and specimen may be invalid in some * way. * - * @template {AttenuateExpectation} T + * @template {AttenuateTestCase} T * @template B * @param {Arbitrary} arbRecursive a self-reference for recursion * @param {Arbitrary} [arbBad] for generating a "bad" value. Required by @@ -109,7 +109,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ * from a generic one and a value from `arrBad` * @returns {Arbitrary} */ - const makeArbExpectation = (arbRecursive, arbBad, makeBad) => { + const makeArbTestCase = (arbRecursive, arbBad, makeBad) => { /** Test case for an arbitrary value with no attenuation. */ const base = fc .tuple(arbShallow, arbPermitLeaf) @@ -132,7 +132,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ .uniqueArray( fc.record({ name: arbString, - subExpectation: arbRecursive, + subCase: arbRecursive, skip: fc.boolean(), corruption: arbBad ? fc.oneof(arbBad, arbUndefined) : arbUndefined, }), @@ -148,20 +148,18 @@ const { value: arbShallow } = fc.letrec(tie => ({ let problem; for (const propRecord of propRecords) { const { name, skip, corruption } = propRecord; - let { subExpectation } = /** @type {{ subExpectation: T }} */ ( - propRecord - ); - defineDataProperty(specimen, name, subExpectation.specimen); + let { subCase } = /** @type {{ subCase: T }} */ (propRecord); + defineDataProperty(specimen, name, subCase.specimen); if (skip) continue; - problem ||= subExpectation.problem; + problem ||= subCase.problem; if (!problem && corruption) { // @ts-expect-error TS2722 makeBad is not undefined here - subExpectation = makeBad(subExpectation, corruption); - defineDataProperty(specimen, name, subExpectation.specimen); - problem = subExpectation.problem; + subCase = makeBad(subCase, corruption); + defineDataProperty(specimen, name, subCase.specimen); + problem = subCase.problem; } - defineDataProperty(permit, name, subExpectation.permit); - defineDataProperty(attenuation, name, subExpectation.attenuation); + defineDataProperty(permit, name, subCase.permit); + defineDataProperty(attenuation, name, subCase.attenuation); } return { specimen, permit, attenuation, problem }; }) @@ -173,82 +171,82 @@ const { value: arbShallow } = fc.letrec(tie => ({ recurse, ); }; + const { - expectation: arbExpectation, + goodCase: arbGoodCase, badPermit: arbBadPermit, badSpecimen: arbBadSpecimen, specimenMissingKey: arbSpecimenMissingKey, } = fc.letrec(tie => ({ // Happy-path test cases. - expectation: makeArbExpectation(tie('expectation')), + goodCase: makeArbTestCase(tie('goodCase')), // Test cases in which the permit is invalid. - badPermit: makeArbExpectation( + badPermit: makeArbTestCase( tie('badPermit'), arbShallow.filter( x => x !== true && typeof x !== 'string' && !hasObjectType(x), ), - (expectation, badPermit) => { - expectation.permit = badPermit; - expectation.problem = 'bad permit'; - return expectation; + (testCase, badPermit) => { + testCase.permit = badPermit; + testCase.problem = 'bad permit'; + return testCase; }, ).filter( - expectation => - !!(/** @type {AttenuateExpectation} */ (expectation).problem), + testCase => !!(/** @type {AttenuateTestCase} */ (testCase).problem), ), // Test cases in which the permit is an object but the specimen is not. - badSpecimen: makeArbExpectation( + badSpecimen: makeArbTestCase( tie('badSpecimen'), fc.oneof(arbPrimitive, arbFunction), - (expectation, badSpecimen) => { - if (!hasObjectType(expectation.permit)) expectation.permit = {}; - expectation.specimen = badSpecimen; - expectation.problem = 'bad specimen'; - return expectation; + (testCase, badSpecimen) => { + if (!hasObjectType(testCase.permit)) testCase.permit = {}; + testCase.specimen = badSpecimen; + testCase.problem = 'bad specimen'; + return testCase; }, ).filter( - expectation => - !!(/** @type {AttenuateExpectation} */ (expectation).problem), + testCase => !!(/** @type {AttenuateTestCase} */ (testCase).problem), ), // Test cases in which the specimen is missing a permit property. - specimenMissingKey: makeArbExpectation( + specimenMissingKey: makeArbTestCase( tie('specimenMissingKey'), arbString, - (expectation, prop) => { - if (!hasObjectType(expectation.specimen)) expectation.specimen = {}; - if (!hasObjectType(expectation.permit)) { - expectation.permit = { [prop]: true }; + (testCase, prop) => { + if (!hasObjectType(testCase.specimen)) testCase.specimen = {}; + if (!hasObjectType(testCase.permit)) { + testCase.permit = { [prop]: true }; } // Some properties are not configurable (e.g., an array's `length`), so // accept failure as an option. try { - delete (/** @type {any} */ (expectation.specimen)[prop]); - expectation.problem = 'specimen missing key'; + delete (/** @type {any} */ (testCase.specimen)[prop]); + testCase.problem = 'specimen missing key'; // eslint-disable-next-line no-empty } catch (err) {} - return expectation; + return testCase; }, ).filter( - expectation => - !!(/** @type {AttenuateExpectation} */ (expectation).problem), + testCase => !!(/** @type {AttenuateTestCase} */ (testCase).problem), ), })); + testProp( 'attenuate', - /** @type {any} */ ([arbExpectation]), + /** @type {any} */ ([arbGoodCase]), // @ts-expect-error TS2345 function signature async (t, { specimen, permit, attenuation }) => { const actualAttenuation = attenuate(specimen, permit); t.deepEqual(actualAttenuation, attenuation); }, ); + testProp( 'attenuate - transform', /** @type {any} */ ([ - arbExpectation.filter(({ specimen }) => hasObjectType(specimen)), + arbGoodCase.filter(({ specimen }) => hasObjectType(specimen)), ]), // @ts-expect-error TS2345 function signature async (t, { specimen, permit }) => { @@ -287,6 +285,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ } }, ); + testProp( 'attenuate - bad permit', /** @type {any} */ ([arbBadPermit]), @@ -298,6 +297,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ }); }, ); + testProp( 'attenuate - bad specimen', /** @type {any} */ ([arbBadSpecimen]), @@ -309,6 +309,7 @@ const { value: arbShallow } = fc.letrec(tie => ({ }); }, ); + testProp( 'attenuate - specimen missing key', /** @type {any} */ ([arbSpecimenMissingKey]), From 1bbd5935b7fdad86f23e0c6f57a8752a308523de Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 28 Feb 2025 12:59:47 -0500 Subject: [PATCH 7/7] test(internal): Add static tests for `attenuate` --- packages/internal/test/utils.test.js | 84 ++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index 819929c0a2a..e50b9b57533 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -233,6 +233,61 @@ const { value: arbShallow } = fc.letrec(tie => ({ ), })); + test('attenuate static cases', t => { + const specimen = { + foo: 'bar', + baz: [42], + deep: { qux: 1, quux: 2, quuux: 3 }, + }; + const { foo, baz, deep } = specimen; + + t.is( + attenuate(specimen, true), + specimen, + 'blanket permit must preserve identity', + ); + t.is( + attenuate(specimen, 'ok'), + specimen, + 'blanket string permit must preserve identity', + ); + const deepExtraction = attenuate(specimen, { deep: true }); + t.deepEqual(deepExtraction, { deep }); + t.is( + deepExtraction.deep, + deep, + 'deep permit must preserve identity at its depth', + ); + + /** @typedef {Pick} PartialCase */ + /** @type {Record} */ + const cases = { + 'pick 1': { permit: { deep: true }, attenuation: { deep } }, + 'pick 2': { + permit: { foo: true, baz: true }, + attenuation: { foo, baz }, + }, + 'pick 3': { + permit: { foo: true, baz: true, deep: true }, + attenuation: { foo, baz, deep }, + }, + hollow: { + permit: { foo: true, deep: {} }, + attenuation: { foo, deep: {} }, + }, + deep: { + permit: { foo, deep: { quux: true } }, + attenuation: { foo, deep: { quux: 2 } }, + }, + }; + for (const [label, testCase] of Object.entries(cases)) { + const { permit, attenuation: expected } = testCase; + const actual = attenuate(specimen, permit); + // eslint-disable-next-line ava/assertion-arguments + t.deepEqual(actual, expected, label); + } + }); + testProp( 'attenuate', /** @type {any} */ ([arbGoodCase]), @@ -243,6 +298,35 @@ const { value: arbShallow } = fc.letrec(tie => ({ }, ); + test('attenuate - transform static cases', t => { + const specimen = { + foo: 'bar', + arr: [42], + empty: {}, + deep: { qux: 1, quux: 2, quuux: 3 }, + }; + const { foo, arr, empty, deep } = specimen; + const deepClone = { ...deep }; + + const marked = true; + const attenuation = attenuate( + specimen, + { foo: true, arr: true, empty: {}, deep: true }, + /** @type {any} */ (obj => Object.assign(obj, { marked })), + ); + const expected = { marked, foo, arr, empty: { marked }, deep: deepClone }; + t.deepEqual(attenuation, expected); + for (const [label, obj] of Object.entries({ + specimen, + 'array in specimen': arr, + 'object in specimen': empty, + 'whole object in specimen': deep, + })) { + // @ts-expect-error + t.is(obj.marked, undefined, `transformation must not affect ${label}`); + } + }); + testProp( 'attenuate - transform', /** @type {any} */ ([