Skip to content

Commit 4e03c18

Browse files
Run the repetitions of a repeated test and the attempts of a retried test through their IterativeTestSuite when tests are run in parallel
1 parent ec286b1 commit 4e03c18

16 files changed

Lines changed: 540 additions & 51 deletions

phpunit.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<exclude>tests/end-to-end/parallel/phpt-coverage/_files</exclude>
5454
<exclude>tests/end-to-end/parallel/phpt-conflicts/_files</exclude>
5555
<exclude>tests/end-to-end/parallel/phpt-with-classes/_files</exclude>
56+
<exclude>tests/end-to-end/parallel/repeat-retry/_files</exclude>
5657
<exclude>tests/end-to-end/repeat/_files</exclude>
5758
<exclude>tests/end-to-end/retry/_files</exclude>
5859
<exclude>tests/end-to-end/self-direct-indirect/_files</exclude>

src/Event/Dispatcher/CollectingDispatcher.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
namespace PHPUnit\Event;
1111

12+
use function assert;
1213
use PHPUnit\Runner\DeprecationCollector\Facade as DeprecationCollector;
1314
use PHPUnit\Runner\DeprecationCollector\TestTriggeredDeprecationSubscriber;
1415

@@ -21,6 +22,7 @@ final class CollectingDispatcher implements Dispatcher
2122
{
2223
private EventCollection $events;
2324
private DirectDispatcher $isolatedDirectDispatcher;
25+
private ?EventCollection $collectedEvents = null;
2426

2527
public function __construct(DirectDispatcher $directDispatcher)
2628
{
@@ -32,6 +34,12 @@ public function __construct(DirectDispatcher $directDispatcher)
3234

3335
public function dispatch(Event $event): void
3436
{
37+
if ($this->collectedEvents !== null) {
38+
$this->collectedEvents->add($event);
39+
40+
return;
41+
}
42+
3543
$this->events->add($event);
3644

3745
try {
@@ -41,6 +49,31 @@ public function dispatch(Event $event): void
4149
}
4250
}
4351

52+
/**
53+
* Open a collection window: until stopCollectingEvents() is called, events
54+
* are diverted into a separate collection instead of being recorded and
55+
* dispatched. This mirrors the collection window of the DeferringDispatcher
56+
* so that a RetryTestSuite can suppress the events of a failed attempt when
57+
* it runs in a process whose event facade was initialized for isolation.
58+
*/
59+
public function startCollectingEvents(): void
60+
{
61+
assert($this->collectedEvents === null);
62+
63+
$this->collectedEvents = new EventCollection;
64+
}
65+
66+
public function stopCollectingEvents(): EventCollection
67+
{
68+
assert($this->collectedEvents !== null);
69+
70+
$events = $this->collectedEvents;
71+
72+
$this->collectedEvents = null;
73+
74+
return $events;
75+
}
76+
4477
public function flush(): EventCollection
4578
{
4679
$events = $this->events;

src/Event/Facade.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ final class Facade
2424
{
2525
private static ?self $instance = null;
2626
private Emitter $emitter;
27-
private ?TypeMap $typeMap = null;
28-
private ?DeferringDispatcher $deferringDispatcher = null;
29-
private bool $sealed = false;
27+
private ?TypeMap $typeMap = null;
28+
private ?DeferringDispatcher $deferringDispatcher = null;
29+
private ?CollectingDispatcher $isolationDispatcher = null;
30+
private bool $sealed = false;
3031

3132
public static function instance(): self
3233
{
@@ -108,6 +109,8 @@ public function initForIsolation(HRTime $offset): CollectingDispatcher
108109

109110
$this->sealed = true;
110111

112+
$this->isolationDispatcher = $dispatcher;
113+
111114
return $dispatcher;
112115
}
113116

@@ -135,20 +138,41 @@ public function collectingEmitter(): CollectingEmitter
135138

136139
public function forward(EventCollection $events): void
137140
{
138-
$dispatcher = $this->deferredDispatcher();
141+
if ($this->isolationDispatcher !== null) {
142+
$dispatcher = $this->isolationDispatcher;
143+
} else {
144+
$dispatcher = $this->deferredDispatcher();
145+
}
139146

140147
foreach ($events as $event) {
141148
$dispatcher->dispatch($event);
142149
}
143150
}
144151

152+
/**
153+
* In a process whose event facade was initialized for isolation — a
154+
* parallel test runner worker, for example — the collection window is
155+
* opened on the isolation dispatcher, because that is the dispatcher the
156+
* emitter dispatches to there; the deferring dispatcher is unused in such
157+
* a process.
158+
*/
145159
public function startCollectingEvents(): void
146160
{
161+
if ($this->isolationDispatcher !== null) {
162+
$this->isolationDispatcher->startCollectingEvents();
163+
164+
return;
165+
}
166+
147167
$this->deferredDispatcher()->startCollectingEvents();
148168
}
149169

150170
public function stopCollectingEvents(): EventCollection
151171
{
172+
if ($this->isolationDispatcher !== null) {
173+
return $this->isolationDispatcher->stopCollectingEvents();
174+
}
175+
152176
return $this->deferredDispatcher()->stopCollectingEvents();
153177
}
154178

src/Framework/RepeatTestSuite.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ public static function fromTests(string $name, array $tests, int $failureThresho
4646
return $suite;
4747
}
4848

49+
/**
50+
* @return positive-int
51+
*/
52+
public function failureThreshold(): int
53+
{
54+
return $this->failureThreshold;
55+
}
56+
4957
/**
5058
* @param list<Test> $tests
5159
*/

src/Runner/Parallel/PersistentWorker.php

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use function assert;
1313
use function base64_encode;
1414
use function bin2hex;
15+
use function count;
1516
use function defined;
1617
use function file_get_contents;
1718
use function get_include_path;
@@ -26,6 +27,9 @@
2627
use function unlink;
2728
use function var_export;
2829
use PHPUnit\Event\Facade as EventFacade;
30+
use PHPUnit\Framework\RepeatTestSuite;
31+
use PHPUnit\Framework\RetryTestSuite;
32+
use PHPUnit\Framework\TestCase;
2933
use PHPUnit\Runner\CodeCoverage;
3034
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
3135
use PHPUnit\TextUI\Configuration\SourceMapper;
@@ -263,31 +267,43 @@ private function testClassCommand(TestClassWorkUnit $unit, array $offset, string
263267
$tests = [];
264268

265269
foreach ($unit->tests() as $test) {
266-
try {
267-
$data = base64_encode(serialize($test->providedData()));
268-
$dependencyInput = base64_encode(serialize($test->dependencyInput()));
269-
} catch (Throwable $t) {
270-
@unlink($resultFile);
271-
272-
throw new WorkerException(
273-
sprintf(
274-
'The tests of class %s cannot be run in parallel because their data cannot be serialized: %s',
275-
$unit->className(),
276-
$t->getMessage(),
277-
),
278-
);
270+
if ($test instanceof RetryTestSuite) {
271+
$aggregated = $test->tests();
272+
273+
assert(count($aggregated) === 1 && $aggregated[0] instanceof TestCase);
274+
275+
$tests[] = [
276+
'type' => 'retry',
277+
'name' => $test->name(),
278+
'maxAttempts' => $test->maxAttempts(),
279+
'test' => $this->testDescriptor($aggregated[0], $unit->className(), $resultFile),
280+
];
281+
282+
continue;
283+
}
284+
285+
if ($test instanceof RepeatTestSuite) {
286+
$repetitions = [];
287+
288+
foreach ($test->tests() as $repetition) {
289+
assert($repetition instanceof TestCase);
290+
291+
$repetitions[] = $this->testDescriptor($repetition, $unit->className(), $resultFile);
292+
}
293+
294+
$tests[] = [
295+
'type' => 'repeat',
296+
'name' => $test->name(),
297+
'failureThreshold' => $test->failureThreshold(),
298+
'tests' => $repetitions,
299+
];
300+
301+
continue;
279302
}
280303

281-
$tests[] = [
282-
'methodName' => $test->name(),
283-
'data' => $data,
284-
'dataName' => $test->dataName(),
285-
'dependencyInput' => $dependencyInput,
286-
'repetition' => $test->repetition(),
287-
'totalRepetitions' => $test->totalRepetitions(),
288-
'attempt' => $test->attempt(),
289-
'maxAttempts' => $test->maxAttempts(),
290-
];
304+
assert($test instanceof TestCase);
305+
306+
$tests[] = $this->testDescriptor($test, $unit->className(), $resultFile);
291307
}
292308

293309
return [
@@ -303,6 +319,47 @@ private function testClassCommand(TestClassWorkUnit $unit, array $offset, string
303319
];
304320
}
305321

322+
/**
323+
* The transportable description of a single test case, from which the
324+
* worker reconstructs it.
325+
*
326+
* @param class-string $className
327+
* @param non-empty-string $resultFile
328+
*
329+
* @throws WorkerException
330+
*
331+
* @return array<string, mixed>
332+
*/
333+
private function testDescriptor(TestCase $test, string $className, string $resultFile): array
334+
{
335+
try {
336+
$data = base64_encode(serialize($test->providedData()));
337+
$dependencyInput = base64_encode(serialize($test->dependencyInput()));
338+
} catch (Throwable $t) {
339+
@unlink($resultFile);
340+
341+
throw new WorkerException(
342+
sprintf(
343+
'The tests of class %s cannot be run in parallel because their data cannot be serialized: %s',
344+
$className,
345+
$t->getMessage(),
346+
),
347+
);
348+
}
349+
350+
return [
351+
'type' => 'test',
352+
'methodName' => $test->name(),
353+
'data' => $data,
354+
'dataName' => $test->dataName(),
355+
'dependencyInput' => $dependencyInput,
356+
'repetition' => $test->repetition(),
357+
'totalRepetitions' => $test->totalRepetitions(),
358+
'attempt' => $test->attempt(),
359+
'maxAttempts' => $test->maxAttempts(),
360+
];
361+
}
362+
306363
/**
307364
* Harvest the result of the unit the worker has just reported as finished.
308365
*/

src/Runner/Parallel/TestClassWorkUnit.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
namespace PHPUnit\Runner\Parallel;
1111

12+
use PHPUnit\Framework\IterativeTestSuite;
1213
use PHPUnit\Framework\TestCase;
1314

1415
/**
@@ -17,6 +18,11 @@
1718
* #[AfterClass]) and intra-class ordering are preserved when the unit is run by
1819
* a single worker.
1920
*
21+
* A member of the unit is either a single test case or an IterativeTestSuite
22+
* that aggregates the repetitions of a repeated test method or the attempts of
23+
* a retried test method; such a suite travels as one atomic member so that its
24+
* repetition and retry orchestration runs inside the worker.
25+
*
2026
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
2127
*
2228
* @internal This class is not covered by the backward compatibility promise for PHPUnit
@@ -34,14 +40,14 @@
3440
private string $className;
3541

3642
/**
37-
* @var list<TestCase>
43+
* @var list<IterativeTestSuite|TestCase>
3844
*/
3945
private array $tests;
4046

4147
/**
42-
* @param non-negative-int $index
43-
* @param class-string<TestCase> $className
44-
* @param list<TestCase> $tests
48+
* @param non-negative-int $index
49+
* @param class-string<TestCase> $className
50+
* @param list<IterativeTestSuite|TestCase> $tests
4551
*/
4652
public function __construct(int $index, string $className, array $tests)
4753
{
@@ -67,7 +73,7 @@ public function className(): string
6773
}
6874

6975
/**
70-
* @return list<TestCase>
76+
* @return list<IterativeTestSuite|TestCase>
7177
*/
7278
public function tests(): array
7379
{

0 commit comments

Comments
 (0)