Yaffs site version 1.1
[yaffs-website] / vendor / symfony / process / Tests / ProcessTest.php
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
12 namespace Symfony\Component\Process\Tests;
13
14 use PHPUnit\Framework\TestCase;
15 use Symfony\Component\Process\Exception\LogicException;
16 use Symfony\Component\Process\Exception\ProcessTimedOutException;
17 use Symfony\Component\Process\Exception\RuntimeException;
18 use Symfony\Component\Process\PhpExecutableFinder;
19 use Symfony\Component\Process\Pipes\PipesInterface;
20 use Symfony\Component\Process\Process;
21
22 /**
23  * @author Robert Schönthal <seroscho@googlemail.com>
24  */
25 class ProcessTest extends TestCase
26 {
27     private static $phpBin;
28     private static $process;
29     private static $sigchild;
30     private static $notEnhancedSigchild = false;
31
32     public static function setUpBeforeClass()
33     {
34         $phpBin = new PhpExecutableFinder();
35         self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === PHP_SAPI ? 'php' : $phpBin->find());
36         if ('\\' !== DIRECTORY_SEPARATOR) {
37             // exec is mandatory to deal with sending a signal to the process
38             // see https://github.com/symfony/symfony/issues/5030 about prepending
39             // command with exec
40             self::$phpBin = 'exec '.self::$phpBin;
41         }
42
43         ob_start();
44         phpinfo(INFO_GENERAL);
45         self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
46     }
47
48     protected function tearDown()
49     {
50         if (self::$process) {
51             self::$process->stop(0);
52             self::$process = null;
53         }
54     }
55
56     public function testThatProcessDoesNotThrowWarningDuringRun()
57     {
58         if ('\\' === DIRECTORY_SEPARATOR) {
59             $this->markTestSkipped('This test is transient on Windows');
60         }
61         @trigger_error('Test Error', E_USER_NOTICE);
62         $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'");
63         $process->run();
64         $actualError = error_get_last();
65         $this->assertEquals('Test Error', $actualError['message']);
66         $this->assertEquals(E_USER_NOTICE, $actualError['type']);
67     }
68
69     /**
70      * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
71      */
72     public function testNegativeTimeoutFromConstructor()
73     {
74         $this->getProcess('', null, null, null, -1);
75     }
76
77     /**
78      * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
79      */
80     public function testNegativeTimeoutFromSetter()
81     {
82         $p = $this->getProcess('');
83         $p->setTimeout(-1);
84     }
85
86     public function testFloatAndNullTimeout()
87     {
88         $p = $this->getProcess('');
89
90         $p->setTimeout(10);
91         $this->assertSame(10.0, $p->getTimeout());
92
93         $p->setTimeout(null);
94         $this->assertNull($p->getTimeout());
95
96         $p->setTimeout(0.0);
97         $this->assertNull($p->getTimeout());
98     }
99
100     /**
101      * @requires extension pcntl
102      */
103     public function testStopWithTimeoutIsActuallyWorking()
104     {
105         $p = $this->getProcess(self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 30');
106         $p->start();
107
108         while (false === strpos($p->getOutput(), 'received')) {
109             usleep(1000);
110         }
111         $start = microtime(true);
112         $p->stop(0.1);
113
114         $p->wait();
115
116         $this->assertLessThan(15, microtime(true) - $start);
117     }
118
119     public function testAllOutputIsActuallyReadOnTermination()
120     {
121         // this code will result in a maximum of 2 reads of 8192 bytes by calling
122         // start() and isRunning().  by the time getOutput() is called the process
123         // has terminated so the internal pipes array is already empty. normally
124         // the call to start() will not read any data as the process will not have
125         // generated output, but this is non-deterministic so we must count it as
126         // a possibility.  therefore we need 2 * PipesInterface::CHUNK_SIZE plus
127         // another byte which will never be read.
128         $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2;
129
130         $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
131         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
132
133         $p->start();
134
135         // Don't call Process::run nor Process::wait to avoid any read of pipes
136         $h = new \ReflectionProperty($p, 'process');
137         $h->setAccessible(true);
138         $h = $h->getValue($p);
139         $s = @proc_get_status($h);
140
141         while (!empty($s['running'])) {
142             usleep(1000);
143             $s = proc_get_status($h);
144         }
145
146         $o = $p->getOutput();
147
148         $this->assertEquals($expectedOutputSize, strlen($o));
149     }
150
151     public function testCallbacksAreExecutedWithStart()
152     {
153         $process = $this->getProcess('echo foo');
154         $process->start(function ($type, $buffer) use (&$data) {
155             $data .= $buffer;
156         });
157
158         $process->wait();
159
160         $this->assertSame('foo'.PHP_EOL, $data);
161     }
162
163     /**
164      * tests results from sub processes.
165      *
166      * @dataProvider responsesCodeProvider
167      */
168     public function testProcessResponses($expected, $getter, $code)
169     {
170         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
171         $p->run();
172
173         $this->assertSame($expected, $p->$getter());
174     }
175
176     /**
177      * tests results from sub processes.
178      *
179      * @dataProvider pipesCodeProvider
180      */
181     public function testProcessPipes($code, $size)
182     {
183         $expected = str_repeat(str_repeat('*', 1024), $size).'!';
184         $expectedLength = (1024 * $size) + 1;
185
186         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
187         $p->setInput($expected);
188         $p->run();
189
190         $this->assertEquals($expectedLength, strlen($p->getOutput()));
191         $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
192     }
193
194     /**
195      * @dataProvider pipesCodeProvider
196      */
197     public function testSetStreamAsInput($code, $size)
198     {
199         $expected = str_repeat(str_repeat('*', 1024), $size).'!';
200         $expectedLength = (1024 * $size) + 1;
201
202         $stream = fopen('php://temporary', 'w+');
203         fwrite($stream, $expected);
204         rewind($stream);
205
206         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
207         $p->setInput($stream);
208         $p->run();
209
210         fclose($stream);
211
212         $this->assertEquals($expectedLength, strlen($p->getOutput()));
213         $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
214     }
215
216     public function testLiveStreamAsInput()
217     {
218         $stream = fopen('php://memory', 'r+');
219         fwrite($stream, 'hello');
220         rewind($stream);
221
222         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);')));
223         $p->setInput($stream);
224         $p->start(function ($type, $data) use ($stream) {
225             if ('hello' === $data) {
226                 fclose($stream);
227             }
228         });
229         $p->wait();
230
231         $this->assertSame('hello', $p->getOutput());
232     }
233
234     /**
235      * @expectedException \Symfony\Component\Process\Exception\LogicException
236      * @expectedExceptionMessage Input can not be set while the process is running.
237      */
238     public function testSetInputWhileRunningThrowsAnException()
239     {
240         $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
241         $process->start();
242         try {
243             $process->setInput('foobar');
244             $process->stop();
245             $this->fail('A LogicException should have been raised.');
246         } catch (LogicException $e) {
247         }
248         $process->stop();
249
250         throw $e;
251     }
252
253     /**
254      * @dataProvider provideInvalidInputValues
255      * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
256      * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings or stream resources.
257      */
258     public function testInvalidInput($value)
259     {
260         $process = $this->getProcess('foo');
261         $process->setInput($value);
262     }
263
264     public function provideInvalidInputValues()
265     {
266         return array(
267             array(array()),
268             array(new NonStringifiable()),
269         );
270     }
271
272     /**
273      * @dataProvider provideInputValues
274      */
275     public function testValidInput($expected, $value)
276     {
277         $process = $this->getProcess('foo');
278         $process->setInput($value);
279         $this->assertSame($expected, $process->getInput());
280     }
281
282     public function provideInputValues()
283     {
284         return array(
285             array(null, null),
286             array('24.5', 24.5),
287             array('input data', 'input data'),
288         );
289     }
290
291     /**
292      * @dataProvider provideLegacyInputValues
293      * @group legacy
294      */
295     public function testLegacyValidInput($expected, $value)
296     {
297         $process = $this->getProcess(self::$phpBin.' -v');
298         $process->setInput($value);
299         $this->assertSame($expected, $process->getInput());
300     }
301
302     public function provideLegacyInputValues()
303     {
304         return array(
305             array('stringifiable', new Stringifiable()),
306         );
307     }
308
309     public function chainedCommandsOutputProvider()
310     {
311         if ('\\' === DIRECTORY_SEPARATOR) {
312             return array(
313                 array("2 \r\n2\r\n", '&&', '2'),
314             );
315         }
316
317         return array(
318             array("1\n1\n", ';', '1'),
319             array("2\n2\n", '&&', '2'),
320         );
321     }
322
323     /**
324      * @dataProvider chainedCommandsOutputProvider
325      */
326     public function testChainedCommandsOutput($expected, $operator, $input)
327     {
328         $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input));
329         $process->run();
330         $this->assertEquals($expected, $process->getOutput());
331     }
332
333     public function testCallbackIsExecutedForOutput()
334     {
335         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';')));
336
337         $called = false;
338         $p->run(function ($type, $buffer) use (&$called) {
339             $called = $buffer === 'foo';
340         });
341
342         $this->assertTrue($called, 'The callback should be executed with the output');
343     }
344
345     public function testGetErrorOutput()
346     {
347         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
348
349         $p->run();
350         $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
351     }
352
353     public function testFlushErrorOutput()
354     {
355         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
356
357         $p->run();
358         $p->clearErrorOutput();
359         $this->assertEmpty($p->getErrorOutput());
360     }
361
362     /**
363      * @dataProvider provideIncrementalOutput
364      */
365     public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
366     {
367         $lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
368
369         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');')));
370
371         $h = fopen($lock, 'w');
372         flock($h, LOCK_EX);
373
374         $p->start();
375
376         foreach (array('foo', 'bar') as $s) {
377             while (false === strpos($p->$getOutput(), $s)) {
378                 usleep(1000);
379             }
380
381             $this->assertSame($s, $p->$getIncrementalOutput());
382             $this->assertSame('', $p->$getIncrementalOutput());
383
384             flock($h, LOCK_UN);
385         }
386
387         fclose($h);
388     }
389
390     public function provideIncrementalOutput()
391     {
392         return array(
393             array('getOutput', 'getIncrementalOutput', 'php://stdout'),
394             array('getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'),
395         );
396     }
397
398     public function testGetOutput()
399     {
400         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }')));
401
402         $p->run();
403         $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
404     }
405
406     public function testFlushOutput()
407     {
408         $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}')));
409
410         $p->run();
411         $p->clearOutput();
412         $this->assertEmpty($p->getOutput());
413     }
414
415     public function testZeroAsOutput()
416     {
417         if ('\\' === DIRECTORY_SEPARATOR) {
418             // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
419             $p = $this->getProcess('echo | set /p dummyName=0');
420         } else {
421             $p = $this->getProcess('printf 0');
422         }
423
424         $p->run();
425         $this->assertSame('0', $p->getOutput());
426     }
427
428     public function testExitCodeCommandFailed()
429     {
430         if ('\\' === DIRECTORY_SEPARATOR) {
431             $this->markTestSkipped('Windows does not support POSIX exit code');
432         }
433         $this->skipIfNotEnhancedSigchild();
434
435         // such command run in bash return an exitcode 127
436         $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
437         $process->run();
438
439         $this->assertGreaterThan(0, $process->getExitCode());
440     }
441
442     public function testTTYCommand()
443     {
444         if ('\\' === DIRECTORY_SEPARATOR) {
445             $this->markTestSkipped('Windows does not have /dev/tty support');
446         }
447
448         $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"');
449         $process->setTty(true);
450         $process->start();
451         $this->assertTrue($process->isRunning());
452         $process->wait();
453
454         $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
455     }
456
457     public function testTTYCommandExitCode()
458     {
459         if ('\\' === DIRECTORY_SEPARATOR) {
460             $this->markTestSkipped('Windows does have /dev/tty support');
461         }
462         $this->skipIfNotEnhancedSigchild();
463
464         $process = $this->getProcess('echo "foo" >> /dev/null');
465         $process->setTty(true);
466         $process->run();
467
468         $this->assertTrue($process->isSuccessful());
469     }
470
471     /**
472      * @expectedException \Symfony\Component\Process\Exception\RuntimeException
473      * @expectedExceptionMessage TTY mode is not supported on Windows platform.
474      */
475     public function testTTYInWindowsEnvironment()
476     {
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     /**
527      * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException
528      */
529     public function testMustRunThrowsException()
530     {
531         $this->skipIfNotEnhancedSigchild();
532
533         $process = $this->getProcess('exit 1');
534         $process->mustRun();
535     }
536
537     public function testExitCodeText()
538     {
539         $this->skipIfNotEnhancedSigchild();
540
541         $process = $this->getProcess('');
542         $r = new \ReflectionObject($process);
543         $p = $r->getProperty('exitcode');
544         $p->setAccessible(true);
545
546         $p->setValue($process, 2);
547         $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
548     }
549
550     public function testStartIsNonBlocking()
551     {
552         $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"');
553         $start = microtime(true);
554         $process->start();
555         $end = microtime(true);
556         $this->assertLessThan(0.4, $end - $start);
557         $process->stop();
558     }
559
560     public function testUpdateStatus()
561     {
562         $process = $this->getProcess('echo foo');
563         $process->run();
564         $this->assertTrue(strlen($process->getOutput()) > 0);
565     }
566
567     public function testGetExitCodeIsNullOnStart()
568     {
569         $this->skipIfNotEnhancedSigchild();
570
571         $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
572         $this->assertNull($process->getExitCode());
573         $process->start();
574         $this->assertNull($process->getExitCode());
575         $process->wait();
576         $this->assertEquals(0, $process->getExitCode());
577     }
578
579     public function testGetExitCodeIsNullOnWhenStartingAgain()
580     {
581         $this->skipIfNotEnhancedSigchild();
582
583         $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
584         $process->run();
585         $this->assertEquals(0, $process->getExitCode());
586         $process->start();
587         $this->assertNull($process->getExitCode());
588         $process->wait();
589         $this->assertEquals(0, $process->getExitCode());
590     }
591
592     public function testGetExitCode()
593     {
594         $this->skipIfNotEnhancedSigchild();
595
596         $process = $this->getProcess('echo foo');
597         $process->run();
598         $this->assertSame(0, $process->getExitCode());
599     }
600
601     public function testStatus()
602     {
603         $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
604         $this->assertFalse($process->isRunning());
605         $this->assertFalse($process->isStarted());
606         $this->assertFalse($process->isTerminated());
607         $this->assertSame(Process::STATUS_READY, $process->getStatus());
608         $process->start();
609         $this->assertTrue($process->isRunning());
610         $this->assertTrue($process->isStarted());
611         $this->assertFalse($process->isTerminated());
612         $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
613         $process->wait();
614         $this->assertFalse($process->isRunning());
615         $this->assertTrue($process->isStarted());
616         $this->assertTrue($process->isTerminated());
617         $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
618     }
619
620     public function testStop()
621     {
622         $process = $this->getProcess(self::$phpBin.' -r "sleep(31);"');
623         $process->start();
624         $this->assertTrue($process->isRunning());
625         $process->stop();
626         $this->assertFalse($process->isRunning());
627     }
628
629     public function testIsSuccessful()
630     {
631         $this->skipIfNotEnhancedSigchild();
632
633         $process = $this->getProcess('echo foo');
634         $process->run();
635         $this->assertTrue($process->isSuccessful());
636     }
637
638     public function testIsSuccessfulOnlyAfterTerminated()
639     {
640         $this->skipIfNotEnhancedSigchild();
641
642         $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
643         $process->start();
644
645         $this->assertFalse($process->isSuccessful());
646
647         $process->wait();
648
649         $this->assertTrue($process->isSuccessful());
650     }
651
652     public function testIsNotSuccessful()
653     {
654         $this->skipIfNotEnhancedSigchild();
655
656         $process = $this->getProcess(self::$phpBin.' -r "throw new \Exception(\'BOUM\');"');
657         $process->run();
658         $this->assertFalse($process->isSuccessful());
659     }
660
661     public function testProcessIsNotSignaled()
662     {
663         if ('\\' === DIRECTORY_SEPARATOR) {
664             $this->markTestSkipped('Windows does not support POSIX signals');
665         }
666         $this->skipIfNotEnhancedSigchild();
667
668         $process = $this->getProcess('echo foo');
669         $process->run();
670         $this->assertFalse($process->hasBeenSignaled());
671     }
672
673     public function testProcessWithoutTermSignal()
674     {
675         if ('\\' === DIRECTORY_SEPARATOR) {
676             $this->markTestSkipped('Windows does not support POSIX signals');
677         }
678         $this->skipIfNotEnhancedSigchild();
679
680         $process = $this->getProcess('echo foo');
681         $process->run();
682         $this->assertEquals(0, $process->getTermSignal());
683     }
684
685     public function testProcessIsSignaledIfStopped()
686     {
687         if ('\\' === DIRECTORY_SEPARATOR) {
688             $this->markTestSkipped('Windows does not support POSIX signals');
689         }
690         $this->skipIfNotEnhancedSigchild();
691
692         $process = $this->getProcess(self::$phpBin.' -r "sleep(32);"');
693         $process->start();
694         $process->stop();
695         $this->assertTrue($process->hasBeenSignaled());
696         $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
697     }
698
699     /**
700      * @expectedException \Symfony\Component\Process\Exception\RuntimeException
701      * @expectedExceptionMessage The process has been signaled
702      */
703     public function testProcessThrowsExceptionWhenExternallySignaled()
704     {
705         if (!function_exists('posix_kill')) {
706             $this->markTestSkipped('Function posix_kill is required.');
707         }
708         $this->skipIfNotEnhancedSigchild(false);
709
710         $process = $this->getProcess(self::$phpBin.' -r "sleep(32.1)"');
711         $process->start();
712         posix_kill($process->getPid(), 9); // SIGKILL
713
714         $process->wait();
715     }
716
717     public function testRestart()
718     {
719         $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"');
720         $process1->run();
721         $process2 = $process1->restart();
722
723         $process2->wait(); // wait for output
724
725         // Ensure that both processed finished and the output is numeric
726         $this->assertFalse($process1->isRunning());
727         $this->assertFalse($process2->isRunning());
728         $this->assertInternalType('numeric', $process1->getOutput());
729         $this->assertInternalType('numeric', $process2->getOutput());
730
731         // Ensure that restart returned a new process by check that the output is different
732         $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
733     }
734
735     /**
736      * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
737      * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
738      */
739     public function testRunProcessWithTimeout()
740     {
741         $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
742         $process->setTimeout(0.1);
743         $start = microtime(true);
744         try {
745             $process->run();
746             $this->fail('A RuntimeException should have been raised');
747         } catch (RuntimeException $e) {
748         }
749
750         $this->assertLessThan(15, microtime(true) - $start);
751
752         throw $e;
753     }
754
755     public function testCheckTimeoutOnNonStartedProcess()
756     {
757         $process = $this->getProcess('echo foo');
758         $this->assertNull($process->checkTimeout());
759     }
760
761     public function testCheckTimeoutOnTerminatedProcess()
762     {
763         $process = $this->getProcess('echo foo');
764         $process->run();
765         $this->assertNull($process->checkTimeout());
766     }
767
768     /**
769      * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
770      * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
771      */
772     public function testCheckTimeoutOnStartedProcess()
773     {
774         $process = $this->getProcess(self::$phpBin.' -r "sleep(33);"');
775         $process->setTimeout(0.1);
776
777         $process->start();
778         $start = microtime(true);
779
780         try {
781             while ($process->isRunning()) {
782                 $process->checkTimeout();
783                 usleep(100000);
784             }
785             $this->fail('A ProcessTimedOutException should have been raised');
786         } catch (ProcessTimedOutException $e) {
787         }
788
789         $this->assertLessThan(15, microtime(true) - $start);
790
791         throw $e;
792     }
793
794     public function testIdleTimeout()
795     {
796         $process = $this->getProcess(self::$phpBin.' -r "sleep(34);"');
797         $process->setTimeout(60);
798         $process->setIdleTimeout(0.1);
799
800         try {
801             $process->run();
802
803             $this->fail('A timeout exception was expected.');
804         } catch (ProcessTimedOutException $e) {
805             $this->assertTrue($e->isIdleTimeout());
806             $this->assertFalse($e->isGeneralTimeout());
807             $this->assertEquals(0.1, $e->getExceededTimeout());
808         }
809     }
810
811     public function testIdleTimeoutNotExceededWhenOutputIsSent()
812     {
813         $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('while (true) {echo \'foo \'; usleep(1000);}')));
814         $process->setTimeout(1);
815         $process->start();
816
817         while (false === strpos($process->getOutput(), 'foo')) {
818             usleep(1000);
819         }
820
821         $process->setIdleTimeout(0.5);
822
823         try {
824             $process->wait();
825             $this->fail('A timeout exception was expected.');
826         } catch (ProcessTimedOutException $e) {
827             $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
828             $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
829             $this->assertEquals(1, $e->getExceededTimeout());
830         }
831     }
832
833     /**
834      * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
835      * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
836      */
837     public function testStartAfterATimeout()
838     {
839         $process = $this->getProcess(self::$phpBin.' -r "sleep(35);"');
840         $process->setTimeout(0.1);
841
842         try {
843             $process->run();
844             $this->fail('A ProcessTimedOutException should have been raised.');
845         } catch (ProcessTimedOutException $e) {
846         }
847         $this->assertFalse($process->isRunning());
848         $process->start();
849         $this->assertTrue($process->isRunning());
850         $process->stop(0);
851
852         throw $e;
853     }
854
855     public function testGetPid()
856     {
857         $process = $this->getProcess(self::$phpBin.' -r "sleep(36);"');
858         $process->start();
859         $this->assertGreaterThan(0, $process->getPid());
860         $process->stop(0);
861     }
862
863     public function testGetPidIsNullBeforeStart()
864     {
865         $process = $this->getProcess('foo');
866         $this->assertNull($process->getPid());
867     }
868
869     public function testGetPidIsNullAfterRun()
870     {
871         $process = $this->getProcess('echo foo');
872         $process->run();
873         $this->assertNull($process->getPid());
874     }
875
876     /**
877      * @requires extension pcntl
878      */
879     public function testSignal()
880     {
881         $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/SignalListener.php');
882         $process->start();
883
884         while (false === strpos($process->getOutput(), 'Caught')) {
885             usleep(1000);
886         }
887         $process->signal(SIGUSR1);
888         $process->wait();
889
890         $this->assertEquals('Caught SIGUSR1', $process->getOutput());
891     }
892
893     /**
894      * @requires extension pcntl
895      */
896     public function testExitCodeIsAvailableAfterSignal()
897     {
898         $this->skipIfNotEnhancedSigchild();
899
900         $process = $this->getProcess('sleep 4');
901         $process->start();
902         $process->signal(SIGKILL);
903
904         while ($process->isRunning()) {
905             usleep(10000);
906         }
907
908         $this->assertFalse($process->isRunning());
909         $this->assertTrue($process->hasBeenSignaled());
910         $this->assertFalse($process->isSuccessful());
911         $this->assertEquals(137, $process->getExitCode());
912     }
913
914     /**
915      * @expectedException \Symfony\Component\Process\Exception\LogicException
916      * @expectedExceptionMessage Can not send signal on a non running process.
917      */
918     public function testSignalProcessNotRunning()
919     {
920         $process = $this->getProcess('foo');
921         $process->signal(1); // SIGHUP
922     }
923
924     /**
925      * @dataProvider provideMethodsThatNeedARunningProcess
926      */
927     public function testMethodsThatNeedARunningProcess($method)
928     {
929         $process = $this->getProcess('foo');
930
931         if (method_exists($this, 'expectException')) {
932             $this->expectException('Symfony\Component\Process\Exception\LogicException');
933             $this->expectExceptionMessage(sprintf('Process must be started before calling %s.', $method));
934         } else {
935             $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
936         }
937
938         $process->{$method}();
939     }
940
941     public function provideMethodsThatNeedARunningProcess()
942     {
943         return array(
944             array('getOutput'),
945             array('getIncrementalOutput'),
946             array('getErrorOutput'),
947             array('getIncrementalErrorOutput'),
948             array('wait'),
949         );
950     }
951
952     /**
953      * @dataProvider provideMethodsThatNeedATerminatedProcess
954      * @expectedException \Symfony\Component\Process\Exception\LogicException
955      * @expectedExceptionMessage Process must be terminated before calling
956      */
957     public function testMethodsThatNeedATerminatedProcess($method)
958     {
959         $process = $this->getProcess(self::$phpBin.' -r "sleep(37);"');
960         $process->start();
961         try {
962             $process->{$method}();
963             $process->stop(0);
964             $this->fail('A LogicException must have been thrown');
965         } catch (\Exception $e) {
966         }
967         $process->stop(0);
968
969         throw $e;
970     }
971
972     public function provideMethodsThatNeedATerminatedProcess()
973     {
974         return array(
975             array('hasBeenSignaled'),
976             array('getTermSignal'),
977             array('hasBeenStopped'),
978             array('getStopSignal'),
979         );
980     }
981
982     /**
983      * @dataProvider provideWrongSignal
984      * @expectedException \Symfony\Component\Process\Exception\RuntimeException
985      */
986     public function testWrongSignal($signal)
987     {
988         if ('\\' === DIRECTORY_SEPARATOR) {
989             $this->markTestSkipped('POSIX signals do not work on Windows');
990         }
991
992         $process = $this->getProcess(self::$phpBin.' -r "sleep(38);"');
993         $process->start();
994         try {
995             $process->signal($signal);
996             $this->fail('A RuntimeException must have been thrown');
997         } catch (RuntimeException $e) {
998             $process->stop(0);
999         }
1000
1001         throw $e;
1002     }
1003
1004     public function provideWrongSignal()
1005     {
1006         return array(
1007             array(-4),
1008             array('Céphalopodes'),
1009         );
1010     }
1011
1012     public function testDisableOutputDisablesTheOutput()
1013     {
1014         $p = $this->getProcess('foo');
1015         $this->assertFalse($p->isOutputDisabled());
1016         $p->disableOutput();
1017         $this->assertTrue($p->isOutputDisabled());
1018         $p->enableOutput();
1019         $this->assertFalse($p->isOutputDisabled());
1020     }
1021
1022     /**
1023      * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1024      * @expectedExceptionMessage Disabling output while the process is running is not possible.
1025      */
1026     public function testDisableOutputWhileRunningThrowsException()
1027     {
1028         $p = $this->getProcess(self::$phpBin.' -r "sleep(39);"');
1029         $p->start();
1030         $p->disableOutput();
1031     }
1032
1033     /**
1034      * @expectedException \Symfony\Component\Process\Exception\RuntimeException
1035      * @expectedExceptionMessage Enabling output while the process is running is not possible.
1036      */
1037     public function testEnableOutputWhileRunningThrowsException()
1038     {
1039         $p = $this->getProcess(self::$phpBin.' -r "sleep(40);"');
1040         $p->disableOutput();
1041         $p->start();
1042         $p->enableOutput();
1043     }
1044
1045     public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
1046     {
1047         $p = $this->getProcess('echo foo');
1048         $p->disableOutput();
1049         $p->run();
1050         $p->enableOutput();
1051         $p->disableOutput();
1052         $this->assertTrue($p->isOutputDisabled());
1053     }
1054
1055     /**
1056      * @expectedException \Symfony\Component\Process\Exception\LogicException
1057      * @expectedExceptionMessage Output can not be disabled while an idle timeout is set.
1058      */
1059     public function testDisableOutputWhileIdleTimeoutIsSet()
1060     {
1061         $process = $this->getProcess('foo');
1062         $process->setIdleTimeout(1);
1063         $process->disableOutput();
1064     }
1065
1066     /**
1067      * @expectedException \Symfony\Component\Process\Exception\LogicException
1068      * @expectedExceptionMessage timeout can not be set while the output is disabled.
1069      */
1070     public function testSetIdleTimeoutWhileOutputIsDisabled()
1071     {
1072         $process = $this->getProcess('foo');
1073         $process->disableOutput();
1074         $process->setIdleTimeout(1);
1075     }
1076
1077     public function testSetNullIdleTimeoutWhileOutputIsDisabled()
1078     {
1079         $process = $this->getProcess('foo');
1080         $process->disableOutput();
1081         $this->assertSame($process, $process->setIdleTimeout(null));
1082     }
1083
1084     /**
1085      * @dataProvider provideStartMethods
1086      */
1087     public function testStartWithACallbackAndDisabledOutput($startMethod, $exception, $exceptionMessage)
1088     {
1089         $p = $this->getProcess('foo');
1090         $p->disableOutput();
1091
1092         if (method_exists($this, 'expectException')) {
1093             $this->expectException($exception);
1094             $this->expectExceptionMessage($exceptionMessage);
1095         } else {
1096             $this->setExpectedException($exception, $exceptionMessage);
1097         }
1098
1099         if ('mustRun' === $startMethod) {
1100             $this->skipIfNotEnhancedSigchild();
1101         }
1102         $p->{$startMethod}(function () {});
1103     }
1104
1105     public function provideStartMethods()
1106     {
1107         return array(
1108             array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
1109             array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
1110             array('mustRun', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
1111         );
1112     }
1113
1114     /**
1115      * @dataProvider provideOutputFetchingMethods
1116      * @expectedException \Symfony\Component\Process\Exception\LogicException
1117      * @expectedExceptionMessage Output has been disabled.
1118      */
1119     public function testGetOutputWhileDisabled($fetchMethod)
1120     {
1121         $p = $this->getProcess(self::$phpBin.' -r "sleep(41);"');
1122         $p->disableOutput();
1123         $p->start();
1124         $p->{$fetchMethod}();
1125     }
1126
1127     public function provideOutputFetchingMethods()
1128     {
1129         return array(
1130             array('getOutput'),
1131             array('getIncrementalOutput'),
1132             array('getErrorOutput'),
1133             array('getIncrementalErrorOutput'),
1134         );
1135     }
1136
1137     public function testStopTerminatesProcessCleanly()
1138     {
1139         $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(42);"');
1140         $process->run(function () use ($process) {
1141             $process->stop();
1142         });
1143         $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
1144     }
1145
1146     public function testKillSignalTerminatesProcessCleanly()
1147     {
1148         $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(43);"');
1149         $process->run(function () use ($process) {
1150             $process->signal(9); // SIGKILL
1151         });
1152         $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1153     }
1154
1155     public function testTermSignalTerminatesProcessCleanly()
1156     {
1157         $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(44);"');
1158         $process->run(function () use ($process) {
1159             $process->signal(15); // SIGTERM
1160         });
1161         $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
1162     }
1163
1164     public function responsesCodeProvider()
1165     {
1166         return array(
1167             //expected output / getter / code to execute
1168             //array(1,'getExitCode','exit(1);'),
1169             //array(true,'isSuccessful','exit();'),
1170             array('output', 'getOutput', 'echo \'output\';'),
1171         );
1172     }
1173
1174     public function pipesCodeProvider()
1175     {
1176         $variations = array(
1177             'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
1178             'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
1179         );
1180
1181         if ('\\' === DIRECTORY_SEPARATOR) {
1182             // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650
1183             $sizes = array(1, 2, 4, 8);
1184         } else {
1185             $sizes = array(1, 16, 64, 1024, 4096);
1186         }
1187
1188         $codes = array();
1189         foreach ($sizes as $size) {
1190             foreach ($variations as $code) {
1191                 $codes[] = array($code, $size);
1192             }
1193         }
1194
1195         return $codes;
1196     }
1197
1198     /**
1199      * @dataProvider provideVariousIncrementals
1200      */
1201     public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
1202     {
1203         $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }'), null, null, null, null);
1204         $process->start();
1205         $result = '';
1206         $limit = microtime(true) + 3;
1207         $expected = '012';
1208
1209         while ($result !== $expected && microtime(true) < $limit) {
1210             $result .= $process->$method();
1211         }
1212
1213         $this->assertSame($expected, $result);
1214         $process->stop();
1215     }
1216
1217     public function provideVariousIncrementals()
1218     {
1219         return array(
1220             array('php://stdout', 'getIncrementalOutput'),
1221             array('php://stderr', 'getIncrementalErrorOutput'),
1222         );
1223     }
1224
1225     /**
1226      * @param string      $commandline
1227      * @param null|string $cwd
1228      * @param null|array  $env
1229      * @param null|string $input
1230      * @param int         $timeout
1231      * @param array       $options
1232      *
1233      * @return Process
1234      */
1235     private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array())
1236     {
1237         $process = new Process($commandline, $cwd, $env, $input, $timeout, $options);
1238
1239         if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
1240             try {
1241                 $process->setEnhanceSigchildCompatibility(false);
1242                 $process->getExitCode();
1243                 $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
1244             } catch (RuntimeException $e) {
1245                 $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
1246                 if ($enhance) {
1247                     $process->setEnhanceSigchildCompatibility(true);
1248                 } else {
1249                     self::$notEnhancedSigchild = true;
1250                 }
1251             }
1252         }
1253
1254         if (self::$process) {
1255             self::$process->stop(0);
1256         }
1257
1258         return self::$process = $process;
1259     }
1260
1261     private function skipIfNotEnhancedSigchild($expectException = true)
1262     {
1263         if (self::$sigchild) {
1264             if (!$expectException) {
1265                 $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
1266             } elseif (self::$notEnhancedSigchild) {
1267                 if (method_exists($this, 'expectException')) {
1268                     $this->expectException('Symfony\Component\Process\Exception\RuntimeException');
1269                     $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.');
1270                 } else {
1271                     $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild.');
1272                 }
1273             }
1274         }
1275     }
1276 }
1277
1278 class Stringifiable
1279 {
1280     public function __toString()
1281     {
1282         return 'stringifiable';
1283     }
1284 }
1285
1286 class NonStringifiable
1287 {
1288 }