From 0b3e3ab990aeb45dfd1a7ccdaec689c42be66904 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 30 Oct 2019 11:11:55 -0700 Subject: [PATCH] `@0x:contracts-integrations` Addressed more review comments --- .../function_assertion_test.ts | 26 ++-- .../test/utils/function_assertions.ts | 128 +++++++++--------- 2 files changed, 82 insertions(+), 72 deletions(-) diff --git a/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts b/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts index 945e2082a7..b354ef87a6 100644 --- a/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts +++ b/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts @@ -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 () => { let sideEffectTarget = ZERO_AMOUNT; const assertion = new FunctionAssertion(exampleContract.returnInteger, { @@ -36,7 +36,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => { after: async (beforeInfo: any, result: Result, input: BigNumber) => {}, }); const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); - await assertion.runAsync(randomInput); + await assertion.executeAsync(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); - await assertion.runAsync(randomInput); + await assertion.executeAsync(randomInput); expect(sideEffectTarget).bignumber.to.be.eq(randomInput); }); @@ -58,7 +58,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => { before: async () => {}, after: async (beforeInfo: any, result: Result) => {}, }); - await assertion.runAsync(); + await assertion.executeAsync(); }); it('should pass the return value of "before" to "after"', async () => { @@ -72,7 +72,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => { sideEffectTarget = beforeInfo; }, }); - await assertion.runAsync(randomInput); + await assertion.executeAsync(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); - await assertion.runAsync(randomInput); + await assertion.executeAsync(randomInput); expect(sideEffectTarget).bignumber.to.be.eq(randomInput); }); it('should pass the receipt from the function call to "after"', async () => { let sideEffectTarget = {} as TransactionReceiptWithDecodedLogs; const assertion = new FunctionAssertion(exampleContract.emitEvent, { - before: async (input: string) => {}, - after: async (beforeInfo: any, result: Result, input: string) => { + before: async (input: string) => { + return {}; + }, + after: async (beforeInfo: {}, result: Result, input: string) => { if (result.receipt) { sideEffectTarget = result.receipt; } @@ -101,7 +103,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => { }); const input = 'emitted data'; - await assertion.runAsync(input); + await assertion.executeAsync(input); // Ensure that the correct events were emitted. const [event] = filterLogsToArguments( @@ -114,13 +116,15 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => { it('should pass the error to "after" if the function call fails', async () => { let sideEffectTarget: Error; const assertion = new FunctionAssertion(exampleContract.stringRevert, { - before: async string => {}, + before: async string => { + return {}; + }, after: async (any, result: Result, string) => { sideEffectTarget = result.data; }, }); const message = 'error message'; - await assertion.runAsync(message); + await assertion.executeAsync(message); const expectedError = new StringRevertError(message); return expect( diff --git a/contracts/integrations/test/utils/function_assertions.ts b/contracts/integrations/test/utils/function_assertions.ts index 653c8b37c3..2926bacb1b 100644 --- a/contracts/integrations/test/utils/function_assertions.ts +++ b/contracts/integrations/test/utils/function_assertions.ts @@ -29,15 +29,19 @@ export interface Result { * function. */ export interface Condition { - before?: (...args: any[]) => Promise; - after?: (beforeInfo: TBefore, result: Result, ...args: any[]) => Promise; + before: (...args: any[]) => Promise; + after: (beforeInfo: TBefore, result: Result, ...args: any[]) => Promise; } /** - * + * 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 { - runAsync: (...args: any[]) => Promise; + executeAsync: (...args: any[]) => Promise; } export interface RunResult { @@ -45,16 +49,20 @@ export interface RunResult { afterInfo: any; } +/** + * This class implements `Assertion` and represents a "Hoare Triple" that can be + * executed. + */ export class FunctionAssertion implements Assertion { // A condition that will be applied to `wrapperFunction`. // Note: `TBefore | undefined` is used because the `before` and `after` functions // are optional in `Condition`. - public condition: Condition; + public condition: Condition; // The wrapper function that will be wrapped in assertions. public wrapperFunction: ContractWrapperFunction; - constructor(wrapperFunction: ContractWrapperFunction, condition: Condition) { + constructor(wrapperFunction: ContractWrapperFunction, condition: Condition) { this.condition = condition; this.wrapperFunction = wrapperFunction; } @@ -63,9 +71,9 @@ export class FunctionAssertion implements Assertion { * Runs the wrapped function and fails if the before or after assertions fail. * @param ...args The args to the contract wrapper function. */ - public async runAsync(...args: any[]): Promise { + public async executeAsync(...args: any[]): Promise { // 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. let callResult: Result = { success: true }; @@ -85,10 +93,7 @@ export class FunctionAssertion implements Assertion { } // Call the after condition. - const afterInfo = - this.condition.after !== undefined - ? await this.condition.after(beforeInfo, callResult, ...args) - : undefined; + const afterInfo = await this.condition.after(beforeInfo, callResult, ...args); return { beforeInfo, @@ -97,6 +102,8 @@ export class FunctionAssertion implements Assertion { } } +export type IndexGenerator = () => number; + export type InputGenerator = () => Promise; export interface AssertionGenerator { @@ -105,68 +112,67 @@ export interface AssertionGenerator { } /** - * A class that can run a set of function assertions in a sequence. This will terminate - * after every assertion in the sequence has been executed. + * This class is an abstract way to represent collections of function assertions. + * 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 { - /** - * @constructor Initializes a readonly list of AssertionGenerator objects. - * @param assertionGenerators A list of objects that contain (1) assertions - * and (2) functions that generate the arguments to "run" the assertions. - */ - constructor(protected readonly assertionGenerators: AssertionGenerator[]) {} +class MetaAssertion implements Assertion { + constructor( + protected readonly assertionGenerators: AssertionGenerator[], + protected readonly indexGenerator: IndexGenerator, + ) {} - /** - * Execute this class's function assertions in the order that they were initialized. - * The assertions corresponding input generators will provide the arguments when the - * assertion is executed. - */ - public async runAsync(): Promise { - for (let i = 0; i < this.assertionGenerators.length; i++) { - const args = await this.assertionGenerators[i].generator(); - await this.assertionGenerators[i].assertion.runAsync(...args); + public async executeAsync(): Promise { + let idx: number; + while ((idx = this.indexGenerator()) > 0) { + const args = await this.assertionGenerators[idx].generator(); + this.assertionGenerators[idx].assertion.executeAsync(...args); } } } +/** + * Returns a class that can execute a set of function assertions in sequence. + * @param assertionGenerators A set of assertion generators to run in sequence. + */ +export function FunctionAssertionSequence(assertionGenerators: AssertionGenerator[]): MetaAssertion { + let idx = 0; + return new MetaAssertion(assertionGenerators, () => { + if (idx < assertionGenerators.length) { + return idx++; + } else { + idx = 0; + return -1; + } + }); +} + export interface WeightedAssertionGenerator extends AssertionGenerator { weight?: number; } /** - * 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. + * @param weightedAssertionGenerators A set of function assertions that have been + * assigned weights. */ -export class ContinuousFunctionAssertionSet { - protected readonly assertionGenerators: AssertionGenerator[] = []; - - /** - * @constructor Initializes assertion generators so that assertion's can be - * selected using a uniform distribution and the weights of the - * 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) { - const weightedAssertions: AssertionGenerator[] = []; - _.fill(weightedAssertions, { assertion, generator }, 0, weight || 1); - this.assertionGenerators = this.assertionGenerators.concat(weightedAssertions); - } +export function ContinuousFunctionAssertionSet( + weightedAssertionGenerators: WeightedAssertionGenerator[], +): MetaAssertion { + // Construct an array of assertion generators that allows random sampling from a + // uniform distribution to correctly bias assertion selection. + let assertionGenerators: AssertionGenerator[] = []; + for (const { assertion, generator, weight } of weightedAssertionGenerators) { + const weightedAssertions: AssertionGenerator[] = []; + _.fill(weightedAssertions, { assertion, generator }, 0, weight || 1); + assertionGenerators = assertionGenerators.concat(weightedAssertions); } - /** - * Execute this class's function assertions continuously and randomly using the weights - * of the assertions to bias the assertion selection. The assertions corresponding - * input generators will provide the arguments when the - * assertion is executed. - */ - public async runAsync(): Promise { - 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); - } - } + // The index generator simply needs to sample from a uniform distribution. + const indexGenerator = () => Math.round(Math.random() * (assertionGenerators.length - 1)); + + return new MetaAssertion(assertionGenerators, indexGenerator); }