PageRenderTime 56ms CodeModel.GetById 7ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php

http://github.com/guzzle/guzzle
PHP | 600 lines | 399 code | 77 blank | 124 comment | 13 complexity | ba495cd83e946d704643c24a5954d7a8 MD5 | raw file
  1. <?php
  2. namespace Guzzle\Tests\Http\Curl;
  3. use Guzzle\Common\Event;
  4. use Guzzle\Common\Exception\ExceptionCollection;;
  5. use Guzzle\Common\Collection;
  6. use Guzzle\Common\Log\ClosureLogAdapter;
  7. use Guzzle\Http\Client;
  8. use Guzzle\Http\Message\Request;
  9. use Guzzle\Http\Message\Response;
  10. use Guzzle\Http\Message\RequestFactory;
  11. use Guzzle\Http\Curl\CurlHandle;
  12. use Guzzle\Http\Curl\CurlMulti;
  13. use Guzzle\Http\Exception\CurlException;
  14. use Guzzle\Http\Plugin\LogPlugin;
  15. use Guzzle\Tests\Mock\MockMulti;
  16. /**
  17. * @group server
  18. * @covers Guzzle\Http\Curl\CurlMulti
  19. */
  20. class CurlMultiTest extends \Guzzle\Tests\GuzzleTestCase
  21. {
  22. /**
  23. * @var Guzzle\Http\Curl\CurlMulti
  24. */
  25. private $multi;
  26. /**
  27. * @var Guzzle\Common\Collection
  28. */
  29. private $updates;
  30. private $mock;
  31. /**
  32. * Prepares the environment before running a test.
  33. */
  34. protected function setUp()
  35. {
  36. parent::setUp();
  37. $this->updates = new Collection();
  38. $this->multi = new MockMulti();
  39. $this->mock = $this->getWildcardObserver($this->multi);
  40. }
  41. /**
  42. * @covers Guzzle\Http\Curl\CurlMulti::getInstance
  43. */
  44. public function testReturnsCachedInstance()
  45. {
  46. $c = CurlMulti::getInstance();
  47. $this->assertInstanceOf('Guzzle\\Http\\Curl\\CurlMultiInterface', $c);
  48. $this->assertSame($c, CurlMulti::getInstance());
  49. }
  50. /**
  51. * @covers Guzzle\Http\Curl\CurlMulti::__construct
  52. * @covers Guzzle\Http\Curl\CurlMulti::__destruct
  53. */
  54. public function testConstructorCreateMultiHandle()
  55. {
  56. $this->assertInternalType('resource', $this->multi->getHandle());
  57. $this->assertEquals('curl_multi', get_resource_type($this->multi->getHandle()));
  58. }
  59. /**
  60. * @covers Guzzle\Http\Curl\CurlMulti::__destruct
  61. */
  62. public function testDestructorClosesMultiHandle()
  63. {
  64. $handle = $this->multi->getHandle();
  65. $this->multi->__destruct();
  66. $this->assertFalse(is_resource($handle));
  67. }
  68. /**
  69. * @covers Guzzle\Http\Curl\curlMulti::add
  70. * @covers Guzzle\Http\Curl\curlMulti::all
  71. * @covers Guzzle\Http\Curl\curlMulti::count
  72. */
  73. public function testRequestsCanBeAddedAndCounted()
  74. {
  75. $multi = new CurlMulti();
  76. $mock = $this->getWildcardObserver($multi);
  77. $request1 = new Request('GET', 'http://www.google.com/');
  78. $multi->add($request1);
  79. $this->assertEquals(array($request1), $multi->all());
  80. $request2 = new Request('POST', 'http://www.google.com/');
  81. $multi->add($request2);
  82. $this->assertEquals(array($request1, $request2), $multi->all());
  83. $this->assertEquals(2, count($multi));
  84. $this->assertTrue($mock->has(CurlMulti::ADD_REQUEST));
  85. $this->assertFalse($mock->has(CurlMulti::REMOVE_REQUEST));
  86. $this->assertFalse($mock->has(CurlMulti::POLLING));
  87. $this->assertFalse($mock->has(CurlMulti::COMPLETE));
  88. }
  89. /**
  90. * @covers Guzzle\Http\Curl\CurlMulti::remove
  91. * @covers Guzzle\Http\Curl\CurlMulti::all
  92. */
  93. public function testRequestsCanBeRemoved()
  94. {
  95. $request1 = new Request('GET', 'http://www.google.com/');
  96. $this->multi->add($request1);
  97. $request2 = new Request('PUT', 'http://www.google.com/');
  98. $this->multi->add($request2);
  99. $this->assertEquals(array($request1, $request2), $this->multi->all());
  100. $this->assertSame($this->multi, $this->multi->remove($request1));
  101. $this->assertEquals(array($request2), $this->multi->all());
  102. }
  103. /**
  104. * @covers Guzzle\Http\Curl\CurlMulti::reset
  105. */
  106. public function testsResetRemovesRequestsAndResetsState()
  107. {
  108. $request1 = new Request('GET', 'http://www.google.com/');
  109. $this->multi->add($request1);
  110. $this->multi->reset();
  111. $this->assertEquals(array(), $this->multi->all());
  112. $this->assertEquals('idle', $this->multi->getState());
  113. }
  114. /**
  115. * @covers Guzzle\Http\Curl\CurlMulti::send
  116. * @covers Guzzle\Http\Curl\CurlMulti::getState
  117. */
  118. public function testSendsRequestsInParallel()
  119. {
  120. $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nBody");
  121. $this->assertEquals('idle', $this->multi->getState());
  122. $request = new Request('GET', $this->getServer()->getUrl());
  123. $this->multi->add($request);
  124. $this->multi->send();
  125. $this->assertEquals('idle', $this->multi->getState());
  126. $this->assertTrue($this->mock->has(CurlMulti::ADD_REQUEST));
  127. $this->assertTrue($this->mock->has(CurlMulti::COMPLETE));
  128. $this->assertEquals('Body', $request->getResponse()->getBody()->__toString());
  129. // Sending it again will not do anything because there are no requests
  130. $this->multi->send();
  131. }
  132. /**
  133. * @covers Guzzle\Http\Curl\CurlMulti::send
  134. */
  135. public function testSendsRequestsThroughCurl()
  136. {
  137. $this->getServer()->enqueue(array(
  138. "HTTP/1.1 204 No content\r\n" .
  139. "Content-Length: 0\r\n" .
  140. "Server: Jetty(6.1.3)\r\n\r\n",
  141. "HTTP/1.1 200 OK\r\n" .
  142. "Content-Type: text/html; charset=utf-8\r\n" .
  143. "Content-Length: 4\r\n" .
  144. "Server: Jetty(6.1.3)\r\n\r\n" .
  145. "data"
  146. ));
  147. $request1 = new Request('GET', $this->getServer()->getUrl());
  148. $mock1 = $this->getWildcardObserver($request1);
  149. $request2 = new Request('GET', $this->getServer()->getUrl());
  150. $mock2 = $this->getWildcardObserver($request2);
  151. $this->multi->add($request1);
  152. $this->multi->add($request2);
  153. $this->multi->send();
  154. $response1 = $request1->getResponse();
  155. $response2 = $request2->getResponse();
  156. $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response1);
  157. $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response2);
  158. $this->assertTrue($response1->getBody(true) == 'data' || $response2->getBody(true) == 'data');
  159. $this->assertTrue($response1->getBody(true) == '' || $response2->getBody(true) == '');
  160. $this->assertTrue($response1->getStatusCode() == '204' || $response2->getStatusCode() == '204');
  161. $this->assertNotEquals((string) $response1, (string) $response2);
  162. $this->assertTrue($mock1->has('request.before_send'));
  163. $this->assertTrue($mock2->has('request.before_send'));
  164. }
  165. /**
  166. * @covers Guzzle\Http\Curl\CurlMulti::send
  167. */
  168. public function testSendsThroughCurlAndAggregatesRequestExceptions()
  169. {
  170. $this->getServer()->enqueue(array(
  171. "HTTP/1.1 200 OK\r\n" .
  172. "Content-Type: text/html; charset=utf-8\r\n" .
  173. "Content-Length: 4\r\n" .
  174. "Server: Jetty(6.1.3)\r\n" .
  175. "\r\n" .
  176. "data",
  177. "HTTP/1.1 204 No content\r\n" .
  178. "Content-Length: 0\r\n" .
  179. "Server: Jetty(6.1.3)\r\n" .
  180. "\r\n",
  181. "HTTP/1.1 404 Not Found\r\n" .
  182. "Content-Length: 0\r\n" .
  183. "\r\n"
  184. ));
  185. $request1 = new Request('GET', $this->getServer()->getUrl());
  186. $request2 = new Request('HEAD', $this->getServer()->getUrl());
  187. $request3 = new Request('GET', $this->getServer()->getUrl());
  188. $this->multi->add($request1);
  189. $this->multi->add($request2);
  190. $this->multi->add($request3);
  191. try {
  192. $this->multi->send();
  193. $this->fail('ExceptionCollection not thrown when aggregating request exceptions');
  194. } catch (ExceptionCollection $e) {
  195. $this->assertInstanceOf('ArrayIterator', $e->getIterator());
  196. $this->assertEquals(1, count($e));
  197. $exceptions = $e->getIterator();
  198. $response1 = $request1->getResponse();
  199. $response2 = $request2->getResponse();
  200. $response3 = $request3->getResponse();
  201. $this->assertNotEquals((string) $response1, (string) $response2);
  202. $this->assertNotEquals((string) $response3, (string) $response1);
  203. $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response1);
  204. $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response2);
  205. $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response3);
  206. $failed = $exceptions[0]->getResponse();
  207. $this->assertEquals(404, $failed->getStatusCode());
  208. $this->assertEquals(1, count($e));
  209. // Test the IteratorAggregate functionality
  210. foreach ($e as $except) {
  211. $this->assertEquals($failed, $except->getResponse());
  212. }
  213. }
  214. }
  215. /**
  216. * @covers Guzzle\Http\Curl\CurlMulti::send
  217. * @covers Guzzle\Http\Curl\CurlMulti::processResponse
  218. */
  219. public function testCurlErrorsAreCaught()
  220. {
  221. $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
  222. try {
  223. $request = RequestFactory::getInstance()->create('GET', 'http://127.0.0.1:9876/');
  224. $request->setClient(new Client());
  225. $request->getCurlOptions()->set(CURLOPT_FRESH_CONNECT, true);
  226. $request->getCurlOptions()->set(CURLOPT_FORBID_REUSE, true);
  227. $request->getCurlOptions()->set(CURLOPT_CONNECTTIMEOUT_MS, 5);
  228. $request->send();
  229. $this->fail('CurlException not thrown');
  230. } catch (CurlException $e) {
  231. $m = $e->getMessage();
  232. $this->assertContains('[curl] ', $m);
  233. $this->assertContains('[url] http://127.0.0.1:9876/', $m);
  234. $this->assertContains('[debug] ', $m);
  235. $this->assertContains('[info] array (', $m);
  236. }
  237. }
  238. /**
  239. * @covers Guzzle\Http\Curl\CurlMulti
  240. */
  241. public function testRemovesQueuedRequests()
  242. {
  243. $request = RequestFactory::getInstance()->create('GET', 'http://127.0.0.1:9876/');
  244. $request->setClient(new Client());
  245. $request->setResponse(new Response(200), true);
  246. $this->multi->add($request);
  247. $this->multi->send();
  248. $this->assertTrue($this->mock->has(CurlMulti::ADD_REQUEST));
  249. $this->assertTrue($this->mock->has(CurlMulti::POLLING) === false);
  250. $this->assertTrue($this->mock->has(CurlMulti::COMPLETE) !== false);
  251. }
  252. /**
  253. * @covers Guzzle\Http\Curl\CurlMulti
  254. */
  255. public function testRemovesQueuedRequestsAddedInTransit()
  256. {
  257. $this->getServer()->flush();
  258. $this->getServer()->enqueue(array(
  259. "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
  260. ));
  261. $client = new Client($this->getServer()->getUrl());
  262. $r = $client->get();
  263. $r->getEventDispatcher()->addListener('request.receive.status_line', function(Event $event) use ($client) {
  264. // Create a request using a queued response
  265. $request = $client->get()->setResponse(new Response(200), true);
  266. $request->send();
  267. });
  268. $r->send();
  269. $this->assertEquals(1, count($this->getServer()->getReceivedRequests(false)));
  270. }
  271. /**
  272. * @covers Guzzle\Http\Curl\CurlMulti
  273. */
  274. public function testProperlyBlocksBasedOnRequestsInScope()
  275. {
  276. $this->getServer()->flush();
  277. $this->getServer()->enqueue(array(
  278. "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\ntest1",
  279. "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\ntest2",
  280. "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\ntest3",
  281. "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\ntest4",
  282. "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\ntest5",
  283. "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\ntest6",
  284. "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
  285. "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
  286. ));
  287. $client = new Client($this->getServer()->getUrl());
  288. $requests = array(
  289. $client->get(),
  290. $client->get()
  291. );
  292. $sendHeadFunction = function($event) use ($client) {
  293. $client->head()->send();
  294. };
  295. // Sends 2 new requests in the middle of a CurlMulti loop while other requests
  296. // are completing. This causes the scope of the multi handle to go up.
  297. $callback = function(Event $event) use ($client, $sendHeadFunction) {
  298. $client->getConfig()->set('called', $client->getConfig('called') + 1);
  299. if ($client->getConfig('called') <= 2) {
  300. $request = $client->get();
  301. $request->getEventDispatcher()->addListener('request.complete', $sendHeadFunction);
  302. $request->send();
  303. }
  304. };
  305. $requests[0]->getEventDispatcher()->addListener('request.complete', $callback);
  306. $client->send($requests);
  307. $this->assertEquals(4, count($this->getServer()->getReceivedRequests(false)));
  308. }
  309. /**
  310. * @covers Guzzle\Http\Curl\CurlMulti
  311. * @expectedException RuntimeException
  312. * @expectedExceptionMessage Testing!
  313. */
  314. public function testCatchesExceptionsBeforeSendingCurlMulti()
  315. {
  316. $client = new Client($this->getServer()->getUrl());
  317. $multi = new CurlMulti();
  318. $client->setCurlMulti($multi);
  319. $multi->getEventDispatcher()->addListener(CurlMulti::BEFORE_SEND, function() {
  320. throw new \RuntimeException('Testing!');
  321. });
  322. $client->get()->send();
  323. }
  324. /**
  325. * @covers Guzzle\Http\Curl\CurlMulti
  326. * @covers Guzzle\Http\Curl\CurlMulti::removeErroredRequest
  327. * @expectedException Guzzle\Common\Exception\ExceptionCollection
  328. * @expectedExceptionMessage Thrown before sending!
  329. */
  330. public function testCatchesExceptionsBeforeSendingRequests()
  331. {
  332. $client = new Client($this->getServer()->getUrl());
  333. $request = $client->get();
  334. $request->getEventDispatcher()->addListener('request.before_send', function() {
  335. throw new \RuntimeException('Thrown before sending!');
  336. });
  337. $client->send(array($request));
  338. }
  339. /**
  340. * @covers Guzzle\Http\Curl\CurlMulti
  341. * @covers Guzzle\Http\Curl\CurlMulti::removeErroredRequest
  342. * @expectedException Guzzle\Http\Exception\BadResponseException
  343. */
  344. public function testCatchesExceptionsWhenRemovingQueuedRequests()
  345. {
  346. $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
  347. $client = new Client($this->getServer()->getUrl());
  348. $r = $client->get();
  349. $r->getEventDispatcher()->addListener('request.sent', function() use ($client) {
  350. // Create a request using a queued response
  351. $client->get()->setResponse(new Response(404), true)->send();
  352. });
  353. $r->send();
  354. }
  355. /**
  356. * @covers Guzzle\Http\Curl\CurlMulti
  357. * @covers Guzzle\Http\Curl\CurlMulti::removeErroredRequest
  358. * @expectedException Guzzle\Http\Exception\BadResponseException
  359. */
  360. public function testCatchesExceptionsWhenRemovingQueuedRequestsBeforeSending()
  361. {
  362. $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
  363. $client = new Client($this->getServer()->getUrl());
  364. $r = $client->get();
  365. $r->getEventDispatcher()->addListener('request.before_send', function() use ($client) {
  366. // Create a request using a queued response
  367. $client->get()->setResponse(new Response(404), true)->send();
  368. });
  369. $r->send();
  370. }
  371. /**
  372. * @covers Guzzle\Http\Curl\CurlMulti::send
  373. * @covers Guzzle\Http\Curl\CurlMulti::removeErroredRequest
  374. * @expectedException Guzzle\Common\Exception\ExceptionCollection
  375. * @expectedExceptionMessage test
  376. */
  377. public function testCatchesRandomExceptionsThrownDuringPerform()
  378. {
  379. $client = new Client($this->getServer()->getUrl());
  380. $multi = $this->getMock('Guzzle\\Http\\Curl\\CurlMulti', array('perform'));
  381. $multi->expects($this->once())
  382. ->method('perform')
  383. ->will($this->throwException(new \Exception('test')));
  384. $multi->add($client->get());
  385. $multi->send();
  386. }
  387. /**
  388. * @covers Guzzle\Http\Curl\CurlMulti::send
  389. */
  390. public function testDoesNotSendRequestsDecliningToBeSent()
  391. {
  392. if (!defined('CURLOPT_TIMEOUT_MS')) {
  393. $this->markTestSkipped('Update curl');
  394. }
  395. // Create a client that is bound to fail connecting
  396. $client = new Client('http://localhost:123', array(
  397. 'curl.CURLOPT_PORT' => 123,
  398. 'curl.CURLOPT_CONNECTTIMEOUT_MS' => 1,
  399. ));
  400. $request = $client->get();
  401. $multi = new CurlMulti();
  402. $multi->add($request);
  403. // Listen for request exceptions, and when they occur, first change the
  404. // state of the request back to transferring, and then just allow it to
  405. // exception out
  406. $request->getEventDispatcher()->addListener('request.exception', function(Event $event) use ($multi) {
  407. $retries = $event['request']->getParams()->get('retries');
  408. // Allow the first failure to retry
  409. if ($retries == 0) {
  410. $event['request']->setState('transfer');
  411. $event['request']->getParams()->set('retries', 1);
  412. // Remove the request to try again
  413. $multi->remove($event['request']);
  414. $multi->add($event['request'], true);
  415. }
  416. });
  417. try {
  418. $multi->send();
  419. $this->fail('Did not throw an exception at all!?!');
  420. } catch (\Exception $e) {
  421. $this->assertEquals(1, $request->getParams()->get('retries'));
  422. }
  423. }
  424. /**
  425. * @covers Guzzle\Http\Curl\CurlMulti::send
  426. */
  427. public function testDoesNotThrowExceptionsWhenRequestsRecoverWithRetry()
  428. {
  429. $this->getServer()->flush();
  430. $client = new Client($this->getServer()->getUrl());
  431. $request = $client->get();
  432. $request->getEventDispatcher()->addListener('request.before_send', function(Event $event) {
  433. $event['request']->setResponse(new Response(200));
  434. });
  435. $multi = new CurlMulti();
  436. $multi->add($request);
  437. $multi->send();
  438. $this->assertEquals(0, count($this->getServer()->getReceivedRequests(false)));
  439. }
  440. /**
  441. * @covers Guzzle\Http\Curl\CurlMulti::send
  442. */
  443. public function testDoesNotThrowExceptionsWhenRequestsRecoverWithSuccess()
  444. {
  445. // Attempt a port that 99.9% is not listening
  446. $client = new Client('http://localhost:123');
  447. $request = $client->get();
  448. // Ensure it times out quickly if needed
  449. $request->getCurlOptions()->set(CURLOPT_TIMEOUT_MS, 1)->set(CURLOPT_CONNECTTIMEOUT_MS, 1);
  450. $request->getEventDispatcher()->addListener('request.exception', function(Event $event) use (&$count) {
  451. $event['request']->setResponse(new Response(200));
  452. });
  453. $multi = new CurlMulti();
  454. $multi->add($request);
  455. $multi->send();
  456. // Ensure that the exception was caught, and the response was set manually
  457. $this->assertEquals(200, $request->getResponse()->getStatusCode());
  458. }
  459. /**
  460. * @covers Guzzle\Http\Curl\CurlMulti::reset
  461. */
  462. public function testHardResetReopensMultiHandle()
  463. {
  464. $this->getServer()->enqueue(array(
  465. "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
  466. "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
  467. ));
  468. $client = new Client($this->getServer()->getUrl());
  469. $message = '';
  470. $plugin = new LogPlugin(new ClosureLogAdapter(function($msg) use (&$message) {
  471. $message .= $msg . "\n";
  472. }), LogPlugin::LOG_VERBOSE);
  473. $client->getEventDispatcher()->addSubscriber($plugin);
  474. $request = $client->get();
  475. $multi = new CurlMulti();
  476. $multi->add($request);
  477. $multi->send();
  478. $multi->reset(true);
  479. $multi->add($request);
  480. $multi->send();
  481. $this->assertNotContains('Re-using existing connection', $message);
  482. }
  483. /**
  484. * @covers Guzzle\Http\Curl\CurlMulti::checkCurlResult
  485. */
  486. public function testThrowsMeaningfulExceptionsForCurlMultiErrors()
  487. {
  488. $multi = new CurlMulti();
  489. // Set the state of the multi object to sending to trigger the exception
  490. $reflector = new \ReflectionMethod('Guzzle\Http\Curl\CurlMulti', 'checkCurlResult');
  491. $reflector->setAccessible(true);
  492. // Successful
  493. $reflector->invoke($multi, 0);
  494. // Known error
  495. try {
  496. $reflector->invoke($multi, CURLM_BAD_HANDLE);
  497. $this->fail('Expected an exception here');
  498. } catch (CurlException $e) {
  499. $this->assertContains('The passed-in handle is not a valid CURLM handle.', $e->getMessage());
  500. $this->assertContains('CURLM_BAD_HANDLE', $e->getMessage());
  501. $this->assertContains(strval(CURLM_BAD_HANDLE), $e->getMessage());
  502. }
  503. // Unknown error
  504. try {
  505. $reflector->invoke($multi, 255);
  506. $this->fail('Expected an exception here');
  507. } catch (CurlException $e) {
  508. $this->assertEquals('Unexpected cURL error: 255', $e->getMessage());
  509. }
  510. }
  511. /**
  512. * @covers Guzzle\Http\Curl\curlMulti::add
  513. */
  514. public function testAddsAsyncRequestsNormallyWhenNotSending()
  515. {
  516. $multi = new CurlMulti();
  517. $request = new Request('GET', 'http://www.google.com/');
  518. $multi->add($request, true);
  519. // Ensure that the request was added at the correct next scope
  520. $requests = $this->readAttribute($multi, 'requests');
  521. $this->assertEquals(array($request), $requests[0]);
  522. }
  523. }