1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Process\Tests;
13
14use PHPUnit\Framework\TestCase;
15use Symfony\Component\Process\Exception\LogicException;
16use Symfony\Component\Process\Exception\ProcessTimedOutException;
17use Symfony\Component\Process\Exception\RuntimeException;
18use Symfony\Component\Process\InputStream;
19use Symfony\Component\Process\PhpExecutableFinder;
20use Symfony\Component\Process\Pipes\PipesInterface;
21use Symfony\Component\Process\Process;
22
23/**
24 * @author Robert Schönthal <seroscho@googlemail.com>
25 */
26class ProcessTest extends TestCase
27{
28    private static $phpBin;
29    private static $process;
30    private static $sigchild;
31    private static $notEnhancedSigchild = false;
32
33    public static function setUpBeforeClass()
34    {
35        $phpBin = new PhpExecutableFinder();
36        self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find());
37
38        ob_start();
39        phpinfo(\INFO_GENERAL);
40        self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
41    }
42
43    protected function tearDown()
44    {
45        if (self::$process) {
46            self::$process->stop(0);
47            self::$process = null;
48        }
49    }
50
51    /**
52     * @group legacy
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.
54     */
55    public function testInvalidCwd()
56    {
57        if ('\\' === \DIRECTORY_SEPARATOR) {
58            $this->markTestSkipped('False-positive on Windows/appveyor.');
59        }
60
61        // Check that it works fine if the CWD exists
62        $cmd = new Process('echo test', __DIR__);
63        $cmd->run();
64
65        $cmd = new Process('echo test', __DIR__.'/notfound/');
66        $cmd->run();
67    }
68
69    public function testThatProcessDoesNotThrowWarningDuringRun()
70    {
71        if ('\\' === \DIRECTORY_SEPARATOR) {
72            $this->markTestSkipped('This test is transient on Windows');
73        }
74        @trigger_error('Test Error', \E_USER_NOTICE);
75        $process = $this->getProcessForCode('sleep(3)');
76        $process->run();
77        $actualError = error_get_last();
78        $this->assertEquals('Test Error', $actualError['message']);
79        $this->assertEquals(\E_USER_NOTICE, $actualError['type']);
80    }
81
82    public function testNegativeTimeoutFromConstructor()
83    {
84        $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException');
85        $this->getProcess('', null, null, null, -1);
86    }
87
88    public function testNegativeTimeoutFromSetter()
89    {
90        $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException');
91        $p = $this->getProcess('');
92        $p->setTimeout(-1);
93    }
94
95    public function testFloatAndNullTimeout()
96    {
97        $p = $this->getProcess('');
98
99        $p->setTimeout(10);
100        $this->assertSame(10.0, $p->getTimeout());
101
102        $p->setTimeout(null);
103        $this->assertNull($p->getTimeout());
104
105        $p->setTimeout(0.0);
106        $this->assertNull($p->getTimeout());
107    }
108
109    /**
110     * @requires extension pcntl
111     */
112    public function testStopWithTimeoutIsActuallyWorking()
113    {
114        $p = $this->getProcess([self::$phpBin, __DIR__.'/NonStopableProcess.php', 30]);
115        $p->start();
116
117        while (false === strpos($p->getOutput(), 'received')) {
118            usleep(1000);
119        }
120        $start = microtime(true);
121        $p->stop(0.1);
122
123        $p->wait();
124
125        $this->assertLessThan(15, microtime(true) - $start);
126    }
127
128    public function testAllOutputIsActuallyReadOnTermination()
129    {
130        // this code will result in a maximum of 2 reads of 8192 bytes by calling
131        // start() and isRunning().  by the time getOutput() is called the process
132        // has terminated so the internal pipes array is already empty. normally
133        // the call to start() will not read any data as the process will not have
134        // generated output, but this is non-deterministic so we must count it as
135        // a possibility.  therefore we need 2 * PipesInterface::CHUNK_SIZE plus
136        // another byte which will never be read.
137        $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2;
138
139        $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
140        $p = $this->getProcessForCode($code);
141
142        $p->start();
143
144        // Don't call Process::run nor Process::wait to avoid any read of pipes
145        $h = new \ReflectionProperty($p, 'process');
146        $h->setAccessible(true);
147        $h = $h->getValue($p);
148        $s = @proc_get_status($h);
149
150        while (!empty($s['running'])) {
151            usleep(1000);
152            $s = proc_get_status($h);
153        }
154
155        $o = $p->getOutput();
156
157        $this->assertEquals($expectedOutputSize, \strlen($o));
158    }
159
160    public function testCallbacksAreExecutedWithStart()
161    {
162        $process = $this->getProcess('echo foo');
163        $process->start(function ($type, $buffer) use (&$data) {
164            $data .= $buffer;
165        });
166
167        $process->wait();
168
169        $this->assertSame('foo'.\PHP_EOL, $data);
170    }
171
172    /**
173     * tests results from sub processes.
174     *
175     * @dataProvider responsesCodeProvider
176     */
177    public function testProcessResponses($expected, $getter, $code)
178    {
179        $p = $this->getProcessForCode($code);
180        $p->run();
181
182        $this->assertSame($expected, $p->$getter());
183    }
184
185    /**
186     * tests results from sub processes.
187     *
188     * @dataProvider pipesCodeProvider
189     */
190    public function testProcessPipes($code, $size)
191    {
192        $expected = str_repeat(str_repeat('*', 1024), $size).'!';
193        $expectedLength = (1024 * $size) + 1;
194
195        $p = $this->getProcessForCode($code);
196        $p->setInput($expected);
197        $p->run();
198
199        $this->assertEquals($expectedLength, \strlen($p->getOutput()));
200        $this->assertEquals($expectedLength, \strlen($p->getErrorOutput()));
201    }
202
203    /**
204     * @dataProvider pipesCodeProvider
205     */
206    public function testSetStreamAsInput($code, $size)
207    {
208        $expected = str_repeat(str_repeat('*', 1024), $size).'!';
209        $expectedLength = (1024 * $size) + 1;
210
211        $stream = fopen('php://temporary', 'w+');
212        fwrite($stream, $expected);
213        rewind($stream);
214
215        $p = $this->getProcessForCode($code);
216        $p->setInput($stream);
217        $p->run();
218
219        fclose($stream);
220
221        $this->assertEquals($expectedLength, \strlen($p->getOutput()));
222        $this->assertEquals($expectedLength, \strlen($p->getErrorOutput()));
223    }
224
225    public function testLiveStreamAsInput()
226    {
227        $stream = fopen('php://memory', 'r+');
228        fwrite($stream, 'hello');
229        rewind($stream);
230
231        $p = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
232        $p->setInput($stream);
233        $p->start(function ($type, $data) use ($stream) {
234            if ('hello' === $data) {
235                fclose($stream);
236            }
237        });
238        $p->wait();
239
240        $this->assertSame('hello', $p->getOutput());
241    }
242
243    public function testSetInputWhileRunningThrowsAnException()
244    {
245        $this->expectException('Symfony\Component\Process\Exception\LogicException');
246        $this->expectExceptionMessage('Input can not be set while the process is running.');
247        $process = $this->getProcessForCode('sleep(30);');
248        $process->start();
249        try {
250            $process->setInput('foobar');
251            $process->stop();
252            $this->fail('A LogicException should have been raised.');
253        } catch (LogicException $e) {
254        }
255        $process->stop();
256
257        throw $e;
258    }
259
260    /**
261     * @dataProvider provideInvalidInputValues
262     */
263    public function testInvalidInput($value)
264    {
265        $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException');
266        $this->expectExceptionMessage('"Symfony\Component\Process\Process::setInput" only accepts strings, Traversable objects or stream resources.');
267        $process = $this->getProcess('foo');
268        $process->setInput($value);
269    }
270
271    public function provideInvalidInputValues()
272    {
273        return [
274            [[]],
275            [new NonStringifiable()],
276        ];
277    }
278
279    /**
280     * @dataProvider provideInputValues
281     */
282    public function testValidInput($expected, $value)
283    {
284        $process = $this->getProcess('foo');
285        $process->setInput($value);
286        $this->assertSame($expected, $process->getInput());
287    }
288
289    public function provideInputValues()
290    {
291        return [
292            [null, null],
293            ['24.5', 24.5],
294            ['input data', 'input data'],
295        ];
296    }
297
298    public function chainedCommandsOutputProvider()
299    {
300        if ('\\' === \DIRECTORY_SEPARATOR) {
301            return [
302                ["2 \r\n2\r\n", '&&', '2'],
303            ];
304        }
305
306        return [
307            ["1\n1\n", ';', '1'],
308            ["2\n2\n", '&&', '2'],
309        ];
310    }
311
312    /**
313     * @dataProvider chainedCommandsOutputProvider
314     */
315    public function testChainedCommandsOutput($expected, $operator, $input)
316    {
317        $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input));
318        $process->run();
319        $this->assertEquals($expected, $process->getOutput());
320    }
321
322    public function testCallbackIsExecutedForOutput()
323    {
324        $p = $this->getProcessForCode('echo \'foo\';');
325
326        $called = false;
327        $p->run(function ($type, $buffer) use (&$called) {
328            $called = 'foo' === $buffer;
329        });
330
331        $this->assertTrue($called, 'The callback should be executed with the output');
332    }
333
334    public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled()
335    {
336        $p = $this->getProcessForCode('echo \'foo\';');
337        $p->disableOutput();
338
339        $called = false;
340        $p->run(function ($type, $buffer) use (&$called) {
341            $called = 'foo' === $buffer;
342        });
343
344        $this->assertTrue($called, 'The callback should be executed with the output');
345    }
346
347    public function testGetErrorOutput()
348    {
349        $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }');
350
351        $p->run();
352        $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
353    }
354
355    public function testFlushErrorOutput()
356    {
357        $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }');
358
359        $p->run();
360        $p->clearErrorOutput();
361        $this->assertEmpty($p->getErrorOutput());
362    }
363
364    /**
365     * @dataProvider provideIncrementalOutput
366     */
367    public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
368    {
369        $lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
370
371        $p = $this->getProcessForCode('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');');
372
373        $h = fopen($lock, 'w');
374        flock($h, \LOCK_EX);
375
376        $p->start();
377
378        foreach (['foo', 'bar'] as $s) {
379            while (false === strpos($p->$getOutput(), $s)) {
380                usleep(1000);
381            }
382
383            $this->assertSame($s, $p->$getIncrementalOutput());
384            $this->assertSame('', $p->$getIncrementalOutput());
385
386            flock($h, \LOCK_UN);
387        }
388
389        fclose($h);
390    }
391
392    public function provideIncrementalOutput()
393    {
394        return [
395            ['getOutput', 'getIncrementalOutput', 'php://stdout'],
396            ['getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'],
397        ];
398    }
399
400    public function testGetOutput()
401    {
402        $p = $this->getProcessForCode('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }');
403
404        $p->run();
405        $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
406    }
407
408    public function testFlushOutput()
409    {
410        $p = $this->getProcessForCode('$n=0;while ($n<3) {echo \' foo \';$n++;}');
411
412        $p->run();
413        $p->clearOutput();
414        $this->assertEmpty($p->getOutput());
415    }
416
417    public function testZeroAsOutput()
418    {
419        if ('\\' === \DIRECTORY_SEPARATOR) {
420            // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
421            $p = $this->getProcess('echo | set /p dummyName=0');
422        } else {
423            $p = $this->getProcess('printf 0');
424        }
425
426        $p->run();
427        $this->assertSame('0', $p->getOutput());
428    }
429
430    public function testExitCodeCommandFailed()
431    {
432        if ('\\' === \DIRECTORY_SEPARATOR) {
433            $this->markTestSkipped('Windows does not support POSIX exit code');
434        }
435        $this->skipIfNotEnhancedSigchild();
436
437        // such command run in bash return an exitcode 127
438        $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
439        $process->run();
440
441        $this->assertGreaterThan(0, $process->getExitCode());
442    }
443
444    public function testTTYCommand()
445    {
446        if ('\\' === \DIRECTORY_SEPARATOR) {
447            $this->markTestSkipped('Windows does not have /dev/tty support');
448        }
449
450        $process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine());
451        $process->setTty(true);
452        $process->start();
453        $this->assertTrue($process->isRunning());
454        $process->wait();
455
456        $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
457    }
458
459    public function testTTYCommandExitCode()
460    {
461        if ('\\' === \DIRECTORY_SEPARATOR) {
462            $this->markTestSkipped('Windows does have /dev/tty support');
463        }
464        $this->skipIfNotEnhancedSigchild();
465
466        $process = $this->getProcess('echo "foo" >> /dev/null');
467        $process->setTty(true);
468        $process->run();
469
470        $this->assertTrue($process->isSuccessful());
471    }
472
473    public function testTTYInWindowsEnvironment()
474    {
475        $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
476        $this->expectExceptionMessage('TTY mode is not supported on Windows platform.');
477        if ('\\' !== \DIRECTORY_SEPARATOR) {
478            $this->markTestSkipped('This test is for Windows platform only');
479        }
480
481        $process = $this->getProcess('echo "foo" >> /dev/null');
482        $process->setTty(false);
483        $process->setTty(true);
484    }
485
486    public function testExitCodeTextIsNullWhenExitCodeIsNull()
487    {
488        $this->skipIfNotEnhancedSigchild();
489
490        $process = $this->getProcess('');
491        $this->assertNull($process->getExitCodeText());
492    }
493
494    public function testPTYCommand()
495    {
496        if (!Process::isPtySupported()) {
497            $this->markTestSkipped('PTY is not supported on this operating system.');
498        }
499
500        $process = $this->getProcess('echo "foo"');
501        $process->setPty(true);
502        $process->run();
503
504        $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
505        $this->assertEquals("foo\r\n", $process->getOutput());
506    }
507
508    public function testMustRun()
509    {
510        $this->skipIfNotEnhancedSigchild();
511
512        $process = $this->getProcess('echo foo');
513
514        $this->assertSame($process, $process->mustRun());
515        $this->assertEquals('foo'.\PHP_EOL, $process->getOutput());
516    }
517
518    public function testSuccessfulMustRunHasCorrectExitCode()
519    {
520        $this->skipIfNotEnhancedSigchild();
521
522        $process = $this->getProcess('echo foo')->mustRun();
523        $this->assertEquals(0, $process->getExitCode());
524    }
525
526    public function testMustRunThrowsException()
527    {
528        $this->expectException('Symfony\Component\Process\Exception\ProcessFailedException');
529        $this->skipIfNotEnhancedSigchild();
530
531        $process = $this->getProcess('exit 1');
532        $process->mustRun();
533    }
534
535    public function testExitCodeText()
536    {
537        $this->skipIfNotEnhancedSigchild();
538
539        $process = $this->getProcess('');
540        $r = new \ReflectionObject($process);
541        $p = $r->getProperty('exitcode');
542        $p->setAccessible(true);
543
544        $p->setValue($process, 2);
545        $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
546    }
547
548    public function testStartIsNonBlocking()
549    {
550        $process = $this->getProcessForCode('usleep(500000);');
551        $start = microtime(true);
552        $process->start();
553        $end = microtime(true);
554        $this->assertLessThan(0.4, $end - $start);
555        $process->stop();
556    }
557
558    public function testUpdateStatus()
559    {
560        $process = $this->getProcess('echo foo');
561        $process->run();
562        $this->assertGreaterThan(0, \strlen($process->getOutput()));
563    }
564
565    public function testGetExitCodeIsNullOnStart()
566    {
567        $this->skipIfNotEnhancedSigchild();
568
569        $process = $this->getProcessForCode('usleep(100000);');
570        $this->assertNull($process->getExitCode());
571        $process->start();
572        $this->assertNull($process->getExitCode());
573        $process->wait();
574        $this->assertEquals(0, $process->getExitCode());
575    }
576
577    public function testGetExitCodeIsNullOnWhenStartingAgain()
578    {
579        $this->skipIfNotEnhancedSigchild();
580
581        $process = $this->getProcessForCode('usleep(100000);');
582        $process->run();
583        $this->assertEquals(0, $process->getExitCode());
584        $process->start();
585        $this->assertNull($process->getExitCode());
586        $process->wait();
587        $this->assertEquals(0, $process->getExitCode());
588    }
589
590    public function testGetExitCode()
591    {
592        $this->skipIfNotEnhancedSigchild();
593
594        $process = $this->getProcess('echo foo');
595        $process->run();
596        $this->assertSame(0, $process->getExitCode());
597    }
598
599    public function testStatus()
600    {
601        $process = $this->getProcessForCode('usleep(100000);');
602        $this->assertFalse($process->isRunning());
603        $this->assertFalse($process->isStarted());
604        $this->assertFalse($process->isTerminated());
605        $this->assertSame(Process::STATUS_READY, $process->getStatus());
606        $process->start();
607        $this->assertTrue($process->isRunning());
608        $this->assertTrue($process->isStarted());
609        $this->assertFalse($process->isTerminated());
610        $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
611        $process->wait();
612        $this->assertFalse($process->isRunning());
613        $this->assertTrue($process->isStarted());
614        $this->assertTrue($process->isTerminated());
615        $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
616    }
617
618    public function testStop()
619    {
620        $process = $this->getProcessForCode('sleep(31);');
621        $process->start();
622        $this->assertTrue($process->isRunning());
623        $process->stop();
624        $this->assertFalse($process->isRunning());
625    }
626
627    public function testIsSuccessful()
628    {
629        $this->skipIfNotEnhancedSigchild();
630
631        $process = $this->getProcess('echo foo');
632        $process->run();
633        $this->assertTrue($process->isSuccessful());
634    }
635
636    public function testIsSuccessfulOnlyAfterTerminated()
637    {
638        $this->skipIfNotEnhancedSigchild();
639
640        $process = $this->getProcessForCode('usleep(100000);');
641        $process->start();
642
643        $this->assertFalse($process->isSuccessful());
644
645        $process->wait();
646
647        $this->assertTrue($process->isSuccessful());
648    }
649
650    public function testIsNotSuccessful()
651    {
652        $this->skipIfNotEnhancedSigchild();
653
654        $process = $this->getProcessForCode('throw new \Exception(\'BOUM\');');
655        $process->run();
656        $this->assertFalse($process->isSuccessful());
657    }
658
659    public function testProcessIsNotSignaled()
660    {
661        if ('\\' === \DIRECTORY_SEPARATOR) {
662            $this->markTestSkipped('Windows does not support POSIX signals');
663        }
664        $this->skipIfNotEnhancedSigchild();
665
666        $process = $this->getProcess('echo foo');
667        $process->run();
668        $this->assertFalse($process->hasBeenSignaled());
669    }
670
671    public function testProcessWithoutTermSignal()
672    {
673        if ('\\' === \DIRECTORY_SEPARATOR) {
674            $this->markTestSkipped('Windows does not support POSIX signals');
675        }
676        $this->skipIfNotEnhancedSigchild();
677
678        $process = $this->getProcess('echo foo');
679        $process->run();
680        $this->assertEquals(0, $process->getTermSignal());
681    }
682
683    public function testProcessIsSignaledIfStopped()
684    {
685        if ('\\' === \DIRECTORY_SEPARATOR) {
686            $this->markTestSkipped('Windows does not support POSIX signals');
687        }
688        $this->skipIfNotEnhancedSigchild();
689
690        $process = $this->getProcessForCode('sleep(32);');
691        $process->start();
692        $process->stop();
693        $this->assertTrue($process->hasBeenSignaled());
694        $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
695    }
696
697    public function testProcessThrowsExceptionWhenExternallySignaled()
698    {
699        $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
700        $this->expectExceptionMessage('The process has been signaled');
701        if (!\function_exists('posix_kill')) {
702            $this->markTestSkipped('Function posix_kill is required.');
703        }
704        $this->skipIfNotEnhancedSigchild(false);
705
706        $process = $this->getProcessForCode('sleep(32.1);');
707        $process->start();
708        posix_kill($process->getPid(), 9); // SIGKILL
709
710        $process->wait();
711    }
712
713    public function testRestart()
714    {
715        $process1 = $this->getProcessForCode('echo getmypid();');
716        $process1->run();
717        $process2 = $process1->restart();
718
719        $process2->wait(); // wait for output
720
721        // Ensure that both processed finished and the output is numeric
722        $this->assertFalse($process1->isRunning());
723        $this->assertFalse($process2->isRunning());
724        $this->assertIsNumeric($process1->getOutput());
725        $this->assertIsNumeric($process2->getOutput());
726
727        // Ensure that restart returned a new process by check that the output is different
728        $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
729    }
730
731    public function testRunProcessWithTimeout()
732    {
733        $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException');
734        $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
735        $process = $this->getProcessForCode('sleep(30);');
736        $process->setTimeout(0.1);
737        $start = microtime(true);
738        try {
739            $process->run();
740            $this->fail('A RuntimeException should have been raised');
741        } catch (RuntimeException $e) {
742        }
743
744        $this->assertLessThan(15, microtime(true) - $start);
745
746        throw $e;
747    }
748
749    public function testIterateOverProcessWithTimeout()
750    {
751        $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException');
752        $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
753        $process = $this->getProcessForCode('sleep(30);');
754        $process->setTimeout(0.1);
755        $start = microtime(true);
756        try {
757            $process->start();
758            foreach ($process as $buffer);
759            $this->fail('A RuntimeException should have been raised');
760        } catch (RuntimeException $e) {
761        }
762
763        $this->assertLessThan(15, microtime(true) - $start);
764
765        throw $e;
766    }
767
768    public function testCheckTimeoutOnNonStartedProcess()
769    {
770        $process = $this->getProcess('echo foo');
771        $this->assertNull($process->checkTimeout());
772    }
773
774    public function testCheckTimeoutOnTerminatedProcess()
775    {
776        $process = $this->getProcess('echo foo');
777        $process->run();
778        $this->assertNull($process->checkTimeout());
779    }
780
781    public function testCheckTimeoutOnStartedProcess()
782    {
783        $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException');
784        $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
785        $process = $this->getProcessForCode('sleep(33);');
786        $process->setTimeout(0.1);
787
788        $process->start();
789        $start = microtime(true);
790
791        try {
792            while ($process->isRunning()) {
793                $process->checkTimeout();
794                usleep(100000);
795            }
796            $this->fail('A ProcessTimedOutException should have been raised');
797        } catch (ProcessTimedOutException $e) {
798        }
799
800        $this->assertLessThan(15, microtime(true) - $start);
801
802        throw $e;
803    }
804
805    public function testIdleTimeout()
806    {
807        $process = $this->getProcessForCode('sleep(34);');
808        $process->setTimeout(60);
809        $process->setIdleTimeout(0.1);
810
811        try {
812            $process->run();
813
814            $this->fail('A timeout exception was expected.');
815        } catch (ProcessTimedOutException $e) {
816            $this->assertTrue($e->isIdleTimeout());
817            $this->assertFalse($e->isGeneralTimeout());
818            $this->assertEquals(0.1, $e->getExceededTimeout());
819        }
820    }
821
822    public function testIdleTimeoutNotExceededWhenOutputIsSent()
823    {
824        $process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}');
825        $process->setTimeout(1);
826        $process->start();
827
828        while (false === strpos($process->getOutput(), 'foo')) {
829            usleep(1000);
830        }
831
832        $process->setIdleTimeout(0.5);
833
834        try {
835            $process->wait();
836            $this->fail('A timeout exception was expected.');
837        } catch (ProcessTimedOutException $e) {
838            $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
839            $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
840            $this->assertEquals(1, $e->getExceededTimeout());
841        }
842    }
843
844    public function testStartAfterATimeout()
845    {
846        $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException');
847        $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
848        $process = $this->getProcessForCode('sleep(35);');
849        $process->setTimeout(0.1);
850
851        try {
852            $process->run();
853            $this->fail('A ProcessTimedOutException should have been raised.');
854        } catch (ProcessTimedOutException $e) {
855        }
856        $this->assertFalse($process->isRunning());
857        $process->start();
858        $this->assertTrue($process->isRunning());
859        $process->stop(0);
860
861        throw $e;
862    }
863
864    public function testGetPid()
865    {
866        $process = $this->getProcessForCode('sleep(36);');
867        $process->start();
868        $this->assertGreaterThan(0, $process->getPid());
869        $process->stop(0);
870    }
871
872    public function testGetPidIsNullBeforeStart()
873    {
874        $process = $this->getProcess('foo');
875        $this->assertNull($process->getPid());
876    }
877
878    public function testGetPidIsNullAfterRun()
879    {
880        $process = $this->getProcess('echo foo');
881        $process->run();
882        $this->assertNull($process->getPid());
883    }
884
885    /**
886     * @requires extension pcntl
887     */
888    public function testSignal()
889    {
890        $process = $this->getProcess([self::$phpBin, __DIR__.'/SignalListener.php']);
891        $process->start();
892
893        while (false === strpos($process->getOutput(), 'Caught')) {
894            usleep(1000);
895        }
896        $process->signal(\SIGUSR1);
897        $process->wait();
898
899        $this->assertEquals('Caught SIGUSR1', $process->getOutput());
900    }
901
902    /**
903     * @requires extension pcntl
904     */
905    public function testExitCodeIsAvailableAfterSignal()
906    {
907        $this->skipIfNotEnhancedSigchild();
908
909        $process = $this->getProcess('sleep 4');
910        $process->start();
911        $process->signal(\SIGKILL);
912
913        while ($process->isRunning()) {
914            usleep(10000);
915        }
916
917        $this->assertFalse($process->isRunning());
918        $this->assertTrue($process->hasBeenSignaled());
919        $this->assertFalse($process->isSuccessful());
920        $this->assertEquals(137, $process->getExitCode());
921    }
922
923    public function testSignalProcessNotRunning()
924    {
925        $this->expectException('Symfony\Component\Process\Exception\LogicException');
926        $this->expectExceptionMessage('Can not send signal on a non running process.');
927        $process = $this->getProcess('foo');
928        $process->signal(1); // SIGHUP
929    }
930
931    /**
932     * @dataProvider provideMethodsThatNeedARunningProcess
933     */
934    public function testMethodsThatNeedARunningProcess($method)
935    {
936        $process = $this->getProcess('foo');
937
938        $this->expectException('Symfony\Component\Process\Exception\LogicException');
939        $this->expectExceptionMessage(sprintf('Process must be started before calling "%s()".', $method));
940
941        $process->{$method}();
942    }
943
944    public function provideMethodsThatNeedARunningProcess()
945    {
946        return [
947            ['getOutput'],
948            ['getIncrementalOutput'],
949            ['getErrorOutput'],
950            ['getIncrementalErrorOutput'],
951            ['wait'],
952        ];
953    }
954
955    /**
956     * @dataProvider provideMethodsThatNeedATerminatedProcess
957     */
958    public function testMethodsThatNeedATerminatedProcess($method)
959    {
960        $this->expectException('Symfony\Component\Process\Exception\LogicException');
961        $this->expectExceptionMessage('Process must be terminated before calling');
962        $process = $this->getProcessForCode('sleep(37);');
963        $process->start();
964        try {
965            $process->{$method}();
966            $process->stop(0);
967            $this->fail('A LogicException must have been thrown');
968        } catch (\Exception $e) {
969        }
970        $process->stop(0);
971
972        throw $e;
973    }
974
975    public function provideMethodsThatNeedATerminatedProcess()
976    {
977        return [
978            ['hasBeenSignaled'],
979            ['getTermSignal'],
980            ['hasBeenStopped'],
981            ['getStopSignal'],
982        ];
983    }
984
985    /**
986     * @dataProvider provideWrongSignal
987     */
988    public function testWrongSignal($signal)
989    {
990        if ('\\' === \DIRECTORY_SEPARATOR) {
991            $this->markTestSkipped('POSIX signals do not work on Windows');
992        }
993
994        if (\PHP_VERSION_ID < 80000 || \is_int($signal)) {
995            $this->expectException(RuntimeException::class);
996        } else {
997            $this->expectException('TypeError');
998        }
999
1000        $process = $this->getProcessForCode('sleep(38);');
1001        $process->start();
1002        try {
1003            $process->signal($signal);
1004            $this->fail('A RuntimeException must have been thrown');
1005        } catch (\TypeError $e) {
1006            $process->stop(0);
1007        } catch (RuntimeException $e) {
1008            $process->stop(0);
1009        }
1010
1011        throw $e;
1012    }
1013
1014    public function provideWrongSignal()
1015    {
1016        return [
1017            [-4],
1018            ['Céphalopodes'],
1019        ];
1020    }
1021
1022    public function testDisableOutputDisablesTheOutput()
1023    {
1024        $p = $this->getProcess('foo');
1025        $this->assertFalse($p->isOutputDisabled());
1026        $p->disableOutput();
1027        $this->assertTrue($p->isOutputDisabled());
1028        $p->enableOutput();
1029        $this->assertFalse($p->isOutputDisabled());
1030    }
1031
1032    public function testDisableOutputWhileRunningThrowsException()
1033    {
1034        $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1035        $this->expectExceptionMessage('Disabling output while the process is running is not possible.');
1036        $p = $this->getProcessForCode('sleep(39);');
1037        $p->start();
1038        $p->disableOutput();
1039    }
1040
1041    public function testEnableOutputWhileRunningThrowsException()
1042    {
1043        $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1044        $this->expectExceptionMessage('Enabling output while the process is running is not possible.');
1045        $p = $this->getProcessForCode('sleep(40);');
1046        $p->disableOutput();
1047        $p->start();
1048        $p->enableOutput();
1049    }
1050
1051    public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
1052    {
1053        $p = $this->getProcess('echo foo');
1054        $p->disableOutput();
1055        $p->run();
1056        $p->enableOutput();
1057        $p->disableOutput();
1058        $this->assertTrue($p->isOutputDisabled());
1059    }
1060
1061    public function testDisableOutputWhileIdleTimeoutIsSet()
1062    {
1063        $this->expectException('Symfony\Component\Process\Exception\LogicException');
1064        $this->expectExceptionMessage('Output can not be disabled while an idle timeout is set.');
1065        $process = $this->getProcess('foo');
1066        $process->setIdleTimeout(1);
1067        $process->disableOutput();
1068    }
1069
1070    public function testSetIdleTimeoutWhileOutputIsDisabled()
1071    {
1072        $this->expectException('Symfony\Component\Process\Exception\LogicException');
1073        $this->expectExceptionMessage('timeout can not be set while the output is disabled.');
1074        $process = $this->getProcess('foo');
1075        $process->disableOutput();
1076        $process->setIdleTimeout(1);
1077    }
1078
1079    public function testSetNullIdleTimeoutWhileOutputIsDisabled()
1080    {
1081        $process = $this->getProcess('foo');
1082        $process->disableOutput();
1083        $this->assertSame($process, $process->setIdleTimeout(null));
1084    }
1085
1086    /**
1087     * @dataProvider provideOutputFetchingMethods
1088     */
1089    public function testGetOutputWhileDisabled($fetchMethod)
1090    {
1091        $this->expectException('Symfony\Component\Process\Exception\LogicException');
1092        $this->expectExceptionMessage('Output has been disabled.');
1093        $p = $this->getProcessForCode('sleep(41);');
1094        $p->disableOutput();
1095        $p->start();
1096        $p->{$fetchMethod}();
1097    }
1098
1099    public function provideOutputFetchingMethods()
1100    {
1101        return [
1102            ['getOutput'],
1103            ['getIncrementalOutput'],
1104            ['getErrorOutput'],
1105            ['getIncrementalErrorOutput'],
1106        ];
1107    }
1108
1109    public function testStopTerminatesProcessCleanly()
1110    {
1111        $process = $this->getProcessForCode('echo 123; sleep(42);');
1112        $process->run(function () use ($process) {
1113            $process->stop();
1114        });
1115        $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
1116    }
1117
1118    public function testKillSignalTerminatesProcessCleanly()
1119    {
1120        $process = $this->getProcessForCode('echo 123; sleep(43);');
1121        $process->run(function () use ($process) {
1122            $process->signal(9); // SIGKILL
1123        });
1124        $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1125    }
1126
1127    public function testTermSignalTerminatesProcessCleanly()
1128    {
1129        $process = $this->getProcessForCode('echo 123; sleep(44);');
1130        $process->run(function () use ($process) {
1131            $process->signal(15); // SIGTERM
1132        });
1133        $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1134    }
1135
1136    public function responsesCodeProvider()
1137    {
1138        return [
1139            //expected output / getter / code to execute
1140            //[1,'getExitCode','exit(1);'],
1141            //[true,'isSuccessful','exit();'],
1142            ['output', 'getOutput', 'echo \'output\';'],
1143        ];
1144    }
1145
1146    public function pipesCodeProvider()
1147    {
1148        $variations = [
1149            'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
1150            'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
1151        ];
1152
1153        if ('\\' === \DIRECTORY_SEPARATOR) {
1154            // Avoid XL buffers on Windows because of https://bugs.php.net/65650
1155            $sizes = [1, 2, 4, 8];
1156        } else {
1157            $sizes = [1, 16, 64, 1024, 4096];
1158        }
1159
1160        $codes = [];
1161        foreach ($sizes as $size) {
1162            foreach ($variations as $code) {
1163                $codes[] = [$code, $size];
1164            }
1165        }
1166
1167        return $codes;
1168    }
1169
1170    /**
1171     * @dataProvider provideVariousIncrementals
1172     */
1173    public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
1174    {
1175        $process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null);
1176        $process->start();
1177        $result = '';
1178        $limit = microtime(true) + 3;
1179        $expected = '012';
1180
1181        while ($result !== $expected && microtime(true) < $limit) {
1182            $result .= $process->$method();
1183        }
1184
1185        $this->assertSame($expected, $result);
1186        $process->stop();
1187    }
1188
1189    public function provideVariousIncrementals()
1190    {
1191        return [
1192            ['php://stdout', 'getIncrementalOutput'],
1193            ['php://stderr', 'getIncrementalErrorOutput'],
1194        ];
1195    }
1196
1197    public function testIteratorInput()
1198    {
1199        $input = function () {
1200            yield 'ping';
1201            yield 'pong';
1202        };
1203
1204        $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input());
1205        $process->run();
1206        $this->assertSame('pingpong', $process->getOutput());
1207    }
1208
1209    public function testSimpleInputStream()
1210    {
1211        $input = new InputStream();
1212
1213        $process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);');
1214        $process->setInput($input);
1215
1216        $process->start(function ($type, $data) use ($input) {
1217            if ('ping' === $data) {
1218                $input->write('pang');
1219            } elseif (!$input->isClosed()) {
1220                $input->write('pong');
1221                $input->close();
1222            }
1223        });
1224
1225        $process->wait();
1226        $this->assertSame('pingpangpong', $process->getOutput());
1227    }
1228
1229    public function testInputStreamWithCallable()
1230    {
1231        $i = 0;
1232        $stream = fopen('php://memory', 'w+');
1233        $stream = function () use ($stream, &$i) {
1234            if ($i < 3) {
1235                rewind($stream);
1236                fwrite($stream, ++$i);
1237                rewind($stream);
1238
1239                return $stream;
1240            }
1241
1242            return null;
1243        };
1244
1245        $input = new InputStream();
1246        $input->onEmpty($stream);
1247        $input->write($stream());
1248
1249        $process = $this->getProcessForCode('echo fread(STDIN, 3);');
1250        $process->setInput($input);
1251        $process->start(function ($type, $data) use ($input) {
1252            $input->close();
1253        });
1254
1255        $process->wait();
1256        $this->assertSame('123', $process->getOutput());
1257    }
1258
1259    public function testInputStreamWithGenerator()
1260    {
1261        $input = new InputStream();
1262        $input->onEmpty(function ($input) {
1263            yield 'pong';
1264            $input->close();
1265        });
1266
1267        $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
1268        $process->setInput($input);
1269        $process->start();
1270        $input->write('ping');
1271        $process->wait();
1272        $this->assertSame('pingpong', $process->getOutput());
1273    }
1274
1275    public function testInputStreamOnEmpty()
1276    {
1277        $i = 0;
1278        $input = new InputStream();
1279        $input->onEmpty(function () use (&$i) { ++$i; });
1280
1281        $process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;');
1282        $process->setInput($input);
1283        $process->start(function ($type, $data) use ($input) {
1284            if ('123' === $data) {
1285                $input->close();
1286            }
1287        });
1288        $process->wait();
1289
1290        $this->assertSame(0, $i, 'InputStream->onEmpty callback should be called only when the input *becomes* empty');
1291        $this->assertSame('123456', $process->getOutput());
1292    }
1293
1294    public function testIteratorOutput()
1295    {
1296        $input = new InputStream();
1297
1298        $process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);');
1299        $process->setInput($input);
1300        $process->start();
1301        $output = [];
1302
1303        foreach ($process as $type => $data) {
1304            $output[] = [$type, $data];
1305            break;
1306        }
1307        $expectedOutput = [
1308            [$process::OUT, '123'],
1309        ];
1310        $this->assertSame($expectedOutput, $output);
1311
1312        $input->write(345);
1313
1314        foreach ($process as $type => $data) {
1315            $output[] = [$type, $data];
1316        }
1317
1318        $this->assertSame('', $process->getOutput());
1319        $this->assertFalse($process->isRunning());
1320
1321        $expectedOutput = [
1322            [$process::OUT, '123'],
1323            [$process::ERR, '234'],
1324            [$process::OUT, '345'],
1325            [$process::ERR, '456'],
1326        ];
1327        $this->assertSame($expectedOutput, $output);
1328    }
1329
1330    public function testNonBlockingNorClearingIteratorOutput()
1331    {
1332        $input = new InputStream();
1333
1334        $process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));');
1335        $process->setInput($input);
1336        $process->start();
1337        $output = [];
1338
1339        foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
1340            $output[] = [$type, $data];
1341            break;
1342        }
1343        $expectedOutput = [
1344            [$process::OUT, ''],
1345        ];
1346        $this->assertSame($expectedOutput, $output);
1347
1348        $input->write(123);
1349
1350        foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
1351            if ('' !== $data) {
1352                $output[] = [$type, $data];
1353            }
1354        }
1355
1356        $this->assertSame('123', $process->getOutput());
1357        $this->assertFalse($process->isRunning());
1358
1359        $expectedOutput = [
1360            [$process::OUT, ''],
1361            [$process::OUT, '123'],
1362        ];
1363        $this->assertSame($expectedOutput, $output);
1364    }
1365
1366    public function testChainedProcesses()
1367    {
1368        $p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);');
1369        $p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
1370        $p2->setInput($p1);
1371
1372        $p1->start();
1373        $p2->run();
1374
1375        $this->assertSame('123', $p1->getErrorOutput());
1376        $this->assertSame('', $p1->getOutput());
1377        $this->assertSame('', $p2->getErrorOutput());
1378        $this->assertSame('456', $p2->getOutput());
1379    }
1380
1381    public function testSetBadEnv()
1382    {
1383        $process = $this->getProcess('echo hello');
1384        $process->setEnv(['bad%%' => '123']);
1385        $process->inheritEnvironmentVariables(true);
1386
1387        $process->run();
1388
1389        $this->assertSame('hello'.\PHP_EOL, $process->getOutput());
1390        $this->assertSame('', $process->getErrorOutput());
1391    }
1392
1393    public function testEnvBackupDoesNotDeleteExistingVars()
1394    {
1395        putenv('existing_var=foo');
1396        $_ENV['existing_var'] = 'foo';
1397        $process = $this->getProcess('php -r "echo getenv(\'new_test_var\');"');
1398        $process->setEnv(['existing_var' => 'bar', 'new_test_var' => 'foo']);
1399        $process->inheritEnvironmentVariables();
1400
1401        $process->run();
1402
1403        $this->assertSame('foo', $process->getOutput());
1404        $this->assertSame('foo', getenv('existing_var'));
1405        $this->assertFalse(getenv('new_test_var'));
1406
1407        putenv('existing_var');
1408        unset($_ENV['existing_var']);
1409    }
1410
1411    public function testEnvIsInherited()
1412    {
1413        $process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ', 'EMPTY' => '']);
1414
1415        putenv('FOO=BAR');
1416        $_ENV['FOO'] = 'BAR';
1417
1418        $process->run();
1419
1420        $expected = ['BAR' => 'BAZ', 'EMPTY' => '', 'FOO' => 'BAR'];
1421        $env = array_intersect_key(unserialize($process->getOutput()), $expected);
1422
1423        $this->assertEquals($expected, $env);
1424
1425        putenv('FOO');
1426        unset($_ENV['FOO']);
1427    }
1428
1429    /**
1430     * @group legacy
1431     */
1432    public function testInheritEnvDisabled()
1433    {
1434        $process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ']);
1435
1436        putenv('FOO=BAR');
1437        $_ENV['FOO'] = 'BAR';
1438
1439        $this->assertSame($process, $process->inheritEnvironmentVariables(false));
1440        $this->assertFalse($process->areEnvironmentVariablesInherited());
1441
1442        $process->run();
1443
1444        $expected = ['BAR' => 'BAZ', 'FOO' => 'BAR'];
1445        $env = array_intersect_key(unserialize($process->getOutput()), $expected);
1446        unset($expected['FOO']);
1447
1448        $this->assertSame($expected, $env);
1449
1450        putenv('FOO');
1451        unset($_ENV['FOO']);
1452    }
1453
1454    public function testGetCommandLine()
1455    {
1456        $p = new Process(['/usr/bin/php']);
1457
1458        $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'";
1459        $this->assertSame($expected, $p->getCommandLine());
1460    }
1461
1462    /**
1463     * @dataProvider provideEscapeArgument
1464     */
1465    public function testEscapeArgument($arg)
1466    {
1467        $p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg]);
1468        $p->run();
1469
1470        $this->assertSame((string) $arg, $p->getOutput());
1471    }
1472
1473    /**
1474     * @dataProvider provideEscapeArgument
1475     * @group legacy
1476     */
1477    public function testEscapeArgumentWhenInheritEnvDisabled($arg)
1478    {
1479        $p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg], null, ['BAR' => 'BAZ']);
1480        $p->inheritEnvironmentVariables(false);
1481        $p->run();
1482
1483        $this->assertSame((string) $arg, $p->getOutput());
1484    }
1485
1486    public function testRawCommandLine()
1487    {
1488        $p = new Process(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);')));
1489        $p->run();
1490
1491        $expected = <<<EOTXT
1492Array
1493(
1494    [0] => -
1495    [1] => a
1496    [2] =>
1497    [3] => b
1498)
1499
1500EOTXT;
1501        $this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput()));
1502    }
1503
1504    public function provideEscapeArgument()
1505    {
1506        yield ['a"b%c%'];
1507        yield ['a"b^c^'];
1508        yield ["a\nb'c"];
1509        yield ['a^b c!'];
1510        yield ["a!b\tc"];
1511        yield ['a\\\\"\\"'];
1512        yield ['éÉèÈàÀöä'];
1513        yield [null];
1514        yield [1];
1515        yield [1.1];
1516    }
1517
1518    public function testEnvArgument()
1519    {
1520        $env = ['FOO' => 'Foo', 'BAR' => 'Bar'];
1521        $cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ';
1522        $p = new Process($cmd, null, $env);
1523        $p->run(null, ['BAR' => 'baR', 'BAZ' => 'baZ']);
1524
1525        $this->assertSame('Foo baR baZ', rtrim($p->getOutput()));
1526        $this->assertSame($env, $p->getEnv());
1527    }
1528
1529    public function testWaitStoppedDeadProcess()
1530    {
1531        $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/ErrorProcessInitiator.php -e '.self::$phpBin);
1532        $process->start();
1533        $process->setTimeout(2);
1534        $process->wait();
1535        $this->assertFalse($process->isRunning());
1536    }
1537
1538    /**
1539     * @param string      $commandline
1540     * @param string|null $cwd
1541     * @param string|null $input
1542     * @param int         $timeout
1543     *
1544     * @return Process
1545     */
1546    private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60)
1547    {
1548        $process = new Process($commandline, $cwd, $env, $input, $timeout);
1549        $process->inheritEnvironmentVariables();
1550
1551        if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
1552            try {
1553                $process->setEnhanceSigchildCompatibility(false);
1554                $process->getExitCode();
1555                $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
1556            } catch (RuntimeException $e) {
1557                $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
1558                if ($enhance) {
1559                    $process->setEnhanceSigchildCompatibility(true);
1560                } else {
1561                    self::$notEnhancedSigchild = true;
1562                }
1563            }
1564        }
1565
1566        if (self::$process) {
1567            self::$process->stop(0);
1568        }
1569
1570        return self::$process = $process;
1571    }
1572
1573    /**
1574     * @return Process
1575     */
1576    private function getProcessForCode($code, $cwd = null, array $env = null, $input = null, $timeout = 60)
1577    {
1578        return $this->getProcess([self::$phpBin, '-r', $code], $cwd, $env, $input, $timeout);
1579    }
1580
1581    private function skipIfNotEnhancedSigchild($expectException = true)
1582    {
1583        if (self::$sigchild) {
1584            if (!$expectException) {
1585                $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
1586            } elseif (self::$notEnhancedSigchild) {
1587                $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1588                $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.');
1589            }
1590        }
1591    }
1592}
1593
1594class NonStringifiable
1595{
1596}
1597