Skip to content

Commit

Permalink
api: add ability to mock connections in tests
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DerekBum authored and oleg-jukovec committed Jan 25, 2024
1 parent c59e514 commit 514b0d0
Show file tree
Hide file tree
Showing 25 changed files with 1,027 additions and 348 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -248,6 +252,8 @@ 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.
* `ErrorNo` constant is added to indicate that no error has occurred while
getting the response. It should be used instead of the removed `OkCode`.

#### Request changes

Expand Down Expand Up @@ -285,9 +291,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

Expand All @@ -304,8 +311,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
Expand Down
13 changes: 6 additions & 7 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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{
Expand Down Expand Up @@ -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{}{}:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 7 additions & 1 deletion connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
35 changes: 24 additions & 11 deletions dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
61 changes: 55 additions & 6 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}]
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 514b0d0

Please sign in to comment.