From 73b95cd663817eca77f59e3d35681f939871ceee Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 13 Jun 2026 10:43:03 +0200 Subject: [PATCH 1/2] Implement process factory --- README.md | 4 + src/Formatter/CliPipeFormatter.php | 48 +++--- src/Formatter/DockerPipeFormatter.php | 96 ++++++------ src/Formatter/WslPipeFormatter.php | 26 ++-- src/Process/ProcessFactoryInterface.php | 17 +++ src/Process/ProcessFailedException.php | 10 ++ src/Process/ProcessInterface.php | 15 ++ src/Process/SymfonyProcess.php | 40 +++++ src/Process/SymfonyProcessFactory.php | 31 ++++ tests/Integration/DockerPipeFormatterTest.php | 72 +++++++++ tests/MockProcessFactoryTrait.php | 61 ++++++++ tests/Unit/Fixer/BlockStringFixerTest.php | 5 + tests/Unit/Formatter/CliPipeFormatterTest.php | 27 +++- .../Formatter/DockerPipeFormatterTest.php | 137 +++++++++++++----- tests/Unit/Formatter/WslPipeFormatterTest.php | 25 +++- 15 files changed, 468 insertions(+), 146 deletions(-) create mode 100644 src/Process/ProcessFactoryInterface.php create mode 100644 src/Process/ProcessFailedException.php create mode 100644 src/Process/ProcessInterface.php create mode 100644 src/Process/SymfonyProcess.php create mode 100644 src/Process/SymfonyProcessFactory.php create mode 100644 tests/Integration/DockerPipeFormatterTest.php create mode 100644 tests/MockProcessFactoryTrait.php diff --git a/README.md b/README.md index bceb6bc..bae65ff 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,8 @@ return (new PhpCsFixer\Config()) interpolationCodec: new PlainStringCodec(), // A normalizer for handling end-of-line characters. lineEndingNormalizer: null + // Factory for creating processes. Defaults to Symfony process factory. + processFactory = null, ) ]), ]); @@ -403,6 +405,8 @@ return (new PhpCsFixer\Config()) interpolationCodec: new PlainStringCodec(), // A normalizer for handling end-of-line characters. lineEndingNormalizer: null, + // Factory for creating processes. Defaults to Symfony process factory. + processFactory = null, ) ]), ]); diff --git a/src/Formatter/CliPipeFormatter.php b/src/Formatter/CliPipeFormatter.php index f26d9db..e773a70 100644 --- a/src/Formatter/CliPipeFormatter.php +++ b/src/Formatter/CliPipeFormatter.php @@ -2,10 +2,11 @@ namespace uuf6429\PhpCsFixerBlockstring\Formatter; -use Symfony\Component\Process\Process; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\CodecInterface; use uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer\DefaultNormalizer; use uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer\NormalizerInterface; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessFactoryInterface; +use uuf6429\PhpCsFixerBlockstring\Process\SymfonyProcessFactory; /** * It's no secret that the best formatting tools are not directly available in PHP. This formatter off-loads formatting @@ -27,6 +28,8 @@ * interpolationCodec: new PlainStringCodec(), * // A normalizer for handling end-of-line characters. * lineEndingNormalizer: null + * // Factory for creating processes. Defaults to Symfony process factory. + * processFactory = null, * ) * ]), * ]); @@ -49,32 +52,25 @@ class CliPipeFormatter extends AbstractStringFormatter */ private array $formatter; + /** + * @readonly + */ + private ProcessFactoryInterface $processFactory; + /** * @param TVersion|TCommand $versionValueOrCommand Either the version (as a string) or a command to retrieve the * version (as an array). * @param TCommand $formatCommand A command, as an array, to perform the formatting. - * @param null|bool|NormalizerInterface $lineEndingNormalizer */ public function __construct( $versionValueOrCommand, array $formatCommand, ?CodecInterface $interpolationCodec = null, - $lineEndingNormalizer = false + ?NormalizerInterface $lineEndingNormalizer = null, + ?ProcessFactoryInterface $processFactory = null ) { $this->formatter = $formatCommand; - - if (is_bool($lineEndingNormalizer)) { - trigger_deprecation( - 'uuf6429/php-cs-fixer-blockstring', - '1.0.4', - 'Passing a bool for argument $lineEndingNormalizer to %s is deprecated', - __METHOD__ - ); - $lineEndingNormalizer = new DefaultNormalizer( - DefaultNormalizer::LF, - $lineEndingNormalizer ? DefaultNormalizer::STRIP : DefaultNormalizer::NO_CHANGE - ); - } + $this->processFactory = $processFactory ?? new SymfonyProcessFactory(); parent::__construct( sprintf( @@ -88,7 +84,7 @@ public function __construct( : $this->exec($versionValueOrCommand, null) ), $interpolationCodec, - $lineEndingNormalizer + $lineEndingNormalizer ?? new DefaultNormalizer(DefaultNormalizer::LF, DefaultNormalizer::NO_CHANGE) ); } @@ -97,23 +93,15 @@ public function __construct( */ protected function exec(array $spec, ?string $input): string { - $process = is_array($spec['cmd']) - ? new Process( + return $this->processFactory + ->create( $spec['cmd'], $spec['cwd'] ?? null, $spec['env'] ?? null, - $input, - null + $input ) - : Process::fromShellCommandline( - $spec['cmd'], - $spec['cwd'] ?? null, - $spec['env'] ?? null, - $input, - null - ); - - return $process->mustRun()->getOutput(); + ->mustRun() + ->getOutput(); } protected function formatContent(string $original): string diff --git a/src/Formatter/DockerPipeFormatter.php b/src/Formatter/DockerPipeFormatter.php index 081fc7f..e28564d 100644 --- a/src/Formatter/DockerPipeFormatter.php +++ b/src/Formatter/DockerPipeFormatter.php @@ -4,11 +4,12 @@ use InvalidArgumentException; use RuntimeException; -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\Process; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\CodecInterface; use uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer\DefaultNormalizer; use uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer\NormalizerInterface; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessFactoryInterface; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessFailedException; +use uuf6429\PhpCsFixerBlockstring\Process\SymfonyProcessFactory; /** * The minimal setup, stable repeatability, and a rich ecosystem make Docker images an ideal source of formatting @@ -34,6 +35,8 @@ * interpolationCodec: new PlainStringCodec(), * // A normalizer for handling end-of-line characters. * lineEndingNormalizer: null, + * // Factory for creating processes. Defaults to Symfony process factory. + * processFactory = null, * ) * ]), * ]); @@ -72,39 +75,32 @@ class DockerPipeFormatter extends AbstractStringFormatter */ private array $imageDetails; + /** + * @readonly + */ + private ProcessFactoryInterface $processFactory; + /** * @param list $options * @param list $command * @param 'never'|'missing'|'always' $pullMode - * @param null|bool|NormalizerInterface $lineEndingNormalizer */ public function __construct( - string $image, - array $options = [], - array $command = [], - string $pullMode = 'never', - ?CodecInterface $interpolationCodec = null, - $lineEndingNormalizer = true + string $image, + array $options = [], + array $command = [], + string $pullMode = 'never', + ?CodecInterface $interpolationCodec = null, + ?NormalizerInterface $lineEndingNormalizer = null, + ?ProcessFactoryInterface $processFactory = null ) { $this->image = $image; $this->options = $options; $this->command = $command; $this->pullMode = $pullMode; + $this->processFactory = $processFactory ?? new SymfonyProcessFactory(); $this->imageDetails = $this->resolveImageDetails(); - if (is_bool($lineEndingNormalizer)) { - trigger_deprecation( - 'uuf6429/php-cs-fixer-blockstring', - '1.0.4', - 'Passing a bool for argument $lineEndingNormalizer to %s is deprecated', - __METHOD__ - ); - $lineEndingNormalizer = new DefaultNormalizer( - DefaultNormalizer::LF, - $lineEndingNormalizer ? DefaultNormalizer::STRIP : DefaultNormalizer::NO_CHANGE - ); - } - parent::__construct( sprintf( '%s: %s', @@ -119,7 +115,7 @@ public function __construct( ) ), $interpolationCodec, - $lineEndingNormalizer + $lineEndingNormalizer ?? new DefaultNormalizer(DefaultNormalizer::LF, DefaultNormalizer::STRIP) ); } @@ -139,7 +135,7 @@ private function resolveImageDetails(): array } $this->pullImage(); return $this->inspectImage(true); - // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreEnd case 'always': $this->pullImage(); @@ -155,17 +151,12 @@ private function resolveImageDetails(): array */ private function inspectImage(bool $throwOnFailure): ?array { - $process = new Process( - ['docker', 'image', 'inspect', $this->image, '--format={{.Os}}/{{.Architecture}} {{.Id}}'], - null, - null, - null, - null + $process = $this->processFactory->create( + ['docker', 'image', 'inspect', $this->image, '--format={{.Os}}/{{.Architecture}} {{.Id}}'] ); try { $result = $process->mustRun()->getOutput(); $result = explode(' ', trim($result), 2); - return ['platform' => $result[0], 'digest' => $result[1]]; } catch (ProcessFailedException $ex) { if (!$throwOnFailure) { @@ -179,33 +170,28 @@ private function inspectImage(bool $throwOnFailure): ?array private function pullImage(): void { - (new Process( - ['docker', 'image', 'pull', $this->image], - null, - null, - null, - null - ))->mustRun(); + $this->processFactory + ->create(['docker', 'image', 'pull', $this->image]) + ->mustRun(); } protected function formatContent(string $original): string { - $process = new Process( - [ - 'docker', - 'run', - '--rm', - '--interactive', - ...$this->options, - $this->imageDetails['digest'], - ...$this->command, - ], - null, - null, - $original, - null - ); - - return $process->mustRun()->getOutput(); + return $this->processFactory + ->create( + [ + 'docker', + 'run', + '--rm', + '--interactive', + ...$this->options, + $this->imageDetails['digest'], + ...$this->command, + ], + null, + null, + $original + )->mustRun() + ->getOutput(); } } diff --git a/src/Formatter/WslPipeFormatter.php b/src/Formatter/WslPipeFormatter.php index f0d0c06..d8e0a11 100644 --- a/src/Formatter/WslPipeFormatter.php +++ b/src/Formatter/WslPipeFormatter.php @@ -5,6 +5,7 @@ use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\CodecInterface; use uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer\DefaultNormalizer; use uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer\NormalizerInterface; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessFactoryInterface; /** * A formatter making use of Windows Subsystem for Linux (WSL). Of course, you will need to be running on Windows, @@ -20,31 +21,24 @@ class WslPipeFormatter extends CliPipeFormatter /** * @param 'standard'|'login'|'none' $shellType - * @param null|bool|NormalizerInterface $lineEndingNormalizer */ public function __construct( $versionValueOrCommand, array $formatCommand, ?CodecInterface $interpolationCodec = null, string $shellType = 'login', - $lineEndingNormalizer = true + ?NormalizerInterface $lineEndingNormalizer = null, + ?ProcessFactoryInterface $processFactory = null ) { $this->shellType = $shellType; - if (is_bool($lineEndingNormalizer)) { - trigger_deprecation( - 'uuf6429/php-cs-fixer-blockstring', - '1.0.4', - 'Passing a bool for argument $lineEndingNormalizer to %s is deprecated', - __METHOD__ - ); - $lineEndingNormalizer = new DefaultNormalizer( - DefaultNormalizer::LF, - $lineEndingNormalizer ? DefaultNormalizer::STRIP : DefaultNormalizer::NO_CHANGE - ); - } - - parent::__construct($versionValueOrCommand, $formatCommand, $interpolationCodec, $lineEndingNormalizer); + parent::__construct( + $versionValueOrCommand, + $formatCommand, + $interpolationCodec, + $lineEndingNormalizer ?? new DefaultNormalizer(DefaultNormalizer::LF, DefaultNormalizer::STRIP), + $processFactory + ); } protected function exec(array $spec, ?string $input): string diff --git a/src/Process/ProcessFactoryInterface.php b/src/Process/ProcessFactoryInterface.php new file mode 100644 index 0000000..1c4a0f1 --- /dev/null +++ b/src/Process/ProcessFactoryInterface.php @@ -0,0 +1,17 @@ + $command + * @param null|array $env + */ + public function create( + $command, + ?string $cwd = null, + ?array $env = null, + ?string $input = null + ): ProcessInterface; +} diff --git a/src/Process/ProcessFailedException.php b/src/Process/ProcessFailedException.php new file mode 100644 index 0000000..f17f9cc --- /dev/null +++ b/src/Process/ProcessFailedException.php @@ -0,0 +1,10 @@ +process = $process; + } + + public function mustRun(): self + { + try { + $this->process->mustRun(); + } catch (SymfonyProcessFailedException $e) { + throw new ProcessFailedException("Process failed to run {$e->getMessage()}", 0, $e); + } + + return $this; + } + + public function getOutput(): string + { + return $this->process->getOutput(); + } + + public function getErrorOutput(): string + { + return $this->process->getErrorOutput(); + } +} diff --git a/src/Process/SymfonyProcessFactory.php b/src/Process/SymfonyProcessFactory.php new file mode 100644 index 0000000..1850e05 --- /dev/null +++ b/src/Process/SymfonyProcessFactory.php @@ -0,0 +1,31 @@ +timeout = $timeout; + } + + public function create( + $command, + ?string $cwd = null, + ?array $env = null, + ?string $input = null + ): ProcessInterface { + return new SymfonyProcess( + is_array($command) + ? new Process($command, $cwd, $env, $input, $this->timeout) + : Process::fromShellCommandline($command, $cwd, $env, $input, $this->timeout) + ); + } +} diff --git a/tests/Integration/DockerPipeFormatterTest.php b/tests/Integration/DockerPipeFormatterTest.php new file mode 100644 index 0000000..c8e18a9 --- /dev/null +++ b/tests/Integration/DockerPipeFormatterTest.php @@ -0,0 +1,72 @@ +markTestSkipped( + 'GitHub actions are not able to run non-Windows docker images: https://github.com/orgs/community/discussions/138554' + ); + } + } + + public function testFormat(): void + { + $formatter = new DockerPipeFormatter('ghcr.io/jqlang/jq', [], [], 'always'); + $inputBlockString = new BlockString('', '', [new StringSegment( + " {\"hello\"\n : \"world\" , \"bye\":[ \"mars\" \n ]}" + )]); + + $outputBlockString = $formatter->formatBlock($inputBlockString); + + $this->assertSame( + <<<'JSON' + { + "hello": "world", + "bye": [ + "mars" + ] + } + JSON, + implode('', $outputBlockString->segments) + ); + } + + public function testBadPullMode(): void + { + $this->expectException(InvalidArgumentException::class); + + // @phpstan-ignore argument.type + new DockerPipeFormatter('ghcr.io/jqlang/jq', [], [], 'bad'); + } + + public function testBadImageWithoutPulling(): void + { + $this->expectException(RuntimeException::class); + + new DockerPipeFormatter('docker.io/uuf6429/bad-image', [], [], 'never'); + } + + public function testBadImageWithMissingPulling(): void + { + $this->expectException(ProcessFailedException::class); + + new DockerPipeFormatter('docker.io/uuf6429/bad-image', [], [], 'missing'); + } +} diff --git a/tests/MockProcessFactoryTrait.php b/tests/MockProcessFactoryTrait.php new file mode 100644 index 0000000..df11288 --- /dev/null +++ b/tests/MockProcessFactoryTrait.php @@ -0,0 +1,61 @@ +, ProcessInterface}> $processesToCreate + * @return ProcessFactoryInterface + */ + private function createProcessFactoryMock(array $processesToCreate): ProcessFactoryInterface + { + $processFactory = $this->createMock(ProcessFactoryInterface::class); + $processFactory->method('create') + ->willReturnCallback(function () use ($processesToCreate) { + $actualArgs = func_get_args(); + foreach ($processesToCreate as [$expectedArgs, $resultingProcess]) { + if ($actualArgs === $expectedArgs) { + return $resultingProcess; + } + } + + throw new RuntimeException(sprintf( + 'Unexpected call to ProcessFactory::create(%s)', + implode(', ', array_map(static fn($arg) => var_export($arg, true), $actualArgs)) + )); + }); + + return $processFactory; + } + + private function createProcessMock(string $output = '', string $errorOutput = ''): ProcessInterface + { + $process = $this->createMock(ProcessInterface::class); + $process->method('mustRun')->willReturnSelf(); + $process->method('getOutput')->willReturn($output); + $process->method('getErrorOutput')->willReturn($errorOutput); + + return $process; + } + + private function createFailingProcessMock(string $errorOutput = ''): ProcessInterface + { + $process = $this->createMock(ProcessInterface::class); + $process->method('mustRun')->willThrowException(new ProcessFailedException('Process failed to run')); + $process->method('getErrorOutput')->willReturn($errorOutput); + + return $process; + } +} diff --git a/tests/Unit/Fixer/BlockStringFixerTest.php b/tests/Unit/Fixer/BlockStringFixerTest.php index c89fc7f..47cb4a5 100644 --- a/tests/Unit/Fixer/BlockStringFixerTest.php +++ b/tests/Unit/Fixer/BlockStringFixerTest.php @@ -47,6 +47,11 @@ public function testGetConfigurationDefinition(): void (new BlockStringFixer())->getConfigurationDefinition(); } + public function testConfigDsl(): void + { + $this->assertSame(['formatters' => []], BlockStringFixer::config([])); + } + /** * @param mixed $formatters * @dataProvider invalidConfigurationProvider diff --git a/tests/Unit/Formatter/CliPipeFormatterTest.php b/tests/Unit/Formatter/CliPipeFormatterTest.php index a06790c..6208e2f 100644 --- a/tests/Unit/Formatter/CliPipeFormatterTest.php +++ b/tests/Unit/Formatter/CliPipeFormatterTest.php @@ -6,17 +6,32 @@ use uuf6429\PhpCsFixerBlockstring\BlockString\BlockString; use uuf6429\PhpCsFixerBlockstring\BlockString\StringSegment; use uuf6429\PhpCsFixerBlockstring\Formatter\CliPipeFormatter; +use uuf6429\PhpCsFixerBlockstringTests\MockProcessFactoryTrait; /** * @internal */ final class CliPipeFormatterTest extends TestCase { + use MockProcessFactoryTrait; + public function testFormat(): void { $formatter = new CliPipeFormatter( ['cmd' => 'php -v'], - ['cmd' => ['php', '-r', 'echo "(" . stream_get_contents(STDIN) . ")";']] + ['cmd' => ['php', '-r', 'echo "(" . stream_get_contents(STDIN) . ")";']], + null, + null, + $this->createProcessFactoryMock([ + [ + ['php -v', null, null, null], + $this->createProcessMock('v1.0'), + ], + [ + [['php', '-r', 'echo "(" . stream_get_contents(STDIN) . ")";'], null, null, 'foo'], + $this->createProcessMock('(foo)'), + ] + ]) ); $inputBlockString = new BlockString('', '', [new StringSegment('foo')]); @@ -29,7 +44,15 @@ public function testFormatWithVersionOverride(): void { $formatter = new CliPipeFormatter( 'some version', - ['cmd' => ['php', '-r', 'echo "(" . stream_get_contents(STDIN) . ")";']] + ['cmd' => ['php', '-r', 'echo "(" . stream_get_contents(STDIN) . ")";']], + null, + null, + $this->createProcessFactoryMock([ + [ + [['php', '-r', 'echo "(" . stream_get_contents(STDIN) . ")";'], null, null, 'foo'], + $this->createProcessMock('(foo)'), + ] + ]) ); $inputBlockString = new BlockString('', '', [new StringSegment('foo')]); diff --git a/tests/Unit/Formatter/DockerPipeFormatterTest.php b/tests/Unit/Formatter/DockerPipeFormatterTest.php index 0919720..3d07a74 100644 --- a/tests/Unit/Formatter/DockerPipeFormatterTest.php +++ b/tests/Unit/Formatter/DockerPipeFormatterTest.php @@ -5,68 +5,135 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use RuntimeException; -use Symfony\Component\Process\Exception\ProcessFailedException; use uuf6429\PhpCsFixerBlockstring\BlockString\BlockString; use uuf6429\PhpCsFixerBlockstring\BlockString\StringSegment; use uuf6429\PhpCsFixerBlockstring\Formatter\DockerPipeFormatter; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessFactoryInterface; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessFailedException; +use uuf6429\PhpCsFixerBlockstring\Process\ProcessInterface; +use uuf6429\PhpCsFixerBlockstringTests\MockProcessFactoryTrait; /** * @internal */ final class DockerPipeFormatterTest extends TestCase { - protected function setUp(): void + use MockProcessFactoryTrait; + + public function testFormatWithNeverPullMode(): void { - parent::setUp(); + $formatter = new DockerPipeFormatter( + 'my-image', + [], + [], + 'never', + null, + null, + $this->createProcessFactoryMock([ + [ + [['docker', 'image', 'inspect', 'my-image', '--format={{.Os}}/{{.Architecture}} {{.Id}}'], null, null, null], + $this->createProcessMock("linux/amd64 sha256:digest123\n"), + ], + [ + [['docker', 'run', '--rm', '--interactive', 'sha256:digest123'], null, null, 'input content'], + $this->createProcessMock('formatted content'), + ], + ]) + ); + + $input = new BlockString('', '', [new StringSegment('input content')]); + $output = $formatter->formatBlock($input); - if (PHP_OS_FAMILY === 'Windows' && getenv('GITHUB_ACTIONS') === 'true') { - $this->markTestSkipped( - 'GitHub actions are not able to run non-Windows docker images: https://github.com/orgs/community/discussions/138554' - ); - } + $this->assertSame('formatted content', implode('', $output->segments)); } - public function testFormat(): void + public function testFormatWithAlwaysPullMode(): void { - $formatter = new DockerPipeFormatter('ghcr.io/jqlang/jq', [], [], 'always'); - $inputBlockString = new BlockString('', '', [new StringSegment( - " {\"hello\"\n : \"world\" , \"bye\":[ \"mars\" \n ]}" - )]); - - $outputBlockString = $formatter->formatBlock($inputBlockString); - - $this->assertSame( - <<<'JSON' - { - "hello": "world", - "bye": [ - "mars" - ] - } - JSON, - implode('', $outputBlockString->segments) + $formatter = new DockerPipeFormatter( + 'my-image', + [], + [], + 'always', + null, + null, + $this->createProcessFactoryMock([ + [ + [['docker', 'image', 'pull', 'my-image'], null, null, null], + $this->createProcessMock("dummy docker pull output\n"), + ], + [ + [['docker', 'image', 'inspect', 'my-image', '--format={{.Os}}/{{.Architecture}} {{.Id}}'], null, null, null], + $this->createProcessMock("linux/amd64 sha256:digest123\n"), + ], + [ + [['docker', 'run', '--rm', '--interactive', 'sha256:digest123'], null, null, 'input content'], + $this->createProcessMock('formatted content'), + ], + ]) ); + + $input = new BlockString('', '', [new StringSegment('input content')]); + $output = $formatter->formatBlock($input); + + $this->assertSame('formatted content', implode('', $output->segments)); } - public function testBadPullMode(): void + public function testFormatWithMissingPullModeWhenImageIsMissing(): void { - $this->expectException(InvalidArgumentException::class); + $formatter = new DockerPipeFormatter( + 'my-image', + [], + [], + 'missing', + null, + null, + $this->createProcessFactoryMock([ + [ + [], + $this->createFailingProcessMock('No such image'), + ], + [ + [['docker', 'image', 'pull', 'my-image'], null, null, null], + $this->createProcessMock("dummy docker pull output\n"), + ], + [ + [['docker', 'image', 'inspect', 'my-image', '--format={{.Os}}/{{.Architecture}} {{.Id}}'], null, null, null], + $this->createProcessMock("linux/amd64 sha256:digest123\n"), + ], + [ + [['docker', 'run', '--rm', '--interactive', 'sha256:digest123'], null, null, 'input content'], + $this->createProcessMock('formatted content'), + ], + ]) + ); + + $input = new BlockString('', '', [new StringSegment('input content')]); + $output = $formatter->formatBlock($input); - // @phpstan-ignore argument.type - new DockerPipeFormatter('ghcr.io/jqlang/jq', [], [], 'bad'); + $this->assertSame('formatted content', implode('', $output->segments)); } - public function testBadImageWithoutPulling(): void + public function testInspectFailureThrowsException(): void { + $process = $this->createMock(ProcessInterface::class); + $process->method('mustRun')->willThrowException(new ProcessFailedException('No such image')); + $process->method('getErrorOutput')->willReturn('No such image'); + + $processFactory = $this->createMock(ProcessFactoryInterface::class); + $processFactory->method('create')->willReturn($process); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No such image'); - new DockerPipeFormatter('docker.io/uuf6429/bad-image', [], [], 'never'); + new DockerPipeFormatter('my-image', [], [], 'never', null, null, $processFactory); } - public function testBadImageWithMissingPulling(): void + public function testInvalidPullModeThrowsException(): void { - $this->expectException(ProcessFailedException::class); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported Pull Mode: invalid'); - new DockerPipeFormatter('docker.io/uuf6429/bad-image', [], [], 'missing'); + // @phpstan-ignore-next-line + new DockerPipeFormatter('my-image', [], [], 'invalid'); } } diff --git a/tests/Unit/Formatter/WslPipeFormatterTest.php b/tests/Unit/Formatter/WslPipeFormatterTest.php index f620ae4..9a89da2 100644 --- a/tests/Unit/Formatter/WslPipeFormatterTest.php +++ b/tests/Unit/Formatter/WslPipeFormatterTest.php @@ -6,24 +6,33 @@ use uuf6429\PhpCsFixerBlockstring\BlockString\BlockString; use uuf6429\PhpCsFixerBlockstring\BlockString\StringSegment; use uuf6429\PhpCsFixerBlockstring\Formatter\WslPipeFormatter; +use uuf6429\PhpCsFixerBlockstringTests\MockProcessFactoryTrait; /** * @internal */ final class WslPipeFormatterTest extends TestCase { + use MockProcessFactoryTrait; + public function testFormat(): void { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('WSL is only available on Windows'); - } - if (getenv('GITHUB_ACTIONS') === 'true') { - $this->markTestSkipped('WSL on GitHub Actions is poorly supported and unusable'); - } - $formatter = new WslPipeFormatter( ['cmd' => 'php -v'], - ['cmd' => ['php', '-r', 'echo strrev(stream_get_contents(STDIN));']] + ['cmd' => ['php', '-r', 'echo strrev(stream_get_contents(STDIN));']], + null, + 'login', + null, + $this->createProcessFactoryMock([ + [ + ['wsl --shell-type login -- php -v', null, null, null], + $this->createProcessMock('v1.0'), + ], + [ + ['wsl --shell-type login -- "php" "-r" "echo strrev(stream_get_contents(STDIN));"', null, null, 'foobar'], + $this->createProcessMock('raboof'), + ] + ]) ); $inputBlockString = new BlockString('', '', [new StringSegment('foobar')]); From 837065c83af8fb5202fd68aa03a49ed1bb1e3235 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 13 Jun 2026 10:49:46 +0200 Subject: [PATCH 2/2] Small fixes --- README.md | 6 +++--- src/Formatter/CliPipeFormatter.php | 4 ++-- src/Formatter/DockerPipeFormatter.php | 2 +- tests/Unit/Formatter/WslPipeFormatterTest.php | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bae65ff..d1deafe 100644 --- a/README.md +++ b/README.md @@ -366,9 +366,9 @@ return (new PhpCsFixer\Config()) // A codec for handling placeholers in template strings; depends on the content being formatted. interpolationCodec: new PlainStringCodec(), // A normalizer for handling end-of-line characters. - lineEndingNormalizer: null + lineEndingNormalizer: null, // Factory for creating processes. Defaults to Symfony process factory. - processFactory = null, + processFactory: null, ) ]), ]); @@ -406,7 +406,7 @@ return (new PhpCsFixer\Config()) // A normalizer for handling end-of-line characters. lineEndingNormalizer: null, // Factory for creating processes. Defaults to Symfony process factory. - processFactory = null, + processFactory: null, ) ]), ]); diff --git a/src/Formatter/CliPipeFormatter.php b/src/Formatter/CliPipeFormatter.php index e773a70..7cac83c 100644 --- a/src/Formatter/CliPipeFormatter.php +++ b/src/Formatter/CliPipeFormatter.php @@ -27,9 +27,9 @@ * // A codec for handling placeholers in template strings; depends on the content being formatted. * interpolationCodec: new PlainStringCodec(), * // A normalizer for handling end-of-line characters. - * lineEndingNormalizer: null + * lineEndingNormalizer: null, * // Factory for creating processes. Defaults to Symfony process factory. - * processFactory = null, + * processFactory: null, * ) * ]), * ]); diff --git a/src/Formatter/DockerPipeFormatter.php b/src/Formatter/DockerPipeFormatter.php index e28564d..21f539b 100644 --- a/src/Formatter/DockerPipeFormatter.php +++ b/src/Formatter/DockerPipeFormatter.php @@ -36,7 +36,7 @@ * // A normalizer for handling end-of-line characters. * lineEndingNormalizer: null, * // Factory for creating processes. Defaults to Symfony process factory. - * processFactory = null, + * processFactory: null, * ) * ]), * ]); diff --git a/tests/Unit/Formatter/WslPipeFormatterTest.php b/tests/Unit/Formatter/WslPipeFormatterTest.php index 9a89da2..9949ad3 100644 --- a/tests/Unit/Formatter/WslPipeFormatterTest.php +++ b/tests/Unit/Formatter/WslPipeFormatterTest.php @@ -31,6 +31,10 @@ public function testFormat(): void [ ['wsl --shell-type login -- "php" "-r" "echo strrev(stream_get_contents(STDIN));"', null, null, 'foobar'], $this->createProcessMock('raboof'), + ], + [ + ['wsl --shell-type login -- \'php\' \'-r\' \'echo strrev(stream_get_contents(STDIN));\'', null, null, 'foobar'], + $this->createProcessMock('raboof'), ] ]) );