diff --git a/src/Foundation/Providers/ArtisanServiceProvider.php b/src/Foundation/Providers/ArtisanServiceProvider.php index 0cf920083..75d161c32 100644 --- a/src/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Foundation/Providers/ArtisanServiceProvider.php @@ -1,4 +1,6 @@ - \Illuminate\Foundation\Console\RouteClearCommand::class, 'RouteList' => \Illuminate\Foundation\Console\RouteListCommand::class, 'ScheduleFinish' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'ScheduleList' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, 'ScheduleRun' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'ScheduleTest' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'ScheduleWork' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, 'Up' => \Illuminate\Foundation\Console\UpCommand::class, 'ViewClear' => \Illuminate\Foundation\Console\ViewClearCommand::class, @@ -54,10 +59,7 @@ class ArtisanServiceProvider extends ArtisanServiceProviderBase // 'OptimizeClear' => OptimizeClearCommand::class, // 'QueueClear' => QueueClearCommand::class, // 'SchemaDump' => DumpCommand::class, - // 'ScheduleList' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, // 'ScheduleClearCache' => ScheduleClearCacheCommand::class, - // 'ScheduleTest' => ScheduleTestCommand::class, - // 'ScheduleWork' => ScheduleWorkCommand::class, // 'ViewCache' => ViewCacheCommand::class, // Explicitly unsupported in Winter: diff --git a/tests/Scheduling/ScheduleListCommandTest.php b/tests/Scheduling/ScheduleListCommandTest.php new file mode 100644 index 000000000..7e5e3b04c --- /dev/null +++ b/tests/Scheduling/ScheduleListCommandTest.php @@ -0,0 +1,135 @@ + 80); + + $this->schedule = $this->app->make(Schedule::class); + } + + public function testDisplayEmptySchedule() + { + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutputToContain('No scheduled tasks have been defined.'); + } + + public function testDisplaySchedule() + { + $this->schedule->command(FooCommand::class)->quarterly(); + $this->schedule->command('inspire')->twiceDaily(14, 18); + $this->schedule->command('foobar', ['a' => 'b'])->everyMinute(); + $this->schedule->job(FooJob::class)->everyMinute(); + $this->schedule->command('inspire')->cron('0 9,17 * * *'); + $this->schedule->command('inspire')->cron("0 10\t* * *"); + $this->schedule->call(FooCall::class)->everyMinute(); + $this->schedule->call([FooCall::class, 'fooFunction'])->everyMinute(); + + $this->schedule->call(fn () => '')->everyMinute(); + $closureLineNumber = __LINE__ - 1; + $closureFilePath = __FILE__; + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutput(' 0 0 1 1-12/3 * php artisan foo:command .... Next Due: 3 months from now') + ->expectsOutput(' 0 14,18 * * * php artisan inspire ........ Next Due: 14 hours from now') + ->expectsOutput(' * * * * * php artisan foobar a='.ProcessUtils::escapeArgument('b').' ... Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Winter\Storm\Tests\Scheduling\FooJob Next Due: 1 minute from now') + ->expectsOutput(' 0 9,17 * * * php artisan inspire ......... Next Due: 9 hours from now') + ->expectsOutput(' 0 10 * * * php artisan inspire ........ Next Due: 10 hours from now') + ->expectsOutput(' * * * * * Closure at: Winter\Storm\Tests\Scheduling\FooCall Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Closure at: Winter\Storm\Tests\Scheduling\FooCall::fooFunction Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Closure at: '.$closureFilePath.':'.$closureLineNumber.' Next Due: 1 minute from now'); + } + + public function testDisplayScheduleWithSort() + { + $this->schedule->command(FooCommand::class)->quarterly(); + $this->schedule->command('inspire')->twiceDaily(14, 18); + $this->schedule->command('foobar', ['a' => 'b'])->everyMinute(); + $this->schedule->job(FooJob::class)->everyMinute(); + $this->schedule->command('inspire')->cron('0 9,17 * * *'); + $this->schedule->command('inspire')->cron("0 10\t* * *"); + $this->schedule->call(FooCall::class)->everyMinute(); + $this->schedule->call([FooCall::class, 'fooFunction'])->everyMinute(); + + $this->schedule->call(fn () => '')->everyMinute(); + $closureLineNumber = __LINE__ - 1; + $closureFilePath = __FILE__; + + $this->artisan(ScheduleListCommand::class, ['--next' => true]) + ->assertSuccessful() + ->expectsOutput(' * * * * * php artisan foobar a='.ProcessUtils::escapeArgument('b').' ... Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Winter\Storm\Tests\Scheduling\FooJob Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Closure at: Winter\Storm\Tests\Scheduling\FooCall Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Closure at: Winter\Storm\Tests\Scheduling\FooCall::fooFunction Next Due: 1 minute from now') + ->expectsOutput(' * * * * * Closure at: '.$closureFilePath.':'.$closureLineNumber.' Next Due: 1 minute from now') + ->expectsOutput(' 0 9,17 * * * php artisan inspire ......... Next Due: 9 hours from now') + ->expectsOutput(' 0 10 * * * php artisan inspire ........ Next Due: 10 hours from now') + ->expectsOutput(' 0 14,18 * * * php artisan inspire ........ Next Due: 14 hours from now') + ->expectsOutput(' 0 0 1 1-12/3 * php artisan foo:command .... Next Due: 3 months from now'); + } + + public function testDisplayScheduleInVerboseMode() + { + $this->schedule->command(FooCommand::class)->everyMinute(); + + $this->artisan(ScheduleListCommand::class, ['-v' => true]) + ->assertSuccessful() + ->expectsOutputToContain('Next Due: '.now()->setMinutes(1)->format('Y-m-d H:i:s P')) + ->expectsOutput(' ⇁ This is the description of the command.'); + } + + protected function tearDown(): void + { + putenv('SHELL_VERBOSITY'); + + parent::tearDown(); + } +} + +class FooCommand extends Command +{ + protected $signature = 'foo:command'; + + protected $description = 'This is the description of the command.'; +} + +class FooJob +{ +} + +class FooParamJob +{ + public function __construct($param) + { + } +} + +class FooCall +{ + public function __invoke(): void + { + } + + public function fooFunction(): void + { + } +} diff --git a/tests/Scheduling/ScheduleTestCommandTest.php b/tests/Scheduling/ScheduleTestCommandTest.php new file mode 100644 index 000000000..f1c82f16a --- /dev/null +++ b/tests/Scheduling/ScheduleTestCommandTest.php @@ -0,0 +1,102 @@ +startOfYear()); + + $this->schedule = $this->app->make(Schedule::class); + } + + public function testRunNoDefinedCommands() + { + $this->artisan(ScheduleTestCommand::class) + ->assertSuccessful() + ->expectsOutputToContain('No scheduled commands have been defined.'); + } + + public function testRunNoMatchingCommand() + { + $this->schedule->command(BarCommandStub::class); + + $this->artisan(ScheduleTestCommand::class, ['--name' => 'missing:command']) + ->assertSuccessful() + ->expectsOutputToContain('No matching scheduled command found.'); + } + + public function testRunUsingNameOption() + { + $this->schedule->command(BarCommandStub::class)->name('bar-command'); + $this->schedule->job(BarJobStub::class); + $this->schedule->call(fn () => true)->name('callback'); + + $expectedOutput = windows_os() + ? 'Running ["artisan" bar:command]' + : "Running ['artisan' bar:command]"; + + $this->artisan(ScheduleTestCommand::class, ['--name' => 'bar:command']) + ->assertSuccessful() + ->expectsOutputToContain($expectedOutput); + + $this->artisan(ScheduleTestCommand::class, ['--name' => BarJobStub::class]) + ->assertSuccessful() + ->expectsOutputToContain(sprintf('Running [%s]', BarJobStub::class)); + + $this->artisan(ScheduleTestCommand::class, ['--name' => 'callback']) + ->assertSuccessful() + ->expectsOutputToContain('Running [callback]'); + } + + public function testRunUsingChoices() + { + $this->schedule->command(BarCommandStub::class)->name('bar-command'); + $this->schedule->job(BarJobStub::class); + $this->schedule->call(fn () => true)->name('callback'); + + $this->artisan(ScheduleTestCommand::class) + ->assertSuccessful() + ->expectsChoice( + 'Which command would you like to run?', + 'callback', + [Application::formatCommandString('bar:command'), BarJobStub::class, 'callback'], + true + ) + ->expectsOutputToContain('Running [callback]'); + } + + protected function tearDown(): void + { + parent::tearDown(); + + Carbon::setTestNow(null); + } +} + +class BarCommandStub extends Command +{ + protected $signature = 'bar:command'; + + protected $description = 'This is the description of the command.'; +} + +class BarJobStub +{ + public function __invoke() + { + // .. + } +}