@0x:contracts-integrations Addressed more review comments

This commit is contained in:
Alex Towle 2019-10-30 11:11:55 -07:00
parent 205c895d75
commit 0b3e3ab990
2 changed files with 82 additions and 72 deletions

View File

@ -26,7 +26,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
); );
}); });
describe('runAsync', () => { describe('executeAsync', () => {
it('should call the before function with the provided arguments', async () => { it('should call the before function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT; let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion(exampleContract.returnInteger, { const assertion = new FunctionAssertion(exampleContract.returnInteger, {
@ -36,7 +36,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
after: async (beforeInfo: any, result: Result, input: BigNumber) => {}, after: async (beforeInfo: any, result: Result, input: BigNumber) => {},
}); });
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.runAsync(randomInput); await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput); expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
}); });
@ -49,7 +49,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
}, },
}); });
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.runAsync(randomInput); await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput); expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
}); });
@ -58,7 +58,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
before: async () => {}, before: async () => {},
after: async (beforeInfo: any, result: Result) => {}, after: async (beforeInfo: any, result: Result) => {},
}); });
await assertion.runAsync(); await assertion.executeAsync();
}); });
it('should pass the return value of "before" to "after"', async () => { it('should pass the return value of "before" to "after"', async () => {
@ -72,7 +72,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
sideEffectTarget = beforeInfo; sideEffectTarget = beforeInfo;
}, },
}); });
await assertion.runAsync(randomInput); await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput); expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
}); });
@ -85,15 +85,17 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
}, },
}); });
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.runAsync(randomInput); await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput); expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
}); });
it('should pass the receipt from the function call to "after"', async () => { it('should pass the receipt from the function call to "after"', async () => {
let sideEffectTarget = {} as TransactionReceiptWithDecodedLogs; let sideEffectTarget = {} as TransactionReceiptWithDecodedLogs;
const assertion = new FunctionAssertion(exampleContract.emitEvent, { const assertion = new FunctionAssertion(exampleContract.emitEvent, {
before: async (input: string) => {}, before: async (input: string) => {
after: async (beforeInfo: any, result: Result, input: string) => { return {};
},
after: async (beforeInfo: {}, result: Result, input: string) => {
if (result.receipt) { if (result.receipt) {
sideEffectTarget = result.receipt; sideEffectTarget = result.receipt;
} }
@ -101,7 +103,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
}); });
const input = 'emitted data'; const input = 'emitted data';
await assertion.runAsync(input); await assertion.executeAsync(input);
// Ensure that the correct events were emitted. // Ensure that the correct events were emitted.
const [event] = filterLogsToArguments<TestFrameworkEventEventArgs>( const [event] = filterLogsToArguments<TestFrameworkEventEventArgs>(
@ -114,13 +116,15 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
it('should pass the error to "after" if the function call fails', async () => { it('should pass the error to "after" if the function call fails', async () => {
let sideEffectTarget: Error; let sideEffectTarget: Error;
const assertion = new FunctionAssertion(exampleContract.stringRevert, { const assertion = new FunctionAssertion(exampleContract.stringRevert, {
before: async string => {}, before: async string => {
return {};
},
after: async (any, result: Result, string) => { after: async (any, result: Result, string) => {
sideEffectTarget = result.data; sideEffectTarget = result.data;
}, },
}); });
const message = 'error message'; const message = 'error message';
await assertion.runAsync(message); await assertion.executeAsync(message);
const expectedError = new StringRevertError(message); const expectedError = new StringRevertError(message);
return expect( return expect(

View File

@ -29,15 +29,19 @@ export interface Result {
* function. * function.
*/ */
export interface Condition<TBefore extends any> { export interface Condition<TBefore extends any> {
before?: (...args: any[]) => Promise<TBefore>; before: (...args: any[]) => Promise<TBefore>;
after?: (beforeInfo: TBefore, result: Result, ...args: any[]) => Promise<any>; after: (beforeInfo: TBefore, result: Result, ...args: any[]) => Promise<any>;
} }
/** /**
* * The basic unit of abstraction for testing. This just consists of a command that
* can be run. For example, this can represent a simple command that can be run, or
* it can represent a command that executes a "Hoare Triple" (this is what most of
* our `Assertion` implementations will do in practice).
* @param runAsync The function to execute for the assertion.
*/ */
export interface Assertion { export interface Assertion {
runAsync: (...args: any[]) => Promise<any>; executeAsync: (...args: any[]) => Promise<any>;
} }
export interface RunResult { export interface RunResult {
@ -45,16 +49,20 @@ export interface RunResult {
afterInfo: any; afterInfo: any;
} }
/**
* This class implements `Assertion` and represents a "Hoare Triple" that can be
* executed.
*/
export class FunctionAssertion<TBefore extends any> implements Assertion { export class FunctionAssertion<TBefore extends any> implements Assertion {
// A condition that will be applied to `wrapperFunction`. // A condition that will be applied to `wrapperFunction`.
// Note: `TBefore | undefined` is used because the `before` and `after` functions // Note: `TBefore | undefined` is used because the `before` and `after` functions
// are optional in `Condition`. // are optional in `Condition`.
public condition: Condition<TBefore | undefined>; public condition: Condition<TBefore>;
// The wrapper function that will be wrapped in assertions. // The wrapper function that will be wrapped in assertions.
public wrapperFunction: ContractWrapperFunction; public wrapperFunction: ContractWrapperFunction;
constructor(wrapperFunction: ContractWrapperFunction, condition: Condition<TBefore | undefined>) { constructor(wrapperFunction: ContractWrapperFunction, condition: Condition<TBefore>) {
this.condition = condition; this.condition = condition;
this.wrapperFunction = wrapperFunction; this.wrapperFunction = wrapperFunction;
} }
@ -63,9 +71,9 @@ export class FunctionAssertion<TBefore extends any> implements Assertion {
* Runs the wrapped function and fails if the before or after assertions fail. * Runs the wrapped function and fails if the before or after assertions fail.
* @param ...args The args to the contract wrapper function. * @param ...args The args to the contract wrapper function.
*/ */
public async runAsync(...args: any[]): Promise<RunResult> { public async executeAsync(...args: any[]): Promise<RunResult> {
// Call the before condition. // Call the before condition.
const beforeInfo = this.condition.before !== undefined ? await this.condition.before(...args) : undefined; const beforeInfo = await this.condition.before(...args);
// Initialize the callResult so that the default success value is true. // Initialize the callResult so that the default success value is true.
let callResult: Result = { success: true }; let callResult: Result = { success: true };
@ -85,10 +93,7 @@ export class FunctionAssertion<TBefore extends any> implements Assertion {
} }
// Call the after condition. // Call the after condition.
const afterInfo = const afterInfo = await this.condition.after(beforeInfo, callResult, ...args);
this.condition.after !== undefined
? await this.condition.after(beforeInfo, callResult, ...args)
: undefined;
return { return {
beforeInfo, beforeInfo,
@ -97,6 +102,8 @@ export class FunctionAssertion<TBefore extends any> implements Assertion {
} }
} }
export type IndexGenerator = () => number;
export type InputGenerator = () => Promise<any[]>; export type InputGenerator = () => Promise<any[]>;
export interface AssertionGenerator { export interface AssertionGenerator {
@ -105,28 +112,41 @@ export interface AssertionGenerator {
} }
/** /**
* A class that can run a set of function assertions in a sequence. This will terminate * This class is an abstract way to represent collections of function assertions.
* after every assertion in the sequence has been executed. * Using this, we can use closures to build up many useful collections with different
* properties. Notably, this abstraction supports function assertion collections
* that can be run continuously and also those that terminate in a finite number
* of steps.
*/ */
export class FunctionAssertionSequence { class MetaAssertion implements Assertion {
/** constructor(
* @constructor Initializes a readonly list of AssertionGenerator objects. protected readonly assertionGenerators: AssertionGenerator[],
* @param assertionGenerators A list of objects that contain (1) assertions protected readonly indexGenerator: IndexGenerator,
* and (2) functions that generate the arguments to "run" the assertions. ) {}
*/
constructor(protected readonly assertionGenerators: AssertionGenerator[]) {} public async executeAsync(): Promise<void> {
let idx: number;
while ((idx = this.indexGenerator()) > 0) {
const args = await this.assertionGenerators[idx].generator();
this.assertionGenerators[idx].assertion.executeAsync(...args);
}
}
}
/** /**
* Execute this class's function assertions in the order that they were initialized. * Returns a class that can execute a set of function assertions in sequence.
* The assertions corresponding input generators will provide the arguments when the * @param assertionGenerators A set of assertion generators to run in sequence.
* assertion is executed.
*/ */
public async runAsync(): Promise<void> { export function FunctionAssertionSequence(assertionGenerators: AssertionGenerator[]): MetaAssertion {
for (let i = 0; i < this.assertionGenerators.length; i++) { let idx = 0;
const args = await this.assertionGenerators[i].generator(); return new MetaAssertion(assertionGenerators, () => {
await this.assertionGenerators[i].assertion.runAsync(...args); if (idx < assertionGenerators.length) {
} return idx++;
} else {
idx = 0;
return -1;
} }
});
} }
export interface WeightedAssertionGenerator extends AssertionGenerator { export interface WeightedAssertionGenerator extends AssertionGenerator {
@ -134,39 +154,25 @@ export interface WeightedAssertionGenerator extends AssertionGenerator {
} }
/** /**
* A class that can execute a set of function assertions at random continuously. * Returns a class that can execute a set of function assertions at random continuously.
* This will not terminate unless the process that called `runAsync` terminates. * This will not terminate unless the process that called `runAsync` terminates.
* @param weightedAssertionGenerators A set of function assertions that have been
* assigned weights.
*/ */
export class ContinuousFunctionAssertionSet { export function ContinuousFunctionAssertionSet(
protected readonly assertionGenerators: AssertionGenerator[] = []; weightedAssertionGenerators: WeightedAssertionGenerator[],
): MetaAssertion {
/** // Construct an array of assertion generators that allows random sampling from a
* @constructor Initializes assertion generators so that assertion's can be // uniform distribution to correctly bias assertion selection.
* selected using a uniform distribution and the weights of the let assertionGenerators: AssertionGenerator[] = [];
* assertions hold.
* @param weightedAssertionGenerators An array of assertion generators that
* have specified "weights." These "weights" specify the relative frequency
* that assertions should be executed when the set is run.
*/
constructor(weightedAssertionGenerators: WeightedAssertionGenerator[]) {
for (const { assertion, generator, weight } of weightedAssertionGenerators) { for (const { assertion, generator, weight } of weightedAssertionGenerators) {
const weightedAssertions: AssertionGenerator[] = []; const weightedAssertions: AssertionGenerator[] = [];
_.fill(weightedAssertions, { assertion, generator }, 0, weight || 1); _.fill(weightedAssertions, { assertion, generator }, 0, weight || 1);
this.assertionGenerators = this.assertionGenerators.concat(weightedAssertions); assertionGenerators = assertionGenerators.concat(weightedAssertions);
}
} }
/** // The index generator simply needs to sample from a uniform distribution.
* Execute this class's function assertions continuously and randomly using the weights const indexGenerator = () => Math.round(Math.random() * (assertionGenerators.length - 1));
* of the assertions to bias the assertion selection. The assertions corresponding
* input generators will provide the arguments when the return new MetaAssertion(assertionGenerators, indexGenerator);
* assertion is executed.
*/
public async runAsync(): Promise<void> {
for (;;) {
const randomIdx = Math.round(Math.random() * (this.assertionGenerators.length - 1));
const args = await this.assertionGenerators[randomIdx].generator();
await this.assertionGenerators[randomIdx].assertion.runAsync(...args);
}
}
} }