WorkerCommandTest.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Tests\Console\Command;
  13. use Clue\React\NDJson\Decoder;
  14. use Clue\React\NDJson\Encoder;
  15. use PhpCsFixer\Console\Application;
  16. use PhpCsFixer\Console\Command\FixCommand;
  17. use PhpCsFixer\Console\Command\WorkerCommand;
  18. use PhpCsFixer\FixerFileProcessedEvent;
  19. use PhpCsFixer\Runner\Parallel\ParallelAction;
  20. use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
  21. use PhpCsFixer\Runner\Parallel\ParallelisationException;
  22. use PhpCsFixer\Runner\Parallel\ProcessFactory;
  23. use PhpCsFixer\Runner\Parallel\ProcessIdentifier;
  24. use PhpCsFixer\Runner\RunnerConfig;
  25. use PhpCsFixer\Tests\TestCase;
  26. use PhpCsFixer\ToolInfo;
  27. use React\ChildProcess\Process;
  28. use React\EventLoop\StreamSelectLoop;
  29. use React\Socket\ConnectionInterface;
  30. use React\Socket\TcpServer;
  31. use Symfony\Component\Console\Command\Command;
  32. use Symfony\Component\Console\Input\ArrayInput;
  33. use Symfony\Component\Console\Output\OutputInterface;
  34. use Symfony\Component\Console\Tester\CommandTester;
  35. /**
  36. * @author Greg Korba <greg@codito.dev>
  37. *
  38. * @internal
  39. *
  40. * @covers \PhpCsFixer\Console\Command\WorkerCommand
  41. */
  42. final class WorkerCommandTest extends TestCase
  43. {
  44. public function testMissingIdentifierCausesFailure(): void
  45. {
  46. self::expectException(ParallelisationException::class);
  47. self::expectExceptionMessage('Missing parallelisation options');
  48. $commandTester = $this->doTestExecute(['--port' => 12_345]);
  49. }
  50. public function testMissingPortCausesFailure(): void
  51. {
  52. self::expectException(ParallelisationException::class);
  53. self::expectExceptionMessage('Missing parallelisation options');
  54. $commandTester = $this->doTestExecute(['--identifier' => ProcessIdentifier::create()->toString()]);
  55. }
  56. public function testWorkerCantConnectToServerWhenExecutedDirectly(): void
  57. {
  58. $commandTester = $this->doTestExecute([
  59. '--identifier' => ProcessIdentifier::create()->toString(),
  60. '--port' => 12_345,
  61. ]);
  62. self::assertStringContainsString(
  63. 'Connection refused',
  64. $commandTester->getErrorOutput()
  65. );
  66. }
  67. /**
  68. * This test is not executed on Windows because process pipes are not supported there, due to their blocking nature
  69. * on this particular OS. The cause of this lays in `react/child-process` component, but it's related only to tests,
  70. * as parallel runner works properly on Windows too. Feel free to fiddle with it and add testing support for Windows.
  71. *
  72. * @requires OS Linux|Darwin
  73. */
  74. public function testWorkerCommunicatesWithTheServer(): void
  75. {
  76. $streamSelectLoop = new StreamSelectLoop();
  77. $server = new TcpServer('127.0.0.1:0', $streamSelectLoop);
  78. $serverPort = parse_url($server->getAddress() ?? '', PHP_URL_PORT);
  79. $processIdentifier = ProcessIdentifier::create();
  80. $processFactory = new ProcessFactory(
  81. new ArrayInput([], (new FixCommand(new ToolInfo()))->getDefinition())
  82. );
  83. $process = new Process(implode(' ', $processFactory->getCommandArgs(
  84. $serverPort, // @phpstan-ignore-line
  85. $processIdentifier,
  86. new RunnerConfig(true, false, ParallelConfigFactory::sequential())
  87. )));
  88. /**
  89. * @var array{
  90. * identifier: string,
  91. * messages: list<array<string, mixed>>,
  92. * connected: bool,
  93. * chunkRequested: bool,
  94. * resultReported: bool
  95. * } $workerScope
  96. */
  97. $workerScope = [
  98. 'identifier' => $processIdentifier->toString(),
  99. 'messages' => [],
  100. 'connected' => false,
  101. 'chunkRequested' => false,
  102. 'resultReported' => false,
  103. ];
  104. $server->on(
  105. 'connection',
  106. static function (ConnectionInterface $connection) use (&$workerScope): void {
  107. $jsonInvalidUtf8Ignore = \defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0;
  108. $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore);
  109. $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore);
  110. $decoder->on(
  111. 'data',
  112. static function (array $data) use ($encoder, &$workerScope): void {
  113. $workerScope['messages'][] = $data;
  114. $ds = \DIRECTORY_SEPARATOR;
  115. if (ParallelAction::WORKER_HELLO === $data['action']) {
  116. $encoder->write(['action' => ParallelAction::RUNNER_REQUEST_ANALYSIS, 'files' => [
  117. realpath(__DIR__.$ds.'..'.$ds.'..').$ds.'Fixtures'.$ds.'FixerTest'.$ds.'fix'.$ds.'somefile.php',
  118. ]]);
  119. return;
  120. }
  121. if (3 === \count($workerScope['messages'])) {
  122. $encoder->write(['action' => ParallelAction::RUNNER_THANK_YOU]);
  123. }
  124. }
  125. );
  126. }
  127. );
  128. $process->on('exit', static function () use ($streamSelectLoop): void {
  129. $streamSelectLoop->stop();
  130. });
  131. // Start worker in the async process, handle communication with server and wait for it to exit
  132. $process->start($streamSelectLoop);
  133. $streamSelectLoop->run();
  134. self::assertSame(Command::SUCCESS, $process->getExitCode());
  135. self::assertCount(3, $workerScope['messages']);
  136. self::assertSame(ParallelAction::WORKER_HELLO, $workerScope['messages'][0]['action']);
  137. self::assertSame(ParallelAction::WORKER_RESULT, $workerScope['messages'][1]['action']);
  138. self::assertSame(FixerFileProcessedEvent::STATUS_FIXED, $workerScope['messages'][1]['status']);
  139. self::assertSame(ParallelAction::WORKER_GET_FILE_CHUNK, $workerScope['messages'][2]['action']);
  140. $server->close();
  141. }
  142. /**
  143. * @param array<string, mixed> $arguments
  144. */
  145. private function doTestExecute(array $arguments): CommandTester
  146. {
  147. $application = new Application();
  148. $application->add(new WorkerCommand(new ToolInfo()));
  149. $command = $application->find('worker');
  150. $commandTester = new CommandTester($command);
  151. $commandTester->execute(
  152. array_merge(
  153. ['command' => $command->getName()],
  154. $arguments
  155. ),
  156. [
  157. 'capture_stderr_separately' => true,
  158. 'interactive' => false,
  159. 'decorated' => false,
  160. 'verbosity' => OutputInterface::VERBOSITY_DEBUG,
  161. ]
  162. );
  163. return $commandTester;
  164. }
  165. }