toBeInstanceOf(MultiHandle::class); expect($multi->count())->toBe(0); }); it('can be initialized with handles', function () { $handle1 = new Handle('https://httpbin.org/get'); $handle2 = new Handle('https://httpbin.org/uuid'); $multi = new MultiHandle($handle1, $handle2); expect($multi)->toBeInstanceOf(MultiHandle::class); expect($multi->count())->toBe(2); expect($multi->getHandles())->toHaveCount(2); }); it('can add handles', function () { $multi = new MultiHandle(); $handle = new Handle('https://httpbin.org/get'); $result = $multi->addHandle($handle); expect($result)->toBe($multi); // Fluent API expect($multi->count())->toBe(1); }); it('can remove handles', function () { $multi = new MultiHandle(); $handle = new Handle('https://httpbin.org/get'); $multi->addHandle($handle); expect($multi->count())->toBe(1); $multi->removeHandle($handle); expect($multi->count())->toBe(0); }); it('can get all attached handles', function () { $multi = new MultiHandle(); $handle1 = new Handle('https://httpbin.org/get'); $handle2 = new Handle('https://httpbin.org/uuid'); $multi->addHandle($handle1); $multi->addHandle($handle2); $handles = $multi->getHandles(); expect($handles)->toHaveCount(2); expect($handles)->toContain($handle1); expect($handles)->toContain($handle2); }); it('can execute parallel requests with executeAll()', function () { $multi = new MultiHandle(); $urls = [ 'https://httpbin.org/delay/1', 'https://httpbin.org/uuid', 'https://httpbin.org/get', ]; $handles = []; foreach ($urls as $url) { $handle = new Handle($url); $handle->setOption(HandleOption::Timeout, 10); $handles[] = $handle; $multi->addHandle($handle); } expect($multi->count())->toBe(3); $startTime = microtime(true); $completed = $multi->executeAll(); $duration = microtime(true) - $startTime; // Parallel execution should be faster than sequential (3+ seconds) expect($duration)->toBeLessThan(2.5); expect($completed)->toHaveCount(3); // Verify all requests completed successfully foreach ($handles as $handle) { expect($handle->getInfo(Info::ResponseCode))->toBe(200); } }); it('executes requests in parallel not sequentially', function () { $multi = new MultiHandle(); // Create 3 requests with 1 second delay each $handles = []; for ($i = 0; $i < 3; $i++) { $handle = new Handle('https://httpbin.org/delay/1'); $handle->setOption(HandleOption::Timeout, 10); $handles[] = $handle; $multi->addHandle($handle); } $startTime = microtime(true); $multi->executeAll(); $duration = microtime(true) - $startTime; // If sequential: ~3 seconds // If parallel: ~1 second expect($duration)->toBeLessThan(2.0); expect($duration)->toBeGreaterThan(0.8); }); it('can handle mixed success and failure', function () { $multi = new MultiHandle(); $handle1 = new Handle('https://httpbin.org/get'); $handle1->setOption(HandleOption::Timeout, 10); $handle2 = new Handle('https://httpbin.org/status/404'); $handle2->setOption(HandleOption::Timeout, 10); $handle3 = new Handle('https://httpbin.org/status/500'); $handle3->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle1); $multi->addHandle($handle2); $multi->addHandle($handle3); $completed = $multi->executeAll(); expect($completed)->toHaveCount(3); // Check individual status codes expect($handle1->getInfo(Info::ResponseCode))->toBe(200); expect($handle2->getInfo(Info::ResponseCode))->toBe(404); expect($handle3->getInfo(Info::ResponseCode))->toBe(500); }); it('can execute with manual control loop', function () { $multi = new MultiHandle(); $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle); $stillRunning = 0; $iterations = 0; do { $multi->execute($stillRunning); if ($stillRunning > 0) { $multi->select(); } $iterations++; } while ($stillRunning > 0 && $iterations < 100); expect($stillRunning)->toBe(0); expect($handle->getInfo(Info::ResponseCode))->toBe(200); }); it('can handle many parallel requests', function () { $multi = new MultiHandle(); $numRequests = 10; $handles = []; for ($i = 0; $i < $numRequests; $i++) { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $handles[] = $handle; $multi->addHandle($handle); } expect($multi->count())->toBe($numRequests); $startTime = microtime(true); $completed = $multi->executeAll(); $duration = microtime(true) - $startTime; expect($completed)->toHaveCount($numRequests); // Should complete much faster than sequential expect($duration)->toBeLessThan(5.0); // Verify all completed foreach ($handles as $handle) { expect($handle->getInfo(Info::ResponseCode))->toBe(200); } }); it('provides access to underlying resource', function () { $multi = new MultiHandle(); $resource = $multi->getResource(); expect($resource)->toBeInstanceOf(CurlMultiHandle::class); }); it('can set multi handle options', function () { $multi = new MultiHandle(); // CURLMOPT_PIPELINING enable HTTP/1.1 pipelining if (defined('CURLMOPT_PIPELINING')) { $result = $multi->setOption(CURLMOPT_PIPELINING, 1); expect($result)->toBe($multi); // Fluent API } expect($multi)->toBeInstanceOf(MultiHandle::class); }); it('handles empty multi handle gracefully', function () { $multi = new MultiHandle(); // Execute without any handles $completed = $multi->executeAll(); expect($completed)->toHaveCount(0); expect($multi->count())->toBe(0); }); it('can add and remove handles multiple times', function () { $multi = new MultiHandle(); $handle1 = new Handle('https://httpbin.org/get'); $handle2 = new Handle('https://httpbin.org/uuid'); $multi->addHandle($handle1); expect($multi->count())->toBe(1); $multi->addHandle($handle2); expect($multi->count())->toBe(2); $multi->removeHandle($handle1); expect($multi->count())->toBe(1); $multi->removeHandle($handle2); expect($multi->count())->toBe(0); }); it('select returns number of handles with activity', function () { $multi = new MultiHandle(); $handle = new Handle('https://httpbin.org/delay/1'); $handle->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle); $stillRunning = 0; $multi->execute($stillRunning); if ($stillRunning > 0) { $result = $multi->select(1.0); // Result can be -1 (error), 0 (timeout), or >0 (activity) expect($result)->toBeGreaterThanOrEqual(-1); } expect($stillRunning)->toBeGreaterThanOrEqual(0); }); it('getInfo returns transfer information', function () { $multi = new MultiHandle(); $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle); $stillRunning = 0; $messagesInQueue = 0; do { $multi->execute($stillRunning); if ($stillRunning > 0) { $multi->select(); } // Check for completed transfers while ($info = $multi->getInfo($messagesInQueue)) { expect($info)->toBeArray(); expect($info)->toHaveKey('handle'); expect($info)->toHaveKey('result'); } } while ($stillRunning > 0); expect($stillRunning)->toBe(0); }); it('can handle POST requests in parallel', function () { $multi = new MultiHandle(); $handles = []; $data = ['name' => 'Test', 'timestamp' => time()]; for ($i = 0; $i < 3; $i++) { $handle = new Handle('https://httpbin.org/post'); $handle->setOptions([ HandleOption::Post->value => true, HandleOption::PostFields->value => json_encode($data), HandleOption::HttpHeader->value => ['Content-Type: application/json'], HandleOption::Timeout->value => 10, ]); $handles[] = $handle; $multi->addHandle($handle); } $completed = $multi->executeAll(); expect($completed)->toHaveCount(3); foreach ($handles as $handle) { expect($handle->getInfo(Info::ResponseCode))->toBe(200); } }); it('automatically cleans up on destruction', function () { $multi = new MultiHandle(); $handle1 = new Handle('https://httpbin.org/get'); $handle2 = new Handle('https://httpbin.org/uuid'); $multi->addHandle($handle1); $multi->addHandle($handle2); expect($multi->count())->toBe(2); unset($multi); // If we reach here without errors, cleanup worked expect(true)->toBeTrue(); }); it('can register onComplete callback', function () { $multi = new MultiHandle(); $completedUrls = []; $multi->onComplete(function(Handle $handle) use (&$completedUrls) { $completedUrls[] = $handle->getInfo(Info::EffectiveUrl); }); $handle1 = new Handle('https://httpbin.org/get'); $handle2 = new Handle('https://httpbin.org/uuid'); $handle1->setOption(HandleOption::Timeout, 10); $handle2->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle1); $multi->addHandle($handle2); $multi->executeAll(); expect($completedUrls)->toHaveCount(2); expect($completedUrls[0])->toContain('httpbin.org'); expect($completedUrls[1])->toContain('httpbin.org'); }); it('can register onError callback', function () { $multi = new MultiHandle(); $errors = []; $multi->onError(function(Handle $handle, int $errorCode) use (&$errors) { $errors[] = [ 'url' => $handle->getInfo(Info::EffectiveUrl), 'code' => $errorCode ]; }); // Create a handle that will timeout $handle = new Handle('https://httpbin.org/delay/10'); $handle->setOption(HandleOption::Timeout, 1); // 1 second timeout $multi->addHandle($handle); $multi->executeAll(); expect($errors)->toHaveCount(1); expect($errors[0]['code'])->toBe(CURLE_OPERATION_TIMEDOUT); }); it('can register onProgress callback', function () { $multi = new MultiHandle(); $progressUpdates = []; $multi->onProgress(function(int $completed, int $total) use (&$progressUpdates) { $progressUpdates[] = ['completed' => $completed, 'total' => $total]; }); $handle1 = new Handle('https://httpbin.org/get'); $handle2 = new Handle('https://httpbin.org/uuid'); $handle3 = new Handle('https://httpbin.org/delay/1'); $handle1->setOption(HandleOption::Timeout, 10); $handle2->setOption(HandleOption::Timeout, 10); $handle3->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle1); $multi->addHandle($handle2); $multi->addHandle($handle3); $multi->executeAll(); // Should have progress updates expect($progressUpdates)->not->toBeEmpty(); expect($progressUpdates)->toHaveCount(3); // Final update should show all completed $finalUpdate = end($progressUpdates); expect($finalUpdate['completed'])->toBe(3); expect($finalUpdate['total'])->toBe(3); }); it('can chain callback registrations', function () { $multi = new MultiHandle(); $result = $multi ->onComplete(fn($handle) => null) ->onError(fn($handle, $code) => null) ->onProgress(fn($completed, $total) => null); expect($result)->toBe($multi); }); it('triggers callbacks in correct order', function () { $multi = new MultiHandle(); $events = []; $multi->onComplete(function(Handle $handle) use (&$events) { $events[] = 'complete'; }); $multi->onProgress(function(int $completed, int $total) use (&$events) { $events[] = 'progress'; }); $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle); $multi->executeAll(); expect($events)->toContain('complete'); expect($events)->toContain('progress'); }); }); describe('Curl\MultiHandle edge cases', function () { it('handles timeout in parallel requests', function () { $multi = new MultiHandle(); $handle1 = new Handle('https://httpbin.org/delay/1'); $handle1->setOption(HandleOption::Timeout, 10); $handle2 = new Handle('https://httpbin.org/delay/20'); $handle2->setOption(HandleOption::Timeout, 2); $multi->addHandle($handle1); $multi->addHandle($handle2); $completed = $multi->executeAll(); // Handle1 should succeed expect($handle1->getInfo(Info::ResponseCode))->toBe(200); // Handle2 should timeout (response code 0 or error) $handle2Code = $handle2->getInfo(Info::ResponseCode); expect($handle2Code)->toBeIn([0, 28]); // 0 = timeout, 28 = operation timeout }); it('can reuse multi handle for multiple batches', function () { $multi = new MultiHandle(); // First batch $handle1 = new Handle('https://httpbin.org/get'); $handle1->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle1); $multi->executeAll(); expect($handle1->getInfo(Info::ResponseCode))->toBe(200); $multi->removeHandle($handle1); // Second batch $handle2 = new Handle('https://httpbin.org/uuid'); $handle2->setOption(HandleOption::Timeout, 10); $multi->addHandle($handle2); $multi->executeAll(); expect($handle2->getInfo(Info::ResponseCode))->toBe(200); }); });