From 2f1da9ad78b0a8a9579438ac436a364ca6debf56 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 5 Dec 2022 14:06:26 -0500 Subject: [PATCH 1/3] test(SwingSet): Simplify upgrade testing --- ...grade.js => bootstrap-scripted-upgrade.js} | 0 .../SwingSet/test/upgrade/test-upgrade.js | 219 ++++++------------ 2 files changed, 76 insertions(+), 143 deletions(-) rename packages/SwingSet/test/upgrade/{bootstrap-upgrade.js => bootstrap-scripted-upgrade.js} (100%) diff --git a/packages/SwingSet/test/upgrade/bootstrap-upgrade.js b/packages/SwingSet/test/upgrade/bootstrap-scripted-upgrade.js similarity index 100% rename from packages/SwingSet/test/upgrade/bootstrap-upgrade.js rename to packages/SwingSet/test/upgrade/bootstrap-scripted-upgrade.js diff --git a/packages/SwingSet/test/upgrade/test-upgrade.js b/packages/SwingSet/test/upgrade/test-upgrade.js index 9c2a903cefb..1f7ff2e1527 100644 --- a/packages/SwingSet/test/upgrade/test-upgrade.js +++ b/packages/SwingSet/test/upgrade/test-upgrade.js @@ -35,6 +35,23 @@ const dumpState = (hostStorage, vatID) => { } }; +const makeRun = swingsetController => { + const run = async (method, args = []) => { + assert(Array.isArray(args)); + const kpid = swingsetController.queueToVatRoot('bootstrap', method, args); + await swingsetController.run(); + const status = swingsetController.kpStatus(kpid); + if (status === 'fulfilled') { + const result = swingsetController.kpResolution(kpid); + return kunser(result); + } + assert(status === 'rejected'); + const err = swingsetController.kpResolution(kpid); + throw kunser(err); + }; + return run; +}; + const testUpgrade = async ( t, defaultManagerType, @@ -47,7 +64,7 @@ const testUpgrade = async ( // defaultReapInterval: 'never', // defaultReapInterval: 1, vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -63,37 +80,24 @@ const testUpgrade = async ( t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); await c.run(); + const run = makeRun(c); - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; - - const mcd = await run('getMarker'); - t.is(mcd[0], 'fulfilled'); - const marker = kunser(mcd[1]); // probably ko26 + const marker = await run('getMarker'); // probably ko26 t.is(marker.iface(), 'marker'); // fetch all the "importSensors": exported by bootstrap, imported by // the upgraded vat. We'll determine their krefs and later query the // upgraded vat to see if it's still importing them or not - const [impstatus, impresult] = await run('getImportSensors', []); - t.is(impstatus, 'fulfilled'); + const impresult = await run('getImportSensors', []); // eslint-disable-next-line no-unused-vars - const impKrefs = ['skip0', ...kunser(impresult).slice(1).map(krefOf)]; + const impKrefs = ['skip0', ...impresult.slice(1).map(krefOf)]; if (doVatAdminRestart) { await restartVatAdminVat(c); } // create initial version - const [v1status, v1resultEnc] = await run('buildV1', []); - const v1result = kunser(v1resultEnc); - t.is(v1status, 'fulfilled'); + const v1result = await run('buildV1', []); t.is(v1result.version, 'v1'); t.is(v1result.youAre, 'v1'); t.truthy(krefOf(v1result.marker)); @@ -170,9 +174,7 @@ const testUpgrade = async ( // now perform the upgrade // console.log(`-- starting upgradeV2`); - const [v2status, v2resultEnc] = await run('upgradeV2', []); - const v2result = kunser(v2resultEnc); - t.is(v2status, 'fulfilled'); + const v2result = await run('upgradeV2', []); t.deepEqual(v2result.version, 'v2'); t.deepEqual(v2result.youAre, 'v2'); t.deepEqual(krefOf(v2result.marker), krefOf(marker)); @@ -291,7 +293,7 @@ test('vat upgrade - omit vatParameters', async t => { bootstrap: 'bootstrap', defaultReapInterval: 'never', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -306,20 +308,11 @@ test('vat upgrade - omit vatParameters', async t => { t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (name, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', name, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; + const run = makeRun(c); // create initial version - const [status, result] = await run('doUpgradeWithoutVatParameters', []); - t.is(status, 'fulfilled'); - t.deepEqual(kunser(result), [undefined, undefined]); + const result = await run('doUpgradeWithoutVatParameters', []); + t.deepEqual(result, [undefined, undefined]); }); test('failed upgrade - relaxed durable rules', async t => { @@ -328,7 +321,7 @@ test('failed upgrade - relaxed durable rules', async t => { includeDevDependencies: true, // for vat-data bootstrap: 'bootstrap', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -343,26 +336,16 @@ test('failed upgrade - relaxed durable rules', async t => { t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; + const run = makeRun(c); // create initial version - const [v1status] = await run('buildV1', []); - t.is(v1status, 'fulfilled'); + await run('buildV1', []); // upgrade should fail - const [v2status, v2result] = await run('upgradeV2', []); - t.is(v2status, 'rejected'); - const e = kunser(v2result); - t.truthy(e instanceof Error); - t.regex(e.message, /vat-upgrade failure/); + await t.throwsAsync(run('upgradeV2', []), { + instanceOf: Error, + message: /vat-upgrade failure/, + }); }); test('failed upgrade - lost kind', async t => { @@ -372,7 +355,7 @@ test('failed upgrade - lost kind', async t => { bootstrap: 'bootstrap', defaultReapInterval: 'never', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -387,26 +370,15 @@ test('failed upgrade - lost kind', async t => { t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; + const run = makeRun(c); // create initial version - const [v1status, v1result] = await run('buildV1WithLostKind', []); - t.is(v1status, 'fulfilled'); - t.deepEqual(kunser(v1result), ['ping 1']); + const v1result = await run('buildV1WithLostKind', []); + t.deepEqual(v1result, ['ping 1']); // upgrade should fail, get rewound console.log(`note: expect a 'defineDurableKind not called' error below`); - const [v2status, v2result] = await run('upgradeV2WhichLosesKind', []); - t.is(v2status, 'fulfilled'); - const events = kunser(v2result); + const events = await run('upgradeV2WhichLosesKind', []); t.is(events[0], 'ping 2'); // The v2 vat starts with a 'ping from v2' (which will be unwound). @@ -442,7 +414,7 @@ test('failed upgrade - explode', async t => { bootstrap: 'bootstrap', defaultReapInterval: 'never', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -455,25 +427,14 @@ test('failed upgrade - explode', async t => { const c = await makeSwingsetController(hostStorage); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; + const run = makeRun(c); // create initial version - const [v1status, v1result] = await run('buildV1WithPing', []); - t.is(v1status, 'fulfilled'); - t.deepEqual(kunser(v1result), ['hello from v1', 'ping 1']); + const v1result = await run('buildV1WithPing', []); + t.deepEqual(v1result, ['hello from v1', 'ping 1']); // upgrade should fail, error returned in array - const [v2status, v2result] = await run('upgradeV2WhichExplodes', []); - t.is(v2status, 'fulfilled'); - const events = kunser(v2result); + const events = await run('upgradeV2WhichExplodes', []); const e = events[0]; t.truthy(e instanceof Error); t.regex(e.message, /vat-upgrade failure/); @@ -497,7 +458,7 @@ async function testMultiKindUpgradeChecks(t, mode, complaint) { bootstrap: 'bootstrap', defaultReapInterval: 'never', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -512,35 +473,25 @@ async function testMultiKindUpgradeChecks(t, mode, complaint) { t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; + const run = makeRun(c); // create initial version - const [v1status] = await run('buildV1WithMultiKind', [mode]); - t.is(v1status, 'fulfilled'); - - // upgrade should fail - if (complaint) { - console.log(`note: expect a '${complaint}' error below`); - } - const [v2status, v2result] = await run('upgradeV2Simple', [mode]); - if (complaint) { - t.is(v2status, 'rejected'); - const e = kunser(v2result); - t.truthy(e instanceof Error); - t.regex(e.message, /vat-upgrade failure/); - // TODO: who should see the details of what v2 did wrong? calling - // vat? only the console? - } else { - t.is(v2status, 'fulfilled'); + await run('buildV1WithMultiKind', [mode]); + + // upgrade + const resultP = run('upgradeV2Simple', [mode]); + if (!complaint) { + await resultP; + t.pass(); + return; } + console.log(`note: expect a '${complaint}' error below`); + // TODO: who should see the details of what v2 did wrong? calling + // vat? only the console? + await t.throwsAsync(resultP, { + instanceOf: Error, + message: /vat-upgrade failure/, + }); } test('facet kind redefinition - fail on facet count mismatch', async t => { @@ -590,7 +541,7 @@ test('failed upgrade - unknown options', async t => { bootstrap: 'bootstrap', defaultReapInterval: 'never', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -605,24 +556,15 @@ test('failed upgrade - unknown options', async t => { t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (name, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', name, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; - - const [status, result] = await run('doUpgradeWithBadOption', []); - t.is(status, 'rejected'); - const e = kunser(result); - t.truthy(e instanceof Error); - // TODO Since we should be running with `errorTaming: unsafe`, the - // following should have worked. - // t.regex(e.message, /upgrade\(\) received unknown options: bad/); - t.regex(e.message, /upgrade\(\) received unknown options: \(a string\)/); + const run = makeRun(c); + + await t.throwsAsync(run('doUpgradeWithBadOption', []), { + instanceOf: Error, + // TODO Since we should be running with `errorTaming: unsafe`, the + // following should have worked. + // message: /upgrade\(\) received unknown options: bad/, + message: /upgrade\(\) received unknown options: \(a string\)/, + }); }); test('failed vatAdmin upgrade - bad replacement code', async t => { @@ -631,7 +573,7 @@ test('failed vatAdmin upgrade - bad replacement code', async t => { bootstrap: 'bootstrap', defaultReapInterval: 'never', vats: { - bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, + bootstrap: { sourceSpec: bfile('bootstrap-scripted-upgrade.js') }, }, bundles: { ulrik1: { sourceSpec: bfile('vat-ulrik-1.js') }, @@ -643,15 +585,7 @@ test('failed vatAdmin upgrade - bad replacement code', async t => { const c = await makeSwingsetController(hostStorage); c.pinVatRoot('bootstrap'); await c.run(); - - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - const result = c.kpResolution(kpid); - return [status, result]; - }; + const run = makeRun(c); const badVABundle = await bundleSource( new URL('./vat-junk.js', import.meta.url).pathname, @@ -667,8 +601,7 @@ test('failed vatAdmin upgrade - bad replacement code', async t => { t.regex(vaUpgradeResult.message, /vat-upgrade failure/); // Now try doing something that uses vatAdmin to verify that original vatAdmin is intact. - const [v1status, v1result] = await run('buildV1', []); - t.is(v1status, 'fulfilled'); + const v1result = await run('buildV1', []); // Just a taste to verify that the create went right; other tests check the rest - t.deepEqual(kunser(v1result).data, ['some', 'data']); + t.deepEqual(v1result.data, ['some', 'data']); }); From e148113f62c6434c279dcc62ca04bdcea08aea07 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 5 Dec 2022 15:56:03 -0500 Subject: [PATCH 2/3] test(SwingSet): Add minimal null-upgrade testing --- packages/SwingSet/test/bootstrap-relay.js | 98 +++++++++++++++++++ .../SwingSet/test/upgrade/test-upgrade.js | 59 +++++++++++ .../SwingSet/test/vat-durable-singleton.js | 23 +++++ 3 files changed, 180 insertions(+) create mode 100644 packages/SwingSet/test/bootstrap-relay.js create mode 100644 packages/SwingSet/test/vat-durable-singleton.js diff --git a/packages/SwingSet/test/bootstrap-relay.js b/packages/SwingSet/test/bootstrap-relay.js new file mode 100644 index 00000000000..d589bdea652 --- /dev/null +++ b/packages/SwingSet/test/bootstrap-relay.js @@ -0,0 +1,98 @@ +import { E } from '@endo/eventual-send'; +import { Far, makeMarshal } from '@endo/marshal'; +import { assert } from '@agoric/assert'; + +const { Fail, quote: q } = assert; + +export const buildRootObject = () => { + let vatAdmin; + const vatData = new Map(); + + // Represent all data as passable by replacing non-passable values + // with special-prefix registered symbols. + const replaced = new Map(); + // This is testing code, so we don't enforce absence of this prefix + // from manually created symbols. + const replacementPrefix = 'replaced:'; + const makeReplacement = value => { + const replacement = Symbol.for(`${replacementPrefix}${replaced.size}`); + replaced.set(replacement, value); + return replacement; + }; + const { serialize: encodeReplacements } = makeMarshal( + makeReplacement, + undefined, + { + marshalSaveError: () => {}, + serializeBodyFormat: 'capdata', + }, + ); + const { unserialize: decodeReplacements } = makeMarshal( + undefined, + undefined, + { + serializeBodyFormat: 'capdata', + }, + ); + const encodePassable = value => decodeReplacements(encodeReplacements(value)); + const decodePassable = arg => { + // This is testing code, so we look for replacements only at top level. + if (typeof arg !== 'symbol' || !Symbol.keyFor(arg)) { + return arg; + } + const { description } = arg; + if (!description.startsWith(replacementPrefix)) { + return arg; + } + const value = + replaced.get(arg) || Fail`no value for replacement: ${q(arg)}`; + return value; + }; + + return Far('root', { + bootstrap: async (vats, devices) => { + vatAdmin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); + }, + + createVat: async ({ name, bundleCapName, vatParameters = {} }) => { + const bcap = await E(vatAdmin).getNamedBundleCap(bundleCapName); + const options = { vatParameters }; + const { adminNode, root } = await E(vatAdmin).createVat(bcap, options); + vatData.set(name, { adminNode, root }); + return root; + }, + + upgradeVat: async ({ name, bundleCapName, vatParameters = {} }) => { + const vat = vatData.get(name) || Fail`unknown vat name: ${q(name)}`; + const bcap = await E(vatAdmin).getNamedBundleCap(bundleCapName); + const options = { vatParameters }; + const incarnationNumber = await E(vat.adminNode).upgrade(bcap, options); + vat.incarnationNumber = incarnationNumber; + return incarnationNumber; + }, + + messageVat: async ({ name, methodName, args = [] }) => { + const vat = vatData.get(name) || Fail`unknown vat name: ${q(name)}`; + const { root } = vat; + const decodedArgs = args.map(decodePassable); + const result = await E(root)[methodName](...decodedArgs); + return encodePassable(result); + }, + + messageVatObject: async ({ presence, methodName, args = [] }) => { + const object = decodePassable(presence); + const decodedArgs = args.map(decodePassable); + const result = await E(object)[methodName](...decodedArgs); + return encodePassable(result); + }, + + awaitVatObject: async ({ presence, path = [] }) => { + let value = await decodePassable(presence); + for (const key of path) { + // eslint-disable-next-line no-await-in-loop + value = await value[key]; + } + return encodePassable(value); + }, + }); +}; diff --git a/packages/SwingSet/test/upgrade/test-upgrade.js b/packages/SwingSet/test/upgrade/test-upgrade.js index 1f7ff2e1527..0afa2281d9a 100644 --- a/packages/SwingSet/test/upgrade/test-upgrade.js +++ b/packages/SwingSet/test/upgrade/test-upgrade.js @@ -52,6 +52,65 @@ const makeRun = swingsetController => { return run; }; +const testNullUpgrade = async (t, defaultManagerType) => { + const config = { + includeDevDependencies: true, // for vat-data + defaultManagerType, + bootstrap: 'bootstrap', + defaultReapInterval: 'never', + vats: { + bootstrap: { sourceSpec: bfile('../bootstrap-relay.js') }, + }, + bundles: { + durableSingleton: { sourceSpec: bfile('../vat-durable-singleton.js') }, + }, + }; + + const hostStorage = provideHostStorage(); + const { initOpts, runtimeOpts } = bundleOpts(t.context.data); + await initializeSwingset(config, [], hostStorage, initOpts); + const c = await makeSwingsetController(hostStorage, {}, runtimeOpts); + t.teardown(c.shutdown); + c.pinVatRoot('bootstrap'); + await c.run(); + const run = makeRun(c); + + await run('createVat', [ + { + name: 'durableSingleton', + bundleCapName: 'durableSingleton', + vatParameters: { version: 'v1' }, + }, + ]); + t.is( + await run('messageVat', [ + { name: 'durableSingleton', methodName: 'getVersion' }, + ]), + 'v1', + ); + await run('upgradeVat', [ + { + name: 'durableSingleton', + bundleCapName: 'durableSingleton', + vatParameters: { version: 'v2' }, + }, + ]); + t.is( + await run('messageVat', [ + { name: 'durableSingleton', methodName: 'getVersion' }, + ]), + 'v2', + ); +}; + +test('null upgrade - local', async t => { + return testNullUpgrade(t, 'local'); +}); + +test('null upgrade - xsnap', async t => { + return testNullUpgrade(t, 'xs-worker'); +}); + const testUpgrade = async ( t, defaultManagerType, diff --git a/packages/SwingSet/test/vat-durable-singleton.js b/packages/SwingSet/test/vat-durable-singleton.js new file mode 100644 index 00000000000..6f49b73783e --- /dev/null +++ b/packages/SwingSet/test/vat-durable-singleton.js @@ -0,0 +1,23 @@ +import { Far } from '@endo/marshal'; +import { M } from '@agoric/store'; +import { provide, vivifyFarClassKit } from '@agoric/vat-data'; + +export const buildRootObject = (_vatPowers, vatParameters, baggage) => { + const { version } = vatParameters; + + // Define and guarantee a single durable instance of a minimal far class. + const emptyFacetI = M.interface('Facet', {}); + const iKit = harden({ facet1: emptyFacetI, facet2: emptyFacetI }); + const initState = () => ({}); + const makeInstance = vivifyFarClassKit(baggage, 'ClassKit', iKit, initState, { + facet1: {}, + facet2: {}, + }); + const singleton = provide(baggage, 'durableSingleton', () => makeInstance()); + + return Far('root', { + getVersion: () => version, + getParameters: () => vatParameters, + getSingleton: () => singleton, + }); +}; From 2015a0a6c6489b1f676e6176afbb17781186eabb Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 5 Dec 2022 15:57:24 -0500 Subject: [PATCH 3/3] fix(SwingSet): Restore LRU cache flushing when stopping a vat Fixes #6604 --- packages/SwingSet/src/liveslots/stop-vat.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/SwingSet/src/liveslots/stop-vat.js b/packages/SwingSet/src/liveslots/stop-vat.js index 1c369120f5f..1c977b8c7cf 100644 --- a/packages/SwingSet/src/liveslots/stop-vat.js +++ b/packages/SwingSet/src/liveslots/stop-vat.js @@ -317,8 +317,13 @@ export async function releaseOldState(tools) { identifyExportedFacets(abandonedVrefSet, tools); abandonExports(abandonedVrefSet, tools); - /* Disabling the rest of this in the interest of stop-vat performance. We - * expect that in the fullness of time the following will be superseded by a + // bringOutYourDead remains to ensure that the LRU cache is flushed, + // but the rest of this function has been disabled to improve stop-vat + // performance. + // eslint-disable-next-line no-use-before-define + await tools.bringOutYourDead(); + + /* We expect that in the fullness of time the following will be superseded by a * post-upgrade scavenger process that cleans up dead database debris * incrementally, rather than taking the hit of a potentially large delay at * shutdown time. If that change happens, the below code can simply be