From 8d5a2b535a7832bc68e5ef6ca33b6e4d2e9bcd5f Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Thu, 23 Feb 2023 11:48:30 +0000 Subject: [PATCH 01/11] [10.x] Add pipe function to Process layer --- src/Illuminate/Process/PendingProcess.php | 22 ++++++++++++++++++++++ src/Illuminate/Support/Facades/Process.php | 1 + tests/Process/ProcessTest.php | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 339d8c33045c..260b490ba854 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -280,6 +280,28 @@ public function start(array|string $command = null, callable $output = null) return new InvokedProcess(tap($process)->start($output)); } + /** + * Runs a sequence of processes, passing the result of the previous command as the input for the next + * + * @param array $commands + * @param callable|null $output + * @return string + */ + public function pipe(array $commands, callable $output = null): string + { + $result = null; + foreach ($commands as $command) { + if (! is_null($result)) { + $this->input($result); + } + + $processResult = $this->run($command, $output); + $result = $processResult->output(); + } + + return $result; + } + /** * Get a Symfony Process instance from the current pending command. * diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index acb0612f6a3d..5196097ba701 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -16,6 +16,7 @@ * @method static \Illuminate\Process\PendingProcess quietly() * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) + * @method static \Illuminate\Process\PendingProcess pipe(array $commands, callable $output = null) * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable $output = null) * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 4a58cd94fb12..26fc65165770 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -6,6 +6,7 @@ use Illuminate\Process\Exceptions\ProcessFailedException; use Illuminate\Process\Exceptions\ProcessTimedOutException; use Illuminate\Process\Factory; +use Illuminate\Support\Facades\Process; use Mockery as m; use OutOfBoundsException; use PHPUnit\Framework\TestCase; @@ -496,6 +497,25 @@ public function testRealProcessesCanUseStandardInput() $this->assertSame('foobar', $result->output()); } + public function testProcessPipe() + { + if (windows_os()) { + $this->markTestSkipped('Requires Linux.'); + } + + $factory = new Factory(); + $factory->fake([ + 'cat *' => "Hello, world\nfoo\nbar", + ]); + + $result = $factory->pipe([ + 'cat test', + 'grep -i "foo"' + ]); + + $this->assertSame("foo\n", $result); + } + public function testFakeInvokedProcessOutputWithLatestOutput() { $factory = new Factory; From 79b92e34a5125f63c6f550bc37279b003fd447d9 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Thu, 23 Feb 2023 11:54:27 +0000 Subject: [PATCH 02/11] Fixing code style --- src/Illuminate/Process/PendingProcess.php | 2 +- tests/Process/ProcessTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 260b490ba854..ab0291b254ac 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -281,7 +281,7 @@ public function start(array|string $command = null, callable $output = null) } /** - * Runs a sequence of processes, passing the result of the previous command as the input for the next + * Runs a sequence of processes, passing the result of the previous command as the input for the next. * * @param array $commands * @param callable|null $output diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 26fc65165770..7181aff25891 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -510,7 +510,7 @@ public function testProcessPipe() $result = $factory->pipe([ 'cat test', - 'grep -i "foo"' + 'grep -i "foo"', ]); $this->assertSame("foo\n", $result); From 0aa144f5a3af47306a276779953db695c450e4e3 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Thu, 23 Feb 2023 13:02:56 +0000 Subject: [PATCH 03/11] Update pipe to accept fully configured processes as parameters --- src/Illuminate/Process/Factory.php | 11 +++ src/Illuminate/Process/PendingProcess.php | 22 ----- src/Illuminate/Process/Pipe.php | 100 +++++++++++++++++++++ src/Illuminate/Support/Facades/Process.php | 2 +- tests/Process/ProcessTest.php | 14 +-- 5 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 src/Illuminate/Process/Pipe.php diff --git a/src/Illuminate/Process/Factory.php b/src/Illuminate/Process/Factory.php index ccd7e4689e49..ce3f1e3037e1 100644 --- a/src/Illuminate/Process/Factory.php +++ b/src/Illuminate/Process/Factory.php @@ -271,6 +271,17 @@ public function pool(callable $callback) return new Pool($this, $callback); } + /** + * Start defining a pipe of processes. + * + * @param callable $callback + * @return \Illuminate\Process\Pipe + */ + public function pipe(callable $callback) + { + return new Pipe($this, $callback); + } + /** * Run a pool of processes and wait for them to finish executing. * diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index ab0291b254ac..339d8c33045c 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -280,28 +280,6 @@ public function start(array|string $command = null, callable $output = null) return new InvokedProcess(tap($process)->start($output)); } - /** - * Runs a sequence of processes, passing the result of the previous command as the input for the next. - * - * @param array $commands - * @param callable|null $output - * @return string - */ - public function pipe(array $commands, callable $output = null): string - { - $result = null; - foreach ($commands as $command) { - if (! is_null($result)) { - $this->input($result); - } - - $processResult = $this->run($command, $output); - $result = $processResult->output(); - } - - return $result; - } - /** * Get a Symfony Process instance from the current pending command. * diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php new file mode 100644 index 000000000000..81549dfc0c89 --- /dev/null +++ b/src/Illuminate/Process/Pipe.php @@ -0,0 +1,100 @@ +factory = $factory; + $this->callback = $callback; + } + + /** + * Add a process to the pipe with a key. + * + * @param string $key + * @return \Illuminate\Process\PendingProcess + */ + public function as(string $key) + { + return tap($this->factory->newPendingProcess(), function ($pendingProcess) use ($key) { + $this->pendingProcesses[$key] = $pendingProcess; + }); + } + + /** + * Runs the processes in the pipe + * + * @param callable|null $output + * @return string + */ + public function run(?callable $output = null) + { + call_user_func($this->callback, $this); + + $result = null; + foreach ($this->pendingProcesses as $pendingProcess) { + if (! $pendingProcess instanceof PendingProcess) { + throw new InvalidArgumentException('Process pipe must only contain pending processes.'); + } + + if (! is_null($result)) { + $pendingProcess->input($result); + } + + $processResult = $pendingProcess->run(null, $output); + $result = $processResult->output(); + } + + return $result; + } + + /** + * Dynamically proxy methods calls to a new pending process. + * + * @param string $method + * @param array $parameters + * @return \Illuminate\Process\PendingProcess + */ + public function __call($method, $parameters) + { + return tap($this->factory->{$method}(...$parameters), function ($pendingProcess) { + $this->pendingProcesses[] = $pendingProcess; + }); + } +} diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index 5196097ba701..3003782cef6f 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -16,7 +16,6 @@ * @method static \Illuminate\Process\PendingProcess quietly() * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) - * @method static \Illuminate\Process\PendingProcess pipe(array $commands, callable $output = null) * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable $output = null) * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) @@ -34,6 +33,7 @@ * @method static \Illuminate\Process\Factory assertDidntRun(\Closure|string $callback) * @method static \Illuminate\Process\Factory assertNothingRan() * @method static \Illuminate\Process\Pool pool(callable $callback) + * @method static \Illuminate\Process\Pipe pipe(callable $callback) * @method static \Illuminate\Process\ProcessPoolResults concurrently(callable $callback, callable|null $output = null) * @method static \Illuminate\Process\PendingProcess newPendingProcess() * @method static void macro(string $name, object|callable $macro) diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 7181aff25891..ac0e92507094 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -503,17 +503,19 @@ public function testProcessPipe() $this->markTestSkipped('Requires Linux.'); } - $factory = new Factory(); + $factory = new Factory; $factory->fake([ 'cat *' => "Hello, world\nfoo\nbar", ]); - $result = $factory->pipe([ - 'cat test', - 'grep -i "foo"', - ]); + $pipe = $factory->pipe(function ($pipe) { + return [ + $pipe->command('cat test'), + $pipe->command('grep -i "foo"'), + ]; + }); - $this->assertSame("foo\n", $result); + $this->assertSame("foo\n", $pipe->run()); } public function testFakeInvokedProcessOutputWithLatestOutput() From 80749873416e445098dbdeee7a2bbad5b629d6ba Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Thu, 23 Feb 2023 13:03:53 +0000 Subject: [PATCH 04/11] Fixing code style --- src/Illuminate/Process/Pipe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index 81549dfc0c89..a4e32cecbddb 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -58,7 +58,7 @@ public function as(string $key) } /** - * Runs the processes in the pipe + * Runs the processes in the pipe. * * @param callable|null $output * @return string From 5f0dea800bfa746d66ea8a06ae54f99f49ac4d10 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Thu, 23 Feb 2023 13:06:46 +0000 Subject: [PATCH 05/11] Remove unused use statement on test file --- tests/Process/ProcessTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index ac0e92507094..1a2147fe1ce4 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -6,7 +6,6 @@ use Illuminate\Process\Exceptions\ProcessFailedException; use Illuminate\Process\Exceptions\ProcessTimedOutException; use Illuminate\Process\Factory; -use Illuminate\Support\Facades\Process; use Mockery as m; use OutOfBoundsException; use PHPUnit\Framework\TestCase; From 2fcebc88b4994cde8408d554f2c98a3819f8ad88 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 25 Feb 2023 12:09:24 +0530 Subject: [PATCH 06/11] formatting --- src/Illuminate/Process/Factory.php | 2 +- src/Illuminate/Process/PendingProcess.php | 3 +++ src/Illuminate/Process/Pipe.php | 28 ++++++++++------------- tests/Process/ProcessTest.php | 8 +++---- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/Illuminate/Process/Factory.php b/src/Illuminate/Process/Factory.php index ce3f1e3037e1..261ce7e50365 100644 --- a/src/Illuminate/Process/Factory.php +++ b/src/Illuminate/Process/Factory.php @@ -272,7 +272,7 @@ public function pool(callable $callback) } /** - * Start defining a pipe of processes. + * Start defining a series of piped processes. * * @param callable $callback * @return \Illuminate\Process\Pipe diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 339d8c33045c..7ed3dd6f1eaa 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Process\Exceptions\ProcessTimedOutException; use Illuminate\Support\Str; +use Illuminate\Support\Traits\Conditionable; use LogicException; use RuntimeException; use Symfony\Component\Process\Exception\ProcessTimedOutException as SymfonyTimeoutException; @@ -12,6 +13,8 @@ class PendingProcess { + use Conditionable; + /** * The process factory instance. * diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index a4e32cecbddb..869f828e5ced 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -32,7 +32,7 @@ class Pipe protected $pendingProcesses = []; /** - * Create a new process pipe. + * Create a new series of piped processes. * * @param \Illuminate\Process\Factory $factory * @param callable $callback @@ -61,27 +61,23 @@ public function as(string $key) * Runs the processes in the pipe. * * @param callable|null $output - * @return string + * @return \Illuminate\Contracts\Process\ProcessResult */ public function run(?callable $output = null) { call_user_func($this->callback, $this); - $result = null; - foreach ($this->pendingProcesses as $pendingProcess) { - if (! $pendingProcess instanceof PendingProcess) { - throw new InvalidArgumentException('Process pipe must only contain pending processes.'); - } + return collect($this->pendingProcesses) + ->reduce(function ($previousProcessResult, $pendingProcess) use ($output) { + if (! $pendingProcess instanceof PendingProcess) { + throw new InvalidArgumentException('Process pipe must only contain process instances.'); + } - if (! is_null($result)) { - $pendingProcess->input($result); - } - - $processResult = $pendingProcess->run(null, $output); - $result = $processResult->output(); - } - - return $result; + return $pendingProcess->when( + $previousProcessResult, + fn () => $pendingProcess->input($previousProcessResult->output()) + )->run(output: $output); + }); } /** diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 1a2147fe1ce4..5c27197f9238 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -508,13 +508,11 @@ public function testProcessPipe() ]); $pipe = $factory->pipe(function ($pipe) { - return [ - $pipe->command('cat test'), - $pipe->command('grep -i "foo"'), - ]; + $pipe->command('cat test'); + $pipe->command('grep -i "foo"'); }); - $this->assertSame("foo\n", $pipe->run()); + $this->assertSame("foo\n", $pipe->run()->output()); } public function testFakeInvokedProcessOutputWithLatestOutput() From 2029a20835f92f66a272dbbf3d931452e36680d8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 25 Feb 2023 12:14:11 +0530 Subject: [PATCH 07/11] exception message --- src/Illuminate/Process/Pipe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index 869f828e5ced..7f96d6617ab8 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -70,7 +70,7 @@ public function run(?callable $output = null) return collect($this->pendingProcesses) ->reduce(function ($previousProcessResult, $pendingProcess) use ($output) { if (! $pendingProcess instanceof PendingProcess) { - throw new InvalidArgumentException('Process pipe must only contain process instances.'); + throw new InvalidArgumentException('Process pipe must only contain pending processes.'); } return $pendingProcess->when( From afdfb4b90bf823d76c1837029364eb9ee783d5a4 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Mon, 20 Mar 2023 13:21:18 -0300 Subject: [PATCH 08/11] Wrap error in process pipe with a custom ProcessPipeException --- src/Illuminate/Process/Pipe.php | 9 ++++++++- src/Illuminate/Process/ProcessPipeException.php | 13 +++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Process/ProcessPipeException.php diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index 7f96d6617ab8..cba7cf34bf81 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -62,6 +62,7 @@ public function as(string $key) * * @param callable|null $output * @return \Illuminate\Contracts\Process\ProcessResult + * @throws ProcessPipeException */ public function run(?callable $output = null) { @@ -73,10 +74,16 @@ public function run(?callable $output = null) throw new InvalidArgumentException('Process pipe must only contain pending processes.'); } - return $pendingProcess->when( + $result = $pendingProcess->when( $previousProcessResult, fn () => $pendingProcess->input($previousProcessResult->output()) )->run(output: $output); + + if ($result->failed()) { + throw new ProcessPipeException($pendingProcess); + } + + return $result; }); } diff --git a/src/Illuminate/Process/ProcessPipeException.php b/src/Illuminate/Process/ProcessPipeException.php new file mode 100644 index 000000000000..5ff1ce6c86ca --- /dev/null +++ b/src/Illuminate/Process/ProcessPipeException.php @@ -0,0 +1,13 @@ +command}' failed"); + } +} From 348d698ea9b06ec7144626d56a7c2b9d14788c4f Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Mon, 20 Mar 2023 13:23:02 -0300 Subject: [PATCH 09/11] Fixed code style issues --- src/Illuminate/Process/Pipe.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index cba7cf34bf81..8b2a778b825a 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -62,6 +62,7 @@ public function as(string $key) * * @param callable|null $output * @return \Illuminate\Contracts\Process\ProcessResult + * * @throws ProcessPipeException */ public function run(?callable $output = null) From e9a8ffd2ea7f089901306e0f8a6d6434101c06de Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Wed, 5 Apr 2023 16:44:11 -0300 Subject: [PATCH 10/11] Updated pipe to return last failed process instead of throwing exception and added test case for failed pipe run --- src/Illuminate/Process/Pipe.php | 14 +++++--------- .../Process/ProcessPipeException.php | 13 ------------- tests/Process/ProcessTest.php | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 22 deletions(-) delete mode 100644 src/Illuminate/Process/ProcessPipeException.php diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index 8b2a778b825a..127b486f3689 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -62,8 +62,6 @@ public function as(string $key) * * @param callable|null $output * @return \Illuminate\Contracts\Process\ProcessResult - * - * @throws ProcessPipeException */ public function run(?callable $output = null) { @@ -75,16 +73,14 @@ public function run(?callable $output = null) throw new InvalidArgumentException('Process pipe must only contain pending processes.'); } - $result = $pendingProcess->when( + if ($previousProcessResult && $previousProcessResult->failed()) { + return $previousProcessResult; + } + + return $pendingProcess->when( $previousProcessResult, fn () => $pendingProcess->input($previousProcessResult->output()) )->run(output: $output); - - if ($result->failed()) { - throw new ProcessPipeException($pendingProcess); - } - - return $result; }); } diff --git a/src/Illuminate/Process/ProcessPipeException.php b/src/Illuminate/Process/ProcessPipeException.php deleted file mode 100644 index 5ff1ce6c86ca..000000000000 --- a/src/Illuminate/Process/ProcessPipeException.php +++ /dev/null @@ -1,13 +0,0 @@ -command}' failed"); - } -} diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 5c27197f9238..f322191fb510 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -515,6 +515,25 @@ public function testProcessPipe() $this->assertSame("foo\n", $pipe->run()->output()); } + public function testProcessPipeFailed() + { + if (windows_os()) { + $this->markTestSkipped('Requires Linux.'); + } + + $factory = new Factory; + $factory->fake([ + 'cat *' => $factory->result(exitCode: 1), + ]); + + $pipe = $factory->pipe(function ($pipe) { + $pipe->command('cat test'); + $pipe->command('grep -i "foo"'); + }); + + $this->assertTrue($pipe->run()->failed()); + } + public function testFakeInvokedProcessOutputWithLatestOutput() { $factory = new Factory; From 0b4bdba9c23efae6dcd819010f9b403484874d1b Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Thu, 6 Apr 2023 12:59:21 -0300 Subject: [PATCH 11/11] Update pipe to handle keys for processes --- src/Illuminate/Process/Pipe.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Process/Pipe.php b/src/Illuminate/Process/Pipe.php index 127b486f3689..d1847af25d00 100644 --- a/src/Illuminate/Process/Pipe.php +++ b/src/Illuminate/Process/Pipe.php @@ -68,7 +68,7 @@ public function run(?callable $output = null) call_user_func($this->callback, $this); return collect($this->pendingProcesses) - ->reduce(function ($previousProcessResult, $pendingProcess) use ($output) { + ->reduce(function ($previousProcessResult, $pendingProcess, $key) use ($output) { if (! $pendingProcess instanceof PendingProcess) { throw new InvalidArgumentException('Process pipe must only contain pending processes.'); } @@ -80,7 +80,9 @@ public function run(?callable $output = null) return $pendingProcess->when( $previousProcessResult, fn () => $pendingProcess->input($previousProcessResult->output()) - )->run(output: $output); + )->run(output: $output ? function ($type, $buffer) use ($key, $output) { + $output($type, $buffer, $key); + } : null); }); }