123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- <?php
- declare(strict_types=1);
- /*
- * This file is part of PHP CS Fixer.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- * Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
- namespace PhpCsFixer\Tests\Console\Command;
- use Clue\React\NDJson\Decoder;
- use Clue\React\NDJson\Encoder;
- use PhpCsFixer\Console\Application;
- use PhpCsFixer\Console\Command\FixCommand;
- use PhpCsFixer\Console\Command\WorkerCommand;
- use PhpCsFixer\FixerFileProcessedEvent;
- use PhpCsFixer\Runner\Parallel\ParallelAction;
- use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
- use PhpCsFixer\Runner\Parallel\ParallelisationException;
- use PhpCsFixer\Runner\Parallel\ProcessFactory;
- use PhpCsFixer\Runner\Parallel\ProcessIdentifier;
- use PhpCsFixer\Runner\RunnerConfig;
- use PhpCsFixer\Tests\TestCase;
- use PhpCsFixer\ToolInfo;
- use React\ChildProcess\Process;
- use React\EventLoop\StreamSelectLoop;
- use React\Socket\ConnectionInterface;
- use React\Socket\TcpServer;
- use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Input\ArrayInput;
- use Symfony\Component\Console\Output\OutputInterface;
- use Symfony\Component\Console\Tester\CommandTester;
- /**
- * @author Greg Korba <greg@codito.dev>
- *
- * @internal
- *
- * @covers \PhpCsFixer\Console\Command\WorkerCommand
- */
- final class WorkerCommandTest extends TestCase
- {
- public function testMissingIdentifierCausesFailure(): void
- {
- self::expectException(ParallelisationException::class);
- self::expectExceptionMessage('Missing parallelisation options');
- $commandTester = $this->doTestExecute(['--port' => 12_345]);
- }
- public function testMissingPortCausesFailure(): void
- {
- self::expectException(ParallelisationException::class);
- self::expectExceptionMessage('Missing parallelisation options');
- $commandTester = $this->doTestExecute(['--identifier' => ProcessIdentifier::create()->toString()]);
- }
- public function testWorkerCantConnectToServerWhenExecutedDirectly(): void
- {
- $commandTester = $this->doTestExecute([
- '--identifier' => ProcessIdentifier::create()->toString(),
- '--port' => 12_345,
- ]);
- self::assertStringContainsString(
- 'Connection refused',
- $commandTester->getErrorOutput()
- );
- }
- /**
- * This test is not executed on Windows because process pipes are not supported there, due to their blocking nature
- * on this particular OS. The cause of this lays in `react/child-process` component, but it's related only to tests,
- * as parallel runner works properly on Windows too. Feel free to fiddle with it and add testing support for Windows.
- *
- * @requires OS Linux|Darwin
- */
- public function testWorkerCommunicatesWithTheServer(): void
- {
- $streamSelectLoop = new StreamSelectLoop();
- $server = new TcpServer('127.0.0.1:0', $streamSelectLoop);
- $serverPort = parse_url($server->getAddress() ?? '', PHP_URL_PORT);
- $processIdentifier = ProcessIdentifier::create();
- $processFactory = new ProcessFactory(
- new ArrayInput([], (new FixCommand(new ToolInfo()))->getDefinition())
- );
- $process = new Process(implode(' ', $processFactory->getCommandArgs(
- $serverPort, // @phpstan-ignore-line
- $processIdentifier,
- new RunnerConfig(true, false, ParallelConfigFactory::sequential())
- )));
- /**
- * @var array{
- * identifier: string,
- * messages: list<array<string, mixed>>,
- * connected: bool,
- * chunkRequested: bool,
- * resultReported: bool
- * } $workerScope
- */
- $workerScope = [
- 'identifier' => $processIdentifier->toString(),
- 'messages' => [],
- 'connected' => false,
- 'chunkRequested' => false,
- 'resultReported' => false,
- ];
- $server->on(
- 'connection',
- static function (ConnectionInterface $connection) use (&$workerScope): void {
- $jsonInvalidUtf8Ignore = \defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0;
- $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore);
- $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore);
- $decoder->on(
- 'data',
- static function (array $data) use ($encoder, &$workerScope): void {
- $workerScope['messages'][] = $data;
- $ds = \DIRECTORY_SEPARATOR;
- if (ParallelAction::WORKER_HELLO === $data['action']) {
- $encoder->write(['action' => ParallelAction::RUNNER_REQUEST_ANALYSIS, 'files' => [
- realpath(__DIR__.$ds.'..'.$ds.'..').$ds.'Fixtures'.$ds.'FixerTest'.$ds.'fix'.$ds.'somefile.php',
- ]]);
- return;
- }
- if (3 === \count($workerScope['messages'])) {
- $encoder->write(['action' => ParallelAction::RUNNER_THANK_YOU]);
- }
- }
- );
- }
- );
- $process->on('exit', static function () use ($streamSelectLoop): void {
- $streamSelectLoop->stop();
- });
- // Start worker in the async process, handle communication with server and wait for it to exit
- $process->start($streamSelectLoop);
- $streamSelectLoop->run();
- self::assertSame(Command::SUCCESS, $process->getExitCode());
- self::assertCount(3, $workerScope['messages']);
- self::assertSame(ParallelAction::WORKER_HELLO, $workerScope['messages'][0]['action']);
- self::assertSame(ParallelAction::WORKER_RESULT, $workerScope['messages'][1]['action']);
- self::assertSame(FixerFileProcessedEvent::STATUS_FIXED, $workerScope['messages'][1]['status']);
- self::assertSame(ParallelAction::WORKER_GET_FILE_CHUNK, $workerScope['messages'][2]['action']);
- $server->close();
- }
- /**
- * @param array<string, mixed> $arguments
- */
- private function doTestExecute(array $arguments): CommandTester
- {
- $application = new Application();
- $application->add(new WorkerCommand(new ToolInfo()));
- $command = $application->find('worker');
- $commandTester = new CommandTester($command);
- $commandTester->execute(
- array_merge(
- ['command' => $command->getName()],
- $arguments
- ),
- [
- 'capture_stderr_separately' => true,
- 'interactive' => false,
- 'decorated' => false,
- 'verbosity' => OutputInterface::VERBOSITY_DEBUG,
- ]
- );
- return $commandTester;
- }
- }
|