Skip to content

Commit

Permalink
🦙 Add tests for calling contract from another contract (#753)
Browse files Browse the repository at this point in the history
  • Loading branch information
yivlad authored Jul 12, 2022
1 parent b9af4f0 commit 09dccac
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-cameras-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ethereum-waffle/chai": patch
---

Improve called on contract matchers error messages
61 changes: 61 additions & 0 deletions waffle-chai/src/matchers/calledOnContract/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type {MockProvider} from '@ethereum-waffle/provider';
import {Contract} from 'ethers';
import {EncodingError} from './error';

export function assertFunctionCalled(chai: Chai.AssertionStatic, contract: Contract, fnName: string) {
const fnSighash = contract.interface.getSighash(fnName);

chai.assert(
(contract.provider as unknown as MockProvider).callHistory.some(
call => call.address === contract.address && call.data.startsWith(fnSighash)
),
`Expected contract function ${fnName} to be called`,
`Expected contract function ${fnName} NOT to be called`,
undefined
);
}

export function assertCalledWithParams(
chai: Chai.AssertionStatic,
contract: Contract,
fnName: string,
parameters: any[],
negated: boolean
) {
if (!negated) {
assertFunctionCalled(chai, contract, fnName);
}

let funCallData: string;
try {
funCallData = contract.interface.encodeFunctionData(fnName, parameters);
} catch (e) {
const error = new EncodingError('Something went wrong - probably wrong parameters format');
error.error = e as any;
throw error;
}

chai.assert(
(contract.provider as unknown as MockProvider).callHistory.some(
call => call.address === contract.address && call.data === funCallData
),
generateWrongParamsMessage(contract, fnName, parameters),
`Expected contract function ${fnName} not to be called with parameters ${parameters}, but it was`,
undefined
);
}

function generateWrongParamsMessage(contract: Contract, fnName: string, parameters: any[]) {
const fnSighash = contract.interface.getSighash(fnName);
const functionCalls = (contract.provider as unknown as MockProvider)
.callHistory.filter(
call => call.address === contract.address && call.data.startsWith(fnSighash)
);
const paramsToDisplay = functionCalls.slice(0, 3);
const leftParamsCount = functionCalls.length - paramsToDisplay.length;

return `Expected contract function ${fnName} to be called with parameters ${parameters} \
but it was called with parameters:
${paramsToDisplay.map(call => contract.interface.decodeFunctionData(fnName, call.data).toString()).join('\n')}` +
(leftParamsCount > 0 ? `\n...and ${leftParamsCount} more.` : '');
}
13 changes: 2 additions & 11 deletions waffle-chai/src/matchers/calledOnContract/calledOnContract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MockProvider} from '@ethereum-waffle/provider';
import {validateContract, validateFnName, validateMockProvider} from './calledOnContractValidators';
import {assertFunctionCalled} from './assertions';

export function supportCalledOnContract(Assertion: Chai.AssertionStatic) {
Assertion.addMethod('calledOnContract', function (contract: any) {
Expand All @@ -11,15 +11,6 @@ export function supportCalledOnContract(Assertion: Chai.AssertionStatic) {
validateFnName(fnName, contract);
}

const fnSighash = contract.interface.getSighash(fnName);

this.assert(
(contract.provider as unknown as MockProvider).callHistory.some(
call => call.address === contract.address && call.data.startsWith(fnSighash)
),
'Expected contract function to be called',
'Expected contract function NOT to be called',
undefined
);
assertFunctionCalled(this, contract, fnName);
});
}
16 changes: 4 additions & 12 deletions waffle-chai/src/matchers/calledOnContract/calledOnContractWith.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import {MockProvider} from '@ethereum-waffle/provider';
import {validateContract, validateFnName, validateMockProvider} from './calledOnContractValidators';
import {assertCalledWithParams} from './assertions';

export function supportCalledOnContractWith(Assertion: Chai.AssertionStatic) {
Assertion.addMethod('calledOnContractWith', function (contract: any, parameters: any[]) {
Assertion.addMethod('calledOnContractWith', function (this: any, contract: any, parameters: any[]) {
const fnName = this._obj;
const negated = this.__flags.negate;

validateContract(contract);
validateMockProvider(contract.provider);
validateFnName(fnName, contract);

const funCallData = contract.interface.encodeFunctionData(fnName, parameters);

this.assert(
(contract.provider as unknown as MockProvider).callHistory.some(
call => call.address === contract.address && call.data === funCallData
),
'Expected contract function with parameters to be called',
'Expected contract function with parameters NOT to be called',
undefined
);
assertCalledWithParams(this, contract, fnName, parameters, negated);
});
}
4 changes: 4 additions & 0 deletions waffle-chai/src/matchers/calledOnContract/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export class ProviderWithHistoryExpected extends Error {
super('calledOnContract matcher requires provider that support call history');
}
}

export class EncodingError extends Error {
error: Error | undefined;
}
24 changes: 21 additions & 3 deletions waffle-chai/test/contracts/Calls.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
export const CALLS_SOURCE = `
pragma solidity ^0.6.0;
pragma solidity ^0.8.0;
contract Calls {
function callWithoutParameter() pure public {}
function callWithParameter(uint param) public {}
function callWithParameters(uint param1, uint param2) public {}
function forwardCallWithoutParameter(address addr) pure public {
Calls other = Calls(addr);
other.callWithoutParameter();
}
function forwardCallWithParameter(address addr, uint param) public {
Calls other = Calls(addr);
other.callWithParameter(param);
}
function forwardCallWithParameters(address addr, uint param1, uint param2) public {
Calls other = Calls(addr);
other.callWithParameters(param1, param2);
}
}
`;

export const CALLS_ABI = [
'function callWithoutParameter() public',
'function callWithParameter(uint param) public',
'function callWithParameters(uint param1, uint param2) public'
'function callWithParameters(uint param1, uint param2) public',
'function forwardCallWithoutParameter(address addr) public',
'function forwardCallWithParameter(address addr, uint param) public',
'function forwardCallWithParameters(address addr, uint param1, uint param2) public'
];

// eslint-disable-next-line max-len
export const CALLS_BYTECODE = '608060405234801561001057600080fd5b5060e88061001f6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c8063270f7979146041578063586a7e23146049578063c3e86c6614607e575b600080fd5b604760a9565b005b607c60048036036040811015605d57600080fd5b81019080803590602001909291908035906020019092919050505060ab565b005b60a760048036036020811015609257600080fd5b810190808035906020019092919050505060af565b005b565b5050565b5056fea2646970667358221220042e49619d2f4371b311b491637d1c6a9c9ad3c55696a6a77435579e3f1baf6b64736f6c63430006000033';
export const CALLS_BYTECODE = '608060405234801561001057600080fd5b506104a9806100206000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063270f7979146100675780632934744d14610071578063586a7e231461008d578063ac5a5f08146100a9578063b920040e146100c5578063c3e86c66146100e1575b600080fd5b61006f6100fd565b005b61008b600480360381019061008691906102f3565b6100ff565b005b6100a760048036038101906100a29190610346565b610177565b005b6100c360048036038101906100be9190610386565b61017b565b005b6100df60048036038101906100da91906103b3565b6101e2565b005b6100fb60048036038101906100f691906103f3565b610257565b005b565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663586a7e2384846040518363ffffffff1660e01b815260040161013f92919061042f565b600060405180830381600087803b15801561015957600080fd5b505af115801561016d573d6000803e3d6000fd5b5050505050505050565b5050565b60008190508073ffffffffffffffffffffffffffffffffffffffff1663270f79796040518163ffffffff1660e01b815260040160006040518083038186803b1580156101c657600080fd5b505afa1580156101da573d6000803e3d6000fd5b505050505050565b60008290508073ffffffffffffffffffffffffffffffffffffffff1663c3e86c66836040518263ffffffff1660e01b81526004016102209190610458565b600060405180830381600087803b15801561023a57600080fd5b505af115801561024e573d6000803e3d6000fd5b50505050505050565b50565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028a8261025f565b9050919050565b61029a8161027f565b81146102a557600080fd5b50565b6000813590506102b781610291565b92915050565b6000819050919050565b6102d0816102bd565b81146102db57600080fd5b50565b6000813590506102ed816102c7565b92915050565b60008060006060848603121561030c5761030b61025a565b5b600061031a868287016102a8565b935050602061032b868287016102de565b925050604061033c868287016102de565b9150509250925092565b6000806040838503121561035d5761035c61025a565b5b600061036b858286016102de565b925050602061037c858286016102de565b9150509250929050565b60006020828403121561039c5761039b61025a565b5b60006103aa848285016102a8565b91505092915050565b600080604083850312156103ca576103c961025a565b5b60006103d8858286016102a8565b92505060206103e9858286016102de565b9150509250929050565b6000602082840312156104095761040861025a565b5b6000610417848285016102de565b91505092915050565b610429816102bd565b82525050565b60006040820190506104446000830185610420565b6104516020830184610420565b9392505050565b600060208201905061046d6000830184610420565b9291505056fea2646970667358221220cbb99a2aa41a58fe79554ea90b3044fae17e2139eac9d7a96525ddc7a9b17cea64736f6c634300080f0033';
25 changes: 23 additions & 2 deletions waffle-chai/test/matchers/calledOnContract/calledOnContractTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const calledOnContractTest = (provider: MockProvider) => {

expect(
() => expect('callWithoutParameter').to.be.calledOnContract(contract)
).to.throw(AssertionError, 'Expected contract function to be called');
).to.throw(AssertionError, 'Expected contract function callWithoutParameter to be called');
});

it('checks that contract function was not called', async () => {
Expand All @@ -39,7 +39,7 @@ export const calledOnContractTest = (provider: MockProvider) => {

expect(
() => expect('callWithoutParameter').not.to.be.calledOnContract(contract)
).to.throw(AssertionError, 'Expected contract function NOT to be called');
).to.throw(AssertionError, 'Expected contract function callWithoutParameter NOT to be called');
});

it(
Expand All @@ -53,4 +53,25 @@ export const calledOnContractTest = (provider: MockProvider) => {
expect('callWithoutParameter').not.to.be.calledOnContract(secondDeployContract);
}
);

it('Checks if function called from another contract was called', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.forwardCallWithoutParameter(contract.address);

expect('callWithoutParameter').to.be.calledOnContract(contract);
expect('callWithoutParameter').not.to.be.calledOnContract(secondDeployContract);
});

it('Throws if expcted function to be called from another contract but it was not', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.callWithoutParameter();

expect(
() => expect('callWithoutParameter').to.be.calledOnContract(contract)
).to.throw(AssertionError, 'Expected contract function callWithoutParameter to be called');
expect('callWithoutParameter').to.be.calledOnContract(secondDeployContract);
expect('callWithoutParameter').not.to.be.calledOnContract(contract);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const calledOnContractWithTest = (provider: MockProvider) => {

expect(
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [1])
).to.throw(AssertionError, 'Expected contract function with parameters to be called');
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called');
});

it('checks that contract function with parameter was not called', async () => {
Expand All @@ -58,7 +58,8 @@ export const calledOnContractWithTest = (provider: MockProvider) => {

expect(
() => expect('callWithParameter').not.to.be.calledOnContractWith(contract, [2])
).to.throw(AssertionError, 'Expected contract function with parameters NOT to be called');
).to.throw(AssertionError, 'Expected contract function callWithParameter not to be called ' +
'with parameters 2, but it was');
});

it(
Expand All @@ -83,4 +84,89 @@ export const calledOnContractWithTest = (provider: MockProvider) => {

expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 3]);
});

it('Checks if function called from another contract with parameter was called', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.forwardCallWithParameter(contract.address, 2);

expect('callWithParameter').to.be.calledOnContractWith(contract, [2]);
expect('callWithParameter').not.to.be.calledOnContractWith(secondDeployContract, [2]);
});

it('Throws if expected function to be called from another contract with parameter but it was not', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.callWithParameter(2);

expect(
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [2])
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called');
expect('callWithParameter').to.be.calledOnContractWith(secondDeployContract, [2]);
expect('callWithParameter').not.to.be.calledOnContractWith(contract, [2]);
});

it('Throws if function with parameter was called from another contract but the arg is wrong', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.forwardCallWithParameter(contract.address, 2);

expect(
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [3])
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called with parameters 3 but' +
' it was called with parameters:\n2');
expect('callWithParameter').not.to.be.calledOnContract(secondDeployContract);
});

it('Hides called parameters if too many', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
const calledParams: number[] = [];
for (let i = 0; i < 10; i++) {
if (i !== 3) {
await secondDeployContract.forwardCallWithParameter(contract.address, i);
calledParams.push(i);
}
}

expect(
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [3])
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called with parameters 3 but' +
' it was called with parameters:\n' + calledParams.slice(0, 3).join('\n') +
'\n...and 6 more.');
expect('callWithParameter').not.to.be.calledOnContract(secondDeployContract);
});

it('Checks if function called from another contract with parameters was called', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.forwardCallWithParameters(contract.address, 2, 3);

expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 3]);
expect('callWithParameters').not.to.be.calledOnContractWith(secondDeployContract, [2, 3]);
});

it('Throws if expected function to be called from another contract with parameters but it was not', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.callWithParameters(2, 3);

expect(
() => expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 3])
).to.throw(AssertionError, 'Expected contract function callWithParameters to be called');
expect('callWithParameters').to.be.calledOnContractWith(secondDeployContract, [2, 3]);
expect('callWithParameters').not.to.be.calledOnContractWith(contract, [2, 3]);
});

it('Throws if function with parameters was called from another contract but the arg is wrong', async () => {
const {contract} = await setup(provider);
const {contract: secondDeployContract} = await setup(provider);
await secondDeployContract.forwardCallWithParameters(contract.address, 2, 3);

expect(
() => expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 4])
).to.throw(AssertionError, 'Expected contract function callWithParameters to be called with parameters 2,4 but' +
' it was called with parameters:\n2,3');
expect('callWithParameters').not.to.be.calledOnContract(secondDeployContract);
});
};

0 comments on commit 09dccac

Please sign in to comment.