PageRenderTime 30ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/symfony/process/Tests/ProcessTest.php

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