@0x/contracts-integrations Created the FunctionAssertion class and examples

This commit is contained in:
Alex Towle
2019-10-07 21:09:28 -07:00
parent 673d45361f
commit 6cba9fd77f
13 changed files with 495 additions and 3 deletions

View File

@@ -0,0 +1,45 @@
pragma solidity ^0.5.9;
contract TestCache {
uint256 public counter;
function setCounter(uint256 newCounter)
external
{
counter = newCounter;
}
function numberSideEffect()
external
view
returns (uint256)
{
return counter;
}
function equalsSideEffect(uint256 possiblyZero)
external
view
returns (bool)
{
if (counter == 0) {
return possiblyZero == 0;
} else {
return false;
}
}
function hashSideEffect(uint256 arg1, bytes32 arg2)
external
view
returns (bytes32)
{
if (counter == 0) {
return keccak256(abi.encode(arg1, arg2));
} else {
return keccak256(hex"");
}
}
}

View File

@@ -35,7 +35,7 @@
"compile:truffle": "truffle compile"
},
"config": {
"abis": "./generated-artifacts/@(TestStakingPlaceholder).json",
"abis": "./generated-artifacts/@(TestCache|TestStakingPlaceholder).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
},
"repository": {

View File

@@ -5,5 +5,9 @@
*/
import { ContractArtifact } from 'ethereum-types';
import * as TestCache from '../generated-artifacts/TestCache.json';
import * as TestStakingPlaceholder from '../generated-artifacts/TestStakingPlaceholder.json';
export const artifacts = { TestStakingPlaceholder: TestStakingPlaceholder as ContractArtifact };
export const artifacts = {
TestCache: TestCache as ContractArtifact,
TestStakingPlaceholder: TestStakingPlaceholder as ContractArtifact,
};

View File

@@ -1,2 +1,4 @@
export * from './artifacts';
export * from './wrappers';
export * from '../test/utils/function_assertions';
export * from '../test/utils/deployment_manager';

View File

@@ -3,4 +3,5 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/test_cache';
export * from '../generated-wrappers/test_staking_placeholder';

View File

@@ -0,0 +1,50 @@
import { blockchainTests, constants, expect } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { DeploymentManager } from '../../src';
blockchainTests('Exchange & Staking', env => {
let accounts: string[];
let deploymentManager: DeploymentManager;
before(async () => {
accounts = await env.getAccountAddressesAsync();
deploymentManager = await DeploymentManager.deployAsync(env);
// Create a staking pool with the operator as a maker address.
await deploymentManager.staking.stakingWrapper.createStakingPool.awaitTransactionSuccessAsync(
constants.ZERO_AMOUNT,
true,
);
// TODO(jalextowle): I will eventually want these utilities to be in the deployment manager.
// Create default order parameters
// const defaultOrderParams = {
// ...constants.STATIC_ORDER_PARAMS,
// makerAddress: accounts[1],
// makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
// takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
// makerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultFeeTokenAddress),
// takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultFeeTokenAddress),
// feeRecipientAddress: feeRecipientAddressLeft,
// exchangeAddress: exchange.address,
// chainId,
// };
});
// Function assertions for all of the functions involved in
// (1) Creating a staking pool
// - At first, this can be isolated.
// (2) Joining a staking pool as a maker
// - This can be isolated too, and we can assume a limited number
// of market makers to make things easy.
// (3) Paying a protocol fee
// - I'm personally of the opinion that we should write integration
// tests for the exchange and the staking contracts to interoperate.
// (4) Going to the next epoch
// - This might be something that just get's called every certain number of itterations
// for simple tests.
// (5) Finalizing a pool in the epoch
// - Ditto
});

View File

@@ -0,0 +1,71 @@
import { blockchainTests, constants, expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { artifacts, Condition, FunctionAssertion, GetterCache, TestCacheContract } from '../../src';
// These tests provide examples for how to use the "FunctionAssertion" class to write
// tests for "payable" and "nonpayable" Solidity functions as well as "pure" and "view" functions.
blockchainTests('TestCache', env => {
let testCache: TestCacheContract;
before(async () => {
testCache = await TestCacheContract.deployFrom0xArtifactAsync(
artifacts.TestCache,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('numberSideEffect', () => {
let assertion: FunctionAssertion;
before(async () => {
const condition = {
before: async () => {},
after: async (result: any, receipt: TransactionReceiptWithDecodedLogs | undefined) => {
const counter = await testCache.counter.callAsync();
expect(result).bignumber.to.be.eq(counter);
},
};
assertion = new FunctionAssertion(testCache.numberSideEffect, condition);
});
it('should return the correct counter', async () => {
await assertion.runAsync();
});
it('should return the correct counter', async () => {
await testCache.setCounter.awaitTransactionSuccessAsync(new BigNumber(2));
await assertion.runAsync();
});
});
describe('setCounter', () => {
let assertion: FunctionAssertion;
before(async () => {
const condition = {
before: async (expectedCounter: BigNumber) => {},
after: async (
result: any,
receipt: TransactionReceiptWithDecodedLogs | undefined,
expectedCounter: BigNumber,
) => {
const counter = await testCache.counter.callAsync();
expect(counter).bignumber.to.be.eq(expectedCounter);
},
};
assertion = new FunctionAssertion(testCache.setCounter, condition);
});
it('should correctly set counter to 1', async () => {
await assertion.runAsync(new BigNumber(1));
});
it('should correctly set counter to 1500', async () => {
await assertion.runAsync(new BigNumber(1500));
});
});
});

View File

@@ -0,0 +1,157 @@
import { blockchainTests, constants, expect, hexSlice } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { BlockParam, CallData } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
import { artifacts, GetterCache, TestCacheContract } from '../src';
blockchainTests.resets('Cache Tests', env => {
let testCacheContract: TestCacheContract;
before(async () => {
testCacheContract = await TestCacheContract.deployFrom0xArtifactAsync(
artifacts.TestCache,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('callAsync', () => {
describe('() => uint', () => {
let cache: GetterCache;
beforeEach(async () => {
cache = new GetterCache(testCacheContract.numberSideEffect);
});
it('should return 0 when "counter" == 0', async () => {
expect(await cache.callAsync()).bignumber.to.be.eq(constants.ZERO_AMOUNT);
});
it('should return 1 when "counter" == 1', async () => {
// Update the counter to 1.
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
// Ensure that the returned value is the updated counter.
expect(await cache.callAsync()).bignumber.to.be.eq(new BigNumber(1));
});
it('should return the cached counter', async () => {
// Cache a value.
expect(await cache.callAsync()).bignumber.to.be.eq(constants.ZERO_AMOUNT);
// Update the counter to 1.
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
// Ensure that the returned value is the cached counter.
expect(await cache.callAsync()).bignumber.to.be.eq(constants.ZERO_AMOUNT);
});
});
describe('uint => boolean', () => {
let cache: GetterCache;
beforeEach(async () => {
cache = new GetterCache(testCacheContract.equalsSideEffect);
});
it('should return true when "possiblyZero" == 0 && "counter" == 0', async () => {
expect(await cache.callAsync(constants.ZERO_AMOUNT)).to.be.true();
});
it('should return false when "possiblyZero" == 0 && "counter" != 0', async () => {
// Update "counter" to "1", which will cause all calls to return false.
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
expect(await cache.callAsync(constants.ZERO_AMOUNT)).to.be.false();
});
it('should return the cached value', async () => {
// Cache a "true" value when "possiblyZero" == 0 && "counter" == 0
expect(await cache.callAsync(constants.ZERO_AMOUNT)).to.be.true();
// Update "counter" to "1", which will cause all calls of "isZeroOrFalse" to return "false".
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
// This should return "true" because a value was cached.
expect(await cache.callAsync(constants.ZERO_AMOUNT)).to.be.true();
});
});
describe('(uint, bytes32) => bytes32', () => {
let cache: GetterCache;
beforeEach(async () => {
cache = new GetterCache(testCacheContract.hashSideEffect);
});
it('should return correct hash when counter == 0', async () => {
// Get the calldata for the function call, which includes the abi-encoded data to hash.
const hashData = testCacheContract.hashSideEffect.getABIEncodedTransactionData(
new BigNumber(1),
ethUtil.bufferToHex(ethUtil.sha3(0)),
);
// Ensure that the correct hash was returned from the cache.
expect(await cache.callAsync(new BigNumber(1), ethUtil.bufferToHex(ethUtil.sha3(0)))).to.be.eq(
ethUtil.bufferToHex(ethUtil.sha3(hexSlice(hashData, 4))),
);
});
it('should return the null hash when counter != 0', async () => {
// Update "counter" to "1", which will cause all calls to return the null hash.
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
// Ensure that the cache returns the correct value.
expect(await cache.callAsync(new BigNumber(1), ethUtil.bufferToHex(ethUtil.sha3(0)))).to.be.eq(
ethUtil.bufferToHex(ethUtil.sha3('0x')),
);
});
it('should return the cached hash', async () => {
// Get the calldata for the function call, which includes the abi-encoded data to hash.
const hashData = testCacheContract.hashSideEffect.getABIEncodedTransactionData(
new BigNumber(1),
ethUtil.bufferToHex(ethUtil.sha3(0)),
);
// Ensure that the cache returns the correct value.
expect(await cache.callAsync(new BigNumber(1), ethUtil.bufferToHex(ethUtil.sha3(0)))).to.be.eq(
ethUtil.bufferToHex(ethUtil.sha3(hexSlice(hashData, 4))),
);
// Update "counter" to "1", which will cause all calls to return the null hash.
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
// Ensure that the cache returns the correct value.
expect(await cache.callAsync(new BigNumber(1), ethUtil.bufferToHex(ethUtil.sha3(0)))).to.be.eq(
ethUtil.bufferToHex(ethUtil.sha3(hexSlice(hashData, 4))),
);
});
});
});
describe('flush', () => {
describe('uint => boolean', () => {
let cache: GetterCache;
beforeEach(async () => {
cache = new GetterCache(testCacheContract.equalsSideEffect);
});
it('should return false when the cache was flushed && "possiblyZero" == 0 && "counter" != 0', async () => {
// Cache a "true" value when "possiblyZero" == 0 && "counter" == 0
expect(await cache.callAsync(constants.ZERO_AMOUNT)).to.be.true();
// Update "counter" to "1", which will cause all calls of "isZeroOrFalse" to return "false".
await testCacheContract.setCounter.awaitTransactionSuccessAsync(new BigNumber(1));
// Flush the entire cache.
cache.flush();
// This should return "false" because the value was flushed.
expect(await cache.callAsync(constants.ZERO_AMOUNT)).to.be.false();
});
});
});
});

View File

@@ -34,6 +34,11 @@ import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import * as _ from 'lodash';
<<<<<<< HEAD:contracts/integrations/test/deployment/deployment_mananger.ts
=======
import { artifacts, TestStakingPlaceholderContract } from '../../src';
>>>>>>> `@0x/contracts-integrations` Created the FunctionAssertion class and examples:contracts/integrations/test/utils/deployment_mananger.ts
/**
* Adds a batch of authorities to a list of authorizable contracts.
* @param owner The owner of the authorizable contracts.
@@ -106,9 +111,15 @@ interface StakingContracts {
// Contract wrappers for tokens.
interface TokenContracts {
<<<<<<< HEAD:contracts/integrations/test/deployment/deployment_mananger.ts
erc20: DummyERC20TokenContract[];
erc721: DummyERC721TokenContract[];
erc1155: ERC1155MintableContract[];
=======
erc1155: ERC1155Contract;
erc20: ERC20TokenContract[];
erc721: ERC721TokenContract;
>>>>>>> `@0x/contracts-integrations` Created the FunctionAssertion class and examples:contracts/integrations/test/utils/deployment_mananger.ts
weth: WETH9Contract;
zrx: ZRXTokenContract;
}
@@ -407,6 +418,7 @@ export class DeploymentManager {
txDefaults: Partial<TxData>,
options: Partial<DeploymentOptions>,
): Promise<TokenContracts> {
<<<<<<< HEAD:contracts/integrations/test/deployment/deployment_mananger.ts
const numErc20TokensToDeploy =
options.numErc20TokensToDeploy !== undefined
? options.numErc20TokensToDeploy
@@ -435,6 +447,26 @@ export class DeploymentManager {
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
),
),
=======
const erc20 = [];
erc20[0] = await ERC20TokenContract.deployFrom0xArtifactAsync(
ERC20Artifacts.ERC20Token,
environment.provider,
txDefaults,
ERC20Artifacts,
);
erc20[1] = await ERC20TokenContract.deployFrom0xArtifactAsync(
ERC20Artifacts.ERC20Token,
environment.provider,
txDefaults,
ERC20Artifacts,
);
erc20[2] = await ERC20TokenContract.deployFrom0xArtifactAsync(
ERC20Artifacts.ERC20Token,
environment.provider,
txDefaults,
ERC20Artifacts,
>>>>>>> `@0x/contracts-integrations` Created the FunctionAssertion class and examples:contracts/integrations/test/utils/deployment_mananger.ts
);
const erc721 = await Promise.all(
_.times(

View File

@@ -0,0 +1,130 @@
import { PromiseWithTransactionHash } from '@0x/base-contract';
import { BlockParam, CallData, TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { DeploymentManager } from './';
export interface ContractGetterFunction {
callAsync: (...args: any[]) => Promise<any>;
getABIEncodedTransactionData: (...args: any[]) => string;
}
export interface ContractWrapperFunction extends ContractGetterFunction {
awaitTransactionSuccessAsync?: (...args: any[]) => PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs>;
}
export interface Cache {
getter: ContractGetterFunction;
callAsync: (...args: any[]) => Promise<any>;
flush: () => void;
}
export interface Condition {
before: (...args: any[]) => Promise<void>;
after: (result: any, receipt: TransactionReceiptWithDecodedLogs | undefined, ...args: any[]) => Promise<void>;
}
export class FunctionAssertion {
// A before and an after assertion that will be called around the wrapper function.
public condition: Condition;
// The wrapper function that will be wrapped in assertions.
public wrapperFunction: ContractWrapperFunction;
constructor(wrapperFunction: ContractWrapperFunction, condition: Condition) {
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 runAsync(...args: any[]): Promise<void> {
await this.condition.before(...args);
const result = await this.wrapperFunction.callAsync(...args);
const receipt =
this.wrapperFunction.awaitTransactionSuccessAsync !== undefined
? await this.wrapperFunction.awaitTransactionSuccessAsync(...args)
: undefined;
await this.condition.after(result, receipt, ...args);
}
}
export class GetterCache {
// The getter function whose values will be cached.
public getter: ContractGetterFunction;
// The cache that will be used to store values. This has to use a "string" for indexing
// because the "Map" datastructure uses reference equality when the keys are objects,
// which was unsuitable for our purposes.
private cache: {
[key: string]: any;
};
constructor(getter: ContractGetterFunction) {
this.getter = getter;
this.cache = {};
}
/**
* Calls the contract getter and caches the result if there is not a cached value. Otherwise,
* this returns the cached value.
* @param ...args A variadic list of args to use when calling "callAsync". Due
* to the fact that we need to serialize the arguments to "callAsync" these
* arguments must be valid arguments to "getABIEncodedTransactionData".
* @return Either a cached value or the queried value.
*/
public async callAsync(...args: any[]): Promise<any> {
const cachedResult = this.cache[this.getter.getABIEncodedTransactionData(...args)];
if (cachedResult !== undefined) {
return cachedResult;
} else {
const result = await this.getter.callAsync(...args);
this.cache[this.getter.getABIEncodedTransactionData(...args)] = result;
return result;
}
}
/**
* Flushes the entire cache so that future calls to "callAsync" call the contract getter.
*/
public flush(): void {
this.cache = {};
}
}
export class GetterCacheCollection {
// A dictionary of getter cache's that allow the user of the collection to reference
// the getter functions by name.
public getters: {
[getter: string]: GetterCache;
};
/**
* Constructs a getter collection with pre-seeded getter names and getters.
* @param getterNames The names of the getter functions to register.
* @param getters The getter functions to register.
*/
constructor(getterNames: string[], getters: ContractGetterFunction[]) {
if (getterNames.length !== getters.length) {
throw new Error('Mismatched getter names and getters');
}
// Register all of the getters.
this.getters = {};
for (const getter of _.zip(getterNames, getters)) {
this.registerGetter(getter[0] as string, getter[1] as ContractGetterFunction);
}
}
/**
* Registers a new getter in the collection.
* @param getterName The name of the contract getter function.
* @param getter The actual contract getter function.
*/
public registerGetter(getterName: string, getter: ContractGetterFunction): void {
this.getters[getterName] = new GetterCache(getter);
}
}

View File

@@ -2,5 +2,5 @@
"extends": "../../tsconfig",
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": ["generated-artifacts/TestStakingPlaceholder.json"]
"files": ["generated-artifacts/TestCache.json", "generated-artifacts/TestStakingPlaceholder.json"]
}