-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
wangjiahan
committed
Aug 22, 2024
1 parent
e91263a
commit ea8f4a0
Showing
18 changed files
with
2,412 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
package gmm | ||
|
||
import ( | ||
"database/sql" | ||
"errors" | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
"reflect" | ||
"strconv" | ||
"sync/atomic" | ||
|
||
"github.com/dolthub/go-mysql-server/server" | ||
"github.com/google/uuid" | ||
"gorm.io/gorm" | ||
"gorm.io/gorm/schema" | ||
) | ||
|
||
var port atomic.Int32 | ||
|
||
func init() { | ||
port.Store(19527) | ||
} | ||
|
||
type GMMBuilder struct { | ||
dbName string | ||
port int | ||
server *server.Server | ||
sqlDB *sql.DB | ||
gormDB *gorm.DB | ||
err error | ||
|
||
tables []schema.Tabler | ||
models []schema.Tabler | ||
sqlStmts []string | ||
sqlFiles []string | ||
} | ||
|
||
func Builder(db ...string) *GMMBuilder { | ||
b := &GMMBuilder{ | ||
port: int(port.Add(1)), | ||
tables: make([]schema.Tabler, 0), | ||
models: make([]schema.Tabler, 0), | ||
sqlStmts: make([]string, 0), | ||
sqlFiles: make([]string, 0), | ||
} | ||
dbName := "gmm-test-db-" + uuid.NewString()[:6] | ||
if len(db) > 0 { | ||
dbName = db[0] | ||
} | ||
b.dbName = dbName | ||
return b | ||
} | ||
|
||
func (b *GMMBuilder) Port(port int) *GMMBuilder { | ||
if b.err != nil { | ||
return b | ||
} | ||
b.port = port | ||
return b | ||
} | ||
|
||
func (b *GMMBuilder) Build() (sDB *sql.DB, gDB *gorm.DB, shutdown func(), err error) { | ||
b.initServer() | ||
if b.err != nil { | ||
return nil, nil, nil, b.err | ||
} | ||
|
||
// start server | ||
slog.Info("start go mysql mocker server, listening at 0.0.0.0:" + strconv.Itoa(b.port)) | ||
go func() { | ||
if err := b.server.Start(); err != nil { | ||
panic(err) | ||
} | ||
}() | ||
|
||
shutdown = func() { | ||
_ = b.server.Close() | ||
} | ||
|
||
// create client and connect to server | ||
b.sqlDB, b.gormDB, err = createMySQLClient(b.port, b.dbName) | ||
if err != nil { | ||
b.err = fmt.Errorf("failed to create sql client: %w", err) | ||
return nil, nil, nil, b.err | ||
} | ||
|
||
// prepare init data | ||
b.initTables() | ||
b.initWithModels() | ||
b.initWithStmts() | ||
b.initWithFiles() | ||
if b.err != nil { | ||
return nil, nil, nil, b.err | ||
} | ||
|
||
return b.sqlDB, b.gormDB, shutdown, nil | ||
} | ||
|
||
func (b *GMMBuilder) initServer() *GMMBuilder { | ||
if b.err != nil { | ||
return b | ||
} | ||
b.server, b.err = createMySQLServer(b.dbName, b.port) | ||
return b | ||
} | ||
|
||
func (b *GMMBuilder) CreateTable(table schema.Tabler) *GMMBuilder { | ||
if b.err != nil { | ||
return b | ||
} | ||
b.tables = append(b.tables, table) | ||
return b | ||
} | ||
|
||
// InitData adds init data, if data is a struct/pointer, should implement schema.Tabler. | ||
// if data is a slice, each item should implement schema.Tabler, otherwise will return error. | ||
func (b *GMMBuilder) InitData(data interface{}) *GMMBuilder { | ||
if b.err != nil { | ||
return b | ||
} | ||
|
||
if reflect.TypeOf(data).Kind() == reflect.Slice { | ||
slice := reflect.ValueOf(data) | ||
for i := 0; i < slice.Len(); i++ { | ||
item := slice.Index(i).Interface() | ||
model, ok := item.(schema.Tabler) | ||
if !ok { | ||
b.err = errors.New("every single data should implement gorm schema.Tabler") | ||
return b | ||
} | ||
b.models = append(b.models, model) | ||
} | ||
return b | ||
} | ||
|
||
model, ok := data.(schema.Tabler) | ||
if !ok { | ||
b.err = errors.New("data should implement gorm schema.Tabler") | ||
return b | ||
} | ||
b.models = append(b.models, model) | ||
return b | ||
} | ||
|
||
// SQLStmts adds sql statements, each statement should be a valid sql statement. | ||
func (b *GMMBuilder) SQLStmts(stmts ...string) *GMMBuilder { | ||
if b.err != nil { | ||
return b | ||
} | ||
b.sqlStmts = append(b.sqlStmts, stmts...) | ||
return b | ||
} | ||
|
||
// SQLFiles adds sql files, each file should be a valid sql file. | ||
func (b *GMMBuilder) SQLFiles(files ...string) *GMMBuilder { | ||
if b.err != nil { | ||
return b | ||
} | ||
|
||
for _, file := range files { | ||
if _, err := os.Stat(file); os.IsNotExist(err) { | ||
b.err = fmt.Errorf("sql file %s not exist", file) | ||
return b | ||
} | ||
} | ||
|
||
b.sqlFiles = append(b.sqlFiles, files...) | ||
return b | ||
} | ||
|
||
func (b *GMMBuilder) initTables() { | ||
if b.err != nil || len(b.tables) == 0 { | ||
return | ||
} | ||
|
||
slog.Info("start to init tables, count = " + strconv.Itoa(len(b.tables))) | ||
for _, table := range b.tables { | ||
if err := b.gormDB.AutoMigrate(table); err != nil { | ||
b.err = fmt.Errorf("failed to auto migrate(type=%T): %w", table, err) | ||
return | ||
} | ||
} | ||
slog.Info("init tables successfully, count = " + strconv.Itoa(len(b.tables))) | ||
} | ||
|
||
func (b *GMMBuilder) initWithModels() { | ||
if b.err != nil || len(b.models) == 0 { | ||
return | ||
} | ||
|
||
slog.Info("start to init data with models, count = " + strconv.Itoa(len(b.models))) | ||
for _, model := range b.models { | ||
if err := b.gormDB.AutoMigrate(model); err != nil { | ||
b.err = fmt.Errorf("failed to auto migrate(type=%T): %w", model, err) | ||
return | ||
} | ||
if err := b.gormDB.Create(model).Error; err != nil { | ||
b.err = fmt.Errorf("failed to init data(type=%T): %w", model, err) | ||
return | ||
} | ||
} | ||
slog.Info("init data with models successfully, count = " + strconv.Itoa(len(b.models))) | ||
} | ||
|
||
func (b *GMMBuilder) initWithStmts() { | ||
if b.err != nil || len(b.sqlStmts) == 0 { | ||
return | ||
} | ||
slog.Info("start to init data with sql stmts, count = " + strconv.Itoa(len(b.sqlStmts))) | ||
for _, stmt := range b.sqlStmts { | ||
stmts, err := splitSQLStatements(stmt) | ||
if err != nil { | ||
b.err = err | ||
return | ||
} | ||
if err = b.executeSQLStatements(stmts); err != nil { | ||
b.err = err | ||
return | ||
} | ||
} | ||
slog.Info("init data with sql stmts successfully, count = " + strconv.Itoa(len(b.sqlStmts))) | ||
} | ||
|
||
func (b *GMMBuilder) initWithFiles() { | ||
if b.err != nil || len(b.sqlFiles) == 0 { | ||
return | ||
} | ||
slog.Info("start to init data with sql files, count = " + strconv.Itoa(len(b.sqlFiles))) | ||
for _, file := range b.sqlFiles { | ||
stmts, err := splitSQLFile(file) | ||
if err != nil { | ||
b.err = fmt.Errorf("failed to split sql file '%s': %w", file, err) | ||
return | ||
} | ||
if err = b.executeSQLStatements(stmts); err != nil { | ||
b.err = err | ||
return | ||
} | ||
} | ||
slog.Info("init data with sql files successfully, count = " + strconv.Itoa(len(b.sqlFiles))) | ||
} | ||
|
||
func (b *GMMBuilder) executeSQLStatements(stmts []string) error { | ||
for _, stmt := range stmts { | ||
_, err := b.sqlDB.Exec(stmt) | ||
if err != nil { | ||
return fmt.Errorf("failed to exec sql stmt '%s': %w", stmt, err) | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package gmm | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
type CertificationInfo struct { | ||
ID int `gorm:"column:id;index;primaryKey;autoIncrement"` | ||
Username string `gorm:"column:username"` | ||
Password string `gorm:"column:password"` | ||
} | ||
|
||
func (receiver CertificationInfo) TableName() string { | ||
return "certification_info" | ||
} | ||
|
||
type UserState struct { | ||
UID string `gorm:"primaryKey;column:uid"` | ||
State string `gorm:"column:state"` | ||
} | ||
|
||
func (receiver UserState) TableName() string { | ||
return "user_state" | ||
} | ||
|
||
type UnknownType struct{} | ||
|
||
func Test_GMMBuilder_Port(t *testing.T) { | ||
t.Run("listening on same port should return err", func(t *testing.T) { | ||
_, _, _, err := Builder().Port(9527).Build() | ||
assert.Nil(t, err) | ||
_, _, _, err = Builder().Port(9527).Build() | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "failed to create server: Port 127.0.0.1:9527 already in use.", err.Error()) | ||
}) | ||
} | ||
|
||
func Test_GMMBuilder_File(t *testing.T) { | ||
t.Run("non existing file should return err", func(t *testing.T) { | ||
_, _, _, err := Builder().SQLFiles("fixtures/not-exist.sql").Build() | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "sql file fixtures/not-exist.sql not exist", err.Error()) | ||
}) | ||
|
||
t.Run("existing and valid file should ok", func(t *testing.T) { | ||
_, _, _, err := Builder().SQLFiles("fixtures/sequel_ace.sql").Build() | ||
assert.Nil(t, err) | ||
}) | ||
} | ||
|
||
//nolint:lll | ||
func Test_GMMBuilder_Stmt(t *testing.T) { | ||
t.Run("invalid sql statement should return err", func(t *testing.T) { | ||
_, _, _, err := Builder().SQLStmts("invalid sql statement").Build() | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "failed to exec sql stmt 'invalid sql statement': Error 1105 (HY000): syntax error at position 8 near 'invalid'", err.Error()) | ||
}) | ||
|
||
t.Run("valid sql statement should ok", func(t *testing.T) { | ||
_, _, _, err := Builder().CreateTable(CertificationInfo{}).SQLStmts("insert into certification_info(id, username, password) values (1, 'hedon', 'hedon-pwd');").Build() | ||
assert.Nil(t, err) | ||
}) | ||
|
||
t.Run("multi valid sql statements should ok", func(t *testing.T) { | ||
sql := ` | ||
CREATE TABLE IF NOT EXISTS users ( | ||
id INT NOT NULL AUTO_INCREMENT, | ||
username VARCHAR(50) NOT NULL, | ||
email VARCHAR(100) NOT NULL, | ||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
PRIMARY KEY (id) | ||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||
INSERT INTO users (username, email) VALUES | ||
('john_doe', 'john.doe@example.com'), | ||
('jane_smith', 'jane.smith@example.com'), | ||
('alice_jones', 'alice.jones@example.com');` | ||
_, _, _, err := Builder().SQLStmts(sql).Build() | ||
assert.Nil(t, err) | ||
}) | ||
} | ||
|
||
func TestGMMBuilder_Data(t *testing.T) { | ||
t.Run("invalid data type should return err", func(t *testing.T) { | ||
_, _, _, err := Builder().InitData(1).Build() | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "data should implement gorm schema.Tabler", err.Error()) | ||
}) | ||
|
||
t.Run("auto increment, but struct is unaddressable, should panic", func(t *testing.T) { | ||
assert.Panics(t, func() { | ||
_, _, _, _ = Builder().InitData(CertificationInfo{}).Build() | ||
}) | ||
}) | ||
|
||
t.Run("struct is unaddressable, but would not be changed, should ok", func(t *testing.T) { | ||
_, _, _, err := Builder().InitData(UserState{}).Build() | ||
assert.Nil(t, err) | ||
}) | ||
|
||
t.Run("type not implemented schema.Tabler, should return error", func(t *testing.T) { | ||
_, _, _, err := Builder().InitData(&UnknownType{}).Build() | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "data should implement gorm schema.Tabler", err.Error()) | ||
}) | ||
|
||
t.Run("pointer implemented schema.Tabler should ok", func(t *testing.T) { | ||
_, _, _, err := Builder().InitData(&CertificationInfo{}).Build() | ||
assert.Nil(t, err) | ||
}) | ||
|
||
t.Run("slice implemented schema.Tabler should ok", func(t *testing.T) { | ||
_, _, _, err := Builder().InitData([]CertificationInfo{}).Build() | ||
assert.Nil(t, err) | ||
}) | ||
|
||
t.Run("slice of different types that implemented schema.Tabler should ok", func(t *testing.T) { | ||
_, _, _, err := Builder().InitData([]interface{}{&CertificationInfo{}, &UserState{}}).Build() | ||
assert.Nil(t, err) | ||
}) | ||
} |
Oops, something went wrong.