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());
453 public function testTTYCommand()
455 if ('\\' === DIRECTORY_SEPARATOR) {
456 $this->markTestSkipped('Windows does not have /dev/tty support');
459 $process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine());
460 $process->setTty(true);
462 $this->assertTrue($process->isRunning());
465 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
471 public function testTTYCommandExitCode()
473 if ('\\' === DIRECTORY_SEPARATOR) {
474 $this->markTestSkipped('Windows does have /dev/tty support');
476 $this->skipIfNotEnhancedSigchild();
478 $process = $this->getProcess('echo "foo" >> /dev/null');
479 $process->setTty(true);
482 $this->assertTrue($process->isSuccessful());
486 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
487 * @expectedExceptionMessage TTY mode is not supported on Windows platform.
489 public function testTTYInWindowsEnvironment()
491 if ('\\' !== DIRECTORY_SEPARATOR) {
492 $this->markTestSkipped('This test is for Windows platform only');
495 $process = $this->getProcess('echo "foo" >> /dev/null');
496 $process->setTty(false);
497 $process->setTty(true);
500 public function testExitCodeTextIsNullWhenExitCodeIsNull()
502 $this->skipIfNotEnhancedSigchild();
504 $process = $this->getProcess('');
505 $this->assertNull($process->getExitCodeText());
508 public function testPTYCommand()
510 if (!Process::isPtySupported()) {
511 $this->markTestSkipped('PTY is not supported on this operating system.');
514 $process = $this->getProcess('echo "foo"');
515 $process->setPty(true);
518 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
519 $this->assertEquals("foo\r\n", $process->getOutput());
522 public function testMustRun()
524 $this->skipIfNotEnhancedSigchild();
526 $process = $this->getProcess('echo foo');
528 $this->assertSame($process, $process->mustRun());
529 $this->assertEquals('foo'.PHP_EOL, $process->getOutput());
532 public function testSuccessfulMustRunHasCorrectExitCode()
534 $this->skipIfNotEnhancedSigchild();
536 $process = $this->getProcess('echo foo')->mustRun();
537 $this->assertEquals(0, $process->getExitCode());
541 * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException
543 public function testMustRunThrowsException()
545 $this->skipIfNotEnhancedSigchild();
547 $process = $this->getProcess('exit 1');
551 public function testExitCodeText()
553 $this->skipIfNotEnhancedSigchild();
555 $process = $this->getProcess('');
556 $r = new \ReflectionObject($process);
557 $p = $r->getProperty('exitcode');
558 $p->setAccessible(true);
560 $p->setValue($process, 2);
561 $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
564 public function testStartIsNonBlocking()
566 $process = $this->getProcessForCode('usleep(500000);');
567 $start = microtime(true);
569 $end = microtime(true);
570 $this->assertLessThan(0.4, $end - $start);
574 public function testUpdateStatus()
576 $process = $this->getProcess('echo foo');
578 $this->assertGreaterThan(0, strlen($process->getOutput()));
581 public function testGetExitCodeIsNullOnStart()
583 $this->skipIfNotEnhancedSigchild();
585 $process = $this->getProcessForCode('usleep(100000);');
586 $this->assertNull($process->getExitCode());
588 $this->assertNull($process->getExitCode());
590 $this->assertEquals(0, $process->getExitCode());
593 public function testGetExitCodeIsNullOnWhenStartingAgain()
595 $this->skipIfNotEnhancedSigchild();
597 $process = $this->getProcessForCode('usleep(100000);');
599 $this->assertEquals(0, $process->getExitCode());
601 $this->assertNull($process->getExitCode());
603 $this->assertEquals(0, $process->getExitCode());
606 public function testGetExitCode()
608 $this->skipIfNotEnhancedSigchild();
610 $process = $this->getProcess('echo foo');
612 $this->assertSame(0, $process->getExitCode());
615 public function testStatus()
617 $process = $this->getProcessForCode('usleep(100000);');
618 $this->assertFalse($process->isRunning());
619 $this->assertFalse($process->isStarted());
620 $this->assertFalse($process->isTerminated());
621 $this->assertSame(Process::STATUS_READY, $process->getStatus());
623 $this->assertTrue($process->isRunning());
624 $this->assertTrue($process->isStarted());
625 $this->assertFalse($process->isTerminated());
626 $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
628 $this->assertFalse($process->isRunning());
629 $this->assertTrue($process->isStarted());
630 $this->assertTrue($process->isTerminated());
631 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
634 public function testStop()
636 $process = $this->getProcessForCode('sleep(31);');
638 $this->assertTrue($process->isRunning());
640 $this->assertFalse($process->isRunning());
643 public function testIsSuccessful()
645 $this->skipIfNotEnhancedSigchild();
647 $process = $this->getProcess('echo foo');
649 $this->assertTrue($process->isSuccessful());
652 public function testIsSuccessfulOnlyAfterTerminated()
654 $this->skipIfNotEnhancedSigchild();
656 $process = $this->getProcessForCode('usleep(100000);');
659 $this->assertFalse($process->isSuccessful());
663 $this->assertTrue($process->isSuccessful());
666 public function testIsNotSuccessful()
668 $this->skipIfNotEnhancedSigchild();
670 $process = $this->getProcessForCode('throw new \Exception(\'BOUM\');');
672 $this->assertFalse($process->isSuccessful());
675 public function testProcessIsNotSignaled()
677 if ('\\' === DIRECTORY_SEPARATOR) {
678 $this->markTestSkipped('Windows does not support POSIX signals');
680 $this->skipIfNotEnhancedSigchild();
682 $process = $this->getProcess('echo foo');
684 $this->assertFalse($process->hasBeenSignaled());
687 public function testProcessWithoutTermSignal()
689 if ('\\' === DIRECTORY_SEPARATOR) {
690 $this->markTestSkipped('Windows does not support POSIX signals');
692 $this->skipIfNotEnhancedSigchild();
694 $process = $this->getProcess('echo foo');
696 $this->assertEquals(0, $process->getTermSignal());
699 public function testProcessIsSignaledIfStopped()
701 if ('\\' === DIRECTORY_SEPARATOR) {
702 $this->markTestSkipped('Windows does not support POSIX signals');
704 $this->skipIfNotEnhancedSigchild();
706 $process = $this->getProcessForCode('sleep(32);');
709 $this->assertTrue($process->hasBeenSignaled());
710 $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
714 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
715 * @expectedExceptionMessage The process has been signaled
717 public function testProcessThrowsExceptionWhenExternallySignaled()
719 if (!function_exists('posix_kill')) {
720 $this->markTestSkipped('Function posix_kill is required.');
722 $this->skipIfNotEnhancedSigchild(false);
724 $process = $this->getProcessForCode('sleep(32.1);');
726 posix_kill($process->getPid(), 9); // SIGKILL
731 public function testRestart()
733 $process1 = $this->getProcessForCode('echo getmypid();');
735 $process2 = $process1->restart();
737 $process2->wait(); // wait for output
739 // Ensure that both processed finished and the output is numeric
740 $this->assertFalse($process1->isRunning());
741 $this->assertFalse($process2->isRunning());
742 $this->assertInternalType('numeric', $process1->getOutput());
743 $this->assertInternalType('numeric', $process2->getOutput());
745 // Ensure that restart returned a new process by check that the output is different
746 $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
750 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
751 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
753 public function testRunProcessWithTimeout()
755 $process = $this->getProcessForCode('sleep(30);');
756 $process->setTimeout(0.1);
757 $start = microtime(true);
760 $this->fail('A RuntimeException should have been raised');
761 } catch (RuntimeException $e) {
764 $this->assertLessThan(15, microtime(true) - $start);
770 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
771 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
773 public function testIterateOverProcessWithTimeout()
775 $process = $this->getProcessForCode('sleep(30);');
776 $process->setTimeout(0.1);
777 $start = microtime(true);
780 foreach ($process as $buffer);
781 $this->fail('A RuntimeException should have been raised');
782 } catch (RuntimeException $e) {
785 $this->assertLessThan(15, microtime(true) - $start);
790 public function testCheckTimeoutOnNonStartedProcess()
792 $process = $this->getProcess('echo foo');
793 $this->assertNull($process->checkTimeout());
796 public function testCheckTimeoutOnTerminatedProcess()
798 $process = $this->getProcess('echo foo');
800 $this->assertNull($process->checkTimeout());
804 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
805 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
807 public function testCheckTimeoutOnStartedProcess()
809 $process = $this->getProcessForCode('sleep(33);');
810 $process->setTimeout(0.1);
813 $start = microtime(true);
816 while ($process->isRunning()) {
817 $process->checkTimeout();
820 $this->fail('A ProcessTimedOutException should have been raised');
821 } catch (ProcessTimedOutException $e) {
824 $this->assertLessThan(15, microtime(true) - $start);
829 public function testIdleTimeout()
831 $process = $this->getProcessForCode('sleep(34);');
832 $process->setTimeout(60);
833 $process->setIdleTimeout(0.1);
838 $this->fail('A timeout exception was expected.');
839 } catch (ProcessTimedOutException $e) {
840 $this->assertTrue($e->isIdleTimeout());
841 $this->assertFalse($e->isGeneralTimeout());
842 $this->assertEquals(0.1, $e->getExceededTimeout());
846 public function testIdleTimeoutNotExceededWhenOutputIsSent()
848 $process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}');
849 $process->setTimeout(1);
852 while (false === strpos($process->getOutput(), 'foo')) {
856 $process->setIdleTimeout(0.5);
860 $this->fail('A timeout exception was expected.');
861 } catch (ProcessTimedOutException $e) {
862 $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
863 $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
864 $this->assertEquals(1, $e->getExceededTimeout());
869 * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
870 * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
872 public function testStartAfterATimeout()
874 $process = $this->getProcessForCode('sleep(35);');
875 $process->setTimeout(0.1);
879 $this->fail('A ProcessTimedOutException should have been raised.');
880 } catch (ProcessTimedOutException $e) {
882 $this->assertFalse($process->isRunning());
884 $this->assertTrue($process->isRunning());
890 public function testGetPid()
892 $process = $this->getProcessForCode('sleep(36);');
894 $this->assertGreaterThan(0, $process->getPid());
898 public function testGetPidIsNullBeforeStart()
900 $process = $this->getProcess('foo');
901 $this->assertNull($process->getPid());
904 public function testGetPidIsNullAfterRun()
906 $process = $this->getProcess('echo foo');
908 $this->assertNull($process->getPid());
912 * @requires extension pcntl
914 public function testSignal()
916 $process = $this->getProcess(array(self::$phpBin, __DIR__.'/SignalListener.php'));
919 while (false === strpos($process->getOutput(), 'Caught')) {
922 $process->signal(SIGUSR1);
925 $this->assertEquals('Caught SIGUSR1', $process->getOutput());
929 * @requires extension pcntl
931 public function testExitCodeIsAvailableAfterSignal()
933 $this->skipIfNotEnhancedSigchild();
935 $process = $this->getProcess('sleep 4');
937 $process->signal(SIGKILL);
939 while ($process->isRunning()) {
943 $this->assertFalse($process->isRunning());
944 $this->assertTrue($process->hasBeenSignaled());
945 $this->assertFalse($process->isSuccessful());
946 $this->assertEquals(137, $process->getExitCode());
950 * @expectedException \Symfony\Component\Process\Exception\LogicException
951 * @expectedExceptionMessage Can not send signal on a non running process.
953 public function testSignalProcessNotRunning()
955 $process = $this->getProcess('foo');
956 $process->signal(1); // SIGHUP
960 * @dataProvider provideMethodsThatNeedARunningProcess
962 public function testMethodsThatNeedARunningProcess($method)
964 $process = $this->getProcess('foo');
966 if (method_exists($this, 'expectException')) {
967 $this->expectException('Symfony\Component\Process\Exception\LogicException');
968 $this->expectExceptionMessage(sprintf('Process must be started before calling %s.', $method));
970 $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
973 $process->{$method}();
976 public function provideMethodsThatNeedARunningProcess()
980 array('getIncrementalOutput'),
981 array('getErrorOutput'),
982 array('getIncrementalErrorOutput'),
988 * @dataProvider provideMethodsThatNeedATerminatedProcess
989 * @expectedException \Symfony\Component\Process\Exception\LogicException
990 * @expectedExceptionMessage Process must be terminated before calling
992 public function testMethodsThatNeedATerminatedProcess($method)
994 $process = $this->getProcessForCode('sleep(37);');
997 $process->{$method}();
999 $this->fail('A LogicException must have been thrown');
1000 } catch (\Exception $e) {
1007 public function provideMethodsThatNeedATerminatedProcess()
1010 array('hasBeenSignaled'),
1011 array('getTermSignal'),
1012 array('hasBeenStopped'),
1013 array('getStopSignal'),
1018 * @dataProvider provideWrongSignal
1019 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1021 public function testWrongSignal($signal)
1023 if ('\\' === DIRECTORY_SEPARATOR) {
1024 $this->markTestSkipped('POSIX signals do not work on Windows');
1027 $process = $this->getProcessForCode('sleep(38);');
1030 $process->signal($signal);
1031 $this->fail('A RuntimeException must have been thrown');
1032 } catch (RuntimeException $e) {
1039 public function provideWrongSignal()
1043 array('Céphalopodes'),
1047 public function testDisableOutputDisablesTheOutput()
1049 $p = $this->getProcess('foo');
1050 $this->assertFalse($p->isOutputDisabled());
1051 $p->disableOutput();
1052 $this->assertTrue($p->isOutputDisabled());
1054 $this->assertFalse($p->isOutputDisabled());
1058 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1059 * @expectedExceptionMessage Disabling output while the process is running is not possible.
1061 public function testDisableOutputWhileRunningThrowsException()
1063 $p = $this->getProcessForCode('sleep(39);');
1065 $p->disableOutput();
1069 * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1070 * @expectedExceptionMessage Enabling output while the process is running is not possible.
1072 public function testEnableOutputWhileRunningThrowsException()
1074 $p = $this->getProcessForCode('sleep(40);');
1075 $p->disableOutput();
1080 public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
1082 $p = $this->getProcess('echo foo');
1083 $p->disableOutput();
1086 $p->disableOutput();
1087 $this->assertTrue($p->isOutputDisabled());
1091 * @expectedException \Symfony\Component\Process\Exception\LogicException
1092 * @expectedExceptionMessage Output can not be disabled while an idle timeout is set.
1094 public function testDisableOutputWhileIdleTimeoutIsSet()
1096 $process = $this->getProcess('foo');
1097 $process->setIdleTimeout(1);
1098 $process->disableOutput();
1102 * @expectedException \Symfony\Component\Process\Exception\LogicException
1103 * @expectedExceptionMessage timeout can not be set while the output is disabled.
1105 public function testSetIdleTimeoutWhileOutputIsDisabled()
1107 $process = $this->getProcess('foo');
1108 $process->disableOutput();
1109 $process->setIdleTimeout(1);
1112 public function testSetNullIdleTimeoutWhileOutputIsDisabled()
1114 $process = $this->getProcess('foo');
1115 $process->disableOutput();
1116 $this->assertSame($process, $process->setIdleTimeout(null));
1120 * @dataProvider provideOutputFetchingMethods
1121 * @expectedException \Symfony\Component\Process\Exception\LogicException
1122 * @expectedExceptionMessage Output has been disabled.
1124 public function testGetOutputWhileDisabled($fetchMethod)
1126 $p = $this->getProcessForCode('sleep(41);');
1127 $p->disableOutput();
1129 $p->{$fetchMethod}();
1132 public function provideOutputFetchingMethods()
1136 array('getIncrementalOutput'),
1137 array('getErrorOutput'),
1138 array('getIncrementalErrorOutput'),
1142 public function testStopTerminatesProcessCleanly()
1144 $process = $this->getProcessForCode('echo 123; sleep(42);');
1145 $process->run(function () use ($process) {
1148 $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
1151 public function testKillSignalTerminatesProcessCleanly()
1153 $process = $this->getProcessForCode('echo 123; sleep(43);');
1154 $process->run(function () use ($process) {
1155 $process->signal(9); // SIGKILL
1157 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1160 public function testTermSignalTerminatesProcessCleanly()
1162 $process = $this->getProcessForCode('echo 123; sleep(44);');
1163 $process->run(function () use ($process) {
1164 $process->signal(15); // SIGTERM
1166 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1169 public function responsesCodeProvider()
1172 //expected output / getter / code to execute
1173 //array(1,'getExitCode','exit(1);'),
1174 //array(true,'isSuccessful','exit();'),
1175 array('output', 'getOutput', 'echo \'output\';'),
1179 public function pipesCodeProvider()
1181 $variations = array(
1182 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
1183 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
1186 if ('\\' === DIRECTORY_SEPARATOR) {
1187 // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650
1188 $sizes = array(1, 2, 4, 8);
1190 $sizes = array(1, 16, 64, 1024, 4096);
1194 foreach ($sizes as $size) {
1195 foreach ($variations as $code) {
1196 $codes[] = array($code, $size);
1204 * @dataProvider provideVariousIncrementals
1206 public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
1208 $process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null);
1211 $limit = microtime(true) + 3;
1214 while ($result !== $expected && microtime(true) < $limit) {
1215 $result .= $process->$method();
1218 $this->assertSame($expected, $result);
1222 public function provideVariousIncrementals()
1225 array('php://stdout', 'getIncrementalOutput'),
1226 array('php://stderr', 'getIncrementalErrorOutput'),
1230 public function testIteratorInput()
1232 $input = function () {
1237 $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input());
1239 $this->assertSame('pingpong', $process->getOutput());
1242 public function testSimpleInputStream()
1244 $input = new InputStream();
1246 $process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);');
1247 $process->setInput($input);
1249 $process->start(function ($type, $data) use ($input) {
1250 if ('ping' === $data) {
1251 $input->write('pang');
1252 } elseif (!$input->isClosed()) {
1253 $input->write('pong');
1259 $this->assertSame('pingpangpong', $process->getOutput());
1262 public function testInputStreamWithCallable()
1265 $stream = fopen('php://memory', 'w+');
1266 $stream = function () use ($stream, &$i) {
1269 fwrite($stream, ++$i);
1276 $input = new InputStream();
1277 $input->onEmpty($stream);
1278 $input->write($stream());
1280 $process = $this->getProcessForCode('echo fread(STDIN, 3);');
1281 $process->setInput($input);
1282 $process->start(function ($type, $data) use ($input) {
1287 $this->assertSame('123', $process->getOutput());
1290 public function testInputStreamWithGenerator()
1292 $input = new InputStream();
1293 $input->onEmpty(function ($input) {
1298 $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
1299 $process->setInput($input);
1301 $input->write('ping');
1303 $this->assertSame('pingpong', $process->getOutput());
1306 public function testInputStreamOnEmpty()
1309 $input = new InputStream();
1310 $input->onEmpty(function () use (&$i) { ++$i; });
1312 $process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;');
1313 $process->setInput($input);
1314 $process->start(function ($type, $data) use ($input) {
1315 if ('123' === $data) {
1321 $this->assertSame(0, $i, 'InputStream->onEmpty callback should be called only when the input *becomes* empty');
1322 $this->assertSame('123456', $process->getOutput());
1325 public function testIteratorOutput()
1327 $input = new InputStream();
1329 $process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);');
1330 $process->setInput($input);
1334 foreach ($process as $type => $data) {
1335 $output[] = array($type, $data);
1338 $expectedOutput = array(
1339 array($process::OUT, '123'),
1341 $this->assertSame($expectedOutput, $output);
1345 foreach ($process as $type => $data) {
1346 $output[] = array($type, $data);
1349 $this->assertSame('', $process->getOutput());
1350 $this->assertFalse($process->isRunning());
1352 $expectedOutput = array(
1353 array($process::OUT, '123'),
1354 array($process::ERR, '234'),
1355 array($process::OUT, '345'),
1356 array($process::ERR, '456'),
1358 $this->assertSame($expectedOutput, $output);
1361 public function testNonBlockingNorClearingIteratorOutput()
1363 $input = new InputStream();
1365 $process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));');
1366 $process->setInput($input);
1370 foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
1371 $output[] = array($type, $data);
1374 $expectedOutput = array(
1375 array($process::OUT, ''),
1377 $this->assertSame($expectedOutput, $output);
1381 foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
1383 $output[] = array($type, $data);
1387 $this->assertSame('123', $process->getOutput());
1388 $this->assertFalse($process->isRunning());
1390 $expectedOutput = array(
1391 array($process::OUT, ''),
1392 array($process::OUT, '123'),
1394 $this->assertSame($expectedOutput, $output);
1397 public function testChainedProcesses()
1399 $p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);');
1400 $p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
1406 $this->assertSame('123', $p1->getErrorOutput());
1407 $this->assertSame('', $p1->getOutput());
1408 $this->assertSame('', $p2->getErrorOutput());
1409 $this->assertSame('456', $p2->getOutput());
1412 public function testSetBadEnv()
1414 $process = $this->getProcess('echo hello');
1415 $process->setEnv(array('bad%%' => '123'));
1416 $process->inheritEnvironmentVariables(true);
1420 $this->assertSame('hello'.PHP_EOL, $process->getOutput());
1421 $this->assertSame('', $process->getErrorOutput());
1424 public function testEnvBackupDoesNotDeleteExistingVars()
1426 putenv('existing_var=foo');
1427 $_ENV['existing_var'] = 'foo';
1428 $process = $this->getProcess('php -r "echo getenv(\'new_test_var\');"');
1429 $process->setEnv(array('existing_var' => 'bar', 'new_test_var' => 'foo'));
1430 $process->inheritEnvironmentVariables();
1434 $this->assertSame('foo', $process->getOutput());
1435 $this->assertSame('foo', getenv('existing_var'));
1436 $this->assertFalse(getenv('new_test_var'));
1438 putenv('existing_var');
1439 unset($_ENV['existing_var']);
1442 public function testEnvIsInherited()
1444 $process = $this->getProcessForCode('echo serialize($_SERVER);', null, array('BAR' => 'BAZ', 'EMPTY' => ''));
1447 $_ENV['FOO'] = 'BAR';
1451 $expected = array('BAR' => 'BAZ', 'EMPTY' => '', 'FOO' => 'BAR');
1452 $env = array_intersect_key(unserialize($process->getOutput()), $expected);
1454 $this->assertEquals($expected, $env);
1457 unset($_ENV['FOO']);
1463 public function testInheritEnvDisabled()
1465 $process = $this->getProcessForCode('echo serialize($_SERVER);', null, array('BAR' => 'BAZ'));
1468 $_ENV['FOO'] = 'BAR';
1470 $this->assertSame($process, $process->inheritEnvironmentVariables(false));
1471 $this->assertFalse($process->areEnvironmentVariablesInherited());
1475 $expected = array('BAR' => 'BAZ', 'FOO' => 'BAR');
1476 $env = array_intersect_key(unserialize($process->getOutput()), $expected);
1477 unset($expected['FOO']);
1479 $this->assertSame($expected, $env);
1482 unset($_ENV['FOO']);
1485 public function testGetCommandLine()
1487 $p = new Process(array('/usr/bin/php'));
1489 $expected = '\\' === DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'";
1490 $this->assertSame($expected, $p->getCommandLine());
1494 * @dataProvider provideEscapeArgument
1496 public function testEscapeArgument($arg)
1498 $p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg));
1501 $this->assertSame($arg, $p->getOutput());
1505 * @dataProvider provideEscapeArgument
1508 public function testEscapeArgumentWhenInheritEnvDisabled($arg)
1510 $p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg), null, array('BAR' => 'BAZ'));
1511 $p->inheritEnvironmentVariables(false);
1514 $this->assertSame($arg, $p->getOutput());
1517 public function testRawCommandLine()
1519 $p = new Process(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);')));
1522 $expected = <<<EOTXT
1532 $this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput()));
1535 public function provideEscapeArgument()
1537 yield array('a"b%c%');
1538 yield array('a"b^c^');
1539 yield array("a\nb'c");
1540 yield array('a^b c!');
1541 yield array("a!b\tc");
1542 yield array('a\\\\"\\"');
1543 yield array('éÉèÈàÀöä');
1546 public function testEnvArgument()
1548 $env = array('FOO' => 'Foo', 'BAR' => 'Bar');
1549 $cmd = '\\' === DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ';
1550 $p = new Process($cmd, null, $env);
1551 $p->run(null, array('BAR' => 'baR', 'BAZ' => 'baZ'));
1553 $this->assertSame('Foo baR baZ', rtrim($p->getOutput()));
1554 $this->assertSame($env, $p->getEnv());
1558 * @param string $commandline
1559 * @param null|string $cwd
1560 * @param null|array $env
1561 * @param null|string $input
1562 * @param int $timeout
1563 * @param array $options
1567 private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60)
1569 $process = new Process($commandline, $cwd, $env, $input, $timeout);
1570 $process->inheritEnvironmentVariables();
1572 if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
1574 $process->setEnhanceSigchildCompatibility(false);
1575 $process->getExitCode();
1576 $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
1577 } catch (RuntimeException $e) {
1578 $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
1580 $process->setEnhanceSigchildCompatibility(true);
1582 self::$notEnhancedSigchild = true;
1587 if (self::$process) {
1588 self::$process->stop(0);
1591 return self::$process = $process;
1597 private function getProcessForCode($code, $cwd = null, array $env = null, $input = null, $timeout = 60)
1599 return $this->getProcess(array(self::$phpBin, '-r', $code), $cwd, $env, $input, $timeout);
1602 private function skipIfNotEnhancedSigchild($expectException = true)
1604 if (self::$sigchild) {
1605 if (!$expectException) {
1606 $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
1607 } elseif (self::$notEnhancedSigchild) {
1608 if (method_exists($this, 'expectException')) {
1609 $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1610 $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.');
1612 $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild.');
1619 class NonStringifiable