From 569b525a1db9891f115530ae6aae281a63a204b0 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 5 Dec 2022 12:29:59 +0300 Subject: [PATCH 1/4] changelog: remove a dot from the end of an entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3019df4e0..7b2c49dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added -- Support iproto feature discovery (#120). +- Support iproto feature discovery (#120) ### Changed From 594e5a9446711cf2430272b8927f25f13f7e2163 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 11 Nov 2022 18:46:07 +0300 Subject: [PATCH 2/4] code health: rename error iproto code Since 2.4.1 IPROTO_ERROR (0x31) is renamed to IPROTO_ERROR_24 and IPROTO_ERROR name is used for 0x52 constant describing extended error info [1]. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-error-type Part of #209 --- const.go | 2 +- response.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/const.go b/const.go index 35ec83380..8cd9bc490 100644 --- a/const.go +++ b/const.go @@ -35,7 +35,7 @@ const ( KeyExpression = 0x27 KeyDefTuple = 0x28 KeyData = 0x30 - KeyError = 0x31 + KeyError24 = 0x31 KeyMetaData = 0x32 KeyBindCount = 0x34 KeySQLText = 0x40 diff --git a/response.go b/response.go index 9e38e970d..df0b0eca7 100644 --- a/response.go +++ b/response.go @@ -172,7 +172,7 @@ func (resp *Response) decodeBody() (err error) { if resp.Data, ok = res.([]interface{}); !ok { return fmt.Errorf("result is not array: %v", res) } - case KeyError: + case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err } @@ -262,7 +262,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if err = d.Decode(res); err != nil { return err } - case KeyError: + case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err } From 134329742156b9e735b83ba5c9d94d199270fa35 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 23 Nov 2022 15:38:00 +0300 Subject: [PATCH 3/4] api: support errors extended information Since Tarantool 2.4.1, iproto error responses contain extended info with backtrace [1]. After this patch, Error would contain ExtendedInfo field (BoxError object), if it was provided. Error() handle now will print extended info, if possible. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#responses-for-errors Part of #209 --- CHANGELOG.md | 1 + box_error.go | 186 +++++++++++++++++++++++++++++++++++++++ box_error_test.go | 200 ++++++++++++++++++++++++++++++++++++++++++ config.lua | 37 ++++++++ const.go | 3 +- errors.go | 9 +- response.go | 15 +++- tarantool_test.go | 97 ++++++++++++++++++++ test_helpers/utils.go | 56 ++++++++++++ 9 files changed, 599 insertions(+), 5 deletions(-) create mode 100644 box_error.go create mode 100644 box_error_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2c49dab..e870c6a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Support iproto feature discovery (#120) +- Support errors extended information (#209) ### Changed diff --git a/box_error.go b/box_error.go new file mode 100644 index 000000000..97c5c1774 --- /dev/null +++ b/box_error.go @@ -0,0 +1,186 @@ +package tarantool + +import ( + "bytes" + "fmt" +) + +const ( + keyErrorStack = 0x00 + keyErrorType = 0x00 + keyErrorFile = 0x01 + keyErrorLine = 0x02 + keyErrorMessage = 0x03 + keyErrorErrno = 0x04 + keyErrorErrcode = 0x05 + keyErrorFields = 0x06 +) + +// BoxError is a type representing Tarantool `box.error` object: a single +// MP_ERROR_STACK object with a link to the previous stack error. +// See https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/ +// +// Since 1.10.0 +type BoxError struct { + // Type is error type that implies its source (for example, "ClientError"). + Type string + // File is a source code file where the error was caught. + File string + // Line is a number of line in the source code file where the error was caught. + Line uint64 + // Msg is the text of reason. + Msg string + // Errno is the ordinal number of the error. + Errno uint64 + // Code is the number of the error as defined in `errcode.h`. + Code uint64 + // Fields are additional fields depending on error type. For example, if + // type is "AccessDeniedError", then it will include "object_type", + // "object_name", "access_type". + Fields map[string]interface{} + // Prev is the previous error in stack. + Prev *BoxError +} + +// Error converts a BoxError to a string. +func (e *BoxError) Error() string { + s := fmt.Sprintf("%s (%s, code 0x%x), see %s line %d", + e.Msg, e.Type, e.Code, e.File, e.Line) + + if e.Prev != nil { + return fmt.Sprintf("%s: %s", s, e.Prev) + } + + return s +} + +// Depth computes the count of errors in stack, including the current one. +func (e *BoxError) Depth() int { + depth := int(0) + + cur := e + for cur != nil { + cur = cur.Prev + depth++ + } + + return depth +} + +func decodeBoxError(d *decoder) (*BoxError, error) { + var l, larr, l1, l2 int + var errorStack []BoxError + var err error + + if l, err = d.DecodeMapLen(); err != nil { + return nil, err + } + + for ; l > 0; l-- { + var cd int + if cd, err = d.DecodeInt(); err != nil { + return nil, err + } + switch cd { + case keyErrorStack: + if larr, err = d.DecodeArrayLen(); err != nil { + return nil, err + } + + errorStack = make([]BoxError, larr) + + for i := 0; i < larr; i++ { + if l1, err = d.DecodeMapLen(); err != nil { + return nil, err + } + + for ; l1 > 0; l1-- { + var cd1 int + if cd1, err = d.DecodeInt(); err != nil { + return nil, err + } + switch cd1 { + case keyErrorType: + if errorStack[i].Type, err = d.DecodeString(); err != nil { + return nil, err + } + case keyErrorFile: + if errorStack[i].File, err = d.DecodeString(); err != nil { + return nil, err + } + case keyErrorLine: + if errorStack[i].Line, err = d.DecodeUint64(); err != nil { + return nil, err + } + case keyErrorMessage: + if errorStack[i].Msg, err = d.DecodeString(); err != nil { + return nil, err + } + case keyErrorErrno: + if errorStack[i].Errno, err = d.DecodeUint64(); err != nil { + return nil, err + } + case keyErrorErrcode: + if errorStack[i].Code, err = d.DecodeUint64(); err != nil { + return nil, err + } + case keyErrorFields: + var mapk string + var mapv interface{} + + errorStack[i].Fields = make(map[string]interface{}) + + if l2, err = d.DecodeMapLen(); err != nil { + return nil, err + } + for ; l2 > 0; l2-- { + if mapk, err = d.DecodeString(); err != nil { + return nil, err + } + if mapv, err = d.DecodeInterface(); err != nil { + return nil, err + } + errorStack[i].Fields[mapk] = mapv + } + default: + if err = d.Skip(); err != nil { + return nil, err + } + } + } + + if i > 0 { + errorStack[i-1].Prev = &errorStack[i] + } + } + default: + if err = d.Skip(); err != nil { + return nil, err + } + } + } + + if len(errorStack) == 0 { + return nil, fmt.Errorf("msgpack: unexpected empty BoxError stack on decode") + } + + return &errorStack[0], nil +} + +// UnmarshalMsgpack deserializes a BoxError value from a MessagePack +// representation. +func (e *BoxError) UnmarshalMsgpack(b []byte) error { + if e == nil { + panic("cannot unmarshal to a nil pointer") + } + + buf := bytes.NewBuffer(b) + dec := newDecoder(buf) + + if val, err := decodeBoxError(dec); err != nil { + return err + } else { + *e = *val + return nil + } +} diff --git a/box_error_test.go b/box_error_test.go new file mode 100644 index 000000000..76ba3290d --- /dev/null +++ b/box_error_test.go @@ -0,0 +1,200 @@ +package tarantool_test + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" + . "github.com/tarantool/go-tarantool" +) + +var samples = map[string]BoxError{ + "SimpleError": { + Type: "ClientError", + File: "config.lua", + Line: uint64(202), + Msg: "Unknown error", + Errno: uint64(0), + Code: uint64(0), + }, + "AccessDeniedError": { + Type: "AccessDeniedError", + File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c", + Line: uint64(535), + Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'", + Errno: uint64(0), + Code: uint64(42), + Fields: map[string]interface{}{ + "object_type": "function", + "object_name": "forbidden_function", + "access_type": "Execute", + }, + }, + "ChainedError": { + Type: "ClientError", + File: "config.lua", + Line: uint64(205), + Msg: "Timeout exceeded", + Errno: uint64(0), + Code: uint64(78), + Prev: &BoxError{ + Type: "ClientError", + File: "config.lua", + Line: uint64(202), + Msg: "Unknown error", + Errno: uint64(0), + Code: uint64(0), + }, + }, +} + +var stringCases = map[string]struct { + e BoxError + s string +}{ + "SimpleError": { + samples["SimpleError"], + "Unknown error (ClientError, code 0x0), see config.lua line 202", + }, + "AccessDeniedError": { + samples["AccessDeniedError"], + "Execute access to function 'forbidden_function' is denied for user " + + "'no_grants' (AccessDeniedError, code 0x2a), see " + + "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c line 535", + }, + "ChainedError": { + samples["ChainedError"], + "Timeout exceeded (ClientError, code 0x4e), see config.lua line 205: " + + "Unknown error (ClientError, code 0x0), see config.lua line 202", + }, +} + +func TestBoxErrorStringRepr(t *testing.T) { + for name, testcase := range stringCases { + t.Run(name, func(t *testing.T) { + require.Equal(t, testcase.s, testcase.e.Error()) + }) + } +} + +var mpDecodeSamples = map[string]struct { + b []byte + ok bool + err *regexp.Regexp +}{ + "OuterMapInvalidLen": { + []byte{0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`), + }, + "OuterMapInvalidKey": { + []byte{0x81, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`), + }, + "OuterMapExtraKey": { + []byte{0x82, 0x00, 0x91, 0x81, 0x02, 0x01, 0x11, 0x00}, + true, + regexp.MustCompile(``), + }, + "OuterMapExtraInvalidKey": { + []byte{0x81, 0x11, 0x81}, + false, + regexp.MustCompile(`EOF`), + }, + "ArrayInvalidLen": { + []byte{0x81, 0x00, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding array length`), + }, + "ArrayZeroLen": { + []byte{0x81, 0x00, 0x90}, + false, + regexp.MustCompile(`msgpack: unexpected empty BoxError stack on decode`), + }, + "InnerMapInvalidLen": { + []byte{0x81, 0x00, 0x91, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`), + }, + "InnerMapInvalidKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`), + }, + "InnerMapInvalidErrorType": { + []byte{0x81, 0x00, 0x91, 0x81, 0x00, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorFile": { + []byte{0x81, 0x00, 0x91, 0x81, 0x01, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorLine": { + []byte{0x81, 0x00, 0x91, 0x81, 0x02, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`), + }, + "InnerMapInvalidErrorMessage": { + []byte{0x81, 0x00, 0x91, 0x81, 0x03, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorErrno": { + []byte{0x81, 0x00, 0x91, 0x81, 0x04, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`), + }, + "InnerMapInvalidErrorErrcode": { + []byte{0x81, 0x00, 0x91, 0x81, 0x05, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`), + }, + "InnerMapInvalidErrorFields": { + []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`), + }, + "InnerMapInvalidErrorFieldsKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorFieldsValue": { + []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xa3, 0x6b, 0x65, 0x79, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding interface{}`), + }, + "InnerMapExtraKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x00}, + true, + regexp.MustCompile(``), + }, + "InnerMapExtraInvalidKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x81}, + false, + regexp.MustCompile(`EOF`), + }, +} + +func TestMessagePackDecode(t *testing.T) { + for name, testcase := range mpDecodeSamples { + t.Run(name, func(t *testing.T) { + var val *BoxError = &BoxError{} + err := val.UnmarshalMsgpack(testcase.b) + if testcase.ok { + require.Nilf(t, err, "No errors on decode") + } else { + require.Regexp(t, testcase.err, err.Error()) + } + }) + } +} + +func TestMessagePackUnmarshalToNil(t *testing.T) { + var val *BoxError = nil + require.PanicsWithValue(t, "cannot unmarshal to a nil pointer", + func() { val.UnmarshalMsgpack(mpDecodeSamples["InnerMapExtraKey"].b) }) +} diff --git a/config.lua b/config.lua index 5ab98cdfe..c2d52d209 100644 --- a/config.lua +++ b/config.lua @@ -130,6 +130,8 @@ box.once("init", function() -- grants for sql tests box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') box.schema.user.grant('test', 'create', 'sequence') + + box.schema.user.create('no_grants') end) local function func_name() @@ -157,6 +159,41 @@ local function push_func(cnt) end rawset(_G, 'push_func', push_func) +local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch) + -- https://github.com/tarantool/crud/blob/733528be02c1ffa3dacc12c034ee58c9903127fc/test/helper.lua#L316-L337 + local major_minor_patch = _TARANTOOL:split('-', 1)[1] + local major_minor_patch_parts = major_minor_patch:split('.', 2) + + local major = tonumber(major_minor_patch_parts[1]) + local minor = tonumber(major_minor_patch_parts[2]) + local patch = tonumber(major_minor_patch_parts[3]) + + if major < (wanted_major or 0) then return false end + if major > (wanted_major or 0) then return true end + + if minor < (wanted_minor or 0) then return false end + if minor > (wanted_minor or 0) then return true end + + if patch < (wanted_patch or 0) then return false end + if patch > (wanted_patch or 0) then return true end + + return true +end + +if tarantool_version_at_least(2, 4, 1) then + local e1 = box.error.new(box.error.UNKNOWN) + local e2 = box.error.new(box.error.TIMEOUT) + e2:set_prev(e1) + rawset(_G, 'chained_error', e2) + + local user = box.session.user() + box.schema.func.create('forbidden_function', {body = 'function() end'}) + box.session.su('no_grants') + local _, access_denied_error = pcall(function() box.func.forbidden_function:call() end) + box.session.su(user) + rawset(_G, 'access_denied_error', access_denied_error) +end + box.space.test:truncate() --box.schema.user.revoke('guest', 'read,write,execute', 'universe') diff --git a/const.go b/const.go index 8cd9bc490..0dd8d708d 100644 --- a/const.go +++ b/const.go @@ -35,13 +35,14 @@ const ( KeyExpression = 0x27 KeyDefTuple = 0x28 KeyData = 0x30 - KeyError24 = 0x31 + KeyError24 = 0x31 /* Error in pre-2.4 format. */ KeyMetaData = 0x32 KeyBindCount = 0x34 KeySQLText = 0x40 KeySQLBind = 0x41 KeySQLInfo = 0x42 KeyStmtID = 0x43 + KeyError = 0x52 /* Extended error in >= 2.4 format. */ KeyVersion = 0x54 KeyFeatures = 0x55 KeyTimeout = 0x56 diff --git a/errors.go b/errors.go index 5677d07fc..02e4635bb 100644 --- a/errors.go +++ b/errors.go @@ -4,12 +4,17 @@ import "fmt" // Error is wrapper around error returned by Tarantool. type Error struct { - Code uint32 - Msg string + Code uint32 + Msg string + ExtendedInfo *BoxError } // Error converts an Error to a string. func (tnterr Error) Error() string { + if tnterr.ExtendedInfo != nil { + return tnterr.ExtendedInfo.Error() + } + return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) } diff --git a/response.go b/response.go index df0b0eca7..6c3f69c99 100644 --- a/response.go +++ b/response.go @@ -151,6 +151,7 @@ func (resp *Response) decodeBody() (err error) { var stmtID, bindCount uint64 var serverProtocolInfo ProtocolInfo var feature ProtocolFeature + var errorExtendedInfo *BoxError = nil d := newDecoder(&resp.buf) @@ -172,6 +173,10 @@ func (resp *Response) decodeBody() (err error) { if resp.Data, ok = res.([]interface{}); !ok { return fmt.Errorf("result is not array: %v", res) } + case KeyError: + if errorExtendedInfo, err = decodeBoxError(d); err != nil { + return err + } case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err @@ -236,7 +241,7 @@ func (resp *Response) decodeBody() (err error) { if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit - err = Error{resp.Code, resp.Error} + err = Error{resp.Code, resp.Error, errorExtendedInfo} } } return @@ -247,6 +252,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { offset := resp.buf.Offset() defer resp.buf.Seek(offset) + var errorExtendedInfo *BoxError = nil + var l int d := newDecoder(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { @@ -262,6 +269,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if err = d.Decode(res); err != nil { return err } + case KeyError: + if errorExtendedInfo, err = decodeBoxError(d); err != nil { + return err + } case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err @@ -282,7 +293,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit - err = Error{resp.Code, resp.Error} + err = Error{resp.Code, resp.Error, errorExtendedInfo} } } return diff --git a/tarantool_test.go b/tarantool_test.go index 31d287272..0accac5e7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3148,6 +3148,103 @@ func TestConnectionFeatureOptsImmutable(t *testing.T) { require.True(t, connected, "Reconnect success") } +func TestErrorExtendedInfoBasic(t *testing.T) { + test_helpers.SkipIfErrorExtendedInfoUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Eval("not a Lua code", []interface{}{}) + require.NotNilf(t, err, "expected error on invalid Lua code") + + ttErr, ok := err.(Error) + require.Equalf(t, ok, true, "error is built from a Tarantool error") + + expected := BoxError{ + Type: "LuajitError", + File: "eval", + Line: uint64(1), + Msg: "eval:1: unexpected symbol near 'not'", + Errno: uint64(0), + Code: uint64(32), + } + + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) +} + +func TestErrorExtendedInfoStack(t *testing.T) { + test_helpers.SkipIfErrorExtendedInfoUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Eval("error(chained_error)", []interface{}{}) + require.NotNilf(t, err, "expected error on explicit error raise") + + ttErr, ok := err.(Error) + require.Equalf(t, ok, true, "error is built from a Tarantool error") + + expected := BoxError{ + Type: "ClientError", + File: "config.lua", + Line: uint64(214), + Msg: "Timeout exceeded", + Errno: uint64(0), + Code: uint64(78), + Prev: &BoxError{ + Type: "ClientError", + File: "config.lua", + Line: uint64(213), + Msg: "Unknown error", + Errno: uint64(0), + Code: uint64(0), + }, + } + + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) +} + +func TestErrorExtendedInfoFields(t *testing.T) { + test_helpers.SkipIfErrorExtendedInfoUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Eval("error(access_denied_error)", []interface{}{}) + require.NotNilf(t, err, "expected error on forbidden action") + + ttErr, ok := err.(Error) + require.Equalf(t, ok, true, "error is built from a Tarantool error") + + expected := BoxError{ + Type: "AccessDeniedError", + File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c", + Line: uint64(535), + Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'", + Errno: uint64(0), + Code: uint64(42), + Fields: map[string]interface{}{ + "object_type": "function", + "object_name": "forbidden_function", + "access_type": "Execute", + }, + } + + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/test_helpers/utils.go b/test_helpers/utils.go index dff0bb357..be25b5804 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool" ) @@ -120,3 +121,58 @@ func SkipIfIdSupported(t *testing.T) { t.Skip("Skipping test for Tarantool with non-zero protocol version and features") } } + +// CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. +// +// Tarantool errors are not comparable by nature: +// +// tarantool> msgpack.decode(mp_error_repr) == msgpack.decode(mp_error_repr) +// --- +// - false +// ... +// +// Tarantool error file and line could differ even between +// different patches. +// +// So we check equivalence of all attributes except for Line and File. +// For Line and File, we check that they are filled with some non-default values +// (lines are counted starting with 1 and empty file path is not expected too). +func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual tarantool.BoxError) { + t.Helper() + + require.Equalf(t, expected.Depth(), actual.Depth(), "Error stack depth is the same") + + for { + require.Equal(t, expected.Type, actual.Type) + require.Greater(t, len(expected.File), 0) + require.Greater(t, expected.Line, uint64(0)) + require.Equal(t, expected.Msg, actual.Msg) + require.Equal(t, expected.Errno, actual.Errno) + require.Equal(t, expected.Code, actual.Code) + require.Equal(t, expected.Fields, actual.Fields) + + if expected.Prev != nil { + // Stack depth is the same + expected = *expected.Prev + actual = *actual.Prev + } else { + break + } + } +} + +// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without +// IPROTO_ERROR (0x52) support is used. +func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { + t.Helper() + + // Tarantool provides extended error info only since 2.4.1 version. + isLess, err := IsTarantoolVersionLess(2, 4, 1) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if isLess { + t.Skip("Skipping test for Tarantool without error extended info support") + } +} From 11970e1eac03f37a1ce378eb09858c12259ade2f Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 28 Nov 2022 17:02:32 +0300 Subject: [PATCH 4/4] api: support error type in MessagePack Tarantool supports error extension type since version 2.4.1 [1], encoding was introduced in Tarantool 2.10.0 [2]. This patch introduces the support of Tarantool error extension type in msgpack decoders and encoders. Tarantool error extension type objects are decoded to `*tarantool.BoxError` type. `*tarantool.BoxError` may be encoded to Tarantool error extension type objects. Error extension type internals are the same as errors extended information: the only difference is that extra information is encoded as a separate error dictionary field and error extension type objects are encoded as MessagePack extension type objects. The only way to receive an error extension type object from Tarantool is to receive an explicitly built `box.error` object: either from `return box.error.new(...)` or a tuple with it. All errors raised within Tarantool (including those raised with `box.error(...)`) are encoded based on the same rules as simple errors due to backward compatibility. It is possible to create error extension type objects with Go code, but it not likely to be really useful since most of their fields is computed on error initialization on the server side (even for custom error types). This patch also adds ErrorExtensionFeature flag to client protocol features list. Without this flag, all `box.error` object sent over iproto are encoded to string. We behave like Tarantool `net.box` here: if we support the feature, we provide the feature flag. Since it may become too complicated to enable/disable feature flag through import, error extension type is available as a part of the base package, in contrary to Decimal, UUID, Datetime and Interval types which are enabled by importing underscore subpackage. 1. tarantool/tarantool#4398 2. tarantool/tarantool#6433 Closes #209 --- CHANGELOG.md | 1 + box_error.go | 113 +++++++++++++++ box_error_test.go | 293 ++++++++++++++++++++++++++++++++++++++ config.lua | 67 +++++++++ example_test.go | 2 +- msgpack.go | 4 + msgpack_helper_test.go | 14 ++ msgpack_v5.go | 4 + msgpack_v5_helper_test.go | 17 +++ protocol.go | 7 +- tarantool_test.go | 16 ++- test_helpers/utils.go | 17 +++ 12 files changed, 548 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e870c6a9d..0585e129f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support iproto feature discovery (#120) - Support errors extended information (#209) +- Error type support in MessagePack (#209) ### Changed diff --git a/box_error.go b/box_error.go index 97c5c1774..ab8981b0b 100644 --- a/box_error.go +++ b/box_error.go @@ -5,6 +5,8 @@ import ( "fmt" ) +const errorExtID = 3 + const ( keyErrorStack = 0x00 keyErrorType = 0x00 @@ -167,6 +169,105 @@ func decodeBoxError(d *decoder) (*BoxError, error) { return &errorStack[0], nil } +func encodeBoxError(enc *encoder, boxError *BoxError) error { + if boxError == nil { + return fmt.Errorf("msgpack: unexpected nil BoxError on encode") + } + + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := encodeUint(enc, keyErrorStack); err != nil { + return err + } + + var stackDepth = boxError.Depth() + if err := enc.EncodeArrayLen(stackDepth); err != nil { + return err + } + + for ; stackDepth > 0; stackDepth-- { + fieldsLen := len(boxError.Fields) + + if fieldsLen > 0 { + if err := enc.EncodeMapLen(7); err != nil { + return err + } + } else { + if err := enc.EncodeMapLen(6); err != nil { + return err + } + } + + if err := encodeUint(enc, keyErrorType); err != nil { + return err + } + if err := enc.EncodeString(boxError.Type); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorFile); err != nil { + return err + } + if err := enc.EncodeString(boxError.File); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorLine); err != nil { + return err + } + if err := enc.EncodeUint64(boxError.Line); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorMessage); err != nil { + return err + } + if err := enc.EncodeString(boxError.Msg); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorErrno); err != nil { + return err + } + if err := enc.EncodeUint64(boxError.Errno); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorErrcode); err != nil { + return err + } + if err := enc.EncodeUint64(boxError.Code); err != nil { + return err + } + + if fieldsLen > 0 { + if err := encodeUint(enc, keyErrorFields); err != nil { + return err + } + + if err := enc.EncodeMapLen(fieldsLen); err != nil { + return err + } + + for k, v := range boxError.Fields { + if err := enc.EncodeString(k); err != nil { + return err + } + if err := enc.Encode(v); err != nil { + return err + } + } + } + + if stackDepth > 1 { + boxError = boxError.Prev + } + } + + return nil +} + // UnmarshalMsgpack deserializes a BoxError value from a MessagePack // representation. func (e *BoxError) UnmarshalMsgpack(b []byte) error { @@ -184,3 +285,15 @@ func (e *BoxError) UnmarshalMsgpack(b []byte) error { return nil } } + +// MarshalMsgpack serializes the BoxError into a MessagePack representation. +func (e *BoxError) MarshalMsgpack() ([]byte, error) { + var buf bytes.Buffer + + enc := newEncoder(&buf) + if err := encodeBoxError(enc, e); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/box_error_test.go b/box_error_test.go index 76ba3290d..276ca2cf8 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -1,11 +1,13 @@ package tarantool_test import ( + "fmt" "regexp" "testing" "github.com/stretchr/testify/require" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) var samples = map[string]BoxError{ @@ -198,3 +200,294 @@ func TestMessagePackUnmarshalToNil(t *testing.T) { require.PanicsWithValue(t, "cannot unmarshal to a nil pointer", func() { val.UnmarshalMsgpack(mpDecodeSamples["InnerMapExtraKey"].b) }) } + +func TestMessagePackEncodeNil(t *testing.T) { + var val *BoxError + + _, err := val.MarshalMsgpack() + require.NotNil(t, err) + require.Equal(t, "msgpack: unexpected nil BoxError on encode", err.Error()) +} + +var space = "test_error_type" +var index = "primary" + +type TupleBoxError struct { + pk string // BoxError cannot be used as a primary key. + val BoxError +} + +func (t *TupleBoxError) EncodeMsgpack(e *encoder) error { + if err := e.EncodeArrayLen(2); err != nil { + return err + } + + if err := e.EncodeString(t.pk); err != nil { + return err + } + + return e.Encode(&t.val) +} + +func (t *TupleBoxError) DecodeMsgpack(d *decoder) error { + var err error + var l int + if l, err = d.DecodeArrayLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("Array length doesn't match: %d", l) + } + + if t.pk, err = d.DecodeString(); err != nil { + return err + } + + return d.Decode(&t.val) +} + +// Raw bytes encoding test is impossible for +// object with Fields since map iterating is random. +var tupleCases = map[string]struct { + tuple TupleBoxError + ttObj string +}{ + "SimpleError": { + TupleBoxError{ + "simple_error_pk", + samples["SimpleError"], + }, + "simple_error", + }, + "AccessDeniedError": { + TupleBoxError{ + "access_denied_error_pk", + samples["AccessDeniedError"], + }, + "access_denied_error", + }, + "ChainedError": { + TupleBoxError{ + "chained_error_pk", + samples["ChainedError"], + }, + "chained_error", + }, +} + +func TestErrorTypeMPEncodeDecode(t *testing.T) { + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + buf, err := marshal(&testcase.tuple) + require.Nil(t, err) + + var res TupleBoxError + err = unmarshal(buf, &res) + require.Nil(t, err) + + require.Equal(t, testcase.tuple, res) + }) + } +} + +func TestErrorTypeEval(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + resp, err := conn.Eval("return ...", []interface{}{&testcase.tuple.val}) + require.Nil(t, err) + require.NotNil(t, resp.Data) + require.Equal(t, len(resp.Data), 1) + actual, ok := toBoxError(resp.Data[0]) + require.Truef(t, ok, "Response data has valid type") + require.Equal(t, testcase.tuple.val, actual) + }) + } +} + +func TestErrorTypeEvalTyped(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + var res []BoxError + err := conn.EvalTyped("return ...", []interface{}{&testcase.tuple.val}, &res) + require.Nil(t, err) + require.NotNil(t, res) + require.Equal(t, len(res), 1) + require.Equal(t, testcase.tuple.val, res[0]) + }) + } +} + +func TestErrorTypeInsert(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + _, err = conn.Insert(space, &testcase.tuple) + require.Nil(t, err) + + checkEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:get(%q) + assert(tuple ~= nil) + + local tuple_err = tuple[2] + assert(tuple_err ~= nil) + + return compare_box_errors(err, tuple_err) + `, testcase.ttObj, space, testcase.tuple.pk) + + // In fact, compare_box_errors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + _, err := conn.Eval(checkEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + }) + } +} + +func TestErrorTypeInsertTyped(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + var res []TupleBoxError + err = conn.InsertTyped(space, &testcase.tuple, &res) + require.Nil(t, err) + require.NotNil(t, res) + require.Equal(t, len(res), 1) + require.Equal(t, testcase.tuple, res[0]) + + checkEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:get(%q) + assert(tuple ~= nil) + + local tuple_err = tuple[2] + assert(tuple_err ~= nil) + + return compare_box_errors(err, tuple_err) + `, testcase.ttObj, space, testcase.tuple.pk) + + // In fact, compare_box_errors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + _, err := conn.Eval(checkEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + }) + } +} + +func TestErrorTypeSelect(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + insertEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:insert{%q, err} + assert(tuple ~= nil) + `, testcase.ttObj, space, testcase.tuple.pk) + + _, err := conn.Eval(insertEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + + var resp *Response + var offset uint32 = 0 + var limit uint32 = 1 + resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}) + require.Nil(t, err) + require.NotNil(t, resp.Data) + require.Equalf(t, len(resp.Data), 1, "Exactly one tuple had been found") + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "Tuple has valid type") + require.Equal(t, testcase.tuple.pk, tpl[0]) + var actual BoxError + actual, ok = toBoxError(tpl[1]) + require.Truef(t, ok, "BoxError tuple field has valid type") + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, testcase.tuple.val, actual) + }) + } +} + +func TestErrorTypeSelectTyped(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + insertEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:insert{%q, err} + assert(tuple ~= nil) + `, testcase.ttObj, space, testcase.tuple.pk) + + _, err := conn.Eval(insertEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + + var offset uint32 = 0 + var limit uint32 = 1 + var resp []TupleBoxError + err = conn.SelectTyped(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}, &resp) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equalf(t, len(resp), 1, "Exactly one tuple had been found") + require.Equal(t, testcase.tuple.pk, resp[0].pk) + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, testcase.tuple.val, resp[0].val) + }) + } +} diff --git a/config.lua b/config.lua index c2d52d209..bb976ef43 100644 --- a/config.lua +++ b/config.lua @@ -116,6 +116,21 @@ box.once("init", function() } end + local s = box.schema.space.create('test_error_type', { + id = 522, + temporary = true, + if_not_exists = true, + field_count = 2, + -- You can't specify box.error as format type, + -- but can put box.error objects. + }) + s:create_index('primary', { + type = 'tree', + unique = true, + parts = {1, 'string'}, + if_not_exists = true + }) + --box.schema.user.grant('guest', 'read,write,execute', 'universe') box.schema.func.create('box.info') box.schema.func.create('simple_concat') @@ -126,6 +141,7 @@ box.once("init", function() box.schema.user.grant('test', 'read,write', 'space', 'test') box.schema.user.grant('test', 'read,write', 'space', 'schematest') box.schema.user.grant('test', 'read,write', 'space', 'test_perf') + box.schema.user.grant('test', 'read,write', 'space', 'test_error_type') -- grants for sql tests box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') @@ -182,6 +198,8 @@ end if tarantool_version_at_least(2, 4, 1) then local e1 = box.error.new(box.error.UNKNOWN) + rawset(_G, 'simple_error', e1) + local e2 = box.error.new(box.error.TIMEOUT) e2:set_prev(e1) rawset(_G, 'chained_error', e2) @@ -192,6 +210,55 @@ if tarantool_version_at_least(2, 4, 1) then local _, access_denied_error = pcall(function() box.func.forbidden_function:call() end) box.session.su(user) rawset(_G, 'access_denied_error', access_denied_error) + + -- cdata structure is as follows: + -- + -- tarantool> err:unpack() + -- - code: val + -- base_type: val + -- type: val + -- message: val + -- field1: val + -- field2: val + -- trace: + -- - file: val + -- line: val + + local function compare_box_error_attributes(expected, actual, attr_provider) + for attr, _ in pairs(attr_provider:unpack()) do + if (attr ~= 'prev') and (attr ~= 'trace') then + if expected[attr] ~= actual[attr] then + error(('%s expected %s is not equal to actual %s'):format( + attr, expected[attr], actual[attr])) + end + end + end + end + + local function compare_box_errors(expected, actual) + if (expected == nil) and (actual ~= nil) then + error(('Expected error stack is empty, but actual error ' .. + 'has previous %s (%s) error'):format( + actual.type, actual.message)) + end + + if (expected ~= nil) and (actual == nil) then + error(('Actual error stack is empty, but expected error ' .. + 'has previous %s (%s) error'):format( + expected.type, expected.message)) + end + + compare_box_error_attributes(expected, actual, expected) + compare_box_error_attributes(expected, actual, actual) + + if (expected.prev ~= nil) or (actual.prev ~= nil) then + return compare_box_errors(expected.prev, actual.prev) + end + + return true + end + + rawset(_G, 'compare_box_errors', compare_box_errors) end box.space.test:truncate() diff --git a/example_test.go b/example_test.go index 15574d099..54202eb46 100644 --- a/example_test.go +++ b/example_test.go @@ -329,7 +329,7 @@ func ExampleProtocolVersion() { fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) // Output: // Connector client protocol version: 4 - // Connector client protocol features: [StreamsFeature TransactionsFeature] + // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature] } func getTestTxnOpts() tarantool.Opts { diff --git a/msgpack.go b/msgpack.go index 34ecc4b3b..9977e9399 100644 --- a/msgpack.go +++ b/msgpack.go @@ -48,3 +48,7 @@ func msgpackIsString(code byte) bool { return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || code == msgpcode.Str16 || code == msgpcode.Str32 } + +func init() { + msgpack.RegisterExt(errorExtID, &BoxError{}) +} diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go index fa47c2fda..896c105d3 100644 --- a/msgpack_helper_test.go +++ b/msgpack_helper_test.go @@ -4,6 +4,7 @@ package tarantool_test import ( + "github.com/tarantool/go-tarantool" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -13,3 +14,16 @@ type decoder = msgpack.Decoder func encodeUint(e *encoder, v uint64) error { return e.EncodeUint(uint(v)) } + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + v, ok = i.(tarantool.BoxError) + return +} + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/msgpack_v5.go b/msgpack_v5.go index 806dd1632..e8cd9aa29 100644 --- a/msgpack_v5.go +++ b/msgpack_v5.go @@ -52,3 +52,7 @@ func msgpackIsString(code byte) bool { return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || code == msgpcode.Str16 || code == msgpcode.Str32 } + +func init() { + msgpack.RegisterExt(errorExtID, (*BoxError)(nil)) +} diff --git a/msgpack_v5_helper_test.go b/msgpack_v5_helper_test.go index 347c1ba95..88154c26f 100644 --- a/msgpack_v5_helper_test.go +++ b/msgpack_v5_helper_test.go @@ -4,6 +4,7 @@ package tarantool_test import ( + "github.com/tarantool/go-tarantool" "github.com/vmihailenco/msgpack/v5" ) @@ -13,3 +14,19 @@ type decoder = msgpack.Decoder func encodeUint(e *encoder, v uint64) error { return e.EncodeUint(v) } + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + var ptr *tarantool.BoxError + if ptr, ok = i.(*tarantool.BoxError); ok { + v = *ptr + } + return +} + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/protocol.go b/protocol.go index 1eaf60e2b..fa890fdd4 100644 --- a/protocol.go +++ b/protocol.go @@ -42,7 +42,7 @@ const ( // (unsupported by connector). ErrorExtensionFeature ProtocolFeature = 2 // WatchersFeature represents support of watchers - // (unsupported by connector). + // (supported by connector). WatchersFeature ProtocolFeature = 3 // PaginationFeature represents support of pagination // (unsupported by connector). @@ -76,10 +76,13 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // 1.10.0. Version: ProtocolVersion(4), // Streams and transactions were introduced in protocol version 1 - // (Tarantool 2.10.0), in connector since 1.7.0. + // (Tarantool 2.10.0), in connector since 1.7.0. Error extension + // type was introduced in protocol version 2 (Tarantool 2.10.0), + // in connector since 1.10.0. Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, + ErrorExtensionFeature, }, } diff --git a/tarantool_test.go b/tarantool_test.go index 0accac5e7..202c10b17 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2868,8 +2868,12 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { require.Equal(t, clientProtocolInfo, ProtocolInfo{ - Version: ProtocolVersion(4), - Features: []ProtocolFeature{StreamsFeature, TransactionsFeature}, + Version: ProtocolVersion(4), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + }, }) serverProtocolInfo := conn.ServerProtocolInfo() @@ -2997,8 +3001,12 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { require.Equal(t, clientProtocolInfo, ProtocolInfo{ - Version: ProtocolVersion(4), - Features: []ProtocolFeature{StreamsFeature, TransactionsFeature}, + Version: ProtocolVersion(4), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + }, }) serverProtocolInfo := conn.ServerProtocolInfo() diff --git a/test_helpers/utils.go b/test_helpers/utils.go index be25b5804..5081de6c2 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -176,3 +176,20 @@ func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { t.Skip("Skipping test for Tarantool without error extended info support") } } + +// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without +// MP_ERROR type over iproto support is used. +func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { + t.Helper() + + // Tarantool error type over MessagePack supported only since 2.10.0 version. + isLess, err := IsTarantoolVersionLess(2, 10, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if isLess { + t.Skip("Skipping test for Tarantool without support of error type over MessagePack") + t.Skip("Skipping test for Tarantool without error extended info support") + } +}