diff --git a/src/Illuminate/Contracts/Process/InvokedProcess.php b/src/Illuminate/Contracts/Process/InvokedProcess.php index fee2f6622583..99746d783fef 100644 --- a/src/Illuminate/Contracts/Process/InvokedProcess.php +++ b/src/Illuminate/Contracts/Process/InvokedProcess.php @@ -61,4 +61,12 @@ public function latestErrorOutput(); * @return \Illuminate\Process\ProcessResult */ public function wait(?callable $output = null); + + /** + * Wait until the given callback returns true. + * + * @param callable|null $output + * @return \Illuminate\Process\ProcessResult + */ + public function waitUntil(?callable $output = null); } diff --git a/src/Illuminate/Process/FakeInvokedProcess.php b/src/Illuminate/Process/FakeInvokedProcess.php index b5cb0bd22784..78ced5adbf49 100644 --- a/src/Illuminate/Process/FakeInvokedProcess.php +++ b/src/Illuminate/Process/FakeInvokedProcess.php @@ -286,6 +286,37 @@ public function wait(?callable $output = null) return $this->process->toProcessResult($this->command); } + /** + * Wait until the given callback returns true. + * + * @param callable|null $output + * @return \Illuminate\Contracts\Process\ProcessResult + */ + public function waitUntil(?callable $output = null) + { + $shouldStop = false; + + $this->outputHandler = $output + ? function ($type, $buffer) use ($output, &$shouldStop) { + $shouldStop = call_user_func($output, $type, $buffer); + } + : $this->outputHandler; + + if (! $this->outputHandler) { + $this->remainingRunIterations = 0; + + return $this->predictProcessResult(); + } + + while ($this->running() && ! $shouldStop) { + // + } + + $this->remainingRunIterations = 0; + + return $this->process->toProcessResult($this->command); + } + /** * Get the ultimate process result that will be returned by this "process". * diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index b3fd5903a699..b9b7d6e0f171 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -765,6 +765,261 @@ public function testFakeInvokedProcessOutputWithLatestOutput() $this->assertEquals("ONE\nTWO\nTHREE\n", $output[2]); } + public function testFakeInvokedProcessWaitUntil() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('WAITING') + ->output('READY') + ->output('DONE') + ->runsFor(iterations: 3); + }); + + $process = $factory->start('echo "WAITING"; sleep 1; echo "READY"; sleep 1; echo "DONE";'); + + $callbackInvoked = []; + + $result = $process->waitUntil(function ($type, $buffer) use (&$callbackInvoked) { + $callbackInvoked[] = $buffer; + + return str_contains($buffer, 'READY'); + }); + + $this->assertInstanceOf(ProcessResult::class, $result); + $this->assertTrue($result->successful()); + $this->assertContains("WAITING\n", $callbackInvoked); + $this->assertContains("READY\n", $callbackInvoked); + } + + public function testFakeInvokedProcessWaitUntilWithNoCallback() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('OUTPUT'); + }); + + $process = $factory->start('echo "OUTPUT"'); + + $result = $process->waitUntil(); + + $this->assertInstanceOf(ProcessResult::class, $result); + $this->assertTrue($result->successful()); + $this->assertEquals("OUTPUT\n", $result->output()); + } + + public function testFakeInvokedProcessWaitUntilWithErrorOutput() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('STDOUT') + ->errorOutput('ERROR1') + ->errorOutput('TARGET_ERROR') + ->output('MORE_STDOUT') + ->runsFor(iterations: 4); + }); + + $process = $factory->start('echo "STDOUT"; echo "ERROR1" >&2; echo "TARGET_ERROR" >&2; echo "MORE_STDOUT";'); + + $callbackInvoked = []; + + $result = $process->waitUntil(function ($type, $buffer) use (&$callbackInvoked) { + $callbackInvoked[] = [$type, $buffer]; + + return str_contains($buffer, 'TARGET_ERROR'); + }); + + $this->assertInstanceOf(ProcessResult::class, $result); + $this->assertTrue($result->successful()); + $this->assertContains(['out', "STDOUT\n"], $callbackInvoked); + $this->assertContains(['err', "ERROR1\n"], $callbackInvoked); + $this->assertContains(['err', "TARGET_ERROR\n"], $callbackInvoked); + } + + public function testFakeInvokedProcessWaitUntilCalledTwice() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('FIRST') + ->output('SECOND') + ->output('THIRD') + ->output('FOURTH') + ->runsFor(iterations: 4); + }); + + $process = $factory->start('echo "FIRST"; echo "SECOND"; echo "THIRD"; echo "FOURTH";'); + + $firstCallbackInvoked = []; + $secondCallbackInvoked = []; + + $firstResult = $process->waitUntil(function ($type, $buffer) use (&$firstCallbackInvoked) { + $firstCallbackInvoked[] = $buffer; + + return str_contains($buffer, 'SECOND'); + }); + + $this->assertInstanceOf(ProcessResult::class, $firstResult); + $this->assertTrue($firstResult->successful()); + $this->assertContains("FIRST\n", $firstCallbackInvoked); + $this->assertContains("SECOND\n", $firstCallbackInvoked); + $this->assertCount(2, $firstCallbackInvoked); + + $secondResult = $process->waitUntil(function ($type, $buffer) use (&$secondCallbackInvoked) { + $secondCallbackInvoked[] = $buffer; + + return str_contains($buffer, 'FOURTH'); + }); + + $this->assertInstanceOf(ProcessResult::class, $secondResult); + $this->assertTrue($secondResult->successful()); + $this->assertContains("THIRD\n", $secondCallbackInvoked); + $this->assertContains("FOURTH\n", $secondCallbackInvoked); + $this->assertCount(2, $secondCallbackInvoked); + } + + public function testFakeInvokedProcessWaitUntilThatNeverMatches() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('LINE1') + ->output('LINE2') + ->output('LINE3') + ->runsFor(iterations: 3); + }); + + $process = $factory->start('echo "LINE1"; echo "LINE2"; echo "LINE3";'); + + $callbackInvoked = []; + + $result = $process->waitUntil(function ($type, $buffer) use (&$callbackInvoked) { + $callbackInvoked[] = $buffer; + + return str_contains($buffer, 'NEVER_MATCHES'); + }); + + $this->assertInstanceOf(ProcessResult::class, $result); + $this->assertTrue($result->successful()); + $this->assertCount(3, $callbackInvoked); + $this->assertContains("LINE1\n", $callbackInvoked); + $this->assertContains("LINE2\n", $callbackInvoked); + $this->assertContains("LINE3\n", $callbackInvoked); + } + + public function testFakeInvokedProcessWaitUntilFollowedByWait() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('FIRST') + ->output('SECOND') + ->output('THIRD') + ->runsFor(iterations: 3); + }); + + $process = $factory->start('echo "FIRST"; echo "SECOND"; echo "THIRD";'); + + $waitUntilCallbacks = []; + $waitCallbacks = []; + + $process->waitUntil(function ($type, $buffer) use (&$waitUntilCallbacks) { + $waitUntilCallbacks[] = $buffer; + + return str_contains($buffer, 'FIRST'); + }); + + $result = $process->wait(function ($type, $buffer) use (&$waitCallbacks) { + $waitCallbacks[] = $buffer; + }); + + $this->assertInstanceOf(ProcessResult::class, $result); + $this->assertTrue($result->successful()); + $this->assertCount(1, $waitUntilCallbacks); + $this->assertEquals("FIRST\n", $waitUntilCallbacks[0]); + $this->assertCount(2, $waitCallbacks); + $this->assertContains("SECOND\n", $waitCallbacks); + $this->assertContains("THIRD\n", $waitCallbacks); + } + + public function testFakeInvokedProcessWaitCalledTwice() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('FIRST') + ->output('SECOND') + ->output('THIRD') + ->runsFor(iterations: 3); + }); + + $process = $factory->start('echo "FIRST"; echo "SECOND"; echo "THIRD";'); + + $firstCallbackInvoked = []; + $secondCallbackInvoked = []; + + $firstResult = $process->wait(function ($type, $buffer) use (&$firstCallbackInvoked) { + $firstCallbackInvoked[] = $buffer; + }); + + $this->assertInstanceOf(ProcessResult::class, $firstResult); + $this->assertTrue($firstResult->successful()); + $this->assertCount(3, $firstCallbackInvoked); + $this->assertContains("FIRST\n", $firstCallbackInvoked); + $this->assertContains("SECOND\n", $firstCallbackInvoked); + $this->assertContains("THIRD\n", $firstCallbackInvoked); + + $secondResult = $process->wait(function ($type, $buffer) use (&$secondCallbackInvoked) { + $secondCallbackInvoked[] = $buffer; + }); + + $this->assertInstanceOf(ProcessResult::class, $secondResult); + $this->assertTrue($secondResult->successful()); + $this->assertEmpty($secondCallbackInvoked); + } + + public function testFakeInvokedProcessWaitFollowedByWaitUntil() + { + $factory = new Factory; + + $factory->fake(function () use ($factory) { + return $factory->describe() + ->output('FIRST') + ->output('SECOND') + ->output('THIRD') + ->runsFor(iterations: 3); + }); + + $process = $factory->start('echo "FIRST"; echo "SECOND"; echo "THIRD";'); + + $waitCallbacks = []; + $waitUntilCallbacks = []; + + $process->wait(function ($type, $buffer) use (&$waitCallbacks) { + $waitCallbacks[] = $buffer; + }); + + $result = $process->waitUntil(function ($type, $buffer) use (&$waitUntilCallbacks) { + $waitUntilCallbacks[] = $buffer; + + return str_contains($buffer, 'THIRD'); + }); + + $this->assertInstanceOf(ProcessResult::class, $result); + $this->assertTrue($result->successful()); + $this->assertCount(3, $waitCallbacks); + $this->assertEmpty($waitUntilCallbacks); + } + public function testBasicFakeAssertions() { $factory = new Factory;