ProcessTest.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Process\Tests;
  11. use Symfony\Component\Process\Exception\LogicException;
  12. use Symfony\Component\Process\PhpExecutableFinder;
  13. use Symfony\Component\Process\Process;
  14. use Symfony\Component\Process\Exception\RuntimeException;
  15. use Symfony\Component\Process\ProcessPipes;
  16. /**
  17. * @author Robert Schönthal <seroscho@googlemail.com>
  18. */
  19. class ProcessTest extends \PHPUnit_Framework_TestCase
  20. {
  21. private static $phpBin;
  22. private static $process;
  23. private static $sigchild;
  24. private static $notEnhancedSigchild = false;
  25. public static function setUpBeforeClass()
  26. {
  27. $phpBin = new PhpExecutableFinder();
  28. self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === PHP_SAPI ? 'php' : $phpBin->find());
  29. if ('\\' !== DIRECTORY_SEPARATOR) {
  30. // exec is mandatory to deal with sending a signal to the process
  31. // see https://github.com/symfony/symfony/issues/5030 about prepending
  32. // command with exec
  33. self::$phpBin = 'exec '.self::$phpBin;
  34. }
  35. ob_start();
  36. phpinfo(INFO_GENERAL);
  37. self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
  38. }
  39. protected function tearDown()
  40. {
  41. if (self::$process) {
  42. self::$process->stop(0);
  43. self::$process = null;
  44. }
  45. }
  46. public function testThatProcessDoesNotThrowWarningDuringRun()
  47. {
  48. @trigger_error('Test Error', E_USER_NOTICE);
  49. $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'");
  50. $process->run();
  51. $actualError = error_get_last();
  52. $this->assertEquals('Test Error', $actualError['message']);
  53. $this->assertEquals(E_USER_NOTICE, $actualError['type']);
  54. }
  55. /**
  56. * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
  57. */
  58. public function testNegativeTimeoutFromConstructor()
  59. {
  60. $this->getProcess('', null, null, null, -1);
  61. }
  62. /**
  63. * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
  64. */
  65. public function testNegativeTimeoutFromSetter()
  66. {
  67. $p = $this->getProcess('');
  68. $p->setTimeout(-1);
  69. }
  70. public function testFloatAndNullTimeout()
  71. {
  72. $p = $this->getProcess('');
  73. $p->setTimeout(10);
  74. $this->assertSame(10.0, $p->getTimeout());
  75. $p->setTimeout(null);
  76. $this->assertNull($p->getTimeout());
  77. $p->setTimeout(0.0);
  78. $this->assertNull($p->getTimeout());
  79. }
  80. /**
  81. * @requires extension pcntl
  82. */
  83. public function testStopWithTimeoutIsActuallyWorking()
  84. {
  85. $p = $this->getProcess(self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 30');
  86. $p->start();
  87. while (false === strpos($p->getOutput(), 'received')) {
  88. usleep(1000);
  89. }
  90. $start = microtime(true);
  91. $p->stop(0.1);
  92. $p->wait();
  93. $this->assertLessThan(15, microtime(true) - $start);
  94. }
  95. public function testAllOutputIsActuallyReadOnTermination()
  96. {
  97. // this code will result in a maximum of 2 reads of 8192 bytes by calling
  98. // start() and isRunning(). by the time getOutput() is called the process
  99. // has terminated so the internal pipes array is already empty. normally
  100. // the call to start() will not read any data as the process will not have
  101. // generated output, but this is non-deterministic so we must count it as
  102. // a possibility. therefore we need 2 * ProcessPipes::CHUNK_SIZE plus
  103. // another byte which will never be read.
  104. $expectedOutputSize = ProcessPipes::CHUNK_SIZE * 2 + 2;
  105. $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
  106. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  107. $p->start();
  108. // Don't call Process::run nor Process::wait to avoid any read of pipes
  109. $h = new \ReflectionProperty($p, 'process');
  110. $h->setAccessible(true);
  111. $h = $h->getValue($p);
  112. $s = @proc_get_status($h);
  113. while (!empty($s['running'])) {
  114. usleep(1000);
  115. $s = proc_get_status($h);
  116. }
  117. $o = $p->getOutput();
  118. $this->assertEquals($expectedOutputSize, strlen($o));
  119. }
  120. public function testCallbacksAreExecutedWithStart()
  121. {
  122. $process = $this->getProcess('echo foo');
  123. $process->start(function ($type, $buffer) use (&$data) {
  124. $data .= $buffer;
  125. });
  126. $process->wait();
  127. $this->assertSame('foo'.PHP_EOL, $data);
  128. }
  129. /**
  130. * tests results from sub processes.
  131. *
  132. * @dataProvider responsesCodeProvider
  133. */
  134. public function testProcessResponses($expected, $getter, $code)
  135. {
  136. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  137. $p->run();
  138. $this->assertSame($expected, $p->$getter());
  139. }
  140. /**
  141. * tests results from sub processes.
  142. *
  143. * @dataProvider pipesCodeProvider
  144. */
  145. public function testProcessPipes($code, $size)
  146. {
  147. $expected = str_repeat(str_repeat('*', 1024), $size).'!';
  148. $expectedLength = (1024 * $size) + 1;
  149. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  150. $p->setStdin($expected);
  151. $p->run();
  152. $this->assertEquals($expectedLength, strlen($p->getOutput()));
  153. $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
  154. }
  155. /**
  156. * @expectedException Symfony\Component\Process\Exception\LogicException
  157. * @expectedExceptionMessage STDIN can not be set while the process is running.
  158. */
  159. public function testSetStdinWhileRunningThrowsAnException()
  160. {
  161. $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
  162. $process->start();
  163. try {
  164. $process->setStdin('foobar');
  165. $process->stop();
  166. $this->fail('A LogicException should have been raised.');
  167. } catch (LogicException $e) {
  168. }
  169. $process->stop();
  170. throw $e;
  171. }
  172. /**
  173. * @dataProvider provideInvalidStdinValues
  174. * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
  175. * @expectedExceptionMessage Symfony\Component\Process\Process::setStdin only accepts strings.
  176. */
  177. public function testInvalidStdin($value)
  178. {
  179. $process = $this->getProcess('foo');
  180. $process->setStdin($value);
  181. }
  182. public function provideInvalidStdinValues()
  183. {
  184. return array(
  185. array(array()),
  186. array(new NonStringifiable()),
  187. array(fopen('php://temporary', 'w')),
  188. );
  189. }
  190. /**
  191. * @dataProvider provideStdinValues
  192. */
  193. public function testValidStdin($expected, $value)
  194. {
  195. $process = $this->getProcess('foo');
  196. $process->setStdin($value);
  197. $this->assertSame($expected, $process->getStdin());
  198. }
  199. public function provideStdinValues()
  200. {
  201. return array(
  202. array(null, null),
  203. array('24.5', 24.5),
  204. array('input data', 'input data'),
  205. // to maintain BC, supposed to be removed in 3.0
  206. array('stringifiable', new Stringifiable()),
  207. );
  208. }
  209. public function chainedCommandsOutputProvider()
  210. {
  211. if ('\\' === DIRECTORY_SEPARATOR) {
  212. return array(
  213. array("2 \r\n2\r\n", '&&', '2'),
  214. );
  215. }
  216. return array(
  217. array("1\n1\n", ';', '1'),
  218. array("2\n2\n", '&&', '2'),
  219. );
  220. }
  221. /**
  222. * @dataProvider chainedCommandsOutputProvider
  223. */
  224. public function testChainedCommandsOutput($expected, $operator, $input)
  225. {
  226. $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input));
  227. $process->run();
  228. $this->assertEquals($expected, $process->getOutput());
  229. }
  230. public function testCallbackIsExecutedForOutput()
  231. {
  232. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';')));
  233. $called = false;
  234. $p->run(function ($type, $buffer) use (&$called) {
  235. $called = $buffer === 'foo';
  236. });
  237. $this->assertTrue($called, 'The callback should be executed with the output');
  238. }
  239. public function testGetErrorOutput()
  240. {
  241. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
  242. $p->run();
  243. $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
  244. }
  245. /**
  246. * @dataProvider provideIncrementalOutput
  247. */
  248. public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
  249. {
  250. $lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
  251. $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\');')));
  252. $h = fopen($lock, 'w');
  253. flock($h, LOCK_EX);
  254. $p->start();
  255. foreach (array('foo', 'bar') as $s) {
  256. while (false === strpos($p->$getOutput(), $s)) {
  257. usleep(1000);
  258. }
  259. $this->assertSame($s, $p->$getIncrementalOutput());
  260. $this->assertSame('', $p->$getIncrementalOutput());
  261. flock($h, LOCK_UN);
  262. }
  263. fclose($h);
  264. }
  265. public function provideIncrementalOutput()
  266. {
  267. return array(
  268. array('getOutput', 'getIncrementalOutput', 'php://stdout'),
  269. array('getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'),
  270. );
  271. }
  272. public function testGetOutput()
  273. {
  274. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }')));
  275. $p->run();
  276. $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
  277. }
  278. public function testZeroAsOutput()
  279. {
  280. if ('\\' === DIRECTORY_SEPARATOR) {
  281. // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
  282. $p = $this->getProcess('echo | set /p dummyName=0');
  283. } else {
  284. $p = $this->getProcess('printf 0');
  285. }
  286. $p->run();
  287. $this->assertSame('0', $p->getOutput());
  288. }
  289. public function testExitCodeCommandFailed()
  290. {
  291. if ('\\' === DIRECTORY_SEPARATOR) {
  292. $this->markTestSkipped('Windows does not support POSIX exit code');
  293. }
  294. $this->skipIfNotEnhancedSigchild();
  295. // such command run in bash return an exitcode 127
  296. $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
  297. $process->run();
  298. $this->assertGreaterThan(0, $process->getExitCode());
  299. }
  300. public function testTTYCommand()
  301. {
  302. if ('\\' === DIRECTORY_SEPARATOR) {
  303. $this->markTestSkipped('Windows does not have /dev/tty support');
  304. }
  305. $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"');
  306. $process->setTty(true);
  307. $process->start();
  308. $this->assertTrue($process->isRunning());
  309. $process->wait();
  310. $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
  311. }
  312. public function testTTYCommandExitCode()
  313. {
  314. if ('\\' === DIRECTORY_SEPARATOR) {
  315. $this->markTestSkipped('Windows does have /dev/tty support');
  316. }
  317. $this->skipIfNotEnhancedSigchild();
  318. $process = $this->getProcess('echo "foo" >> /dev/null');
  319. $process->setTty(true);
  320. $process->run();
  321. $this->assertTrue($process->isSuccessful());
  322. }
  323. /**
  324. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  325. * @expectedExceptionMessage TTY mode is not supported on Windows platform.
  326. */
  327. public function testTTYInWindowsEnvironment()
  328. {
  329. if ('\\' !== DIRECTORY_SEPARATOR) {
  330. $this->markTestSkipped('This test is for Windows platform only');
  331. }
  332. $process = $this->getProcess('echo "foo" >> /dev/null');
  333. $process->setTty(false);
  334. $process->setTty(true);
  335. }
  336. public function testExitCodeTextIsNullWhenExitCodeIsNull()
  337. {
  338. $this->skipIfNotEnhancedSigchild();
  339. $process = $this->getProcess('');
  340. $this->assertNull($process->getExitCodeText());
  341. }
  342. public function testExitCodeText()
  343. {
  344. $this->skipIfNotEnhancedSigchild();
  345. $process = $this->getProcess('');
  346. $r = new \ReflectionObject($process);
  347. $p = $r->getProperty('exitcode');
  348. $p->setAccessible(true);
  349. $p->setValue($process, 2);
  350. $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
  351. }
  352. public function testStartIsNonBlocking()
  353. {
  354. $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"');
  355. $start = microtime(true);
  356. $process->start();
  357. $end = microtime(true);
  358. $this->assertLessThan(0.4, $end - $start);
  359. $process->stop();
  360. }
  361. public function testUpdateStatus()
  362. {
  363. $process = $this->getProcess('echo foo');
  364. $process->run();
  365. $this->assertTrue(strlen($process->getOutput()) > 0);
  366. }
  367. public function testGetExitCodeIsNullOnStart()
  368. {
  369. $this->skipIfNotEnhancedSigchild();
  370. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  371. $this->assertNull($process->getExitCode());
  372. $process->start();
  373. $this->assertNull($process->getExitCode());
  374. $process->wait();
  375. $this->assertEquals(0, $process->getExitCode());
  376. }
  377. public function testGetExitCodeIsNullOnWhenStartingAgain()
  378. {
  379. $this->skipIfNotEnhancedSigchild();
  380. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  381. $process->run();
  382. $this->assertEquals(0, $process->getExitCode());
  383. $process->start();
  384. $this->assertNull($process->getExitCode());
  385. $process->wait();
  386. $this->assertEquals(0, $process->getExitCode());
  387. }
  388. public function testGetExitCode()
  389. {
  390. $this->skipIfNotEnhancedSigchild();
  391. $process = $this->getProcess('echo foo');
  392. $process->run();
  393. $this->assertSame(0, $process->getExitCode());
  394. }
  395. public function testStatus()
  396. {
  397. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  398. $this->assertFalse($process->isRunning());
  399. $this->assertFalse($process->isStarted());
  400. $this->assertFalse($process->isTerminated());
  401. $this->assertSame(Process::STATUS_READY, $process->getStatus());
  402. $process->start();
  403. $this->assertTrue($process->isRunning());
  404. $this->assertTrue($process->isStarted());
  405. $this->assertFalse($process->isTerminated());
  406. $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
  407. $process->wait();
  408. $this->assertFalse($process->isRunning());
  409. $this->assertTrue($process->isStarted());
  410. $this->assertTrue($process->isTerminated());
  411. $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
  412. }
  413. public function testStop()
  414. {
  415. $process = $this->getProcess(self::$phpBin.' -r "sleep(31);"');
  416. $process->start();
  417. $this->assertTrue($process->isRunning());
  418. $process->stop();
  419. $this->assertFalse($process->isRunning());
  420. }
  421. public function testIsSuccessful()
  422. {
  423. $this->skipIfNotEnhancedSigchild();
  424. $process = $this->getProcess('echo foo');
  425. $process->run();
  426. $this->assertTrue($process->isSuccessful());
  427. }
  428. public function testIsSuccessfulOnlyAfterTerminated()
  429. {
  430. $this->skipIfNotEnhancedSigchild();
  431. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  432. $process->start();
  433. $this->assertFalse($process->isSuccessful());
  434. $process->wait();
  435. $this->assertTrue($process->isSuccessful());
  436. }
  437. public function testIsNotSuccessful()
  438. {
  439. $this->skipIfNotEnhancedSigchild();
  440. $process = $this->getProcess(self::$phpBin.' -r "throw new \Exception(\'BOUM\');"');
  441. $process->run();
  442. $this->assertFalse($process->isSuccessful());
  443. }
  444. public function testProcessIsNotSignaled()
  445. {
  446. if ('\\' === DIRECTORY_SEPARATOR) {
  447. $this->markTestSkipped('Windows does not support POSIX signals');
  448. }
  449. $this->skipIfNotEnhancedSigchild();
  450. $process = $this->getProcess('echo foo');
  451. $process->run();
  452. $this->assertFalse($process->hasBeenSignaled());
  453. }
  454. public function testProcessWithoutTermSignal()
  455. {
  456. if ('\\' === DIRECTORY_SEPARATOR) {
  457. $this->markTestSkipped('Windows does not support POSIX signals');
  458. }
  459. $this->skipIfNotEnhancedSigchild();
  460. $process = $this->getProcess('echo foo');
  461. $process->run();
  462. $this->assertEquals(0, $process->getTermSignal());
  463. }
  464. public function testProcessIsSignaledIfStopped()
  465. {
  466. if ('\\' === DIRECTORY_SEPARATOR) {
  467. $this->markTestSkipped('Windows does not support POSIX signals');
  468. }
  469. $this->skipIfNotEnhancedSigchild();
  470. $process = $this->getProcess(self::$phpBin.' -r "sleep(32);"');
  471. $process->start();
  472. $process->stop();
  473. $this->assertTrue($process->hasBeenSignaled());
  474. $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
  475. }
  476. /**
  477. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  478. * @expectedExceptionMessage The process has been signaled
  479. */
  480. public function testProcessThrowsExceptionWhenExternallySignaled()
  481. {
  482. if (!function_exists('posix_kill')) {
  483. $this->markTestSkipped('Function posix_kill is required.');
  484. }
  485. $this->skipIfNotEnhancedSigchild(false);
  486. $process = $this->getProcess(self::$phpBin.' -r "sleep(32.1)"');
  487. $process->start();
  488. posix_kill($process->getPid(), 9); // SIGKILL
  489. $process->wait();
  490. }
  491. public function testRestart()
  492. {
  493. $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"');
  494. $process1->run();
  495. $process2 = $process1->restart();
  496. $process2->wait(); // wait for output
  497. // Ensure that both processed finished and the output is numeric
  498. $this->assertFalse($process1->isRunning());
  499. $this->assertFalse($process2->isRunning());
  500. $this->assertTrue(is_numeric($process1->getOutput()));
  501. $this->assertTrue(is_numeric($process2->getOutput()));
  502. // Ensure that restart returned a new process by check that the output is different
  503. $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
  504. }
  505. /**
  506. * @expectedException Symfony\Component\Process\Exception\RuntimeException
  507. * @expectedExceptionMessage The process timed-out.
  508. */
  509. public function testRunProcessWithTimeout()
  510. {
  511. $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
  512. $process->setTimeout(0.1);
  513. $start = microtime(true);
  514. try {
  515. $process->run();
  516. $this->fail('A RuntimeException should have been raised');
  517. } catch (RuntimeException $e) {
  518. }
  519. $this->assertLessThan(15, microtime(true) - $start);
  520. throw $e;
  521. }
  522. public function testCheckTimeoutOnNonStartedProcess()
  523. {
  524. $process = $this->getProcess('echo foo');
  525. $this->assertNull($process->checkTimeout());
  526. }
  527. public function testCheckTimeoutOnTerminatedProcess()
  528. {
  529. $process = $this->getProcess('echo foo');
  530. $process->run();
  531. $this->assertNull($process->checkTimeout());
  532. }
  533. /**
  534. * @expectedException Symfony\Component\Process\Exception\RuntimeException
  535. * @expectedExceptionMessage The process timed-out.
  536. */
  537. public function testCheckTimeoutOnStartedProcess()
  538. {
  539. $process = $this->getProcess(self::$phpBin.' -r "sleep(33);"');
  540. $process->setTimeout(0.1);
  541. $process->start();
  542. $start = microtime(true);
  543. try {
  544. while ($process->isRunning()) {
  545. $process->checkTimeout();
  546. usleep(100000);
  547. }
  548. $this->fail('A RuntimeException should have been raised');
  549. } catch (RuntimeException $e) {
  550. }
  551. $this->assertLessThan(15, microtime(true) - $start);
  552. throw $e;
  553. }
  554. /**
  555. * @expectedException Symfony\Component\Process\Exception\RuntimeException
  556. * @expectedExceptionMessage The process timed-out.
  557. */
  558. public function testStartAfterATimeout()
  559. {
  560. $process = $this->getProcess(self::$phpBin.' -r "sleep(35);"');
  561. $process->setTimeout(0.1);
  562. try {
  563. $process->run();
  564. $this->fail('A RuntimeException should have been raised.');
  565. } catch (RuntimeException $e) {
  566. }
  567. $this->assertFalse($process->isRunning());
  568. $process->start();
  569. $process->stop(0);
  570. throw $e;
  571. }
  572. public function testGetPid()
  573. {
  574. $process = $this->getProcess(self::$phpBin.' -r "sleep(36);"');
  575. $process->start();
  576. $this->assertGreaterThan(0, $process->getPid());
  577. $process->stop(0);
  578. }
  579. public function testGetPidIsNullBeforeStart()
  580. {
  581. $process = $this->getProcess('foo');
  582. $this->assertNull($process->getPid());
  583. }
  584. public function testGetPidIsNullAfterRun()
  585. {
  586. $process = $this->getProcess('echo foo');
  587. $process->run();
  588. $this->assertNull($process->getPid());
  589. }
  590. /**
  591. * @requires extension pcntl
  592. */
  593. public function testSignal()
  594. {
  595. $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/SignalListener.php');
  596. $process->start();
  597. while (false === strpos($process->getOutput(), 'Caught')) {
  598. usleep(1000);
  599. }
  600. $process->signal(SIGUSR1);
  601. $process->wait();
  602. $this->assertEquals('Caught SIGUSR1', $process->getOutput());
  603. }
  604. /**
  605. * @requires extension pcntl
  606. */
  607. public function testExitCodeIsAvailableAfterSignal()
  608. {
  609. $this->skipIfNotEnhancedSigchild();
  610. $process = $this->getProcess('sleep 4');
  611. $process->start();
  612. $process->signal(SIGKILL);
  613. while ($process->isRunning()) {
  614. usleep(10000);
  615. }
  616. $this->assertFalse($process->isRunning());
  617. $this->assertTrue($process->hasBeenSignaled());
  618. $this->assertFalse($process->isSuccessful());
  619. $this->assertEquals(137, $process->getExitCode());
  620. }
  621. /**
  622. * @expectedException \Symfony\Component\Process\Exception\LogicException
  623. * @expectedExceptionMessage Can not send signal on a non running process.
  624. */
  625. public function testSignalProcessNotRunning()
  626. {
  627. $process = $this->getProcess('foo');
  628. $process->signal(1); // SIGHUP
  629. }
  630. /**
  631. * @dataProvider provideMethodsThatNeedARunningProcess
  632. */
  633. public function testMethodsThatNeedARunningProcess($method)
  634. {
  635. $process = $this->getProcess('foo');
  636. $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
  637. $process->{$method}();
  638. }
  639. public function provideMethodsThatNeedARunningProcess()
  640. {
  641. return array(
  642. array('getOutput'),
  643. array('getIncrementalOutput'),
  644. array('getErrorOutput'),
  645. array('getIncrementalErrorOutput'),
  646. array('wait'),
  647. );
  648. }
  649. /**
  650. * @dataProvider provideMethodsThatNeedATerminatedProcess
  651. * @expectedException Symfony\Component\Process\Exception\LogicException
  652. * @expectedExceptionMessage Process must be terminated before calling
  653. */
  654. public function testMethodsThatNeedATerminatedProcess($method)
  655. {
  656. $process = $this->getProcess(self::$phpBin.' -r "sleep(37);"');
  657. $process->start();
  658. try {
  659. $process->{$method}();
  660. $process->stop(0);
  661. $this->fail('A LogicException must have been thrown');
  662. } catch (\Exception $e) {
  663. }
  664. $process->stop(0);
  665. throw $e;
  666. }
  667. public function provideMethodsThatNeedATerminatedProcess()
  668. {
  669. return array(
  670. array('hasBeenSignaled'),
  671. array('getTermSignal'),
  672. array('hasBeenStopped'),
  673. array('getStopSignal'),
  674. );
  675. }
  676. /**
  677. * @dataProvider provideWrongSignal
  678. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  679. */
  680. public function testWrongSignal($signal)
  681. {
  682. if ('\\' === DIRECTORY_SEPARATOR) {
  683. $this->markTestSkipped('POSIX signals do not work on Windows');
  684. }
  685. $process = $this->getProcess(self::$phpBin.' -r "sleep(38);"');
  686. $process->start();
  687. try {
  688. $process->signal($signal);
  689. $this->fail('A RuntimeException must have been thrown');
  690. } catch (RuntimeException $e) {
  691. $process->stop(0);
  692. }
  693. throw $e;
  694. }
  695. public function provideWrongSignal()
  696. {
  697. return array(
  698. array(-4),
  699. array('Céphalopodes'),
  700. );
  701. }
  702. public function testStopTerminatesProcessCleanly()
  703. {
  704. $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(42);"');
  705. $process->run(function () use ($process) {
  706. $process->stop();
  707. });
  708. $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
  709. }
  710. public function testKillSignalTerminatesProcessCleanly()
  711. {
  712. $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(43);"');
  713. $process->run(function () use ($process) {
  714. $process->signal(9); // SIGKILL
  715. });
  716. $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
  717. }
  718. public function testTermSignalTerminatesProcessCleanly()
  719. {
  720. $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(44);"');
  721. $process->run(function () use ($process) {
  722. $process->signal(15); // SIGTERM
  723. });
  724. $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
  725. }
  726. public function responsesCodeProvider()
  727. {
  728. return array(
  729. //expected output / getter / code to execute
  730. //array(1,'getExitCode','exit(1);'),
  731. //array(true,'isSuccessful','exit();'),
  732. array('output', 'getOutput', 'echo \'output\';'),
  733. );
  734. }
  735. public function pipesCodeProvider()
  736. {
  737. $variations = array(
  738. 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
  739. 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
  740. );
  741. if ('\\' === DIRECTORY_SEPARATOR) {
  742. // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650
  743. $sizes = array(1, 2, 4, 8);
  744. } else {
  745. $sizes = array(1, 16, 64, 1024, 4096);
  746. }
  747. $codes = array();
  748. foreach ($sizes as $size) {
  749. foreach ($variations as $code) {
  750. $codes[] = array($code, $size);
  751. }
  752. }
  753. return $codes;
  754. }
  755. /**
  756. * @param string $commandline
  757. * @param null $cwd
  758. * @param array $env
  759. * @param null $stdin
  760. * @param int $timeout
  761. * @param array $options
  762. *
  763. * @return Process
  764. */
  765. private function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array())
  766. {
  767. $process = new Process($commandline, $cwd, $env, $stdin, $timeout, $options);
  768. if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
  769. try {
  770. $process->setEnhanceSigchildCompatibility(false);
  771. $process->getExitCode();
  772. $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
  773. } catch (RuntimeException $e) {
  774. $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
  775. if ($enhance) {
  776. $process->setEnhanceSigChildCompatibility(true);
  777. } else {
  778. self::$notEnhancedSigchild = true;
  779. }
  780. }
  781. }
  782. if (self::$process) {
  783. self::$process->stop(0);
  784. }
  785. return self::$process = $process;
  786. }
  787. private function skipIfNotEnhancedSigchild($expectException = true)
  788. {
  789. if (self::$sigchild) {
  790. if (!$expectException) {
  791. $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
  792. } elseif (self::$notEnhancedSigchild) {
  793. $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild.');
  794. }
  795. }
  796. }
  797. }
  798. class Stringifiable
  799. {
  800. public function __toString()
  801. {
  802. return 'stringifiable';
  803. }
  804. }
  805. class NonStringifiable
  806. {
  807. }