From e2769149d473d692456d09931ab1f3df8d162f85 Mon Sep 17 00:00:00 2001 From: Bowen Date: Tue, 12 Nov 2024 17:36:46 +0800 Subject: [PATCH] optimize docker --- .gitignore | 2 +- contracts/testing/testing.go | 6 +- foundation/application.go | 3 +- foundation/path.go | 51 ---------------- support/docker/container.go | 91 ++++++++++++++++++++++------ support/docker/container_test.go | 68 ++++++++++++--------- support/docker/docker.go | 101 +++++++++++++++++++++---------- support/docker/docker_test.go | 26 +++++--- support/docker/mysql.go | 29 ++++++++- support/docker/postgres.go | 59 ++++++++++++------ support/docker/sqlite.go | 37 ++++++----- support/docker/sqlserver.go | 66 ++++++++++++-------- support/docker/utils.go | 19 +++++- support/env/env.go | 80 ++++++++++++++++++------ testing/docker/database.go | 2 +- 15 files changed, 411 insertions(+), 229 deletions(-) delete mode 100644 foundation/path.go diff --git a/.gitignore b/.gitignore index 10022aa72..eab4fee46 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ .idea .DS_Store .vscode -goravel* +goravel_* /vendor \ No newline at end of file diff --git a/contracts/testing/testing.go b/contracts/testing/testing.go index 1e0befbac..e95b36a67 100644 --- a/contracts/testing/testing.go +++ b/contracts/testing/testing.go @@ -26,12 +26,14 @@ type DatabaseDriver interface { Build() error // Config get database configuration. Config() DatabaseConfig + // Database returns a new instance with a new database, the Build method needs to be called first. + Database(name string) (DatabaseDriver, error) + // Driver gets the database driver name. + Driver() database.Driver // Fresh the database. Fresh() error // Image gets the database image. Image(image Image) - // Driver gets the database driver name. - Driver() database.Driver // Stop the database. Stop() error } diff --git a/foundation/application.go b/foundation/application.go index abe66de19..c945df372 100644 --- a/foundation/application.go +++ b/foundation/application.go @@ -3,6 +3,7 @@ package foundation import ( "context" "flag" + "github.com/goravel/framework/support/env" "os" "path/filepath" "strings" @@ -305,7 +306,7 @@ func setEnv() { } func setRootPath() { - support.RootPath = getCurrentAbsolutePath() + support.RootPath = env.CurrentAbsolutePath() } func getEnvPath() string { diff --git a/foundation/path.go b/foundation/path.go deleted file mode 100644 index a77fd46e8..000000000 --- a/foundation/path.go +++ /dev/null @@ -1,51 +0,0 @@ -package foundation - -import ( - "log" - "os" - "path/filepath" - "strings" -) - -func getCurrentAbsolutePath() string { - executable, err := os.Executable() - if err != nil { - log.Fatal(err) - } - res, _ := filepath.EvalSymlinks(filepath.Dir(executable)) - - if isTesting() || isAir() || isDirectlyRun() { - res, _ = os.Getwd() - } - - return res -} - -// isTesting checks if the application is running in testing mode. -func isTesting() bool { - for _, arg := range os.Args { - if strings.Contains(arg, "-test.") { - return true - } - } - - return false -} - -// isAir checks if the application is running using Air. -func isAir() bool { - for _, arg := range os.Args { - if strings.Contains(filepath.ToSlash(arg), "/storage/temp") { - return true - } - } - - return false -} - -// isDirectlyRun checks if the application is running using go run. -func isDirectlyRun() bool { - executable, _ := os.Executable() - return strings.Contains(filepath.Base(executable), os.TempDir()) || - (strings.Contains(filepath.ToSlash(executable), "/var/folders") && strings.Contains(filepath.ToSlash(executable), "/T/go-build")) // macOS -} diff --git a/support/docker/container.go b/support/docker/container.go index 69eb9d5b8..0b673bb9e 100644 --- a/support/docker/container.go +++ b/support/docker/container.go @@ -2,28 +2,40 @@ package docker import ( "bytes" - "io" - "os" - "path/filepath" - "github.com/goravel/framework/contracts/testing" "github.com/goravel/framework/foundation/json" + "github.com/goravel/framework/support/color" "github.com/goravel/framework/support/file" + "io" + "os" + "path/filepath" ) -type Container struct { - file string +type ContainerManager struct { + file string + lockFile string } -func NewContainer() *Container { - return &Container{ - file: filepath.Join(os.TempDir(), "goravel_docker.txt"), +func NewContainerManager() *ContainerManager { + return &ContainerManager{ + file: filepath.Join(os.TempDir(), "goravel_docker.txt"), + lockFile: filepath.Join(os.TempDir(), "goravel_docker.lock"), } } -func (r *Container) Add(containerType ContainerType, config testing.DatabaseConfig) { - containerTypeToContainers := r.All() - containerTypeToContainers[containerType] = append(containerTypeToContainers[containerType], config) +func (r *ContainerManager) Add(containerType ContainerType, databaseDriver testing.DatabaseDriver) { + containerTypeToDatabaseDrivers := r.All() + containerTypeToDatabaseDrivers[containerType] = append(containerTypeToDatabaseDrivers[containerType], databaseDriver) + + containerTypeToDatabaseConfigs := make(map[ContainerType][]testing.DatabaseConfig) + for k, v := range containerTypeToDatabaseDrivers { + containerTypeToDatabaseConfigs[k] = make([]testing.DatabaseConfig, len(v)) + for i, driver := range v { + containerTypeToDatabaseConfigs[k][i] = driver.Config() + } + } + + color.Red().Println("add", r.file, databaseDriver.Config(), containerTypeToDatabaseConfigs) f, err := os.OpenFile(r.file, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) if err != nil { @@ -31,7 +43,7 @@ func (r *Container) Add(containerType ContainerType, config testing.DatabaseConf } defer f.Close() - content, err := json.NewJson().Marshal(containerTypeToContainers) + content, err := json.NewJson().Marshal(containerTypeToDatabaseConfigs) if err != nil { panic(err) } @@ -42,10 +54,10 @@ func (r *Container) Add(containerType ContainerType, config testing.DatabaseConf } } -func (r *Container) All() map[ContainerType][]testing.DatabaseConfig { - var containerTypeToContainers map[ContainerType][]testing.DatabaseConfig +func (r *ContainerManager) All() map[ContainerType][]testing.DatabaseDriver { + containerTypeToDatabaseDrivers := make(map[ContainerType][]testing.DatabaseDriver) if !file.Exists(r.file) { - return make(map[ContainerType][]testing.DatabaseConfig) + return containerTypeToDatabaseDrivers } f, err := os.OpenFile(r.file, os.O_RDONLY, 0666) @@ -59,13 +71,54 @@ func (r *Container) All() map[ContainerType][]testing.DatabaseConfig { panic(err) } - if err := json.NewJson().Unmarshal(bytes.TrimSpace(content), &containerTypeToContainers); err != nil { + var containerTypeToDatabaseConfigs map[ContainerType][]testing.DatabaseConfig + if err := json.NewJson().Unmarshal(bytes.TrimSpace(content), &containerTypeToDatabaseConfigs); err != nil { + panic(err) + } + + if len(containerTypeToDatabaseConfigs) == 0 { + return containerTypeToDatabaseDrivers + } + + containerTypeToDatabaseConfigs1 := make(map[ContainerType][]testing.DatabaseConfig) + for containerType, databaseConfigs := range containerTypeToDatabaseConfigs { + for _, databaseConfig := range databaseConfigs { + // If the port is not occupied, provide the container is released. + if databaseConfig.Port != 0 && !isPortUsing(databaseConfig.Port) { + continue + } + containerTypeToDatabaseConfigs1[containerType] = append(containerTypeToDatabaseConfigs1[containerType], databaseConfig) + databaseDriver := NewDatabaseDriverByExist(containerType, databaseConfig.ContainerID, databaseConfig.Database, databaseConfig.Username, databaseConfig.Password, databaseConfig.Port) + containerTypeToDatabaseDrivers[containerType] = append(containerTypeToDatabaseDrivers[containerType], databaseDriver) + } + } + + color.Red().Println("all", r.file, containerTypeToDatabaseConfigs1) + + return containerTypeToDatabaseDrivers +} + +func (r *ContainerManager) Lock() { + for { + if !file.Exists(r.lockFile) { + break + } + } + if err := file.Create(r.lockFile, ""); err != nil { panic(err) } +} - return containerTypeToContainers +func (r *ContainerManager) Unlock() { + if err := file.Remove(r.lockFile); err != nil { + panic(err) + } } -func (r *Container) Remove() error { +func (r *ContainerManager) Remove() error { + if err := file.Remove(r.lockFile); err != nil { + return err + } + return file.Remove(r.file) } diff --git a/support/docker/container_test.go b/support/docker/container_test.go index 4813c747d..812afee07 100644 --- a/support/docker/container_test.go +++ b/support/docker/container_test.go @@ -4,40 +4,50 @@ import ( "testing" "github.com/stretchr/testify/assert" - - contractstesting "github.com/goravel/framework/contracts/testing" ) func TestContainer(t *testing.T) { - container := NewContainer() - assert.NoError(t, container.Remove()) - - config1 := contractstesting.DatabaseConfig{ - Host: "localhost", - Port: 5432, - Database: "goravel", - Username: "goravel", - Password: "Framework!123", - ContainerID: "123456", - } - config2 := contractstesting.DatabaseConfig{ - Host: "localhost1", - Port: 5432, - Database: "goravel", - Username: "goravel", - Password: "Framework!123", - ContainerID: "123456", - } - - container.Add(ContainerTypePostgres, config1) - container.Add(ContainerTypePostgres, config2) - container.Add(ContainerTypeSqlite, config1) + testPortUsing = true + container := NewContainerManager() + + postgresDriver1 := NewPostgresImpl("goravel", "goravel", "Framework!123") + postgresDriver1.port = 5432 + postgresDriver1.containerID = "123456" + + postgresDriver2 := NewPostgresImpl("goravel", "goravel", "Framework!123") + postgresDriver2.port = 5433 + postgresDriver2.containerID = "1234565" + + sqliteDriver := NewSqliteImpl("goravel") + + mysqlDriver := NewMysqlImpl("goravel", "goravel", "Framework!123") + mysqlDriver.port = 5432 + mysqlDriver.containerID = "123456" + + sqlserverDriver := NewSqlserverImpl("goravel", "goravel", "Framework!123") + sqlserverDriver.port = 5432 + sqlserverDriver.containerID = "123456" + + container.Add(ContainerTypePostgres, postgresDriver1) + container.Add(ContainerTypePostgres, postgresDriver2) + container.Add(ContainerTypeSqlite, sqliteDriver) + container.Add(ContainerTypeMysql, mysqlDriver) + container.Add(ContainerTypeSqlserver, sqlserverDriver) containers := container.All() - assert.Len(t, containers, 2) + assert.Len(t, containers, 4) assert.Len(t, containers[ContainerTypePostgres], 2) assert.Len(t, containers[ContainerTypeSqlite], 1) - assert.Equal(t, containers[ContainerTypePostgres][0], config1) - assert.Equal(t, containers[ContainerTypePostgres][1], config2) - assert.Equal(t, containers[ContainerTypeSqlite][0], config1) + assert.Len(t, containers[ContainerTypeMysql], 1) + assert.Len(t, containers[ContainerTypeSqlserver], 1) + assert.Equal(t, postgresDriver1, containers[ContainerTypePostgres][0]) + assert.Equal(t, postgresDriver2, containers[ContainerTypePostgres][1]) + assert.Equal(t, sqliteDriver, containers[ContainerTypeSqlite][0]) + assert.Equal(t, mysqlDriver, containers[ContainerTypeMysql][0]) + assert.Equal(t, sqlserverDriver, containers[ContainerTypeSqlserver][0]) + + defer func() { + testPortUsing = false + assert.NoError(t, container.Remove()) + }() } diff --git a/support/docker/docker.go b/support/docker/docker.go index 951ef13d2..3dd7c5517 100644 --- a/support/docker/docker.go +++ b/support/docker/docker.go @@ -2,6 +2,7 @@ package docker import ( "fmt" + "github.com/goravel/framework/support/str" "github.com/goravel/framework/contracts/testing" "github.com/goravel/framework/errors" @@ -15,7 +16,7 @@ const ( TestModelNormal // Switch this value to control the test model. - TestModel = TestModelNormal + TestModel = TestModelMinimum ) type ContainerType string @@ -32,14 +33,12 @@ const ( ContainerTypeRedis ContainerType = "redis" ) -var containers = make(map[ContainerType][]testing.DatabaseDriver) - func Mysql() testing.DatabaseDriver { return Mysqls(1)[0] } func Mysqls(num int) []testing.DatabaseDriver { - return Database(ContainerTypeMysql, testDatabase, testUsername, testPassword, num) + return Database(ContainerTypeMysql, testUsername, testPassword, num) } func Postgres() testing.DatabaseDriver { @@ -47,7 +46,7 @@ func Postgres() testing.DatabaseDriver { } func Postgreses(num int) []testing.DatabaseDriver { - return Database(ContainerTypePostgres, testDatabase, testUsername, testPassword, num) + return Database(ContainerTypePostgres, testUsername, testPassword, num) } func Sqlserver() testing.DatabaseDriver { @@ -55,7 +54,7 @@ func Sqlserver() testing.DatabaseDriver { } func Sqlservers(num int) []testing.DatabaseDriver { - return Database(ContainerTypeSqlserver, testDatabase, testUsername, testPassword, num) + return Database(ContainerTypeSqlserver, testUsername, testPassword, num) } func Sqlite() testing.DatabaseDriver { @@ -63,56 +62,61 @@ func Sqlite() testing.DatabaseDriver { } func Sqlites(num int) []testing.DatabaseDriver { - return Database(ContainerTypeSqlite, testDatabase, testUsername, testPassword, num) + return Database(ContainerTypeSqlite, testUsername, testPassword, num) } -func Database(containerType ContainerType, database, username, password string, num int) []testing.DatabaseDriver { +func Database(containerType ContainerType, username, password string, num int) []testing.DatabaseDriver { if num <= 0 { panic(errors.DockerDatabaseContainerCountZero) } // Get containers from temp file. + containerManager := NewContainerManager() + containerManager.Lock() + defer containerManager.Unlock() + + containerTypeToDatabaseDrivers := containerManager.All() - var drivers []testing.DatabaseDriver - if len(containers[containerType]) >= num { - drivers = containers[containerType][:num] + var databaseDrivers []testing.DatabaseDriver + if len(containerTypeToDatabaseDrivers[containerType]) >= num { + databaseDrivers = containerTypeToDatabaseDrivers[containerType][:num] } else { - drivers = containers[containerType] + databaseDrivers = containerTypeToDatabaseDrivers[containerType] } - newDatabase := database - driverLength := len(drivers) + // Create new database in the exist docker container + for i, databaseDriver := range databaseDrivers { + databaseName := fmt.Sprintf("goravel_%s", str.Random(6)) + if newDatabaseDriver, err := databaseDriver.Database(databaseName); err != nil { + panic(err) + } else { + databaseDrivers[i] = newDatabaseDriver + } + } + + // Create new docker container + driverLength := len(databaseDrivers) surplus := num - driverLength for i := 0; i < surplus; i++ { - if containerType == ContainerTypeSqlite { - newDatabase = fmt.Sprintf("%s%d", database, driverLength+i) - } - databaseDriver := DatabaseDriver(containerType, newDatabase, username, password) + databaseName := fmt.Sprintf("goravel_%s", str.Random(6)) + databaseDriver := NewDatabaseDriver(containerType, databaseName, username, password) if err := databaseDriver.Build(); err != nil { panic(err) } - containers[containerType] = append(containers[containerType], databaseDriver) - drivers = append(drivers, databaseDriver) - - // Write containers to temp file. + containerManager.Add(containerType, databaseDriver) + databaseDrivers = append(databaseDrivers, databaseDriver) } - if len(drivers) != num { - panic(errors.DockerInsufficientDatabaseContainers.Args(num, len(drivers))) + if len(databaseDrivers) != num { + panic(errors.DockerInsufficientDatabaseContainers.Args(num, len(databaseDrivers))) } - for _, driver := range drivers { - if err := driver.Fresh(); err != nil { - panic(err) - } - } - - return drivers + return databaseDrivers } -func DatabaseDriver(containerType ContainerType, database, username, password string) testing.DatabaseDriver { +func NewDatabaseDriver(containerType ContainerType, database, username, password string) testing.DatabaseDriver { switch containerType { case ContainerTypeMysql: return NewMysqlImpl(database, username, password) @@ -127,8 +131,39 @@ func DatabaseDriver(containerType ContainerType, database, username, password st } } +func NewDatabaseDriverByExist(containerType ContainerType, containerID, database, username, password string, port int) testing.DatabaseDriver { + switch containerType { + case ContainerTypeMysql: + driver := NewMysqlImpl(database, username, password) + driver.containerID = containerID + driver.port = port + + return driver + case ContainerTypePostgres: + driver := NewPostgresImpl(database, username, password) + driver.containerID = containerID + driver.port = port + + return driver + case ContainerTypeSqlserver: + driver := NewSqlserverImpl(database, username, password) + driver.containerID = containerID + driver.port = port + + return driver + case ContainerTypeSqlite: + return NewSqliteImpl(database) + + default: + panic(errors.DockerUnknownContainerType) + } +} + func Stop() error { - for _, drivers := range containers { + containerManager := NewContainerManager() + containerTypeToDatabaseDrivers := containerManager.All() + + for _, drivers := range containerTypeToDatabaseDrivers { for _, driver := range drivers { if err := driver.Stop(); err != nil { return err diff --git a/support/docker/docker_test.go b/support/docker/docker_test.go index 99df8b8bc..662060fef 100644 --- a/support/docker/docker_test.go +++ b/support/docker/docker_test.go @@ -1,9 +1,8 @@ package docker import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" contractstesting "github.com/goravel/framework/contracts/testing" "github.com/goravel/framework/support/env" @@ -30,6 +29,11 @@ func TestDatabase(t *testing.T) { containerType: ContainerTypePostgres, num: 1, }, + { + name: "multiple postgres", + containerType: ContainerTypePostgres, + num: 2, + }, } if TestModel == TestModelNormal { @@ -49,11 +53,6 @@ func TestDatabase(t *testing.T) { containerType: ContainerTypeMysql, num: 2, }, - { - name: "multiple postgres", - containerType: ContainerTypePostgres, - num: 2, - }, { name: "single sqlite", containerType: ContainerTypeSqlite, @@ -81,14 +80,21 @@ func TestDatabase(t *testing.T) { t.Run(test.name, func(t *testing.T) { if test.num == 0 { assert.Panics(t, func() { - Database(test.containerType, testDatabase, testUsername, testPassword, test.num) + Database(test.containerType, testUsername, testPassword, test.num) }) } else { - drivers := Database(test.containerType, testDatabase, testUsername, testPassword, test.num) + drivers := Database(test.containerType, testUsername, testPassword, test.num) assert.Len(t, drivers, test.num) - assert.Len(t, containers[test.containerType], test.num) } }) } } + +//func TestA(t *testing.T) { +// go Postgreses(2) +// go Postgreses(2) +// go Postgreses(2) +// +// time.Sleep(20 * time.Second) +//} diff --git a/support/docker/mysql.go b/support/docker/mysql.go index d9e7cb76b..eb9663e66 100644 --- a/support/docker/mysql.go +++ b/support/docker/mysql.go @@ -68,15 +68,33 @@ func (r *MysqlImpl) Build() error { func (r *MysqlImpl) Config() testing.DatabaseConfig { return testing.DatabaseConfig{ + ContainerID: r.containerID, Host: r.host, Port: r.port, Database: r.database, Username: r.username, Password: r.password, - ContainerID: r.containerID, } } +func (r *MysqlImpl) Database(name string) (testing.DatabaseDriver, error) { + instance, err := r.connect("root") + if err != nil { + return nil, fmt.Errorf("connect Mysql error: %v", err) + } + + res := instance.Exec(fmt.Sprintf("CREATE DATABASE %s;", name)) + if res.Error != nil { + return nil, fmt.Errorf("create Mysql database error: %v", res.Error) + } + + mysqlImpl := NewMysqlImpl(name, r.username, r.password) + mysqlImpl.containerID = r.containerID + mysqlImpl.port = r.port + + return mysqlImpl, nil +} + func (r *MysqlImpl) Driver() database.Driver { return database.DriverMysql } @@ -120,16 +138,21 @@ func (r *MysqlImpl) Stop() error { return nil } -func (r *MysqlImpl) connect() (*gormio.DB, error) { +func (r *MysqlImpl) connect(username ...string) (*gormio.DB, error) { var ( instance *gormio.DB err error ) + useUsername := r.username + if len(username) > 0 { + useUsername = username[0] + } + // docker compose need time to start for i := 0; i < 60; i++ { instance, err = gormio.Open(mysql.New(mysql.Config{ - DSN: fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", r.username, r.password, r.host, r.port, r.database), + DSN: fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", useUsername, r.password, r.host, r.port, r.database), })) if err == nil { diff --git a/support/docker/postgres.go b/support/docker/postgres.go index c69ea9749..4ee07cc65 100644 --- a/support/docker/postgres.go +++ b/support/docker/postgres.go @@ -40,8 +40,8 @@ func NewPostgresImpl(database, username, password string) *PostgresImpl { } } -func (receiver *PostgresImpl) Build() error { - command, exposedPorts := imageToCommand(receiver.image) +func (r *PostgresImpl) Build() error { + command, exposedPorts := imageToCommand(r.image) containerID, err := run(command) if err != nil { return fmt.Errorf("init Postgres error: %v", err) @@ -50,32 +50,51 @@ func (receiver *PostgresImpl) Build() error { return fmt.Errorf("no container id return when creating Postgres docker") } - receiver.containerID = containerID - receiver.port = getExposedPort(exposedPorts, 5432) + r.containerID = containerID + r.port = getExposedPort(exposedPorts, 5432) - if _, err := receiver.connect(); err != nil { + if _, err := r.connect(); err != nil { return fmt.Errorf("connect Postgres error: %v", err) } return nil } -func (receiver *PostgresImpl) Config() testing.DatabaseConfig { +func (r *PostgresImpl) Config() testing.DatabaseConfig { return testing.DatabaseConfig{ - Host: receiver.host, - Port: receiver.port, - Database: receiver.database, - Username: receiver.username, - Password: receiver.password, + ContainerID: r.containerID, + Host: r.host, + Port: r.port, + Database: r.database, + Username: r.username, + Password: r.password, } } -func (receiver *PostgresImpl) Driver() database.Driver { +func (r *PostgresImpl) Database(name string) (testing.DatabaseDriver, error) { + instance, err := r.connect() + if err != nil { + return nil, fmt.Errorf("connect Mysql error: %v", err) + } + + res := instance.Exec(fmt.Sprintf("CREATE DATABASE %s;", name)) + if res.Error != nil { + return nil, fmt.Errorf("create Mysql database error: %v", res.Error) + } + + postgresImpl := NewPostgresImpl(name, r.username, r.password) + postgresImpl.containerID = r.containerID + postgresImpl.port = r.port + + return postgresImpl, nil +} + +func (r *PostgresImpl) Driver() database.Driver { return database.DriverPostgres } -func (receiver *PostgresImpl) Fresh() error { - instance, err := receiver.connect() +func (r *PostgresImpl) Fresh() error { + instance, err := r.connect() if err != nil { return fmt.Errorf("connect Postgres error when clearing: %v", err) } @@ -91,19 +110,19 @@ func (receiver *PostgresImpl) Fresh() error { return nil } -func (receiver *PostgresImpl) Image(image testing.Image) { - receiver.image = &image +func (r *PostgresImpl) Image(image testing.Image) { + r.image = &image } -func (receiver *PostgresImpl) Stop() error { - if _, err := run(fmt.Sprintf("docker stop %s", receiver.containerID)); err != nil { +func (r *PostgresImpl) Stop() error { + if _, err := run(fmt.Sprintf("docker stop %s", r.containerID)); err != nil { return fmt.Errorf("stop Postgres error: %v", err) } return nil } -func (receiver *PostgresImpl) connect() (*gormio.DB, error) { +func (r *PostgresImpl) connect() (*gormio.DB, error) { var ( instance *gormio.DB err error @@ -112,7 +131,7 @@ func (receiver *PostgresImpl) connect() (*gormio.DB, error) { // docker compose need time to start for i := 0; i < 60; i++ { instance, err = gormio.Open(postgres.New(postgres.Config{ - DSN: fmt.Sprintf("postgres://%s:%s@%s:%d/%s", receiver.username, receiver.password, receiver.host, receiver.port, receiver.database), + DSN: fmt.Sprintf("postgres://%s:%s@%s:%d/%s", r.username, r.password, r.host, r.port, r.database), })) if err == nil { diff --git a/support/docker/sqlite.go b/support/docker/sqlite.go index 01cc733da..37b77c67f 100644 --- a/support/docker/sqlite.go +++ b/support/docker/sqlite.go @@ -22,48 +22,57 @@ func NewSqliteImpl(database string) *SqliteImpl { } } -func (receiver *SqliteImpl) Build() error { - if _, err := receiver.connect(); err != nil { +func (r *SqliteImpl) Build() error { + if _, err := r.connect(); err != nil { return fmt.Errorf("connect Sqlite error: %v", err) } return nil } -func (receiver *SqliteImpl) Config() testing.DatabaseConfig { +func (r *SqliteImpl) Config() testing.DatabaseConfig { return testing.DatabaseConfig{ - Database: receiver.database, + Database: r.database, } } -func (receiver *SqliteImpl) Driver() database.Driver { +func (r *SqliteImpl) Database(name string) (testing.DatabaseDriver, error) { + sqlserverImpl := NewSqliteImpl(name) + if err := sqlserverImpl.Build(); err != nil { + return nil, err + } + + return sqlserverImpl, nil +} + +func (r *SqliteImpl) Driver() database.Driver { return database.DriverSqlite } -func (receiver *SqliteImpl) Fresh() error { - if err := receiver.Stop(); err != nil { +func (r *SqliteImpl) Fresh() error { + if err := r.Stop(); err != nil { return err } - if _, err := receiver.connect(); err != nil { + if _, err := r.connect(); err != nil { return fmt.Errorf("connect Sqlite error when freshing: %v", err) } return nil } -func (receiver *SqliteImpl) Image(image testing.Image) { - receiver.image = &image +func (r *SqliteImpl) Image(image testing.Image) { + r.image = &image } -func (receiver *SqliteImpl) Stop() error { - if err := file.Remove(receiver.database); err != nil { +func (r *SqliteImpl) Stop() error { + if err := file.Remove(r.database); err != nil { return fmt.Errorf("stop Sqlite error: %v", err) } return nil } -func (receiver *SqliteImpl) connect() (*gormio.DB, error) { - return gormio.Open(sqlite.Open(fmt.Sprintf("%s?multi_stmts=true", receiver.database))) +func (r *SqliteImpl) connect() (*gormio.DB, error) { + return gormio.Open(sqlite.Open(fmt.Sprintf("%s?multi_stmts=true", r.database))) } diff --git a/support/docker/sqlserver.go b/support/docker/sqlserver.go index 2e39a4c04..96e6d57c2 100644 --- a/support/docker/sqlserver.go +++ b/support/docker/sqlserver.go @@ -39,8 +39,8 @@ func NewSqlserverImpl(database, username, password string) *SqlserverImpl { } } -func (receiver *SqlserverImpl) Build() error { - command, exposedPorts := imageToCommand(receiver.image) +func (r *SqlserverImpl) Build() error { + command, exposedPorts := imageToCommand(r.image) containerID, err := run(command) if err != nil { return fmt.Errorf("init Sqlserver docker error: %v", err) @@ -49,32 +49,46 @@ func (receiver *SqlserverImpl) Build() error { return fmt.Errorf("no container id return when creating Sqlserver docker") } - receiver.containerID = containerID - receiver.port = getExposedPort(exposedPorts, 1433) + r.containerID = containerID + r.port = getExposedPort(exposedPorts, 1433) - if _, err := receiver.connect(); err != nil { + if _, err := r.connect(); err != nil { return fmt.Errorf("connect Sqlserver docker error: %v", err) } return nil } -func (receiver *SqlserverImpl) Config() testing.DatabaseConfig { +func (r *SqlserverImpl) Config() testing.DatabaseConfig { return testing.DatabaseConfig{ - Host: receiver.host, - Port: receiver.port, - Database: receiver.database, - Username: receiver.username, - Password: receiver.password, + ContainerID: r.containerID, + Host: r.host, + Port: r.port, + Database: r.database, + Username: r.username, + Password: r.password, } } -func (receiver *SqlserverImpl) Driver() database.Driver { +func (r *SqlserverImpl) Database(name string) (testing.DatabaseDriver, error) { + sqlserverImpl := NewSqlserverImpl(name, r.username, r.password) + sqlserverImpl.containerID = r.containerID + sqlserverImpl.port = r.port + + _, err := r.connect() + if err != nil { + return nil, fmt.Errorf("connect Mysql error: %v", err) + } + + return sqlserverImpl, nil +} + +func (r *SqlserverImpl) Driver() database.Driver { return database.DriverSqlserver } -func (receiver *SqlserverImpl) Fresh() error { - instance, err := receiver.connect() +func (r *SqlserverImpl) Fresh() error { + instance, err := r.connect() if err != nil { return fmt.Errorf("connect Sqlserver error when clearing: %v", err) } @@ -100,19 +114,19 @@ func (receiver *SqlserverImpl) Fresh() error { return nil } -func (receiver *SqlserverImpl) Image(image testing.Image) { - receiver.image = &image +func (r *SqlserverImpl) Image(image testing.Image) { + r.image = &image } -func (receiver *SqlserverImpl) Stop() error { - if _, err := run(fmt.Sprintf("docker stop %s", receiver.containerID)); err != nil { +func (r *SqlserverImpl) Stop() error { + if _, err := run(fmt.Sprintf("docker stop %s", r.containerID)); err != nil { return fmt.Errorf("stop Sqlserver error: %v", err) } return nil } -func (receiver *SqlserverImpl) connect() (*gormio.DB, error) { +func (r *SqlserverImpl) connect() (*gormio.DB, error) { var ( instance *gormio.DB err error @@ -122,42 +136,42 @@ func (receiver *SqlserverImpl) connect() (*gormio.DB, error) { for i := 0; i < 100; i++ { instance, err = gormio.Open(sqlserver.New(sqlserver.Config{ DSN: fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=master", - "sa", receiver.password, receiver.host, receiver.port), + "sa", r.password, r.host, r.port), })) if err == nil { // Check if database exists var exists bool - query := fmt.Sprintf("SELECT CASE WHEN EXISTS (SELECT * FROM sys.databases WHERE name = '%s') THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) END", receiver.database) + query := fmt.Sprintf("SELECT CASE WHEN EXISTS (SELECT * FROM sys.databases WHERE name = '%s') THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) END", r.database) if err := instance.Raw(query).Scan(&exists).Error; err != nil { return nil, err } if !exists { // Create User database - if err := instance.Exec(fmt.Sprintf("CREATE DATABASE %s", receiver.database)).Error; err != nil { + if err := instance.Exec(fmt.Sprintf("CREATE DATABASE %s", r.database)).Error; err != nil { return nil, err } // Create User account - if err := instance.Exec(fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD = '%s'", receiver.username, receiver.password)).Error; err != nil { + if err := instance.Exec(fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD = '%s'", r.username, r.password)).Error; err != nil { return nil, err } // Create DB account for User - if err := instance.Exec(fmt.Sprintf("USE %s; CREATE USER %s FOR LOGIN %s", receiver.database, receiver.username, receiver.username)).Error; err != nil { + if err := instance.Exec(fmt.Sprintf("USE %s; CREATE USER %s FOR LOGIN %s", r.database, r.username, r.username)).Error; err != nil { return nil, err } // Add permission - if err := instance.Exec(fmt.Sprintf("USE %s; ALTER ROLE db_owner ADD MEMBER %s", receiver.database, receiver.username)).Error; err != nil { + if err := instance.Exec(fmt.Sprintf("USE %s; ALTER ROLE db_owner ADD MEMBER %s", r.database, r.username)).Error; err != nil { return nil, err } } instance, err = gormio.Open(sqlserver.New(sqlserver.Config{ DSN: fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", - receiver.username, receiver.password, receiver.host, receiver.port, receiver.database), + r.username, r.password, r.host, r.port, r.database), })) break diff --git a/support/docker/utils.go b/support/docker/utils.go index e791f908f..e18c437b8 100644 --- a/support/docker/utils.go +++ b/support/docker/utils.go @@ -6,7 +6,6 @@ import ( "math/rand" "net" "os/exec" - "strconv" "strings" "github.com/spf13/cast" @@ -15,6 +14,22 @@ import ( "github.com/goravel/framework/support/str" ) +// Used by TestContainer, to simulate the port is using. +var testPortUsing = false + +func isPortUsing(port int) bool { + if testPortUsing { + return true + } + + l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if l != nil { + l.Close() + } + + return err != nil +} + func getExposedPort(exposedPorts []string, port int) int { for _, exposedPort := range exposedPorts { if !strings.Contains(exposedPort, cast.ToString(port)) { @@ -32,7 +47,7 @@ func getExposedPort(exposedPorts []string, port int) int { func getValidPort() int { for i := 0; i < 60; i++ { random := rand.Intn(10000) + 10000 - l, err := net.Listen("tcp", fmt.Sprintf(":%s", strconv.Itoa(random))) + l, err := net.Listen("tcp", fmt.Sprintf(":%d", random)) if err != nil { continue } diff --git a/support/env/env.go b/support/env/env.go index e2f0bcb0b..178ec4b13 100644 --- a/support/env/env.go +++ b/support/env/env.go @@ -1,20 +1,28 @@ package env import ( + "log" "os" + "path/filepath" "runtime" + "strings" ) -// IsWindows returns whether the current operating system is Windows. -// IsWindows 返回当前操作系统是否为 Windows。 -func IsWindows() bool { - return runtime.GOOS == "windows" +// IsAir checks if the application is running using Air. +func IsAir() bool { + for _, arg := range os.Args { + if strings.Contains(filepath.ToSlash(arg), "/storage/temp") { + return true + } + } + + return false } -// IsLinux returns whether the current operating system is Linux. -// IsLinux 返回当前操作系统是否为 Linux。 -func IsLinux() bool { - return runtime.GOOS == "linux" +// IsArm returns whether the current CPU architecture is ARM. +// IsArm 返回当前 CPU 架构是否为 ARM。 +func IsArm() bool { + return runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" } // IsDarwin returns whether the current operating system is Darwin. @@ -23,10 +31,42 @@ func IsDarwin() bool { return runtime.GOOS == "darwin" } -// IsArm returns whether the current CPU architecture is ARM. -// IsArm 返回当前 CPU 架构是否为 ARM。 -func IsArm() bool { - return runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" +// IsDirectlyRun checks if the application is running using go run . +func IsDirectlyRun() bool { + executable, _ := os.Executable() + return strings.Contains(filepath.Base(executable), os.TempDir()) || + (strings.Contains(filepath.ToSlash(executable), "/var/folders") && strings.Contains(filepath.ToSlash(executable), "/T/go-build")) // macOS +} + +// IsGithub returns whether the current environment is github action. +// IsGithub 返回当前系统环境是否为 github action。 +func IsGithub() bool { + _, exists := os.LookupEnv("GITHUB_ACTION") + + return exists +} + +// IsLinux returns whether the current operating system is Linux. +// IsLinux 返回当前操作系统是否为 Linux。 +func IsLinux() bool { + return runtime.GOOS == "linux" +} + +// IsTesting checks if the application is running in testing mode. +func IsTesting() bool { + for _, arg := range os.Args { + if strings.Contains(arg, "-test.") { + return true + } + } + + return false +} + +// IsWindows returns whether the current operating system is Windows. +// IsWindows 返回当前操作系统是否为 Windows。 +func IsWindows() bool { + return runtime.GOOS == "windows" } // IsX86 returns whether the current CPU architecture is X86. @@ -41,10 +81,16 @@ func Is64Bit() bool { return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" } -// IsGithub returns whether the current environment is github action. -// IsGithub 返回当前系统环境是否为 github action。 -func IsGithub() bool { - _, exists := os.LookupEnv("GITHUB_ACTION") +func CurrentAbsolutePath() string { + executable, err := os.Executable() + if err != nil { + log.Fatal(err) + } + res, _ := filepath.EvalSymlinks(filepath.Dir(executable)) - return exists + if IsTesting() || IsAir() || IsDirectlyRun() { + res, _ = os.Getwd() + } + + return res } diff --git a/testing/docker/database.go b/testing/docker/database.go index adf45dbae..2bc477dbb 100644 --- a/testing/docker/database.go +++ b/testing/docker/database.go @@ -40,7 +40,7 @@ func NewDatabase(app foundation.Application, connection string) (*Database, erro database := config.GetString(fmt.Sprintf("database.connections.%s.database", connection)) username := config.GetString(fmt.Sprintf("database.connections.%s.username", connection)) password := config.GetString(fmt.Sprintf("database.connections.%s.password", connection)) - databaseDriver := supportdocker.DatabaseDriver(supportdocker.ContainerType(driver), database, username, password) + databaseDriver := supportdocker.NewDatabaseDriver(supportdocker.ContainerType(driver), database, username, password) return &Database{ DatabaseDriver: databaseDriver,