Skip to content

Commit

Permalink
feat(ses-ava): Support the full ava API (#1465)
Browse files Browse the repository at this point in the history
  • Loading branch information
gibson042 authored Jan 27, 2023
1 parent 53719ca commit 0e4f980
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 116 deletions.
1 change: 0 additions & 1 deletion packages/promise-kit/test/test-promise-kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ if (typeof globalThis.gc !== 'function') {
}

/** @type {typeof rawTest} */
// @ts-expect-error cast
const test = wrapTest(rawTest);

test('makePromiseKit', async t => {
Expand Down
3 changes: 3 additions & 0 deletions packages/ses-ava/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
User-visible changes in SES-Ava:

# Next release
* Support the full ava API ([#1235](https://github.com/endojs/endo/issues/1235))

# 0.2.0 (2021-06-01)

* *BREAKING*: Removes CommonJS and UMD downgrade compatibility.
Expand Down
1 change: 0 additions & 1 deletion packages/ses-ava/exported.js
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
import './src/types.js';
135 changes: 92 additions & 43 deletions packages/ses-ava/src/ses-ava-test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'ses';
import './types.js';

const { defineProperty, freeze } = Object;
const { apply } = Reflect;

/**
* Just forwards to global `console.error`.
*
* @type {Logger}
* @typedef {(...args: unknown[]) => void} Logger
*/

const defaultLogger = (...args) => {
console.error(...args);
};
Expand All @@ -23,9 +22,28 @@ const isPromise = maybePromise =>
Promise.resolve(maybePromise) === maybePromise;

/**
* @type {LogCallError}
* Calls `func(...args)` passing back approximately its outcome, but first
* logging any erroneous outcome to the `logger`.
*
* * If `func(...args)` returns a non-promise, silently return it.
* * If `func(...args)` throws, log what was thrown and then rethrow it.
* * If `func(...args)` 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 `func(...args)` but will be equivalent enough for most
* purposes.
*
* @param {(...unknown) => unknown} func
* @param {unknown[]} args
* @param {string} name
* @param {Logger} logger
*/
const logErrorFirst = (func, args, name, logger = defaultLogger) => {
const logErrorFirst = (func, args, name, logger) => {
let result;
try {
result = apply(func, undefined, args);
Expand All @@ -46,45 +64,72 @@ const logErrorFirst = (func, args, name, logger = defaultLogger) => {
}
};

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

/**
* @param {TesterFunc} testerFunc
* @param {Logger} [logger]
* @returns {TesterFunc} Not yet frozen!
* @template {import('ava').TestFn} T
* @param {T} testerFunc
* @param {Logger} logger
* @returns {T} Not yet frozen!
*/
const wrapTester = (testerFunc, logger = defaultLogger) => {
/** @type {TesterFunc} */
const testerWrapper = (title, implFunc, ...otherArgs) => {
/** @type {ImplFunc} */
const testFuncWrapper = t => {
harden(t);
return logErrorFirst(implFunc, [t, ...otherArgs], 'ava test', logger);
const augmentLogging = (testerFunc, logger) => {
const testerFuncName = `ava ${testerFunc.name || 'test'}`;
const augmented = (...args) => {
// Align with ava argument parsing.
// https://github.com/avajs/ava/blob/c74934853db1d387c46ed1f953970c777feed6a0/lib/parse-test-args.js
const maybeTitle = typeof args[0] === 'string' ? [args.shift()] : [];
const implFuncOrObj = args.shift();
const wrapImplFunc = fn => {
const wrappedFunc = t => {
harden(t);
return logErrorFirst(fn, [t, ...args], testerFuncName, logger);
};
if (fn.title) {
wrappedFunc.title = fn.title;
}
return wrappedFunc;
};
if (implFunc && implFunc.title) {
testFuncWrapper.title = implFunc.title;
let implArg;
if (typeof implFuncOrObj === 'function') {
// Handle common cases like `test(title, t => { ... }, ...)`.
implArg = wrapImplFunc(implFuncOrObj);
} else if (typeof implFuncOrObj === 'object' && implFuncOrObj) {
// Handle cases like `test(title, test.macro(...), ...)`.
// Note that this will need updating if a future version of ava adds an alternative to `exec`.
implArg = freeze({
...implFuncOrObj,
exec: wrapImplFunc(implFuncOrObj.exec),
});
} else {
// Let ava handle this bad input.
implArg = implFuncOrObj;
}
return testerFunc(title, testFuncWrapper, ...otherArgs);
// @ts-expect-error these spreads are acceptable
return testerFunc(...maybeTitle, implArg, ...args);
};
return testerWrapper;
// re-use other properties (e.g. `.always`)
// https://github.com/endojs/endo/issues/647#issuecomment-809010961
Object.assign(augmented, testerFunc);
// @ts-expect-error cast
return /** @type {import('ava').TestFn} */ augmented;
};

/**
* The ava `test` function takes a callback argument of the form
* `t => {...}`. If the outcome of this function indicates an error, either
* `t => {...}` or `async 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`.
* own console-like display of this error and its stacktrace.
* However, it does not use the SES `console` and so misses out on features
* such as unredaction.
*
* To use this package, a test file replaces the line
* ```js
Expand All @@ -100,27 +145,31 @@ const wrapTester = (testerFunc, logger = defaultLogger) => {
* 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`.
* that eventually rejects, the error is first sent to the logger
* (which defaults to using the SES-aware `console.error`)
* before propagating into `rawTest`.
*
* @param {TesterInterface} avaTest
* @template {import('ava').TestFn} T ava `test`
* @param {T} avaTest
* @param {Logger} [logger]
* @returns {TesterInterface}
* @returns {T}
*/
const wrapTest = (avaTest, logger = defaultLogger) => {
const testerWrapper = /** @type {TesterInterface} */ (
wrapTester(avaTest, logger)
);
for (const methodName of testerMethodsWhitelist) {
if (methodName in avaTest) {
/** @type {TesterFunc} */
const testerMethod = (title, implFunc, ...otherArgs) =>
avaTest[methodName](title, implFunc, ...otherArgs);
testerWrapper[methodName] = wrapTester(testerMethod);
}
const sesAvaTest = augmentLogging(avaTest, logger);
for (const methodName of overrideList) {
defineProperty(sesAvaTest, methodName, {
value: augmentLogging(avaTest[methodName], logger),
writable: true,
enumerable: true,
configurable: true,
});
}
harden(testerWrapper);
return testerWrapper;
harden(sesAvaTest);
return sesAvaTest;
};
// harden(wrapTest);
// Successful instantiation of this module must be possible before `lockdown`
// allows `harden(wrapTest)` to function, but `freeze` is a suitable replacement
// because all objects reachable from the result are intrinsics hardened by
// lockdown.
freeze(wrapTest);
export { wrapTest };
71 changes: 0 additions & 71 deletions packages/ses-ava/src/types.js

This file was deleted.

0 comments on commit 0e4f980

Please sign in to comment.