fs = new Filesystem; $this->id = Str::random(); $this->logfile = storage_path("logs/command_scheduling_test_{$this->id}.log"); $this->writeArtisanScript(); } protected function tearDown(): void { $this->fs->delete($this->logfile); $this->fs->delete(base_path('artisan')); if (! is_null($this->originalArtisan)) { $this->fs->put(base_path('artisan'), $this->originalArtisan); } parent::tearDown(); } /** * @dataProvider executionProvider */ public function testExecutionOrder($background) { $event = $this->app->make(Schedule::class) ->command("test:{$this->id}") ->onOneServer() ->after(function () { $this->fs->append($this->logfile, "after\n"); }) ->before(function () { $this->fs->append($this->logfile, "before\n"); }); if ($background) { $event->runInBackground(); } // We'll trigger the scheduler three times to simulate multiple servers $this->artisan('schedule:run'); $this->artisan('schedule:run'); $this->artisan('schedule:run'); if ($background) { // Since our command is running in a separate process, we need to wait // until it has finished executing before running our assertions. $this->waitForLogMessages('before', 'handled', 'after'); } $this->assertLogged('before', 'handled', 'after'); } public function executionProvider() { return [ 'Foreground' => [false], 'Background' => [true], ]; } protected function waitForLogMessages(...$messages) { $tries = 0; $sleep = 100000; // 100K microseconds = 0.1 second $limit = 50; // 0.1s * 50 = 5 second wait limit do { $log = $this->fs->get($this->logfile); if (Str::containsAll($log, $messages)) { return; } $tries++; usleep($sleep); } while ($tries < $limit); } protected function assertLogged(...$messages) { $log = trim($this->fs->get($this->logfile)); $this->assertEquals(implode("\n", $messages), $log); } protected function writeArtisanScript() { $path = base_path('artisan'); // Save existing artisan script if there is one if ($this->fs->exists($path)) { $this->originalArtisan = $this->fs->get($path); } $thisFile = __FILE__; $logfile = var_export($this->logfile, true); $script = <<make(Illuminate\Contracts\Console\Kernel::class); // Here is our custom command for the test class CommandSchedulingTestCommand_{$this->id} extends Illuminate\Console\Command { protected \$signature = 'test:{$this->id}'; public function handle() { \$logfile = {$logfile}; (new Illuminate\Filesystem\Filesystem)->append(\$logfile, "handled\\n"); } } // Register command with Kernel Illuminate\Console\Application::starting(function (\$artisan) { \$artisan->add(new CommandSchedulingTestCommand_{$this->id}); }); // Add command to scheduler so that the after() callback is trigger in our spawned process Illuminate\Foundation\Application::getInstance() ->booted(function (\$app) { \$app->resolving(Illuminate\Console\Scheduling\Schedule::class, function(\$schedule) { \$fs = new Illuminate\Filesystem\Filesystem; \$schedule->command("test:{$this->id}") ->after(function() use (\$fs) { \$logfile = {$logfile}; \$fs->append(\$logfile, "after\\n"); }) ->before(function() use (\$fs) { \$logfile = {$logfile}; \$fs->append(\$logfile, "before\\n"); }); }); }); \$status = \$kernel->handle( \$input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput ); \$kernel->terminate(\$input, \$status); exit(\$status); PHP; $this->fs->put($path, $script); } }