From 97c7c3a801a6ec26605aa6546227ed1905ebe1fe Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 1 Sep 2020 14:56:20 -0700 Subject: [PATCH] Move assert from agoric-sdk to SES/shim --- packages/assert/.prettierrc.json | 4 + packages/assert/LICENSE | 201 ++++++++++ packages/assert/NEWS.md | 0 packages/assert/README.md | 0 packages/assert/package.json | 38 ++ packages/assert/rollup.config.js | 43 +++ packages/assert/src/assert.js | 352 ++++++++++++++++++ packages/assert/src/main.js | 5 + packages/assert/src/types.js | 80 ++++ packages/assert/test/assert.test.js | 26 ++ packages/console/.prettierrc.json | 4 + packages/console/LICENSE | 201 ++++++++++ packages/console/NEWS.md | 0 packages/console/README.md | 0 packages/console/package.json | 27 ++ packages/console/src/console.js | 221 +++++++++++ packages/console/src/main.js | 1 + packages/console/test/assert-log.test.js | 263 +++++++++++++ packages/console/test/throwsAndLogs.js | 121 ++++++ packages/ses/package.json | 6 +- packages/ses/src/assert.js | 5 - packages/ses/src/assertions.js | 30 -- packages/ses/src/evaluate.js | 14 +- packages/ses/src/lockdown-shim.js | 23 +- packages/ses/src/make-function-constructor.js | 2 +- packages/ses/src/scope-handler.js | 6 +- packages/ses/src/tame-console.js | 25 ++ packages/ses/src/tame-locale-methods.js | 8 +- packages/ses/src/tame-v8-error-constructor.js | 8 +- packages/ses/test/assertions.test.js | 65 ---- packages/ses/test/check-anon-intrinsics.js | 3 +- packages/ses/test/scope-handler.test.js | 8 +- packages/ses/test/ses.test.js | 18 - packages/ses/test/tame-console-unit.test.js | 93 +++++ .../tame-console-unsafe-unsafeError.test.js | 94 +++++ packages/ses/test/tame-console-unsafe.test.js | 94 +++++ .../ses/test/tame-console-unsafeError.test.js | 94 +++++ packages/ses/test/tame-console.test.js | 94 +++++ yarn.lock | 203 +++++++++- 39 files changed, 2324 insertions(+), 156 deletions(-) create mode 100644 packages/assert/.prettierrc.json create mode 100644 packages/assert/LICENSE create mode 100644 packages/assert/NEWS.md create mode 100644 packages/assert/README.md create mode 100644 packages/assert/package.json create mode 100644 packages/assert/rollup.config.js create mode 100644 packages/assert/src/assert.js create mode 100644 packages/assert/src/main.js create mode 100644 packages/assert/src/types.js create mode 100644 packages/assert/test/assert.test.js create mode 100644 packages/console/.prettierrc.json create mode 100644 packages/console/LICENSE create mode 100644 packages/console/NEWS.md create mode 100644 packages/console/README.md create mode 100644 packages/console/package.json create mode 100644 packages/console/src/console.js create mode 100644 packages/console/src/main.js create mode 100644 packages/console/test/assert-log.test.js create mode 100644 packages/console/test/throwsAndLogs.js delete mode 100644 packages/ses/src/assert.js delete mode 100644 packages/ses/src/assertions.js create mode 100644 packages/ses/src/tame-console.js delete mode 100644 packages/ses/test/assertions.test.js create mode 100644 packages/ses/test/tame-console-unit.test.js create mode 100644 packages/ses/test/tame-console-unsafe-unsafeError.test.js create mode 100644 packages/ses/test/tame-console-unsafe.test.js create mode 100644 packages/ses/test/tame-console-unsafeError.test.js create mode 100644 packages/ses/test/tame-console.test.js diff --git a/packages/assert/.prettierrc.json b/packages/assert/.prettierrc.json new file mode 100644 index 0000000000..6e778b4fb9 --- /dev/null +++ b/packages/assert/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "singleQuote": true +} diff --git a/packages/assert/LICENSE b/packages/assert/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/assert/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/assert/NEWS.md b/packages/assert/NEWS.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/assert/README.md b/packages/assert/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/assert/package.json b/packages/assert/package.json new file mode 100644 index 0000000000..3c3586c612 --- /dev/null +++ b/packages/assert/package.json @@ -0,0 +1,38 @@ +{ + "name": "@agoric/assert", + "version": "0.1.0", + "description": "Assert expression support that protects sensitive data.", + "author": "Agoric", + "license": "Apache-2.0", + "type": "module", + "main": "./dist/assert.cjs", + "module": "./src/main.js", + "browser": "./dist/assert.umd.js", + "unpkg": "./dist/assert.umd.js", + "exports": { + "import": "./src/main.js", + "require": "./dist/assert.cjs", + "browser": "./dist/assert.umd.js" + }, + "scripts": { + "build": "rollup --config rollup.config.js", + "clean": "rm -rf dist", + "depcheck": "depcheck", + "lint": "eslint '**/*.js'", + "lint-fix": "eslint --fix '**/*.js'", + "prepublish": "yarn clean && yarn build", + "test": "yarn build && tap --no-esm --no-coverage --reporter spec test/**/*.test.js" + }, + "dependencies": {}, + "devDependencies": { + "@rollup/plugin-node-resolve": "^6.1.0", + "rollup-plugin-terser": "^5.1.3", + "tap": "14.10.5", + "tape": "5.0.1" + }, + "files": [ + "LICENSE*", + "dist", + "src" + ] +} diff --git a/packages/assert/rollup.config.js b/packages/assert/rollup.config.js new file mode 100644 index 0000000000..081f00150c --- /dev/null +++ b/packages/assert/rollup.config.js @@ -0,0 +1,43 @@ +import resolve from '@rollup/plugin-node-resolve'; +import { terser } from 'rollup-plugin-terser'; +import fs from 'fs'; + +const metaPath = new URL('package.json', import.meta.url).pathname; +const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); +const name = meta.name.split('/').pop(); +const umd = meta.umd || name; + +export default [ + { + input: 'src/main.js', + output: [ + { + file: `dist/${name}.mjs`, + format: 'esm', + }, + { + file: `dist/${name}.cjs`, + format: 'cjs', + }, + ], + plugins: [resolve()], + }, + { + input: 'src/main.js', + output: { + file: `dist/${name}.umd.js`, + format: 'umd', + name: umd, + }, + plugins: [resolve()], + }, + { + input: 'src/main.js', + output: { + file: `dist/${name}.umd.min.js`, + format: 'umd', + name: umd, + }, + plugins: [resolve(), terser()], + }, +]; diff --git a/packages/assert/src/assert.js b/packages/assert/src/assert.js new file mode 100644 index 0000000000..00b4861c0d --- /dev/null +++ b/packages/assert/src/assert.js @@ -0,0 +1,352 @@ +// Copyright (C) 2019 Agoric, under Apache License 2.0 + +// @ts-check + +import './types.js'; + +const { freeze, is: isSame, assign } = Object; + +// This module assumes the de-facto standard `console` host object. +// To the extent that this `console` is considered a resource, +// this module must be considered a resource module. + +// ///////////////////////////////////////////////////////////////////////////// + +// If this magic symbol occurs as the first argument to a console level method +// and last argument is an error object, then the arguments between +// them are remembered as the cause of the error, to be emitted only when +// the error is. This is designed to fail soft when sent to a normal console +// that doesn't recognize the string. The full cause will be emitted when +// it is logged, rather than being remembered to be logged later. +// +// Aside from tests, nothing else should import care what the +// actual encoding is. +const ERROR_CAUSE = Symbol('ERROR CAUSE:'); + +/** + * Encode the `causeRecord` into the returned LogRecord. This encoding + * must + * * be recognized and reversed by `getCauseRecord`. + * * be unlikely to collide with LogRecords representing unelated + * calls to console. + * * If used to invoke a normal console not built to recognize this + * encoding, the result is still pleasant and understandable to a + * human looking at the log. + * Aside from tests, nothing else should care what the actual encoding is, + * as long as `asLogRecord` and `getCauseRecord` preserve this relationship. + * This lets us revise the encoding over time. + * + * @param {CauseRecord} causeRecord + * @returns {LogRecord} + */ +const asLogRecord = ({ level, cause, error }) => { + const outerArgs = freeze([ERROR_CAUSE, ...cause, error]); + return freeze({ level, outerArgs }); +}; +freeze(asLogRecord); +export { asLogRecord }; + +/** + * Recognize if the LogRecord is of the form created by `asLogRecord`. If so + * then return the CauseRecord that was encoded into this LogRecord. + * Otherwise return `undefined`. + * + * @param { LogRecord } logRecord + * @returns { CauseRecord | undefined } + */ +const getCauseRecord = ({ level, outerArgs }) => { + if (outerArgs.length >= 2) { + const [first, ...cause] = outerArgs; + const error = cause.pop(); + if (first === ERROR_CAUSE && error instanceof Error) { + freeze(cause); + return freeze({ level, cause, error }); + } + } + return undefined; +}; +freeze(getCauseRecord); +export { getCauseRecord }; + +/** + * Log to `aConsole` according to `logRecord` + * + * @param {Console} aConsole + * @param {LogRecord} logRecord + * @return {void} + */ +const logToConsole = (aConsole, { level, outerArgs }) => { + aConsole[level](...outerArgs); +}; +freeze(logToConsole); +export { logToConsole }; + +// ///////////////////////////////////////////////////////////////////////////// + +/** + * Prepend the correct indefinite article onto a noun, typically a typeof + * result, e.g., "an Object" vs. "a Number" + * + * @param {string} str The noun to prepend + * @returns {string} The noun prepended with a/an + */ +const an = str => { + str = `${str}`; + if (str.length >= 1 && 'aeiouAEIOU'.includes(str[0])) { + return `an ${str}`; + } + return `a ${str}`; +}; +freeze(an); +export { 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. + */ +const 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); +}; +freeze(cycleTolerantStringify); +export { cycleTolerantStringify }; + +const declassifiers = new WeakMap(); + +/** + * 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 + * want to reveal the sky's actual color, but we do want the thrown error's + * message to reveal what color the sky was supposed to be: + * ```js + * assert.equal( + * sky.color, + * color, + * details`${sky.color} should be ${q(color)}`, + * ); + * ``` + * + * @typedef {Object} StringablePayload + * @property {() => string} toString How to print the payload + * + * @param {*} payload What to declassify + * @returns {StringablePayload} The declassified payload + */ +const q = payload => { + // Don't harden the payload + const result = freeze({ + toString: freeze(() => cycleTolerantStringify(payload)), + }); + declassifiers.set(result, payload); + return result; +}; +freeze(q); +export { 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"`); + * ``` + * 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 console but include only the typeof information in the thrown error + * to prevent revealing secrets up the exceptional path. In the example + * above, the thrown error may reveal only that `sky.color` is a string, + * whereas the same diagnostic printed to the console reveals that the + * sky was green. + * + * WARNING: this function currently returns an unhardened result, as hardening + * proved to cause significant performance degradation. Consequently, callers + * should take care to use it only in contexts where this lack of hardening + * does not present a hazard. In current usage, a `details` template literal + * may only appear either as an argument to `assert`, where we know hardening + * won't matter, or inside another hardened object graph, where hardening is + * already ensured. However, there is currently no means to enfoce these + * constraints, so users are required to employ this function with caution. + * Our intent is to eventually have a lint rule that will check for + * inappropriate uses or find an alternative means of implementing `details` + * that does not encounter the performance issue. The final disposition of + * this is being discussed and tracked in issue #679 in the agoric-sdk + * repository. + * + * @typedef {Object} Complainer An object that has custom assert behaviour + * @property {() => RangeError} complain Return a RangeError to throw, and + * print details to the console + * + * @typedef {string|Complainer} Details Either a plain string, or made by + * details`` + * + * @param {TemplateStringsArray | string[]} template The template to format + * @param {any[]} args Arguments to the template + * @returns {Complainer} The complainer for these details + */ +const details = (template, ...args) => { + // const complainer = harden({ // remove harden per above discussion + const complainer = { + complain() { + const cause = [template[0]]; + const parts = [template[0]]; + for (let i = 0; i < args.length; i += 1) { + let arg = args[i]; + let argStr; + if (declassifiers.has(arg)) { + argStr = `${arg}`; + arg = declassifiers.get(arg); + } else if (arg instanceof Error) { + argStr = `(${an(arg.name)})`; + } else { + argStr = `(${an(typeof arg)})`; + } + + // Remove the extra spaces (since console.error puts them + // between each cause). + const priorWithoutSpace = (cause.pop() || '').replace(/ $/, ''); + if (priorWithoutSpace !== '') { + cause.push(priorWithoutSpace); + } + + const nextWithoutSpace = template[i + 1].replace(/^ /, ''); + cause.push(arg, nextWithoutSpace); + + parts.push(argStr, template[i + 1]); + } + if (cause[cause.length - 1] === '') { + cause.pop(); + } + const error = new RangeError(parts.join('')); + logToConsole(console, asLogRecord({ level: 'log', cause, error })); + // eslint-disable-next-line no-debugger + debugger; + return error; + }, + }; + // }); + return complainer; +}; +freeze(details); +export { details }; +/** + * Fail an assertion, recording details to the console and + * raising an exception with just type information. + * + * The optional `optDetails` can be a string for backwards compatibility + * with the nodejs assertion library. + * @param {Details} [optDetails] The details of what was asserted + * @returns {never} + */ +const fail = (optDetails = details`Assert failed`) => { + if (typeof optDetails === 'string') { + // 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(); +}; +freeze(fail); + +// Don't freeze or export `baseAssert` until we add methods. +// TODO If I change this from a `function` function to an arrow +// function, I seem to get type errors from TypeScript. Why? +/** + * @param {*} flag The truthy/falsy value + * @param {Details} [optDetails] The details to throw + * @returns {asserts flag} + */ +function baseAssert(flag, optDetails = details`Check failed`) { + if (!flag) { + throw fail(optDetails); + } +} + +/** + * 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 + * @returns {void} + */ +const equal = ( + actual, + expected, + optDetails = details`Expected ${actual} is same as ${expected}`, +) => { + baseAssert(isSame(actual, expected), optDetails); +}; +freeze(equal); + +/** + * Assert an expected typeof result. + * @type {AssertTypeof} + * @param {any} specimen The value to get the typeof + * @param {string} typename The expected name + * @param {Details} [optDetails] The details to throw + */ +const assertTypeof = (specimen, typename, optDetails) => { + baseAssert( + 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); +}; +freeze(assertTypeof); + +// Note that "assert === baseAssert" +/** + * assert that expr is truthy, with an optional details to describe + * the assertion. It is a tagged template literal like + * ```js + * assert(expr, details`....`);` + * ``` + * If expr is falsy, then the template contents are reported to + * the console and also in a thrown error. + * + * The literal portions of the template are assumed non-sensitive, as + * are the `typeof` types of the substitution values. These are + * assembled into the thrown error message. The actual contents of the + * substitution values are assumed sensitive, to be revealed to + * the console only. We assume only the virtual platform's owner can read + * what is written to the console, where the owner is in a privileged + * position over computation running on that platform. + * + * The optional `optDetails` can be a string for backwards compatibility + * with the nodejs assertion library. + * @type {typeof baseAssert & { typeof: AssertTypeof, fail: typeof fail, equal: typeof equal }} + */ +const assert = assign(baseAssert, { equal, fail, typeof: assertTypeof }); +freeze(assert); +export { assert }; diff --git a/packages/assert/src/main.js b/packages/assert/src/main.js new file mode 100644 index 0000000000..8a7d6cbdff --- /dev/null +++ b/packages/assert/src/main.js @@ -0,0 +1,5 @@ +// TODO How do I export ./types.js ? +// See TODO at top of console.js +// import '@agoric/src/types'; + +export * from './assert.js'; diff --git a/packages/assert/src/types.js b/packages/assert/src/types.js new file mode 100644 index 0000000000..497e5c4512 --- /dev/null +++ b/packages/assert/src/types.js @@ -0,0 +1,80 @@ +/// + +/** + * A LogRecord represents an invocation of a given log level of a console + * with a given set of arguments. We call these `outerArgs` because they may + * contain an encoding of a CauseRecord containing `cause` -- to be + * processed specially when recognized. + * + * @typedef {Object} LogRecord + * @property {string} level + * @property {readonly any[]} outerArgs + */ + +/** + * A CauseRecord is an allegation that the log `level` together with the + * `cause` represent one of the causes of `error`. These are called + * `cause` because the CauseRecord may be encoded into a LogRecord. + * + * @typedef {Object} CauseRecord + * @property {string} level + * @property {readonly any[]} cause + * @property {Error} error + */ + +// Type all the overloads of the assertTypeof function. +// There may eventually be a better way to do this, but +// thems the breaks with Typescript 4.0. +/** + * @callback AssertTypeofBigint + * @param {any} specimen + * @param {'bigint'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is bigint} + * + * @callback AssertTypeofBoolean + * @param {any} specimen + * @param {'boolean'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is boolean} + * + * @callback AssertTypeofFunction + * @param {any} specimen + * @param {'function'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is Function} + * + * @callback AssertTypeofNumber + * @param {any} specimen + * @param {'number'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is number} + * + * @callback AssertTypeofObject + * @param {any} specimen + * @param {'object'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is object} + * + * @callback AssertTypeofString + * @param {any} specimen + * @param {'string'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is string} + * + * @callback AssertTypeofSymbol + * @param {any} specimen + * @param {'symbol'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is symbol} + * + * @callback AssertTypeofUndefined + * @param {any} specimen + * @param {'undefined'} typename + * @param {Details} [optDetails] + * @returns {asserts specimen is undefined} + */ + +/** + * @typedef {AssertTypeofBigint & AssertTypeofBoolean & AssertTypeofFunction & AssertTypeofNumber & AssertTypeofObject & AssertTypeofString & AssertTypeofSymbol & AssertTypeofUndefined} AssertTypeof + */ diff --git a/packages/assert/test/assert.test.js b/packages/assert/test/assert.test.js new file mode 100644 index 0000000000..aaec2748f4 --- /dev/null +++ b/packages/assert/test/assert.test.js @@ -0,0 +1,26 @@ +// This is just a token test of the `assert` module. The real test is in +// console/test/assert.test.js, so these tests can build on the logging +// console. + +// TODO The following line came from agoric-sdk which already supports ava. +// Potentially re-enable once SES-shim does too. +// import test from 'ava'; + +// The following lines mentioning tap are what we do for now instead. +import tap from 'tap'; +import { an } from '../src/assert.js'; + +const { test } = tap; + +test('an', t => { + t.is(an('object'), 'an object'); + t.is(an('function'), 'a function'); + // does not treat an initial 'y' as a vowel + t.is(an('yaml file'), 'a yaml file'); + // recognize upper case vowels + t.is(an('Object'), 'an Object'); + // coerce non-objects to strings. + // non-letters are treated as non-vowels + t.is(an({}), 'a [object Object]'); + t.end(); +}); diff --git a/packages/console/.prettierrc.json b/packages/console/.prettierrc.json new file mode 100644 index 0000000000..6e778b4fb9 --- /dev/null +++ b/packages/console/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "singleQuote": true +} diff --git a/packages/console/LICENSE b/packages/console/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/console/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/console/NEWS.md b/packages/console/NEWS.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/console/README.md b/packages/console/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/console/package.json b/packages/console/package.json new file mode 100644 index 0000000000..32eb049c86 --- /dev/null +++ b/packages/console/package.json @@ -0,0 +1,27 @@ +{ + "name": "@agoric/console", + "private": true, + "version": "0.1.0+1-dev", + "description": "Description forthcoming.", + "author": "Agoric", + "license": "Apache-2.0", + "type": "module", + "main": "./src/main.js", + "scripts": { + "lint": "eslint '**/*.js'", + "lint-fix": "eslint --fix '**/*.js'", + "test": "tap --no-esm --no-coverage --reporter spec test/**/*.test.js" + }, + "dependencies": { + "@agoric/assert": "0.1.0" + }, + "devDependencies": { + "tap": "14.10.5", + "tape": "5.0.1" + }, + "files": [ + "LICENSE*", + "dist", + "src" + ] +} diff --git a/packages/console/src/console.js b/packages/console/src/console.js new file mode 100644 index 0000000000..f67d693a26 --- /dev/null +++ b/packages/console/src/console.js @@ -0,0 +1,221 @@ +// @ts-check + +// TODO How do I import assert/src/types.js ? +// See TODO at top of assert/src/main.js +import { getCauseRecord } from '@agoric/assert'; + +// @ts-ignore fromEntries missing from Object type +const { defineProperty, freeze, fromEntries } = Object; + +// For our internal debugging purposes +// const originalConsole = console; + +// The whitelist of console methods: +// Organized by whatwg "living standard" at https://console.spec.whatwg.org/ +// but with parameters according to Node's +// https://nodejs.org/dist/latest-v14.x/docs/api/console.html +// See MDN https://developer.mozilla.org/en-US/docs/Web/API/Console_API +// See TS https://openstapps.gitlab.io/projectmanagement/interfaces/_node_modules__types_node_globals_d_.console.html + +// All console level methods have parameters (data?, ...args) +export const consoleLevelMethods = freeze([ + 'debug', + 'error', + 'info', + 'log', + 'warn', +]); + +export const consoleOtherMethods = freeze([ + // Logging + // ...consoleLevels methods are considered logging methods + 'assert', // node: (value, ...message). TS: (value, message?, ...args) + 'clear', // () + + // 'exception', MDN rare alias for error. not whatwg or node + 'table', // (tabularData, properties?) + 'trace', // (message?, ...args) + + 'dir', // (item, options?) + 'dirxml', // (...data) + + // Counting + 'count', // (label?) + 'countReset', // (label?) + + // Grouping + 'group', // (...label) + 'groupCollapsed', // node,TS: (). whatwg: (...data). MDN: (label?) + 'groupEnd', // () + + // Timing + 'time', // (label?) + 'timeLog', // (label?, ...data) + 'timeEnd', // (label?) + + // Node Inspector only methods. Also MDN,TS but not whatwg + 'profile', // (label?) + 'profileEnd', // (label?) + 'timeStamp', // (label?) + + // Symbols + '@@toStringTag', // Chrome: "Object", Safari: "Console" +]); + +export const consoleWhitelist = freeze([ + ...consoleLevelMethods, + ...consoleOtherMethods, +]); + +export const consoleOmittedProperties = freeze([ + 'memory', // Chrome + 'exception', // FF, MDN + '_ignoreErrors', // Node + '_stderr', // Node + '_stderrErrorHandler', // Node + '_stdout', // Node + '_stdoutErrorHandler', // Node + '_times', // Node + 'context', // Chrome, Node + 'record', // Safari + 'recordEnd', // Safari + 'screenshot', // Safari + // A variety of symbols also seen on Node +]); + +/** + * @returns {LoggingConsoleKit} + */ +const makeLoggingConsoleKit = () => { + // Not frozen! + const logArray = []; + + const loggingConsole = freeze( + fromEntries( + consoleWhitelist.map(name => { + // Use an arrow function so that it doesn't come with its own name in + // its printed form. Instead, we're hoping that tooling uses only + // the `.name` property set below. + const f = (...args) => { + logArray.push([name, ...args]); + return undefined; + }; + defineProperty(f, 'name', { value: name }); + return [name, freeze(f)]; + }), + ), + ); + + const takeLog = freeze(() => { + const result = freeze([...logArray]); + logArray.length = 0; + return result; + }); + + return freeze({ loggingConsole, takeLog }); +}; +freeze(makeLoggingConsoleKit); +export { makeLoggingConsoleKit }; + +/** + * @param {Console} baseConsole + * @returns {Console} + */ +const makeCausalConsole = (baseConsole, optGetStackString = undefined) => { + /** @type WeakMap */ + const errorCauses = new WeakMap(); + /** + * @param {CauseRecord} causeRecord + */ + const rememberCause = causeRecord => { + const { error } = causeRecord; + if (errorCauses.has(error)) { + errorCauses.get(error).push(causeRecord); + } else { + errorCauses.set(error, [causeRecord]); + } + }; + + // by "printed", we mean first sent to the baseConsole as an argument in a + // console level + // method call. We number the errors according to the order in which they + // were first printed, starting at 1. + let numErrorsPrinted = 0; + /** @type WeakMap */ + const errorPrintOrder = new WeakMap(); + + /** + * @param {Error} err + * @returns {number} + */ + const errorPrinted = err => { + if (errorPrintOrder.has(err)) { + return errorPrintOrder.get(err); + } + numErrorsPrinted += 1; + errorPrintOrder.set(err, numErrorsPrinted); + return numErrorsPrinted; + }; + + /** + * @param {Error} err + * @returns {string} + */ + const errorRef = err => { + const errNum = errorPrinted(err); + return `${err.name}#${errNum}`; + }; + + const levelMethods = consoleLevelMethods.map(level => { + const levelMethod = (...outerArgs) => { + const logRecord = freeze({ level, outerArgs }); + const causeRecord = getCauseRecord(logRecord); + if (causeRecord) { + rememberCause(causeRecord); + } else { + const errors = []; + const newOuterArgs = outerArgs.map(arg => { + if (arg instanceof Error) { + errors.push(arg); + return `(${errorRef(arg)}: ${arg.message})`; + } + return arg; + }); + baseConsole[level](...newOuterArgs); + + for (const err of errors) { + const arg = optGetStackString ? optGetStackString(err) : err; + baseConsole[level](`(${errorRef(err)}) ERR:`, arg); + if (errorCauses.has(err)) { + const errCauseRecords = errorCauses.get(err); + errorCauses.delete(err); // prevent cyclic causation + for (const { level: causingLevel, cause } of errCauseRecords) { + const label = `(${errorRef(err)}) CAUSE:`; + // eslint-disable-next-line no-use-before-define + causalConsole[causingLevel](label, ...cause); + } + } + } + } + return undefined; + }; + defineProperty(levelMethod, 'name', { value: level }); + return [level, freeze(levelMethod)]; + }); + const otherMethodNames = consoleOtherMethods.filter( + name => name in baseConsole, + ); + const otherMethods = otherMethodNames.map(name => { + const otherMethod = (...args) => { + baseConsole[name](...args); + return undefined; + }; + defineProperty(otherMethod, 'name', { value: name }); + return [name, freeze(otherMethod)]; + }); + + const causalConsole = fromEntries([...levelMethods, ...otherMethods]); + return freeze(causalConsole); +}; +freeze(makeCausalConsole); +export { makeCausalConsole }; diff --git a/packages/console/src/main.js b/packages/console/src/main.js new file mode 100644 index 0000000000..953d1f822d --- /dev/null +++ b/packages/console/src/main.js @@ -0,0 +1 @@ +export { makeCausalConsole } from './console.js'; diff --git a/packages/console/test/assert-log.test.js b/packages/console/test/assert-log.test.js new file mode 100644 index 0000000000..016fb2b52c --- /dev/null +++ b/packages/console/test/assert-log.test.js @@ -0,0 +1,263 @@ +// TODO The following two lines came from agoric-sdk with already supports ava. +// Potentially re-enable once SES-shim does too. +// import '@agoric/install-ses'; +// import test from 'ava'; + +// The following lines mentioning tap are what we do for now instead. +import tap from 'tap'; +import { assert, details, q } from '@agoric/assert'; +import { assertLogs, throwsAndLogs } from './throwsAndLogs.js'; + +const { test } = tap; + +// Self-test of the example from the throwsAndLogs comment. +test('throwsAndLogs with data', t => { + const obj = {}; + throwsAndLogs( + t, + () => { + console.error('what', obj); + throw new TypeError('foo'); + }, + /foo/, + [ + ['error', 'what', obj], + ['log', 'Caught', '(TypeError#1: foo)'], + ['log', '(TypeError#1) ERR:', TypeError], + ], + ); + t.end(); +}); + +test('throwsAndLogs with error', t => { + const err = new EvalError('bar'); + throwsAndLogs( + t, + () => { + console.warn('what', err); + throw new URIError('foo'); + }, + /foo/, + [ + ['warn', 'what', '(EvalError#1: bar)'], + ['warn', '(EvalError#1) ERR:', EvalError], + ['log', 'Caught', '(URIError#2: foo)'], + ['log', '(URIError#2) ERR:', URIError], + ], + ); + t.end(); +}); + +test('assert', t => { + assert(2 + 3 === 5); + + throwsAndLogs(t, () => assert(false), /Check failed/, [ + ['log', 'Caught', '(RangeError#1: Check failed)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'Check failed'], + ]); + + throwsAndLogs(t, () => assert(false, 'foo'), /foo/, [ + ['log', 'Caught', '(RangeError#1: foo)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'foo'], + ]); + throwsAndLogs(t, () => assert.fail(), /Assert failed/, [ + ['log', 'Caught', '(RangeError#1: Assert failed)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'Assert failed'], + ]); + throwsAndLogs(t, () => assert.fail('foo'), /foo/, [ + ['log', 'Caught', '(RangeError#1: foo)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'foo'], + ]); + + t.end(); +}); + +test('causal tree', t => { + throwsAndLogs( + t, + () => { + const fooErr = new SyntaxError('foo'); + let err1; + try { + assert.fail(details`synful ${fooErr}`); + } catch (e1) { + err1 = e1; + } + assert.fail(details`because ${err1}`); + }, + /because/, + [ + ['log', 'Caught', '(RangeError#1: because (a RangeError))'], + ['log', '(RangeError#1) ERR:', RangeError], + [ + 'log', + '(RangeError#1) CAUSE:', + 'because', + '(RangeError#2: synful (a SyntaxError))', + ], + ['log', '(RangeError#2) ERR:', RangeError], + ['log', '(RangeError#2) CAUSE:', 'synful', '(SyntaxError#3: foo)'], + ['log', '(SyntaxError#3) ERR:', SyntaxError], + ], + ); + t.end(); +}); + +test('if a causal tree falls silently', t => { + assertLogs( + t, + () => { + try { + assert(false); + } catch (err) { + t.assert(err instanceof RangeError); + } + }, + [], + ); + assertLogs( + t, + () => { + const fooErr = new SyntaxError('foo'); + let err1; + try { + assert.fail(details`synful ${fooErr}`); + } catch (e1) { + err1 = e1; + } + try { + assert.fail(details`because ${err1}`); + } catch (e2) { + t.assert(e2 instanceof RangeError); + } + }, + [], + ); + t.end(); +}); + +test('assert equals', t => { + assert.equal(2 + 3, 5); + throwsAndLogs(t, () => assert.equal(5, 6, 'foo'), /foo/, [ + ['log', 'Caught', '(RangeError#1: foo)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'foo'], + ]); + throwsAndLogs( + t, + () => assert.equal(5, 6, details`${5} !== ${6}`), + /\(a number\) !== \(a number\)/, + [ + ['log', 'Caught', '(RangeError#1: (a number) !== (a number))'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 5, '!==', 6], + ], + ); + throwsAndLogs( + t, + () => assert.equal(5, 6, details`${5} !== ${q(6)}`), + /\(a number\) !== 6/, + [ + ['log', 'Caught', '(RangeError#1: (a number) !== 6)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 5, '!==', 6], + ], + ); + assert.equal(NaN, NaN); + throwsAndLogs( + t, + () => assert.equal(-0, 0), + /Expected \(a number\) is same as \(a number\)/, + [ + [ + 'log', + 'Caught', + '(RangeError#1: Expected (a number) is same as (a number))', + ], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'Expected', -0, 'is same as', 0], + ], + ); + t.end(); +}); + +test('assert typeof', t => { + assert.typeof(2, 'number'); + throwsAndLogs( + t, + () => assert.typeof(2, 'string'), + /\(a number\) must be a string/, + [ + ['log', 'Caught', '(RangeError#1: (a number) must be a string)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 2, 'must be a string'], + ], + ); + throwsAndLogs(t, () => assert.typeof(2, 'string', 'foo'), /foo/, [ + ['log', 'Caught', '(RangeError#1: foo)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', 'foo'], + ]); + t.end(); +}); + +test('assert q', t => { + throwsAndLogs( + t, + () => assert.fail(details`<${'bar'},${q('baz')}>`), + /<\(a string\),"baz">/, + [ + ['log', 'Caught', '(RangeError#1: <(a string),"baz">)'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', '<', 'bar', ',', 'baz', '>'], + ], + ); + const list = ['a', 'b', 'c']; + throwsAndLogs(t, () => assert.fail(details`${q(list)}`), /\["a","b","c"\]/, [ + ['log', 'Caught', '(RangeError#1: ["a","b","c"])'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', list], + ]); + const repeat = { x: list, y: list }; + throwsAndLogs( + t, + () => assert.fail(details`${q(repeat)}`), + /{"x":\["a","b","c"\],"y":"<\*\*seen\*\*>"}/, + [ + ['log', 'Caught', '(RangeError#1: {"x":["a","b","c"],"y":"<**seen**>"})'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', repeat], + ], + ); + // Make it into a cycle + list[1] = list; + throwsAndLogs( + t, + () => assert.fail(details`${q(list)}`), + /\["a","<\*\*seen\*\*>","c"\]/, + [ + ['log', 'Caught', '(RangeError#1: ["a","<**seen**>","c"])'], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', list], + ], + ); + throwsAndLogs( + t, + () => assert.fail(details`${q(repeat)}`), + /{"x":\["a","<\*\*seen\*\*>","c"\],"y":"<\*\*seen\*\*>"}/, + [ + [ + 'log', + 'Caught', + '(RangeError#1: {"x":["a","<**seen**>","c"],"y":"<**seen**>"})', + ], + ['log', '(RangeError#1) ERR:', RangeError], + ['log', '(RangeError#1) CAUSE:', repeat], + ], + ); + t.end(); +}); diff --git a/packages/console/test/throwsAndLogs.js b/packages/console/test/throwsAndLogs.js new file mode 100644 index 0000000000..7754c421c9 --- /dev/null +++ b/packages/console/test/throwsAndLogs.js @@ -0,0 +1,121 @@ +import { cycleTolerantStringify } from '@agoric/assert'; +import { makeLoggingConsoleKit, makeCausalConsole } from '../src/console.js'; + +const { freeze, getPrototypeOf } = Object; + +// For our internal debugging purposes +const originalConsole = console; +const dumpActualFlag = false; + +// To see what it would look like on a causal wrapping of the +// original console, instead of checking the output against +// the golden logs, set "checkLogs" to false. +// For this purpose, we reuse one nonLoggingConsole across all +// test cases, because the global numbering helps readability. This +// reflects how it would look in a read repair. +// When checkLogs is true, then we make a causalConsole per call +// to assertLogs so that the golden logs are decoupled. +const checkLogs = true; +const nonLoggingConsole = makeCausalConsole(console); + +const compareLogs = freeze((t, log, goldenLog) => { + if (dumpActualFlag) { + originalConsole.log('DUMP ACTUAL:', cycleTolerantStringify(log)); + } + t.is(log.length, goldenLog.length, 'wrong log length'); + log.forEach((logRecord, i) => { + const goldenRecord = goldenLog[i]; + t.is( + logRecord.length, + goldenRecord.length, + `wrong length of log record {$i}`, + ); + logRecord.forEach((logEntry, j) => { + const goldenEntry = goldenRecord[j]; + if ( + goldenEntry === Error || + (typeof goldenEntry === 'function' && + getPrototypeOf(goldenEntry) === Error) + ) { + t.assert(logEntry instanceof goldenEntry, 'not the right error'); + } else { + // tap uses `===` instead of `Object.is`. + // Assuming ava does the right thing, switch back to this when + // switching back to ava. + // t.is(logEntry, goldenEntry); + t.assert( + Object.is(logEntry, goldenEntry), + `${logEntry} not same as ${goldenEntry}`, + ); + } + }); + }); +}); + +// Intended to be used with tape or something like it. +// +// Wraps thunk() but also checks the console. +// TODO It currently checks the console by temporarily assigning +// a fake logging console to the global `console` variable. Once we +// have full Compartment support, we should run tests in a compartment +// with a `console` of our choosing. +// +// During thunk(), each time a console method is called, it +// will just log an array of the method name and the +// args. For example, if the code being tested does +// ```js +// console.error('what ', err); +// ``` +// the test code might check for exactly that with +// ```js +// assertLogs(t, () => /*as above*/, +// [['error', 'what ', err]]); +// ``` +export const assertLogs = freeze((t, thunk, goldenLog) => { + const { loggingConsole, takeLog } = makeLoggingConsoleKit(); + const causalConsole = makeCausalConsole(loggingConsole); + const useConsole = checkLogs ? causalConsole : nonLoggingConsole; + const priorConsole = console; + // eslint-disable-next-line no-global-assign + console = useConsole; + try { + // If thunk() throws, we restore the console and the logging array. + // An outer catcher could then check the error. + thunk(); + } catch (err) { + useConsole.log('Caught', err); + throw err; + } finally { + // eslint-disable-next-line no-global-assign + console = priorConsole; + if (checkLogs) { + const log = takeLog(); + compareLogs(t, log, goldenLog); + } + } +}); + +// Intended to be used with tape or something like it. +// +// Wraps t.throws(thunk, msg) but also checks the console. +// TODO It currently checks the console by temporarily assigning +// a fake logging console to the global `console` variable. Once we +// have full Compartment support, we should run tests in a compartment +// with a `console` of our choosing. +// +// During thunk(), each time a console method is called, it +// will just log an array of the method name and the +// args. For example, if the code being tested does +// ```js +// console.error('what ', err); +// throw new Error('foo'); +// ``` +// the test code might check for exactly that with +// ```js +// throwsAndLogs(t, () => /*as above*/, /foo/, +// [['error', 'what ', err]]); +// ``` +export const throwsAndLogs = freeze((t, thunk, regexp, goldenLog) => { + // assertLogs(t, () => t.throws(thunk, { message: regexp }), goldenLog); + t.throws(() => assertLogs(t, thunk, goldenLog), { message: regexp }); +}); diff --git a/packages/ses/package.json b/packages/ses/package.json index 5f29e2964b..fbd051d8cf 100644 --- a/packages/ses/package.json +++ b/packages/ses/package.json @@ -29,13 +29,15 @@ "clean": "rm -rf dist", "lint": "eslint '**/*.js'", "lint-fix": "eslint --fix '**/*.js'", - "qt": "tap --no-esm --no-coverage --reporter spec 'test/**/*.test.js'", - "test": "yarn build && yarn qt", + "tq": "tap --no-esm --no-coverage --reporter spec 'test/**/*.test.js'", + "test": "yarn build && yarn tq", "test262": "tap --no-esm --no-coverage --reporter spec test262/*.js", "build": "rollup --config rollup.config.js", "demo": "http-server -o /demos" }, "dependencies": { + "@agoric/assert": "0.1.0", + "@agoric/console": "~0.1.0", "@agoric/babel-standalone": "^7.9.5", "@agoric/make-hardener": "^0.1.0", "@agoric/transform-module": "^0.4.1" diff --git a/packages/ses/src/assert.js b/packages/ses/src/assert.js deleted file mode 100644 index b73d3e1a33..0000000000 --- a/packages/ses/src/assert.js +++ /dev/null @@ -1,5 +0,0 @@ -export function assert(condition, errorMessage) { - if (!condition) { - throw new TypeError(errorMessage); - } -} diff --git a/packages/ses/src/assertions.js b/packages/ses/src/assertions.js deleted file mode 100644 index d96f9ac187..0000000000 --- a/packages/ses/src/assertions.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * throwTantrum() - * We'd like to abandon, but we can't, so just scream and break a lot of - * stuff. However, since we aren't really aborting the process, be careful to - * not throw an Error object which could be captured by child-Realm code and - * used to access the (too-powerful) primal-realm Error object. - */ -export function throwTantrum(message, err = undefined) { - const msg = `please report internal shim error: ${message}`; - - // we want to log these 'should never happen' things. - console.error(msg); - if (err) { - console.error(`${err}`); - console.error(`${err.stack}`); - } - - // eslint-disable-next-line no-debugger - debugger; - throw TypeError(msg); -} - -/** - * assert() - */ -export function assert(condition, message) { - if (!condition) { - throwTantrum(message); - } -} diff --git a/packages/ses/src/evaluate.js b/packages/ses/src/evaluate.js index 00b18e9b69..88a81abb3e 100644 --- a/packages/ses/src/evaluate.js +++ b/packages/ses/src/evaluate.js @@ -1,7 +1,7 @@ // Portions adapted from V8 - Copyright 2016 the V8 project authors. // https://github.com/v8/v8/blob/master/src/builtins/builtins-function.cc -import { throwTantrum } from './assertions.js'; +import { assert, details } from '@agoric/assert'; import { apply, immutableObject, proxyRevocable } from './commons.js'; import { getScopeConstants } from './scope-constants.js'; import { createScopeHandler } from './scope-handler.js'; @@ -53,11 +53,15 @@ export function performEval( } finally { if (scopeHandler.useUnsafeEvaluator === true) { // The proxy switches off useUnsafeEvaluator immediately after - // the first access, but if that's not the case we abort. - throwTantrum('handler did not revoke useUnsafeEvaluator', err); - // If we were not able to abort, at least prevent further - // variable resolution via the scopeHandler. + // the first access, but if that's not the case we should abort. + // This condition is one where this vat is now hopelessly confused, + // and the vat as a whole should be aborted. All immediately reachable + // state should be abandoned. However, that is not yet possible, + // so we at least prevent further variable resolution via the + // scopeHandler, and throw an error with diagnostic info including + // the thrown error if any from evaluating the source code. scopeProxyRevocable.revoke(); + assert.fail(details`handler did not revoke useUnsafeEvaluator ${err}`); } } } diff --git a/packages/ses/src/lockdown-shim.js b/packages/ses/src/lockdown-shim.js index 9cf3d0ccbb..c5019df29a 100644 --- a/packages/ses/src/lockdown-shim.js +++ b/packages/ses/src/lockdown-shim.js @@ -13,14 +13,14 @@ // limitations under the License. import makeHardener from '@agoric/make-hardener'; - -import { assert } from './assert.js'; +import { assert, details, q } from '@agoric/assert'; import { keys } from './commons.js'; import { makeIntrinsicsCollector } from './intrinsics.js'; import whitelistIntrinsics from './whitelist-intrinsics.js'; import repairLegacyAccessors from './repair-legacy-accessors.js'; import tameFunctionConstructors from './tame-function-constructors.js'; +import { tameConsole } from './tame-console.js'; import tameDateConstructor from './tame-date-constructor.js'; import tameErrorConstructor from './tame-error-constructor.js'; import tameMathObject from './tame-math-object.js'; @@ -43,7 +43,7 @@ let lockedDown = false; const lockdownHarden = makeHardener(); export const harden = ref => { - assert(lockedDown, `Cannot harden before lockdown`); + assert(lockedDown, 'Cannot harden before lockdown'); return lockdownHarden(ref); }; @@ -66,6 +66,7 @@ export function repairIntrinsics( mathTaming = 'safe', regExpTaming = 'safe', localeTaming = 'safe', + consoleTaming = 'safe', ...extraOptions } = options; @@ -75,7 +76,7 @@ export function repairIntrinsics( const extraOptionsNames = Reflect.ownKeys(extraOptions); assert( extraOptionsNames.length === 0, - `lockdown(): non supported option ${extraOptionsNames.join(', ')}`, + details`lockdown(): non supported option ${q(extraOptionsNames)}`, ); // Asserts for multiple invocation of lockdown(). @@ -83,7 +84,7 @@ export function repairIntrinsics( for (const name of keys(firstOptions)) { assert( options[name] === firstOptions[name], - `lockdown(): cannot re-invoke with different option ${name}`, + details`lockdown(): cannot re-invoke with different option ${q(name)}`, ); } return alreadyHardenedIntrinsics; @@ -95,6 +96,7 @@ export function repairIntrinsics( mathTaming, regExpTaming, localeTaming, + consoleTaming, }; /** @@ -115,6 +117,17 @@ export function repairIntrinsics( const intrinsics = intrinsicsCollector.finalIntrinsics(); + // Wrap console unless suppressed. + // At the moment, the console is considered a host power in the start + // compartment, and not a primordial. Hence it is absent from the whilelist + // and bypasses the intrinsicsCollector. + let optGetStackString; + if (errorTaming !== 'unsafe') { + optGetStackString = intrinsics['%InitialGetStackString%']; + } + const consoleRecord = tameConsole(consoleTaming, optGetStackString); + globalThis.console = consoleRecord.console; + // Replace *Locale* methods with their non-locale equivalents tameLocaleMethods(intrinsics, localeTaming); diff --git a/packages/ses/src/make-function-constructor.js b/packages/ses/src/make-function-constructor.js index c42a9b181a..9ce32101df 100644 --- a/packages/ses/src/make-function-constructor.js +++ b/packages/ses/src/make-function-constructor.js @@ -1,4 +1,4 @@ -import { assert } from './assertions.js'; +import { assert } from '@agoric/assert'; import { arrayJoin, arrayPop, diff --git a/packages/ses/src/scope-handler.js b/packages/ses/src/scope-handler.js index 2aea32fc19..8019354ee4 100644 --- a/packages/ses/src/scope-handler.js +++ b/packages/ses/src/scope-handler.js @@ -1,4 +1,4 @@ -import { throwTantrum } from './assertions.js'; +import { assert, details, q } from '@agoric/assert'; import { getOwnPropertyDescriptor, immutableObject, @@ -21,7 +21,9 @@ const FERAL_EVAL = eval; */ const alwaysThrowHandler = new Proxy(immutableObject, { get(_shadow, prop) { - throwTantrum(`unexpected scope handler trap called: ${String(prop)}`); + assert.fail( + details`Please report unexpected scope handler trap: ${q(String(prop))}`, + ); }, }); diff --git a/packages/ses/src/tame-console.js b/packages/ses/src/tame-console.js new file mode 100644 index 0000000000..e87c180d62 --- /dev/null +++ b/packages/ses/src/tame-console.js @@ -0,0 +1,25 @@ +import { makeCausalConsole } from '@agoric/console'; + +const originalConsole = console; + +/** + * Wrap console unless suppressed. + * At the moment, the console is considered a host power in the start + * compartment, and not a primordial. Hence it is absent from the whilelist + * and bypasses the intrinsicsCollector. + */ +export const tameConsole = ( + consoleTaming = 'safe', + optGetStackString = undefined, +) => { + if (consoleTaming !== 'safe' && consoleTaming !== 'unsafe') { + throw new Error(`unrecognized consoleTaming ${consoleTaming}`); + } + + const causalConsole = makeCausalConsole(originalConsole, optGetStackString); + + if (consoleTaming === 'unsafe') { + return { console: originalConsole }; + } + return { console: causalConsole }; +}; diff --git a/packages/ses/src/tame-locale-methods.js b/packages/ses/src/tame-locale-methods.js index c8904d895b..89c20b4dfe 100644 --- a/packages/ses/src/tame-locale-methods.js +++ b/packages/ses/src/tame-locale-methods.js @@ -1,4 +1,4 @@ -import { assert } from './assert.js'; +import { assert, details, q } from '@agoric/assert'; import { getOwnPropertyNames, defineProperty } from './commons.js'; const localePattern = /^(\w*[a-z])Locale([A-Z]\w*)$/; @@ -21,7 +21,7 @@ const tamedMethods = { if (s > that) { return 1; } - assert(s === that, `expected ${s} and ${that} to compare`); + assert(s === that, details`expected ${q(s)} and ${q(that)} to compare`); return 0; }, }; @@ -48,13 +48,13 @@ export default function tameLocaleMethods(intrinsics, localeTaming = 'safe') { if (match) { assert( typeof intrinsic[methodName] === 'function', - `expected ${methodName} to be a function`, + details`expected ${q(methodName)} to be a function`, ); const nonLocaleMethodName = `${match[1]}${match[2]}`; const method = intrinsic[nonLocaleMethodName]; assert( typeof method === 'function', - `function ${nonLocaleMethodName} not found`, + details`function ${q(nonLocaleMethodName)} not found`, ); defineProperty(intrinsic, methodName, { value: method }); } diff --git a/packages/ses/src/tame-v8-error-constructor.js b/packages/ses/src/tame-v8-error-constructor.js index a417a927da..68508b46ad 100644 --- a/packages/ses/src/tame-v8-error-constructor.js +++ b/packages/ses/src/tame-v8-error-constructor.js @@ -39,8 +39,14 @@ const safeV8CallSiteFacet = callSite => { const safeV8SST = sst => sst.map(safeV8CallSiteFacet); +const callSiteFilter = _callSite => true; +// const callSiteFilter = callSite => +// !callSite.getFileName().includes('/node_modules/'); + +const callSiteStringifier = callSite => `\n at ${callSite}`; + const stackStringFromSST = (error, sst) => - [`${error}`, ...sst.map(callSite => `\n at ${callSite}`)].join(''); + [`${error}`, ...sst.filter(callSiteFilter).map(callSiteStringifier)].join(''); export function tameV8ErrorConstructor( OriginalError, diff --git a/packages/ses/test/assertions.test.js b/packages/ses/test/assertions.test.js deleted file mode 100644 index cb4292cdca..0000000000 --- a/packages/ses/test/assertions.test.js +++ /dev/null @@ -1,65 +0,0 @@ -import tap from 'tap'; -import sinon from 'sinon'; -import { assert, throwTantrum } from '../src/assertions.js'; - -const { test } = tap; - -test('throwTantrum', t => { - t.plan(3); - - sinon.stub(console, 'error').callsFake(); - - t.throws( - () => throwTantrum('foo'), - /^please report internal shim error: foo$/, - ); - - t.equals(console.error.callCount, 1); - t.equals( - console.error.getCall(0).args[0], - 'please report internal shim error: foo', - ); - - sinon.restore(); -}); - -test('throwTantrum', t => { - t.plan(5); - - sinon.stub(console, 'error'); - - t.throws( - () => throwTantrum('foo', new Error('bar')), - /^please report internal shim error: foo$/, - ); - - t.equals(console.error.callCount, 3); - t.equals( - console.error.getCall(0).args[0], - 'please report internal shim error: foo', - ); - t.equals(console.error.getCall(1).args[0], 'Error: bar'); - t.ok(console.error.getCall(2).args[0].match(/\sat (Test|t)\.throws/)); - - sinon.restore(); -}); - -test('assert', t => { - t.plan(4); - - sinon.stub(console, 'error').callsFake(); - - t.doesNotThrow(() => assert(true, 'foo')); - t.throws( - () => assert(false, 'foo'), - /^please report internal shim error: foo$/, - ); - - t.equals(console.error.callCount, 1); - t.equals( - console.error.getCall(0).args[0], - 'please report internal shim error: foo', - ); - - sinon.restore(); -}); diff --git a/packages/ses/test/check-anon-intrinsics.js b/packages/ses/test/check-anon-intrinsics.js index c34bf84d68..35de50bbf7 100644 --- a/packages/ses/test/check-anon-intrinsics.js +++ b/packages/ses/test/check-anon-intrinsics.js @@ -1,4 +1,4 @@ -import { assert } from './assert.js'; +import { assert } from '@agoric/assert'; import { getPrototypeOf } from './commons.js'; /** @@ -59,7 +59,6 @@ export function checkAnonIntrinsics(intrinsics) { // object visible. assert( getPrototypeOf(TypedArray) === Function.prototype, - 'TypedArray.__proto__ should be Function.prototype', ); diff --git a/packages/ses/test/scope-handler.test.js b/packages/ses/test/scope-handler.test.js index 902663c897..9209c10567 100644 --- a/packages/ses/test/scope-handler.test.js +++ b/packages/ses/test/scope-handler.test.js @@ -174,9 +174,11 @@ test('scopeHandler - throw only for unsupported traps', t => { 'ownKeys', 'preventExtensions', 'setPrototypeOf', - ].forEach( - trap => t.throws(() => handler[trap]), - /unexpected scope handler trap called/, + ].forEach(trap => + t.throws( + () => handler[trap], + /Please report unexpected scope handler trap:/, + ), ); sinon.restore(); diff --git a/packages/ses/test/ses.test.js b/packages/ses/test/ses.test.js index fa236e575a..5ebf8dbe5d 100644 --- a/packages/ses/test/ses.test.js +++ b/packages/ses/test/ses.test.js @@ -1,28 +1,10 @@ import test from 'tape'; import '../ses.js'; -const originalConsole = console; - lockdown(); /* eslint-disable no-proto, no-empty-function */ -test('console', t => { - t.plan(3); - - t.equal(console, originalConsole); - - harden(console.__proto__); - harden(console); - const c1 = new Compartment({ console }); - t.equal(console, c1.evaluate('(console)')); - - const fakeConsole = { log: console.log }; - harden(fakeConsole); - const c2 = new Compartment({ console: fakeConsole }); - t.equal(console.log, c2.evaluate('(console.log)')); -}); - test('tamed constructors', t => { t.plan(12); diff --git a/packages/ses/test/tame-console-unit.test.js b/packages/ses/test/tame-console-unit.test.js new file mode 100644 index 0000000000..361b4560e9 --- /dev/null +++ b/packages/ses/test/tame-console-unit.test.js @@ -0,0 +1,93 @@ +import { logToConsole, asLogRecord } from '@agoric/assert'; +import tap from 'tap'; +import { tameConsole } from '../src/tame-console.js'; + +const { test } = tap; + +const { console: safeConsole } = tameConsole(); +const { console: unsafeConsole } = tameConsole('unsafe'); + +// The @agoric/console package has the automated console tests. +// The following console tests are only a sanity check for eyeballing the +// output. See the descriptions below for what you should expect to see for +// each test case. + +// This shows the cause-tracking. We instruct the console to +// silently remember the cause as explaining the cause of barErr. +// Once barErr itself is actually logged, we give it a unique tag (URIError#1), +// log the error with stack trace in a separate log message beginning +// "(URIError#1) ERR:", and then emit a log message for each of its causes +// beginning "(URIError#1) CAUSE:". +test('tameConsole unit - safe', t => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + const barErr = new URIError('bar'); + logToConsole( + safeConsole, + asLogRecord({ + level: 'log', + cause: ['foo,obj cause bar', fooErr, obj], + error: barErr, + }), + ); + safeConsole.log('bar happens', barErr); + t.end(); +}); + +// This shows that code assuming such a cause tracking console fails soft if +// interacting with a normal system console. No log messages are remembered for +// later display. Rather, each is immediately output. When a logged message has +// multiple error arguments, on node at the time of this writing, their stack +// traces are shown continuously with no break between them. Without naming the +// errors uniquely, we cannot reliably tell from this log when the same error +// reappears, and so cannot reliably recover the causality tracking. +test('tameConsole unit - unsafe', t => { + const obj = {}; + const faaErr = new TypeError('faa'); + const borErr = new ReferenceError('bor'); + logToConsole( + unsafeConsole, + asLogRecord({ + level: 'log', + cause: ['faa,obj cause bor', faaErr, obj], + error: borErr, + }), + ); + unsafeConsole.log('bor happens', borErr); + t.end(); +}); + +// This shows that a message remembered as associated with an error (ubarErr) +// is never seen if the error it allegedly caused is never actually logged. +test('tameConsole unit - unlogged safe', t => { + const obj = {}; + const ufooErr = new SyntaxError('ufoo'); + const ubarErr = new URIError('ubar'); + logToConsole( + safeConsole, + asLogRecord({ + level: 'log', + cause: ['ufoo,obj cause ubar', ufooErr, obj], + error: ubarErr, + }), + ); + t.end(); +}); + +// Code assuming a cause tracking console again fails soft, but noisily, +// if interacting with a normal system console. Each of the cause tracking +// messages is immediately emitted rather than being silently remembered. +test('tameConsole unit - unlogged unsafe', t => { + const obj = {}; + const ufaaErr = new TypeError('ufaa'); + const uborErr = new ReferenceError('ubor'); + logToConsole( + unsafeConsole, + asLogRecord({ + level: 'log', + cause: ['ufaa,obj cause ubor', ufaaErr, obj], + error: uborErr, + }), + ); + t.end(); +}); diff --git a/packages/ses/test/tame-console-unsafe-unsafeError.test.js b/packages/ses/test/tame-console-unsafe-unsafeError.test.js new file mode 100644 index 0000000000..9cee065661 --- /dev/null +++ b/packages/ses/test/tame-console-unsafe-unsafeError.test.js @@ -0,0 +1,94 @@ +import { assert, details, logToConsole, asLogRecord } from '@agoric/assert'; +import test from 'tape'; +import '../ses.js'; + +const { getPrototypeOf } = Object; + +const originalConsole = console; + +lockdown({ consoleTaming: 'unsafe', errorTaming: 'unsafe' }); + +test('console', t => { + t.plan(3); + + t.equal(console, originalConsole); + + harden(getPrototypeOf(console)); + harden(console); + const c1 = new Compartment({ console }); + t.equal(console, c1.evaluate('(console)')); + + const fakeConsole = { log: console.log }; + harden(fakeConsole); + const c2 = new Compartment({ console: fakeConsole }); + t.equal(console.log, c2.evaluate('(console.log)')); +}); + +// The @agoric/console package has the automated console tests. +// The following console tests are only a sanity check for eyeballing the +// output. See the following descriptions for what you should expect to +// see for each test case. + +// The assert failure both throws an error, and tries to silently +// remembers a pending console as the alleged cause of the thrown error. +// It does so assuming the console is the safe causality-tracking console. +// However, if assert outputs to the normal system console instead, it fails +// softly but noisily. These causality tracking messages are immediately logged +// in the encoding defined by `asLogRecord`, rather than being associated with +// the thrown error. +test('assert - unsafe', t => { + try { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + } catch (barErr) { + console.error('bar happens', barErr); + } + t.end(); +}); + +// As above, the causality tracking message is output immediately. +// In this case, even though the thrown error itself is never +// explicitly logged. +test('assert - unlogged unsafe', t => { + t.throws(() => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + }); + t.end(); +}); + +// See the descriptions in tame-console-unit.test.js for what you +// should expect to see for each of the following test cases. + +test('tameConsole - unsafe', t => { + const obj = {}; + const faaErr = new TypeError('faa'); + const borErr = new ReferenceError('bor'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['faa,obj cause bor', faaErr, obj], + error: borErr, + }), + ); + console.log('bor happens', borErr); + t.end(); +}); + +test('tameConsole - unlogged unsafe', t => { + const obj = {}; + const ufaaErr = new TypeError('ufaa'); + const uborErr = new ReferenceError('ubor'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['ufaa,obj cause ubor', ufaaErr, obj], + error: uborErr, + }), + ); + t.end(); +}); diff --git a/packages/ses/test/tame-console-unsafe.test.js b/packages/ses/test/tame-console-unsafe.test.js new file mode 100644 index 0000000000..c71f894592 --- /dev/null +++ b/packages/ses/test/tame-console-unsafe.test.js @@ -0,0 +1,94 @@ +import { assert, details, logToConsole, asLogRecord } from '@agoric/assert'; +import test from 'tape'; +import '../ses.js'; + +const { getPrototypeOf } = Object; + +const originalConsole = console; + +lockdown({ consoleTaming: 'unsafe' }); + +test('console', t => { + t.plan(3); + + t.equal(console, originalConsole); + + harden(getPrototypeOf(console)); + harden(console); + const c1 = new Compartment({ console }); + t.equal(console, c1.evaluate('(console)')); + + const fakeConsole = { log: console.log }; + harden(fakeConsole); + const c2 = new Compartment({ console: fakeConsole }); + t.equal(console.log, c2.evaluate('(console.log)')); +}); + +// The @agoric/console package has the automated console tests. +// The following console tests are only a sanity check for eyeballing the +// output. See the following descriptions for what you should expect to +// see for each test case. + +// The assert failure both throws an error, and tries to silently +// remembers a pending console as the alleged cause of the thrown error. +// It does so assuming the console is the safe causality-tracking console. +// However, if assert outputs to the normal system console instead, it fails +// softly but noisily. These causality tracking messages are immediately logged +// in the encoding defined by `asLogRecord`, rather than being associated with +// the thrown error. +test('assert - unsafe', t => { + try { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + } catch (barErr) { + console.error('bar happens', barErr); + } + t.end(); +}); + +// As above, the causality tracking message is output immediately. +// In this case, even though the thrown error itself is never +// explicitly logged. +test('assert - unlogged unsafe', t => { + t.throws(() => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + }); + t.end(); +}); + +// See the descriptions in tame-console-unit.test.js for what you +// should expect to see for each of the following test cases. + +test('tameConsole - unsafe', t => { + const obj = {}; + const faaErr = new TypeError('faa'); + const borErr = new ReferenceError('bor'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['faa,obj cause bor', faaErr, obj], + error: borErr, + }), + ); + console.log('bor happens', borErr); + t.end(); +}); + +test('tameConsole - unlogged unsafe', t => { + const obj = {}; + const ufaaErr = new TypeError('ufaa'); + const uborErr = new ReferenceError('ubor'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['ufaa,obj cause ubor', ufaaErr, obj], + error: uborErr, + }), + ); + t.end(); +}); diff --git a/packages/ses/test/tame-console-unsafeError.test.js b/packages/ses/test/tame-console-unsafeError.test.js new file mode 100644 index 0000000000..c3af29dba7 --- /dev/null +++ b/packages/ses/test/tame-console-unsafeError.test.js @@ -0,0 +1,94 @@ +import { assert, details, logToConsole, asLogRecord } from '@agoric/assert'; +import test from 'tape'; +import '../ses.js'; + +const { getPrototypeOf } = Object; + +const originalConsole = console; + +lockdown({ errorTaming: 'unsafe' }); + +test('console', t => { + t.plan(3); + + t.notEqual(console, originalConsole); + + harden(getPrototypeOf(console)); + harden(console); + const c1 = new Compartment({ console }); + t.equal(console, c1.evaluate('(console)')); + + const fakeConsole = { log: console.log }; + harden(fakeConsole); + const c2 = new Compartment({ console: fakeConsole }); + t.equal(console.log, c2.evaluate('(console.log)')); +}); + +// The @agoric/console package has the automated console tests. +// The following console tests are only a sanity check for eyeballing the +// output. See the following descriptions for what you should expect to +// see for each test case. + +// The assert failure both throws an error, and silently remembers +// a pending console as the alleged cause of the thrown error. +// The thrown error message has placeholders for the data in the details +// template literal, like "(a SyntaxError)" and "(an object)". +// The corresponding pending console message remembers the actual values. +// When the thrown error is actually logged, the remembered causes are also +// logged, as are any errors embedded in them, and the causes of those errors. +test('assert - safe', t => { + try { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + } catch (barErr) { + console.error('bar happens', barErr); + } + t.end(); +}); + +// The assert failure both throws and error and silently remembers +// the data from the details as the alleged cause. However, if the thrown +// error is never logged, then neither is the associated cause. +test('assert - unlogged safe', t => { + t.throws(() => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + }); + t.end(); +}); + +// See the descriptions in tame-console-unit.test.js for what you +// should expect to see for each of the following test cases. + +test('tameConsole - safe', t => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + const barErr = new URIError('bar'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['foo,obj cause bar', fooErr, obj], + error: barErr, + }), + ); + console.log('bar happens', barErr); + t.end(); +}); + +test('tameConsole - unlogged safe', t => { + const obj = {}; + const ufooErr = new SyntaxError('ufoo'); + const ubarErr = new URIError('ubar'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['ufoo,obj cause ubar', ufooErr, obj], + error: ubarErr, + }), + ); + t.end(); +}); diff --git a/packages/ses/test/tame-console.test.js b/packages/ses/test/tame-console.test.js new file mode 100644 index 0000000000..c23b0725c0 --- /dev/null +++ b/packages/ses/test/tame-console.test.js @@ -0,0 +1,94 @@ +import { assert, details, logToConsole, asLogRecord } from '@agoric/assert'; +import test from 'tape'; +import '../ses.js'; + +const { getPrototypeOf } = Object; + +const originalConsole = console; + +lockdown(); + +test('console', t => { + t.plan(3); + + t.notEqual(console, originalConsole); + + harden(getPrototypeOf(console)); + harden(console); + const c1 = new Compartment({ console }); + t.equal(console, c1.evaluate('(console)')); + + const fakeConsole = { log: console.log }; + harden(fakeConsole); + const c2 = new Compartment({ console: fakeConsole }); + t.equal(console.log, c2.evaluate('(console.log)')); +}); + +// The @agoric/console package has the automated console tests. +// The following console tests are only a sanity check for eyeballing the +// output. See the following descriptions for what you should expect to +// see for each test case. + +// The assert failure both throws an error, and silently remembers +// a pending console as the alleged cause of the thrown error. +// The thrown error message has placeholders for the data in the details +// template literal, like "(a SyntaxError)" and "(an object)". +// The corresponding pending console message remembers the actual values. +// When the thrown error is actually logged, the remembered causes are also +// logged, as are any errors embedded in them, and the causes of those errors. +test('assert - safe', t => { + try { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + } catch (barErr) { + console.error('bar happens', barErr); + } + t.end(); +}); + +// The assert failure both throws and error and silently remembers +// the data from the details as the alleged cause. However, if the thrown +// error is never logged, then neither is the associated cause. +test('assert - unlogged safe', t => { + t.throws(() => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + assert.fail(details`${fooErr},${obj} cause failure`); + }); + t.end(); +}); + +// See the descriptions in tame-console-unit.test.js for what you +// should expect to see for each of the following test cases. + +test('tameConsole - safe', t => { + const obj = {}; + const fooErr = new SyntaxError('foo'); + const barErr = new URIError('bar'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['foo,obj cause bar', fooErr, obj], + error: barErr, + }), + ); + console.log('bar happens', barErr); + t.end(); +}); + +test('tameConsole - unlogged safe', t => { + const obj = {}; + const ufooErr = new SyntaxError('ufoo'); + const ubarErr = new URIError('ubar'); + logToConsole( + console, + asLogRecord({ + level: 'log', + cause: ['ufoo,obj cause ubar', ufooErr, obj], + error: ubarErr, + }), + ); + t.end(); +}); diff --git a/yarn.lock b/yarn.lock index a870a2b558..a5ef682855 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2442,6 +2442,11 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= +array-filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= + array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -2595,6 +2600,13 @@ auto-bind@^4.0.0: resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== +available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" + integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== + dependencies: + array-filter "^1.0.0" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -4030,6 +4042,26 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" + integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== + dependencies: + es-abstract "^1.17.5" + es-get-iterator "^1.1.0" + is-arguments "^1.0.4" + is-date-object "^1.0.2" + is-regex "^1.0.5" + isarray "^2.0.5" + object-is "^1.1.2" + object-keys "^1.1.1" + object.assign "^4.1.0" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.2" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.1" + which-typed-array "^1.1.2" + deep-equal@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -4283,7 +4315,7 @@ dotenv@^5.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow== -dotignore@~0.1.2: +dotignore@^0.1.2, dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw== @@ -4435,7 +4467,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: version "1.17.6" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== @@ -4452,6 +4484,37 @@ es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstrac string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" +es-abstract@^1.18.0-next.0: + version "1.18.0-next.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc" + integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-get-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" + integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== + dependencies: + es-abstract "^1.17.4" + has-symbols "^1.0.1" + is-arguments "^1.0.4" + is-map "^2.0.1" + is-set "^2.0.1" + is-string "^1.0.5" + isarray "^2.0.5" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -5070,7 +5133,7 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== -for-each@~0.3.3: +for-each@^0.3.3, for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== @@ -6076,6 +6139,11 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" + integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== + is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -6090,6 +6158,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" + integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== + is-buffer@^1.1.0, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -6133,7 +6206,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== @@ -6221,11 +6294,26 @@ is-html@^1.1.0: dependencies: html-tags "^1.0.0" +is-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" + integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + +is-number-object@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" + integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -6277,7 +6365,7 @@ is-reference@^1.1.2: dependencies: "@types/estree" "*" -is-regex@^1.0.4, is-regex@^1.1.0: +is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -6296,6 +6384,11 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== +is-set@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" + integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== + is-ssh@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.2.tgz#a4b82ab63d73976fd8263cceee27f99a88bdae2b" @@ -6308,7 +6401,7 @@ is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-string@^1.0.5: +is-string@^1.0.4, is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== @@ -6334,6 +6427,16 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" +is-typed-array@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" + integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== + dependencies: + available-typed-arrays "^1.0.0" + es-abstract "^1.17.4" + foreach "^2.0.5" + has-symbols "^1.0.1" + is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -6354,6 +6457,16 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakset@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" + integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== + is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -6374,7 +6487,7 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@^2.0.1: +isarray@^2.0.1, isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== @@ -7727,7 +7840,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.7.0: +object-inspect@^1.7.0, object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== @@ -7742,7 +7855,7 @@ object-inspect@~1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== -object-is@^1.0.1: +object-is@^1.0.1, object-is@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== @@ -9235,7 +9348,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.2.0: +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== @@ -9418,7 +9531,7 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -resumer@~0.0.0: +resumer@^0.0.0, resumer@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" integrity sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k= @@ -9750,6 +9863,14 @@ shell-quote@^1.6.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +side-channel@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" + integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== + dependencies: + es-abstract "^1.18.0-next.0" + object-inspect "^1.8.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -10157,7 +10278,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trim@~1.2.1: +string.prototype.trim@^1.2.1, string.prototype.trim@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== @@ -10525,6 +10646,29 @@ tape@4.12.1: string.prototype.trim "~1.2.1" through "~2.3.8" +tape@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/tape/-/tape-5.0.1.tgz#0d70ce90a586387c4efda4393e72872672a416a3" + integrity sha512-wVsOl2shKPcjdJdc8a+PwacvrOdJZJ57cLUXlxW4TQ2R6aihXwG0m0bKm4mA4wjtQNTaLMCrYNEb4f9fjHKUYQ== + dependencies: + deep-equal "^2.0.3" + defined "^1.0.0" + dotignore "^0.1.2" + for-each "^0.3.3" + function-bind "^1.1.1" + glob "^7.1.6" + has "^1.0.3" + inherits "^2.0.4" + is-regex "^1.0.5" + minimist "^1.2.5" + object-inspect "^1.7.0" + object-is "^1.1.2" + object.assign "^4.1.0" + resolve "^1.17.0" + resumer "^0.0.0" + string.prototype.trim "^1.2.1" + through "^2.3.8" + tape@^4.12.1: version "4.13.3" resolved "https://registry.yarnpkg.com/tape/-/tape-4.13.3.tgz#51b3d91c83668c7a45b1a594b607dee0a0b46278" @@ -10674,7 +10818,7 @@ through2@^3.0.0: inherits "^2.0.4" readable-stream "2 || 3" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3.4, through@~2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -11322,11 +11466,44 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" +which-boxed-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" + integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== + dependencies: + is-bigint "^1.0.0" + is-boolean-object "^1.0.0" + is-number-object "^1.0.3" + is-string "^1.0.4" + is-symbol "^1.0.2" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-typed-array@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" + integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== + dependencies: + available-typed-arrays "^1.0.2" + es-abstract "^1.17.5" + foreach "^2.0.5" + function-bind "^1.1.1" + has-symbols "^1.0.1" + is-typed-array "^1.1.3" + which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"