-
Notifications
You must be signed in to change notification settings - Fork 11k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[10.x] Fix parsed input arguments for command events using dispatcher rerouting #46442
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
<?php | ||
|
||
namespace Illuminate\Tests\Integration\Console; | ||
|
||
use Illuminate\Console\Events\CommandFinished; | ||
use Illuminate\Console\Events\CommandStarting; | ||
use Illuminate\Events\Dispatcher; | ||
use Illuminate\Filesystem\Filesystem; | ||
use Illuminate\Support\Facades\Event; | ||
use Illuminate\Support\Str; | ||
use Orchestra\Testbench\TestCase; | ||
|
||
class CommandEventsTest extends TestCase | ||
{ | ||
/** | ||
* Each run of this test is assigned a random ID to ensure that separate runs | ||
* do not interfere with each other. | ||
* | ||
* @var string | ||
*/ | ||
protected $id; | ||
|
||
/** | ||
* The path to the file that execution logs will be written to. | ||
* | ||
* @var string | ||
*/ | ||
protected $logfile; | ||
|
||
/** | ||
* Just in case Testbench starts to ship an `artisan` script, we'll check and save a backup. | ||
* | ||
* @var string|null | ||
*/ | ||
protected $originalArtisan; | ||
|
||
/** | ||
* The Filesystem instance for writing stubs and logs. | ||
* | ||
* @var \Illuminate\Filesystem\Filesystem | ||
*/ | ||
protected $fs; | ||
|
||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->fs = new Filesystem; | ||
|
||
$this->id = Str::random(); | ||
$this->logfile = storage_path("logs/command_events_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 commandEventsProvider | ||
*/ | ||
public function testCommandEventsReceiveParsedInput($processType, $argumentType) | ||
{ | ||
switch ($processType) { | ||
case 'foreground': | ||
$this->app[\Illuminate\Contracts\Console\Kernel::class]->registerCommand(new CommandEventsTestCommand); | ||
$this->app[Dispatcher::class]->listen(function (CommandStarting $event) { | ||
array_map(fn ($e) => $this->fs->append($this->logfile, $e."\n"), [ | ||
'CommandStarting', | ||
$event->input->getArgument('firstname'), | ||
$event->input->getArgument('lastname'), | ||
$event->input->getOption('occupation'), | ||
]); | ||
}); | ||
|
||
Event::listen(function (CommandFinished $event) { | ||
array_map(fn ($e) => $this->fs->append($this->logfile, $e."\n"), [ | ||
'CommandFinished', | ||
$event->input->getArgument('firstname'), | ||
$event->input->getArgument('lastname'), | ||
$event->input->getOption('occupation'), | ||
]); | ||
}); | ||
switch ($argumentType) { | ||
case 'array': | ||
$this->artisan(CommandEventsTestCommand::class, [ | ||
'firstname' => 'taylor', | ||
'lastname' => 'otwell', | ||
'--occupation' => 'coding', | ||
]); | ||
break; | ||
case 'string': | ||
$this->artisan('command-events-test-command taylor otwell --occupation=coding'); | ||
break; | ||
} | ||
break; | ||
case 'background': | ||
// Initialize empty logfile. | ||
$this->fs->append($this->logfile, ''); | ||
exec('php '.base_path('artisan').' command-events-test-command-'.$this->id.' taylor otwell --occupation=coding'); | ||
// Since our command is running in a separate process, we need to wait | ||
// until it has finished executing before running our assertions. | ||
$this->waitForLogMessages( | ||
'CommandStarting', 'taylor', 'otwell', 'coding', | ||
'CommandFinished', 'taylor', 'otwell', 'coding', | ||
); | ||
break; | ||
} | ||
|
||
$this->assertLogged( | ||
'CommandStarting', 'taylor', 'otwell', 'coding', | ||
'CommandFinished', 'taylor', 'otwell', 'coding', | ||
); | ||
} | ||
|
||
public static function commandEventsProvider() | ||
{ | ||
return [ | ||
'Foreground with array' => ['foreground', 'array'], | ||
'Foreground with string' => ['foreground', 'string'], | ||
'Background' => ['background', 'string'], | ||
]; | ||
} | ||
|
||
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 = <<<PHP | ||
#!/usr/bin/env php | ||
<?php | ||
|
||
// This is a custom artisan script made specifically for: | ||
// | ||
// {$thisFile} | ||
// | ||
// It should be automatically cleaned up when the tests have finished executing. | ||
// If you are seeing this file, an unexpected error must have occurred. Please | ||
// manually remove it. | ||
define('LARAVEL_START', microtime(true)); | ||
|
||
require __DIR__.'/../../../autoload.php'; | ||
|
||
\$app = require_once __DIR__.'/bootstrap/app.php'; | ||
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class); | ||
|
||
class CommandEventsTestCommand extends Illuminate\Console\Command | ||
{ | ||
protected \$signature = 'command-events-test-command-{$this->id} {firstname} {lastname} {--occupation=cook}'; | ||
|
||
public function handle() | ||
{ | ||
// ... | ||
} | ||
} | ||
|
||
// Register command with Kernel | ||
Illuminate\Console\Application::starting(function (\$artisan) { | ||
\$artisan->add(new CommandEventsTestCommand); | ||
}); | ||
|
||
// Add command to scheduler so that the after() callback is trigger in our spawned process | ||
Illuminate\Foundation\Application::getInstance() | ||
->booted(function (\$app) { | ||
\$fs = new Illuminate\Filesystem\Filesystem; | ||
\$log = fn (\$msg) => \$fs->append({$logfile}, \$msg."\\n"); | ||
|
||
\$app[\Illuminate\Events\Dispatcher::class]->listen(function (\Illuminate\Console\Events\CommandStarting \$event) use (\$log) { | ||
array_map(fn (\$msg) => \$log(\$msg), [ | ||
'CommandStarting', | ||
\$event->input->getArgument('firstname'), | ||
\$event->input->getArgument('lastname'), | ||
\$event->input->getOption('occupation'), | ||
]); | ||
}); | ||
|
||
\$app[\Illuminate\Events\Dispatcher::class]->listen(function (\Illuminate\Console\Events\CommandFinished \$event) use (\$log) { | ||
array_map(fn (\$msg) => \$log(\$msg), [ | ||
'CommandFinished', | ||
\$event->input->getArgument('firstname'), | ||
\$event->input->getArgument('lastname'), | ||
\$event->input->getOption('occupation'), | ||
]); | ||
}); | ||
}); | ||
|
||
\$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); | ||
} | ||
} | ||
|
||
class CommandEventsTestCommand extends \Illuminate\Console\Command | ||
{ | ||
protected $signature = 'command-events-test-command {firstname} {lastname} {--occupation=cook}'; | ||
|
||
public function handle() | ||
{ | ||
// ... | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optionally this can be changed to
$dispatcher->addSubscriber(...)
and move this logic to a separate class