Skip to content

Commit

Permalink
fix: Implement ava wrapper, and monkey test it.
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Mar 5, 2021
1 parent f8524bc commit 376711f
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"packages/make-importer",
"packages/make-simple-evaluate",
"packages/ses",
"packages/ses-ava",
"packages/ses-integration-test",
"packages/test262-runner",
"packages/transform-module"
Expand Down
4 changes: 4 additions & 0 deletions packages/ses-ava/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"trailingComma": "all",
"singleQuote": true
}
1 change: 1 addition & 0 deletions packages/ses-ava/exported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './src/types';
11 changes: 11 additions & 0 deletions packages/ses-ava/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"noEmit": true,
"downlevelIteration": true,
"strictNullChecks": true,
"moduleResolution": "node"
},
"include": ["src/**/*.js"]
}
40 changes: 32 additions & 8 deletions packages/ses-ava/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@agoric/ses-ava",
"version": "0.0.0+1-dev",
"description": "Description forthcoming.",
"description": "Virtualize Ava's test to work better under SES.",
"author": "Agoric",
"license": "Apache-2.0",
"type": "module",
Expand All @@ -17,21 +17,45 @@
"scripts": {
"build": "rollup --config rollup.config.js",
"clean": "rm -rf dist",
"lint": "eslint '**/*.js'",
"lint": "yarn lint:types && yarn lint:js",
"lint:types": "tsc --build jsconfig.json",
"lint:js": "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"
"test": "yarn build && yarn ava",
"qt": "yarn ava"
},
"dependencies": {
"ses": "^0.12.3"
},
"dependencies": {},
"devDependencies": {
"ava": "^3.12.1",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-node-resolve": "^6.1.0",
"rollup-plugin-terser": "^5.1.3",
"tap": "14.10.5",
"tape": "5.0.1"
"@rollup/plugin-json": "^4.1.0",
"@agoric/eslint-config": "^0.1.0",
"prettier": "^1.19.1",
"rollup-plugin-terser": "^5.1.3"
},
"ava": {
"files": [
"*test*/**/test-*.js"
],
"require": []
},
"eslintConfig": {
"extends": [
"@agoric"
]
},
"prettier": {
"trailingComma": "all",
"singleQuote": true
},
"files": [
"LICENSE*",
"dist",
"src"
"src",
"exported.js"
]
}
7 changes: 4 additions & 3 deletions packages/ses-ava/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import commonjs from '@rollup/plugin-commonjs';
import fs from 'fs';

const metaPath = new URL('package.json', import.meta.url).pathname;
Expand All @@ -20,7 +21,7 @@ export default [
format: 'cjs',
},
],
plugins: [resolve()],
plugins: [resolve(), commonjs()],
},
{
input: 'src/main.js',
Expand All @@ -29,7 +30,7 @@ export default [
format: 'umd',
name: umd,
},
plugins: [resolve()],
plugins: [resolve(), commonjs()],
},
{
input: 'src/main.js',
Expand All @@ -38,6 +39,6 @@ export default [
format: 'umd',
name: umd,
},
plugins: [resolve(), terser()],
plugins: [resolve(), commonjs(), terser()],
},
];
1 change: 1 addition & 0 deletions packages/ses-ava/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { wrapTest } from './ses-ava-test';
123 changes: 123 additions & 0 deletions packages/ses-ava/src/ses-ava-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// @ts-check
import 'ses';
import './types.js';

const { apply } = Reflect;

/**
* Just forwards to global `console.error`.
*
* @type {import('./types').Logger}
*/
const defaultLogger = (...args) => {
console.error(...args);
};

/**
* Determine if the argument is a Promise.
* (Approximately copied from promiseKit.js)
*
* @param {any} maybePromise The value to examine
* @returns {maybePromise is Promise} Whether it is a promise
*/
const isPromise = maybePromise =>
Promise.resolve(maybePromise) === maybePromise;

/**
* @type {import('./types').LogCallError}
*/
const logErrorFirst = (func, args, name, logger = defaultLogger) => {
let result;
try {
result = apply(func, undefined, args);
} catch (err) {
logger(`THROWN from ${name}:`, err);
throw err;
}
if (isPromise(result)) {
return result.then(
v => v,
reason => {
logger(`REJECTED from ${name}:`, reason);
return result;
},
);
} else {
return result;
}
};

const testerMethodsWhitelist = [
'after',
'afterEach',
'before',
'beforeEach',
'cb',
'failing',
'serial',
'only',
'skip',
];

/**
* @param { import('./types').TesterFunc } testerFunc
* @param { import('./types').Logger =} logger
* @returns { import('./types').TesterFunc } Not yet frozen!
*/
const wrapTester = (testerFunc, logger = defaultLogger) => {
/** @type {import('./types').TesterFunc} */
const testerWrapper = (title, implFunc) => {
/** @type {import('./types').ImplFunc} */
const testFuncWrapper = t => {
harden(t);
return logErrorFirst(implFunc, [t], 'ava test', logger);
};
return testerFunc(title, testFuncWrapper);
};
return testerWrapper;
};

/**
* The ava `test` function takes a callback argument of the form
* `t => {...}`. If the outcome of this function indicates an error, either
* by throwing or by eventually rejecting a returned promise, ava does its
* own peculiar console-like display of this error and its stacktrace.
* However, it does not use the ses `console` and so bypasses all the fancy
* diagnostics provided by the ses `console`.
*
* To use this package, a test file replaces the line
* ```js
* import test from 'ava';
* ```
* with
* ```js
* import { wrapTest } from '@agoric/ses-ava';
* import rawTest from 'ava';
*
* const test = wrapTest(rawTest);
* ```
* Then the calls to `test` in the rest of the test file will act like they
* used to, except that, if a test fails because the test function (the
* callback argument to `test`) throws or returns a promise
* that eventually rejects, the error is first sent to the `console` before
* propagating into `rawTest`.
*
* @param {import('./types').TesterInterface} avaTest
* @param {import('./types').Logger=} logger
* @returns {import('./types').TesterInterface}
*/
const wrapTest = (avaTest, logger = defaultLogger) => {
/** @type {import('./types').TesterInterface} */
const testerWrapper = wrapTester(avaTest, logger);
for (const methodName of testerMethodsWhitelist) {
if (methodName in avaTest) {
const testerMethod = (title, implFunc) =>
avaTest[methodName](title, implFunc);
testerWrapper[methodName] = wrapTester(testerMethod);
}
}
harden(testerWrapper);
return testerWrapper;
};
// harden(wrapTest);
export { wrapTest };
69 changes: 69 additions & 0 deletions packages/ses-ava/src/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// @ts-check
import 'ses';

/**
* @callback Logger
* @param {...any} args
* @returns {void}
*/

/**
* @callback LogCallError
*
* Calls `thunk()` passing back approximately its outcome, but first
* logging any erroneous outcome to the `logger`, which defaults to
* `console.log`.
*
* If thunk returns a non-promise, silently return it.
* If thunk throws, log what was thrown and then rethrow it.
* If thunk returns a promise, immediately return a new unresolved promise.
* If the first promise fulfills, silently fulfill the returned promise even if
* the fulfillment was an error.
* If the first promise rejects, log the rejection reason and then reject the
* returned promise with the same reason.
* The delayed rejection of the returned promise is an observable difference
* from directly calling `thunk()` but will be equivalent enough for most
* purposes.
*
* TODO This function is useful independent of ava, so consider moving it
* somewhere and exporting it for general reuse.
*
* @param {(...any) => any} func
* @param {any[]} args
* @param {string} name
* @param {Logger=} logger
*/

/**
* Simplified form of ava's types.
* TODO perhaps just import ava's type declarations instead
*
* @typedef {Object} Assertions
* @property {(actual: any, message?: string) => void} assert
* // TODO is, deepEqual, truthy, falsy, etc...
*/

/**
* @callback ImplFunc
* This is the function that invariably starts `t => {`.
* Ava's types call this `Implementation`, but that's just too confusing.
*
* @param {Assertions} t
* @returns {any}
*
* @callback TesterFunc
* @param {string} title
* @param {ImplFunc} implFunc
* @returns {void}
*
* @typedef {TesterFunc} TesterInterface
* @property {TesterFunc} after
* @property {TesterFunc} afterEach
* @property {TesterFunc} before
* @property {TesterFunc} beforeEach
* @property {TesterFunc} cb
* @property {TesterFunc} failing
* @property {TesterFunc} serial
* @property {TesterFunc} only
* @property {TesterFunc} skip
*/
54 changes: 54 additions & 0 deletions packages/ses-ava/test/test-raw-ava-reject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'ses';
import test from 'ava';

lockdown({
// Comment or uncomment each of these switches to see variations of the
// output shown below. When all the switches are uncommented, you should
// see that output.
//
stackFiltering: 'verbose', // Exclude `assert` infrastructure
consoleTaming: 'unsafe', // Doesn't make a difference here
errorTaming: 'unsafe', // Redacts entire `error.stack`
});

test('raw ava reject console output', t => {
t.assert(true);
// Uncomment this to see something like the text in the extended comment below

/*
return Promise.resolve(null)
.then(v => v)
.then(v => v)
.then(_ => {
assert.typeof(88, 'string', assert.details`msg ${'NOTICE ME'}`);
});
*/
});

/*
Uncommenting the test code above should produce something like the following.
This output is all from ava. The stack-like display comes from ava's direct
use of the `error.stack` property. Ava bypasses the normal `console`.
For the error message, ava has no access to the non-disclosed
`'NOTICE ME'`, only the redacted `'(a string)'.
```
raw ava reject console output
Rejected promise returned by test. Reason:
TypeError {
message: 'msg (a string)',
}
› makeError (file:///Users/markmiller/src/ongithub/agoric/SES-shim/packages/ses/src/error/assert.js:141:17)
› fail (file:///Users/markmiller/src/ongithub/agoric/SES-shim/packages/ses/src/error/assert.js:260:20)
› baseAssert (file:///Users/markmiller/src/ongithub/agoric/SES-shim/packages/ses/src/error/assert.js:278:13)
› equal (file:///Users/markmiller/src/ongithub/agoric/SES-shim/packages/ses/src/error/assert.js:289:5)
› Function.assertTypeof [as typeof] (file:///Users/markmiller/src/ongithub/agoric/SES-shim/packages/ses/src/error/assert.js:308:5)
› file://test/test-raw-ava-reject.js:22:20
1 test failed
```
*/
Loading

0 comments on commit 376711f

Please sign in to comment.