Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🦙 Add tests for calling contract from another contract #753

Merged
merged 3 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
};