diff --git a/contracts/binding/binding.go b/contracts/binding/binding.go index 600c7e15b..791d12531 100644 --- a/contracts/binding/binding.go +++ b/contracts/binding/binding.go @@ -383,6 +383,7 @@ var ( Cache, Config, Orm, + Process, Route, Session, }, diff --git a/errors/list.go b/errors/list.go index 6ddf89653..ec38509f6 100644 --- a/errors/list.go +++ b/errors/list.go @@ -9,6 +9,7 @@ var ( JSONParserNotSet = New("JSON parser is not initialized") LogFacadeNotSet = New("log facade is not initialized") OrmFacadeNotSet = New("orm facade is not initialized") + ProcessFacadeNotSet = New("process facade is not initialized") QueueFacadeNotSet = New("queue facade is not initialized") RateLimiterFacadeNotSet = New("rate limiter facade is not initialized") ScheduleFacadeNotSet = New("schedule facade is not initialized") diff --git a/process/pipeline.go b/process/pipeline.go index d17f990bd..b1f515688 100644 --- a/process/pipeline.go +++ b/process/pipeline.go @@ -242,6 +242,7 @@ type Pipe struct { } func (r *Pipe) Command(name string, args ...string) contractsprocess.PipeCommand { + name, args = formatCommand(name, args) command := NewPipeCommand(strconv.Itoa(len(r.commands)), name, args) r.commands = append(r.commands, command) return command diff --git a/process/pool.go b/process/pool.go index 28131e94a..84f196d1d 100644 --- a/process/pool.go +++ b/process/pool.go @@ -225,6 +225,7 @@ type Pool struct { } func (r *Pool) Command(name string, args ...string) contractsprocess.PoolCommand { + name, args = formatCommand(name, args) command := NewPoolCommand(strconv.Itoa(len(r.commands)), name, args) r.commands = append(r.commands, command) return command diff --git a/process/process.go b/process/process.go index 110dbaf30..9f0ef65b9 100644 --- a/process/process.go +++ b/process/process.go @@ -9,6 +9,8 @@ import ( "time" contractsprocess "github.com/goravel/framework/contracts/process" + "github.com/goravel/framework/support/env" + "github.com/goravel/framework/support/str" ) var _ contractsprocess.Process = (*Process)(nil) @@ -78,6 +80,7 @@ func (r *Process) Quietly() contractsprocess.Process { } func (r *Process) Run(name string, args ...string) contractsprocess.Result { + name, args = formatCommand(name, args) run, err := r.start(name, args...) if err != nil { return NewResult(err, 1, "", "", "") @@ -185,3 +188,17 @@ func (r *Process) start(name string, args ...string) (contractsprocess.Running, return NewRunning(ctx, cmd, cancel, stdoutBuffer, stderrBuffer, r.loading, r.loadingMessage), nil } + +func formatCommand(name string, args []string) (string, []string) { + if len(args) == 0 && str.Of(name).Contains(" ", "&", "|") { + if env.IsWindows() { + args = []string{"/c", name} + name = "cmd" + } else { + args = []string{"-c", name} + name = "/bin/sh" + } + } + + return name, args +} diff --git a/process/process_unix_test.go b/process/process_unix_test.go index 79aa05638..a9045e6f8 100644 --- a/process/process_unix_test.go +++ b/process/process_unix_test.go @@ -257,3 +257,106 @@ func TestProcess_Pipe_Unix(t *testing.T) { assert.ErrorIs(t, result.Error(), errors.ProcessPipeNilConfigurer) }) } + +func TestFormatCommand_Unix(t *testing.T) { + tests := []struct { + name string + inputName string + inputArgs []string + expectedName string + expectedArgs []string + }{ + { + name: "command with args - not wrapped", + inputName: "echo", + inputArgs: []string{"hello", "world"}, + expectedName: "echo", + expectedArgs: []string{"hello", "world"}, + }, + { + name: "simple command - not wrapped", + inputName: "ls", + inputArgs: []string{}, + expectedName: "ls", + expectedArgs: []string{}, + }, + { + name: "command with space only - wrapped", + inputName: "echo hello", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "echo hello"}, + }, + { + name: "command with space and ampersand but has args - not wrapped", + inputName: "sleep 5 &", + inputArgs: []string{"-v"}, + expectedName: "sleep 5 &", + expectedArgs: []string{"-v"}, + }, + { + name: "background command - wrapped", + inputName: "sleep 5 &", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "sleep 5 &"}, + }, + { + name: "piped command - wrapped", + inputName: "cat file.txt | grep test", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "cat file.txt | grep test"}, + }, + { + name: "piped background command - wrapped", + inputName: "cat file.txt | grep test &", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "cat file.txt | grep test &"}, + }, + { + name: "logical AND operators - wrapped", + inputName: "echo hello && echo world", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "echo hello && echo world"}, + }, + { + name: "multiple pipes - wrapped", + inputName: "cat file | sort | uniq", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "cat file | sort | uniq"}, + }, + { + name: "command with ampersand only - wrapped", + inputName: "test&", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "test&"}, + }, + { + name: "command with pipe only - wrapped", + inputName: "test|", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "test|"}, + }, + { + name: "single ampersand - wrapped", + inputName: "&", + inputArgs: []string{}, + expectedName: "/bin/sh", + expectedArgs: []string{"-c", "&"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotName, gotArgs := formatCommand(tt.inputName, tt.inputArgs) + assert.Equal(t, tt.expectedName, gotName) + assert.Equal(t, tt.expectedArgs, gotArgs) + }) + } +} diff --git a/process/process_windows_test.go b/process/process_windows_test.go index 0040866d7..8bbaf56e7 100644 --- a/process/process_windows_test.go +++ b/process/process_windows_test.go @@ -145,3 +145,106 @@ func TestProcess_Pipe_Windows(t *testing.T) { assert.Equal(t, errors.ProcessPipeNilConfigurer, res.Error()) }) } + +func TestFormatCommand_Windows(t *testing.T) { + tests := []struct { + name string + inputName string + inputArgs []string + expectedName string + expectedArgs []string + }{ + { + name: "command with args - not wrapped", + inputName: "echo", + inputArgs: []string{"hello", "world"}, + expectedName: "echo", + expectedArgs: []string{"hello", "world"}, + }, + { + name: "simple command - not wrapped", + inputName: "dir", + inputArgs: []string{}, + expectedName: "dir", + expectedArgs: []string{}, + }, + { + name: "command with space only - wrapped", + inputName: "echo hello", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "echo hello"}, + }, + { + name: "command with space and ampersand but has args - not wrapped", + inputName: "timeout 5 &", + inputArgs: []string{"/nobreak"}, + expectedName: "timeout 5 &", + expectedArgs: []string{"/nobreak"}, + }, + { + name: "background command - wrapped", + inputName: "timeout 5 &", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "timeout 5 &"}, + }, + { + name: "piped command - wrapped", + inputName: "type file.txt | findstr test", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "type file.txt | findstr test"}, + }, + { + name: "piped background command - wrapped", + inputName: "type file.txt | findstr test &", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "type file.txt | findstr test &"}, + }, + { + name: "logical AND operators - wrapped", + inputName: "echo hello && echo world", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "echo hello && echo world"}, + }, + { + name: "multiple pipes - wrapped", + inputName: "type file | sort | findstr test", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "type file | sort | findstr test"}, + }, + { + name: "command with ampersand only - wrapped", + inputName: "test&", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "test&"}, + }, + { + name: "command with pipe only - wrapped", + inputName: "test|", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "test|"}, + }, + { + name: "single ampersand - wrapped", + inputName: "&", + inputArgs: []string{}, + expectedName: "cmd", + expectedArgs: []string{"/c", "&"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotName, gotArgs := formatCommand(tt.inputName, tt.inputArgs) + assert.Equal(t, tt.expectedName, gotName) + assert.Equal(t, tt.expectedArgs, gotArgs) + }) + } +} diff --git a/support/docker/utils.go b/support/docker/utils.go index 2d82ec558..7f816ac77 100644 --- a/support/docker/utils.go +++ b/support/docker/utils.go @@ -2,10 +2,12 @@ package docker import ( "fmt" + "math/rand" + "net" "strings" "github.com/goravel/framework/contracts/testing/docker" - "github.com/goravel/framework/support/process" + "github.com/goravel/framework/errors" ) func ExposedPort(exposedPorts []string, port string) string { @@ -41,7 +43,7 @@ func ImageToCommand(image *docker.Image) (command string, exposedPorts []string) if len(image.ExposedPorts) > 0 { for _, port := range image.ExposedPorts { if !strings.Contains(port, ":") { - port = fmt.Sprintf("%d:%s", process.ValidPort(), port) + port = fmt.Sprintf("%d:%s", ValidPort(), port) } ports = append(ports, port) commands = append(commands, "-p", port) @@ -60,3 +62,30 @@ func ImageToCommand(image *docker.Image) (command string, exposedPorts []string) return strings.Join(commands, " "), ports } + +// 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 { + errors.Ignore(l.Close) + } + + return err != nil +} + +func ValidPort() int { + for range 60 { + random := rand.Intn(10000) + 10000 + if !IsPortUsing(random) { + return random + } + } + + return 0 +} diff --git a/support/docker/utils_test.go b/support/docker/utils_test.go index 65958d1e2..cbf474525 100644 --- a/support/docker/utils_test.go +++ b/support/docker/utils_test.go @@ -56,3 +56,7 @@ func TestImageToCommand(t *testing.T) { }) assert.Equal(t, "docker run --rm -d -e a=b -p 1234:6379 redis:latest --a=b sleep 1000", command) } + +func TestValidPort(t *testing.T) { + assert.True(t, ValidPort() > 0) +} diff --git a/support/process/process.go b/support/process/process.go deleted file mode 100644 index eed3f35d9..000000000 --- a/support/process/process.go +++ /dev/null @@ -1,25 +0,0 @@ -package process - -import ( - "bytes" - "fmt" - "os/exec" - - "github.com/goravel/framework/support/str" -) - -func Run(command string) (string, error) { - cmd := exec.Command("/bin/sh", "-c", command) - - var out bytes.Buffer - var stderr bytes.Buffer - - cmd.Stdout = &out - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("%s: %s", err, stderr.String()) - } - - return str.Of(out.String()).Squish().String(), nil -} diff --git a/support/process/process_test.go b/support/process/process_test.go deleted file mode 100644 index 1cd5ef6c1..000000000 --- a/support/process/process_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package process - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/goravel/framework/support/env" -) - -func TestRun(t *testing.T) { - if env.IsWindows() { - t.Skip("Skip test") - } - - _, err := Run("ls") - assert.Nil(t, err) -} diff --git a/support/process/utils.go b/support/process/utils.go deleted file mode 100644 index b9632ef13..000000000 --- a/support/process/utils.go +++ /dev/null @@ -1,36 +0,0 @@ -package process - -import ( - "fmt" - "math/rand" - "net" - - "github.com/goravel/framework/errors" -) - -// 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 { - errors.Ignore(l.Close) - } - - return err != nil -} - -func ValidPort() int { - for range 60 { - random := rand.Intn(10000) + 10000 - if !IsPortUsing(random) { - return random - } - } - - return 0 -} diff --git a/support/process/utils_test.go b/support/process/utils_test.go deleted file mode 100644 index 763d84769..000000000 --- a/support/process/utils_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package process - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidPort(t *testing.T) { - assert.True(t, ValidPort() > 0) -} diff --git a/testing/application.go b/testing/application.go index 705d4e70e..463f582ac 100644 --- a/testing/application.go +++ b/testing/application.go @@ -5,6 +5,7 @@ import ( contractsconfig "github.com/goravel/framework/contracts/config" contractsconsole "github.com/goravel/framework/contracts/console" contractsorm "github.com/goravel/framework/contracts/database/orm" + contractsprocess "github.com/goravel/framework/contracts/process" "github.com/goravel/framework/contracts/testing" "github.com/goravel/framework/testing/docker" ) @@ -14,17 +15,25 @@ type Application struct { cache contractscache.Cache config contractsconfig.Config orm contractsorm.Orm + process contractsprocess.Process } -func NewApplication(artisan contractsconsole.Artisan, cache contractscache.Cache, config contractsconfig.Config, orm contractsorm.Orm) *Application { +func NewApplication( + artisan contractsconsole.Artisan, + cache contractscache.Cache, + config contractsconfig.Config, + orm contractsorm.Orm, + process contractsprocess.Process, +) *Application { return &Application{ artisan: artisan, cache: cache, config: config, orm: orm, + process: process, } } func (r *Application) Docker() testing.Docker { - return docker.NewDocker(r.artisan, r.cache, r.config, r.orm) + return docker.NewDocker(r.artisan, r.cache, r.config, r.orm, r.process) } diff --git a/testing/docker/docker.go b/testing/docker/docker.go index 9fa6bf46d..d16d381f2 100644 --- a/testing/docker/docker.go +++ b/testing/docker/docker.go @@ -5,6 +5,7 @@ import ( contractsconfig "github.com/goravel/framework/contracts/config" contractsconsole "github.com/goravel/framework/contracts/console" contractsorm "github.com/goravel/framework/contracts/database/orm" + contractsprocess "github.com/goravel/framework/contracts/process" "github.com/goravel/framework/contracts/testing/docker" "github.com/goravel/framework/errors" ) @@ -14,22 +15,29 @@ type Docker struct { cache contractscache.Cache config contractsconfig.Config orm contractsorm.Orm + process contractsprocess.Process } -func NewDocker(artisan contractsconsole.Artisan, cache contractscache.Cache, config contractsconfig.Config, orm contractsorm.Orm) *Docker { +func NewDocker( + artisan contractsconsole.Artisan, + cache contractscache.Cache, + config contractsconfig.Config, + orm contractsorm.Orm, + process contractsprocess.Process, +) *Docker { return &Docker{ artisan: artisan, cache: cache, config: config, orm: orm, + process: process, } } func (r *Docker) Cache(store ...string) (docker.CacheDriver, error) { - if r.config == nil { - return nil, errors.ConfigFacadeNotSet + if r.cache == nil { + return nil, errors.CacheFacadeNotSet.SetModule(errors.ModuleTesting) } - if len(store) == 0 { store = append(store, r.config.GetString("cache.default")) } @@ -38,6 +46,13 @@ func (r *Docker) Cache(store ...string) (docker.CacheDriver, error) { } func (r *Docker) Database(connection ...string) (docker.Database, error) { + if r.artisan == nil { + return nil, errors.ConsoleFacadeNotSet.SetModule(errors.ModuleTesting) + } + if r.orm == nil { + return nil, errors.OrmFacadeNotSet.SetModule(errors.ModuleTesting) + } + if len(connection) == 0 { return NewDatabase(r.artisan, r.config, r.orm, "") } else { @@ -46,5 +61,5 @@ func (r *Docker) Database(connection ...string) (docker.Database, error) { } func (r *Docker) Image(image docker.Image) docker.ImageDriver { - return NewImageDriver(image) + return NewImageDriver(image, r.process) } diff --git a/testing/docker/docker_test.go b/testing/docker/docker_test.go index 931c99175..0d991871a 100644 --- a/testing/docker/docker_test.go +++ b/testing/docker/docker_test.go @@ -11,6 +11,7 @@ import ( mocksconfig "github.com/goravel/framework/mocks/config" mocksconsole "github.com/goravel/framework/mocks/console" mocksorm "github.com/goravel/framework/mocks/database/orm" + mocksprocess "github.com/goravel/framework/mocks/process" mocksdocker "github.com/goravel/framework/mocks/testing/docker" ) @@ -20,6 +21,7 @@ type DockerTestSuite struct { mockCache *mockscache.Cache mockConfig *mocksconfig.Config mockOrm *mocksorm.Orm + mockProcess *mocksprocess.Process mockCacheDriver *mockscache.Driver mockDocker *mocksdocker.CacheDriver docker *Docker @@ -34,9 +36,10 @@ func (s *DockerTestSuite) SetupTest() { s.mockCache = mockscache.NewCache(s.T()) s.mockConfig = mocksconfig.NewConfig(s.T()) s.mockOrm = mocksorm.NewOrm(s.T()) + s.mockProcess = mocksprocess.NewProcess(s.T()) s.mockCacheDriver = mockscache.NewDriver(s.T()) s.mockDocker = mocksdocker.NewCacheDriver(s.T()) - s.docker = NewDocker(s.mockArtisan, s.mockCache, s.mockConfig, s.mockOrm) + s.docker = NewDocker(s.mockArtisan, s.mockCache, s.mockConfig, s.mockOrm, s.mockProcess) } func (s *DockerTestSuite) TestCache() { @@ -66,12 +69,12 @@ func (s *DockerTestSuite) TestCache() { wantErr: nil, }, { - name: "error when config is nil", + name: "error when cache is nil", store: []string{}, setup: func() { - s.docker.config = nil + s.docker.cache = nil }, - wantErr: errors.ConfigFacadeNotSet, + wantErr: errors.CacheFacadeNotSet, }, { name: "error when docker returns error", diff --git a/testing/docker/image.go b/testing/docker/image.go index eb40c1279..f0ab1bdf1 100644 --- a/testing/docker/image.go +++ b/testing/docker/image.go @@ -5,29 +5,38 @@ import ( "fmt" "time" + contractsprocess "github.com/goravel/framework/contracts/process" contractsdocker "github.com/goravel/framework/contracts/testing/docker" "github.com/goravel/framework/errors" supportdocker "github.com/goravel/framework/support/docker" - "github.com/goravel/framework/support/process" + "github.com/goravel/framework/support/str" ) type ImageDriver struct { - config contractsdocker.ImageConfig - image contractsdocker.Image + config contractsdocker.ImageConfig + image contractsdocker.Image + process contractsprocess.Process } -func NewImageDriver(image contractsdocker.Image) *ImageDriver { +func NewImageDriver(image contractsdocker.Image, process contractsprocess.Process) *ImageDriver { return &ImageDriver{ - image: image, + image: image, + process: process, } } func (r *ImageDriver) Build() error { + if r.process == nil { + return errors.ProcessFacadeNotSet.SetModule(errors.ModuleTesting) + } + command, exposedPorts := supportdocker.ImageToCommand(&r.image) - containerID, err := process.Run(command) - if err != nil { - return errors.TestingImageBuildFailed.Args(r.image.Repository, err) + res := r.process.Run(command) + if res.Failed() { + return errors.TestingImageBuildFailed.Args(r.image.Repository, res.Error()) } + + containerID := str.Of(res.Output()).Squish().String() if containerID == "" { return errors.TestingImageNoContainerId.Args(r.image.Repository) } @@ -69,8 +78,8 @@ func (r *ImageDriver) Ready(fn func() error, durations ...time.Duration) error { func (r *ImageDriver) Shutdown() error { if r.config.ContainerID != "" { - if _, err := process.Run(fmt.Sprintf("docker stop %s", r.config.ContainerID)); err != nil { - return errors.TestingImageStopFailed.Args(r.image.Repository, err) + if res := r.process.Run(fmt.Sprintf("docker stop %s", r.config.ContainerID)); res.Failed() { + return errors.TestingImageStopFailed.Args(r.image.Repository, res.Error()) } } diff --git a/testing/docker/image_test.go b/testing/docker/image_test.go index d589ca209..ad9008596 100644 --- a/testing/docker/image_test.go +++ b/testing/docker/image_test.go @@ -1,20 +1,24 @@ package docker import ( - "errors" + "strings" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" contractsdocker "github.com/goravel/framework/contracts/testing/docker" + "github.com/goravel/framework/errors" + mocksprocess "github.com/goravel/framework/mocks/process" "github.com/goravel/framework/support/env" ) type ImageDriverTestSuite struct { suite.Suite - image contractsdocker.Image + image contractsdocker.Image + mockProcess *mocksprocess.Process } func TestImageDriverTestSuite(t *testing.T) { @@ -31,29 +35,98 @@ func (s *ImageDriverTestSuite) SetupTest() { Tag: "latest", ExposedPorts: []string{"6379"}, } + s.mockProcess = mocksprocess.NewProcess(s.T()) } func (s *ImageDriverTestSuite) TestNewImageDriver() { - driver := NewImageDriver(s.image) + driver := NewImageDriver(s.image, s.mockProcess) assert.NotNil(s.T(), driver) assert.Equal(s.T(), s.image, driver.image) } -func (s *ImageDriverTestSuite) TestBuildConfigReadyShutdown() { - driver := NewImageDriver(s.image) - err := driver.Build() - s.NoError(err) +func (s *ImageDriverTestSuite) TestBuildConfigShutdown() { + s.Run("happty path", func() { + containerID := "mocked-container-id" + mockProcessResult := mocksprocess.NewResult(s.T()) + mockProcessResult.EXPECT().Failed().Return(false).Once() + mockProcessResult.EXPECT().Output().Return(containerID).Once() + s.mockProcess.EXPECT().Run(mock.MatchedBy(func(command string) bool { + return strings.Contains(command, "docker run --rm -d -p ") && strings.Contains(command, ":6379 redis:latest") + })).Return(mockProcessResult).Once() - config := driver.Config() - s.NotEmpty(config.ContainerID) - s.True(len(config.ExposedPorts) > 0) + driver := NewImageDriver(s.image, s.mockProcess) + err := driver.Build() + s.NoError(err) - err = driver.Shutdown() - s.NoError(err) + config := driver.Config() + + s.Equal(containerID, config.ContainerID) + s.True(len(config.ExposedPorts) > 0) + + mockProcessResult.EXPECT().Failed().Return(false).Once() + s.mockProcess.EXPECT().Run("docker stop " + containerID).Return(mockProcessResult).Once() + + err = driver.Shutdown() + s.NoError(err) + }) + + s.Run("failed to shutdown", func() { + containerID := "mocked-container-id" + mockProcessResult := mocksprocess.NewResult(s.T()) + mockProcessResult.EXPECT().Failed().Return(false).Once() + mockProcessResult.EXPECT().Output().Return(containerID).Once() + s.mockProcess.EXPECT().Run(mock.MatchedBy(func(command string) bool { + return strings.Contains(command, "docker run --rm -d -p ") && strings.Contains(command, ":6379 redis:latest") + })).Return(mockProcessResult).Once() + + driver := NewImageDriver(s.image, s.mockProcess) + err := driver.Build() + s.NoError(err) + + config := driver.Config() + + s.Equal(containerID, config.ContainerID) + s.True(len(config.ExposedPorts) > 0) + + mockProcessResult.EXPECT().Failed().Return(true).Once() + mockProcessResult.EXPECT().Error().Return(assert.AnError).Once() + s.mockProcess.EXPECT().Run("docker stop " + containerID).Return(mockProcessResult).Once() + + err = driver.Shutdown() + s.Equal(errors.TestingImageStopFailed.Args(s.image.Repository, assert.AnError), err) + }) + + s.Run("containerID is empty", func() { + mockProcessResult := mocksprocess.NewResult(s.T()) + mockProcessResult.EXPECT().Failed().Return(false).Once() + mockProcessResult.EXPECT().Output().Return("").Once() + s.mockProcess.EXPECT().Run(mock.MatchedBy(func(command string) bool { + return strings.Contains(command, "docker run --rm -d -p ") && strings.Contains(command, ":6379 redis:latest") + })).Return(mockProcessResult).Once() + + driver := NewImageDriver(s.image, s.mockProcess) + err := driver.Build() + + s.Equal(errors.TestingImageNoContainerId.Args(s.image.Repository), err) + }) + + s.Run("failed to shutdown", func() { + mockProcessResult := mocksprocess.NewResult(s.T()) + mockProcessResult.EXPECT().Failed().Return(true).Once() + mockProcessResult.EXPECT().Error().Return(assert.AnError).Once() + s.mockProcess.EXPECT().Run(mock.MatchedBy(func(command string) bool { + return strings.Contains(command, "docker run --rm -d -p ") && strings.Contains(command, ":6379 redis:latest") + })).Return(mockProcessResult).Once() + + driver := NewImageDriver(s.image, s.mockProcess) + err := driver.Build() + + s.Equal(errors.TestingImageBuildFailed.Args(s.image.Repository, assert.AnError), err) + }) } func (s *ImageDriverTestSuite) TestReady() { - driver := NewImageDriver(s.image) + driver := NewImageDriver(s.image, s.mockProcess) err := driver.Ready(func() error { return nil diff --git a/testing/service_provider.go b/testing/service_provider.go index e72a7adf6..c82521595 100644 --- a/testing/service_provider.go +++ b/testing/service_provider.go @@ -32,7 +32,17 @@ func (r *ServiceProvider) Relationship() binding.Relationship { func (r *ServiceProvider) Register(app foundation.Application) { app.Singleton(binding.Testing, func(app foundation.Application) (any, error) { - return NewApplication(app.MakeArtisan(), app.MakeCache(), app.MakeConfig(), app.MakeOrm()), nil + config := app.MakeConfig() + if config == nil { + return nil, errors.ConfigFacadeNotSet.SetModule(errors.ModuleTesting) + } + + artisan := app.MakeArtisan() + cache := app.MakeCache() + orm := app.MakeOrm() + process := app.MakeProcess() + + return NewApplication(artisan, cache, config, orm, process), nil }) }