diff --git a/bootstrap/app.go b/bootstrap/app.go index c911360..0c7380b 100644 --- a/bootstrap/app.go +++ b/bootstrap/app.go @@ -26,6 +26,14 @@ import ( "goravel/routes" ) +// BootstrapRunners returns user-defined runners. Tests populate this slice to +// verify the disabled_runners config properly controls which runners start. +var BootstrapRunnerList []contractsfoundation.Runner + +var BootstrapRunners = func() []contractsfoundation.Runner { + return BootstrapRunnerList +} + func Boot() contractsfoundation.Application { return foundation.Setup(). WithMigrations(Migrations). @@ -89,6 +97,7 @@ func Boot() contractsfoundation.Application { paths.App("app") }). WithProviders(Providers). + WithRunners(BootstrapRunners). WithSchedule(func() []schedule.Event { return []schedule.Event{ facades.Schedule().Call(func() { diff --git a/config/app.go b/config/app.go index 7ed07f3..be1f525 100644 --- a/config/app.go +++ b/config/app.go @@ -73,5 +73,24 @@ func init() { "driver": config.Env("APP_MAINTENANCE_DRIVER", "file"), "store": config.Env("APP_MAINTENANCE_STORE", ""), }, + + // Disabled Runners + // + // Here you may specify which auto-run runners should be skipped when + // app.Start() is called. Patterns use path.Match glob matching. + // + // Framework runner signatures: + // goravel:http – HTTP server + // goravel:grpc – gRPC server + // goravel:queue – Queue worker + // goravel:schedule – Scheduler + // goravel:telemetry – Telemetry + // + // Example: disable scheduler for a web-only container + // "disabled_runners": []string{"goravel:schedule"}, + // + // Example: disable all framework runners + // "disabled_runners": []string{"goravel:*"}, + "disabled_runners": []string{}, }) } diff --git a/tests/feature/disabled_runners_test.go b/tests/feature/disabled_runners_test.go new file mode 100644 index 0000000..cb75c96 --- /dev/null +++ b/tests/feature/disabled_runners_test.go @@ -0,0 +1,143 @@ +package feature + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + contractsfoundation "github.com/goravel/framework/contracts/foundation" + "github.com/stretchr/testify/suite" + + "goravel/bootstrap" + "goravel/tests" +) + +type trackerRunner struct { + sig string + started chan struct{} + done chan struct{} + once sync.Once + didRun atomic.Bool +} + +func newTrackerRunner(sig string) *trackerRunner { + return &trackerRunner{ + sig: sig, + started: make(chan struct{}), + done: make(chan struct{}), + } +} + +func (r *trackerRunner) Signature() string { return r.sig } +func (r *trackerRunner) ShouldRun() bool { return true } + +func (r *trackerRunner) Run() error { + r.didRun.Store(true) + close(r.started) + <-r.done + return nil +} + +func (r *trackerRunner) Shutdown() error { + r.once.Do(func() { close(r.done) }) + return nil +} + +func (r *trackerRunner) Started() bool { return r.didRun.Load() } + +type DisabledRunnersTestSuite struct { + suite.Suite + tests.TestCase +} + +func TestDisabledRunnersTestSuite(t *testing.T) { + suite.Run(t, new(DisabledRunnersTestSuite)) +} + +func (s *DisabledRunnersTestSuite) SetupTest() { + bootstrap.BootstrapRunnerList = nil +} + +func (s *DisabledRunnersTestSuite) TearDownTest() { + bootstrap.BootstrapRunnerList = nil +} + +func (s *DisabledRunnersTestSuite) withRunners(runners []contractsfoundation.Runner, disabledPatterns []string) func() { + bootstrap.BootstrapRunnerList = runners + scope, err := tests.OverrideConfig(map[string]any{ + "app.disabled_runners": disabledPatterns, + }) + s.Require().NoError(err) + + return func() { + bootstrap.BootstrapRunnerList = nil + s.Require().NoError(scope.Restore()) + } +} + +func (s *DisabledRunnersTestSuite) waitForStart(runner *trackerRunner) { + s.Require().Eventually(func() bool { + return runner.Started() + }, 3*time.Second, 50*time.Millisecond) +} + +func (s *DisabledRunnersTestSuite) waitForNotStarted(runner *trackerRunner) { + s.Require().Never(func() bool { + return runner.Started() + }, 2*time.Second, 50*time.Millisecond) +} + +func (s *DisabledRunnersTestSuite) TestDisabledRunners_RunnerStartsWhenNotDisabled() { + runner := newTrackerRunner("test:demo") + cleanup := s.withRunners([]contractsfoundation.Runner{runner}, []string{}) + defer cleanup() + + s.waitForStart(runner) +} + +func (s *DisabledRunnersTestSuite) TestDisabledRunners_RunnerSkippedWhenDisabled() { + runner := newTrackerRunner("test:demo") + cleanup := s.withRunners([]contractsfoundation.Runner{runner}, []string{"test:demo"}) + defer cleanup() + + s.waitForNotStarted(runner) +} + +func (s *DisabledRunnersTestSuite) TestDisabledRunners_WildcardSkipsAll() { + runner := newTrackerRunner("test:demo") + cleanup := s.withRunners([]contractsfoundation.Runner{runner}, []string{"*"}) + defer cleanup() + + s.waitForNotStarted(runner) +} + +func (s *DisabledRunnersTestSuite) TestDisabledRunners_NamespaceWildcard() { + runner := newTrackerRunner("test:demo") + cleanup := s.withRunners([]contractsfoundation.Runner{runner}, []string{"test:*"}) + defer cleanup() + + s.waitForNotStarted(runner) +} + +func (s *DisabledRunnersTestSuite) TestDisabledRunners_SelectiveDisable() { + alpha := newTrackerRunner("test:alpha") + beta := newTrackerRunner("test:beta") + cleanup := s.withRunners([]contractsfoundation.Runner{alpha, beta}, []string{"test:alpha"}) + defer cleanup() + + s.waitForNotStarted(alpha) + s.waitForStart(beta) +} + +func (s *DisabledRunnersTestSuite) TestDisabledRunners_RestoreEnablesRunner() { + runner := newTrackerRunner("test:demo") + cleanup := s.withRunners([]contractsfoundation.Runner{runner}, []string{"test:demo"}) + s.waitForNotStarted(runner) + cleanup() + + runner2 := newTrackerRunner("test:demo") + cleanup = s.withRunners([]contractsfoundation.Runner{runner2}, []string{}) + defer cleanup() + s.waitForStart(runner2) +}