From cc1d25045589cf9e9f4e67dcc0410071e1a67fcb Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 26 Dec 2023 16:14:24 +0300 Subject: [PATCH] api: add ability to mock connections in tests Create a mock implementations `MockRequest`, `MockResponse` and `MockDoer`. The last one allows to mock not the full `Connection`, but its part -- a structure, that implements new `Doer` interface (a `Do` function). Also added missing comments, all the changes are recorded in the `CHANGELOG` and `README` files. Added new tests and examples. So this entity is easier to implement and it is enough to mock tests that require working `Connection`. All new mock structs and an example for `MockDoer` usage are added to the `test_helpers` package. Added new structs `MockDoer`, `MockRequest` to `test_helpers`. Renamed `StrangerResponse` to `MockResponse`. Added new connection log constant: `LogAppendPushFailed`. It is logged when connection fails to append a push response. Closes #237 --- CHANGELOG.md | 7 +- README.md | 18 ++- connection.go | 13 +- connector.go | 8 +- const.go | 4 +- dial.go | 35 ++++-- example_test.go | 61 ++++++++- future.go | 12 +- future_test.go | 163 +++++++++++++++++++++--- header.go | 10 +- pool/connection_pool.go | 2 +- pool/connection_pool_test.go | 5 +- prepared.go | 4 +- request.go | 4 +- request_test.go | 160 ++++++++++++++++-------- response.go | 181 +++++++++++++++------------ schema.go | 14 ++- schema_test.go | 162 ++++++++++++++++++++++++ stream.go | 2 +- tarantool_test.go | 235 ++++++++++++++++++++--------------- test_helpers/doer.go | 69 ++++++++++ test_helpers/example_test.go | 36 ++++++ test_helpers/request.go | 52 ++++++++ test_helpers/request_mock.go | 45 ------- test_helpers/response.go | 74 +++++++++++ 25 files changed, 1028 insertions(+), 348 deletions(-) create mode 100644 schema_test.go create mode 100644 test_helpers/doer.go create mode 100644 test_helpers/example_test.go create mode 100644 test_helpers/request.go delete mode 100644 test_helpers/request_mock.go create mode 100644 test_helpers/response.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 81582e5e5..fa3e02810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - `Response` method added to the `Request` interface (#237) - New `LogAppendPushFailed` connection log constant (#237). It is logged when connection fails to append a push response. +- `ErrorNo` constant that indicates that no error has occurred while getting + the response (#237) +- Ability to mock connections for tests (#237). Added new types `MockDoer`, + `MockRequest` to `test_helpers`. ### Changed @@ -88,6 +92,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` and `Pooler` return response data instead of an actual responses (#237) +- Renamed `StrangerResponse` to `MockResponse` (#237) ### Deprecated @@ -110,7 +115,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IPROTO constants (#158) - Code() method from the Request interface (#158) - `Schema` field from the `Connection` struct (#7) -- `PushCode` constant (#237) +- `OkCode` and `PushCode` constants (#237) ### Fixed diff --git a/README.md b/README.md index 4f82896de..d63d67669 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,10 @@ The subpackage has been deleted. You could use `pool` instead. * `crud` operations `Timeout` option has `crud.OptFloat64` type instead of `crud.OptUint`. +#### test_helpers package + +Renamed `StrangerResponse` to `MockResponse`. + #### msgpack.v5 Most function names and argument types in `msgpack.v5` and `msgpack.v2` @@ -248,6 +252,9 @@ of the requests is an array instead of array of arrays. * IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). * `PushCode` constant is removed. To check whether the current response is a push response, use `IsPush()` method of the response iterator instead. +* `OkCode` constant is removed. +* `ErrorNo` constant is added to indicate that no error has occurred while + getting the response. #### Request changes @@ -285,9 +292,10 @@ for an `ops` field. `*Operations` needs to be used instead. #### Connector changes -Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, -`Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return -response data instead of an actual responses. +* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, + `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return + response data instead of an actual responses. +* New interface `Doer` is added as a child-interface instead of a `Do` method. #### Connect function @@ -304,8 +312,8 @@ for creating a connection are now stored in corresponding `Dialer`, not in `Opts #### Connection schema * Removed `Schema` field from the `Connection` struct. Instead, new -`GetSchema(Connector)` function was added to get the actual connection -schema on demand. + `GetSchema(Doer)` function was added to get the actual connection + schema on demand. * `OverrideSchema(*Schema)` method replaced with the `SetSchema(Schema)`. #### Protocol changes diff --git a/connection.go b/connection.go index 8e3939190..8f8631a31 100644 --- a/connection.go +++ b/connection.go @@ -813,7 +813,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { return } buf := smallBuf{b: respBytes} - header, err := decodeHeader(conn.dec, &buf) + header, code, err := decodeHeader(conn.dec, &buf) if err != nil { err = ClientError{ ErrProtocolError, @@ -824,7 +824,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { } var fut *Future = nil - if iproto.Type(header.Code) == iproto.IPROTO_EVENT { + if code == iproto.IPROTO_EVENT { if event, err := readWatchEvent(&buf); err == nil { events <- event } else { @@ -835,7 +835,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { conn.opts.Logger.Report(LogWatchEventReadFailed, conn, err) } continue - } else if header.Code == uint32(iproto.IPROTO_CHUNK) { + } else if code == iproto.IPROTO_CHUNK { if fut = conn.peekFuture(header.RequestId); fut != nil { if err := fut.AppendPush(header, &buf); err != nil { err = ClientError{ @@ -887,8 +887,7 @@ func (conn *Connection) eventer(events <-chan connWatchEvent) { func (conn *Connection) newFuture(req Request) (fut *Future) { ctx := req.Ctx() - fut = NewFuture() - fut.SetRequest(req) + fut = NewFuture(req) if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { select { case conn.rlimit <- struct{}{}: @@ -1069,7 +1068,7 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { if fut = conn.fetchFuture(reqid); fut != nil { header := Header{ RequestId: reqid, - Code: OkCode, + Error: ErrorNo, } fut.SetResponse(header, nil) conn.markDone(fut) @@ -1217,7 +1216,7 @@ func (conn *Connection) nextRequestId(context bool) (requestId uint32) { func (conn *Connection) Do(req Request) *Future { if connectedReq, ok := req.(ConnectedRequest); ok { if connectedReq.Conn() != conn { - fut := NewFuture() + fut := NewFuture(req) fut.SetError(errUnknownRequest) return fut } diff --git a/connector.go b/connector.go index 72b5d19a8..9917cff0f 100644 --- a/connector.go +++ b/connector.go @@ -2,14 +2,20 @@ package tarantool import "time" +// Doer is an interface that performs requests asynchronously. +type Doer interface { + // Do performs a request asynchronously. + Do(req Request) (fut *Future) +} + type Connector interface { + Doer ConnectedNow() bool Close() error ConfiguredTimeout() time.Duration NewPrepared(expr string) (*Prepared, error) NewStream() (*Stream, error) NewWatcher(key string, callback WatchCallback) (Watcher, error) - Do(req Request) (fut *Future) // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. diff --git a/const.go b/const.go index ede4c988d..e8f389253 100644 --- a/const.go +++ b/const.go @@ -9,5 +9,7 @@ const ( ) const ( - OkCode = uint32(iproto.IPROTO_OK) + // ErrorNo indicates that no error has occurred. It could be used to + // check that a response has an error without the response body decoding. + ErrorNo = iproto.ER_UNKNOWN ) diff --git a/dial.go b/dial.go index 378687925..ff5419760 100644 --- a/dial.go +++ b/dial.go @@ -398,16 +398,17 @@ func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { return info, err } - resp, err := readResponse(r) + resp, err := readResponse(r, req) if err != nil { + if resp != nil && + resp.Header().Error == iproto.ER_UNKNOWN_REQUEST_TYPE { + // IPROTO_ID requests are not supported by server. + return info, nil + } return info, err } data, err := resp.Decode() if err != nil { - if iproto.Error(resp.Header().Code) == iproto.ER_UNKNOWN_REQUEST_TYPE { - // IPROTO_ID requests are not supported by server. - return info, nil - } return info, err } @@ -477,7 +478,7 @@ func authenticate(c Conn, auth Auth, user string, pass string, salt string) erro if err = writeRequest(c, req); err != nil { return err } - if _, err = readResponse(c); err != nil { + if _, err = readResponse(c, req); err != nil { return err } return nil @@ -501,19 +502,31 @@ func writeRequest(w writeFlusher, req Request) error { } // readResponse reads a response from the reader. -func readResponse(r io.Reader) (Response, error) { +func readResponse(r io.Reader, req Request) (Response, error) { var lenbuf [packetLengthBytes]byte respBytes, err := read(r, lenbuf[:]) if err != nil { - return &BaseResponse{}, fmt.Errorf("read error: %w", err) + return nil, fmt.Errorf("read error: %w", err) } buf := smallBuf{b: respBytes} - header, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) - resp := &BaseResponse{header: header, buf: buf} + header, _, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) if err != nil { - return resp, fmt.Errorf("decode response header error: %w", err) + return nil, fmt.Errorf("decode response header error: %w", err) + } + resp, err := req.Response(header, &buf) + if err != nil { + return nil, fmt.Errorf("creating response error: %w", err) + } + _, err = resp.Decode() + if err != nil { + switch err.(type) { + case Error: + return resp, err + default: + return resp, fmt.Errorf("decode response body error: %w", err) + } } return resp, nil } diff --git a/example_test.go b/example_test.go index 3c4d12112..965c25a82 100644 --- a/example_test.go +++ b/example_test.go @@ -198,16 +198,34 @@ func ExampleSelectRequest() { } key := []interface{}{uint(1111)} - data, err := conn.Do(tarantool.NewSelectRequest(617). + resp, err := conn.Do(tarantool.NewSelectRequest(617). Limit(100). Iterator(tarantool.IterEq). Key(key), - ).Get() + ).GetResponse() if err != nil { fmt.Printf("error in select is %v", err) return } + selResp, ok := resp.(*tarantool.SelectResponse) + if !ok { + fmt.Print("wrong response type") + return + } + + pos, err := selResp.Pos() + if err != nil { + fmt.Printf("error in Pos: %v", err) + return + } + fmt.Printf("pos for Select is %v\n", pos) + + data, err := resp.Decode() + if err != nil { + fmt.Printf("error while decoding: %v", err) + return + } fmt.Printf("response is %#v\n", data) var res []Tuple @@ -224,6 +242,7 @@ func ExampleSelectRequest() { fmt.Printf("response is %v\n", res) // Output: + // pos for Select is [] // response is []interface {}{[]interface {}{0x457, "hello", "world"}} // response is [{{} 1111 hello world}] } @@ -567,17 +586,21 @@ func ExampleExecuteRequest() { resp, err := conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) + data, err := resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) + exResp, ok := resp.(*tarantool.ExecuteResponse) if !ok { fmt.Printf("wrong response type") return } + metaData, err := exResp.MetaData() fmt.Println("MetaData", metaData) fmt.Println("Error", err) + sqlInfo, err := exResp.SQLInfo() fmt.Println("SQL Info", sqlInfo) fmt.Println("Error", err) @@ -992,6 +1015,26 @@ func ExampleBeginRequest_TxnIsolation() { fmt.Printf("Select after Rollback: response is %#v\n", data) } +func ExampleErrorNo() { + conn := exampleConnect(dialer, opts) + defer conn.Close() + + req := tarantool.NewPingRequest() + resp, err := conn.Do(req).GetResponse() + if err != nil { + fmt.Printf("error getting the response: %s\n", err) + return + } + + if resp.Header().Error != tarantool.ErrorNo { + fmt.Printf("response error code: %s\n", resp.Header().Error) + } else { + fmt.Println("Success.") + } + // Output: + // Success. +} + func ExampleFuture_GetIterator() { conn := exampleConnect(dialer, opts) defer conn.Close() @@ -1008,11 +1051,11 @@ func ExampleFuture_GetIterator() { if it.IsPush() { // It is a push message. fmt.Printf("push message: %v\n", data[0]) - } else if resp.Header().Code == tarantool.OkCode { + } else if resp.Header().Error == tarantool.ErrorNo { // It is a regular response. fmt.Printf("response: %v", data[0]) } else { - fmt.Printf("an unexpected response code %d", resp.Header().Code) + fmt.Printf("an unexpected response code %d", resp.Header().Error) } } if err := it.Err(); err != nil { @@ -1224,6 +1267,11 @@ func ExampleConnection_Do_failure() { if err != nil { fmt.Printf("Error in the future: %s\n", err) } + // Optional step: check a response error. + // It allows checking that response has or hasn't an error without decoding. + if resp.Header().Error != tarantool.ErrorNo { + fmt.Printf("Response error: %s\n", resp.Header().Error) + } data, err := future.Get() if err != nil { @@ -1239,8 +1287,8 @@ func ExampleConnection_Do_failure() { } else { // Response exist. So it could be a Tarantool error or a decode // error. We need to check the error code. - fmt.Printf("Error code from the response: %d\n", resp.Header().Code) - if resp.Header().Code == tarantool.OkCode { + fmt.Printf("Error code from the response: %d\n", resp.Header().Error) + if resp.Header().Error == tarantool.ErrorNo { fmt.Printf("Decode error: %s\n", err) } else { code := err.(tarantool.Error).Code @@ -1251,6 +1299,7 @@ func ExampleConnection_Do_failure() { } // Output: + // Response error: ER_NO_SUCH_PROC // Data: [] // Error code from the response: 33 // Error code from the error: 33 diff --git a/future.go b/future.go index 139782637..1b9f2ed14 100644 --- a/future.go +++ b/future.go @@ -121,7 +121,7 @@ func (it *asyncResponseIterator) nextResponse() (resp Response) { // PushResponse is used for push requests for the Future. type PushResponse struct { - BaseResponse + baseResponse } func createPushResponse(header Header, body io.Reader) (Response, error) { @@ -132,12 +132,13 @@ func createPushResponse(header Header, body io.Reader) (Response, error) { return &PushResponse{resp}, nil } -// NewFuture creates a new empty Future. -func NewFuture() (fut *Future) { +// NewFuture creates a new empty Future for a given Request. +func NewFuture(req Request) (fut *Future) { fut = &Future{} fut.ready = make(chan struct{}, 1000000000) fut.done = make(chan struct{}) fut.pushes = make([]Response, 0) + fut.req = req return fut } @@ -163,11 +164,6 @@ func (fut *Future) AppendPush(header Header, body io.Reader) error { return nil } -// SetRequest sets a request, for which the future was created. -func (fut *Future) SetRequest(req Request) { - fut.req = req -} - // SetResponse sets a response for the future and finishes the future. func (fut *Future) SetResponse(header Header, body io.Reader) error { fut.mutex.Lock() diff --git a/future_test.go b/future_test.go index 947e4ed28..6efda10a1 100644 --- a/future_test.go +++ b/future_test.go @@ -1,15 +1,86 @@ package tarantool_test import ( + "bytes" + "context" "errors" + "io" + "io/ioutil" "sync" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/vmihailenco/msgpack/v5" ) +type futureMockRequest struct { +} + +func (req *futureMockRequest) Type() iproto.Type { + return iproto.Type(0) +} + +func (req *futureMockRequest) Async() bool { + return false +} + +func (req *futureMockRequest) Body(resolver SchemaResolver, enc *msgpack.Encoder) error { + return nil +} + +func (req *futureMockRequest) Conn() *Connection { + return &Connection{} +} + +func (req *futureMockRequest) Ctx() context.Context { + return nil +} + +func (req *futureMockRequest) Response(header Header, + body io.Reader) (Response, error) { + resp, err := createFutureMockResponse(header, body) + return resp, err +} + +type futureMockResponse struct { + header Header + data []byte + + decodeCnt int + decodeTypedCnt int +} + +func (resp *futureMockResponse) Header() Header { + return resp.header +} + +func (resp *futureMockResponse) Decode() ([]interface{}, error) { + resp.decodeCnt++ + + dataInt := make([]interface{}, len(resp.data)) + for i := range resp.data { + dataInt[i] = resp.data[i] + } + return dataInt, nil +} + +func (resp *futureMockResponse) DecodeTyped(res interface{}) error { + resp.decodeTypedCnt++ + return nil +} + +func createFutureMockResponse(header Header, body io.Reader) (Response, error) { + data, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + return &futureMockResponse{header: header, data: data}, nil +} + func assertResponseIteratorValue(t testing.TB, it ResponseIterator, isPush bool, resp Response) { t.Helper() @@ -43,7 +114,7 @@ func assertResponseIteratorFinished(t testing.TB, it ResponseIterator) { } func TestFutureGetIteratorNoItems(t *testing.T) { - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) it := fut.GetIterator() if it.Next() { @@ -56,7 +127,7 @@ func TestFutureGetIteratorNoItems(t *testing.T) { func TestFutureGetIteratorNoResponse(t *testing.T) { pushHeader := Header{} push := &PushResponse{} - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) fut.AppendPush(pushHeader, nil) if it := fut.GetIterator(); it.Next() { @@ -73,7 +144,7 @@ func TestFutureGetIteratorNoResponse(t *testing.T) { func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { pushHeader := Header{} push := &PushResponse{} - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) fut.AppendPush(pushHeader, nil) if it := fut.GetIterator().WithTimeout(1 * time.Nanosecond); it.Next() { @@ -91,8 +162,8 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { pushHeader := Header{} respHeader := Header{} push := &PushResponse{} - resp := &BaseResponse{} - fut := NewFuture() + resp := &test_helpers.MockResponse{} + fut := NewFuture(test_helpers.NewMockRequest()) fut.AppendPush(pushHeader, nil) var done sync.WaitGroup @@ -128,15 +199,13 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { wait.Wait() - fut.SetRequest(&InsertRequest{}) fut.SetResponse(respHeader, nil) done.Wait() } func TestFutureGetIteratorFirstResponse(t *testing.T) { - resp := &BaseResponse{} - fut := NewFuture() - fut.SetRequest(&InsertRequest{}) + resp := &test_helpers.MockResponse{} + fut := NewFuture(test_helpers.NewMockRequest()) fut.SetResponse(Header{}, nil) fut.SetResponse(Header{}, nil) @@ -155,7 +224,7 @@ func TestFutureGetIteratorFirstError(t *testing.T) { const errMsg1 = "error1" const errMsg2 = "error2" - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) fut.SetError(errors.New(errMsg1)) fut.SetError(errors.New(errMsg2)) @@ -173,11 +242,10 @@ func TestFutureGetIteratorResponse(t *testing.T) { responses := []Response{ &PushResponse{}, &PushResponse{}, - &BaseResponse{}, + &test_helpers.MockResponse{}, } header := Header{} - fut := NewFuture() - fut.SetRequest(&InsertRequest{}) + fut := NewFuture(test_helpers.NewMockRequest()) for i := range responses { if i == len(responses)-1 { fut.SetResponse(header, nil) @@ -215,7 +283,7 @@ func TestFutureGetIteratorError(t *testing.T) { {}, } err := errors.New(errMsg) - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) for range responses { fut.AppendPush(Header{}, nil) } @@ -249,8 +317,7 @@ func TestFutureSetStateRaceCondition(t *testing.T) { err := errors.New("any error") for i := 0; i < 1000; i++ { - fut := NewFuture() - fut.SetRequest(&InsertRequest{}) + fut := NewFuture(test_helpers.NewMockRequest()) for j := 0; j < 9; j++ { go func(opt int) { if opt%3 == 0 { @@ -266,3 +333,67 @@ func TestFutureSetStateRaceCondition(t *testing.T) { // It may be false-positive, but very rarely - it's ok for such very // simple race conditions tests. } + +func TestFutureGetIteratorIsPush(t *testing.T) { + fut := NewFuture(test_helpers.NewMockRequest()) + fut.AppendPush(Header{}, nil) + fut.SetResponse(Header{}, nil) + it := fut.GetIterator() + + it.Next() + assert.True(t, it.IsPush()) + it.Next() + assert.False(t, it.IsPush()) +} + +func TestFuture_Get(t *testing.T) { + fut := NewFuture(&futureMockRequest{}) + fut.SetResponse(Header{}, bytes.NewReader([]byte{'v', '2'})) + + resp, err := fut.GetResponse() + assert.NoError(t, err) + mockResp, ok := resp.(*futureMockResponse) + assert.True(t, ok) + + data, err := fut.Get() + assert.NoError(t, err) + assert.Equal(t, []interface{}{uint8('v'), uint8('2')}, data) + assert.Equal(t, 1, mockResp.decodeCnt) + assert.Equal(t, 0, mockResp.decodeTypedCnt) +} + +func TestFuture_GetTyped(t *testing.T) { + fut := NewFuture(&futureMockRequest{}) + fut.SetResponse(Header{}, bytes.NewReader([]byte{'v', '2'})) + + resp, err := fut.GetResponse() + assert.NoError(t, err) + mockResp, ok := resp.(*futureMockResponse) + assert.True(t, ok) + + var data []byte + + err = fut.GetTyped(&data) + assert.NoError(t, err) + assert.Equal(t, 0, mockResp.decodeCnt) + assert.Equal(t, 1, mockResp.decodeTypedCnt) +} + +func TestFuture_GetResponse(t *testing.T) { + mockResp, err := createFutureMockResponse(Header{}, + bytes.NewReader([]byte{'v', '2'})) + assert.NoError(t, err) + + fut := NewFuture(&futureMockRequest{}) + fut.SetResponse(Header{}, bytes.NewReader([]byte{'v', '2'})) + + resp, err := fut.GetResponse() + assert.NoError(t, err) + respConv, ok := resp.(*futureMockResponse) + assert.True(t, ok) + assert.Equal(t, mockResp, respConv) + + data, err := resp.Decode() + assert.NoError(t, err) + assert.Equal(t, []interface{}{uint8('v'), uint8('2')}, data) +} diff --git a/header.go b/header.go index d9069c23a..20a4a465f 100644 --- a/header.go +++ b/header.go @@ -1,10 +1,14 @@ package tarantool +import "github.com/tarantool/go-iproto" + // Header is a response header. type Header struct { // RequestId is an id of a corresponding request. RequestId uint32 - // Code is a response code. It could be used to check that response - // has or hasn't an error. - Code uint32 + // Error is a response error. It could be used + // to check that response has or hasn't an error without decoding. + // Error == ErrorNo (iproto.ER_UNKNOWN) if there is no error. + // Otherwise, it contains an error code from iproto.Error enumeration. + Error iproto.Error } diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 3734c4c0a..861221290 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -1460,7 +1460,7 @@ func (p *ConnectionPool) getConnByMode(defaultMode Mode, } func newErrorFuture(err error) *tarantool.Future { - fut := tarantool.NewFuture() + fut := tarantool.NewFuture(nil) fut.SetError(err) return fut } diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index ae1acd223..acdc756d3 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -2379,9 +2379,8 @@ func TestPing(t *testing.T) { require.Nilf(t, data, "response data is not nil after Ping") // RO - data, err = connPool.Ping(pool.RO) + _, err = connPool.Ping(pool.RO) require.Nilf(t, err, "failed to Ping") - require.Nilf(t, data, "response data is not nil after Ping") // PreferRW data, err = connPool.Ping(pool.PreferRW) @@ -2549,7 +2548,7 @@ func TestDoWithStrangerConn(t *testing.T) { defer connPool.Close() - req := test_helpers.NewStrangerRequest() + req := test_helpers.NewMockRequest() _, err = connPool.Do(req, pool.ANY).Get() if err == nil { diff --git a/prepared.go b/prepared.go index 35b73272e..3a03d740f 100644 --- a/prepared.go +++ b/prepared.go @@ -102,7 +102,7 @@ func (req *PrepareRequest) Response(header Header, body io.Reader) (Response, er if err != nil { return nil, err } - return &PrepareResponse{BaseResponse: baseResp}, nil + return &PrepareResponse{baseResponse: baseResp}, nil } // UnprepareRequest helps you to create an unprepare request object for @@ -192,5 +192,5 @@ func (req *ExecutePreparedRequest) Response(header Header, body io.Reader) (Resp if err != nil { return nil, err } - return &ExecuteResponse{BaseResponse: baseResp}, nil + return &ExecuteResponse{baseResponse: baseResp}, nil } diff --git a/request.go b/request.go index 716e5ab2c..8dbe250b5 100644 --- a/request.go +++ b/request.go @@ -1109,7 +1109,7 @@ func (req *SelectRequest) Response(header Header, body io.Reader) (Response, err if err != nil { return nil, err } - return &SelectResponse{BaseResponse: baseResp}, nil + return &SelectResponse{baseResponse: baseResp}, nil } // InsertRequest helps you to create an insert request object for execution @@ -1517,7 +1517,7 @@ func (req *ExecuteRequest) Response(header Header, body io.Reader) (Response, er if err != nil { return nil, err } - return &ExecuteResponse{BaseResponse: baseResp}, nil + return &ExecuteResponse{baseResponse: baseResp}, nil } // WatchOnceRequest synchronously fetches the value currently associated with a diff --git a/request_test.go b/request_test.go index 580259702..84ba23ef6 100644 --- a/request_test.go +++ b/request_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "testing" "time" @@ -317,58 +318,6 @@ func TestRequestsCtx_setter(t *testing.T) { } } -func TestResolverCalledWithoutNameSupport(t *testing.T) { - resolver.nameUseSupported = false - resolver.spaceResolverCalls = 0 - resolver.indexResolverCalls = 0 - - req := NewSelectRequest("valid") - req.Index("valid") - - var reqBuf bytes.Buffer - reqEnc := msgpack.NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) - if err != nil { - t.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } - - if resolver.spaceResolverCalls != 1 { - t.Errorf("ResolveSpace was called %d times instead of 1.", - resolver.spaceResolverCalls) - } - if resolver.indexResolverCalls != 1 { - t.Errorf("ResolveIndex was called %d times instead of 1.", - resolver.indexResolverCalls) - } -} - -func TestResolverNotCalledWithNameSupport(t *testing.T) { - resolver.nameUseSupported = true - resolver.spaceResolverCalls = 0 - resolver.indexResolverCalls = 0 - - req := NewSelectRequest("valid") - req.Index("valid") - - var reqBuf bytes.Buffer - reqEnc := msgpack.NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) - if err != nil { - t.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } - - if resolver.spaceResolverCalls != 0 { - t.Errorf("ResolveSpace was called %d times instead of 0.", - resolver.spaceResolverCalls) - } - if resolver.indexResolverCalls != 0 { - t.Errorf("ResolveIndex was called %d times instead of 0.", - resolver.indexResolverCalls) - } -} - func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer @@ -1007,3 +956,110 @@ func TestWatchOnceRequestDefaultValues(t *testing.T) { req := NewWatchOnceRequest(validKey) assertBodyEqual(t, refBuf.Bytes(), req) } + +func TestResponseDecode(t *testing.T) { + header := Header{} + data := bytes.NewBuffer([]byte{'v', '2'}) + baseExample, err := NewPingRequest().Response(header, data) + assert.NoError(t, err) + + tests := []struct { + req Request + expected Response + }{ + {req: NewSelectRequest(validSpace), expected: &SelectResponse{}}, + {req: NewUpdateRequest(validSpace), expected: baseExample}, + {req: NewUpsertRequest(validSpace), expected: baseExample}, + {req: NewInsertRequest(validSpace), expected: baseExample}, + {req: NewReplaceRequest(validSpace), expected: baseExample}, + {req: NewDeleteRequest(validSpace), expected: baseExample}, + {req: NewCallRequest(validExpr), expected: baseExample}, + {req: NewCall16Request(validExpr), expected: baseExample}, + {req: NewCall17Request(validExpr), expected: baseExample}, + {req: NewEvalRequest(validExpr), expected: baseExample}, + {req: NewExecuteRequest(validExpr), expected: &ExecuteResponse{}}, + {req: NewPingRequest(), expected: baseExample}, + {req: NewPrepareRequest(validExpr), expected: &PrepareResponse{}}, + {req: NewUnprepareRequest(validStmt), expected: baseExample}, + {req: NewExecutePreparedRequest(validStmt), expected: &ExecuteResponse{}}, + {req: NewBeginRequest(), expected: baseExample}, + {req: NewCommitRequest(), expected: baseExample}, + {req: NewRollbackRequest(), expected: baseExample}, + {req: NewIdRequest(validProtocolInfo), expected: baseExample}, + {req: NewBroadcastRequest(validKey), expected: baseExample}, + {req: NewWatchOnceRequest(validKey), expected: baseExample}, + } + + for _, test := range tests { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.Encode([]interface{}{'v', '2'}) + + resp, err := test.req.Response(header, bytes.NewBuffer(buf.Bytes())) + assert.NoError(t, err) + assert.True(t, fmt.Sprintf("%T", resp) == + fmt.Sprintf("%T", test.expected)) + assert.Equal(t, header, resp.Header()) + + decodedInterface, err := resp.Decode() + assert.NoError(t, err) + assert.Equal(t, []interface{}{'v', '2'}, decodedInterface) + } +} + +func TestResponseDecodeTyped(t *testing.T) { + header := Header{} + data := bytes.NewBuffer([]byte{'v', '2'}) + baseExample, err := NewPingRequest().Response(header, data) + assert.NoError(t, err) + + tests := []struct { + req Request + expected Response + }{ + {req: NewSelectRequest(validSpace), expected: &SelectResponse{}}, + {req: NewUpdateRequest(validSpace), expected: baseExample}, + {req: NewUpsertRequest(validSpace), expected: baseExample}, + {req: NewInsertRequest(validSpace), expected: baseExample}, + {req: NewReplaceRequest(validSpace), expected: baseExample}, + {req: NewDeleteRequest(validSpace), expected: baseExample}, + {req: NewCallRequest(validExpr), expected: baseExample}, + {req: NewCall16Request(validExpr), expected: baseExample}, + {req: NewCall17Request(validExpr), expected: baseExample}, + {req: NewEvalRequest(validExpr), expected: baseExample}, + {req: NewExecuteRequest(validExpr), expected: &ExecuteResponse{}}, + {req: NewPingRequest(), expected: baseExample}, + {req: NewPrepareRequest(validExpr), expected: &PrepareResponse{}}, + {req: NewUnprepareRequest(validStmt), expected: baseExample}, + {req: NewExecutePreparedRequest(validStmt), expected: &ExecuteResponse{}}, + {req: NewBeginRequest(), expected: baseExample}, + {req: NewCommitRequest(), expected: baseExample}, + {req: NewRollbackRequest(), expected: baseExample}, + {req: NewIdRequest(validProtocolInfo), expected: baseExample}, + {req: NewBroadcastRequest(validKey), expected: baseExample}, + {req: NewWatchOnceRequest(validKey), expected: baseExample}, + } + + for _, test := range tests { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.EncodeBytes([]byte{'v', '2'}) + + resp, err := test.req.Response(header, bytes.NewBuffer(buf.Bytes())) + assert.NoError(t, err) + assert.True(t, fmt.Sprintf("%T", resp) == + fmt.Sprintf("%T", test.expected)) + assert.Equal(t, header, resp.Header()) + + var decoded []byte + err = resp.DecodeTyped(&decoded) + assert.NoError(t, err) + assert.Equal(t, []byte{'v', '2'}, decoded) + } +} diff --git a/response.go b/response.go index 61c77f413..db88c743c 100644 --- a/response.go +++ b/response.go @@ -19,32 +19,31 @@ type Response interface { DecodeTyped(res interface{}) error } -// BaseResponse is a base Response interface implementation. -type BaseResponse struct { +type baseResponse struct { + // header is a response header. header Header // data contains deserialized data for untyped requests. - data []interface{} - buf smallBuf - decoded bool + data []interface{} + buf smallBuf + // Was the Decode() func called for this response. + decoded bool + // Was the DecodeTyped() func called for this response. decodedTyped bool + err error } -func createBaseResponse(header Header, body io.Reader) (BaseResponse, error) { +func createBaseResponse(header Header, body io.Reader) (baseResponse, error) { if body == nil { - return BaseResponse{header: header}, nil + return baseResponse{header: header}, nil } if buf, ok := body.(*smallBuf); ok { - return BaseResponse{header: header, buf: *buf}, nil + return baseResponse{header: header, buf: *buf}, nil } data, err := ioutil.ReadAll(body) if err != nil { - return BaseResponse{}, err + return baseResponse{}, err } - return BaseResponse{header: header, buf: smallBuf{b: data}}, nil -} - -func (resp *BaseResponse) SetHeader(header Header) { - resp.header = header + return baseResponse{header: header, buf: smallBuf{b: data}}, nil } // SelectResponse is used for the select requests. @@ -52,7 +51,7 @@ func (resp *BaseResponse) SetHeader(header Header) { // // You need to cast to SelectResponse a response from SelectRequest. type SelectResponse struct { - BaseResponse + baseResponse // pos contains a position descriptor of last selected tuple. pos []byte } @@ -70,7 +69,7 @@ type PrepareResponse ExecuteResponse // // You need to cast to ExecuteResponse a response from ExecuteRequest. type ExecuteResponse struct { - BaseResponse + baseResponse metaData []ColumnMetaData sqlInfo SQLInfo } @@ -169,39 +168,43 @@ func smallInt(d *msgpack.Decoder, buf *smallBuf) (i int, err error) { return d.DecodeInt() } -func decodeHeader(d *msgpack.Decoder, buf *smallBuf) (Header, error) { +func decodeHeader(d *msgpack.Decoder, buf *smallBuf) (Header, iproto.Type, error) { var l int + var code int var err error d.Reset(buf) if l, err = d.DecodeMapLen(); err != nil { - return Header{}, err + return Header{}, 0, err } - decodedHeader := Header{} + decodedHeader := Header{Error: ErrorNo} for ; l > 0; l-- { var cd int if cd, err = smallInt(d, buf); err != nil { - return Header{}, err + return Header{}, 0, err } switch iproto.Key(cd) { case iproto.IPROTO_SYNC: var rid uint64 if rid, err = d.DecodeUint64(); err != nil { - return Header{}, err + return Header{}, 0, err } decodedHeader.RequestId = uint32(rid) case iproto.IPROTO_REQUEST_TYPE: - var rcode uint64 - if rcode, err = d.DecodeUint64(); err != nil { - return Header{}, err + if code, err = d.DecodeInt(); err != nil { + return Header{}, 0, err + } + if code&int(iproto.IPROTO_TYPE_ERROR) != 0 { + decodedHeader.Error = iproto.Error(code &^ int(iproto.IPROTO_TYPE_ERROR)) + } else { + decodedHeader.Error = ErrorNo } - decodedHeader.Code = uint32(rcode) default: if err = d.Skip(); err != nil { - return Header{}, err + return Header{}, 0, err } } } - return decodedHeader, nil + return decodedHeader, iproto.Type(code), nil } type decodeInfo struct { @@ -213,7 +216,7 @@ type decodeInfo struct { decodedError string } -func (info *decodeInfo) parseData(resp *BaseResponse) error { +func (info *decodeInfo) parseData(resp *baseResponse) error { if info.stmtID != 0 { stmt := &Prepared{ StatementID: PreparedID(info.stmtID), @@ -307,7 +310,11 @@ func decodeCommonField(d *msgpack.Decoder, cd int, data *[]interface{}, return true, nil } -func (resp *BaseResponse) Decode() ([]interface{}, error) { +func (resp *baseResponse) Decode() ([]interface{}, error) { + if resp.decoded { + return resp.data, resp.err + } + resp.decoded = true var err error if resp.buf.Len() > 2 { @@ -323,37 +330,46 @@ func (resp *BaseResponse) Decode() ([]interface{}, error) { }) if l, err = d.DecodeMapLen(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { - return nil, err + resp.err = err + return nil, resp.err } decoded, err := decodeCommonField(d, cd, &resp.data, info) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if !decoded { if err = d.Skip(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } } } err = info.parseData(resp) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + resp.err = Error{resp.header.Error, info.decodedError, + info.errorExtendedInfo} } } - return resp.data, err + return resp.data, resp.err } func (resp *SelectResponse) Decode() ([]interface{}, error) { + if resp.decoded { + return resp.data, resp.err + } + resp.decoded = true var err error if resp.buf.Len() > 2 { @@ -369,44 +385,54 @@ func (resp *SelectResponse) Decode() ([]interface{}, error) { }) if l, err = d.DecodeMapLen(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { - return nil, err + resp.err = err + return nil, resp.err } decoded, err := decodeCommonField(d, cd, &resp.data, info) if err != nil { + resp.err = err return nil, err } if !decoded { switch iproto.Key(cd) { case iproto.IPROTO_POSITION: if resp.pos, err = d.DecodeBytes(); err != nil { - return nil, fmt.Errorf("unable to decode a position: %w", err) + resp.err = err + return nil, fmt.Errorf("unable to decode a position: %w", resp.err) } default: if err = d.Skip(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } } } } - err = info.parseData(&resp.BaseResponse) + err = info.parseData(&resp.baseResponse) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + resp.err = Error{resp.header.Error, info.decodedError, + info.errorExtendedInfo} } } - return resp.data, err + return resp.data, resp.err } func (resp *ExecuteResponse) Decode() ([]interface{}, error) { + if resp.decoded { + return resp.data, resp.err + } + resp.decoded = true var err error if resp.buf.Len() > 2 { @@ -422,45 +448,52 @@ func (resp *ExecuteResponse) Decode() ([]interface{}, error) { }) if l, err = d.DecodeMapLen(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { - return nil, err + resp.err = err + return nil, resp.err } decoded, err := decodeCommonField(d, cd, &resp.data, info) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if !decoded { switch iproto.Key(cd) { case iproto.IPROTO_SQL_INFO: if err = d.Decode(&resp.sqlInfo); err != nil { - return nil, err + resp.err = err + return nil, resp.err } case iproto.IPROTO_METADATA: if err = d.Decode(&resp.metaData); err != nil { - return nil, err + resp.err = err + return nil, resp.err } default: if err = d.Skip(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } } } } - err = info.parseData(&resp.BaseResponse) + err = info.parseData(&resp.baseResponse) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + resp.err = Error{resp.header.Error, info.decodedError, + info.errorExtendedInfo} } } - return resp.data, err + return resp.data, resp.err } func decodeTypedCommonField(d *msgpack.Decoder, res interface{}, cd int, @@ -486,7 +519,7 @@ func decodeTypedCommonField(d *msgpack.Decoder, res interface{}, cd int, return true, nil } -func (resp *BaseResponse) DecodeTyped(res interface{}) error { +func (resp *baseResponse) DecodeTyped(res interface{}) error { resp.decodedTyped = true var err error @@ -521,8 +554,7 @@ func (resp *BaseResponse) DecodeTyped(res interface{}) error { } } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + err = Error{resp.header.Error, info.decodedError, info.errorExtendedInfo} } } return err @@ -570,8 +602,7 @@ func (resp *SelectResponse) DecodeTyped(res interface{}) error { } } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + err = Error{resp.header.Error, info.decodedError, info.errorExtendedInfo} } } return err @@ -623,51 +654,47 @@ func (resp *ExecuteResponse) DecodeTyped(res interface{}) error { } } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + err = Error{resp.header.Error, info.decodedError, info.errorExtendedInfo} } } return err } -func (resp *BaseResponse) Header() Header { +func (resp *baseResponse) Header() Header { return resp.header } // Pos returns a position descriptor of the last selected tuple for the SelectResponse. // If the response was not decoded, this method will call Decode(). func (resp *SelectResponse) Pos() ([]byte, error) { - var err error if !resp.decoded && !resp.decodedTyped { - _, err = resp.Decode() + resp.Decode() } - return resp.pos, err + return resp.pos, resp.err } // MetaData returns ExecuteResponse meta-data. // If the response was not decoded, this method will call Decode(). func (resp *ExecuteResponse) MetaData() ([]ColumnMetaData, error) { - var err error if !resp.decoded && !resp.decodedTyped { - _, err = resp.Decode() + resp.Decode() } - return resp.metaData, err + return resp.metaData, resp.err } // SQLInfo returns ExecuteResponse sql info. // If the response was not decoded, this method will call Decode(). func (resp *ExecuteResponse) SQLInfo() (SQLInfo, error) { - var err error if !resp.decoded && !resp.decodedTyped { - _, err = resp.Decode() + resp.Decode() } - return resp.sqlInfo, err + return resp.sqlInfo, resp.err } // String implements Stringer interface. -func (resp *BaseResponse) String() (str string) { - if resp.header.Code == OkCode { +func (resp *baseResponse) String() (str string) { + if resp.header.Error == ErrorNo { return fmt.Sprintf("<%d OK %v>", resp.header.RequestId, resp.data) } - return fmt.Sprintf("<%d ERR 0x%x>", resp.header.RequestId, resp.header.Code) + return fmt.Sprintf("<%d ERR %s %v>", resp.header.RequestId, resp.header.Error, resp.err) } diff --git a/schema.go b/schema.go index 6066adf49..72b5e397f 100644 --- a/schema.go +++ b/schema.go @@ -380,15 +380,18 @@ func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { return errors.New("unexpected schema format (index fields)") } -// GetSchema returns the actual schema for the connection. -func GetSchema(conn Connector) (Schema, error) { +// GetSchema returns the actual schema for the Doer. +func GetSchema(doer Doer) (Schema, error) { schema := Schema{} schema.SpacesById = make(map[uint32]Space) schema.Spaces = make(map[string]Space) // Reload spaces. var spaces []Space - err := conn.SelectTyped(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &spaces) + req := NewSelectRequest(vspaceSpId). + Index(0). + Limit(maxSchemas) + err := doer.Do(req).GetTyped(&spaces) if err != nil { return Schema{}, err } @@ -399,7 +402,10 @@ func GetSchema(conn Connector) (Schema, error) { // Reload indexes. var indexes []Index - err = conn.SelectTyped(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &indexes) + req = NewSelectRequest(vindexSpId). + Index(0). + Limit(maxSchemas) + err = doer.Do(req).GetTyped(&indexes) if err != nil { return Schema{}, err } diff --git a/schema_test.go b/schema_test.go new file mode 100644 index 000000000..631591cb2 --- /dev/null +++ b/schema_test.go @@ -0,0 +1,162 @@ +package tarantool_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +func TestGetSchema_ok(t *testing.T) { + space1 := tarantool.Space{ + Id: 1, + Name: "name1", + Indexes: make(map[string]tarantool.Index), + IndexesById: make(map[uint32]tarantool.Index), + Fields: make(map[string]tarantool.Field), + FieldsById: make(map[uint32]tarantool.Field), + } + index := tarantool.Index{ + Id: 1, + SpaceId: 2, + Name: "index_name", + Type: "index_type", + Unique: true, + Fields: make([]tarantool.IndexField, 0), + } + space2 := tarantool.Space{ + Id: 2, + Name: "name2", + Indexes: map[string]tarantool.Index{ + "index_name": index, + }, + IndexesById: map[uint32]tarantool.Index{ + 1: index, + }, + Fields: make(map[string]tarantool.Field), + FieldsById: make(map[uint32]tarantool.Field), + } + + mockDoer := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, [][]interface{}{ + { + uint32(1), + "skip", + "name1", + "", + 0, + }, + { + uint32(2), + "skip", + "name2", + "", + 0, + }, + }), + test_helpers.NewMockResponse(t, [][]interface{}{ + { + uint32(2), + uint32(1), + "index_name", + "index_type", + uint8(1), + uint8(0), + }, + }), + ) + + expectedSchema := tarantool.Schema{ + SpacesById: map[uint32]tarantool.Space{ + 1: space1, + 2: space2, + }, + Spaces: map[string]tarantool.Space{ + "name1": space1, + "name2": space2, + }, + } + + schema, err := tarantool.GetSchema(&mockDoer) + require.NoError(t, err) + require.Equal(t, expectedSchema, schema) +} + +func TestGetSchema_spaces_select_error(t *testing.T) { + mockDoer := test_helpers.NewMockDoer(t, fmt.Errorf("some error")) + + schema, err := tarantool.GetSchema(&mockDoer) + require.EqualError(t, err, "some error") + require.Equal(t, tarantool.Schema{}, schema) +} + +func TestGetSchema_index_select_error(t *testing.T) { + mockDoer := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, [][]interface{}{ + { + uint32(1), + "skip", + "name1", + "", + 0, + }, + }), + fmt.Errorf("some error")) + + schema, err := tarantool.GetSchema(&mockDoer) + require.EqualError(t, err, "some error") + require.Equal(t, tarantool.Schema{}, schema) +} + +func TestResolverCalledWithoutNameSupport(t *testing.T) { + resolver := ValidSchemeResolver{nameUseSupported: false} + + req := tarantool.NewSelectRequest("valid") + req.Index("valid") + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) + if err != nil { + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + if resolver.spaceResolverCalls != 1 { + t.Errorf("ResolveSpace was called %d times instead of 1.", + resolver.spaceResolverCalls) + } + if resolver.indexResolverCalls != 1 { + t.Errorf("ResolveIndex was called %d times instead of 1.", + resolver.indexResolverCalls) + } +} + +func TestResolverNotCalledWithNameSupport(t *testing.T) { + resolver := ValidSchemeResolver{nameUseSupported: true} + + req := tarantool.NewSelectRequest("valid") + req.Index("valid") + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) + if err != nil { + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + if resolver.spaceResolverCalls != 0 { + t.Errorf("ResolveSpace was called %d times instead of 0.", + resolver.spaceResolverCalls) + } + if resolver.indexResolverCalls != 0 { + t.Errorf("ResolveIndex was called %d times instead of 0.", + resolver.indexResolverCalls) + } +} diff --git a/stream.go b/stream.go index 5144ea6f1..43e80fc28 100644 --- a/stream.go +++ b/stream.go @@ -199,7 +199,7 @@ func (req *RollbackRequest) Context(ctx context.Context) *RollbackRequest { func (s *Stream) Do(req Request) *Future { if connectedReq, ok := req.(ConnectedRequest); ok { if connectedReq.Conn() != s.Conn { - fut := NewFuture() + fut := NewFuture(req) fut.SetError(errUnknownStreamRequest) return fut } diff --git a/tarantool_test.go b/tarantool_test.go index 3b8bc4653..c3f6b4c0b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -858,7 +858,7 @@ func TestClient(t *testing.T) { // Ping data, err := conn.Ping() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } if data != nil { t.Fatalf("Response data is not nil after Ping") @@ -867,7 +867,7 @@ func TestClient(t *testing.T) { // Insert data, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } if len(data) != 1 { t.Errorf("Response Body len != 1") @@ -896,7 +896,7 @@ func TestClient(t *testing.T) { // Delete data, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(1)}) if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if len(data) != 1 { t.Errorf("Response Body len != 1") @@ -916,7 +916,7 @@ func TestClient(t *testing.T) { } data, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if len(data) != 0 { t.Errorf("Response Data len != 0") @@ -925,14 +925,14 @@ func TestClient(t *testing.T) { // Replace data, err = conn.Replace(spaceNo, []interface{}{uint(2), "hello", "world"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Fatalf("Response is nil after Replace") } data, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { - t.Fatalf("Failed to Replace (duplicate): %s", err.Error()) + t.Fatalf("Failed to Replace (duplicate): %s", err) } if len(data) != 1 { t.Errorf("Response Data len != 1") @@ -955,7 +955,7 @@ func TestClient(t *testing.T) { data, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, NewOperations().Assign(1, "bye").Delete(2, 1)) if err != nil { - t.Fatalf("Failed to Update: %s", err.Error()) + t.Fatalf("Failed to Update: %s", err) } if len(data) != 1 { t.Errorf("Response Data len != 1") @@ -978,7 +978,7 @@ func TestClient(t *testing.T) { data, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (insert): %s", err.Error()) + t.Fatalf("Failed to Upsert (insert): %s", err) } if data == nil { t.Fatalf("Response is nil after Upsert (insert)") @@ -986,7 +986,7 @@ func TestClient(t *testing.T) { data, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (update): %s", err.Error()) + t.Fatalf("Failed to Upsert (update): %s", err) } if data == nil { t.Errorf("Response is nil after Upsert (update)") @@ -996,7 +996,7 @@ func TestClient(t *testing.T) { for i := 10; i < 20; i++ { data, err = conn.Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Errorf("Response is nil after Replace") @@ -1004,7 +1004,7 @@ func TestClient(t *testing.T) { } data, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -1023,7 +1023,7 @@ func TestClient(t *testing.T) { // Select empty data, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}) if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Errorf("Response Data len != 0") @@ -1033,7 +1033,7 @@ func TestClient(t *testing.T) { var tpl []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -1045,7 +1045,7 @@ func TestClient(t *testing.T) { var singleTpl = Tuple{} err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(10)}, &singleTpl) if err != nil { - t.Fatalf("Failed to GetTyped: %s", err.Error()) + t.Fatalf("Failed to GetTyped: %s", err) } if singleTpl.Id != 10 { t.Errorf("Bad value loaded from GetTyped") @@ -1055,7 +1055,7 @@ func TestClient(t *testing.T) { var tpl1 [1]Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl1) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -1067,7 +1067,7 @@ func TestClient(t *testing.T) { var singleTpl2 Tuple err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(30)}, &singleTpl2) if err != nil { - t.Fatalf("Failed to GetTyped: %s", err.Error()) + t.Fatalf("Failed to GetTyped: %s", err) } if singleTpl2.Id != 0 { t.Errorf("Bad value loaded from GetTyped") @@ -1077,7 +1077,7 @@ func TestClient(t *testing.T) { var tpl2 []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl2) != 0 { t.Errorf("Result len of SelectTyped != 1") @@ -1086,7 +1086,7 @@ func TestClient(t *testing.T) { // Call16 data, err = conn.Call16("box.info", []interface{}{"box.schema.SPACE_ID"}) if err != nil { - t.Fatalf("Failed to Call16: %s", err.Error()) + t.Fatalf("Failed to Call16: %s", err) } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -1112,7 +1112,7 @@ func TestClient(t *testing.T) { // Eval data, err = conn.Eval("return 5 + 6", []interface{}{}) if err != nil { - t.Fatalf("Failed to Eval: %s", err.Error()) + t.Fatalf("Failed to Eval: %s", err) } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -1157,7 +1157,7 @@ func TestClientSessionPush(t *testing.T) { // Future.Get ignores push messages. data, err := fut1.Get() if err != nil { - t.Errorf("Failed to Call17: %s", err.Error()) + t.Errorf("Failed to Call17: %s", err) } else if len(data) < 1 { t.Errorf("Response.Data is empty after Call17Async") } else if val, err := test_helpers.ConvertUint64(data[0]); err != nil || val != pushMax { @@ -1185,7 +1185,7 @@ func TestClientSessionPush(t *testing.T) { } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) break } if len(data) < 1 { @@ -1411,20 +1411,20 @@ func TestSQL(t *testing.T) { assert.NoError(t, err, "Failed to Execute, query: %s", test.Query) assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) data, err := resp.Decode() - assert.Nil(t, err, "Failed to Decode") + assert.NoError(t, err, "Failed to Decode") for j := range data { assert.Equal(t, data[j], test.data[j], "Response data is wrong") } exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err := exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") assert.Equal(t, sqlInfo.AffectedCount, test.sqlInfo.AffectedCount, "Affected count is wrong") errorMsg := "Response Metadata is wrong" metaData, err := exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") for j := range metaData { assert.Equal(t, metaData[j], test.metaData[j], errorMsg) } @@ -1510,14 +1510,14 @@ func TestSQLBindings(t *testing.T) { req := NewExecuteRequest(selectNamedQuery2).Args(bind) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with named arguments failed") @@ -1525,7 +1525,7 @@ func TestSQLBindings(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err := exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1537,14 +1537,14 @@ func TestSQLBindings(t *testing.T) { req := NewExecuteRequest(selectPosQuery2).Args(sqlBind5) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") @@ -1552,7 +1552,7 @@ func TestSQLBindings(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err := exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1563,14 +1563,14 @@ func TestSQLBindings(t *testing.T) { req = NewExecuteRequest(mixedQuery).Args(sqlBind6) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err = resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") @@ -1578,7 +1578,7 @@ func TestSQLBindings(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err = exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1596,7 +1596,7 @@ func TestStressSQL(t *testing.T) { req := NewExecuteRequest(createTableQuery) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") @@ -1604,7 +1604,7 @@ func TestStressSQL(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err := exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -1613,7 +1613,7 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(createTableQuery) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") @@ -1624,14 +1624,15 @@ func TestStressSQL(t *testing.T) { tntErr, ok := err.(Error) assert.True(t, ok) assert.Equal(t, iproto.ER_SPACE_EXISTS, tntErr.Code) - if iproto.Error(resp.Header().Code) != iproto.ER_SPACE_EXISTS { - t.Fatalf("Unexpected response code: %d", resp.Header().Code) + if resp.Header().Error != iproto.ER_SPACE_EXISTS { + t.Fatalf("Unexpected response error: %d", resp.Header().Error) } + prevErr := err exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Unexpected error") + assert.Equal(t, prevErr, err) if sqlInfo.AffectedCount != 0 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -1640,12 +1641,12 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(createTableQuery).Args(nil) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Header().Code == OkCode { + if resp.Header().Error == ErrorNo { t.Fatal("Unexpected successful Execute") } exResp, ok = resp.(*ExecuteResponse) @@ -1660,12 +1661,12 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest("") resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Header().Code == OkCode { + if resp.Header().Error == ErrorNo { t.Fatal("Unexpected successful Execute") } exResp, ok = resp.(*ExecuteResponse) @@ -1680,7 +1681,7 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(dropQuery2) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") @@ -1688,7 +1689,7 @@ func TestStressSQL(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of dropped spaces: %d", sqlInfo.AffectedCount) } @@ -1697,12 +1698,12 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(dropQuery2) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Header().Code == OkCode { + if resp.Header().Error == ErrorNo { t.Fatal("Unexpected successful Execute") } _, err = resp.Decode() @@ -1712,7 +1713,9 @@ func TestStressSQL(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + if err == nil { + t.Fatal("Unexpected lack of error") + } if sqlInfo.AffectedCount != 0 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -1738,7 +1741,7 @@ func TestNewPrepared(t *testing.T) { } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, "test"}) { t.Error("Select with named arguments failed") @@ -1746,7 +1749,7 @@ func TestNewPrepared(t *testing.T) { prepResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err := prepResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1797,7 +1800,7 @@ func TestConnection_DoWithStrangerConn(t *testing.T) { " connection or connection pool") conn1 := &Connection{} - req := test_helpers.NewStrangerRequest() + req := test_helpers.NewMockRequest() _, err := conn1.Do(req).Get() if err == nil { @@ -1841,7 +1844,7 @@ func TestConnection_SetSchema_Changes(t *testing.T) { req.Tuple([]interface{}{uint(1010), "Tarantool"}) _, err := conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } s, err := GetSchema(conn) @@ -1858,7 +1861,7 @@ func TestConnection_SetSchema_Changes(t *testing.T) { reqS.Key([]interface{}{uint(1010)}) data, err := conn.Do(reqS).Get() if err != nil { - t.Fatalf("failed to Select: %s", err.Error()) + t.Fatalf("failed to Select: %s", err) } if data[0].([]interface{})[1] != "Tarantool" { t.Errorf("wrong Select body: %v", data) @@ -2051,6 +2054,35 @@ func TestSchema_IsNullable(t *testing.T) { } } +func TestNewPreparedFromResponse(t *testing.T) { + var ( + ErrNilResponsePassed = fmt.Errorf("passed nil response") + ErrNilResponseData = fmt.Errorf("response Data is nil") + ErrWrongDataFormat = fmt.Errorf("response Data format is wrong") + ) + + testConn := &Connection{} + testCases := []struct { + name string + resp Response + expectedError error + }{ + {"ErrNilResponsePassed", nil, ErrNilResponsePassed}, + {"ErrNilResponseData", test_helpers.NewMockResponse(t, nil), + ErrNilResponseData}, + {"ErrWrongDataFormat", test_helpers.NewMockResponse(t, []interface{}{}), + ErrWrongDataFormat}, + {"ErrWrongDataFormat", test_helpers.NewMockResponse(t, []interface{}{"test"}), + ErrWrongDataFormat}, + } + for _, testCase := range testCases { + t.Run("Expecting error "+testCase.name, func(t *testing.T) { + _, err := NewPreparedFromResponse(testConn, testCase.resp) + assert.Equal(t, testCase.expectedError, err) + }) + } +} + func TestClientNamed(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() @@ -2058,7 +2090,7 @@ func TestClientNamed(t *testing.T) { // Insert data, err := conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } if data == nil { t.Errorf("Response is nil after Insert") @@ -2067,7 +2099,7 @@ func TestClientNamed(t *testing.T) { // Delete data, err = conn.Delete(spaceName, indexName, []interface{}{uint(1001)}) if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if data == nil { t.Errorf("Response is nil after Delete") @@ -2076,7 +2108,7 @@ func TestClientNamed(t *testing.T) { // Replace data, err = conn.Replace(spaceName, []interface{}{uint(1002), "hello", "world"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Errorf("Response is nil after Replace") @@ -2088,7 +2120,7 @@ func TestClientNamed(t *testing.T) { uint(1002)}, NewOperations().Assign(1, "buy").Delete(2, 1)) if err != nil { - t.Fatalf("Failed to Update: %s", err.Error()) + t.Fatalf("Failed to Update: %s", err) } if data == nil { t.Errorf("Response is nil after Update") @@ -2098,7 +2130,7 @@ func TestClientNamed(t *testing.T) { data, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (insert): %s", err.Error()) + t.Fatalf("Failed to Upsert (insert): %s", err) } if data == nil { t.Errorf("Response is nil after Upsert (insert)") @@ -2106,7 +2138,7 @@ func TestClientNamed(t *testing.T) { data, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (update): %s", err.Error()) + t.Fatalf("Failed to Upsert (update): %s", err) } if data == nil { t.Errorf("Response is nil after Upsert (update)") @@ -2117,7 +2149,7 @@ func TestClientNamed(t *testing.T) { data, err = conn.Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Errorf("Response is nil after Replace") @@ -2125,7 +2157,7 @@ func TestClientNamed(t *testing.T) { } data, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if data == nil { t.Errorf("Response is nil after Select") @@ -2135,7 +2167,7 @@ func TestClientNamed(t *testing.T) { var tpl []Tuple err = conn.SelectTyped(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}, &tpl) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -2155,7 +2187,7 @@ func TestClientRequestObjects(t *testing.T) { req = NewPingRequest() data, err := conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } if len(data) != 0 { t.Errorf("Response Body len != 0") @@ -2172,7 +2204,7 @@ func TestClientRequestObjects(t *testing.T) { Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } if len(data) != 1 { t.Fatalf("Response Body len != 1") @@ -2201,7 +2233,7 @@ func TestClientRequestObjects(t *testing.T) { Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "blar"}) data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != 1 { t.Fatalf("Response Body len != 1") @@ -2229,7 +2261,7 @@ func TestClientRequestObjects(t *testing.T) { Key([]interface{}{uint(1016)}) data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if data == nil { t.Fatalf("Response data is nil after Delete") @@ -2260,7 +2292,7 @@ func TestClientRequestObjects(t *testing.T) { Key([]interface{}{uint(1010)}) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Update: %s", err.Error()) + t.Errorf("Failed to Update: %s", err) } if data == nil { t.Fatalf("Response data is nil after Update") @@ -2289,7 +2321,7 @@ func TestClientRequestObjects(t *testing.T) { Operations(NewOperations().Assign(1, "bye").Insert(2, 1)) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Update: %s", err.Error()) + t.Errorf("Failed to Update: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2313,7 +2345,7 @@ func TestClientRequestObjects(t *testing.T) { Tuple([]interface{}{uint(1010), "hi", "hi"}) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) + t.Errorf("Failed to Upsert (update): %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2325,7 +2357,7 @@ func TestClientRequestObjects(t *testing.T) { Operations(NewOperations().Assign(2, "bye")) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) + t.Errorf("Failed to Upsert (update): %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2355,7 +2387,7 @@ func TestClientRequestObjects(t *testing.T) { req = NewEvalRequest("return 5 + 6") data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Eval: %s", err.Error()) + t.Fatalf("Failed to Eval: %s", err) } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -2376,14 +2408,14 @@ func TestClientRequestObjects(t *testing.T) { req = NewExecuteRequest(createTableQuery) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err = resp.Decode() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != 0 { t.Fatalf("Response Body len != 0") @@ -2391,7 +2423,7 @@ func TestClientRequestObjects(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err := exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -2399,14 +2431,14 @@ func TestClientRequestObjects(t *testing.T) { req = NewExecuteRequest(dropQuery2) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err = resp.Decode() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != 0 { t.Fatalf("Response Body len != 0") @@ -2414,7 +2446,7 @@ func TestClientRequestObjects(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of dropped spaces: %d", sqlInfo.AffectedCount) } @@ -2437,14 +2469,14 @@ func testConnectionDoSelectRequestCheck(t *testing.T, t.Helper() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if resp == nil { t.Fatalf("Response is nil after Select") } respPos, err := resp.Pos() if err != nil { - t.Errorf("Error while getting Pos: %s", err.Error()) + t.Errorf("Error while getting Pos: %s", err) } if !pos && respPos != nil { t.Errorf("Response should not have a position descriptor") @@ -2454,7 +2486,7 @@ func testConnectionDoSelectRequestCheck(t *testing.T, } data, err := resp.Decode() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != dataLen { t.Fatalf("Response Data len %d != %d", len(data), dataLen) @@ -2608,7 +2640,7 @@ func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { testConnectionDoSelectRequestCheck(t, selResp, err, true, 2, 1010) selPos, err := selResp.Pos() - assert.Nil(t, err, "Error while getting Pos") + assert.NoError(t, err, "Error while getting Pos") resp, err = conn.Do(req.After(selPos)).GetResponse() selResp, ok = resp.(*SelectResponse) @@ -2652,7 +2684,7 @@ func TestClientRequestObjectsWithNilContext(t *testing.T) { req := NewPingRequest().Context(nil) //nolint data, err := conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } if len(data) != 0 { t.Errorf("Response Body len != 0") @@ -2701,9 +2733,8 @@ func (req *waitCtxRequest) Async() bool { } func (req *waitCtxRequest) Response(header Header, body io.Reader) (Response, error) { - resp := BaseResponse{} - resp.SetHeader(header) - return &resp, nil + resp, err := test_helpers.CreateMockResponse(header, body) + return resp, err } func TestClientRequestObjectsWithContext(t *testing.T) { @@ -2753,13 +2784,13 @@ func TestComplexStructs(t *testing.T) { tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} _, err = conn.Replace(spaceNo, &tuple) if err != nil { - t.Fatalf("Failed to insert: %s", err.Error()) + t.Fatalf("Failed to insert: %s", err) } var tuples [1]Tuple2 err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) if err != nil { - t.Fatalf("Failed to selectTyped: %s", err.Error()) + t.Fatalf("Failed to selectTyped: %s", err) } if len(tuples) != 1 { @@ -2801,7 +2832,7 @@ func TestStream_IdValues(t *testing.T) { stream.Id = id _, err := stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } }) } @@ -2823,7 +2854,7 @@ func TestStream_Commit(t *testing.T) { req = NewBeginRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Begin: %s", err.Error()) + t.Fatalf("Failed to Begin: %s", err) } // Insert in stream @@ -2831,7 +2862,7 @@ func TestStream_Commit(t *testing.T) { Tuple([]interface{}{uint(1001), "hello2", "world2"}) _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) @@ -2845,7 +2876,7 @@ func TestStream_Commit(t *testing.T) { Key([]interface{}{uint(1001)}) data, err := conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2854,7 +2885,7 @@ func TestStream_Commit(t *testing.T) { // Select in stream data, err = stream.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2877,13 +2908,13 @@ func TestStream_Commit(t *testing.T) { req = NewCommitRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Commit: %s", err.Error()) + t.Fatalf("Failed to Commit: %s", err) } // Select outside of transaction data, err = conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2919,7 +2950,7 @@ func TestStream_Rollback(t *testing.T) { req = NewBeginRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Begin: %s", err.Error()) + t.Fatalf("Failed to Begin: %s", err) } // Insert in stream @@ -2927,7 +2958,7 @@ func TestStream_Rollback(t *testing.T) { Tuple([]interface{}{uint(1001), "hello2", "world2"}) _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) @@ -2941,7 +2972,7 @@ func TestStream_Rollback(t *testing.T) { Key([]interface{}{uint(1001)}) data, err := conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2950,7 +2981,7 @@ func TestStream_Rollback(t *testing.T) { // Select in stream data, err = stream.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2973,13 +3004,13 @@ func TestStream_Rollback(t *testing.T) { req = NewRollbackRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Rollback: %s", err.Error()) + t.Fatalf("Failed to Rollback: %s", err) } // Select outside of transaction data, err = conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2988,7 +3019,7 @@ func TestStream_Rollback(t *testing.T) { // Select inside of stream after rollback _, err = stream.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -3084,7 +3115,7 @@ func TestStream_DoWithStrangerConn(t *testing.T) { conn := &Connection{} stream, _ := conn.NewStream() - req := test_helpers.NewStrangerRequest() + req := test_helpers.NewMockRequest() _, err := stream.Do(req).Get() if err == nil { diff --git a/test_helpers/doer.go b/test_helpers/doer.go new file mode 100644 index 000000000..c33ff0e69 --- /dev/null +++ b/test_helpers/doer.go @@ -0,0 +1,69 @@ +package test_helpers + +import ( + "bytes" + "testing" + + "github.com/tarantool/go-tarantool/v2" +) + +type doerResponse struct { + resp *MockResponse + err error +} + +// MockDoer is an implementation of the Doer interface +// used for testing purposes. +type MockDoer struct { + // Requests is a slice of received requests. + // It could be used to compare incoming requests with expected. + Requests []tarantool.Request + responses []doerResponse + t *testing.T +} + +// NewMockDoer creates a MockDoer by given responses. +// Each response could be one of two types: MockResponse or error. +func NewMockDoer(t *testing.T, responses ...interface{}) MockDoer { + t.Helper() + + mockDoer := MockDoer{t: t} + for _, response := range responses { + doerResp := doerResponse{} + + switch resp := response.(type) { + case *MockResponse: + doerResp.resp = resp + case error: + doerResp.err = resp + default: + t.Fatalf("unsupported type: %T", response) + } + + mockDoer.responses = append(mockDoer.responses, doerResp) + } + return mockDoer +} + +// Do returns a future with the current response or an error. +// It saves the current request into MockDoer.Requests. +func (doer *MockDoer) Do(req tarantool.Request) *tarantool.Future { + doer.Requests = append(doer.Requests, req) + + mockReq := NewMockRequest() + fut := tarantool.NewFuture(mockReq) + + if len(doer.responses) == 0 { + doer.t.Fatalf("list of responses is empty") + } + response := doer.responses[0] + + if response.err != nil { + fut.SetError(response.err) + } else { + fut.SetResponse(response.resp.header, bytes.NewBuffer(response.resp.data)) + } + doer.responses = doer.responses[1:] + + return fut +} diff --git a/test_helpers/example_test.go b/test_helpers/example_test.go new file mode 100644 index 000000000..6272d737d --- /dev/null +++ b/test_helpers/example_test.go @@ -0,0 +1,36 @@ +package test_helpers_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +func TestExampleMockDoer(t *testing.T) { + mockDoer := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, []interface{}{"some data"}), + fmt.Errorf("some error"), + test_helpers.NewMockResponse(t, "some typed data"), + fmt.Errorf("some error"), + ) + + data, err := mockDoer.Do(tarantool.NewPingRequest()).Get() + assert.NoError(t, err) + assert.Equal(t, []interface{}{"some data"}, data) + + data, err = mockDoer.Do(tarantool.NewSelectRequest("foo")).Get() + assert.EqualError(t, err, "some error") + assert.Nil(t, data) + + var stringData string + err = mockDoer.Do(tarantool.NewInsertRequest("space")).GetTyped(&stringData) + assert.NoError(t, err) + assert.Equal(t, "some typed data", stringData) + + err = mockDoer.Do(tarantool.NewPrepareRequest("expr")).GetTyped(&stringData) + assert.EqualError(t, err, "some error") + assert.Nil(t, data) +} diff --git a/test_helpers/request.go b/test_helpers/request.go new file mode 100644 index 000000000..3756a2b54 --- /dev/null +++ b/test_helpers/request.go @@ -0,0 +1,52 @@ +package test_helpers + +import ( + "context" + "io" + + "github.com/tarantool/go-iproto" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" +) + +// MockRequest is an empty mock request used for testing purposes. +type MockRequest struct { +} + +// NewMockRequest creates an empty MockRequest. +func NewMockRequest() *MockRequest { + return &MockRequest{} +} + +// Type returns an iproto type for MockRequest. +func (req *MockRequest) Type() iproto.Type { + return iproto.Type(0) +} + +// Async returns if MockRequest expects a response. +func (req *MockRequest) Async() bool { + return false +} + +// Body fills an msgpack.Encoder with the watch request body. +func (req *MockRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { + return nil +} + +// Conn returns the Connection object the request belongs to. +func (req *MockRequest) Conn() *tarantool.Connection { + return &tarantool.Connection{} +} + +// Ctx returns a context of the MockRequest. +func (req *MockRequest) Ctx() context.Context { + return nil +} + +// Response creates a response for the MockRequest. +func (req *MockRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + resp, err := CreateMockResponse(header, body) + return resp, err +} diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go deleted file mode 100644 index 980c35db2..000000000 --- a/test_helpers/request_mock.go +++ /dev/null @@ -1,45 +0,0 @@ -package test_helpers - -import ( - "context" - "io" - - "github.com/tarantool/go-iproto" - "github.com/vmihailenco/msgpack/v5" - - "github.com/tarantool/go-tarantool/v2" -) - -type StrangerRequest struct { -} - -func NewStrangerRequest() *StrangerRequest { - return &StrangerRequest{} -} - -func (sr *StrangerRequest) Type() iproto.Type { - return iproto.Type(0) -} - -func (sr *StrangerRequest) Async() bool { - return false -} - -func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { - return nil -} - -func (sr *StrangerRequest) Conn() *tarantool.Connection { - return &tarantool.Connection{} -} - -func (sr *StrangerRequest) Ctx() context.Context { - return nil -} - -func (sr *StrangerRequest) Response(header tarantool.Header, - body io.Reader) (tarantool.Response, error) { - resp := tarantool.BaseResponse{} - resp.SetHeader(header) - return &resp, nil -} diff --git a/test_helpers/response.go b/test_helpers/response.go new file mode 100644 index 000000000..4a28400c0 --- /dev/null +++ b/test_helpers/response.go @@ -0,0 +1,74 @@ +package test_helpers + +import ( + "bytes" + "io" + "io/ioutil" + "testing" + + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" +) + +// MockResponse is a mock response used for testing purposes. +type MockResponse struct { + // header contains response header + header tarantool.Header + // data contains data inside a response. + data []byte +} + +// NewMockResponse creates a new MockResponse with an empty header and the given data. +// body should be passed as a structure to be encoded. +// The encoded body is served as response data and will be decoded once the +// response is decoded. +func NewMockResponse(t *testing.T, body interface{}) *MockResponse { + t.Helper() + + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + err := enc.Encode(body) + if err != nil { + t.Errorf("unexpected error while encoding: %s", err) + } + + return &MockResponse{data: buf.Bytes()} +} + +// CreateMockResponse creates a MockResponse from the header and a data, +// packed inside an io.Reader. +func CreateMockResponse(header tarantool.Header, body io.Reader) (*MockResponse, error) { + if body == nil { + return &MockResponse{header: header, data: nil}, nil + } + data, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + return &MockResponse{header: header, data: data}, nil +} + +// Header returns a header for the MockResponse. +func (resp *MockResponse) Header() tarantool.Header { + return resp.header +} + +// Decode returns the result of decoding the response data as slice. +func (resp *MockResponse) Decode() ([]interface{}, error) { + if resp.data == nil { + return nil, nil + } + dec := msgpack.NewDecoder(bytes.NewBuffer(resp.data)) + return dec.DecodeSlice() +} + +// DecodeTyped returns the result of decoding the response data. +func (resp *MockResponse) DecodeTyped(res interface{}) error { + if resp.data == nil { + return nil + } + dec := msgpack.NewDecoder(bytes.NewBuffer(resp.data)) + return dec.Decode(res) +}