diff --git a/src/Input/ChildCommandFactory.php b/src/Input/ChildCommandFactory.php index 6056c2e..8736ee3 100644 --- a/src/Input/ChildCommandFactory.php +++ b/src/Input/ChildCommandFactory.php @@ -30,7 +30,7 @@ * @param list $phpExecutable */ public function __construct( - private array $phpExecutable, + public array $phpExecutable, private string $scriptPath, private string $commandName, private InputDefinition $commandDefinition, @@ -53,7 +53,7 @@ private function createBaseCommand( InputInterface $input ): array { return array_filter([ - ...$this->phpExecutable, + //...$this->phpExecutable, $this->scriptPath, $this->commandName, ...array_map(strval(...), self::getArguments($input)), diff --git a/src/ParallelExecutor.php b/src/ParallelExecutor.php index 074882c..ad451b5 100644 --- a/src/ParallelExecutor.php +++ b/src/ParallelExecutor.php @@ -266,6 +266,7 @@ private function createProcessLauncher( Logger $logger ): ProcessLauncher { return $this->processLauncherFactory->create( + $this->childCommandFactory->phpExecutable, $this->childCommandFactory->createChildCommand($input), $this->workingDirectory, $this->extraEnvironmentVariables, diff --git a/src/ParallelExecutorFactory.php b/src/ParallelExecutorFactory.php index 7de66d4..b85d8c1 100644 --- a/src/ParallelExecutorFactory.php +++ b/src/ParallelExecutorFactory.php @@ -232,7 +232,24 @@ public function withProgressSymbol(string $progressSymbol): self * The path of the PHP executable. It is the executable that will be used * to spawn the child process(es). * - * @param string|list $phpExecutable e.g. ['/path/to/php', '-dmemory_limit=512M'] + * It can be a string (the path of the PHP executable), or an array + * to set some PHP settings or others, for example: + * + * ``` + * ['/path/to/php', '-dmemory_limit=512M'] + * ``` + * + * However, beware that those settings will take precedence over the + * inherited settings from the main process. As a result, if you execute: + * + * ``` + * $ php -dmemory_limit=1024M bin/console my:command + * ``` + * + * Then the memory limit of the main process will be 1024M, but the memory + * limit of the child processes will remain 512M. + * + * @param string|list $phpExecutable */ public function withPhpExecutable(string|array $phpExecutable): self { diff --git a/src/Process/ProcessLauncherFactory.php b/src/Process/ProcessLauncherFactory.php index 95f9ec1..89fdc0a 100644 --- a/src/Process/ProcessLauncherFactory.php +++ b/src/Process/ProcessLauncherFactory.php @@ -31,6 +31,7 @@ interface ProcessLauncherFactory * @param callable(): void $tick */ public function create( + array $phpExecutable, array $command, string $workingDirectory, ?array $extraEnvironmentVariables, diff --git a/src/Process/StandardSymfonyProcessFactory.php b/src/Process/StandardSymfonyProcessFactory.php index b244404..fd638a3 100644 --- a/src/Process/StandardSymfonyProcessFactory.php +++ b/src/Process/StandardSymfonyProcessFactory.php @@ -14,6 +14,7 @@ namespace Webmozarts\Console\Parallelization\Process; use Symfony\Component\Process\InputStream; +use Symfony\Component\Process\PhpSubprocess; use Symfony\Component\Process\Process; final class StandardSymfonyProcessFactory implements SymfonyProcessFactory @@ -21,17 +22,17 @@ final class StandardSymfonyProcessFactory implements SymfonyProcessFactory public function startProcess( int $index, InputStream $inputStream, + array $phpExecutable, array $command, string $workingDirectory, ?array $environmentVariables, callable $processOutput ): Process { - $process = new Process( + $process = new PhpSubprocess( $command, $workingDirectory, $environmentVariables, - null, - null, + php: $phpExecutable, ); $process->setInput($inputStream); diff --git a/src/Process/SymfonyProcessFactory.php b/src/Process/SymfonyProcessFactory.php index c06db8a..a121130 100644 --- a/src/Process/SymfonyProcessFactory.php +++ b/src/Process/SymfonyProcessFactory.php @@ -35,6 +35,7 @@ interface SymfonyProcessFactory public function startProcess( int $index, InputStream $inputStream, + array $phpExecutable, array $command, string $workingDirectory, ?array $environmentVariables, diff --git a/src/Process/SymfonyProcessLauncher.php b/src/Process/SymfonyProcessLauncher.php index 3fdaa0a..044f4e8 100644 --- a/src/Process/SymfonyProcessLauncher.php +++ b/src/Process/SymfonyProcessLauncher.php @@ -60,6 +60,7 @@ final class SymfonyProcessLauncher implements ProcessLauncher * @param callable(): void $tick */ public function __construct( + private readonly array $phpExecutable, private readonly array $command, private readonly string $workingDirectory, private readonly ?array $environmentVariables, @@ -135,6 +136,7 @@ private function startProcess(InputStream $inputStream): void $process = $this->processFactory->startProcess( $index, $inputStream, + $this->phpExecutable, $this->command, $this->workingDirectory, $this->environmentVariables, diff --git a/src/Process/SymfonyProcessLauncherFactory.php b/src/Process/SymfonyProcessLauncherFactory.php index b29bc5d..8e4edcb 100644 --- a/src/Process/SymfonyProcessLauncherFactory.php +++ b/src/Process/SymfonyProcessLauncherFactory.php @@ -32,6 +32,7 @@ public function __construct(private SymfonyProcessFactory $processFactory) * @param callable(): void $tick */ public function create( + array $phpExecutable, array $command, string $workingDirectory, ?array $extraEnvironmentVariables, @@ -42,6 +43,7 @@ public function create( callable $tick ): ProcessLauncher { return new SymfonyProcessLauncher( + $phpExecutable, $command, $workingDirectory, $extraEnvironmentVariables, diff --git a/tests/Fixtures/Command/PhpSettingsCommand.php b/tests/Fixtures/Command/PhpSettingsCommand.php new file mode 100644 index 0000000..df6c832 --- /dev/null +++ b/tests/Fixtures/Command/PhpSettingsCommand.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Webmozarts\Console\Parallelization\Fixtures\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Terminal; +use Symfony\Component\Filesystem\Filesystem; +use Webmozarts\Console\Parallelization\ErrorHandler\ErrorHandler; +use Webmozarts\Console\Parallelization\Input\ParallelizationInput; +use Webmozarts\Console\Parallelization\Integration\TestDebugProgressBarFactory; +use Webmozarts\Console\Parallelization\Integration\TestLogger; +use Webmozarts\Console\Parallelization\Logger\Logger; +use Webmozarts\Console\Parallelization\Logger\NullLogger; +use Webmozarts\Console\Parallelization\Logger\StandardLogger; +use Webmozarts\Console\Parallelization\ParallelCommand; +use Webmozarts\Console\Parallelization\ParallelExecutorFactory; +use Webmozarts\Console\Parallelization\Parallelization; +use Webmozarts\Console\Parallelization\Process\PhpExecutableFinder; +use function file_get_contents; +use function ini_get; +use function json_decode; +use function realpath; +use function sprintf; +use function xdebug_break; +use const JSON_THROW_ON_ERROR; + +final class PhpSettingsCommand extends ParallelCommand +{ + public const string MAIN_PROCESS_OUTPUT_DIR = __DIR__.'/../../../dist/php-settings_main-process'; + public const string CHILD_PROCESS_OUTPUT_DIR = __DIR__.'/../../../dist/php-settings_child-process'; + + public function __construct( + private Filesystem $filesystem, + ) { + parent::__construct('test:php-settings'); + } + + /** + * @return list + */ + protected function fetchItems(InputInterface $input, OutputInterface $output): array + { + return ['item0']; + } + + protected function getParallelExecutableFactory( + callable $fetchItems, + callable $runSingleCommand, + callable $getItemName, + string $commandName, + InputDefinition $commandDefinition, + ErrorHandler $errorHandler + ): ParallelExecutorFactory { + return ParallelExecutorFactory::create( + $fetchItems, + $runSingleCommand, + $getItemName, + $commandName, + $commandDefinition, + $errorHandler, + ) + ->withRunBeforeFirstCommand(self::runBeforeFirstCommand(...)) + ->withPhpExecutable([ + ...PhpExecutableFinder::find(), + '-dmax_input_time=30', + ]) + ->withScriptPath(realpath(__DIR__.'/../../../bin/console')); + } + + private function runBeforeFirstCommand(): void + { + $this->dumpMemoryLimit(self::MAIN_PROCESS_OUTPUT_DIR); + } + + protected function runSingleCommand(string $item, InputInterface $input, OutputInterface $output): void + { + $this->dumpMemoryLimit(self::CHILD_PROCESS_OUTPUT_DIR); + } + + private function dumpMemoryLimit(string $filePath): void + { + $this->filesystem->dumpFile( + $filePath, + sprintf( + 'memory_limit=%s%smax_input_time=%s', + ini_get('memory_limit'), + "\n", + ini_get('max_input_time'), + ), + ); + } + + public static function createSettingsOutput(string $memoryLimit, string $maxInputTime): string + { + return sprintf( + 'memory_limit=%s%smax_input_time=%s', + $memoryLimit, + "\n", + $maxInputTime, + ); + } + + protected function getItemName(?int $count): string + { + return 1 === $count ? 'item' : 'items'; + } +} diff --git a/tests/Integration/PhpProcessSettingsTest.php b/tests/Integration/PhpProcessSettingsTest.php new file mode 100644 index 0000000..af281a4 --- /dev/null +++ b/tests/Integration/PhpProcessSettingsTest.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Webmozarts\Console\Parallelization\Integration; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; +use Webmozarts\Console\Parallelization\Fixtures\Command\ImportMoviesCommand; +use Webmozarts\Console\Parallelization\Fixtures\Command\ImportUnknownMoviesCountCommand; +use Webmozarts\Console\Parallelization\Fixtures\Command\LegacyCommand; +use Webmozarts\Console\Parallelization\Fixtures\Command\NoSubProcessCommand; +use Webmozarts\Console\Parallelization\Fixtures\Command\PhpSettingsCommand; +use Webmozarts\Console\Parallelization\Integration\BareKernel; +use Webmozarts\Console\Parallelization\Integration\OutputNormalizer; +use Webmozarts\Console\Parallelization\Integration\TestLogger; +use function array_column; +use function array_map; +use function file_get_contents; +use function ini_get; +use function preg_replace; +use function spl_object_id; +use function str_replace; + +/** + * @internal + */ +#[CoversNothing] +class PhpProcessSettingsTest extends TestCase +{ + protected function setUp(): void + { + self::cleanupOutputFiles(); + } + + protected function tearDown(): void + { + self::cleanupOutputFiles(); + } + + public function test_it_can_run_the_command_setting_the_memory_limit(): void + { + $commandProcess = Process::fromShellCommandline( + 'php -dmemory_limit="256M" bin/console test:php-settings', + __DIR__.'/../..', + ['XDEBUG_SESSION' => '1', 'XDEBUG_MODE' => 'debug'], + ); + $commandProcess->run(); + + self::assertTrue( + $commandProcess->isSuccessful(), + $commandProcess->getOutput() . $commandProcess->getErrorOutput(), + ); + + $expectedMainProcessPhpSettings = PhpSettingsCommand::createSettingsOutput( + '256M', // comes from setting it when launching the command + ini_get('max_input_time'), + ); + $actualMainProcessPhpSettings = file_get_contents(PhpSettingsCommand::MAIN_PROCESS_OUTPUT_DIR); + + $expectedChildProcessPhpSettings = PhpSettingsCommand::createSettingsOutput( + '256M', + '30', // comes from PhpSettingsCommand specifying it in the config + ); + $actualChildProcessPhpSettings = file_get_contents(PhpSettingsCommand::CHILD_PROCESS_OUTPUT_DIR); + + self::assertSame( + [ + 'main' => $expectedMainProcessPhpSettings, + 'child' => $expectedChildProcessPhpSettings, + ], + [ + 'main' => $actualMainProcessPhpSettings, + 'child' => $actualChildProcessPhpSettings, + ], + ); + } + + public function test_it_can_run_the_command_setting_the_a_php_setting_configured_in_the_command(): void + { + $commandProcess = Process::fromShellCommandline( + 'php -dmemory_limit="256M" -dmax_input_time=45 bin/console test:php-settings', + __DIR__.'/../..', + ['XDEBUG_SESSION' => '1', 'XDEBUG_MODE' => 'debug'], + ); + $commandProcess->run(); + + self::assertTrue( + $commandProcess->isSuccessful(), + $commandProcess->getOutput() . $commandProcess->getErrorOutput(), + ); + + $expectedMainProcessPhpSettings = PhpSettingsCommand::createSettingsOutput( + '256M', // comes from setting it when launching the command + '45', // comes from setting it when launching the command + ); + $actualMainProcessPhpSettings = file_get_contents(PhpSettingsCommand::MAIN_PROCESS_OUTPUT_DIR); + + $expectedChildProcessPhpSettings = PhpSettingsCommand::createSettingsOutput( + '256M', + '30', // comes from PhpSettingsCommand specifying it in the config + ); + $actualChildProcessPhpSettings = file_get_contents(PhpSettingsCommand::CHILD_PROCESS_OUTPUT_DIR); + + self::assertSame( + [ + 'main' => $expectedMainProcessPhpSettings, + 'child' => $expectedChildProcessPhpSettings, + ], + [ + 'main' => $actualMainProcessPhpSettings, + 'child' => $actualChildProcessPhpSettings, + ], + ); + } + + private static function cleanupOutputFiles(): void + { + $fileSystem = new Filesystem(); + $fileSystem->remove(PhpSettingsCommand::MAIN_PROCESS_OUTPUT_DIR); + $fileSystem->remove(PhpSettingsCommand::CHILD_PROCESS_OUTPUT_DIR); + } +}