ParallelTest.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. namespace tests;
  3. use PHPUnit\Framework\TestCase;
  4. use Workerman\Coroutine\Parallel;
  5. use Workerman\Coroutine;
  6. use Workerman\Timer;
  7. /**
  8. * Test cases for the Workerman\Coroutine\Parallel class.
  9. */
  10. class ParallelTest extends TestCase
  11. {
  12. /**
  13. * Test that callables are added and executed, and results are collected properly.
  14. */
  15. public function testAddAndWait()
  16. {
  17. $parallel = new Parallel();
  18. $parallel->add(function () {
  19. // Simulate some work.
  20. Timer::sleep(0.01);
  21. return 1;
  22. }, 'task1');
  23. $parallel->add(function () {
  24. // Simulate some work.
  25. Timer::sleep(0.005);
  26. return 2;
  27. }, 'task2');
  28. $results = $parallel->wait();
  29. $this->assertEquals(['task1' => 1, 'task2' => 2], $results);
  30. }
  31. /**
  32. * Test that exceptions thrown in callables are caught and can be retrieved.
  33. */
  34. public function testExceptions()
  35. {
  36. $parallel = new Parallel();
  37. $parallel->add(function () {
  38. throw new \Exception('Test exception');
  39. }, 'task_with_exception');
  40. $parallel->add(function () {
  41. return 'normal result';
  42. }, 'normal_task');
  43. $results = $parallel->wait();
  44. $exceptions = $parallel->getExceptions();
  45. // Check that the normal task result is present.
  46. $this->assertEquals(['normal_task' => 'normal result'], $results);
  47. // Check that the exception is captured for the failing task.
  48. $this->assertArrayHasKey('task_with_exception', $exceptions);
  49. $this->assertInstanceOf(\Exception::class, $exceptions['task_with_exception']);
  50. $this->assertEquals('Test exception', $exceptions['task_with_exception']->getMessage());
  51. }
  52. /**
  53. * Test that a synchronous task cannot make wait return before asynchronous tasks complete.
  54. */
  55. public function testSynchronousAndAsynchronousTasksCompleteBeforeWaitReturns()
  56. {
  57. $parallel = new Parallel();
  58. $completed = false;
  59. $parallel->add(function () {
  60. throw new \RuntimeException('immediate failure');
  61. }, 'immediate');
  62. $parallel->add(function () use (&$completed) {
  63. Timer::sleep(0.01);
  64. $completed = true;
  65. return 'delayed result';
  66. }, 'delayed');
  67. $results = $parallel->wait();
  68. $exceptions = $parallel->getExceptions();
  69. $this->assertTrue($completed);
  70. $this->assertSame(['delayed' => 'delayed result'], $results);
  71. $this->assertArrayHasKey('immediate', $exceptions);
  72. $this->assertInstanceOf(\RuntimeException::class, $exceptions['immediate']);
  73. }
  74. /**
  75. * Test concurrency control by limiting the number of concurrent tasks.
  76. */
  77. public function testConcurrencyLimit()
  78. {
  79. $concurrentLimit = 2;
  80. $parallel = new Parallel($concurrentLimit);
  81. $startTimes = [];
  82. $endTimes = [];
  83. for ($i = 0; $i < 5; $i++) {
  84. $parallel->add(function () use (&$startTimes, &$endTimes, $i) {
  85. $startTimes[$i] = microtime(true);
  86. // Simulate some work.
  87. Timer::sleep(0.1); // 100 milliseconds
  88. $endTimes[$i] = microtime(true);
  89. return $i;
  90. }, "task{$i}");
  91. }
  92. $parallel->wait();
  93. // Since we limited concurrency to 2, tasks should finish in batches.
  94. // We'll check that at no point more than $concurrentLimit tasks were running simultaneously.
  95. // Collect start and end times into an array of intervals.
  96. $intervals = [];
  97. for ($i = 0; $i < 5; $i++) {
  98. $intervals[] = ['start' => $startTimes[$i], 'end' => $endTimes[$i]];
  99. }
  100. // Check the maximum number of overlapping intervals does not exceed the concurrency limit.
  101. $maxConcurrent = $this->getMaxConcurrentIntervals($intervals);
  102. $this->assertLessThanOrEqual($concurrentLimit, $maxConcurrent);
  103. }
  104. /**
  105. * Helper function to determine the maximum number of overlapping intervals.
  106. *
  107. * @param array $intervals
  108. * @return int
  109. */
  110. private function getMaxConcurrentIntervals(array $intervals)
  111. {
  112. $events = [];
  113. foreach ($intervals as $interval) {
  114. $events[] = ['time' => $interval['start'], 'type' => 'start'];
  115. $events[] = ['time' => $interval['end'], 'type' => 'end'];
  116. }
  117. // Sort events by time, 'start' before 'end' if times are equal.
  118. usort($events, function ($a, $b) {
  119. if ($a['time'] == $b['time']) {
  120. return $a['type'] === 'start' ? -1 : 1;
  121. }
  122. return $a['time'] < $b['time'] ? -1 : 1;
  123. });
  124. $maxConcurrent = 0;
  125. $currentConcurrent = 0;
  126. foreach ($events as $event) {
  127. if ($event['type'] === 'start') {
  128. $currentConcurrent++;
  129. if ($currentConcurrent > $maxConcurrent) {
  130. $maxConcurrent = $currentConcurrent;
  131. }
  132. } else {
  133. $currentConcurrent--;
  134. }
  135. }
  136. return $maxConcurrent;
  137. }
  138. /**
  139. * Test that callables are executed in parallel when no concurrency limit is set.
  140. */
  141. public function testParallelExecutionWithoutConcurrencyLimit()
  142. {
  143. $parallel = new Parallel();
  144. $startTimes = [];
  145. $endTimes = [];
  146. $parallel->add(function () use (&$startTimes, &$endTimes) {
  147. $startTimes[] = microtime(true);
  148. Timer::sleep(0.1); // 100 milliseconds
  149. $endTimes[] = microtime(true);
  150. return 'task1';
  151. }, 'task1');
  152. $parallel->add(function () use (&$startTimes, &$endTimes) {
  153. $startTimes[] = microtime(true);
  154. Timer::sleep(0.1);// 100 milliseconds
  155. $endTimes[] = microtime(true);
  156. return 'task2';
  157. }, 'task2');
  158. $parallel->wait();
  159. // Calculate total elapsed time.
  160. $totalTime = max($endTimes) - min($startTimes);
  161. // The total time should be approximately the duration of one task, not the sum of both.
  162. $this->assertLessThan(0.2, $totalTime);
  163. }
  164. /**
  165. * Test adding callables without specifying keys and ensure results are correctly indexed.
  166. */
  167. public function testAddWithoutKeys()
  168. {
  169. $parallel = new Parallel();
  170. $parallel->add(function () {
  171. return 'result1';
  172. });
  173. $parallel->add(function () {
  174. return 'result2';
  175. });
  176. $results = $parallel->wait();
  177. // Since no keys were specified, indices should be 0 and 1.
  178. $this->assertEquals(['result1', 'result2'], $results);
  179. }
  180. /**
  181. * Test that the Parallel class can handle a large number of tasks.
  182. */
  183. public function testLargeNumberOfTasks()
  184. {
  185. $parallel = new Parallel();
  186. $taskCount = 100;
  187. for ($i = 0; $i < $taskCount; $i++) {
  188. $parallel->add(function () use ($i) {
  189. return $i * $i;
  190. }, "task{$i}");
  191. }
  192. $results = $parallel->wait();
  193. // Verify that all tasks have been completed and results are correct.
  194. for ($i = 0; $i < $taskCount; $i++) {
  195. $this->assertEquals($i * $i, $results["task{$i}"]);
  196. }
  197. }
  198. /**
  199. * Test that adding a non-callable throws a TypeError.
  200. */
  201. public function testAddNonCallable()
  202. {
  203. $this->expectException(\TypeError::class);
  204. $parallel = new Parallel();
  205. $parallel->add('not a callable');
  206. }
  207. /**
  208. * Test that the wait method can be called multiple times safely.
  209. */
  210. public function testMultipleWaitCalls()
  211. {
  212. $parallel = new Parallel();
  213. $parallel->add(function () {
  214. return 'first call';
  215. }, 'task1');
  216. $resultsFirst = $parallel->wait();
  217. $this->assertEquals(['task1' => 'first call'], $resultsFirst);
  218. // Add another task after first wait.
  219. $parallel->add(function () {
  220. return 'second call';
  221. }, 'task2');
  222. $resultsSecond = $parallel->wait();
  223. // Since the callbacks array is not cleared after wait, results should include both tasks.
  224. $this->assertEquals(['task1' => 'first call', 'task2' => 'second call'], $resultsSecond);
  225. }
  226. /**
  227. * Test that the class properly handles empty tasks (no callables added).
  228. */
  229. public function testNoTasks()
  230. {
  231. $parallel = new Parallel();
  232. $results = $parallel->wait();
  233. $this->assertEmpty($results);
  234. }
  235. /**
  236. * Test that the class handles tasks that return null.
  237. */
  238. public function testTasksReturningNull()
  239. {
  240. $parallel = new Parallel();
  241. $parallel->add(function () {
  242. // No return statement, implicitly returns null.
  243. }, 'nullTask');
  244. $results = $parallel->wait();
  245. $this->assertArrayHasKey('nullTask', $results);
  246. $this->assertNull($results['nullTask']);
  247. }
  248. /**
  249. * Test defer can be used in tasks.
  250. */
  251. public function testWithDefer()
  252. {
  253. $parallel = new Parallel();
  254. $results = [];
  255. $parallel->add(function () use (&$results) {
  256. Coroutine::defer(function () use (&$results) {
  257. $results[] = 'defer1';
  258. });
  259. });
  260. $parallel->wait();
  261. $this->assertEquals(['defer1'], $results);
  262. }
  263. }