implement simple belongs_to tag

This commit is contained in:
Lunny Xiao 2017-03-14 22:25:10 +08:00
parent 43222cbcaf
commit 557d5a4101
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
10 changed files with 359 additions and 76 deletions

149
blongs_to_test.go Normal file
View File

@ -0,0 +1,149 @@
package xorm
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBelongsTo_Get(t *testing.T) {
assert.NoError(t, prepareEngine())
type Face struct {
Id int64
Name string
}
type Nose struct {
Id int64
Face Face `xorm:"belongs_to"`
}
err := testEngine.Sync2(new(Nose), new(Face))
assert.NoError(t, err)
var face = Face{
Name: "face1",
}
_, err = testEngine.Insert(&face)
assert.NoError(t, err)
var cfgFace Face
has, err := testEngine.Get(&cfgFace)
assert.NoError(t, err)
assert.Equal(t, true, has)
assert.Equal(t, cfgFace, face)
var nose = Nose{Face: face}
_, err = testEngine.Insert(&nose)
assert.NoError(t, err)
var cfgNose Nose
has, err = testEngine.Get(&cfgNose)
assert.NoError(t, err)
assert.Equal(t, true, has)
assert.Equal(t, cfgNose.Id, nose.Id)
// FIXME: the id should be set back to the field
//assert.Equal(t, cfgNose.Face.Id, nose.Face.Id)
assert.Equal(t, "", cfgNose.Face.Name)
var cfgNose2 Nose
has, err = testEngine.Cascade().Get(&cfgNose2)
assert.NoError(t, err)
assert.Equal(t, true, has)
assert.Equal(t, cfgNose2.Id, nose.Id)
assert.Equal(t, cfgNose2.Face.Id, nose.Face.Id)
assert.Equal(t, "face1", cfgNose2.Face.Name)
}
func TestBelongsTo_GetPtr(t *testing.T) {
assert.NoError(t, prepareEngine())
type Face struct {
Id int64
Name string
}
type Nose struct {
Id int64
Face *Face `xorm:"belongs_to"`
}
err := testEngine.Sync2(new(Nose), new(Face))
assert.NoError(t, err)
var face = Face{
Name: "face1",
}
_, err = testEngine.Insert(&face)
assert.NoError(t, err)
var cfgFace Face
has, err := testEngine.Get(&cfgFace)
assert.NoError(t, err)
assert.Equal(t, true, has)
assert.Equal(t, cfgFace, face)
var nose = Nose{Face: &face}
_, err = testEngine.Insert(&nose)
assert.NoError(t, err)
var cfgNose Nose
has, err = testEngine.Get(&cfgNose)
assert.NoError(t, err)
assert.Equal(t, true, has)
assert.Equal(t, cfgNose.Id, nose.Id)
// FIXME: the id should be set back to the field
//assert.Equal(t, cfgNose.Face.Id, nose.Face.Id)
var cfgNose2 Nose
has, err = testEngine.Cascade().Get(&cfgNose2)
assert.NoError(t, err)
assert.Equal(t, true, has)
assert.Equal(t, cfgNose2.Id, nose.Id)
assert.Equal(t, cfgNose2.Face.Id, nose.Face.Id)
assert.Equal(t, "face1", cfgNose2.Face.Name)
}
func TestBelongsTo_Find(t *testing.T) {
assert.NoError(t, prepareEngine())
type Face struct {
Id int64
Name string
}
type Nose struct {
Id int64
Face Face `xorm:"belongs_to"`
}
err := testEngine.Sync2(new(Nose), new(Face))
assert.NoError(t, err)
var face1 = Face{
Name: "face1",
}
var face2 = Face{
Name: "face2",
}
_, err = testEngine.Insert(&face1, &face2)
assert.NoError(t, err)
var noses = []Nose{
{Face: face1},
{Face: face2},
}
_, err = testEngine.Insert(&noses)
assert.NoError(t, err)
var noses1 []Nose
err = testEngine.Find(&noses1)
assert.NoError(t, err)
assert.Equal(t, 2, len(noses1))
// FIXME:
//assert.Equal(t, face1.Id, noses1[0].Face.Id)
//assert.Equal(t, face2.Id, noses1[1].Face.Id)
assert.Equal(t, "", noses1[0].Face.Name)
assert.Equal(t, "", noses1[1].Face.Name)
}

View File

@ -811,7 +811,7 @@ func (db *postgres) SqlType(c *core.Column) string {
case core.NVarchar: case core.NVarchar:
res = core.Varchar res = core.Varchar
case core.Uuid: case core.Uuid:
res = core.Uuid return core.Uuid
case core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob: case core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob:
return core.Bytea return core.Bytea
case core.Double: case core.Double:

101
engine.go
View File

@ -9,7 +9,6 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"encoding/gob" "encoding/gob"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -21,6 +20,7 @@ import (
"github.com/go-xorm/builder" "github.com/go-xorm/builder"
"github.com/go-xorm/core" "github.com/go-xorm/core"
"github.com/pkg/errors"
) )
// Engine is the major struct of xorm, it means a database manager. // Engine is the major struct of xorm, it means a database manager.
@ -792,13 +792,18 @@ func (engine *Engine) UnMapType(t reflect.Type) {
} }
func (engine *Engine) autoMapType(v reflect.Value) (*core.Table, error) { func (engine *Engine) autoMapType(v reflect.Value) (*core.Table, error) {
t := v.Type()
engine.mutex.Lock() engine.mutex.Lock()
defer engine.mutex.Unlock() defer engine.mutex.Unlock()
return engine.autoMapTypeNoLock(v)
}
func (engine *Engine) autoMapTypeNoLock(v reflect.Value) (*core.Table, error) {
t := v.Type()
table, ok := engine.Tables[t] table, ok := engine.Tables[t]
if !ok { if !ok {
var err error var err error
table, err = engine.mapType(v) var parsingTables = make(map[reflect.Type]*core.Table)
table, err = engine.mapType(parsingTables, v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -872,9 +877,17 @@ var (
tpTableName = reflect.TypeOf((*TableName)(nil)).Elem() tpTableName = reflect.TypeOf((*TableName)(nil)).Elem()
) )
func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { func (engine *Engine) mapType(parsingTables map[reflect.Type]*core.Table, v reflect.Value) (*core.Table, error) {
if v.Kind() != reflect.Struct {
return nil, errors.New("need a struct to map")
}
t := v.Type() t := v.Type()
if table, ok := parsingTables[t]; ok {
return table, nil
}
table := engine.newTable() table := engine.newTable()
parsingTables[t] = table
if tb, ok := v.Interface().(TableName); ok { if tb, ok := v.Interface().(TableName); ok {
table.Name = tb.TableName() table.Name = tb.TableName()
} else { } else {
@ -901,24 +914,37 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) {
fieldValue := v.Field(i) fieldValue := v.Field(i)
fieldType := fieldValue.Type() fieldType := fieldValue.Type()
if ormTagStr != "" { var ctx = tagContext{
col = &core.Column{FieldName: t.Field(i).Name, Nullable: true, IsPrimaryKey: false, engine: engine,
IsAutoIncrement: false, MapType: core.TWOSIDES, Indexes: make(map[string]int)} parsingTables: parsingTables,
tags := splitTag(ormTagStr)
table: table,
hasCacheTag: false,
hasNoCacheTag: false,
fieldValue: fieldValue,
indexNames: make(map[string]int),
isIndex: false,
isUnique: false,
}
if ormTagStr != "" {
col = &core.Column{
FieldName: t.Field(i).Name,
Nullable: true,
IsPrimaryKey: false,
IsAutoIncrement: false,
MapType: core.TWOSIDES,
Indexes: make(map[string]int),
}
ctx.col = col
tags := splitTag(ormTagStr)
if len(tags) > 0 { if len(tags) > 0 {
if tags[0] == "-" { if tags[0] == "-" {
continue continue
} }
var ctx = tagContext{
table: table,
col: col,
fieldValue: fieldValue,
indexNames: make(map[string]int),
engine: engine,
}
if strings.ToUpper(tags[0]) == "EXTENDS" { if strings.ToUpper(tags[0]) == "EXTENDS" {
if err := ExtendsTagHandler(&ctx); err != nil { if err := ExtendsTagHandler(&ctx); err != nil {
return nil, err return nil, err
@ -1014,20 +1040,55 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) {
} else { } else {
sqlType = core.Type2SQLType(fieldType) sqlType = core.Type2SQLType(fieldType)
} }
col = core.NewColumn(engine.ColumnMapper.Obj2Table(t.Field(i).Name),
t.Field(i).Name, sqlType, sqlType.DefaultLength, col = core.NewColumn(
sqlType.DefaultLength2, true) engine.ColumnMapper.Obj2Table(t.Field(i).Name),
t.Field(i).Name,
sqlType,
sqlType.DefaultLength,
sqlType.DefaultLength2,
true,
)
if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) {
idFieldColName = col.Name idFieldColName = col.Name
} }
ctx.col = col
} }
if col.IsAutoIncrement { if col.IsAutoIncrement {
col.Nullable = false col.Nullable = false
} }
table.AddColumn(col) var tp = fieldType
if isPtrStruct(fieldType) {
tp = fieldType.Elem()
}
if isStruct(tp) && col.AssociateTable == nil {
var isBelongsTo = !(tp.ConvertibleTo(core.TimeType) || col.SQLType.IsJson())
if _, ok := fieldValue.Addr().Interface().(sql.Scanner); ok {
isBelongsTo = false
}
if _, ok := fieldValue.Interface().(sql.Scanner); ok {
isBelongsTo = false
}
if _, ok := fieldValue.Addr().Interface().(core.Conversion); ok {
isBelongsTo = false
}
if _, ok := fieldValue.Interface().(core.Conversion); ok {
isBelongsTo = false
}
if isBelongsTo {
err := BelongsToTagHandler(&ctx)
if err != nil {
return nil, err
}
col.AssociateType = core.AssociateNone
}
}
table.AddColumn(col)
} // end for } // end for
if idFieldColName != "" && len(table.PrimaryKeys) == 0 { if idFieldColName != "" && len(table.PrimaryKeys) == 0 {

View File

@ -16,6 +16,14 @@ import (
"github.com/go-xorm/core" "github.com/go-xorm/core"
) )
func isStruct(t reflect.Type) bool {
return t.Kind() == reflect.Struct || isPtrStruct(t)
}
func isPtrStruct(t reflect.Type) bool {
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct
}
// str2PK convert string value to primary key value according to tp // str2PK convert string value to primary key value according to tp
func str2PKValue(s string, tp reflect.Type) (reflect.Value, error) { func str2PKValue(s string, tp reflect.Type) (reflect.Value, error) {
var err error var err error

View File

@ -149,7 +149,7 @@ func (session *Session) Alias(alias string) *Session {
// NoCascade indicate that no cascade load child object // NoCascade indicate that no cascade load child object
func (session *Session) NoCascade() *Session { func (session *Session) NoCascade() *Session {
session.statement.UseCascade = false session.statement.cascadeMode = cascadeManuallyLoad
return session return session
} }
@ -204,9 +204,16 @@ func (session *Session) Charset(charset string) *Session {
// Cascade indicates if loading sub Struct // Cascade indicates if loading sub Struct
func (session *Session) Cascade(trueOrFalse ...bool) *Session { func (session *Session) Cascade(trueOrFalse ...bool) *Session {
var mode = cascadeAutoLoad
if len(trueOrFalse) >= 1 { if len(trueOrFalse) >= 1 {
session.statement.UseCascade = trueOrFalse[0] if trueOrFalse[0] {
mode = cascadeAutoLoad
} else {
mode = cascadeManuallyLoad
}
} }
session.statement.cascadeMode = mode
return session return session
} }
@ -629,7 +636,7 @@ func (session *Session) slice2Bean(scanResults []interface{}, fields []string, b
session.engine.logger.Error("sql.Sanner error:", err.Error()) session.engine.logger.Error("sql.Sanner error:", err.Error())
hasAssigned = false hasAssigned = false
} }
} else if col.SQLType.IsJson() { /*} else if col.SQLType.IsJson() {
if rawValueType.Kind() == reflect.String { if rawValueType.Kind() == reflect.String {
hasAssigned = true hasAssigned = true
x := reflect.New(fieldType) x := reflect.New(fieldType)
@ -650,18 +657,19 @@ func (session *Session) slice2Bean(scanResults []interface{}, fields []string, b
} }
fieldValue.Set(x.Elem()) fieldValue.Set(x.Elem())
} }
} }*/
} else if session.statement.UseCascade { } else if (col.AssociateType == core.AssociateNone &&
table, err := session.engine.autoMapType(*fieldValue) session.statement.cascadeMode == cascadeCompitable) ||
if err != nil { (col.AssociateType == core.AssociateBelongsTo &&
return nil, err session.statement.cascadeMode == cascadeAutoLoad) {
} table := col.AssociateTable
hasAssigned = true hasAssigned = true
if len(table.PrimaryKeys) != 1 { if len(table.PrimaryKeys) != 1 {
return nil, errors.New("unsupported non or composited primary key cascade") return nil, errors.New("unsupported non or composited primary key cascade")
} }
var pk = make(core.PK, len(table.PrimaryKeys)) var pk = make(core.PK, len(table.PrimaryKeys))
var err error
pk[0], err = asKind(vv, rawValueType) pk[0], err = asKind(vv, rawValueType)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -203,11 +203,11 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value,
} }
v = x v = x
fieldValue.Set(reflect.ValueOf(v).Convert(fieldType)) fieldValue.Set(reflect.ValueOf(v).Convert(fieldType))
} else if session.statement.UseCascade { } else if (col.AssociateType == core.AssociateNone &&
table, err := session.engine.autoMapType(*fieldValue) session.statement.cascadeMode == cascadeCompitable) ||
if err != nil { (col.AssociateType == core.AssociateBelongsTo &&
return err session.statement.cascadeMode == cascadeAutoLoad) {
} table := col.AssociateTable
// TODO: current only support 1 primary key // TODO: current only support 1 primary key
if len(table.PrimaryKeys) > 1 { if len(table.PrimaryKeys) > 1 {
@ -216,6 +216,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value,
var pk = make(core.PK, len(table.PrimaryKeys)) var pk = make(core.PK, len(table.PrimaryKeys))
rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) rawValueType := table.ColumnType(table.PKColumns()[0].FieldName)
var err error
pk[0], err = str2PK(string(data), rawValueType) pk[0], err = str2PK(string(data), rawValueType)
if err != nil { if err != nil {
return err return err
@ -485,18 +486,17 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value,
v = x v = x
fieldValue.Set(reflect.ValueOf(&x)) fieldValue.Set(reflect.ValueOf(&x))
default: default:
if session.statement.UseCascade { if (col.AssociateType == core.AssociateNone &&
structInter := reflect.New(fieldType.Elem()) session.statement.cascadeMode == cascadeCompitable) ||
table, err := session.engine.autoMapType(structInter.Elem()) (col.AssociateType == core.AssociateBelongsTo &&
if err != nil { session.statement.cascadeMode == cascadeAutoLoad) {
return err table := col.AssociateTable
}
if len(table.PrimaryKeys) > 1 { if len(table.PrimaryKeys) > 1 {
return errors.New("unsupported composited primary key cascade") return errors.New("unsupported composited primary key cascade")
} }
var pk = make(core.PK, len(table.PrimaryKeys)) var pk = make(core.PK, len(table.PrimaryKeys))
var err error
rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) rawValueType := table.ColumnType(table.PKColumns()[0].FieldName)
pk[0], err = str2PK(string(data), rawValueType) pk[0], err = str2PK(string(data), rawValueType)
if err != nil { if err != nil {
@ -504,6 +504,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value,
} }
if !isPKZero(pk) { if !isPKZero(pk) {
structInter := reflect.New(fieldType.Elem())
// !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch
// however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne
// property to be fetched lazily // property to be fetched lazily
@ -518,8 +519,6 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value,
return errors.New("cascade obj is not exist") return errors.New("cascade obj is not exist")
} }
} }
} else {
return fmt.Errorf("unsupported struct type in Scan: %s", fieldValue.Type().String())
} }
} }
default: default:

View File

@ -259,7 +259,7 @@ func (session *Session) Sync2(beans ...interface{}) error {
for _, bean := range beans { for _, bean := range beans {
v := rValue(bean) v := rValue(bean)
table, err := engine.mapType(v) table, err := engine.autoMapType(v)
if err != nil { if err != nil {
return err return err
} }

View File

@ -33,6 +33,14 @@ type exprParam struct {
expr string expr string
} }
type cascadeMode int
const (
cascadeCompitable cascadeMode = iota
cascadeAutoLoad
cascadeManuallyLoad
)
// Statement save all the sql info for executing SQL // Statement save all the sql info for executing SQL
type Statement struct { type Statement struct {
RefTable *core.Table RefTable *core.Table
@ -54,7 +62,7 @@ type Statement struct {
tableName string tableName string
RawSQL string RawSQL string
RawParams []interface{} RawParams []interface{}
UseCascade bool cascadeMode cascadeMode
UseAutoJoin bool UseAutoJoin bool
StoreEngine string StoreEngine string
Charset string Charset string
@ -82,7 +90,7 @@ func (statement *Statement) Init() {
statement.Start = 0 statement.Start = 0
statement.LimitN = 0 statement.LimitN = 0
statement.OrderStr = "" statement.OrderStr = ""
statement.UseCascade = true statement.cascadeMode = cascadeCompitable
statement.JoinStr = "" statement.JoinStr = ""
statement.joinArgs = make([]interface{}, 0) statement.joinArgs = make([]interface{}, 0)
statement.GroupByStr = "" statement.GroupByStr = ""

105
tag.go
View File

@ -5,6 +5,7 @@
package xorm package xorm
import ( import (
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
@ -15,18 +16,22 @@ import (
) )
type tagContext struct { type tagContext struct {
engine *Engine
parsingTables map[reflect.Type]*core.Table
table *core.Table
hasCacheTag bool
hasNoCacheTag bool
col *core.Column
fieldValue reflect.Value
isIndex bool
isUnique bool
indexNames map[string]int
tagName string tagName string
params []string params []string
preTag, nextTag string preTag, nextTag string
table *core.Table
col *core.Column
fieldValue reflect.Value
isIndex bool
isUnique bool
indexNames map[string]int
engine *Engine
hasCacheTag bool
hasNoCacheTag bool
ignoreNext bool ignoreNext bool
} }
@ -36,25 +41,26 @@ type tagHandler func(ctx *tagContext) error
var ( var (
// defaultTagHandlers enumerates all the default tag handler // defaultTagHandlers enumerates all the default tag handler
defaultTagHandlers = map[string]tagHandler{ defaultTagHandlers = map[string]tagHandler{
"<-": OnlyFromDBTagHandler, "<-": OnlyFromDBTagHandler,
"->": OnlyToDBTagHandler, "->": OnlyToDBTagHandler,
"PK": PKTagHandler, "PK": PKTagHandler,
"NULL": NULLTagHandler, "NULL": NULLTagHandler,
"NOT": IgnoreTagHandler, "NOT": IgnoreTagHandler,
"AUTOINCR": AutoIncrTagHandler, "AUTOINCR": AutoIncrTagHandler,
"DEFAULT": DefaultTagHandler, "DEFAULT": DefaultTagHandler,
"CREATED": CreatedTagHandler, "CREATED": CreatedTagHandler,
"UPDATED": UpdatedTagHandler, "UPDATED": UpdatedTagHandler,
"DELETED": DeletedTagHandler, "DELETED": DeletedTagHandler,
"VERSION": VersionTagHandler, "VERSION": VersionTagHandler,
"UTC": UTCTagHandler, "UTC": UTCTagHandler,
"LOCAL": LocalTagHandler, "LOCAL": LocalTagHandler,
"NOTNULL": NotNullTagHandler, "NOTNULL": NotNullTagHandler,
"INDEX": IndexTagHandler, "INDEX": IndexTagHandler,
"UNIQUE": UniqueTagHandler, "UNIQUE": UniqueTagHandler,
"CACHE": CacheTagHandler, "CACHE": CacheTagHandler,
"NOCACHE": NoCacheTagHandler, "NOCACHE": NoCacheTagHandler,
"COMMENT": CommentTagHandler, "COMMENT": CommentTagHandler,
"BELONGS_TO": BelongsToTagHandler,
} }
) )
@ -256,7 +262,7 @@ func ExtendsTagHandler(ctx *tagContext) error {
} }
fallthrough fallthrough
case reflect.Struct: case reflect.Struct:
parentTable, err := ctx.engine.mapType(fieldValue) parentTable, err := ctx.engine.mapType(ctx.parsingTables, fieldValue)
if err != nil { if err != nil {
return err return err
} }
@ -288,3 +294,44 @@ func NoCacheTagHandler(ctx *tagContext) error {
} }
return nil return nil
} }
// BelongsToTagHandler describes belongs_to tag handler
func BelongsToTagHandler(ctx *tagContext) error {
if !isStruct(ctx.fieldValue.Type()) {
return errors.New("Tag belongs_to cannot be applied on non-struct field")
}
ctx.col.AssociateType = core.AssociateBelongsTo
var t reflect.Value
if ctx.fieldValue.Kind() == reflect.Struct {
t = ctx.fieldValue
} else {
if ctx.fieldValue.Type().Kind() == reflect.Ptr && ctx.fieldValue.Type().Elem().Kind() == reflect.Struct {
if ctx.fieldValue.IsNil() {
t = reflect.New(ctx.fieldValue.Type().Elem()).Elem()
} else {
t = ctx.fieldValue
}
} else {
return errors.New("Only struct or ptr to struct field could add belongs_to flag")
}
}
belongsT, err := ctx.engine.mapType(ctx.parsingTables, t)
if err != nil {
return err
}
pks := belongsT.PKColumns()
if len(pks) != 1 {
panic("unsupported non or composited primary key cascade")
return errors.New("blongs_to only should be as a tag of table has one primary key")
}
ctx.col.AssociateTable = belongsT
ctx.col.SQLType = pks[0].SQLType
if len(ctx.col.Name) == 0 {
ctx.col.Name = ctx.engine.ColumnMapper.Obj2Table(ctx.col.FieldName) + "_id"
}
return nil
}

View File

@ -12,6 +12,7 @@ import (
"github.com/go-xorm/core" "github.com/go-xorm/core"
_ "github.com/lib/pq" _ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
_ "github.com/ziutek/mymysql/godrv" _ "github.com/ziutek/mymysql/godrv"
) )
@ -123,6 +124,8 @@ func TestMain(m *testing.M) {
} }
func TestPing(t *testing.T) { func TestPing(t *testing.T) {
assert.NoError(t, prepareEngine())
if err := testEngine.Ping(); err != nil { if err := testEngine.Ping(); err != nil {
t.Fatal(err) t.Fatal(err)
} }