-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Implement ava wrapper, and monkey test it.
- Loading branch information
Showing
13 changed files
with
457 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"trailingComma": "all", | ||
"singleQuote": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import './src/types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { wrapTest } from './ses-ava-test'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` | ||
*/ |
Oops, something went wrong.