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\InputStream;
19 use Symfony\Component\Process\PhpExecutableFinder;
20 use Symfony\Component\Process\Pipes\PipesInterface;
21 use Symfony\Component\Process\Process;
24 * @author Robert Schönthal <seroscho@googlemail.com>
26 class ProcessTest extends TestCase
28 private static $phpBin;
29 private static $process;
30 private static $sigchild;
31 private static $notEnhancedSigchild = false;
33 public static function setUpBeforeClass()
35 $phpBin = new PhpExecutableFinder();
36 self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find());
39 phpinfo(INFO_GENERAL);
40 self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
43 protected function tearDown()
46 self::$process->stop(0);
47 self::$process = null;
53 * @expectedDeprecation The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.
55 public function testInvalidCwd()
57 if ('\\' === \DIRECTORY_SEPARATOR) {
58 $this->markTestSkipped('False-positive on Windows/appveyor.');
61 // Check that it works fine if the CWD exists
62 $cmd = new Process('echo test', __DIR__);
65 $cmd = new Process('echo test', __DIR__.'/notfound/');
69 public function testThatProcessDoesNotThrowWarningDuringRun()
71 if ('\\' === \DIRECTORY_SEPARATOR) {
72 $this->markTestSkipped('This test is transient on Windows');
74 @trigger_error('Test Error', E_USER_NOTICE);
75 $process = $this->getProcessForCode('sleep(3)');
77 $actualError = error_get_last();
78 $this->assertEquals('Test Error', $actualError['message']);
79 $this->assertEquals(E_USER_NOTICE, $actualError['type']);
83 * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
85 public function testNegativeTimeoutFromConstructor()
87 $this->getProcess('', null, null, null, -1);
91 * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
93 public function testNegativeTimeoutFromSetter()
95 $p = $this->getProcess('');
99 public function testFloatAndNullTimeout()
101 $p = $this->getProcess('');
104 $this->assertSame(10.0, $p->getTimeout());
106 $p->setTimeout(null);
107 $this->assertNull($p->getTimeout());
110 $this->assertNull($p->getTimeout());
114 * @requires extension pcntl
116 public function testStopWithTimeoutIsActuallyWorking()
118 $p = $this->getProcess(array(self::$phpBin, __DIR__.'/NonStopableProcess.php', 30));
121 while (false === strpos($p->getOutput(), 'received')) {
124 $start = microtime(true);
129 $this->assertLessThan(15, microtime(true) - $start);
132 public function testAllOutputIsActuallyReadOnTermination()
134 // this code will result in a maximum of 2 reads of 8192 bytes by calling
135 // start() and isRunning(). by the time getOutput() is called the process
136 // has terminated so the internal pipes array is already empty. normally
137 // the call to start() will not read any data as the process will not have
138 // generated output, but this is non-deterministic so we must count it as
139 // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus
140 // another byte which will never be read.
141 $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2;
143 $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
144 $p = $this->getProcessForCode($code);
148 // Don't call Process::run nor Process::wait to avoid any read of pipes
149 $h = new \ReflectionProperty($p, 'process');
150 $h->setAccessible(true);
151 $h = $h->getValue($p);
152 $s = @proc_get_status($h);
154 while (!empty($s['running'])) {
156 $s = proc_get_status($h);
159 $o = $p->getOutput();
161 $this->assertEquals($expectedOutputSize, \strlen($o));
164 public function testCallbacksAreExecutedWithStart()
166 $process = $this->getProcess('echo foo');
167 $process->start(function ($type, $buffer) use (&$data) {
173 $this->assertSame('foo'.PHP_EOL, $data);
177 * tests results from sub processes.
179 * @dataProvider responsesCodeProvider
181 public function testProcessResponses($expected, $getter, $code)
183 $p = $this->getProcessForCode($code);
186 $this->assertSame($expected, $p->$getter());
190 * tests results from sub processes.
192 * @dataProvider pipesCodeProvider
194 public function testProcessPipes($code, $size)
196 $expected = str_repeat(str_repeat('*', 1024), $size).'!';
197 $expectedLength = (1024 * $size) + 1;
199 $p = $this->getProcessForCode($code);
200 $p->setInput($expected);
203 $this->assertEquals($expectedLength, \strlen($p->getOutput()));
204 $this->assertEquals($expectedLength, \strlen($p->getErrorOutput()));
208 * @dataProvider pipesCodeProvider
210 public function testSetStreamAsInput($code, $size)
212 $expected = str_repeat(str_repeat('*', 1024), $size).'!';
213 $expectedLength = (1024 * $size) + 1;
215 $stream = fopen('php://temporary', 'w+');
216 fwrite($stream, $expected);
219 $p = $this->getProcessForCode($code);
220 $p->setInput($stream);
225 $this->assertEquals($expectedLength, \strlen($p->getOutput()));
226 $this->assertEquals($expectedLength, \strlen($p->getErrorOutput()));
229 public function testLiveStreamAsInput()
231 $stream = fopen('php://memory', 'r+');
232 fwrite($stream, 'hello');
235 $p = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
236 $p->setInput($stream);
237 $p->start(function ($type, $data) use ($stream) {
238 if ('hello' === $data) {
244 $this->assertSame('hello', $p->getOutput());
248 * @expectedException \Symfony\Component\Process\Exception\LogicException
249 * @expectedExceptionMessage Input can not be set while the process is running.
251 public function testSetInputWhileRunningThrowsAnException()
253 $process = $this->getProcessForCode('sleep(30);');
256 $process->setInput('foobar');
258 $this->fail('A LogicException should have been raised.');
259 } catch (LogicException $e) {
267 * @dataProvider provideInvalidInputValues
268 * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
269 * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings, Traversable objects or stream resources.
271 public function testInvalidInput($value)
273 $process = $this->getProcess('foo');
274 $process->setInput($value);
277 public function provideInvalidInputValues()
281 array(new NonStringifiable()),
286 * @dataProvider provideInputValues
288 public function testValidInput($expected, $value)
290 $process = $this->getProcess('foo');
291 $process->setInput($value);
292 $this->assertSame($expected, $process->getInput());
295 public function provideInputValues()
300 array('input data', 'input data'),
304 public function chainedCommandsOutputProvider()
306 if ('\\' === \DIRECTORY_SEPARATOR) {
308 array("2 \r\n2\r\n", '&&', '2'),
313 array("1\n1\n", ';', '1'),
314 array("2\n2\n", '&&', '2'),
319 * @dataProvider chainedCommandsOutputProvider
321 public function testChainedCommandsOutput($expected, $operator, $input)
323 $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input));
325 $this->assertEquals($expected, $process->getOutput());
328 public function testCallbackIsExecutedForOutput()
330 $p = $this->getProcessForCode('echo \'foo\';');
333 $p->run(function ($type, $buffer) use (&$called) {
334 $called = 'foo' === $buffer;
337 $this->assertTrue($called, 'The callback should be executed with the output');
340 public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled()
342 $p = $this->getProcessForCode('echo \'foo\';');
346 $p->run(function ($type, $buffer) use (&$called) {
347 $called = 'foo' === $buffer;
350 $this->assertTrue($called, 'The callback should be executed with the output');
353 public function testGetErrorOutput()
355 $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }');
358 $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
361 public function testFlushErrorOutput()
363 $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }');
366 $p->clearErrorOutput();
367 $this->assertEmpty($p->getErrorOutput());
371 * @dataProvider provideIncrementalOutput
373 public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
375 $lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
377 $p = $this->getProcessForCode('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');');
379 $h = fopen($lock, 'w');
384 foreach (array('foo', 'bar') as $s) {
385 while (false === strpos($p->$getOutput(), $s)) {
389 $this->assertSame($s, $p->$getIncrementalOutput());
390 $this->assertSame('', $p->$getIncrementalOutput());
398 public function provideIncrementalOutput()
401 array('getOutput', 'getIncrementalOutput', 'php://stdout'),
402 array('getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'),
406 public function testGetOutput()
408 $p = $this->getProcessForCode('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }');
411 $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
414 public function testFlushOutput()
416 $p = $this->getProcessForCode('$n=0;while ($n<3) {echo \' foo \';$n++;}');
420 $this->assertEmpty($p->getOutput());
423 public function testZeroAsOutput()
425 if ('\\' === \DIRECTORY_SEPARATOR) {
426 // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
427 $p = $this->getProcess('echo | set /p dummyName=0');
429 $p = $this->getProcess('printf 0');
433 $this->assertSame('0', $p->getOutput());
436 public function testExitCodeCommandFailed()
438 if ('\\' === \DIRECTORY_SEPARATOR) {
439 $this->markTestSkipped('Windows does not support POSIX exit code');
441 $this->skipIfNotEnhancedSigchild();
443 // such command run in bash return an exitcode 127
444 $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
447 $this->assertGreaterThan(0, $process->getExitCode());
450 public function testTTYCommand()
452 if ('\\' === \DIRECTORY_SEPARATOR) {
453 $this->markTestSkipped('Windows does not have /dev/tty support');
456 $process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine());
457 $process->setTty(true);
459 $this->assertTrue($process->isRunning());
462 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
465 public function testTTYCommandExitCode()
467 if ('\\' === \DIRECTORY_SEPARATOR) {
468 $this->markTestSkipped('Windows does have /dev/tty support');
470 $this->skipIfNotEnhancedSigchild();
472 $process = $this->getProcess('echo "foo" >> /dev/null');
473 $process->setTty(true);
476 $this->assertTrue($process->isSuccessful());
480 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
481 * @expectedExceptionMessage TTY mode is not supported on Windows platform.
483 public function testTTYInWindowsEnvironment()
485 if ('\\' !== \DIRECTORY_SEPARATOR) {
486 $this->markTestSkipped('This test is for Windows platform only');
489 $process = $this->getProcess('echo "foo" >> /dev/null');
490 $process->setTty(false);
491 $process->setTty(true);
494 public function testExitCodeTextIsNullWhenExitCodeIsNull()
496 $this->skipIfNotEnhancedSigchild();
498 $process = $this->getProcess('');
499 $this->assertNull($process->getExitCodeText());
502 public function testPTYCommand()
504 if (!Process::isPtySupported()) {
505 $this->markTestSkipped('PTY is not supported on this operating system.');
508 $process = $this->getProcess('echo "foo"');
509 $process->setPty(true);
512 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
513 $this->assertEquals("foo\r\n", $process->getOutput());
516 public function testMustRun()
518 $this->skipIfNotEnhancedSigchild();
520 $process = $this->getProcess('echo foo');
522 $this->assertSame($process, $process->mustRun());
523 $this->assertEquals('foo'.PHP_EOL, $process->getOutput());
526 public function testSuccessfulMustRunHasCorrectExitCode()
528 $this->skipIfNotEnhancedSigchild();
530 $process = $this->getProcess('echo foo')->mustRun();
531 $this->assertEquals(0, $process->getExitCode());
535 * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException
537 public function testMustRunThrowsException()
539 $this->skipIfNotEnhancedSigchild();
541 $process = $this->getProcess('exit 1');
545 public function testExitCodeText()
547 $this->skipIfNotEnhancedSigchild();
549 $process = $this->getProcess('');
550 $r = new \ReflectionObject($process);
551 $p = $r->getProperty('exitcode');
552 $p->setAccessible(true);
554 $p->setValue($process, 2);
555 $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
558 public function testStartIsNonBlocking()
560 $process = $this->getProcessForCode('usleep(500000);');
561 $start = microtime(true);
563 $end = microtime(true);
564 $this->assertLessThan(0.4, $end - $start);
568 public function testUpdateStatus()
570 $process = $this->getProcess('echo foo');
572 $this->assertGreaterThan(0, \strlen($process->getOutput()));
575 public function testGetExitCodeIsNullOnStart()
577 $this->skipIfNotEnhancedSigchild();
579 $process = $this->getProcessForCode('usleep(100000);');
580 $this->assertNull($process->getExitCode());
582 $this->assertNull($process->getExitCode());
584 $this->assertEquals(0, $process->getExitCode());
587 public function testGetExitCodeIsNullOnWhenStartingAgain()
589 $this->skipIfNotEnhancedSigchild();
591 $process = $this->getProcessForCode('usleep(100000);');
593 $this->assertEquals(0, $process->getExitCode());
595 $this->assertNull($process->getExitCode());
597 $this->assertEquals(0, $process->getExitCode());
600 public function testGetExitCode()
602 $this->skipIfNotEnhancedSigchild();
604 $process = $this->getProcess('echo foo');
606 $this->assertSame(0, $process->getExitCode());
609 public function testStatus()
611 $process = $this->getProcessForCode('usleep(100000);');
612 $this->assertFalse($process->isRunning());
613 $this->assertFalse($process->isStarted());
614 $this->assertFalse($process->isTerminated());
615 $this->assertSame(Process::STATUS_READY, $process->getStatus());
617 $this->assertTrue($process->isRunning());
618 $this->assertTrue($process->isStarted());
619 $this->assertFalse($process->isTerminated());
620 $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
622 $this->assertFalse($process->isRunning());
623 $this->assertTrue($process->isStarted());
624 $this->assertTrue($process->isTerminated());
625 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
628 public function testStop()
630 $process = $this->getProcessForCode('sleep(31);');
632 $this->assertTrue($process->isRunning());
634 $this->assertFalse($process->isRunning());
637 public function testIsSuccessful()
639 $this->skipIfNotEnhancedSigchild();
641 $process = $this->getProcess('echo foo');
643 $this->assertTrue($process->isSuccessful());
646 public function testIsSuccessfulOnlyAfterTerminated()
648 $this->skipIfNotEnhancedSigchild();
650 $process = $this->getProcessForCode('usleep(100000);');
653 $this->assertFalse($process->isSuccessful());
657 $this->assertTrue($process->isSuccessful());
660 public function testIsNotSuccessful()
662 $this->skipIfNotEnhancedSigchild();
664 $process = $this->getProcessForCode('throw new \Exception(\'BOUM\');');
666 $this->assertFalse($process->isSuccessful());
669 public function testProcessIsNotSignaled()
671 if ('\\' === \DIRECTORY_SEPARATOR) {
672 $this->markTestSkipped('Windows does not support POSIX signals');
674 $this->skipIfNotEnhancedSigchild();
676 $process = $this->getProcess('echo foo');
678 $this->assertFalse($process->hasBeenSignaled());
681 public function testProcessWithoutTermSignal()
683 if ('\\' === \DIRECTORY_SEPARATOR) {
684 $this->markTestSkipped('Windows does not support POSIX signals');
686 $this->skipIfNotEnhancedSigchild();
688 $process = $this->getProcess('echo foo');
690 $this->assertEquals(0, $process->getTermSignal());
693 public function testProcessIsSignaledIfStopped()
695 if ('\\' === \DIRECTORY_SEPARATOR) {
696 $this->markTestSkipped('Windows does not support POSIX signals');
698 $this->skipIfNotEnhancedSigchild();
700 $process = $this->getProcessForCode('sleep(32);');
703 $this->assertTrue($process->hasBeenSignaled());
704 $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
708 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
709 * @expectedExceptionMessage The process has been signaled
711 public function testProcessThrowsExceptionWhenExternallySignaled()
713 if (!\function_exists('posix_kill')) {
714 $this->markTestSkipped('Function posix_kill is required.');
716 $this->skipIfNotEnhancedSigchild(false);
718 $process = $this->getProcessForCode('sleep(32.1);');
720 posix_kill($process->getPid(), 9); // SIGKILL
725 public function testRestart()
727 $process1 = $this->getProcessForCode('echo getmypid();');
729 $process2 = $process1->restart();
731 $process2->wait(); // wait for output
733 // Ensure that both processed finished and the output is numeric
734 $this->assertFalse($process1->isRunning());
735 $this->assertFalse($process2->isRunning());
736 $this->assertInternalType('numeric', $process1->getOutput());
737 $this->assertInternalType('numeric', $process2->getOutput());
739 // Ensure that restart returned a new process by check that the output is different
740 $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
744 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
745 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
747 public function testRunProcessWithTimeout()
749 $process = $this->getProcessForCode('sleep(30);');
750 $process->setTimeout(0.1);
751 $start = microtime(true);
754 $this->fail('A RuntimeException should have been raised');
755 } catch (RuntimeException $e) {
758 $this->assertLessThan(15, microtime(true) - $start);
764 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
765 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
767 public function testIterateOverProcessWithTimeout()
769 $process = $this->getProcessForCode('sleep(30);');
770 $process->setTimeout(0.1);
771 $start = microtime(true);
774 foreach ($process as $buffer);
775 $this->fail('A RuntimeException should have been raised');
776 } catch (RuntimeException $e) {
779 $this->assertLessThan(15, microtime(true) - $start);
784 public function testCheckTimeoutOnNonStartedProcess()
786 $process = $this->getProcess('echo foo');
787 $this->assertNull($process->checkTimeout());
790 public function testCheckTimeoutOnTerminatedProcess()
792 $process = $this->getProcess('echo foo');
794 $this->assertNull($process->checkTimeout());
798 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
799 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
801 public function testCheckTimeoutOnStartedProcess()
803 $process = $this->getProcessForCode('sleep(33);');
804 $process->setTimeout(0.1);
807 $start = microtime(true);
810 while ($process->isRunning()) {
811 $process->checkTimeout();
814 $this->fail('A ProcessTimedOutException should have been raised');
815 } catch (ProcessTimedOutException $e) {
818 $this->assertLessThan(15, microtime(true) - $start);
823 public function testIdleTimeout()
825 $process = $this->getProcessForCode('sleep(34);');
826 $process->setTimeout(60);
827 $process->setIdleTimeout(0.1);
832 $this->fail('A timeout exception was expected.');
833 } catch (ProcessTimedOutException $e) {
834 $this->assertTrue($e->isIdleTimeout());
835 $this->assertFalse($e->isGeneralTimeout());
836 $this->assertEquals(0.1, $e->getExceededTimeout());
840 public function testIdleTimeoutNotExceededWhenOutputIsSent()
842 $process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}');
843 $process->setTimeout(1);
846 while (false === strpos($process->getOutput(), 'foo')) {
850 $process->setIdleTimeout(0.5);
854 $this->fail('A timeout exception was expected.');
855 } catch (ProcessTimedOutException $e) {
856 $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
857 $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
858 $this->assertEquals(1, $e->getExceededTimeout());
863 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
864 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
866 public function testStartAfterATimeout()
868 $process = $this->getProcessForCode('sleep(35);');
869 $process->setTimeout(0.1);
873 $this->fail('A ProcessTimedOutException should have been raised.');
874 } catch (ProcessTimedOutException $e) {
876 $this->assertFalse($process->isRunning());
878 $this->assertTrue($process->isRunning());
884 public function testGetPid()
886 $process = $this->getProcessForCode('sleep(36);');
888 $this->assertGreaterThan(0, $process->getPid());
892 public function testGetPidIsNullBeforeStart()
894 $process = $this->getProcess('foo');
895 $this->assertNull($process->getPid());
898 public function testGetPidIsNullAfterRun()
900 $process = $this->getProcess('echo foo');
902 $this->assertNull($process->getPid());
906 * @requires extension pcntl
908 public function testSignal()
910 $process = $this->getProcess(array(self::$phpBin, __DIR__.'/SignalListener.php'));
913 while (false === strpos($process->getOutput(), 'Caught')) {
916 $process->signal(SIGUSR1);
919 $this->assertEquals('Caught SIGUSR1', $process->getOutput());
923 * @requires extension pcntl
925 public function testExitCodeIsAvailableAfterSignal()
927 $this->skipIfNotEnhancedSigchild();
929 $process = $this->getProcess('sleep 4');
931 $process->signal(SIGKILL);
933 while ($process->isRunning()) {
937 $this->assertFalse($process->isRunning());
938 $this->assertTrue($process->hasBeenSignaled());
939 $this->assertFalse($process->isSuccessful());
940 $this->assertEquals(137, $process->getExitCode());
944 * @expectedException \Symfony\Component\Process\Exception\LogicException
945 * @expectedExceptionMessage Can not send signal on a non running process.
947 public function testSignalProcessNotRunning()
949 $process = $this->getProcess('foo');
950 $process->signal(1); // SIGHUP
954 * @dataProvider provideMethodsThatNeedARunningProcess
956 public function testMethodsThatNeedARunningProcess($method)
958 $process = $this->getProcess('foo');
960 if (method_exists($this, 'expectException')) {
961 $this->expectException('Symfony\Component\Process\Exception\LogicException');
962 $this->expectExceptionMessage(sprintf('Process must be started before calling %s.', $method));
964 $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
967 $process->{$method}();
970 public function provideMethodsThatNeedARunningProcess()
974 array('getIncrementalOutput'),
975 array('getErrorOutput'),
976 array('getIncrementalErrorOutput'),
982 * @dataProvider provideMethodsThatNeedATerminatedProcess
983 * @expectedException \Symfony\Component\Process\Exception\LogicException
984 * @expectedExceptionMessage Process must be terminated before calling
986 public function testMethodsThatNeedATerminatedProcess($method)
988 $process = $this->getProcessForCode('sleep(37);');
991 $process->{$method}();
993 $this->fail('A LogicException must have been thrown');
994 } catch (\Exception $e) {
1001 public function provideMethodsThatNeedATerminatedProcess()
1004 array('hasBeenSignaled'),
1005 array('getTermSignal'),
1006 array('hasBeenStopped'),
1007 array('getStopSignal'),
1012 * @dataProvider provideWrongSignal
1013 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1015 public function testWrongSignal($signal)
1017 if ('\\' === \DIRECTORY_SEPARATOR) {
1018 $this->markTestSkipped('POSIX signals do not work on Windows');
1021 $process = $this->getProcessForCode('sleep(38);');
1024 $process->signal($signal);
1025 $this->fail('A RuntimeException must have been thrown');
1026 } catch (RuntimeException $e) {
1033 public function provideWrongSignal()
1037 array('Céphalopodes'),
1041 public function testDisableOutputDisablesTheOutput()
1043 $p = $this->getProcess('foo');
1044 $this->assertFalse($p->isOutputDisabled());
1045 $p->disableOutput();
1046 $this->assertTrue($p->isOutputDisabled());
1048 $this->assertFalse($p->isOutputDisabled());
1052 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1053 * @expectedExceptionMessage Disabling output while the process is running is not possible.
1055 public function testDisableOutputWhileRunningThrowsException()
1057 $p = $this->getProcessForCode('sleep(39);');
1059 $p->disableOutput();
1063 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1064 * @expectedExceptionMessage Enabling output while the process is running is not possible.
1066 public function testEnableOutputWhileRunningThrowsException()
1068 $p = $this->getProcessForCode('sleep(40);');
1069 $p->disableOutput();
1074 public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
1076 $p = $this->getProcess('echo foo');
1077 $p->disableOutput();
1080 $p->disableOutput();
1081 $this->assertTrue($p->isOutputDisabled());
1085 * @expectedException \Symfony\Component\Process\Exception\LogicException
1086 * @expectedExceptionMessage Output can not be disabled while an idle timeout is set.
1088 public function testDisableOutputWhileIdleTimeoutIsSet()
1090 $process = $this->getProcess('foo');
1091 $process->setIdleTimeout(1);
1092 $process->disableOutput();
1096 * @expectedException \Symfony\Component\Process\Exception\LogicException
1097 * @expectedExceptionMessage timeout can not be set while the output is disabled.
1099 public function testSetIdleTimeoutWhileOutputIsDisabled()
1101 $process = $this->getProcess('foo');
1102 $process->disableOutput();
1103 $process->setIdleTimeout(1);
1106 public function testSetNullIdleTimeoutWhileOutputIsDisabled()
1108 $process = $this->getProcess('foo');
1109 $process->disableOutput();
1110 $this->assertSame($process, $process->setIdleTimeout(null));
1114 * @dataProvider provideOutputFetchingMethods
1115 * @expectedException \Symfony\Component\Process\Exception\LogicException
1116 * @expectedExceptionMessage Output has been disabled.
1118 public function testGetOutputWhileDisabled($fetchMethod)
1120 $p = $this->getProcessForCode('sleep(41);');
1121 $p->disableOutput();
1123 $p->{$fetchMethod}();
1126 public function provideOutputFetchingMethods()
1130 array('getIncrementalOutput'),
1131 array('getErrorOutput'),
1132 array('getIncrementalErrorOutput'),
1136 public function testStopTerminatesProcessCleanly()
1138 $process = $this->getProcessForCode('echo 123; sleep(42);');
1139 $process->run(function () use ($process) {
1142 $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
1145 public function testKillSignalTerminatesProcessCleanly()
1147 $process = $this->getProcessForCode('echo 123; sleep(43);');
1148 $process->run(function () use ($process) {
1149 $process->signal(9); // SIGKILL
1151 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1154 public function testTermSignalTerminatesProcessCleanly()
1156 $process = $this->getProcessForCode('echo 123; sleep(44);');
1157 $process->run(function () use ($process) {
1158 $process->signal(15); // SIGTERM
1160 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1163 public function responsesCodeProvider()
1166 //expected output / getter / code to execute
1167 //array(1,'getExitCode','exit(1);'),
1168 //array(true,'isSuccessful','exit();'),
1169 array('output', 'getOutput', 'echo \'output\';'),
1173 public function pipesCodeProvider()
1175 $variations = array(
1176 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
1177 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
1180 if ('\\' === \DIRECTORY_SEPARATOR) {
1181 // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650
1182 $sizes = array(1, 2, 4, 8);
1184 $sizes = array(1, 16, 64, 1024, 4096);
1188 foreach ($sizes as $size) {
1189 foreach ($variations as $code) {
1190 $codes[] = array($code, $size);
1198 * @dataProvider provideVariousIncrementals
1200 public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
1202 $process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null);
1205 $limit = microtime(true) + 3;
1208 while ($result !== $expected && microtime(true) < $limit) {
1209 $result .= $process->$method();
1212 $this->assertSame($expected, $result);
1216 public function provideVariousIncrementals()
1219 array('php://stdout', 'getIncrementalOutput'),
1220 array('php://stderr', 'getIncrementalErrorOutput'),
1224 public function testIteratorInput()
1226 $input = function () {
1231 $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input());
1233 $this->assertSame('pingpong', $process->getOutput());
1236 public function testSimpleInputStream()
1238 $input = new InputStream();
1240 $process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);');
1241 $process->setInput($input);
1243 $process->start(function ($type, $data) use ($input) {
1244 if ('ping' === $data) {
1245 $input->write('pang');
1246 } elseif (!$input->isClosed()) {
1247 $input->write('pong');
1253 $this->assertSame('pingpangpong', $process->getOutput());
1256 public function testInputStreamWithCallable()
1259 $stream = fopen('php://memory', 'w+');
1260 $stream = function () use ($stream, &$i) {
1263 fwrite($stream, ++$i);
1270 $input = new InputStream();
1271 $input->onEmpty($stream);
1272 $input->write($stream());
1274 $process = $this->getProcessForCode('echo fread(STDIN, 3);');
1275 $process->setInput($input);
1276 $process->start(function ($type, $data) use ($input) {
1281 $this->assertSame('123', $process->getOutput());
1284 public function testInputStreamWithGenerator()
1286 $input = new InputStream();
1287 $input->onEmpty(function ($input) {
1292 $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
1293 $process->setInput($input);
1295 $input->write('ping');
1297 $this->assertSame('pingpong', $process->getOutput());
1300 public function testInputStreamOnEmpty()
1303 $input = new InputStream();
1304 $input->onEmpty(function () use (&$i) { ++$i; });
1306 $process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;');
1307 $process->setInput($input);
1308 $process->start(function ($type, $data) use ($input) {
1309 if ('123' === $data) {
1315 $this->assertSame(0, $i, 'InputStream->onEmpty callback should be called only when the input *becomes* empty');
1316 $this->assertSame('123456', $process->getOutput());
1319 public function testIteratorOutput()
1321 $input = new InputStream();
1323 $process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);');
1324 $process->setInput($input);
1328 foreach ($process as $type => $data) {
1329 $output[] = array($type, $data);
1332 $expectedOutput = array(
1333 array($process::OUT, '123'),
1335 $this->assertSame($expectedOutput, $output);
1339 foreach ($process as $type => $data) {
1340 $output[] = array($type, $data);
1343 $this->assertSame('', $process->getOutput());
1344 $this->assertFalse($process->isRunning());
1346 $expectedOutput = array(
1347 array($process::OUT, '123'),
1348 array($process::ERR, '234'),
1349 array($process::OUT, '345'),
1350 array($process::ERR, '456'),
1352 $this->assertSame($expectedOutput, $output);
1355 public function testNonBlockingNorClearingIteratorOutput()
1357 $input = new InputStream();
1359 $process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));');
1360 $process->setInput($input);
1364 foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
1365 $output[] = array($type, $data);
1368 $expectedOutput = array(
1369 array($process::OUT, ''),
1371 $this->assertSame($expectedOutput, $output);
1375 foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
1377 $output[] = array($type, $data);
1381 $this->assertSame('123', $process->getOutput());
1382 $this->assertFalse($process->isRunning());
1384 $expectedOutput = array(
1385 array($process::OUT, ''),
1386 array($process::OUT, '123'),
1388 $this->assertSame($expectedOutput, $output);
1391 public function testChainedProcesses()
1393 $p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);');
1394 $p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
1400 $this->assertSame('123', $p1->getErrorOutput());
1401 $this->assertSame('', $p1->getOutput());
1402 $this->assertSame('', $p2->getErrorOutput());
1403 $this->assertSame('456', $p2->getOutput());
1406 public function testSetBadEnv()
1408 $process = $this->getProcess('echo hello');
1409 $process->setEnv(array('bad%%' => '123'));
1410 $process->inheritEnvironmentVariables(true);
1414 $this->assertSame('hello'.PHP_EOL, $process->getOutput());
1415 $this->assertSame('', $process->getErrorOutput());
1418 public function testEnvBackupDoesNotDeleteExistingVars()
1420 putenv('existing_var=foo');
1421 $_ENV['existing_var'] = 'foo';
1422 $process = $this->getProcess('php -r "echo getenv(\'new_test_var\');"');
1423 $process->setEnv(array('existing_var' => 'bar', 'new_test_var' => 'foo'));
1424 $process->inheritEnvironmentVariables();
1428 $this->assertSame('foo', $process->getOutput());
1429 $this->assertSame('foo', getenv('existing_var'));
1430 $this->assertFalse(getenv('new_test_var'));
1432 putenv('existing_var');
1433 unset($_ENV['existing_var']);
1436 public function testEnvIsInherited()
1438 $process = $this->getProcessForCode('echo serialize($_SERVER);', null, array('BAR' => 'BAZ', 'EMPTY' => ''));
1441 $_ENV['FOO'] = 'BAR';
1445 $expected = array('BAR' => 'BAZ', 'EMPTY' => '', 'FOO' => 'BAR');
1446 $env = array_intersect_key(unserialize($process->getOutput()), $expected);
1448 $this->assertEquals($expected, $env);
1451 unset($_ENV['FOO']);
1457 public function testInheritEnvDisabled()
1459 $process = $this->getProcessForCode('echo serialize($_SERVER);', null, array('BAR' => 'BAZ'));
1462 $_ENV['FOO'] = 'BAR';
1464 $this->assertSame($process, $process->inheritEnvironmentVariables(false));
1465 $this->assertFalse($process->areEnvironmentVariablesInherited());
1469 $expected = array('BAR' => 'BAZ', 'FOO' => 'BAR');
1470 $env = array_intersect_key(unserialize($process->getOutput()), $expected);
1471 unset($expected['FOO']);
1473 $this->assertSame($expected, $env);
1476 unset($_ENV['FOO']);
1479 public function testGetCommandLine()
1481 $p = new Process(array('/usr/bin/php'));
1483 $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'";
1484 $this->assertSame($expected, $p->getCommandLine());
1488 * @dataProvider provideEscapeArgument
1490 public function testEscapeArgument($arg)
1492 $p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg));
1495 $this->assertSame((string) $arg, $p->getOutput());
1499 * @dataProvider provideEscapeArgument
1502 public function testEscapeArgumentWhenInheritEnvDisabled($arg)
1504 $p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg), null, array('BAR' => 'BAZ'));
1505 $p->inheritEnvironmentVariables(false);
1508 $this->assertSame((string) $arg, $p->getOutput());
1511 public function testRawCommandLine()
1513 $p = new Process(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);')));
1516 $expected = <<<EOTXT
1526 $this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput()));
1529 public function provideEscapeArgument()
1531 yield array('a"b%c%');
1532 yield array('a"b^c^');
1533 yield array("a\nb'c");
1534 yield array('a^b c!');
1535 yield array("a!b\tc");
1536 yield array('a\\\\"\\"');
1537 yield array('éÉèÈàÀöä');
1543 public function testEnvArgument()
1545 $env = array('FOO' => 'Foo', 'BAR' => 'Bar');
1546 $cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ';
1547 $p = new Process($cmd, null, $env);
1548 $p->run(null, array('BAR' => 'baR', 'BAZ' => 'baZ'));
1550 $this->assertSame('Foo baR baZ', rtrim($p->getOutput()));
1551 $this->assertSame($env, $p->getEnv());
1555 * @param string $commandline
1556 * @param string|null $cwd
1557 * @param array|null $env
1558 * @param string|null $input
1559 * @param int $timeout
1560 * @param array $options
1564 private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60)
1566 $process = new Process($commandline, $cwd, $env, $input, $timeout);
1567 $process->inheritEnvironmentVariables();
1569 if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
1571 $process->setEnhanceSigchildCompatibility(false);
1572 $process->getExitCode();
1573 $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
1574 } catch (RuntimeException $e) {
1575 $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
1577 $process->setEnhanceSigchildCompatibility(true);
1579 self::$notEnhancedSigchild = true;
1584 if (self::$process) {
1585 self::$process->stop(0);
1588 return self::$process = $process;
1594 private function getProcessForCode($code, $cwd = null, array $env = null, $input = null, $timeout = 60)
1596 return $this->getProcess(array(self::$phpBin, '-r', $code), $cwd, $env, $input, $timeout);
1599 private function skipIfNotEnhancedSigchild($expectException = true)
1601 if (self::$sigchild) {
1602 if (!$expectException) {
1603 $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
1604 } elseif (self::$notEnhancedSigchild) {
1605 if (method_exists($this, 'expectException')) {
1606 $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1607 $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.');
1609 $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild.');
1616 class NonStringifiable