From 0dd573857a3dcde1396020fed729a64a59b7dcf7 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Sun, 5 May 2024 15:51:12 -0700 Subject: [PATCH] fixup! fix and test bad pass error behavior --- packages/async-flow/src/async-flow.js | 6 +- packages/async-flow/src/replay-membrane.js | 2 + packages/async-flow/test/test-async-flow.js | 6 +- packages/async-flow/test/test-bad-host.js | 208 ++++++++++++++++++++ 4 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 packages/async-flow/test/test-bad-host.js diff --git a/packages/async-flow/src/async-flow.js b/packages/async-flow/src/async-flow.js index aa407d373b15..ed0b96cb3cb4 100644 --- a/packages/async-flow/src/async-flow.js +++ b/packages/async-flow/src/async-flow.js @@ -206,7 +206,9 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => { // async IFFE ensures guestResultP is a fresh promise guestAsyncFunc(...guestArgs))(); - bijection.init(guestResultP, outcomeKit.vow); + if (flow.getFlowState() !== 'Failed') { + bijection.init(guestResultP, outcomeKit.vow); + } // log is driven at first by guestAyncFunc interaction through the // membrane with the host activationArgs. At the end of its first // turn, it returns a promise for its eventual guest result. @@ -347,7 +349,7 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => { // console logging and // resource exhaustion, including infinite loops const err = makeError( - X`In a replay failure: see getFailures() for more information`, + X`In a Failed state: see getFailures() for more information`, ); annotateError(err, X`due to ${fatalProblem}`); throw err; diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 710317327206..9833ea81bfc0 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -119,6 +119,8 @@ export const makeReplayMembrane = ( hostResult = optVerb ? hostTarget[optVerb](...hostArgs) : hostTarget(...hostArgs); + // Try converting here just to route the error correctly + hostToGuest(hostResult, `converting ${optVerb || 'host'} result`); } catch (hostProblem) { return logDo(nestDispatch, harden(['doThrow', callIndex, hostProblem])); } diff --git a/packages/async-flow/test/test-async-flow.js b/packages/async-flow/test/test-async-flow.js index 44de63967a58..d67a1941a6d5 100644 --- a/packages/async-flow/test/test-async-flow.js +++ b/packages/async-flow/test/test-async-flow.js @@ -347,8 +347,6 @@ await test.serial('test virtual async-flow', async t => { await test.serial('test durable async-flow', async t => { annihilate(); - - nextLife(); const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); const vowTools1 = prepareWatchableVowTools(zone1); await testFirstPlay(t, zone1, vowTools1); @@ -372,7 +370,5 @@ await test.serial('test durable async-flow', async t => { nextLife(); const zone4 = makeDurableZone(getBaggage(), 'durableRoot'); const vowTools4 = prepareWatchableVowTools(zone4); - await testAfterPlay(t, zone4, vowTools4); - - await eventLoopIteration(); + return testAfterPlay(t, zone4, vowTools4); }); diff --git a/packages/async-flow/test/test-bad-host.js b/packages/async-flow/test/test-bad-host.js new file mode 100644 index 000000000000..aed2aaf9bdf1 --- /dev/null +++ b/packages/async-flow/test/test-bad-host.js @@ -0,0 +1,208 @@ +// eslint-disable-next-line import/order +import { + test, + getBaggage, + annihilate, + nextLife, +} from './prepare-test-env-ava.js'; + +import { Fail } from '@endo/errors'; +import { M } from '@endo/patterns'; +import { makePromiseKit } from '@endo/promise-kit'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { prepareVowTools } from '@agoric/vow'; +import { prepareVowTools as prepareWatchableVowTools } from '@agoric/vat-data/vow.js'; +import { makeHeapZone } from '@agoric/zone/heap.js'; +import { makeVirtualZone } from '@agoric/zone/virtual.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; + +import { prepareAsyncFlowTools } from '../src/async-flow.js'; + +const nonPassableFunc = () => 'non-passable-function'; +harden(nonPassableFunc); +const guestCreatedPromise = harden(Promise.resolve('guest-created')); +let badResult; + +/** + * @param {Zone} zone + */ +const prepareBadHost = zone => + zone.exoClass( + 'BadHost', + M.interface('BadHost', {}, { defaultGuards: 'raw' }), + () => ({}), + { + badMethod(_badArg = undefined) { + return badResult; + }, + }, + ); + +/** @typedef {ReturnType>} BadHost */ + +// TODO https://github.com/Agoric/agoric-sdk/issues/9231 + +/** + * @param {any} t + * @param {Zone} zone + * @param {VowTools} vowTools + */ +const testBadHostFirstPlay = async (t, zone, vowTools) => { + t.log('badHost firstPlay started'); + const { asyncFlow, adminAsyncFlow } = prepareAsyncFlowTools(zone, { + vowTools, + }); + const makeBadHost = prepareBadHost(zone); + const { makeVowKit } = vowTools; + + const { vow: v1, resolver: _r1 } = zone.makeOnce('v1', () => makeVowKit()); + // purposely violate rule that guestMethod is closed. + const { promise: promiseStep, resolve: resolveStep } = makePromiseKit(); + + const { guestMethod } = { + async guestMethod(badGuest, _p1) { + // nothing bad yet baseline + t.is(badGuest.badMethod(), undefined); + + t.throws(() => badGuest.badMethod(guestCreatedPromise), { + message: 'In a Failed state: see getFailures() for more information', + }); + + resolveStep(true); + t.log(' badHost firstPlay about to return "bogus"'); + // Must not settle outcomeVow + return 'bogus'; + }, + }; + + const wrapperFunc = asyncFlow(zone, 'AsyncFlow1', guestMethod); + + const badHost = zone.makeOnce('badHost', () => makeBadHost()); + + const outcomeV = zone.makeOnce('outcomeV', () => wrapperFunc(badHost, v1)); + + const flow = adminAsyncFlow.getFlowForOutcomeVow(outcomeV); + await promiseStep; + + const fatalProblem = flow.getOptFatalProblem(); + t.throws( + () => { + throw fatalProblem; + }, + { + message: '[3]: [0]: cannot yet send guest promises "[Promise]"', + }, + ); + + t.deepEqual(flow.dump(), [ + ['checkCall', badHost, 'badMethod', [], 0], + ['doReturn', 0, undefined], + // Notice that the bad call was not recorded in the log + ]); + t.log('badHost firstPlay done'); + return promiseStep; +}; + +/** + * @param {any} t + * @param {Zone} zone + * @param {VowTools} vowTools + */ +const testBadHostReplay1 = async (t, zone, vowTools) => { + t.log('badHost replay1 started'); + const { asyncFlow, adminAsyncFlow } = prepareAsyncFlowTools(zone, { + vowTools, + }); + prepareBadHost(zone); + + // const { vow: v1, resolver: r1 } = zone.makeOnce('v1', () => Fail`need v1`); + // purposely violate rule that guestMethod is closed. + const { promise: promiseStep, resolve: resolveStep } = makePromiseKit(); + + const { guestMethod } = { + async guestMethod(badGuest, p1) { + // nothing bad yet baseline + t.is(badGuest.badMethod(), undefined); + + // purposely violate rule that guestMethod is closed. + badResult = nonPassableFunc; + + let gErr; + try { + badGuest.badMethod(); + } catch (err) { + gErr = err; + } + t.throws( + () => { + throw gErr; + }, + { + message: + 'converting badMethod result: "[Symbol(passStyle)]" property expected: "[Function ]"', + }, + ); + t.log(' badHost replay1 guest error caused by host error', gErr); + + resolveStep(true); + t.log(' badHost replay1 to hang awaiting p2'); + // awaiting a promise that won't be resolved until next incarnation + await p1; + t.fail('must not reach here in replay 1'); + }, + }; + + asyncFlow(zone, 'AsyncFlow1', guestMethod); + + const badHost = zone.makeOnce('badHost', () => Fail`need badHost`); + + const outcomeV = zone.makeOnce('outcomeV', () => Fail`need outcomeV`); + + // TODO I shouldn't need to do this. + await adminAsyncFlow.wakeAll(); + + const flow = adminAsyncFlow.getFlowForOutcomeVow(outcomeV); + await promiseStep; + + t.deepEqual(flow.dump(), [ + ['checkCall', badHost, 'badMethod', [], 0], + ['doReturn', 0, undefined], + ['checkCall', badHost, 'badMethod', [], 2], + [ + 'doThrow', + 2, + Error( + 'converting badMethod result: "[Symbol(passStyle)]" property expected: "[Function ]"', + ), + ], + ]); + t.log('badHost replay1 done'); + return promiseStep; +}; + +await test.serial('test heap async-flow bad host', async t => { + const zone = makeHeapZone('heapRoot'); + const vowTools = prepareVowTools(zone); + return testBadHostFirstPlay(t, zone, vowTools); +}); + +await test.serial('test virtual async-flow bad host', async t => { + annihilate(); + const zone = makeVirtualZone('virtualRoot'); + const vowTools = prepareVowTools(zone); + return testBadHostFirstPlay(t, zone, vowTools); +}); + +await test.serial('test durable async-flow bad host', async t => { + annihilate(); + const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); + const vowTools1 = prepareWatchableVowTools(zone1); + await testBadHostFirstPlay(t, zone1, vowTools1); + + await eventLoopIteration(); + + nextLife(); + const zone3 = makeDurableZone(getBaggage(), 'durableRoot'); + const vowTools3 = prepareWatchableVowTools(zone3); + return testBadHostReplay1(t, zone3, vowTools3); +});