From 2b610abee461fce82dd95561c6d2220c80fa29a9 Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 19:14:12 +0000 Subject: [PATCH 01/15] Redesign `php artisan schedule:list` Command. --- .../Scheduling/ScheduleListCommand.php | 136 +++++++++++++++--- .../Scheduling/ScheduleListCommandTest.php | 60 ++++++++ 2 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 tests/Console/Scheduling/ScheduleListCommandTest.php diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index feb7f428dfbc..e4d56c48c606 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -4,8 +4,10 @@ use Cron\CronExpression; use DateTimeZone; +use Illuminate\Console\Application; use Illuminate\Console\Command; use Illuminate\Support\Carbon; +use Symfony\Component\Console\Terminal; class ScheduleListCommand extends Command { @@ -23,6 +25,13 @@ class ScheduleListCommand extends Command */ protected $description = 'List the scheduled commands'; + /** + * The terminal width resolver callback. + * + * @var \Closure|null + */ + protected static $terminalWidthResolver; + /** * Execute the console command. * @@ -33,25 +42,112 @@ class ScheduleListCommand extends Command */ public function handle(Schedule $schedule) { - foreach ($schedule->events() as $event) { - $rows[] = [ - $event->command, - $event->expression, - $event->description, - (new CronExpression($event->expression)) - ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) - ->setTimezone(new DateTimeZone($this->option('timezone') ?? config('app.timezone'))) - ->format('Y-m-d H:i:s P'), - $event->mutex->exists($event) ? 'Yes' : '', - ]; - } - - $this->table([ - 'Command', - 'Interval', - 'Description', - 'Next Due', - 'Has Mutex', - ], $rows ?? []); + $events = collect($schedule->events()); + $terminalWidth = $this->getTerminalWidth(); + $expressionSpacing = $this->getCronExpressionSpacing($events); + + $events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing) { + $expression = $this->formatCronExpression($event->expression, $expressionSpacing); + $command = $event->command; + + if (! $this->output->isVerbose()) { + $command = str_replace( + Application::artisanBinary(), + str_replace("'", '', Application::artisanBinary()), + str_replace(Application::phpBinary(), 'php', $event->command) + ); + } + + $command = mb_strlen($command) > 1 ? "{$command} " : ''; + $nextDueDateLabel = 'Next Due:'; + $nextDueDate = Carbon::create((new CronExpression($event->expression)) + ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) + ->setTimezone(new DateTimeZone($this->option('timezone') ?? config('app.timezone'))) + ); + + $nextDueDate = $this->output->isVerbose() + ? $nextDueDate->format('Y-m-d H:i:s P') + : $nextDueDate->diffForHumans(); + + $hasMutex = $event->mutex->exists($event) ? 'Has Mutex › ' : ''; + + $dots = str_repeat('.', max( + $terminalWidth - mb_strlen($expression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0 + )); + + // Highlights the parameters. + $command = preg_replace("#(='?)([^']+)('?)#", '$1$2$3', $command); + + return [sprintf( + ' %s %s%s %s%s %s', + $expression, + $command, + $dots, + $hasMutex, + $nextDueDateLabel, + $nextDueDate + ), $this->output->isVerbose() && mb_strlen($event->description) > 1 ? sprintf( + ' %s%s %s', + str_repeat(' ', mb_strlen($expression) + 2), + '⇁', + $event->description + ) : '']; + }); + + $this->output->writeln( + $events->flatten()->filter()->prepend('')->push('')->toArray() + ); + } + + /** + * Gets the spacing to be used on each event row. + * + * @param \Illuminate\Support\Collection $events + * @return array + */ + private function getCronExpressionSpacing($events) + { + $rows = $events->map(fn ($event) => array_map('mb_strlen', explode(' ', $event->expression))); + + return collect($rows[0] ?? [])->keys()->map(fn ($key) => $rows->max($key)); + } + + /** + * Formats the cron expression based on the spacing provided. + * + * @param string $expression + * @param array $spacing + * @return string + */ + private function formatCronExpression($expression, $spacing) + { + $expression = explode(' ', $expression); + + return collect($spacing) + ->map(fn ($length, $index) => $expression[$index] = str_pad($expression[$index], $length)) + ->implode(' '); + } + + /** + * Get the terminal width. + * + * @return int + */ + public static function getTerminalWidth() + { + return is_null(static::$terminalWidthResolver) + ? (new Terminal)->getWidth() + : call_user_func(static::$terminalWidthResolver); + } + + /** + * Set a callback that should be used when resolving the terminal width. + * + * @param \Closure|null $resolver + * @return void + */ + public static function resolveTerminalWidthUsing($resolver) + { + static::$terminalWidthResolver = $resolver; } } diff --git a/tests/Console/Scheduling/ScheduleListCommandTest.php b/tests/Console/Scheduling/ScheduleListCommandTest.php new file mode 100644 index 000000000000..2463ec062796 --- /dev/null +++ b/tests/Console/Scheduling/ScheduleListCommandTest.php @@ -0,0 +1,60 @@ +startOfYear()); + ScheduleListCommand::resolveTerminalWidthUsing(fn () => 80); + + $this->schedule = $this->app->make(Schedule::class); + } + + public function testDisplaySchedule() + { + $this->schedule->call(fn () => '')->everyMinute(); + $this->schedule->command(FooCommand::class)->quarterly(); + $this->schedule->command('inspire')->twiceDaily(14, 18); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutput(' * * * * * ............................ Next Due: 1 minute from now') + ->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'); + } + + 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 + { + parent::tearDown(); + + ScheduleListCommand::resolveTerminalWidthUsing(null); + } +} + +class FooCommand extends Command +{ + protected $signature = 'foo:command'; + + protected $description = 'This is the description of the command.'; +} + From fcb85224281421cef45b881a35bf00e852c34aaa Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 19:32:12 +0000 Subject: [PATCH 02/15] style: fix --- tests/Console/Scheduling/ScheduleListCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Console/Scheduling/ScheduleListCommandTest.php b/tests/Console/Scheduling/ScheduleListCommandTest.php index 2463ec062796..bd18e030704c 100644 --- a/tests/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Console/Scheduling/ScheduleListCommandTest.php @@ -39,7 +39,7 @@ public function testDisplayScheduleInVerboseMode() $this->artisan(ScheduleListCommand::class, ['-v' => true]) ->assertSuccessful() - ->expectsOutputToContain('Next Due: ' . now()->setMinutes(1)->format('Y-m-d H:i:s P')) + ->expectsOutputToContain('Next Due: '.now()->setMinutes(1)->format('Y-m-d H:i:s P')) ->expectsOutput(' ⇁ This is the description of the command.'); } From 2f1c345bf15a6841a91d788bbbf72d58262413b9 Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 19:32:55 +0000 Subject: [PATCH 03/15] style: fix --- tests/Console/Scheduling/ScheduleListCommandTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Console/Scheduling/ScheduleListCommandTest.php b/tests/Console/Scheduling/ScheduleListCommandTest.php index bd18e030704c..4cb6e16a6e78 100644 --- a/tests/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Console/Scheduling/ScheduleListCommandTest.php @@ -57,4 +57,3 @@ class FooCommand extends Command protected $description = 'This is the description of the command.'; } - From fd7753ae3ec3889a186b61ef7dee80a009b8159b Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 22:48:43 +0000 Subject: [PATCH 04/15] fix: Remove `SHELL_VERBOSITY` from env after tests. --- tests/Console/Scheduling/ScheduleListCommandTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Console/Scheduling/ScheduleListCommandTest.php b/tests/Console/Scheduling/ScheduleListCommandTest.php index 4cb6e16a6e78..b6b8dceef00d 100644 --- a/tests/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Console/Scheduling/ScheduleListCommandTest.php @@ -47,6 +47,8 @@ protected function tearDown(): void { parent::tearDown(); + putenv('SHELL_VERBOSITY'); + ScheduleListCommand::resolveTerminalWidthUsing(null); } } From 2736cfa2408d62de78ae30365ec7bd7de19ba8f3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 11 Mar 2022 16:56:34 -0600 Subject: [PATCH 05/15] formatting --- src/Illuminate/Console/Scheduling/ScheduleListCommand.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index e4d56c48c606..64e21ac9e2c0 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -48,6 +48,7 @@ public function handle(Schedule $schedule) $events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing) { $expression = $this->formatCronExpression($event->expression, $expressionSpacing); + $command = $event->command; if (! $this->output->isVerbose()) { @@ -59,7 +60,9 @@ public function handle(Schedule $schedule) } $command = mb_strlen($command) > 1 ? "{$command} " : ''; + $nextDueDateLabel = 'Next Due:'; + $nextDueDate = Carbon::create((new CronExpression($event->expression)) ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) ->setTimezone(new DateTimeZone($this->option('timezone') ?? config('app.timezone'))) @@ -75,7 +78,7 @@ public function handle(Schedule $schedule) $terminalWidth - mb_strlen($expression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0 )); - // Highlights the parameters. + // Highlight the parameters... $command = preg_replace("#(='?)([^']+)('?)#", '$1$2$3', $command); return [sprintf( From ea5cd31911f806a85c890d8646b0b04646e806e1 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 11 Mar 2022 16:58:07 -0600 Subject: [PATCH 06/15] empty satte --- src/Illuminate/Console/Scheduling/ScheduleListCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 64e21ac9e2c0..0a064834f548 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -97,6 +97,10 @@ public function handle(Schedule $schedule) ) : '']; }); + if ($events->isEmpty()) { + return $this->comment('No tasks have been scheduled.'); + } + $this->output->writeln( $events->flatten()->filter()->prepend('')->push('')->toArray() ); From 300eb465619a95f12c6dea9f6e58c4fea1e3320d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 11 Mar 2022 16:59:40 -0600 Subject: [PATCH 07/15] adjust wording --- src/Illuminate/Console/Scheduling/ScheduleListCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 0a064834f548..2b6dcbec5cc2 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -98,7 +98,7 @@ public function handle(Schedule $schedule) }); if ($events->isEmpty()) { - return $this->comment('No tasks have been scheduled.'); + return $this->comment('No scheduled tasks have been defined.'); } $this->output->writeln( From 1b7d072cbf847d246e17568846e54120525e3e4b Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 23:08:44 +0000 Subject: [PATCH 08/15] Clear `AliasLoader` after the tests. --- tests/Console/Scheduling/ScheduleListCommandTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Console/Scheduling/ScheduleListCommandTest.php b/tests/Console/Scheduling/ScheduleListCommandTest.php index b6b8dceef00d..269538a6635a 100644 --- a/tests/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Console/Scheduling/ScheduleListCommandTest.php @@ -5,7 +5,9 @@ use Illuminate\Console\Command; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\ScheduleListCommand; +use Illuminate\Foundation\AliasLoader; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Facade; use Orchestra\Testbench\TestCase; class ScheduleListCommandTest extends TestCase @@ -48,6 +50,7 @@ protected function tearDown(): void parent::tearDown(); putenv('SHELL_VERBOSITY'); + AliasLoader::getInstance()->setAliases([]); ScheduleListCommand::resolveTerminalWidthUsing(null); } From b05c13c975ff6cba77d8f0afd48426d2a3f48f1d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 11 Mar 2022 17:09:12 -0600 Subject: [PATCH 09/15] move test --- .../Console/Scheduling/ScheduleListCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{ => Integration}/Console/Scheduling/ScheduleListCommandTest.php (97%) diff --git a/tests/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php similarity index 97% rename from tests/Console/Scheduling/ScheduleListCommandTest.php rename to tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index b6b8dceef00d..4c8ee084c78b 100644 --- a/tests/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -1,6 +1,6 @@ Date: Fri, 11 Mar 2022 17:11:50 -0600 Subject: [PATCH 10/15] remove alias clear --- .../Integration/Console/Scheduling/ScheduleListCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index df5ff87a07d5..7077fe1676cb 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -50,7 +50,7 @@ protected function tearDown(): void parent::tearDown(); putenv('SHELL_VERBOSITY'); - AliasLoader::getInstance()->setAliases([]); + // AliasLoader::getInstance()->setAliases([]); ScheduleListCommand::resolveTerminalWidthUsing(null); } From 8bd16a891ac98776333ff4b16906b76f784d1497 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 11 Mar 2022 17:12:02 -0600 Subject: [PATCH 11/15] delete line --- tests/Integration/Console/Scheduling/ScheduleListCommandTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 7077fe1676cb..6db334d0b181 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -50,7 +50,6 @@ protected function tearDown(): void parent::tearDown(); putenv('SHELL_VERBOSITY'); - // AliasLoader::getInstance()->setAliases([]); ScheduleListCommand::resolveTerminalWidthUsing(null); } From 1407c4f59935da7e3940ceccc339341d83e457b5 Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 23:35:55 +0000 Subject: [PATCH 12/15] fix: Replace " also (Windows) --- src/Illuminate/Console/Scheduling/ScheduleListCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 2b6dcbec5cc2..7d1e797a13ce 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -54,7 +54,7 @@ public function handle(Schedule $schedule) if (! $this->output->isVerbose()) { $command = str_replace( Application::artisanBinary(), - str_replace("'", '', Application::artisanBinary()), + preg_replace("#['\"]#", '', Application::artisanBinary()), str_replace(Application::phpBinary(), 'php', $event->command) ); } @@ -79,7 +79,7 @@ public function handle(Schedule $schedule) )); // Highlight the parameters... - $command = preg_replace("#(='?)([^']+)('?)#", '$1$2$3', $command); + $command = preg_replace("#(=['\"]?)([^']+)(['\"]?)#", '$1$2$3', $command); return [sprintf( ' %s %s%s %s%s %s', From d49503d9115ab82d6d54c229eae2fb0103706ac7 Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 23:40:11 +0000 Subject: [PATCH 13/15] style: fixes --- .../Integration/Console/Scheduling/ScheduleListCommandTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 6db334d0b181..4c8ee084c78b 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -5,9 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\ScheduleListCommand; -use Illuminate\Foundation\AliasLoader; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Facade; use Orchestra\Testbench\TestCase; class ScheduleListCommandTest extends TestCase From 69b60dc4e24a388371acdf3dad8b015de1cc1f9d Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 23:53:21 +0000 Subject: [PATCH 14/15] Improve RegExp for windows machines. --- src/Illuminate/Console/Scheduling/ScheduleListCommand.php | 2 +- .../Console/Scheduling/ScheduleListCommandTest.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 7d1e797a13ce..73ef1f0ecde8 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -79,7 +79,7 @@ public function handle(Schedule $schedule) )); // Highlight the parameters... - $command = preg_replace("#(=['\"]?)([^']+)(['\"]?)#", '$1$2$3', $command); + $command = preg_replace("#(=['\"]?)([^'\"]+)(['\"]?)#", '$1$2$3', $command); return [sprintf( ' %s %s%s %s%s %s', diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 4c8ee084c78b..3043e481a369 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -6,6 +6,7 @@ use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\ScheduleListCommand; use Illuminate\Support\Carbon; +use Illuminate\Support\ProcessUtils; use Orchestra\Testbench\TestCase; class ScheduleListCommandTest extends TestCase @@ -25,12 +26,14 @@ public function testDisplaySchedule() $this->schedule->call(fn () => '')->everyMinute(); $this->schedule->command(FooCommand::class)->quarterly(); $this->schedule->command('inspire')->twiceDaily(14, 18); + $this->schedule->command('foobar', ['a' => 'b'])->everyMinute(); $this->artisan(ScheduleListCommand::class) ->assertSuccessful() ->expectsOutput(' * * * * * ............................ Next Due: 1 minute from now') ->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(' 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"); } public function testDisplayScheduleInVerboseMode() From 7153b0ea4178afb2b65e27dd66e18c62d99261ec Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 11 Mar 2022 23:54:56 +0000 Subject: [PATCH 15/15] style: fixes --- .../Integration/Console/Scheduling/ScheduleListCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 3043e481a369..4a4d7efed7c8 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -33,7 +33,7 @@ public function testDisplaySchedule() ->expectsOutput(' * * * * * ............................ Next Due: 1 minute from now') ->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(' * * * * * php artisan foobar a='.ProcessUtils::escapeArgument('b').' ... Next Due: 1 minute from now'); } public function testDisplayScheduleInVerboseMode()