diff --git a/packages/ERTP/test/unitTests/test-issuerObj.js b/packages/ERTP/test/unitTests/test-issuerObj.js index 978c5eec307..7c95c1147fe 100644 --- a/packages/ERTP/test/unitTests/test-issuerObj.js +++ b/packages/ERTP/test/unitTests/test-issuerObj.js @@ -337,7 +337,7 @@ test('issuer.combine bad payments', t => { t.rejects( () => E(issuer).combine(payments), - /payment not found/, + /"payment" not found/, 'payment from other mint is not found', ); } catch (e) { diff --git a/packages/assert/src/assert.js b/packages/assert/src/assert.js index 2736223a642..1180c3c9882 100644 --- a/packages/assert/src/assert.js +++ b/packages/assert/src/assert.js @@ -25,13 +25,40 @@ function an(str) { } harden(an); +/** + * Like `JSON.stringify` but does not blow up if given a cycle. This is not + * intended to be a serialization to support any useful unserialization, + * or any programmatic use of the resulting string. The string is intended + * only for showing a human, in order to be informative enough for some + * logging purposes. As such, this `cycleTolerantStringify` has an + * imprecise specification and may change over time. + * + * The current `cycleTolerantStringify` possibly emits too many "seen" + * markings: Not only for cycles, but also for repeated subtrees by + * object identity. + */ +function cycleTolerantStringify(payload) { + const seenSet = new Set(); + const replacer = (_, val) => { + if (typeof val === 'object' && val !== null) { + if (seenSet.has(val)) { + return '<**seen**>'; + } + seenSet.add(val); + } + return val; + }; + return JSON.stringify(payload, replacer); +} + const declassifiers = new WeakSet(); /** - * To "declassify" a substitution value used in a details`...` template literal, - * enclose that substitution expression in a call to openDetail. This states - * that the argument should appear, stringified, in the error message of the - * thrown error. + * To "declassify" and quote a substitution value used in a + * details`...` template literal, enclose that substitution expression + * in a call to `q`. This states that the argument should appear quoted (with + * `JSON.stringify`), in the error message of the thrown error. The payload + * itself is still passed unquoted to the console as it would be without q. * * Starting from the example in the `details` comment, say instead that the * color the sky is supposed to be is also computed. Say that we still don't @@ -41,7 +68,7 @@ const declassifiers = new WeakSet(); * assert.equal( * sky.color, * color, - * details`${sky.color} should be ${openDetail(color)}`, + * details`${sky.color} should be ${q(color)}`, * ); * ``` * @@ -52,24 +79,23 @@ const declassifiers = new WeakSet(); * @param {*} payload What to declassify * @returns {StringablePayload} The declassified payload */ -function openDetail(payload) { - const result = harden({ +function q(payload) { + // Don't harden the payload + const result = Object.freeze({ payload, - toString() { - return payload.toString(); - }, + toString: Object.freeze(() => cycleTolerantStringify(payload)), }); declassifiers.add(result); return result; } -harden(openDetail); +harden(q); /** * Use the `details` function as a template literal tag to create * informative error messages. The assertion functions take such messages * as optional arguments: * ```js - * assert(sky.isBlue(), details`${sky.color} should be blue`); + * assert(sky.isBlue(), details`${sky.color} should be "blue"`); * ``` * The details template tag returns an object that can print itself with the * formatted message in two ways. It will report the real details to the @@ -112,8 +138,8 @@ function details(template, ...args) { let arg = args[i]; let argStr; if (declassifiers.has(arg)) { - arg = arg.payload; argStr = `${arg}`; + arg = arg.payload; } else { argStr = `(${an(typeof arg)})`; } @@ -158,7 +184,9 @@ harden(details); */ function fail(optDetails = details`Assert failed`) { if (typeof optDetails === 'string') { - optDetails = details`${openDetail(optDetails)}`; + // If it is a string, use it as the literal part of the template so + // it doesn't get quoted. + optDetails = details([optDetails]); } throw optDetails.complain(); } @@ -185,14 +213,14 @@ function fail(optDetails = details`Assert failed`) { * @param {*} flag The truthy/falsy value * @param {Details} [optDetails] The details to throw */ -function assert(flag, optDetails = details`check failed`) { +function assert(flag, optDetails = details`Check failed`) { if (!flag) { fail(optDetails); } } /** - * Assert that two values must be `===`. + * Assert that two values must be `Object.is`. * @param {*} actual The value we received * @param {*} expected What we wanted * @param {Details} [optDetails] The details to throw @@ -200,9 +228,9 @@ function assert(flag, optDetails = details`check failed`) { function equal( actual, expected, - optDetails = details`Expected ${actual} === ${expected}`, + optDetails = details`Expected ${actual} is same as ${expected}`, ) { - assert(actual === expected, optDetails); + assert(Object.is(actual, expected), optDetails); } /** @@ -212,12 +240,20 @@ function equal( * @param {string} typename The expected name * @param {Details} [optDetails] The details to throw */ -function assertTypeof( - specimen, - typename, - optDetails = details`${specimen} must be ${openDetail(an(typename))}`, -) { - assert(typeof typename === 'string', details`${typename} must be a string`); +function assertTypeof(specimen, typename, optDetails) { + assert( + typeof typename === 'string', + details`${q(typename)} must be a string`, + ); + if (optDetails === undefined) { + // Like + // ```js + // optDetails = details`${specimen} must be ${q(an(typename))}`; + // ``` + // except it puts the typename into the literal part of the template + // so it doesn't get quoted. + optDetails = details(['', ` must be ${an(typename)}`], specimen); + } equal(typeof specimen, typename, optDetails); } @@ -226,4 +262,4 @@ assert.fail = fail; assert.typeof = assertTypeof; harden(assert); -export { assert, details, openDetail, an }; +export { assert, details, q, an }; diff --git a/packages/assert/test/test-assert.js b/packages/assert/test/test-assert.js index 528f29c860e..dcfb08b1ec0 100644 --- a/packages/assert/test/test-assert.js +++ b/packages/assert/test/test-assert.js @@ -1,5 +1,5 @@ import { test } from 'tape'; -import { an, assert, details, openDetail } from '../src/assert'; +import { an, assert, details, q } from '../src/assert'; import { throwsAndLogs } from './throwsAndLogs'; test('an', t => { @@ -45,16 +45,31 @@ test('throwsAndLogs', t => { test('assert', t => { try { assert(2 + 3 === 5); - assert.equal(2 + 3, 5); - throwsAndLogs(t, () => assert(false), /check failed/, [ - ['error', 'LOGGED ERROR:', 'check failed'], + + throwsAndLogs(t, () => assert(false), /Check failed/, [ + ['error', 'LOGGED ERROR:', 'Check failed'], ]); - throwsAndLogs( - t, - () => assert.equal(5, 6), - /Expected \(a number\) === \(a number\)/, - [['error', 'LOGGED ERROR:', 'Expected', 5, '===', 6]], - ); + throwsAndLogs(t, () => assert(false, 'foo'), /foo/, [ + ['error', 'LOGGED ERROR:', 'foo'], + ]); + + throwsAndLogs(t, () => assert.fail(), /Assert failed/, [ + ['error', 'LOGGED ERROR:', 'Assert failed'], + ]); + throwsAndLogs(t, () => assert.fail('foo'), /foo/, [ + ['error', 'LOGGED ERROR:', 'foo'], + ]); + } catch (e) { + console.log('unexpected exception', e); + t.assert(false, e); + } finally { + t.end(); + } +}); + +test('assert equals', t => { + try { + assert.equal(2 + 3, 5); throwsAndLogs(t, () => assert.equal(5, 6, 'foo'), /foo/, [ ['error', 'LOGGED ERROR:', 'foo'], ]); @@ -66,10 +81,84 @@ test('assert', t => { ); throwsAndLogs( t, - () => assert.equal(5, 6, details`${5} !== ${openDetail(6)}`), + () => assert.equal(5, 6, details`${5} !== ${q(6)}`), /\(a number\) !== 6/, [['error', 'LOGGED ERROR:', 5, '!==', 6]], ); + + assert.equal(NaN, NaN); + throwsAndLogs( + t, + () => assert.equal(-0, 0), + /Expected \(a number\) is same as \(a number\)/, + [['error', 'LOGGED ERROR:', 'Expected', -0, 'is same as', 0]], + ); + } catch (e) { + console.log('unexpected exception', e); + t.assert(false, e); + } finally { + t.end(); + } +}); + +test('assert typeof', t => { + try { + assert.typeof(2, 'number'); + throwsAndLogs( + t, + () => assert.typeof(2, 'string'), + /\(a number\) must be a string/, + [['error', 'LOGGED ERROR:', 2, 'must be a string']], + ); + throwsAndLogs(t, () => assert.typeof(2, 'string', 'foo'), /foo/, [ + ['error', 'LOGGED ERROR:', 'foo'], + ]); + } catch (e) { + console.log('unexpected exception', e); + t.assert(false, e); + } finally { + t.end(); + } +}); + +test('assert q', t => { + try { + throwsAndLogs( + t, + () => assert.fail(details`<${'bar'},${q('baz')}>`), + /<\(a string\),"baz">/, + [['error', 'LOGGED ERROR:', '<', 'bar', ',', 'baz', '>']], + ); + + const list = ['a', 'b', 'c']; + throwsAndLogs( + t, + () => assert.fail(details`${q(list)}`), + /\["a","b","c"\]/, + [['error', 'LOGGED ERROR:', ['a', 'b', 'c']]], + ); + const repeat = { x: list, y: list }; + throwsAndLogs( + t, + () => assert.fail(details`${q(repeat)}`), + /{"x":\["a","b","c"\],"y":"<\*\*seen\*\*>"}/, + [['error', 'LOGGED ERROR:', { x: ['a', 'b', 'c'], y: ['a', 'b', 'c'] }]], + ); + + // Make it into a cycle + list[1] = list; + throwsAndLogs( + t, + () => assert.fail(details`${q(list)}`), + /\["a","<\*\*seen\*\*>","c"\]/, + [['error', 'LOGGED ERROR:', ['a', list, 'c']]], + ); + throwsAndLogs( + t, + () => assert.fail(details`${q(repeat)}`), + /{"x":\["a","<\*\*seen\*\*>","c"\],"y":"<\*\*seen\*\*>"}/, + [['error', 'LOGGED ERROR:', { x: list, y: ['a', list, 'c'] }]], + ); } catch (e) { console.log('unexpected exception', e); t.assert(false, e); diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/lib-wallet.js b/packages/cosmic-swingset/lib/ag-solo/vats/lib-wallet.js index fd0f908101c..2eb36e1e9f9 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/lib-wallet.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/lib-wallet.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; import makeStore from '@agoric/store'; import makeWeakStore from '@agoric/weak-store'; import makeAmountMath from '@agoric/ertp/src/amountMath'; @@ -291,9 +291,7 @@ export async function makeWallet({ const makeEmptyPurse = async (brandPetname, petnameForPurse) => { assert( !purseMapping.petnameToVal.has(petnameForPurse), - details`Purse petname ${openDetail( - petnameForPurse, - )} already used in wallet.`, + details`Purse petname ${q(petnameForPurse)} already used in wallet.`, ); const brand = brandMapping.petnameToVal.get(brandPetname); const { issuer } = brandTable.get(brand); diff --git a/packages/same-structure/src/sameStructure.js b/packages/same-structure/src/sameStructure.js index 23aadf74720..d3bb846284c 100644 --- a/packages/same-structure/src/sameStructure.js +++ b/packages/same-structure/src/sameStructure.js @@ -1,6 +1,6 @@ import harden from '@agoric/harden'; import { sameValueZero, passStyleOf } from '@agoric/marshal'; -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; // Shim of Object.fromEntries from // https://github.com/tc39/proposal-object-from-entries/blob/master/polyfill.js @@ -166,7 +166,7 @@ function pathStr(path) { function mustBeSameStructureInternal(left, right, message, path) { function complain(problem) { assert.fail( - details`${openDetail(message)}: ${openDetail(problem)} at ${openDetail( + details`${q(message)}: ${q(problem)} at ${q( pathStr(path), )}: (${left}) vs (${right})`, ); diff --git a/packages/store/src/store.js b/packages/store/src/store.js index 4a9c0de847c..2effb225320 100644 --- a/packages/store/src/store.js +++ b/packages/store/src/store.js @@ -2,7 +2,7 @@ // @ts-check import rawHarden from '@agoric/harden'; -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; const harden = /** @type {(x: T) => T} */ (rawHarden); @@ -32,12 +32,9 @@ const harden = /** @type {(x: T) => T} */ (rawHarden); function makeStore(keyName = 'key') { const store = new Map(); const assertKeyDoesNotExist = key => - assert( - !store.has(key), - details`${openDetail(keyName)} already registered: ${key}`, - ); + assert(!store.has(key), details`${q(keyName)} already registered: ${key}`); const assertKeyExists = key => - assert(store.has(key), details`${openDetail(keyName)} not found: ${key}`); + assert(store.has(key), details`${q(keyName)} not found: ${key}`); return harden({ has: key => store.has(key), init: (key, value) => { diff --git a/packages/wallet-frontend/src/utils/fetch-websocket.js b/packages/wallet-frontend/src/utils/fetch-websocket.js index b4a1149b81a..09a1b444d79 100644 --- a/packages/wallet-frontend/src/utils/fetch-websocket.js +++ b/packages/wallet-frontend/src/utils/fetch-websocket.js @@ -12,7 +12,7 @@ export async function doFetch(req) { } const socket = websocket; - + let resolve; const p = new Promise(res => { resolve = res; diff --git a/packages/weak-store/src/weakStore.js b/packages/weak-store/src/weakStore.js index da6f6522eee..94ac02b11be 100644 --- a/packages/weak-store/src/weakStore.js +++ b/packages/weak-store/src/weakStore.js @@ -1,7 +1,7 @@ // Copyright (C) 2019 Agoric, under Apache license 2.0 import harden from '@agoric/harden'; -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; /** * Distinguishes between adding a new key (init) and updating or * referencing a key (get, set, delete). @@ -13,12 +13,9 @@ import { assert, details, openDetail } from '@agoric/assert'; function makeStore(keyName = 'key') { const wm = new WeakMap(); const assertKeyDoesNotExist = key => - assert( - !wm.has(key), - details`${openDetail(keyName)} already registered: ${key}`, - ); + assert(!wm.has(key), details`${q(keyName)} already registered: ${key}`); const assertKeyExists = key => - assert(wm.has(key), details`${openDetail(keyName)} not found: ${key}`); + assert(wm.has(key), details`${q(keyName)} not found: ${key}`); return harden({ has: key => wm.has(key), init: (key, value) => { diff --git a/packages/zoe/src/cleanProposal.js b/packages/zoe/src/cleanProposal.js index 08af0fe6c1c..4dd4906d292 100644 --- a/packages/zoe/src/cleanProposal.js +++ b/packages/zoe/src/cleanProposal.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; import { mustBeComparable } from '@agoric/same-structure'; import { arrayToObj, assertSubset } from './objArrayConversion'; @@ -18,13 +18,13 @@ export const assertKeywordName = keyword => { const firstCapASCII = /^[A-Z][a-zA-Z0-9_$]*$/; assert( firstCapASCII.test(keyword), - details`keyword ${openDetail( + details`keyword ${q( keyword, )} must be ascii and must start with a capital letter.`, ); assert( keyword !== 'NaN' && keyword !== 'Infinity', - details`keyword ${openDetail(keyword)} must not be a number's name`, + details`keyword ${q(keyword)} must not be a number's name`, ); }; diff --git a/packages/zoe/src/objArrayConversion.js b/packages/zoe/src/objArrayConversion.js index a7857bf07c3..572ecf0e263 100644 --- a/packages/zoe/src/objArrayConversion.js +++ b/packages/zoe/src/objArrayConversion.js @@ -1,4 +1,4 @@ -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; export const arrayToObj = (array, keywords) => { assert( @@ -14,9 +14,9 @@ export const objToArray = (obj, keywords) => { const keys = Object.getOwnPropertyNames(obj); assert( keys.length === keywords.length, - details`object keys (${openDetail(keys)}) and keywords (${openDetail( + details`object keys ${q(keys)} and keywords ${q( keywords, - )}) must be of equal length`, + )} must be of equal length`, ); return keywords.map(keyword => obj[keyword]); }; @@ -26,9 +26,7 @@ export const assertSubset = (whole, part) => { assert.typeof(key, 'string'); assert( whole.includes(key), - details`key ${openDetail( - key, - )} was not one of the expected keys (${openDetail(whole.join(', '))})`, + details`key ${q(key)} was not one of the expected keys ${q(whole)}`, ); }); }; @@ -38,16 +36,16 @@ export const objToArrayAssertFilled = (obj, keywords) => { const keys = Object.getOwnPropertyNames(obj); assert( keys.length === keywords.length, - details`object keys (${openDetail(keys)}) and keywords (${openDetail( + details`object keys ${q(keys)} and keywords ${q( keywords, - )}) must be of equal length`, + )} must be of equal length`, ); assertSubset(keywords, keys); // ensure all keywords are defined on obj return keywords.map(keyword => { assert( obj[keyword] !== undefined, - details`obj[keyword] must be defined for keyword ${openDetail(keyword)}`, + details`obj[keyword] must be defined for keyword ${q(keyword)}`, ); return obj[keyword]; }); @@ -63,7 +61,7 @@ export const filterObj = /** @type {function(T, string[]): T} */ ( subsetKeywords.forEach(keyword => { assert( obj[keyword] !== undefined, - details`obj[keyword] must be defined for keyword ${openDetail(keyword)}`, + details`obj[keyword] must be defined for keyword ${q(keyword)}`, ); newObj[keyword] = obj[keyword]; }); diff --git a/packages/zoe/src/zoe.js b/packages/zoe/src/zoe.js index c5d716d35b7..8b227798b85 100644 --- a/packages/zoe/src/zoe.js +++ b/packages/zoe/src/zoe.js @@ -3,7 +3,7 @@ import harden from '@agoric/harden'; import { E, HandledPromise } from '@agoric/eventual-send'; import makeStore from '@agoric/weak-store'; import produceIssuer from '@agoric/ertp'; -import { assert, details, openDetail } from '@agoric/assert'; +import { assert, details, q } from '@agoric/assert'; import { produceNotifier } from '@agoric/notifier'; import { producePromise } from '@agoric/produce-promise'; @@ -882,7 +882,7 @@ const makeZoe = (additionalEndowments = {}) => { } else { assert( exitKind === 'waived', - details`exit kind was not recognized: ${openDetail(exitKind)}`, + details`exit kind was not recognized: ${q(exitKind)}`, ); } diff --git a/packages/zoe/test/unitTests/test-objArrayConversion.js b/packages/zoe/test/unitTests/test-objArrayConversion.js index ca3707d5f3d..15ef9137b19 100644 --- a/packages/zoe/test/unitTests/test-objArrayConversion.js +++ b/packages/zoe/test/unitTests/test-objArrayConversion.js @@ -38,14 +38,14 @@ test('objToArray', t => { const keywords2 = ['X', 'Y', 'Z']; t.throws( () => objToArray(obj, keywords2), - /Error: object keys \(X,Y\) and keywords \(X,Y,Z\) must be of equal length/, + /Error: object keys \["X","Y"\] and keywords \["X","Y","Z"\] must be of equal length/, `unequal length should throw`, ); const obj2 = { X: 1, Y: 2, Z: 5 }; t.throws( () => objToArray(obj2, keywords), - /Error: object keys \(X,Y,Z\) and keywords \(X,Y\) must be of equal length/, + /Error: object keys \["X","Y","Z"\] and keywords \["X","Y"\] must be of equal length/, `unequal length should throw`, ); } catch (e) {