4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Process\Tests;
14 use PHPUnit\Framework\TestCase;
15 use Symfony\Component\Process\Exception\LogicException;
16 use Symfony\Component\Process\Exception\ProcessTimedOutException;
17 use Symfony\Component\Process\Exception\RuntimeException;
18 use Symfony\Component\Process\PhpExecutableFinder;
19 use Symfony\Component\Process\Pipes\PipesInterface;
20 use Symfony\Component\Process\Process;
23 * @author Robert Schönthal <seroscho@googlemail.com>
25 class ProcessTest extends TestCase
27 private static $phpBin;
28 private static $process;
29 private static $sigchild;
30 private static $notEnhancedSigchild = false;
32 public static function setUpBeforeClass()
34 $phpBin = new PhpExecutableFinder();
35 self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === PHP_SAPI ? 'php' : $phpBin->find());
36 if ('\\' !== DIRECTORY_SEPARATOR) {
37 // exec is mandatory to deal with sending a signal to the process
38 // see https://github.com/symfony/symfony/issues/5030 about prepending
40 self::$phpBin = 'exec '.self::$phpBin;
44 phpinfo(INFO_GENERAL);
45 self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
48 protected function tearDown()
51 self::$process->stop(0);
52 self::$process = null;
56 public function testThatProcessDoesNotThrowWarningDuringRun()
58 if ('\\' === DIRECTORY_SEPARATOR) {
59 $this->markTestSkipped('This test is transient on Windows');
61 @trigger_error('Test Error', E_USER_NOTICE);
62 $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'");
64 $actualError = error_get_last();
65 $this->assertEquals('Test Error', $actualError['message']);
66 $this->assertEquals(E_USER_NOTICE, $actualError['type']);
70 * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
72 public function testNegativeTimeoutFromConstructor()
74 $this->getProcess('', null, null, null, -1);
78 * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
80 public function testNegativeTimeoutFromSetter()
82 $p = $this->getProcess('');
86 public function testFloatAndNullTimeout()
88 $p = $this->getProcess('');
91 $this->assertSame(10.0, $p->getTimeout());
94 $this->assertNull($p->getTimeout());
97 $this->assertNull($p->getTimeout());
101 * @requires extension pcntl
103 public function testStopWithTimeoutIsActuallyWorking()
105 $p = $this->getProcess(self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 30');
108 while (false === strpos($p->getOutput(), 'received')) {
111 $start = microtime(true);
116 $this->assertLessThan(15, microtime(true) - $start);
119 public function testAllOutputIsActuallyReadOnTermination()
121 // this code will result in a maximum of 2 reads of 8192 bytes by calling
122 // start() and isRunning(). by the time getOutput() is called the process
123 // has terminated so the internal pipes array is already empty. normally
124 // the call to start() will not read any data as the process will not have
125 // generated output, but this is non-deterministic so we must count it as
126 // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus
127 // another byte which will never be read.
128 $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2;
130 $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
131 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
135 // Don't call Process::run nor Process::wait to avoid any read of pipes
136 $h = new \ReflectionProperty($p, 'process');
137 $h->setAccessible(true);
138 $h = $h->getValue($p);
139 $s = @proc_get_status($h);
141 while (!empty($s['running'])) {
143 $s = proc_get_status($h);
146 $o = $p->getOutput();
148 $this->assertEquals($expectedOutputSize, strlen($o));
151 public function testCallbacksAreExecutedWithStart()
153 $process = $this->getProcess('echo foo');
154 $process->start(function ($type, $buffer) use (&$data) {
160 $this->assertSame('foo'.PHP_EOL, $data);
164 * tests results from sub processes.
166 * @dataProvider responsesCodeProvider
168 public function testProcessResponses($expected, $getter, $code)
170 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
173 $this->assertSame($expected, $p->$getter());
177 * tests results from sub processes.
179 * @dataProvider pipesCodeProvider
181 public function testProcessPipes($code, $size)
183 $expected = str_repeat(str_repeat('*', 1024), $size).'!';
184 $expectedLength = (1024 * $size) + 1;
186 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
187 $p->setInput($expected);
190 $this->assertEquals($expectedLength, strlen($p->getOutput()));
191 $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
195 * @dataProvider pipesCodeProvider
197 public function testSetStreamAsInput($code, $size)
199 $expected = str_repeat(str_repeat('*', 1024), $size).'!';
200 $expectedLength = (1024 * $size) + 1;
202 $stream = fopen('php://temporary', 'w+');
203 fwrite($stream, $expected);
206 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
207 $p->setInput($stream);
212 $this->assertEquals($expectedLength, strlen($p->getOutput()));
213 $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
216 public function testLiveStreamAsInput()
218 $stream = fopen('php://memory', 'r+');
219 fwrite($stream, 'hello');
222 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);')));
223 $p->setInput($stream);
224 $p->start(function ($type, $data) use ($stream) {
225 if ('hello' === $data) {
231 $this->assertSame('hello', $p->getOutput());
235 * @expectedException \Symfony\Component\Process\Exception\LogicException
236 * @expectedExceptionMessage Input can not be set while the process is running.
238 public function testSetInputWhileRunningThrowsAnException()
240 $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
243 $process->setInput('foobar');
245 $this->fail('A LogicException should have been raised.');
246 } catch (LogicException $e) {
254 * @dataProvider provideInvalidInputValues
255 * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
256 * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings or stream resources.
258 public function testInvalidInput($value)
260 $process = $this->getProcess('foo');
261 $process->setInput($value);
264 public function provideInvalidInputValues()
268 array(new NonStringifiable()),
273 * @dataProvider provideInputValues
275 public function testValidInput($expected, $value)
277 $process = $this->getProcess('foo');
278 $process->setInput($value);
279 $this->assertSame($expected, $process->getInput());
282 public function provideInputValues()
287 array('input data', 'input data'),
292 * @dataProvider provideLegacyInputValues
295 public function testLegacyValidInput($expected, $value)
297 $process = $this->getProcess(self::$phpBin.' -v');
298 $process->setInput($value);
299 $this->assertSame($expected, $process->getInput());
302 public function provideLegacyInputValues()
305 array('stringifiable', new Stringifiable()),
309 public function chainedCommandsOutputProvider()
311 if ('\\' === DIRECTORY_SEPARATOR) {
313 array("2 \r\n2\r\n", '&&', '2'),
318 array("1\n1\n", ';', '1'),
319 array("2\n2\n", '&&', '2'),
324 * @dataProvider chainedCommandsOutputProvider
326 public function testChainedCommandsOutput($expected, $operator, $input)
328 $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input));
330 $this->assertEquals($expected, $process->getOutput());
333 public function testCallbackIsExecutedForOutput()
335 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';')));
338 $p->run(function ($type, $buffer) use (&$called) {
339 $called = $buffer === 'foo';
342 $this->assertTrue($called, 'The callback should be executed with the output');
345 public function testGetErrorOutput()
347 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
350 $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
353 public function testFlushErrorOutput()
355 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
358 $p->clearErrorOutput();
359 $this->assertEmpty($p->getErrorOutput());
363 * @dataProvider provideIncrementalOutput
365 public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
367 $lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
369 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');')));
371 $h = fopen($lock, 'w');
376 foreach (array('foo', 'bar') as $s) {
377 while (false === strpos($p->$getOutput(), $s)) {
381 $this->assertSame($s, $p->$getIncrementalOutput());
382 $this->assertSame('', $p->$getIncrementalOutput());
390 public function provideIncrementalOutput()
393 array('getOutput', 'getIncrementalOutput', 'php://stdout'),
394 array('getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'),
398 public function testGetOutput()
400 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }')));
403 $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
406 public function testFlushOutput()
408 $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}')));
412 $this->assertEmpty($p->getOutput());
415 public function testZeroAsOutput()
417 if ('\\' === DIRECTORY_SEPARATOR) {
418 // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
419 $p = $this->getProcess('echo | set /p dummyName=0');
421 $p = $this->getProcess('printf 0');
425 $this->assertSame('0', $p->getOutput());
428 public function testExitCodeCommandFailed()
430 if ('\\' === DIRECTORY_SEPARATOR) {
431 $this->markTestSkipped('Windows does not support POSIX exit code');
433 $this->skipIfNotEnhancedSigchild();
435 // such command run in bash return an exitcode 127
436 $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
439 $this->assertGreaterThan(0, $process->getExitCode());
442 public function testTTYCommand()
444 if ('\\' === DIRECTORY_SEPARATOR) {
445 $this->markTestSkipped('Windows does not have /dev/tty support');
448 $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"');
449 $process->setTty(true);
451 $this->assertTrue($process->isRunning());
454 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
457 public function testTTYCommandExitCode()
459 if ('\\' === DIRECTORY_SEPARATOR) {
460 $this->markTestSkipped('Windows does have /dev/tty support');
462 $this->skipIfNotEnhancedSigchild();
464 $process = $this->getProcess('echo "foo" >> /dev/null');
465 $process->setTty(true);
468 $this->assertTrue($process->isSuccessful());
472 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
473 * @expectedExceptionMessage TTY mode is not supported on Windows platform.
475 public function testTTYInWindowsEnvironment()
477 if ('\\' !== DIRECTORY_SEPARATOR) {
478 $this->markTestSkipped('This test is for Windows platform only');
481 $process = $this->getProcess('echo "foo" >> /dev/null');
482 $process->setTty(false);
483 $process->setTty(true);
486 public function testExitCodeTextIsNullWhenExitCodeIsNull()
488 $this->skipIfNotEnhancedSigchild();
490 $process = $this->getProcess('');
491 $this->assertNull($process->getExitCodeText());
494 public function testPTYCommand()
496 if (!Process::isPtySupported()) {
497 $this->markTestSkipped('PTY is not supported on this operating system.');
500 $process = $this->getProcess('echo "foo"');
501 $process->setPty(true);
504 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
505 $this->assertEquals("foo\r\n", $process->getOutput());
508 public function testMustRun()
510 $this->skipIfNotEnhancedSigchild();
512 $process = $this->getProcess('echo foo');
514 $this->assertSame($process, $process->mustRun());
515 $this->assertEquals('foo'.PHP_EOL, $process->getOutput());
518 public function testSuccessfulMustRunHasCorrectExitCode()
520 $this->skipIfNotEnhancedSigchild();
522 $process = $this->getProcess('echo foo')->mustRun();
523 $this->assertEquals(0, $process->getExitCode());
527 * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException
529 public function testMustRunThrowsException()
531 $this->skipIfNotEnhancedSigchild();
533 $process = $this->getProcess('exit 1');
537 public function testExitCodeText()
539 $this->skipIfNotEnhancedSigchild();
541 $process = $this->getProcess('');
542 $r = new \ReflectionObject($process);
543 $p = $r->getProperty('exitcode');
544 $p->setAccessible(true);
546 $p->setValue($process, 2);
547 $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
550 public function testStartIsNonBlocking()
552 $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"');
553 $start = microtime(true);
555 $end = microtime(true);
556 $this->assertLessThan(0.4, $end - $start);
560 public function testUpdateStatus()
562 $process = $this->getProcess('echo foo');
564 $this->assertTrue(strlen($process->getOutput()) > 0);
567 public function testGetExitCodeIsNullOnStart()
569 $this->skipIfNotEnhancedSigchild();
571 $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
572 $this->assertNull($process->getExitCode());
574 $this->assertNull($process->getExitCode());
576 $this->assertEquals(0, $process->getExitCode());
579 public function testGetExitCodeIsNullOnWhenStartingAgain()
581 $this->skipIfNotEnhancedSigchild();
583 $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
585 $this->assertEquals(0, $process->getExitCode());
587 $this->assertNull($process->getExitCode());
589 $this->assertEquals(0, $process->getExitCode());
592 public function testGetExitCode()
594 $this->skipIfNotEnhancedSigchild();
596 $process = $this->getProcess('echo foo');
598 $this->assertSame(0, $process->getExitCode());
601 public function testStatus()
603 $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
604 $this->assertFalse($process->isRunning());
605 $this->assertFalse($process->isStarted());
606 $this->assertFalse($process->isTerminated());
607 $this->assertSame(Process::STATUS_READY, $process->getStatus());
609 $this->assertTrue($process->isRunning());
610 $this->assertTrue($process->isStarted());
611 $this->assertFalse($process->isTerminated());
612 $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
614 $this->assertFalse($process->isRunning());
615 $this->assertTrue($process->isStarted());
616 $this->assertTrue($process->isTerminated());
617 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
620 public function testStop()
622 $process = $this->getProcess(self::$phpBin.' -r "sleep(31);"');
624 $this->assertTrue($process->isRunning());
626 $this->assertFalse($process->isRunning());
629 public function testIsSuccessful()
631 $this->skipIfNotEnhancedSigchild();
633 $process = $this->getProcess('echo foo');
635 $this->assertTrue($process->isSuccessful());
638 public function testIsSuccessfulOnlyAfterTerminated()
640 $this->skipIfNotEnhancedSigchild();
642 $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
645 $this->assertFalse($process->isSuccessful());
649 $this->assertTrue($process->isSuccessful());
652 public function testIsNotSuccessful()
654 $this->skipIfNotEnhancedSigchild();
656 $process = $this->getProcess(self::$phpBin.' -r "throw new \Exception(\'BOUM\');"');
658 $this->assertFalse($process->isSuccessful());
661 public function testProcessIsNotSignaled()
663 if ('\\' === DIRECTORY_SEPARATOR) {
664 $this->markTestSkipped('Windows does not support POSIX signals');
666 $this->skipIfNotEnhancedSigchild();
668 $process = $this->getProcess('echo foo');
670 $this->assertFalse($process->hasBeenSignaled());
673 public function testProcessWithoutTermSignal()
675 if ('\\' === DIRECTORY_SEPARATOR) {
676 $this->markTestSkipped('Windows does not support POSIX signals');
678 $this->skipIfNotEnhancedSigchild();
680 $process = $this->getProcess('echo foo');
682 $this->assertEquals(0, $process->getTermSignal());
685 public function testProcessIsSignaledIfStopped()
687 if ('\\' === DIRECTORY_SEPARATOR) {
688 $this->markTestSkipped('Windows does not support POSIX signals');
690 $this->skipIfNotEnhancedSigchild();
692 $process = $this->getProcess(self::$phpBin.' -r "sleep(32);"');
695 $this->assertTrue($process->hasBeenSignaled());
696 $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
700 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
701 * @expectedExceptionMessage The process has been signaled
703 public function testProcessThrowsExceptionWhenExternallySignaled()
705 if (!function_exists('posix_kill')) {
706 $this->markTestSkipped('Function posix_kill is required.');
708 $this->skipIfNotEnhancedSigchild(false);
710 $process = $this->getProcess(self::$phpBin.' -r "sleep(32.1)"');
712 posix_kill($process->getPid(), 9); // SIGKILL
717 public function testRestart()
719 $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"');
721 $process2 = $process1->restart();
723 $process2->wait(); // wait for output
725 // Ensure that both processed finished and the output is numeric
726 $this->assertFalse($process1->isRunning());
727 $this->assertFalse($process2->isRunning());
728 $this->assertInternalType('numeric', $process1->getOutput());
729 $this->assertInternalType('numeric', $process2->getOutput());
731 // Ensure that restart returned a new process by check that the output is different
732 $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
736 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
737 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
739 public function testRunProcessWithTimeout()
741 $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
742 $process->setTimeout(0.1);
743 $start = microtime(true);
746 $this->fail('A RuntimeException should have been raised');
747 } catch (RuntimeException $e) {
750 $this->assertLessThan(15, microtime(true) - $start);
755 public function testCheckTimeoutOnNonStartedProcess()
757 $process = $this->getProcess('echo foo');
758 $this->assertNull($process->checkTimeout());
761 public function testCheckTimeoutOnTerminatedProcess()
763 $process = $this->getProcess('echo foo');
765 $this->assertNull($process->checkTimeout());
769 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
770 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
772 public function testCheckTimeoutOnStartedProcess()
774 $process = $this->getProcess(self::$phpBin.' -r "sleep(33);"');
775 $process->setTimeout(0.1);
778 $start = microtime(true);
781 while ($process->isRunning()) {
782 $process->checkTimeout();
785 $this->fail('A ProcessTimedOutException should have been raised');
786 } catch (ProcessTimedOutException $e) {
789 $this->assertLessThan(15, microtime(true) - $start);
794 public function testIdleTimeout()
796 $process = $this->getProcess(self::$phpBin.' -r "sleep(34);"');
797 $process->setTimeout(60);
798 $process->setIdleTimeout(0.1);
803 $this->fail('A timeout exception was expected.');
804 } catch (ProcessTimedOutException $e) {
805 $this->assertTrue($e->isIdleTimeout());
806 $this->assertFalse($e->isGeneralTimeout());
807 $this->assertEquals(0.1, $e->getExceededTimeout());
811 public function testIdleTimeoutNotExceededWhenOutputIsSent()
813 $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('while (true) {echo \'foo \'; usleep(1000);}')));
814 $process->setTimeout(1);
817 while (false === strpos($process->getOutput(), 'foo')) {
821 $process->setIdleTimeout(0.5);
825 $this->fail('A timeout exception was expected.');
826 } catch (ProcessTimedOutException $e) {
827 $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
828 $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
829 $this->assertEquals(1, $e->getExceededTimeout());
834 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
835 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
837 public function testStartAfterATimeout()
839 $process = $this->getProcess(self::$phpBin.' -r "sleep(35);"');
840 $process->setTimeout(0.1);
844 $this->fail('A ProcessTimedOutException should have been raised.');
845 } catch (ProcessTimedOutException $e) {
847 $this->assertFalse($process->isRunning());
849 $this->assertTrue($process->isRunning());
855 public function testGetPid()
857 $process = $this->getProcess(self::$phpBin.' -r "sleep(36);"');
859 $this->assertGreaterThan(0, $process->getPid());
863 public function testGetPidIsNullBeforeStart()
865 $process = $this->getProcess('foo');
866 $this->assertNull($process->getPid());
869 public function testGetPidIsNullAfterRun()
871 $process = $this->getProcess('echo foo');
873 $this->assertNull($process->getPid());
877 * @requires extension pcntl
879 public function testSignal()
881 $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/SignalListener.php');
884 while (false === strpos($process->getOutput(), 'Caught')) {
887 $process->signal(SIGUSR1);
890 $this->assertEquals('Caught SIGUSR1', $process->getOutput());
894 * @requires extension pcntl
896 public function testExitCodeIsAvailableAfterSignal()
898 $this->skipIfNotEnhancedSigchild();
900 $process = $this->getProcess('sleep 4');
902 $process->signal(SIGKILL);
904 while ($process->isRunning()) {
908 $this->assertFalse($process->isRunning());
909 $this->assertTrue($process->hasBeenSignaled());
910 $this->assertFalse($process->isSuccessful());
911 $this->assertEquals(137, $process->getExitCode());
915 * @expectedException \Symfony\Component\Process\Exception\LogicException
916 * @expectedExceptionMessage Can not send signal on a non running process.
918 public function testSignalProcessNotRunning()
920 $process = $this->getProcess('foo');
921 $process->signal(1); // SIGHUP
925 * @dataProvider provideMethodsThatNeedARunningProcess
927 public function testMethodsThatNeedARunningProcess($method)
929 $process = $this->getProcess('foo');
931 if (method_exists($this, 'expectException')) {
932 $this->expectException('Symfony\Component\Process\Exception\LogicException');
933 $this->expectExceptionMessage(sprintf('Process must be started before calling %s.', $method));
935 $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
938 $process->{$method}();
941 public function provideMethodsThatNeedARunningProcess()
945 array('getIncrementalOutput'),
946 array('getErrorOutput'),
947 array('getIncrementalErrorOutput'),
953 * @dataProvider provideMethodsThatNeedATerminatedProcess
954 * @expectedException \Symfony\Component\Process\Exception\LogicException
955 * @expectedExceptionMessage Process must be terminated before calling
957 public function testMethodsThatNeedATerminatedProcess($method)
959 $process = $this->getProcess(self::$phpBin.' -r "sleep(37);"');
962 $process->{$method}();
964 $this->fail('A LogicException must have been thrown');
965 } catch (\Exception $e) {
972 public function provideMethodsThatNeedATerminatedProcess()
975 array('hasBeenSignaled'),
976 array('getTermSignal'),
977 array('hasBeenStopped'),
978 array('getStopSignal'),
983 * @dataProvider provideWrongSignal
984 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
986 public function testWrongSignal($signal)
988 if ('\\' === DIRECTORY_SEPARATOR) {
989 $this->markTestSkipped('POSIX signals do not work on Windows');
992 $process = $this->getProcess(self::$phpBin.' -r "sleep(38);"');
995 $process->signal($signal);
996 $this->fail('A RuntimeException must have been thrown');
997 } catch (RuntimeException $e) {
1004 public function provideWrongSignal()
1008 array('Céphalopodes'),
1012 public function testDisableOutputDisablesTheOutput()
1014 $p = $this->getProcess('foo');
1015 $this->assertFalse($p->isOutputDisabled());
1016 $p->disableOutput();
1017 $this->assertTrue($p->isOutputDisabled());
1019 $this->assertFalse($p->isOutputDisabled());
1023 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1024 * @expectedExceptionMessage Disabling output while the process is running is not possible.
1026 public function testDisableOutputWhileRunningThrowsException()
1028 $p = $this->getProcess(self::$phpBin.' -r "sleep(39);"');
1030 $p->disableOutput();
1034 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1035 * @expectedExceptionMessage Enabling output while the process is running is not possible.
1037 public function testEnableOutputWhileRunningThrowsException()
1039 $p = $this->getProcess(self::$phpBin.' -r "sleep(40);"');
1040 $p->disableOutput();
1045 public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
1047 $p = $this->getProcess('echo foo');
1048 $p->disableOutput();
1051 $p->disableOutput();
1052 $this->assertTrue($p->isOutputDisabled());
1056 * @expectedException \Symfony\Component\Process\Exception\LogicException
1057 * @expectedExceptionMessage Output can not be disabled while an idle timeout is set.
1059 public function testDisableOutputWhileIdleTimeoutIsSet()
1061 $process = $this->getProcess('foo');
1062 $process->setIdleTimeout(1);
1063 $process->disableOutput();
1067 * @expectedException \Symfony\Component\Process\Exception\LogicException
1068 * @expectedExceptionMessage timeout can not be set while the output is disabled.
1070 public function testSetIdleTimeoutWhileOutputIsDisabled()
1072 $process = $this->getProcess('foo');
1073 $process->disableOutput();
1074 $process->setIdleTimeout(1);
1077 public function testSetNullIdleTimeoutWhileOutputIsDisabled()
1079 $process = $this->getProcess('foo');
1080 $process->disableOutput();
1081 $this->assertSame($process, $process->setIdleTimeout(null));
1085 * @dataProvider provideStartMethods
1087 public function testStartWithACallbackAndDisabledOutput($startMethod, $exception, $exceptionMessage)
1089 $p = $this->getProcess('foo');
1090 $p->disableOutput();
1092 if (method_exists($this, 'expectException')) {
1093 $this->expectException($exception);
1094 $this->expectExceptionMessage($exceptionMessage);
1096 $this->setExpectedException($exception, $exceptionMessage);
1099 if ('mustRun' === $startMethod) {
1100 $this->skipIfNotEnhancedSigchild();
1102 $p->{$startMethod}(function () {});
1105 public function provideStartMethods()
1108 array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
1109 array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
1110 array('mustRun', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
1115 * @dataProvider provideOutputFetchingMethods
1116 * @expectedException \Symfony\Component\Process\Exception\LogicException
1117 * @expectedExceptionMessage Output has been disabled.
1119 public function testGetOutputWhileDisabled($fetchMethod)
1121 $p = $this->getProcess(self::$phpBin.' -r "sleep(41);"');
1122 $p->disableOutput();
1124 $p->{$fetchMethod}();
1127 public function provideOutputFetchingMethods()
1131 array('getIncrementalOutput'),
1132 array('getErrorOutput'),
1133 array('getIncrementalErrorOutput'),
1137 public function testStopTerminatesProcessCleanly()
1139 $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(42);"');
1140 $process->run(function () use ($process) {
1143 $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
1146 public function testKillSignalTerminatesProcessCleanly()
1148 $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(43);"');
1149 $process->run(function () use ($process) {
1150 $process->signal(9); // SIGKILL
1152 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1155 public function testTermSignalTerminatesProcessCleanly()
1157 $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(44);"');
1158 $process->run(function () use ($process) {
1159 $process->signal(15); // SIGTERM
1161 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1164 public function responsesCodeProvider()
1167 //expected output / getter / code to execute
1168 //array(1,'getExitCode','exit(1);'),
1169 //array(true,'isSuccessful','exit();'),
1170 array('output', 'getOutput', 'echo \'output\';'),
1174 public function pipesCodeProvider()
1176 $variations = array(
1177 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
1178 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
1181 if ('\\' === DIRECTORY_SEPARATOR) {
1182 // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650
1183 $sizes = array(1, 2, 4, 8);
1185 $sizes = array(1, 16, 64, 1024, 4096);
1189 foreach ($sizes as $size) {
1190 foreach ($variations as $code) {
1191 $codes[] = array($code, $size);
1199 * @dataProvider provideVariousIncrementals
1201 public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
1203 $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }'), null, null, null, null);
1206 $limit = microtime(true) + 3;
1209 while ($result !== $expected && microtime(true) < $limit) {
1210 $result .= $process->$method();
1213 $this->assertSame($expected, $result);
1217 public function provideVariousIncrementals()
1220 array('php://stdout', 'getIncrementalOutput'),
1221 array('php://stderr', 'getIncrementalErrorOutput'),
1226 * @param string $commandline
1227 * @param null|string $cwd
1228 * @param null|array $env
1229 * @param null|string $input
1230 * @param int $timeout
1231 * @param array $options
1235 private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array())
1237 $process = new Process($commandline, $cwd, $env, $input, $timeout, $options);
1239 if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
1241 $process->setEnhanceSigchildCompatibility(false);
1242 $process->getExitCode();
1243 $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
1244 } catch (RuntimeException $e) {
1245 $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
1247 $process->setEnhanceSigchildCompatibility(true);
1249 self::$notEnhancedSigchild = true;
1254 if (self::$process) {
1255 self::$process->stop(0);
1258 return self::$process = $process;
1261 private function skipIfNotEnhancedSigchild($expectException = true)
1263 if (self::$sigchild) {
1264 if (!$expectException) {
1265 $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
1266 } elseif (self::$notEnhancedSigchild) {
1267 if (method_exists($this, 'expectException')) {
1268 $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1269 $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.');
1271 $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild.');
1280 public function __toString()
1282 return 'stringifiable';
1286 class NonStringifiable