Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions src/Illuminate/Console/Scheduling/ScheduleListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ScheduleListCommand extends Command
protected $signature = 'schedule:list
{--timezone= : The timezone that times should be displayed in}
{--next : Sort the listed tasks by their next due date}
{--json : Output the scheduled tasks as JSON}
';

/**
Expand Down Expand Up @@ -53,21 +54,78 @@ public function handle(Schedule $schedule)
$events = new Collection($schedule->events());

if ($events->isEmpty()) {
$this->components->info('No scheduled tasks have been defined.');
if ($this->option('json')) {
$this->output->writeln('[]');
} else {
$this->components->info('No scheduled tasks have been defined.');
}

return;
}

$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));

$events = $this->sortEvents($events, $timezone);

$this->option('json')
? $this->displayJson($events, $timezone)
: $this->displayForCli($events, $timezone);
}

/**
* Render the scheduled tasks information as JSON.
*
* @param \Illuminate\Support\Collection $events
* @param \DateTimeZone $timezone
* @return void
*/
protected function displayJson(Collection $events, DateTimeZone $timezone)
{
$this->output->writeln($events->map(function ($event) use ($timezone) {
$nextDueDate = $this->getNextDueDateForEvent($event, $timezone);

$command = $event->command ?? '';

if (! $this->output->isVerbose()) {
$command = $event->normalizeCommand($command);
}

if ($event instanceof CallbackEvent) {
$command = $event->getSummaryForDisplay();

if (in_array($command, ['Closure', 'Callback'])) {
$command = 'Closure at: '.$this->getClosureLocation($event);
}
}

return [
'expression' => $event->expression,
'command' => $command,
'description' => $event->description ?? null,
'next_due_date' => $nextDueDate->format('Y-m-d H:i:s P'),
'next_due_date_human' => $nextDueDate->diffForHumans(),
'timezone' => $timezone->getName(),
'has_mutex' => $event->mutex->exists($event),
'repeat_seconds' => $event->isRepeatable() ? $event->repeatSeconds : null,
];
})->values()->toJson());
}

/**
* Render the scheduled tasks information formatted for the CLI.
*
* @param \Illuminate\Support\Collection $events
* @param \DateTimeZone $timezone
* @return void
*/
protected function displayForCli(Collection $events, DateTimeZone $timezone)
{
$terminalWidth = self::getTerminalWidth();

$expressionSpacing = $this->getCronExpressionSpacing($events);

$repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events);

$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));

$events = $this->sortEvents($events, $timezone);

$events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) {
return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone);
});
Expand Down Expand Up @@ -194,6 +252,18 @@ private function sortEvents(\Illuminate\Support\Collection $events, DateTimeZone
: $events;
}

/**
* Render the scheduled tasks information.
*
* @param \Illuminate\Support\Collection $events
* @param \DateTimeZone $timezone
* @return void
*/
protected function display(Collection $events, DateTimeZone $timezone)
{
$this->option('json') ? $this->displayJson($events, $timezone) : $this->displayForCli($events, $timezone);
}

/**
* Get the next due date for an event.
*
Expand Down
132 changes: 132 additions & 0 deletions tests/Integration/Console/Scheduling/ScheduleListCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Tests\Integration\Console\Scheduling;

use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Console\Scheduling\ScheduleListCommand;
Expand Down Expand Up @@ -31,6 +32,15 @@ public function testDisplayEmptySchedule()
->expectsOutputToContain('No scheduled tasks have been defined.');
}

public function testDisplayEmptyScheduleAsJson()
{
$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, ['--json' => true]);
$output = Artisan::output();

$this->assertJson($output);
$this->assertJsonStringEqualsJsonString('[]', $output);
}

public function testDisplaySchedule()
{
$this->schedule->command(FooCommand::class)->quarterly();
Expand Down Expand Up @@ -65,6 +75,128 @@ public function testDisplaySchedule()
->expectsOutput(' * * * * * Closure at: '.$closureFilePath.':'.$closureLineNumber.' Next Due: 1 minute from now');
}

public function testDisplayScheduleAsJson()
{
$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->job(new FooParamJob('test'))->everyMinute();
$this->schedule->job(FooJob::class)->name('foo-named-job')->everyMinute();
$this->schedule->job(new FooParamJob('test'))->name('foo-named-param-job')->everyMinute();
$this->schedule->command('inspire')->cron('0 9,17 * * *');
$this->schedule->call(fn () => '')->everyMinute();

$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, ['--json' => true]);
$output = Artisan::output();

$this->assertJson($output);
$data = json_decode($output, true);

$this->assertIsArray($data);
$this->assertCount(9, $data);

$this->assertSame('0 0 1 1-12/3 *', $data[0]['expression']);
$this->assertNull($data[0]['repeat_seconds']);
$this->assertSame('php artisan foo:command', $data[0]['command']);
$this->assertSame('This is the description of the command.', $data[0]['description']);
$this->assertStringContainsString('2023-04-01 00:00:00', $data[0]['next_due_date']);
$this->assertSame('3 months from now', $data[0]['next_due_date_human']);
$this->assertFalse($data[0]['has_mutex']);

$this->assertSame('* * * * *', $data[2]['expression']);
$this->assertSame('php artisan foobar a='.ProcessUtils::escapeArgument('b'), $data[2]['command']);
$this->assertNull($data[2]['description']);
$this->assertSame('1 minute from now', $data[2]['next_due_date_human']);

$this->assertSame('Illuminate\Tests\Integration\Console\Scheduling\FooJob', $data[3]['command']);

$this->assertSame('foo-named-job', $data[5]['command']);

$this->assertStringContainsString('Closure at:', $data[8]['command']);
$this->assertStringContainsString('ScheduleListCommandTest.php', $data[8]['command']);
}

public function testDisplayScheduleWithSortAsJson()
{
$this->schedule->command(FooCommand::class)->quarterly();
$this->schedule->command('inspire')->twiceDaily(14, 18);
$this->schedule->command('foobar', ['a' => 'b'])->everyMinute();

$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [
'--next' => true,
'--json' => true,
]);
$output = Artisan::output();

$this->assertJson($output);
$data = json_decode($output, true);

$this->assertIsArray($data);
$this->assertCount(3, $data);

$this->assertSame('* * * * *', $data[0]['expression']);
$this->assertSame('1 minute from now', $data[0]['next_due_date_human']);
$this->assertSame('php artisan foobar a='.ProcessUtils::escapeArgument('b'), $data[0]['command']);

$this->assertSame('0 14,18 * * *', $data[1]['expression']);
$this->assertSame('14 hours from now', $data[1]['next_due_date_human']);
$this->assertSame('php artisan inspire', $data[1]['command']);

$this->assertSame('0 0 1 1-12/3 *', $data[2]['expression']);
$this->assertSame('3 months from now', $data[2]['next_due_date_human']);
$this->assertSame('php artisan foo:command', $data[2]['command']);
}

public function testDisplayScheduleAsJsonWithTimezone()
{
$this->schedule->command('inspire')->daily();

$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [
'--timezone' => 'America/Chicago',
'--json' => true,
]);
$output = Artisan::output();

$this->assertJson($output);
$data = json_decode($output, true);

$this->assertIsArray($data);
$this->assertCount(1, $data);
$this->assertSame('America/Chicago', $data[0]['timezone']);
$this->assertStringContainsString('-06:00', $data[0]['next_due_date']);
$this->assertSame('php artisan inspire', $data[0]['command']);
}

public function testDisplayScheduleAsJsonInVerboseMode()
{
$this->schedule->command(FooCommand::class)->quarterly();
$this->schedule->command('inspire')->everyMinute();
$this->schedule->call(fn () => '')->everyMinute();

$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [
'--json' => true,
'-v' => true,
]);
$output = Artisan::output();

$this->assertJson($output);
$data = json_decode($output, true);

$this->assertIsArray($data);
$this->assertCount(3, $data);

$this->assertSame('0 0 1 1-12/3 *', $data[0]['expression']);
$this->assertSame(Application::phpBinary().' '.Application::artisanBinary().' foo:command', $data[0]['command']);
$this->assertSame('This is the description of the command.', $data[0]['description']);

$this->assertSame('* * * * *', $data[1]['expression']);
$this->assertSame(Application::phpBinary().' '.Application::artisanBinary().' inspire', $data[1]['command']);

$this->assertStringContainsString('Closure at:', $data[2]['command']);
$this->assertStringContainsString('ScheduleListCommandTest.php', $data[2]['command']);
}

public function testDisplayScheduleWithSort()
{
$this->schedule->command(FooCommand::class)->quarterly();
Expand Down