Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 333d681

Browse files
fjlMariusVanDerWijden
authored andcommittedDec 3, 2024
ethclient: add RevertErrorData function and example (ethereum#30669)
Here I'm adding a new helper function that extracts the revert reason of a contract call. Unfortunately, this aspect of the API is underspecified. See these spec issues for more detail: - ethereum/execution-apis#232 - ethereum/execution-apis#463 - ethereum/execution-apis#523 The function added here only works with Geth-like servers that return error code `3`. We will not be able to support all possible servers. However, if there is a specific server implementation that makes it possible to extract the same info, we could add it in the same function as well. --------- Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
1 parent f79b21c commit 333d681

File tree

4 files changed

+294
-164
lines changed

4 files changed

+294
-164
lines changed
 

‎ethclient/ethclient.go

+17
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,23 @@ func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) er
630630
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data))
631631
}
632632

633+
// RevertErrorData returns the 'revert reason' data of a contract call.
634+
//
635+
// This can be used with CallContract and EstimateGas, and only when the server is Geth.
636+
func RevertErrorData(err error) ([]byte, bool) {
637+
var ec rpc.Error
638+
var ed rpc.DataError
639+
if errors.As(err, &ec) && errors.As(err, &ed) && ec.ErrorCode() == 3 {
640+
if eds, ok := ed.ErrorData().(string); ok {
641+
revertData, err := hexutil.Decode(eds)
642+
if err == nil {
643+
return revertData, true
644+
}
645+
}
646+
}
647+
return nil, false
648+
}
649+
633650
func toBlockNumArg(number *big.Int) string {
634651
if number == nil {
635652
return "latest"

‎ethclient/ethclient_test.go

+89-164
Original file line numberDiff line numberDiff line change
@@ -14,180 +14,62 @@
1414
// You should have received a copy of the GNU Lesser General Public License
1515
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
1616

17-
package ethclient
17+
package ethclient_test
1818

1919
import (
2020
"bytes"
2121
"context"
2222
"errors"
23+
"fmt"
2324
"math/big"
2425
"reflect"
2526
"testing"
2627
"time"
2728

2829
"github.com/ethereum/go-ethereum"
30+
"github.com/ethereum/go-ethereum/accounts/abi"
2931
"github.com/ethereum/go-ethereum/common"
3032
"github.com/ethereum/go-ethereum/consensus/ethash"
3133
"github.com/ethereum/go-ethereum/core"
3234
"github.com/ethereum/go-ethereum/core/types"
3335
"github.com/ethereum/go-ethereum/crypto"
3436
"github.com/ethereum/go-ethereum/eth"
3537
"github.com/ethereum/go-ethereum/eth/ethconfig"
38+
"github.com/ethereum/go-ethereum/ethclient"
3639
"github.com/ethereum/go-ethereum/node"
3740
"github.com/ethereum/go-ethereum/params"
3841
"github.com/ethereum/go-ethereum/rpc"
3942
)
4043

4144
// Verify that Client implements the ethereum interfaces.
4245
var (
43-
_ = ethereum.ChainReader(&Client{})
44-
_ = ethereum.TransactionReader(&Client{})
45-
_ = ethereum.ChainStateReader(&Client{})
46-
_ = ethereum.ChainSyncReader(&Client{})
47-
_ = ethereum.ContractCaller(&Client{})
48-
_ = ethereum.GasEstimator(&Client{})
49-
_ = ethereum.GasPricer(&Client{})
50-
_ = ethereum.LogFilterer(&Client{})
51-
_ = ethereum.PendingStateReader(&Client{})
52-
// _ = ethereum.PendingStateEventer(&Client{})
53-
_ = ethereum.PendingContractCaller(&Client{})
46+
_ = ethereum.ChainReader(&ethclient.Client{})
47+
_ = ethereum.TransactionReader(&ethclient.Client{})
48+
_ = ethereum.ChainStateReader(&ethclient.Client{})
49+
_ = ethereum.ChainSyncReader(&ethclient.Client{})
50+
_ = ethereum.ContractCaller(&ethclient.Client{})
51+
_ = ethereum.GasEstimator(&ethclient.Client{})
52+
_ = ethereum.GasPricer(&ethclient.Client{})
53+
_ = ethereum.LogFilterer(&ethclient.Client{})
54+
_ = ethereum.PendingStateReader(&ethclient.Client{})
55+
// _ = ethereum.PendingStateEventer(&ethclient.Client{})
56+
_ = ethereum.PendingContractCaller(&ethclient.Client{})
5457
)
5558

56-
func TestToFilterArg(t *testing.T) {
57-
blockHashErr := errors.New("cannot specify both BlockHash and FromBlock/ToBlock")
58-
addresses := []common.Address{
59-
common.HexToAddress("0xD36722ADeC3EdCB29c8e7b5a47f352D701393462"),
60-
}
61-
blockHash := common.HexToHash(
62-
"0xeb94bb7d78b73657a9d7a99792413f50c0a45c51fc62bdcb08a53f18e9a2b4eb",
63-
)
64-
65-
for _, testCase := range []struct {
66-
name string
67-
input ethereum.FilterQuery
68-
output interface{}
69-
err error
70-
}{
71-
{
72-
"without BlockHash",
73-
ethereum.FilterQuery{
74-
Addresses: addresses,
75-
FromBlock: big.NewInt(1),
76-
ToBlock: big.NewInt(2),
77-
Topics: [][]common.Hash{},
78-
},
79-
map[string]interface{}{
80-
"address": addresses,
81-
"fromBlock": "0x1",
82-
"toBlock": "0x2",
83-
"topics": [][]common.Hash{},
84-
},
85-
nil,
86-
},
87-
{
88-
"with nil fromBlock and nil toBlock",
89-
ethereum.FilterQuery{
90-
Addresses: addresses,
91-
Topics: [][]common.Hash{},
92-
},
93-
map[string]interface{}{
94-
"address": addresses,
95-
"fromBlock": "0x0",
96-
"toBlock": "latest",
97-
"topics": [][]common.Hash{},
98-
},
99-
nil,
100-
},
101-
{
102-
"with negative fromBlock and negative toBlock",
103-
ethereum.FilterQuery{
104-
Addresses: addresses,
105-
FromBlock: big.NewInt(-1),
106-
ToBlock: big.NewInt(-1),
107-
Topics: [][]common.Hash{},
108-
},
109-
map[string]interface{}{
110-
"address": addresses,
111-
"fromBlock": "pending",
112-
"toBlock": "pending",
113-
"topics": [][]common.Hash{},
114-
},
115-
nil,
116-
},
117-
{
118-
"with blockhash",
119-
ethereum.FilterQuery{
120-
Addresses: addresses,
121-
BlockHash: &blockHash,
122-
Topics: [][]common.Hash{},
123-
},
124-
map[string]interface{}{
125-
"address": addresses,
126-
"blockHash": blockHash,
127-
"topics": [][]common.Hash{},
128-
},
129-
nil,
130-
},
131-
{
132-
"with blockhash and from block",
133-
ethereum.FilterQuery{
134-
Addresses: addresses,
135-
BlockHash: &blockHash,
136-
FromBlock: big.NewInt(1),
137-
Topics: [][]common.Hash{},
138-
},
139-
nil,
140-
blockHashErr,
141-
},
142-
{
143-
"with blockhash and to block",
144-
ethereum.FilterQuery{
145-
Addresses: addresses,
146-
BlockHash: &blockHash,
147-
ToBlock: big.NewInt(1),
148-
Topics: [][]common.Hash{},
149-
},
150-
nil,
151-
blockHashErr,
152-
},
153-
{
154-
"with blockhash and both from / to block",
155-
ethereum.FilterQuery{
156-
Addresses: addresses,
157-
BlockHash: &blockHash,
158-
FromBlock: big.NewInt(1),
159-
ToBlock: big.NewInt(2),
160-
Topics: [][]common.Hash{},
161-
},
162-
nil,
163-
blockHashErr,
164-
},
165-
} {
166-
t.Run(testCase.name, func(t *testing.T) {
167-
output, err := toFilterArg(testCase.input)
168-
if (testCase.err == nil) != (err == nil) {
169-
t.Fatalf("expected error %v but got %v", testCase.err, err)
170-
}
171-
if testCase.err != nil {
172-
if testCase.err.Error() != err.Error() {
173-
t.Fatalf("expected error %v but got %v", testCase.err, err)
174-
}
175-
} else if !reflect.DeepEqual(testCase.output, output) {
176-
t.Fatalf("expected filter arg %v but got %v", testCase.output, output)
177-
}
178-
})
179-
}
180-
}
181-
18259
var (
183-
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
184-
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
185-
testBalance = big.NewInt(2e15)
60+
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
61+
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
62+
testBalance = big.NewInt(2e15)
63+
revertContractAddr = common.HexToAddress("290f1b36649a61e369c6276f6d29463335b4400c")
64+
revertCode = common.FromHex("7f08c379a0000000000000000000000000000000000000000000000000000000006000526020600452600a6024527f75736572206572726f7200000000000000000000000000000000000000000000604452604e6000fd")
18665
)
18766

18867
var genesis = &core.Genesis{
189-
Config: params.AllEthashProtocolChanges,
190-
Alloc: types.GenesisAlloc{testAddr: {Balance: testBalance}},
68+
Config: params.AllEthashProtocolChanges,
69+
Alloc: types.GenesisAlloc{
70+
testAddr: {Balance: testBalance},
71+
revertContractAddr: {Code: revertCode},
72+
},
19173
ExtraData: []byte("test genesis"),
19274
Timestamp: 9000,
19375
BaseFee: big.NewInt(params.InitialBaseFee),
@@ -209,27 +91,30 @@ var testTx2 = types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), &
20991
To: &common.Address{2},
21092
})
21193

212-
func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
94+
func newTestBackend(config *node.Config) (*node.Node, []*types.Block, error) {
21395
// Generate test chain.
21496
blocks := generateTestChain()
21597

21698
// Create node
217-
n, err := node.New(&node.Config{})
99+
if config == nil {
100+
config = new(node.Config)
101+
}
102+
n, err := node.New(config)
218103
if err != nil {
219-
t.Fatalf("can't create new node: %v", err)
104+
return nil, nil, fmt.Errorf("can't create new node: %v", err)
220105
}
221106
// Create Ethereum Service
222-
config := &ethconfig.Config{Genesis: genesis, RPCGasCap: 1000000}
223-
ethservice, err := eth.New(n, config)
107+
ecfg := &ethconfig.Config{Genesis: genesis, RPCGasCap: 1000000}
108+
ethservice, err := eth.New(n, ecfg)
224109
if err != nil {
225-
t.Fatalf("can't create new ethereum service: %v", err)
110+
return nil, nil, fmt.Errorf("can't create new ethereum service: %v", err)
226111
}
227112
// Import the test chain.
228113
if err := n.Start(); err != nil {
229-
t.Fatalf("can't start test node: %v", err)
114+
return nil, nil, fmt.Errorf("can't start test node: %v", err)
230115
}
231116
if _, err := ethservice.BlockChain().InsertChain(blocks[1:]); err != nil {
232-
t.Fatalf("can't import test blocks: %v", err)
117+
return nil, nil, fmt.Errorf("can't import test blocks: %v", err)
233118
}
234119
// Ensure the tx indexing is fully generated
235120
for ; ; time.Sleep(time.Millisecond * 100) {
@@ -238,7 +123,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
238123
break
239124
}
240125
}
241-
return n, blocks
126+
return n, blocks, nil
242127
}
243128

244129
func generateTestChain() []*types.Block {
@@ -256,7 +141,10 @@ func generateTestChain() []*types.Block {
256141
}
257142

258143
func TestEthClient(t *testing.T) {
259-
backend, chain := newTestBackend(t)
144+
backend, chain, err := newTestBackend(nil)
145+
if err != nil {
146+
t.Fatal(err)
147+
}
260148
client := backend.Attach()
261149
defer backend.Close()
262150
defer client.Close()
@@ -324,7 +212,7 @@ func testHeader(t *testing.T, chain []*types.Block, client *rpc.Client) {
324212
}
325213
for name, tt := range tests {
326214
t.Run(name, func(t *testing.T) {
327-
ec := NewClient(client)
215+
ec := ethclient.NewClient(client)
328216
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
329217
defer cancel()
330218

@@ -373,7 +261,7 @@ func testBalanceAt(t *testing.T, client *rpc.Client) {
373261
}
374262
for name, tt := range tests {
375263
t.Run(name, func(t *testing.T) {
376-
ec := NewClient(client)
264+
ec := ethclient.NewClient(client)
377265
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
378266
defer cancel()
379267

@@ -389,7 +277,7 @@ func testBalanceAt(t *testing.T, client *rpc.Client) {
389277
}
390278

391279
func testTransactionInBlock(t *testing.T, client *rpc.Client) {
392-
ec := NewClient(client)
280+
ec := ethclient.NewClient(client)
393281

394282
// Get current block by number.
395283
block, err := ec.BlockByNumber(context.Background(), nil)
@@ -421,7 +309,7 @@ func testTransactionInBlock(t *testing.T, client *rpc.Client) {
421309
}
422310

423311
func testChainID(t *testing.T, client *rpc.Client) {
424-
ec := NewClient(client)
312+
ec := ethclient.NewClient(client)
425313
id, err := ec.ChainID(context.Background())
426314
if err != nil {
427315
t.Fatalf("unexpected error: %v", err)
@@ -432,7 +320,7 @@ func testChainID(t *testing.T, client *rpc.Client) {
432320
}
433321

434322
func testGetBlock(t *testing.T, client *rpc.Client) {
435-
ec := NewClient(client)
323+
ec := ethclient.NewClient(client)
436324

437325
// Get current block number
438326
blockNumber, err := ec.BlockNumber(context.Background())
@@ -477,7 +365,7 @@ func testGetBlock(t *testing.T, client *rpc.Client) {
477365
}
478366

479367
func testStatusFunctions(t *testing.T, client *rpc.Client) {
480-
ec := NewClient(client)
368+
ec := ethclient.NewClient(client)
481369

482370
// Sync progress
483371
progress, err := ec.SyncProgress(context.Background())
@@ -540,7 +428,7 @@ func testStatusFunctions(t *testing.T, client *rpc.Client) {
540428
}
541429

542430
func testCallContractAtHash(t *testing.T, client *rpc.Client) {
543-
ec := NewClient(client)
431+
ec := ethclient.NewClient(client)
544432

545433
// EstimateGas
546434
msg := ethereum.CallMsg{
@@ -567,7 +455,7 @@ func testCallContractAtHash(t *testing.T, client *rpc.Client) {
567455
}
568456

569457
func testCallContract(t *testing.T, client *rpc.Client) {
570-
ec := NewClient(client)
458+
ec := ethclient.NewClient(client)
571459

572460
// EstimateGas
573461
msg := ethereum.CallMsg{
@@ -594,7 +482,7 @@ func testCallContract(t *testing.T, client *rpc.Client) {
594482
}
595483

596484
func testAtFunctions(t *testing.T, client *rpc.Client) {
597-
ec := NewClient(client)
485+
ec := ethclient.NewClient(client)
598486

599487
block, err := ec.HeaderByNumber(context.Background(), big.NewInt(1))
600488
if err != nil {
@@ -697,7 +585,7 @@ func testAtFunctions(t *testing.T, client *rpc.Client) {
697585
}
698586

699587
func testTransactionSender(t *testing.T, client *rpc.Client) {
700-
ec := NewClient(client)
588+
ec := ethclient.NewClient(client)
701589
ctx := context.Background()
702590

703591
// Retrieve testTx1 via RPC.
@@ -737,7 +625,7 @@ func testTransactionSender(t *testing.T, client *rpc.Client) {
737625
}
738626
}
739627

740-
func sendTransaction(ec *Client) error {
628+
func sendTransaction(ec *ethclient.Client) error {
741629
chainID, err := ec.ChainID(context.Background())
742630
if err != nil {
743631
return err
@@ -760,3 +648,40 @@ func sendTransaction(ec *Client) error {
760648
}
761649
return ec.SendTransaction(context.Background(), tx)
762650
}
651+
652+
// Here we show how to get the error message of reverted contract call.
653+
func ExampleRevertErrorData() {
654+
// First create an ethclient.Client instance.
655+
ctx := context.Background()
656+
ec, _ := ethclient.DialContext(ctx, exampleNode.HTTPEndpoint())
657+
658+
// Call the contract.
659+
// Note we expect the call to return an error.
660+
contract := common.HexToAddress("290f1b36649a61e369c6276f6d29463335b4400c")
661+
call := ethereum.CallMsg{To: &contract, Gas: 30000}
662+
result, err := ec.CallContract(ctx, call, nil)
663+
if len(result) > 0 {
664+
panic("got result")
665+
}
666+
if err == nil {
667+
panic("call did not return error")
668+
}
669+
670+
// Extract the low-level revert data from the error.
671+
revertData, ok := ethclient.RevertErrorData(err)
672+
if !ok {
673+
panic("unpacking revert failed")
674+
}
675+
fmt.Printf("revert: %x\n", revertData)
676+
677+
// Parse the revert data to obtain the error message.
678+
message, err := abi.UnpackRevert(revertData)
679+
if err != nil {
680+
panic("parsing ABI error failed: " + err.Error())
681+
}
682+
fmt.Println("message:", message)
683+
684+
// Output:
685+
// revert: 08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a75736572206572726f72
686+
// message: user error
687+
}

‎ethclient/example_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2024 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethclient_test
18+
19+
import (
20+
"github.com/ethereum/go-ethereum/node"
21+
)
22+
23+
var exampleNode *node.Node
24+
25+
// launch example server
26+
func init() {
27+
config := &node.Config{
28+
HTTPHost: "127.0.0.1",
29+
}
30+
n, _, err := newTestBackend(config)
31+
if err != nil {
32+
panic("can't launch node: " + err.Error())
33+
}
34+
exampleNode = n
35+
}

‎ethclient/types_test.go

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2016 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethclient
18+
19+
import (
20+
"errors"
21+
"math/big"
22+
"reflect"
23+
"testing"
24+
25+
"github.com/ethereum/go-ethereum"
26+
"github.com/ethereum/go-ethereum/common"
27+
)
28+
29+
func TestToFilterArg(t *testing.T) {
30+
blockHashErr := errors.New("cannot specify both BlockHash and FromBlock/ToBlock")
31+
addresses := []common.Address{
32+
common.HexToAddress("0xD36722ADeC3EdCB29c8e7b5a47f352D701393462"),
33+
}
34+
blockHash := common.HexToHash(
35+
"0xeb94bb7d78b73657a9d7a99792413f50c0a45c51fc62bdcb08a53f18e9a2b4eb",
36+
)
37+
38+
for _, testCase := range []struct {
39+
name string
40+
input ethereum.FilterQuery
41+
output interface{}
42+
err error
43+
}{
44+
{
45+
"without BlockHash",
46+
ethereum.FilterQuery{
47+
Addresses: addresses,
48+
FromBlock: big.NewInt(1),
49+
ToBlock: big.NewInt(2),
50+
Topics: [][]common.Hash{},
51+
},
52+
map[string]interface{}{
53+
"address": addresses,
54+
"fromBlock": "0x1",
55+
"toBlock": "0x2",
56+
"topics": [][]common.Hash{},
57+
},
58+
nil,
59+
},
60+
{
61+
"with nil fromBlock and nil toBlock",
62+
ethereum.FilterQuery{
63+
Addresses: addresses,
64+
Topics: [][]common.Hash{},
65+
},
66+
map[string]interface{}{
67+
"address": addresses,
68+
"fromBlock": "0x0",
69+
"toBlock": "latest",
70+
"topics": [][]common.Hash{},
71+
},
72+
nil,
73+
},
74+
{
75+
"with negative fromBlock and negative toBlock",
76+
ethereum.FilterQuery{
77+
Addresses: addresses,
78+
FromBlock: big.NewInt(-1),
79+
ToBlock: big.NewInt(-1),
80+
Topics: [][]common.Hash{},
81+
},
82+
map[string]interface{}{
83+
"address": addresses,
84+
"fromBlock": "pending",
85+
"toBlock": "pending",
86+
"topics": [][]common.Hash{},
87+
},
88+
nil,
89+
},
90+
{
91+
"with blockhash",
92+
ethereum.FilterQuery{
93+
Addresses: addresses,
94+
BlockHash: &blockHash,
95+
Topics: [][]common.Hash{},
96+
},
97+
map[string]interface{}{
98+
"address": addresses,
99+
"blockHash": blockHash,
100+
"topics": [][]common.Hash{},
101+
},
102+
nil,
103+
},
104+
{
105+
"with blockhash and from block",
106+
ethereum.FilterQuery{
107+
Addresses: addresses,
108+
BlockHash: &blockHash,
109+
FromBlock: big.NewInt(1),
110+
Topics: [][]common.Hash{},
111+
},
112+
nil,
113+
blockHashErr,
114+
},
115+
{
116+
"with blockhash and to block",
117+
ethereum.FilterQuery{
118+
Addresses: addresses,
119+
BlockHash: &blockHash,
120+
ToBlock: big.NewInt(1),
121+
Topics: [][]common.Hash{},
122+
},
123+
nil,
124+
blockHashErr,
125+
},
126+
{
127+
"with blockhash and both from / to block",
128+
ethereum.FilterQuery{
129+
Addresses: addresses,
130+
BlockHash: &blockHash,
131+
FromBlock: big.NewInt(1),
132+
ToBlock: big.NewInt(2),
133+
Topics: [][]common.Hash{},
134+
},
135+
nil,
136+
blockHashErr,
137+
},
138+
} {
139+
t.Run(testCase.name, func(t *testing.T) {
140+
output, err := toFilterArg(testCase.input)
141+
if (testCase.err == nil) != (err == nil) {
142+
t.Fatalf("expected error %v but got %v", testCase.err, err)
143+
}
144+
if testCase.err != nil {
145+
if testCase.err.Error() != err.Error() {
146+
t.Fatalf("expected error %v but got %v", testCase.err, err)
147+
}
148+
} else if !reflect.DeepEqual(testCase.output, output) {
149+
t.Fatalf("expected filter arg %v but got %v", testCase.output, output)
150+
}
151+
})
152+
}
153+
}

0 commit comments

Comments
 (0)
Please sign in to comment.