Use seeded rng for simulations
This commit is contained in:
parent
6b0f3570b9
commit
d11cdcd5d2
@ -69,6 +69,7 @@
|
|||||||
"@types/lodash": "4.14.104",
|
"@types/lodash": "4.14.104",
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/mocha": "^5.2.7",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
|
"@types/seedrandom": "^2.4.28",
|
||||||
"chai": "^4.0.1",
|
"chai": "^4.0.1",
|
||||||
"chai-as-promised": "^7.1.0",
|
"chai-as-promised": "^7.1.0",
|
||||||
"chai-bignumber": "^3.0.0",
|
"chai-bignumber": "^3.0.0",
|
||||||
@ -78,6 +79,7 @@
|
|||||||
"mocha": "^6.2.0",
|
"mocha": "^6.2.0",
|
||||||
"nock": "^10.0.6",
|
"nock": "^10.0.6",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
|
"seedrandom": "^3.0.5",
|
||||||
"shx": "^0.2.2",
|
"shx": "^0.2.2",
|
||||||
"solhint": "^1.4.1",
|
"solhint": "^1.4.1",
|
||||||
"truffle": "^5.0.32",
|
"truffle": "^5.0.32",
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { constants, OrderFactory } from '@0x/contracts-test-utils';
|
import { constants, OrderFactory } from '@0x/contracts-test-utils';
|
||||||
import { Order, SignedOrder } from '@0x/types';
|
import { Order, SignedOrder } from '@0x/types';
|
||||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { AssertionResult } from '../assertions/function_assertion';
|
import { AssertionResult } from '../assertions/function_assertion';
|
||||||
import { validJoinStakingPoolAssertion } from '../assertions/joinStakingPool';
|
import { validJoinStakingPoolAssertion } from '../assertions/joinStakingPool';
|
||||||
|
import { Pseudorandom } from '../pseudorandom';
|
||||||
|
|
||||||
import { Actor, ActorConfig, Constructor } from './base';
|
import { Actor, ActorConfig, Constructor } from './base';
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
|||||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||||
const assertion = validJoinStakingPoolAssertion(this.actor.deployment);
|
const assertion = validJoinStakingPoolAssertion(this.actor.deployment);
|
||||||
while (true) {
|
while (true) {
|
||||||
const poolId = _.sample(Object.keys(stakingPools));
|
const poolId = Pseudorandom.sample(Object.keys(stakingPools));
|
||||||
if (poolId === undefined) {
|
if (poolId === undefined) {
|
||||||
yield undefined;
|
yield undefined;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { constants, StakingPoolById } from '@0x/contracts-staking';
|
import { constants, StakingPoolById } from '@0x/contracts-staking';
|
||||||
import { getRandomInteger } from '@0x/contracts-test-utils';
|
|
||||||
import '@azure/core-asynciterator-polyfill';
|
import '@azure/core-asynciterator-polyfill';
|
||||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
@ -7,6 +6,7 @@ import * as _ from 'lodash';
|
|||||||
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
|
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
|
||||||
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
|
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
|
||||||
import { AssertionResult } from '../assertions/function_assertion';
|
import { AssertionResult } from '../assertions/function_assertion';
|
||||||
|
import { Pseudorandom } from '../pseudorandom';
|
||||||
|
|
||||||
import { Actor, Constructor } from './base';
|
import { Actor, Constructor } from './base';
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
|
|||||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||||
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
|
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
|
||||||
while (true) {
|
while (true) {
|
||||||
const operatorShare = getRandomInteger(0, constants.PPM).toNumber();
|
const operatorShare = Pseudorandom.integer(constants.PPM).toNumber();
|
||||||
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
|
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,11 +92,11 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
|
|||||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||||
const assertion = validDecreaseStakingPoolOperatorShareAssertion(this.actor.deployment, stakingPools);
|
const assertion = validDecreaseStakingPoolOperatorShareAssertion(this.actor.deployment, stakingPools);
|
||||||
while (true) {
|
while (true) {
|
||||||
const poolId = _.sample(this._getOperatorPoolIds(stakingPools));
|
const poolId = Pseudorandom.sample(this._getOperatorPoolIds(stakingPools));
|
||||||
if (poolId === undefined) {
|
if (poolId === undefined) {
|
||||||
yield undefined;
|
yield undefined;
|
||||||
} else {
|
} else {
|
||||||
const operatorShare = getRandomInteger(0, stakingPools[poolId].operatorShare).toNumber();
|
const operatorShare = Pseudorandom.integer(stakingPools[poolId].operatorShare).toNumber();
|
||||||
yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
|
yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
||||||
import { getRandomInteger } from '@0x/contracts-test-utils';
|
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import '@azure/core-asynciterator-polyfill';
|
import '@azure/core-asynciterator-polyfill';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
@ -8,6 +7,7 @@ import { AssertionResult } from '../assertions/function_assertion';
|
|||||||
import { validMoveStakeAssertion } from '../assertions/moveStake';
|
import { validMoveStakeAssertion } from '../assertions/moveStake';
|
||||||
import { validStakeAssertion } from '../assertions/stake';
|
import { validStakeAssertion } from '../assertions/stake';
|
||||||
import { validUnstakeAssertion } from '../assertions/unstake';
|
import { validUnstakeAssertion } from '../assertions/unstake';
|
||||||
|
import { Pseudorandom } from '../pseudorandom';
|
||||||
|
|
||||||
import { Actor, Constructor } from './base';
|
import { Actor, Constructor } from './base';
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
|||||||
while (true) {
|
while (true) {
|
||||||
await balanceStore.updateErc20BalancesAsync();
|
await balanceStore.updateErc20BalancesAsync();
|
||||||
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
|
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
|
||||||
const amount = getRandomInteger(0, zrxBalance);
|
const amount = Pseudorandom.integer(zrxBalance);
|
||||||
yield assertion.executeAsync([amount], { from: this.actor.address });
|
yield assertion.executeAsync([amount], { from: this.actor.address });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
|||||||
undelegatedStake.currentEpochBalance,
|
undelegatedStake.currentEpochBalance,
|
||||||
undelegatedStake.nextEpochBalance,
|
undelegatedStake.nextEpochBalance,
|
||||||
);
|
);
|
||||||
const amount = getRandomInteger(0, withdrawableStake);
|
const amount = Pseudorandom.integer(withdrawableStake);
|
||||||
yield assertion.executeAsync([amount], { from: this.actor.address });
|
yield assertion.executeAsync([amount], { from: this.actor.address });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,25 +104,27 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
|||||||
const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools);
|
const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const fromPoolId = _.sample(Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])));
|
const fromPoolId = Pseudorandom.sample(
|
||||||
|
Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])),
|
||||||
|
);
|
||||||
const fromStatus =
|
const fromStatus =
|
||||||
fromPoolId === undefined
|
fromPoolId === undefined
|
||||||
? StakeStatus.Undelegated
|
? StakeStatus.Undelegated
|
||||||
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
||||||
const from = new StakeInfo(fromStatus, fromPoolId);
|
const from = new StakeInfo(fromStatus, fromPoolId);
|
||||||
|
|
||||||
const toPoolId = _.sample(Object.keys(stakingPools));
|
const toPoolId = Pseudorandom.sample(Object.keys(stakingPools));
|
||||||
const toStatus =
|
const toStatus =
|
||||||
toPoolId === undefined
|
toPoolId === undefined
|
||||||
? StakeStatus.Undelegated
|
? StakeStatus.Undelegated
|
||||||
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
||||||
const to = new StakeInfo(toStatus, toPoolId);
|
const to = new StakeInfo(toStatus, toPoolId);
|
||||||
|
|
||||||
const moveableStake =
|
const moveableStake =
|
||||||
from.status === StakeStatus.Undelegated
|
from.status === StakeStatus.Undelegated
|
||||||
? this.stake[StakeStatus.Undelegated].nextEpochBalance
|
? this.stake[StakeStatus.Undelegated].nextEpochBalance
|
||||||
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
|
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
|
||||||
const amount = getRandomInteger(0, moveableStake);
|
const amount = Pseudorandom.integer(moveableStake);
|
||||||
|
|
||||||
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
|
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { constants, getRandomInteger } from '@0x/contracts-test-utils';
|
import { constants } from '@0x/contracts-test-utils';
|
||||||
import { SignedOrder } from '@0x/types';
|
import { SignedOrder } from '@0x/types';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
|
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { validFillOrderCompleteFillAssertion } from '../assertions/fillOrder';
|
import { validFillOrderCompleteFillAssertion } from '../assertions/fillOrder';
|
||||||
import { AssertionResult } from '../assertions/function_assertion';
|
import { AssertionResult } from '../assertions/function_assertion';
|
||||||
import { DeploymentManager } from '../deployment_manager';
|
import { DeploymentManager } from '../deployment_manager';
|
||||||
|
import { Pseudorandom } from '../pseudorandom';
|
||||||
|
|
||||||
import { Actor, Constructor } from './base';
|
import { Actor, Constructor } from './base';
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
|||||||
const { marketMakers } = this.actor.simulationEnvironment!;
|
const { marketMakers } = this.actor.simulationEnvironment!;
|
||||||
const assertion = validFillOrderCompleteFillAssertion(this.actor.deployment);
|
const assertion = validFillOrderCompleteFillAssertion(this.actor.deployment);
|
||||||
while (true) {
|
while (true) {
|
||||||
const maker = _.sample(marketMakers);
|
const maker = Pseudorandom.sample(marketMakers);
|
||||||
if (maker === undefined) {
|
if (maker === undefined) {
|
||||||
yield undefined;
|
yield undefined;
|
||||||
} else {
|
} else {
|
||||||
@ -82,8 +82,8 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const order = await maker.signOrderAsync({
|
const order = await maker.signOrderAsync({
|
||||||
makerAssetAmount: getRandomInteger(constants.ZERO_AMOUNT, constants.INITIAL_ERC20_BALANCE),
|
makerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
|
||||||
takerAssetAmount: getRandomInteger(constants.ZERO_AMOUNT, constants.INITIAL_ERC20_BALANCE),
|
takerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
|
||||||
});
|
});
|
||||||
yield assertion.executeAsync([order, order.takerAssetAmount, order.signature], {
|
yield assertion.executeAsync([order, order.takerAssetAmount, order.signature], {
|
||||||
from: this.actor.address,
|
from: this.actor.address,
|
||||||
|
39
contracts/integrations/test/framework/pseudorandom.ts
Normal file
39
contracts/integrations/test/framework/pseudorandom.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Numberish } from '@0x/contracts-test-utils';
|
||||||
|
import { BigNumber } from '@0x/utils';
|
||||||
|
import * as seedrandom from 'seedrandom';
|
||||||
|
|
||||||
|
class PRNGWrapper {
|
||||||
|
public readonly seed = process.env.UUID || Math.random().toString();
|
||||||
|
private readonly _rng = seedrandom(this.seed);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pseudorandom version of _.sample. Picks an element of the given array with uniform probability.
|
||||||
|
* Return undefined if the array is empty.
|
||||||
|
*/
|
||||||
|
public sample<T>(arr: T[]): T | undefined {
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const index = Math.abs(this._rng.int32()) % arr.length;
|
||||||
|
return arr[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable:unified-signatures
|
||||||
|
/*
|
||||||
|
* Pseudorandom version of getRandomPortion/getRandomInteger. If two arguments are provided,
|
||||||
|
* treats those arguments as the min and max (inclusive) of the desired range. If only one
|
||||||
|
* argument is provided, picks an integer between 0 and the argument.
|
||||||
|
*/
|
||||||
|
public integer(max: Numberish): BigNumber;
|
||||||
|
public integer(min: Numberish, max: Numberish): BigNumber;
|
||||||
|
public integer(a: Numberish, b?: Numberish): BigNumber {
|
||||||
|
if (b === undefined) {
|
||||||
|
return new BigNumber(this._rng()).times(a).integerValue(BigNumber.ROUND_HALF_UP);
|
||||||
|
} else {
|
||||||
|
const range = new BigNumber(b).minus(a);
|
||||||
|
return this.integer(range).plus(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pseudorandom = new PRNGWrapper();
|
@ -1,11 +1,11 @@
|
|||||||
import { blockchainTests } from '@0x/contracts-test-utils';
|
import { blockchainTests } from '@0x/contracts-test-utils';
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { Actor } from '../framework/actors/base';
|
import { Actor } from '../framework/actors/base';
|
||||||
import { PoolOperator } from '../framework/actors/pool_operator';
|
import { PoolOperator } from '../framework/actors/pool_operator';
|
||||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||||
import { DeploymentManager } from '../framework/deployment_manager';
|
import { DeploymentManager } from '../framework/deployment_manager';
|
||||||
|
import { Pseudorandom } from '../framework/pseudorandom';
|
||||||
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
||||||
|
|
||||||
export class PoolManagementSimulation extends Simulation {
|
export class PoolManagementSimulation extends Simulation {
|
||||||
@ -22,7 +22,7 @@ export class PoolManagementSimulation extends Simulation {
|
|||||||
operator.simulationActions.validDecreaseStakingPoolOperatorShare,
|
operator.simulationActions.validDecreaseStakingPoolOperatorShare,
|
||||||
];
|
];
|
||||||
while (true) {
|
while (true) {
|
||||||
const action = _.sample(actions);
|
const action = Pseudorandom.sample(actions);
|
||||||
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { blockchainTests, constants } from '@0x/contracts-test-utils';
|
import { blockchainTests, constants } from '@0x/contracts-test-utils';
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { MakerTaker } from '../framework/actors/hybrids';
|
import { MakerTaker } from '../framework/actors/hybrids';
|
||||||
import { Maker } from '../framework/actors/maker';
|
import { Maker } from '../framework/actors/maker';
|
||||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||||
import { DeploymentManager } from '../framework/deployment_manager';
|
import { DeploymentManager } from '../framework/deployment_manager';
|
||||||
|
import { Pseudorandom } from '../framework/pseudorandom';
|
||||||
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
||||||
|
|
||||||
import { PoolManagementSimulation } from './pool_management_test';
|
import { PoolManagementSimulation } from './pool_management_test';
|
||||||
@ -29,7 +29,7 @@ class PoolMembershipSimulation extends Simulation {
|
|||||||
];
|
];
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const action = _.sample(actions);
|
const action = Pseudorandom.sample(actions);
|
||||||
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { blockchainTests } from '@0x/contracts-test-utils';
|
import { blockchainTests } from '@0x/contracts-test-utils';
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { Actor } from '../framework/actors/base';
|
import { Actor } from '../framework/actors/base';
|
||||||
import { Staker } from '../framework/actors/staker';
|
import { Staker } from '../framework/actors/staker';
|
||||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||||
import { DeploymentManager } from '../framework/deployment_manager';
|
import { DeploymentManager } from '../framework/deployment_manager';
|
||||||
|
import { Pseudorandom } from '../framework/pseudorandom';
|
||||||
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
||||||
|
|
||||||
import { PoolManagementSimulation } from './pool_management_test';
|
import { PoolManagementSimulation } from './pool_management_test';
|
||||||
@ -26,7 +26,7 @@ export class StakeManagementSimulation extends Simulation {
|
|||||||
poolManagement.generator,
|
poolManagement.generator,
|
||||||
];
|
];
|
||||||
while (true) {
|
while (true) {
|
||||||
const action = _.sample(actions);
|
const action = Pseudorandom.sample(actions);
|
||||||
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export {
|
|||||||
export { blockchainTests, BlockchainTestsEnvironment, describe } from './mocha_blockchain';
|
export { blockchainTests, BlockchainTestsEnvironment, describe } from './mocha_blockchain';
|
||||||
export { chaiSetup, expect } from './chai_setup';
|
export { chaiSetup, expect } from './chai_setup';
|
||||||
export { getCodesizeFromArtifact } from './codesize';
|
export { getCodesizeFromArtifact } from './codesize';
|
||||||
export { shortZip } from './lang_utils';
|
export { replaceKeysDeep, shortZip } from './lang_utils';
|
||||||
export {
|
export {
|
||||||
assertIntegerRoughlyEquals,
|
assertIntegerRoughlyEquals,
|
||||||
assertRoughlyEquals,
|
assertRoughlyEquals,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user