Make the chai helper for rich reverts in dev-utils more robust.

Add rich reverts chai helper tests to `dev-utils`
This commit is contained in:
Lawrence Forman
2019-04-03 18:05:42 -04:00
committed by Amir Bandeali
parent ff1a3ab307
commit ed78bde359
3 changed files with 227 additions and 31 deletions

View File

@@ -49,6 +49,9 @@
"@0x/web3-wrapper": "^6.0.7",
"@types/web3-provider-engine": "^14.0.0",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^3.0.0",
"dirty-chai": "^2.0.1",
"ethereum-types": "^2.1.3",
"lodash": "^4.17.11"
},

View File

@@ -1,4 +1,4 @@
import { RichRevertReason } from '@0x/utils';
import { RichRevertReason, StandardError } from '@0x/utils';
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
import ChaiBigNumber = require('chai-bignumber');
@@ -7,44 +7,116 @@ import * as dirtyChai from 'dirty-chai';
export const chaiSetup = {
configure(): void {
chai.config.includeStack = true;
// Order matters.
chai.use(ChaiBigNumber());
chai.use(dirtyChai);
chai.use(chaiAsPromised);
chai.use(richRevertEquality);
chai.use(richRevertExtension);
chai.use(dirtyChai);
},
};
function richRevertEquality(_chai: any): void {
// tslint:disable:only-arrow-functions
const Assertion = _chai.Assertion;
Assertion.overwriteMethod('rejectedWith', function(_super: (x: any) => Promise<void>): (b: any) => Promise<void> {
return async function(this: any, b: any | RichRevertReason): Promise<void> {
const p = this._obj;
let a;
// tslint:disable: only-arrow-functions prefer-conditional-expression
type ChaiPromiseHandler = (x: any) => Promise<void>;
type ChaiAssertHandler = (x: any) => void;
interface Chai {
Assertion: any;
}
interface ChaiUtils {
flag: (assertion: any, name: string, value?: any) => any;
overwriteMethod: (ctx: any, name: string, _super: (expected: any) => any) => void;
}
interface ChaiExtension {
assert: ChaiAssert;
_obj: any;
__flags: any;
}
type ChaiAssert = (
condition: boolean,
failMessage?: string,
negatedFailMessage?: string,
expected?: any,
actual?: any,
) => void;
function richRevertExtension(_chai: Chai, utils: ChaiUtils): void {
utils.overwriteMethod(_chai.Assertion.prototype, 'rejectedWith', function(
_super: ChaiPromiseHandler,
): ChaiPromiseHandler {
return async function(this: ChaiExtension, expected: any): Promise<void> {
const maybePromise = this._obj;
// Make sure we're working with a promise.
new _chai.Assertion().assert(maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
// Wait for the promise to reject.
let err: any;
let didReject = false;
try {
if (p instanceof Promise) {
await p;
this.assert(false, 'Expected promise to reject');
} else {
a = p;
}
} catch (err) {
a = err;
await maybePromise;
} catch (_err) {
err = _err;
didReject = true;
}
if (b instanceof RichRevertReason) {
// Try to decode the result as a rich revert reason.
if (a instanceof Buffer) {
const hex = a.toString('hex');
a = `0x${hex}`;
}
if (typeof a === 'string') {
a = RichRevertReason.decode(a);
}
if (a instanceof RichRevertReason) {
this.assert(a.equals(b), `${a} != ${b}`);
}
if (!didReject) {
new _chai.Assertion().assert(false, `Expected promise to reject`);
}
_super.call(this, b);
return compareRichRevertReasons.call(this, _chai, _super, err, expected);
};
});
utils.overwriteMethod(_chai.Assertion.prototype, 'become', function(
_super: ChaiPromiseHandler,
): ChaiPromiseHandler {
return async function(this: ChaiExtension, expected: any): Promise<void> {
const maybePromise = this._obj;
// Make sure we're working with a promise.
new _chai.Assertion().assert(maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
// Wait for the promise to resolve.
return compareRichRevertReasons.call(this, _chai, _super, await maybePromise, expected);
};
});
utils.overwriteMethod(_chai.Assertion.prototype, 'equal', function(_super: ChaiAssertHandler): ChaiAssertHandler {
return function(this: ChaiExtension, expected: any): void {
compareRichRevertReasons.call(this, _chai, _super, this._obj, expected);
};
});
utils.overwriteMethod(_chai.Assertion.prototype, 'eql', function(_super: ChaiAssertHandler): ChaiAssertHandler {
return function(this: ChaiExtension, expected: any): void {
compareRichRevertReasons.call(this, _chai, _super, this._obj, expected);
};
});
}
function compareRichRevertReasons(
this: ChaiExtension,
_chai: Chai,
_super: ChaiAssertHandler,
_actual: any,
_expected: any,
): void {
let actual = _actual;
let expected = _expected;
// If either subject is a StandardError, try to coerce the other into the same.
if (expected instanceof StandardError || actual instanceof StandardError) {
// `actual` can be a RichRevertReason, string, or an Error type.
if (typeof actual === 'string') {
actual = new StandardError(actual);
} else if (actual instanceof Error) {
actual = new StandardError(actual.message);
} else if (!(actual instanceof RichRevertReason)) {
new _chai.Assertion().assert(false, `Result is not of type RichRevertReason: ${actual}`);
}
// `expected` can be a RichRevertReason or string.
if (typeof expected === 'string') {
expected = new StandardError(expected);
}
}
if (expected instanceof RichRevertReason && actual instanceof RichRevertReason) {
// Check for equality.
this.assert(actual.equals(expected), `${actual} != ${expected}`, `${actual} == ${expected}`, expected, actual);
return;
}
_super.call(this, _expected);
}

View File

@@ -0,0 +1,121 @@
import { StandardError } from '@0x/utils';
import * as chai from 'chai';
import { chaiSetup } from '../src';
chaiSetup.configure();
const expect = chai.expect;
class CustomError extends StandardError {
constructor(msg: string) {
super(msg);
}
}
describe('Chai tests', () => {
describe('RichRevertReasons', () => {
describe('#equal', () => {
it('should equate two identical RichRevertReasons', () => {
const message = 'foo';
const reason1 = new StandardError(message);
const reason2 = new StandardError(message);
expect(reason1).is.equal(reason2);
});
it('should equate two RichRevertReasons where one has missing fields', () => {
const reason1 = new StandardError('foo');
const reason2 = new StandardError();
expect(reason1).is.equal(reason2);
});
it('should not equate two RichRevertReasons with diferent fields', () => {
const reason1 = new StandardError('foo1');
const reason2 = new StandardError('foo2');
expect(reason1).is.not.equal(reason2);
});
it('should not equate two RichRevertReasons with diferent types', () => {
const message = 'foo';
const reason1 = new StandardError(message);
const reason2 = new CustomError(message);
expect(reason1).is.not.equal(reason2);
});
it('should equate a StandardError to a string equal to message', () => {
const message = 'foo';
const reason = new StandardError(message);
expect(message).is.equal(reason);
});
it('should equate an Error to a StandardError with an equal message', () => {
const message = 'foo';
const reason = new StandardError(message);
const error = new Error(message);
expect(error).is.equal(reason);
});
it('should equate a string to a StandardError with the same message', () => {
const message = 'foo';
const reason = new StandardError(message);
expect(reason).is.equal(message);
});
it('should not equate a StandardError to a string not equal to message', () => {
const reason = new StandardError('foo1');
expect('foo2').is.not.equal(reason);
});
it('should not equate a string to a StandardError with a different message', () => {
const reason = new StandardError('foo1');
expect(reason).is.not.equal('foo2');
});
it('should not equate an Error to a StandardError with a different message', () => {
const reason = new StandardError('foo1');
const error = new Error('foo2');
expect(error).is.not.equal(reason);
});
});
describe('#rejectedWith', () => {
it('should equate a promise that rejects to an identical RichRevertReasons', async () => {
const message = 'foo';
const reason1 = new StandardError(message);
const reason2 = new StandardError(message);
const promise = (async () => {
throw reason1;
})();
return expect(promise).to.be.rejectedWith(reason2);
});
it('should not equate a promise that rejects to a StandardError with a different messages', async () => {
const reason1 = new StandardError('foo1');
const reason2 = new StandardError('foo2');
const promise = (async () => {
throw reason1;
})();
return expect(promise).to.not.be.rejectedWith(reason2);
});
it('should not equate a promise that rejects to different RichRevertReason types', async () => {
const message = 'foo';
const reason1 = new StandardError(message);
const reason2 = new CustomError(message);
const promise = (async () => {
throw reason1;
})();
return expect(promise).to.not.be.rejectedWith(reason2);
});
});
describe('#become', () => {
it('should equate a promise that resolves to an identical RichRevertReasons', async () => {
const message = 'foo';
const reason1 = new StandardError(message);
const reason2 = new StandardError(message);
const promise = (async () => reason1)();
return expect(promise).to.become(reason2);
});
it('should not equate a promise that resolves to a StandardError with a different messages', async () => {
const reason1 = new StandardError('foo1');
const reason2 = new StandardError('foo2');
const promise = (async () => reason1)();
return expect(promise).to.not.become(reason2);
});
it('should not equate a promise that resolves to different RichRevertReason types', async () => {
const message = 'foo';
const reason1 = new StandardError(message);
const reason2 = new CustomError(message);
const promise = (async () => reason1)();
return expect(promise).to.not.become(reason2);
});
});
});
});