Skip to content

Commit

Permalink
test: add unit test for plugin device
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Sep 16, 2020
1 parent aa37b4f commit 14b3ab2
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,38 @@ import { makeCapTP } from '@agoric/captp';
import { makePromiseKit } from '@agoric/promise-kit';
import { E, HandledPromise } from '@agoric/eventual-send';

/**
* @template T
* @typedef {T | PromiseLike<T>} ERef
*/

/** @type {{ onReset: (firstTime: Promise<boolean>) => void}} */
const DEFAULT_RESETTER = harden({ onReset: _ => {} });

/** @type {{ walk: (pluginRootP: any) => any }} */
const DEFAULT_WALKER = harden({ walk: pluginRootP => pluginRootP });

/**
* @template T
* @typedef {T} Device
*/

/**
* @callback LoadPlugin
* @param {string} specifier
* @param {any} [opts=undefined]
* @param {{ onReset: (firstTime: Promise<boolean>) => void}} [resetter=DEFAULT_RESETTER]
* @returns {ERef<{ pluginRoot: ERef<any>, actions: { makeStableForwarder:
* MakeStableForwarder }}>}
*
* @callback MakeStableForwarder
* @param {{ walk: (pluginRootP: Promise<any>) => any }} [walker=DEFAULT_WALKER]
* @returns {ERef<any>}
*/

/**
* @typedef {Object} PluginManager
* @property {(mod: string) => ERef<any>} load
* @property {LoadPlugin} load
*/

/**
Expand All @@ -34,7 +58,7 @@ import { E, HandledPromise } from '@agoric/eventual-send';
* Create a handler that manages a promise interface to external modules.
*
* @param {Device<PluginDevice>} pluginDevice The bridge to manage
* @param {{ D<T>(target: Device<T>): T }} param1
* @param {{ D: <T>(target: Device<T>) => T, [prop: string]: any }} param1
* @returns {PluginManager} admin facet for this handler
*/
export function makePluginManager(pluginDevice, { D, ...vatPowers }) {
Expand Down Expand Up @@ -68,13 +92,16 @@ export function makePluginManager(pluginDevice, { D, ...vatPowers }) {
return D(pluginDevice).getPluginDir();
},
/**
* Load a module, and call resetter.onReset(bootP) every time it is instantiated.
* Load a module, and call resetter.onReset(pluginRootP) every time
* it is instantiated.
*
* @type {LoadPlugin}
*/
load(specifier, opts = undefined, resetter = { onReset: _ => {} }) {
load(specifier, opts = undefined, resetter = DEFAULT_RESETTER) {
// This is the internal state: a promise kit that doesn't
// resolve until we are connected. It is replaced by
// a new promise kit when we abort the prior module connection.
let bootPK = makePromiseKit();
let pluginRootPK = makePromiseKit();
let nextEpoch = 0;

let currentEpoch;
Expand Down Expand Up @@ -108,7 +135,7 @@ export function makePluginManager(pluginDevice, { D, ...vatPowers }) {
// Create a CapTP channel.
const myEpoch = nextEpoch;
nextEpoch += 1;
console.info(
console.debug(
`Connecting to ${specifier}.${index} with epoch ${myEpoch}`,
);
const { getBootstrap, dispatch } = makeCapTP(
Expand All @@ -122,10 +149,10 @@ export function makePluginManager(pluginDevice, { D, ...vatPowers }) {
);

currentReset = _epoch => {
bootPK = makePromiseKit();
pluginRootPK = makePromiseKit();

// Tell our clients we are resetting.
E(resetter).onReset(bootPK.promise.then(_ => true));
E(resetter).onReset(pluginRootPK.promise.then(_ => true));

// Attempt to restart the protocol using the same device connection.
connect();
Expand All @@ -139,26 +166,28 @@ export function makePluginManager(pluginDevice, { D, ...vatPowers }) {
currentEpoch = myEpoch;

// Publish our started plugin.
bootPK.resolve(E(getBootstrap()).start(opts));
pluginRootPK.resolve(E(getBootstrap()).start(opts));
};

const actions = harden({
/**
* Create a stable identity that just forwards to the current implementation.
*
* @type {MakeStableForwarder}
*/
makeStableForwarder(walker = { walk: bootP => bootP }) {
makeStableForwarder(walker = DEFAULT_WALKER) {
let pr;
// eslint-disable-next-line no-new
new HandledPromise((_resolve, _reject, resolveWithPresence) => {
pr = resolveWithPresence({
applyMethod(_p, name, args) {
// console.warn('applying method epoch', currentEpoch);
const targetP = E(walker).walk(bootPK.promise);
const targetP = E(walker).walk(pluginRootPK.promise);
return HandledPromise.applyMethod(targetP, name, args);
},
get(_p, name) {
// console.warn('applying get epoch', currentEpoch);
const targetP = E(walker).walk(bootPK.promise);
const targetP = E(walker).walk(pluginRootPK.promise);
return HandledPromise.get(targetP, name);
},
});
Expand All @@ -168,16 +197,16 @@ export function makePluginManager(pluginDevice, { D, ...vatPowers }) {
});

// Declare the first reset.
E(resetter).onReset(false);
E(resetter).onReset(Promise.resolve(false));

// Start the first connection.
connect();

// Give up our bootstrap object for the caller to use.
// Give up our plugin root object for the caller to use.
return harden({
// This is the public state, a promise that never resolves,
// but pipelines messages to the bootPK.promise.
bootstrap: actions.makeStableForwarder(),
// but pipelines messages to the pluginRootPK.promise.
pluginRoot: actions.makeStableForwarder(),
actions,
});
},
Expand Down
33 changes: 33 additions & 0 deletions packages/SwingSet/test/device-plugin/bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* global harden */

import { E } from '@agoric/eventual-send';
import { makePluginManager } from '../../src/vats/plugin-manager';

export function buildRootObject(vatPowers, vatParameters) {
const { D } = vatPowers;
const log = vatPowers.testLog;
return harden({
async bootstrap(vats, devices) {
try {
const { argv } = vatParameters;
if (argv[0] === 'plugin') {
log(`starting plugin test`);
const pluginManager = makePluginManager(devices.plugin, vatPowers);
const { pluginRoot: pingPongP } = await E(pluginManager).load(
'pingpong',
{
prefix: 'Whoopie ',
},
);
E(vats.bridge).init(pingPongP);
D(devices.bridge).registerInboundHandler(vats.bridge);
} else {
throw new Error(`unknown argv mode '${argv[0]}'`);
}
} catch (e) {
console.error('have error', e);
throw e;
}
},
});
}
14 changes: 14 additions & 0 deletions packages/SwingSet/test/device-plugin/pingpong.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* global harden */

export function bootPlugin() {
return harden({
start(opts) {
const { prefix } = opts;
return harden({
ping(msg) {
return `${prefix}${msg}`;
},
});
},
});
}
93 changes: 93 additions & 0 deletions packages/SwingSet/test/device-plugin/test-device.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import '@agoric/install-ses';
import test from 'ava';
import { initSwingStore } from '@agoric/swing-store-simple';

import { buildVatController } from '../../src/index';
import { buildBridge } from '../../src/devices/bridge';
import { buildPlugin } from '../../src/devices/plugin';

test.before('initialize hostStorage', t => {
const { storage } = initSwingStore(null);
t.context.hostStorage = storage;
});

const setupVatController = async t => {
const inputQueue = [];
const queueThunkForKernel = async thunk => {
inputQueue.push(thunk);
};

const pluginRequire = mod => {
t.is(mod, 'pingpong');
// eslint-disable-next-line global-require
return require('./pingpong');
};
const plugin = buildPlugin(__dirname, pluginRequire, queueThunkForKernel);
const bridge = buildBridge();
const config = {
bootstrap: 'bootstrap',
vats: {
bootstrap: {
sourceSpec: require.resolve('./bootstrap'),
},
bridge: {
sourceSpec: require.resolve('./vat-bridge'),
},
},
devices: [
['plugin', plugin.srcPath, plugin.endowments],
['bridge', bridge.srcPath, bridge.endowments],
],
};
const c = await buildVatController(config, ['plugin'], {
hostStorage: t.context.hostStorage,
});
const cycle = async () => {
await c.run();
while (inputQueue.length) {
inputQueue.shift()();
// eslint-disable-next-line no-await-in-loop
await c.run();
}
};
return { bridge, cycle, dump: c.dump, plugin, queueThunkForKernel };
};

test.serial('plugin first time', async t => {
const { bridge, cycle, dump, queueThunkForKernel } = await setupVatController(
t,
);

queueThunkForKernel(() => bridge.deliverInbound('pingpong'));
await cycle();

t.deepEqual(dump().log, [
'starting plugin test',
'installing pingPongP',
'starting pingpong test',
'pingpong reply = Whoopie Agoric!',
]);
});

test.serial('plugin after restart', async t => {
const {
bridge,
cycle,
dump,
plugin,
queueThunkForKernel,
} = await setupVatController(t);

plugin.reset();
queueThunkForKernel(() => bridge.deliverInbound('pingpong'));
await cycle();

t.deepEqual(dump().log, [
'starting plugin test',
'installing pingPongP',
'starting pingpong test',
'pingpong reply = Whoopie Agoric!',
'starting pingpong test',
'pingpong reply = Whoopie Agoric!',
]);
});
32 changes: 32 additions & 0 deletions packages/SwingSet/test/device-plugin/vat-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* global harden */

import { E } from '@agoric/eventual-send';

export function buildRootObject(vatPowers, _vatParameters) {
const log = vatPowers.testLog;
let pingPongP;
return harden({
init(pingPong) {
log(`installing pingPongP`);
pingPongP = pingPong;
},
async inbound(msg) {
try {
switch (msg) {
case 'pingpong': {
log(`starting pingpong test`);
const pong = await E(pingPongP).ping('Agoric!');
log(`pingpong reply = ${pong}`);
break;
}
default: {
throw new Error(`unknown bridge input ${msg}`);
}
}
} catch (e) {
console.error('failed with', e);
log(`failed: ${e}`);
}
},
});
}
2 changes: 1 addition & 1 deletion packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { E } from '@agoric/eventual-send';

// this will return { undefined } until `ag-solo set-gci-ingress`
// has been run to update gci.js
import { makePluginManager } from '@agoric/swingset-vat/src/vats/plugin-manager';
import { GCI } from './gci';
import { makeBridgeManager } from './bridge';
import { makePluginManager } from './plugin';

const NUM_IBC_PORTS = 3;

Expand Down

0 comments on commit 14b3ab2

Please sign in to comment.