From 09dccac1433405011aa1d2dc0b880449982244c3 Mon Sep 17 00:00:00 2001 From: Vladyslav Yatsenko <52505649+yivlad@users.noreply.github.com> Date: Tue, 12 Jul 2022 16:06:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=99=20Add=20tests=20for=20calling=20co?= =?UTF-8?q?ntract=20from=20another=20contract=20(#753)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/two-cameras-bathe.md | 5 ++ .../matchers/calledOnContract/assertions.ts | 61 +++++++++++++ .../calledOnContract/calledOnContract.ts | 13 +-- .../calledOnContract/calledOnContractWith.ts | 16 +--- .../src/matchers/calledOnContract/error.ts | 4 + waffle-chai/test/contracts/Calls.ts | 24 ++++- .../calledOnContract/calledOnContractTest.ts | 25 +++++- .../calledOnContractWithTest.ts | 90 ++++++++++++++++++- 8 files changed, 208 insertions(+), 30 deletions(-) create mode 100644 .changeset/two-cameras-bathe.md create mode 100644 waffle-chai/src/matchers/calledOnContract/assertions.ts diff --git a/.changeset/two-cameras-bathe.md b/.changeset/two-cameras-bathe.md new file mode 100644 index 000000000..a0f825ef8 --- /dev/null +++ b/.changeset/two-cameras-bathe.md @@ -0,0 +1,5 @@ +--- +"@ethereum-waffle/chai": patch +--- + +Improve called on contract matchers error messages diff --git a/waffle-chai/src/matchers/calledOnContract/assertions.ts b/waffle-chai/src/matchers/calledOnContract/assertions.ts new file mode 100644 index 000000000..9daeb55ea --- /dev/null +++ b/waffle-chai/src/matchers/calledOnContract/assertions.ts @@ -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.` : ''); +} diff --git a/waffle-chai/src/matchers/calledOnContract/calledOnContract.ts b/waffle-chai/src/matchers/calledOnContract/calledOnContract.ts index 8e22a6571..eb1b65af1 100644 --- a/waffle-chai/src/matchers/calledOnContract/calledOnContract.ts +++ b/waffle-chai/src/matchers/calledOnContract/calledOnContract.ts @@ -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) { @@ -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); }); } diff --git a/waffle-chai/src/matchers/calledOnContract/calledOnContractWith.ts b/waffle-chai/src/matchers/calledOnContract/calledOnContractWith.ts index f2807b0e7..ac5c16351 100644 --- a/waffle-chai/src/matchers/calledOnContract/calledOnContractWith.ts +++ b/waffle-chai/src/matchers/calledOnContract/calledOnContractWith.ts @@ -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); }); } diff --git a/waffle-chai/src/matchers/calledOnContract/error.ts b/waffle-chai/src/matchers/calledOnContract/error.ts index 89b4f69cc..4372a9cfb 100644 --- a/waffle-chai/src/matchers/calledOnContract/error.ts +++ b/waffle-chai/src/matchers/calledOnContract/error.ts @@ -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; +} diff --git a/waffle-chai/test/contracts/Calls.ts b/waffle-chai/test/contracts/Calls.ts index 39adfdf71..1acbe2074 100644 --- a/waffle-chai/test/contracts/Calls.ts +++ b/waffle-chai/test/contracts/Calls.ts @@ -1,5 +1,5 @@ export const CALLS_SOURCE = ` - pragma solidity ^0.6.0; + pragma solidity ^0.8.0; contract Calls { function callWithoutParameter() pure public {} @@ -7,14 +7,32 @@ export const CALLS_SOURCE = ` 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'; diff --git a/waffle-chai/test/matchers/calledOnContract/calledOnContractTest.ts b/waffle-chai/test/matchers/calledOnContract/calledOnContractTest.ts index 17c9393e2..c7c3eebf6 100644 --- a/waffle-chai/test/matchers/calledOnContract/calledOnContractTest.ts +++ b/waffle-chai/test/matchers/calledOnContract/calledOnContractTest.ts @@ -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 () => { @@ -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( @@ -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); + }); }; diff --git a/waffle-chai/test/matchers/calledOnContract/calledOnContractWithTest.ts b/waffle-chai/test/matchers/calledOnContract/calledOnContractWithTest.ts index 782ef1029..bd48a0243 100644 --- a/waffle-chai/test/matchers/calledOnContract/calledOnContractWithTest.ts +++ b/waffle-chai/test/matchers/calledOnContract/calledOnContractWithTest.ts @@ -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 () => { @@ -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( @@ -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); + }); };