Skip to content

Commit 09dccac

Browse files
authoredJul 12, 2022
🦙 Add tests for calling contract from another contract (#753)
1 parent b9af4f0 commit 09dccac

File tree

8 files changed

+208
-30
lines changed

8 files changed

+208
-30
lines changed
 

Diff for: ‎.changeset/two-cameras-bathe.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ethereum-waffle/chai": patch
3+
---
4+
5+
Improve called on contract matchers error messages
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {MockProvider} from '@ethereum-waffle/provider';
2+
import {Contract} from 'ethers';
3+
import {EncodingError} from './error';
4+
5+
export function assertFunctionCalled(chai: Chai.AssertionStatic, contract: Contract, fnName: string) {
6+
const fnSighash = contract.interface.getSighash(fnName);
7+
8+
chai.assert(
9+
(contract.provider as unknown as MockProvider).callHistory.some(
10+
call => call.address === contract.address && call.data.startsWith(fnSighash)
11+
),
12+
`Expected contract function ${fnName} to be called`,
13+
`Expected contract function ${fnName} NOT to be called`,
14+
undefined
15+
);
16+
}
17+
18+
export function assertCalledWithParams(
19+
chai: Chai.AssertionStatic,
20+
contract: Contract,
21+
fnName: string,
22+
parameters: any[],
23+
negated: boolean
24+
) {
25+
if (!negated) {
26+
assertFunctionCalled(chai, contract, fnName);
27+
}
28+
29+
let funCallData: string;
30+
try {
31+
funCallData = contract.interface.encodeFunctionData(fnName, parameters);
32+
} catch (e) {
33+
const error = new EncodingError('Something went wrong - probably wrong parameters format');
34+
error.error = e as any;
35+
throw error;
36+
}
37+
38+
chai.assert(
39+
(contract.provider as unknown as MockProvider).callHistory.some(
40+
call => call.address === contract.address && call.data === funCallData
41+
),
42+
generateWrongParamsMessage(contract, fnName, parameters),
43+
`Expected contract function ${fnName} not to be called with parameters ${parameters}, but it was`,
44+
undefined
45+
);
46+
}
47+
48+
function generateWrongParamsMessage(contract: Contract, fnName: string, parameters: any[]) {
49+
const fnSighash = contract.interface.getSighash(fnName);
50+
const functionCalls = (contract.provider as unknown as MockProvider)
51+
.callHistory.filter(
52+
call => call.address === contract.address && call.data.startsWith(fnSighash)
53+
);
54+
const paramsToDisplay = functionCalls.slice(0, 3);
55+
const leftParamsCount = functionCalls.length - paramsToDisplay.length;
56+
57+
return `Expected contract function ${fnName} to be called with parameters ${parameters} \
58+
but it was called with parameters:
59+
${paramsToDisplay.map(call => contract.interface.decodeFunctionData(fnName, call.data).toString()).join('\n')}` +
60+
(leftParamsCount > 0 ? `\n...and ${leftParamsCount} more.` : '');
61+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {MockProvider} from '@ethereum-waffle/provider';
21
import {validateContract, validateFnName, validateMockProvider} from './calledOnContractValidators';
2+
import {assertFunctionCalled} from './assertions';
33

44
export function supportCalledOnContract(Assertion: Chai.AssertionStatic) {
55
Assertion.addMethod('calledOnContract', function (contract: any) {
@@ -11,15 +11,6 @@ export function supportCalledOnContract(Assertion: Chai.AssertionStatic) {
1111
validateFnName(fnName, contract);
1212
}
1313

14-
const fnSighash = contract.interface.getSighash(fnName);
15-
16-
this.assert(
17-
(contract.provider as unknown as MockProvider).callHistory.some(
18-
call => call.address === contract.address && call.data.startsWith(fnSighash)
19-
),
20-
'Expected contract function to be called',
21-
'Expected contract function NOT to be called',
22-
undefined
23-
);
14+
assertFunctionCalled(this, contract, fnName);
2415
});
2516
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1-
import {MockProvider} from '@ethereum-waffle/provider';
21
import {validateContract, validateFnName, validateMockProvider} from './calledOnContractValidators';
2+
import {assertCalledWithParams} from './assertions';
33

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

89
validateContract(contract);
910
validateMockProvider(contract.provider);
1011
validateFnName(fnName, contract);
1112

12-
const funCallData = contract.interface.encodeFunctionData(fnName, parameters);
13-
14-
this.assert(
15-
(contract.provider as unknown as MockProvider).callHistory.some(
16-
call => call.address === contract.address && call.data === funCallData
17-
),
18-
'Expected contract function with parameters to be called',
19-
'Expected contract function with parameters NOT to be called',
20-
undefined
21-
);
13+
assertCalledWithParams(this, contract, fnName, parameters, negated);
2214
});
2315
}

Diff for: ‎waffle-chai/src/matchers/calledOnContract/error.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ export class ProviderWithHistoryExpected extends Error {
33
super('calledOnContract matcher requires provider that support call history');
44
}
55
}
6+
7+
export class EncodingError extends Error {
8+
error: Error | undefined;
9+
}

Diff for: ‎waffle-chai/test/contracts/Calls.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
export const CALLS_SOURCE = `
2-
pragma solidity ^0.6.0;
2+
pragma solidity ^0.8.0;
33
44
contract Calls {
55
function callWithoutParameter() pure public {}
66
77
function callWithParameter(uint param) public {}
88
99
function callWithParameters(uint param1, uint param2) public {}
10+
11+
function forwardCallWithoutParameter(address addr) pure public {
12+
Calls other = Calls(addr);
13+
other.callWithoutParameter();
14+
}
15+
16+
function forwardCallWithParameter(address addr, uint param) public {
17+
Calls other = Calls(addr);
18+
other.callWithParameter(param);
19+
}
20+
21+
function forwardCallWithParameters(address addr, uint param1, uint param2) public {
22+
Calls other = Calls(addr);
23+
other.callWithParameters(param1, param2);
24+
}
1025
}
1126
`;
1227

1328
export const CALLS_ABI = [
1429
'function callWithoutParameter() public',
1530
'function callWithParameter(uint param) public',
16-
'function callWithParameters(uint param1, uint param2) public'
31+
'function callWithParameters(uint param1, uint param2) public',
32+
'function forwardCallWithoutParameter(address addr) public',
33+
'function forwardCallWithParameter(address addr, uint param) public',
34+
'function forwardCallWithParameters(address addr, uint param1, uint param2) public'
1735
];
1836

1937
// eslint-disable-next-line max-len
20-
export const CALLS_BYTECODE = '608060405234801561001057600080fd5b5060e88061001f6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c8063270f7979146041578063586a7e23146049578063c3e86c6614607e575b600080fd5b604760a9565b005b607c60048036036040811015605d57600080fd5b81019080803590602001909291908035906020019092919050505060ab565b005b60a760048036036020811015609257600080fd5b810190808035906020019092919050505060af565b005b565b5050565b5056fea2646970667358221220042e49619d2f4371b311b491637d1c6a9c9ad3c55696a6a77435579e3f1baf6b64736f6c63430006000033';
38+
export const CALLS_BYTECODE = '608060405234801561001057600080fd5b506104a9806100206000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063270f7979146100675780632934744d14610071578063586a7e231461008d578063ac5a5f08146100a9578063b920040e146100c5578063c3e86c66146100e1575b600080fd5b61006f6100fd565b005b61008b600480360381019061008691906102f3565b6100ff565b005b6100a760048036038101906100a29190610346565b610177565b005b6100c360048036038101906100be9190610386565b61017b565b005b6100df60048036038101906100da91906103b3565b6101e2565b005b6100fb60048036038101906100f691906103f3565b610257565b005b565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663586a7e2384846040518363ffffffff1660e01b815260040161013f92919061042f565b600060405180830381600087803b15801561015957600080fd5b505af115801561016d573d6000803e3d6000fd5b5050505050505050565b5050565b60008190508073ffffffffffffffffffffffffffffffffffffffff1663270f79796040518163ffffffff1660e01b815260040160006040518083038186803b1580156101c657600080fd5b505afa1580156101da573d6000803e3d6000fd5b505050505050565b60008290508073ffffffffffffffffffffffffffffffffffffffff1663c3e86c66836040518263ffffffff1660e01b81526004016102209190610458565b600060405180830381600087803b15801561023a57600080fd5b505af115801561024e573d6000803e3d6000fd5b50505050505050565b50565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028a8261025f565b9050919050565b61029a8161027f565b81146102a557600080fd5b50565b6000813590506102b781610291565b92915050565b6000819050919050565b6102d0816102bd565b81146102db57600080fd5b50565b6000813590506102ed816102c7565b92915050565b60008060006060848603121561030c5761030b61025a565b5b600061031a868287016102a8565b935050602061032b868287016102de565b925050604061033c868287016102de565b9150509250925092565b6000806040838503121561035d5761035c61025a565b5b600061036b858286016102de565b925050602061037c858286016102de565b9150509250929050565b60006020828403121561039c5761039b61025a565b5b60006103aa848285016102a8565b91505092915050565b600080604083850312156103ca576103c961025a565b5b60006103d8858286016102a8565b92505060206103e9858286016102de565b9150509250929050565b6000602082840312156104095761040861025a565b5b6000610417848285016102de565b91505092915050565b610429816102bd565b82525050565b60006040820190506104446000830185610420565b6104516020830184610420565b9392505050565b600060208201905061046d6000830184610420565b9291505056fea2646970667358221220cbb99a2aa41a58fe79554ea90b3044fae17e2139eac9d7a96525ddc7a9b17cea64736f6c634300080f0033';

Diff for: ‎waffle-chai/test/matchers/calledOnContract/calledOnContractTest.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const calledOnContractTest = (provider: MockProvider) => {
2424

2525
expect(
2626
() => expect('callWithoutParameter').to.be.calledOnContract(contract)
27-
).to.throw(AssertionError, 'Expected contract function to be called');
27+
).to.throw(AssertionError, 'Expected contract function callWithoutParameter to be called');
2828
});
2929

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

4040
expect(
4141
() => expect('callWithoutParameter').not.to.be.calledOnContract(contract)
42-
).to.throw(AssertionError, 'Expected contract function NOT to be called');
42+
).to.throw(AssertionError, 'Expected contract function callWithoutParameter NOT to be called');
4343
});
4444

4545
it(
@@ -53,4 +53,25 @@ export const calledOnContractTest = (provider: MockProvider) => {
5353
expect('callWithoutParameter').not.to.be.calledOnContract(secondDeployContract);
5454
}
5555
);
56+
57+
it('Checks if function called from another contract was called', async () => {
58+
const {contract} = await setup(provider);
59+
const {contract: secondDeployContract} = await setup(provider);
60+
await secondDeployContract.forwardCallWithoutParameter(contract.address);
61+
62+
expect('callWithoutParameter').to.be.calledOnContract(contract);
63+
expect('callWithoutParameter').not.to.be.calledOnContract(secondDeployContract);
64+
});
65+
66+
it('Throws if expcted function to be called from another contract but it was not', async () => {
67+
const {contract} = await setup(provider);
68+
const {contract: secondDeployContract} = await setup(provider);
69+
await secondDeployContract.callWithoutParameter();
70+
71+
expect(
72+
() => expect('callWithoutParameter').to.be.calledOnContract(contract)
73+
).to.throw(AssertionError, 'Expected contract function callWithoutParameter to be called');
74+
expect('callWithoutParameter').to.be.calledOnContract(secondDeployContract);
75+
expect('callWithoutParameter').not.to.be.calledOnContract(contract);
76+
});
5677
};

Diff for: ‎waffle-chai/test/matchers/calledOnContract/calledOnContractWithTest.ts

+88-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const calledOnContractWithTest = (provider: MockProvider) => {
3333

3434
expect(
3535
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [1])
36-
).to.throw(AssertionError, 'Expected contract function with parameters to be called');
36+
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called');
3737
});
3838

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

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

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

8485
expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 3]);
8586
});
87+
88+
it('Checks if function called from another contract with parameter was called', async () => {
89+
const {contract} = await setup(provider);
90+
const {contract: secondDeployContract} = await setup(provider);
91+
await secondDeployContract.forwardCallWithParameter(contract.address, 2);
92+
93+
expect('callWithParameter').to.be.calledOnContractWith(contract, [2]);
94+
expect('callWithParameter').not.to.be.calledOnContractWith(secondDeployContract, [2]);
95+
});
96+
97+
it('Throws if expected function to be called from another contract with parameter but it was not', async () => {
98+
const {contract} = await setup(provider);
99+
const {contract: secondDeployContract} = await setup(provider);
100+
await secondDeployContract.callWithParameter(2);
101+
102+
expect(
103+
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [2])
104+
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called');
105+
expect('callWithParameter').to.be.calledOnContractWith(secondDeployContract, [2]);
106+
expect('callWithParameter').not.to.be.calledOnContractWith(contract, [2]);
107+
});
108+
109+
it('Throws if function with parameter was called from another contract but the arg is wrong', async () => {
110+
const {contract} = await setup(provider);
111+
const {contract: secondDeployContract} = await setup(provider);
112+
await secondDeployContract.forwardCallWithParameter(contract.address, 2);
113+
114+
expect(
115+
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [3])
116+
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called with parameters 3 but' +
117+
' it was called with parameters:\n2');
118+
expect('callWithParameter').not.to.be.calledOnContract(secondDeployContract);
119+
});
120+
121+
it('Hides called parameters if too many', async () => {
122+
const {contract} = await setup(provider);
123+
const {contract: secondDeployContract} = await setup(provider);
124+
const calledParams: number[] = [];
125+
for (let i = 0; i < 10; i++) {
126+
if (i !== 3) {
127+
await secondDeployContract.forwardCallWithParameter(contract.address, i);
128+
calledParams.push(i);
129+
}
130+
}
131+
132+
expect(
133+
() => expect('callWithParameter').to.be.calledOnContractWith(contract, [3])
134+
).to.throw(AssertionError, 'Expected contract function callWithParameter to be called with parameters 3 but' +
135+
' it was called with parameters:\n' + calledParams.slice(0, 3).join('\n') +
136+
'\n...and 6 more.');
137+
expect('callWithParameter').not.to.be.calledOnContract(secondDeployContract);
138+
});
139+
140+
it('Checks if function called from another contract with parameters was called', async () => {
141+
const {contract} = await setup(provider);
142+
const {contract: secondDeployContract} = await setup(provider);
143+
await secondDeployContract.forwardCallWithParameters(contract.address, 2, 3);
144+
145+
expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 3]);
146+
expect('callWithParameters').not.to.be.calledOnContractWith(secondDeployContract, [2, 3]);
147+
});
148+
149+
it('Throws if expected function to be called from another contract with parameters but it was not', async () => {
150+
const {contract} = await setup(provider);
151+
const {contract: secondDeployContract} = await setup(provider);
152+
await secondDeployContract.callWithParameters(2, 3);
153+
154+
expect(
155+
() => expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 3])
156+
).to.throw(AssertionError, 'Expected contract function callWithParameters to be called');
157+
expect('callWithParameters').to.be.calledOnContractWith(secondDeployContract, [2, 3]);
158+
expect('callWithParameters').not.to.be.calledOnContractWith(contract, [2, 3]);
159+
});
160+
161+
it('Throws if function with parameters was called from another contract but the arg is wrong', async () => {
162+
const {contract} = await setup(provider);
163+
const {contract: secondDeployContract} = await setup(provider);
164+
await secondDeployContract.forwardCallWithParameters(contract.address, 2, 3);
165+
166+
expect(
167+
() => expect('callWithParameters').to.be.calledOnContractWith(contract, [2, 4])
168+
).to.throw(AssertionError, 'Expected contract function callWithParameters to be called with parameters 2,4 but' +
169+
' it was called with parameters:\n2,3');
170+
expect('callWithParameters').not.to.be.calledOnContract(secondDeployContract);
171+
});
86172
};

0 commit comments

Comments
 (0)
Please sign in to comment.