protocol/contracts/integrations/test/utils/function_assertions.ts

177 lines
6.5 KiB
TypeScript

import { PromiseWithTransactionHash } from '@0x/base-contract';
import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils';
import { decodeThrownErrorAsRevertError } from '@0x/utils';
import { BlockParam, CallData, TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
export interface ContractGetterFunction {
callAsync: (...args: any[]) => Promise<any>;
}
export interface ContractWrapperFunction extends ContractGetterFunction {
awaitTransactionSuccessAsync?: (...args: any[]) => PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs>;
}
export interface Result {
data?: any;
success: boolean;
receipt?: TransactionReceiptWithDecodedLogs;
}
/**
* This interface represents a condition that can be placed on a contract function.
* This can be used to represent the pre- and post-conditions of a "Hoare Triple" on a
* given contract function. The "Hoare Triple" is a way to represent the way that a
* function changes state.
* @param before A function that will be run before a call to the contract wrapper
* function. Ideally, this will be a "precondition."
* @param after A function that will be run after a call to the contract wrapper
* function.
*/
export interface Condition<TBefore> {
before: (...args: any[]) => Promise<TBefore>;
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 {
executeAsync: (...args: any[]) => Promise<any>;
}
export interface RunResult {
beforeInfo: any;
afterInfo: any;
}
/**
* This class implements `Assertion` and represents a "Hoare Triple" that can be
* executed.
*/
export class FunctionAssertion<TBefore> implements Assertion {
// A condition that will be applied to `wrapperFunction`.
public condition: Condition<TBefore>;
// The wrapper function that will be wrapped in assertions.
public wrapperFunction: ContractWrapperFunction;
constructor(wrapperFunction: ContractWrapperFunction, condition: Condition<TBefore>) {
this.condition = condition;
this.wrapperFunction = wrapperFunction;
}
/**
* Runs the wrapped function and fails if the before or after assertions fail.
* @param ...args The args to the contract wrapper function.
*/
public async executeAsync(...args: any[]): Promise<RunResult> {
// Call the before condition.
const beforeInfo = await this.condition.before(...args);
// Initialize the callResult so that the default success value is true.
let callResult: Result = { success: true };
// Try to make the call to the function. If it is successful, pass the
// result and receipt to the after condition.
try {
callResult.data = await this.wrapperFunction.callAsync(...args);
callResult.receipt =
this.wrapperFunction.awaitTransactionSuccessAsync !== undefined
? await this.wrapperFunction.awaitTransactionSuccessAsync(...args)
: undefined;
} catch (error) {
callResult.data = error;
callResult.success = false;
callResult.receipt = undefined;
}
// Call the after condition.
const afterInfo = await this.condition.after(beforeInfo, callResult, ...args);
return {
beforeInfo,
afterInfo,
};
}
}
export type IndexGenerator = () => number;
export type InputGenerator = () => Promise<any[]>;
export interface AssertionGenerator {
assertion: Assertion;
generator: InputGenerator;
}
/**
* 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.
*/
class MetaAssertion implements Assertion {
constructor(
protected readonly assertionGenerators: AssertionGenerator[],
protected readonly indexGenerator: IndexGenerator,
) {}
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);
}
}
}
/**
* 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;
}
/**
* 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 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);
}
// 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);
}