Skip to content
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 #46430

Closed
wants to merge 3 commits into from
Closed
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
15 changes: 10 additions & 5 deletions src/Illuminate/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
Expand Down Expand Up @@ -87,19 +88,23 @@ public function __construct(Container $laravel, Dispatcher $events, $version)
*
* @return int
*/
public function run(InputInterface $input = null, OutputInterface $output = null): int
protected function doRunCommand(SymfonyCommand $command, InputInterface $input, OutputInterface $output)
{
$commandName = $this->getCommandName(
$input = $input ?: new ArgvInput
);

try {
$input->bind(tap($command)->mergeApplicationDefinition()->getDefinition());
} catch (ExceptionInterface) {
// ...
}

$this->events->dispatch(
new CommandStarting(
$commandName, $input, $output = $output ?: new BufferedConsoleOutput
)
new CommandStarting($commandName, $input, $output = $output ?: new BufferedConsoleOutput)
);

$exitCode = parent::run($input, $output);
$exitCode = parent::doRunCommand($command, $input, $output);

$this->events->dispatch(
new CommandFinished($commandName, $input, $output, $exitCode)
Expand Down
252 changes: 252 additions & 0 deletions tests/Integration/Console/CommandEventsTest.php
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()
{
// ...
}
}