Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<directory suffix=".phpt">tests/end-to-end/logging/testdox</directory>
<directory suffix=".phpt">tests/end-to-end/metadata</directory>
<directory suffix=".phpt">tests/end-to-end/migration</directory>
<directory suffix=".phpt">tests/end-to-end/parallel</directory>
<directory suffix=".phpt">tests/end-to-end/phpt</directory>
<directory suffix=".phpt">tests/end-to-end/regression</directory>
<directory suffix=".phpt">tests/end-to-end/repeat</directory>
Expand All @@ -48,6 +49,11 @@
<exclude>tests/end-to-end/groups-from-configuration/_files</exclude>
<exclude>tests/end-to-end/logging/_files</exclude>
<exclude>tests/end-to-end/migration/_files</exclude>
<exclude>tests/end-to-end/parallel/phpt/_files</exclude>
<exclude>tests/end-to-end/parallel/phpt-coverage/_files</exclude>
<exclude>tests/end-to-end/parallel/phpt-conflicts/_files</exclude>
<exclude>tests/end-to-end/parallel/phpt-with-classes/_files</exclude>
<exclude>tests/end-to-end/parallel/repeat-retry/_files</exclude>
<exclude>tests/end-to-end/repeat/_files</exclude>
<exclude>tests/end-to-end/retry/_files</exclude>
<exclude>tests/end-to-end/self-direct-indirect/_files</exclude>
Expand Down
46 changes: 46 additions & 0 deletions src/Event/CollectingEmitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Event;

/**
* An emitter that collects the events emitted through it into an event
* collection that can later be flushed and replayed, without disturbing the
* global event facade.
*
* Several of these can be in use at the same time in one process, which is what
* lets the parallel test runner run several PHPT tests concurrently in the main
* process, each collecting its own events for deterministic, suite-ordered
* replay through Facade::forward().
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class CollectingEmitter
{
private Emitter $emitter;
private CollectingDispatcher $dispatcher;

public function __construct(Emitter $emitter, CollectingDispatcher $dispatcher)
{
$this->emitter = $emitter;
$this->dispatcher = $dispatcher;
}

public function emitter(): Emitter
{
return $this->emitter;
}

public function flush(): EventCollection
{
return $this->dispatcher->flush();
}
}
33 changes: 33 additions & 0 deletions src/Event/Dispatcher/CollectingDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
namespace PHPUnit\Event;

use function assert;
use PHPUnit\Runner\DeprecationCollector\Facade as DeprecationCollector;
use PHPUnit\Runner\DeprecationCollector\TestTriggeredDeprecationSubscriber;

Expand All @@ -21,6 +22,7 @@ final class CollectingDispatcher implements Dispatcher
{
private EventCollection $events;
private DirectDispatcher $isolatedDirectDispatcher;
private ?EventCollection $collectedEvents = null;

public function __construct(DirectDispatcher $directDispatcher)
{
Expand All @@ -32,6 +34,12 @@ public function __construct(DirectDispatcher $directDispatcher)

public function dispatch(Event $event): void
{
if ($this->collectedEvents !== null) {
$this->collectedEvents->add($event);

return;
}

$this->events->add($event);

try {
Expand All @@ -41,6 +49,31 @@ public function dispatch(Event $event): void
}
}

/**
* Open a collection window: until stopCollectingEvents() is called, events
* are diverted into a separate collection instead of being recorded and
* dispatched. This mirrors the collection window of the DeferringDispatcher
* so that a RetryTestSuite can suppress the events of a failed attempt when
* it runs in a process whose event facade was initialized for isolation.
*/
public function startCollectingEvents(): void
{
assert($this->collectedEvents === null);

$this->collectedEvents = new EventCollection;
}

public function stopCollectingEvents(): EventCollection
{
assert($this->collectedEvents !== null);

$events = $this->collectedEvents;

$this->collectedEvents = null;

return $events;
}

public function flush(): EventCollection
{
$events = $this->events;
Expand Down
54 changes: 50 additions & 4 deletions src/Event/Facade.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ final class Facade
{
private static ?self $instance = null;
private Emitter $emitter;
private ?TypeMap $typeMap = null;
private ?DeferringDispatcher $deferringDispatcher = null;
private bool $sealed = false;
private ?TypeMap $typeMap = null;
private ?DeferringDispatcher $deferringDispatcher = null;
private ?CollectingDispatcher $isolationDispatcher = null;
private bool $sealed = false;

public static function instance(): self
{
Expand Down Expand Up @@ -108,25 +109,70 @@ public function initForIsolation(HRTime $offset): CollectingDispatcher

$this->sealed = true;

$this->isolationDispatcher = $dispatcher;

return $dispatcher;
}

/**
* Mint an emitter that collects its events into an independent collection,
* without sealing this facade or replacing its emitter. Unlike
* initForIsolation(), which can only establish one isolated emitter for the
* whole process, any number of these can be in use at once; the parallel
* test runner uses one per concurrently running PHPT test.
*/
public function collectingEmitter(): CollectingEmitter
{
$dispatcher = new CollectingDispatcher(
new DirectDispatcher($this->typeMap()),
);

return new CollectingEmitter(
new DispatchingEmitter(
$dispatcher,
$this->createTelemetrySystem(),
),
$dispatcher,
);
}

public function forward(EventCollection $events): void
{
$dispatcher = $this->deferredDispatcher();
if ($this->isolationDispatcher !== null) {
$dispatcher = $this->isolationDispatcher;
} else {
$dispatcher = $this->deferredDispatcher();
}

foreach ($events as $event) {
$dispatcher->dispatch($event);
}
}

/**
* In a process whose event facade was initialized for isolation — a
* parallel test runner worker, for example — the collection window is
* opened on the isolation dispatcher, because that is the dispatcher the
* emitter dispatches to there; the deferring dispatcher is unused in such
* a process.
*/
public function startCollectingEvents(): void
{
if ($this->isolationDispatcher !== null) {
$this->isolationDispatcher->startCollectingEvents();

return;
}

$this->deferredDispatcher()->startCollectingEvents();
}

public function stopCollectingEvents(): EventCollection
{
if ($this->isolationDispatcher !== null) {
return $this->isolationDispatcher->stopCollectingEvents();
}

return $this->deferredDispatcher()->stopCollectingEvents();
}

Expand Down
22 changes: 22 additions & 0 deletions src/Framework/Attributes/DoNotRunInParallel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\Attributes;

use Attribute;

/**
* @immutable
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final readonly class DoNotRunInParallel
{
}
6 changes: 6 additions & 0 deletions src/Framework/IterativeTestSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ final protected function dependencies(): array
*/
abstract protected function execute(array $tests, Event\Emitter $emitter): void;

/**
* Synthesize a test attempt event from the events an attempt collected, so
* that a failed or errored attempt that is going to be retried is reported
* as an attempt rather than as the test's final outcome. Returns whether
* such an event could be determined from the collected events.
*/
final protected function emitAttemptEvent(EventCollection $events, Event\Emitter $emitter): bool
{
$duration = $this->durationOf($events);
Expand Down
8 changes: 8 additions & 0 deletions src/Framework/RepeatTestSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public static function fromTests(string $name, array $tests, int $failureThresho
return $suite;
}

/**
* @return positive-int
*/
public function failureThreshold(): int
{
return $this->failureThreshold;
}

/**
* @param list<Test> $tests
*/
Expand Down
80 changes: 80 additions & 0 deletions src/Framework/TestRunner/ChildProcessResultEnvelope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\TestRunner;

use function hash_equals;
use function strlen;
use function substr;
use PHPUnit\Runner\CodeCoverage;
use SebastianBergmann\CodeCoverage\CodeCoverage as CodeCoverageData;

/**
* The mechanics shared by the two consumers of the serialized result envelope
* that a process-isolated child or a parallel worker writes back to the parent:
* the ChildProcessResultProcessor and the parallel ResultAggregator.
*
* Only the parts that are identical between the two live here. The shape each
* one requires of the decoded envelope, and the way each reports a malformed
* result, differ and stay with each consumer.
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class ChildProcessResultEnvelope
{
/**
* Verify the nonce that prefixes the serialized result and strip it,
* returning the bare payload.
*
* A null nonce or an empty payload is passed through unverified — the caller
* then fails on the empty or unexpected payload when it tries to decode it.
* Null is returned only when a nonce was expected but the prefix does not
* match it, which means the result was written by an unexpected process or
* was tampered with.
*
* @param ?non-empty-string $nonce
*/
public static function verifyAndStripNonce(string $serialized, ?string $nonce): ?string
{
if ($nonce === null || $serialized === '') {
return $serialized;
}

$length = strlen($nonce);

if (strlen($serialized) < $length ||
!hash_equals($nonce, substr($serialized, 0, $length))) {
return null;
}

return substr($serialized, $length);
}

/**
* Merge the code coverage carried by a decoded result envelope into the
* parent's coverage, when coverage collection is active and the envelope
* actually carries it.
*/
public static function mergeCodeCoverage(object $result, CodeCoverage $codeCoverage): void
{
if (!$codeCoverage->isActive()) {
return;
}

// @codeCoverageIgnoreStart
if (!isset($result->codeCoverage) || !$result->codeCoverage instanceof CodeCoverageData) {
return;
}

CodeCoverage::instance()->codeCoverage()->merge($result->codeCoverage);
// @codeCoverageIgnoreEnd
}
}
Loading