mirror of
https://github.com/pestphp/pest.git
synced 2026-04-21 06:27:28 +02:00
wip
This commit is contained in:
@ -249,6 +249,17 @@ trait Testable
|
||||
return;
|
||||
}
|
||||
|
||||
// Risky tests have no public PHPUnit hook to replay as-risky.
|
||||
// Best available: short-circuit as a pass so the test doesn't
|
||||
// misreport as a failure. Aggregate risky totals won't
|
||||
// survive replay — accepted trade-off until PHPUnit grows a
|
||||
// programmatic risky-marker API.
|
||||
if ($cached->isRisky()) {
|
||||
$this->__cachedPass = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-success: throw the matching PHPUnit exception. Runner
|
||||
// catches it and marks the test with the correct status so
|
||||
// skips, failures, incompletes and todos appear in output
|
||||
@ -371,7 +382,14 @@ trait Testable
|
||||
private function __runTest(Closure $closure, ...$args): mixed
|
||||
{
|
||||
if ($this->__cachedPass) {
|
||||
$this->addToAssertionCount(1);
|
||||
// Feed the exact assertion count captured during the recorded
|
||||
// run so Pest's "Tests: N passed (M assertions)" banner stays
|
||||
// accurate on replay instead of collapsing to 1-per-test.
|
||||
/** @var Tia $tia */
|
||||
$tia = Container::getInstance()->get(Tia::class);
|
||||
$assertions = $tia->getCachedAssertions($this::class.'::'.$this->name());
|
||||
|
||||
$this->addToAssertionCount($assertions > 0 ? $assertions : 1);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ use Pest\Plugins\Actions\CallsBoot;
|
||||
use Pest\Plugins\Actions\CallsHandleArguments;
|
||||
use Pest\Plugins\Actions\CallsHandleOriginalArguments;
|
||||
use Pest\Plugins\Actions\CallsTerminable;
|
||||
use Pest\Plugins\Tia;
|
||||
use Pest\Support\Container;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\View;
|
||||
@ -37,6 +36,7 @@ final readonly class Kernel
|
||||
*/
|
||||
private const array BOOTSTRAPPERS = [
|
||||
Bootstrappers\BootOverrides::class,
|
||||
Plugins\Tia\Bootstrapper::class,
|
||||
Bootstrappers\BootSubscribers::class,
|
||||
Bootstrappers\BootFiles::class,
|
||||
Bootstrappers\BootView::class,
|
||||
@ -65,17 +65,7 @@ final readonly class Kernel
|
||||
->add(TestSuite::class, $testSuite)
|
||||
->add(InputInterface::class, $input)
|
||||
->add(OutputInterface::class, $output)
|
||||
->add(Container::class, $container)
|
||||
->add(Tia\Recorder::class, new Tia\Recorder)
|
||||
->add(Tia\CoverageCollector::class, new Tia\CoverageCollector)
|
||||
->add(Tia\WatchPatterns::class, new Tia\WatchPatterns)
|
||||
->add(Tia\ResultCollector::class, new Tia\ResultCollector)
|
||||
->add(Tia\Contracts\State::class, new Tia\FileState(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'.temp'))
|
||||
->add(Tia\BaselineSync::class, new Tia\BaselineSync(
|
||||
$container->get(Tia\Contracts\State::class), // @phpstan-ignore argument.type
|
||||
$output,
|
||||
$input,
|
||||
));
|
||||
->add(Container::class, $container);
|
||||
|
||||
$kernel = new self(
|
||||
new Application,
|
||||
|
||||
@ -142,6 +142,16 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
*/
|
||||
private int $executedCount = 0;
|
||||
|
||||
/**
|
||||
* Cached assertion count per test id for the current replay run. Keyed
|
||||
* by `ClassName::methodName`; populated when `getCachedResult()` hits
|
||||
* cache and drained by `Testable::__runTest()` on the short-circuit
|
||||
* path so the emitted count matches the recorded run.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $cachedAssertionsByTestId = [];
|
||||
|
||||
/**
|
||||
* Captured at replay setup so the end-of-run summary can report the
|
||||
* scope of the changes that drove the run.
|
||||
@ -268,6 +278,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
|
||||
if ($result !== null) {
|
||||
$this->replayedCount++;
|
||||
// Cache the assertion count alongside the status so `Testable`
|
||||
// can emit the exact `addToAssertionCount()` at replay time
|
||||
// without hitting the graph twice per test.
|
||||
$assertions = $this->replayGraph->getAssertions($this->branch, $testId);
|
||||
$this->cachedAssertionsByTestId[$testId] = $assertions ?? 0;
|
||||
} else {
|
||||
// Graph knows the test file but has no stored result for this
|
||||
// specific test id (new test, or first time seeing this method).
|
||||
@ -278,6 +293,17 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact assertion count captured for the given test during its last
|
||||
* recorded run. Returns `0` if unknown (new test, or old graph entry
|
||||
* pre-dating assertion-count tracking). `Testable::__runTest` reads
|
||||
* this to feed `addToAssertionCount()` instead of defaulting to 1.
|
||||
*/
|
||||
public function getCachedAssertions(string $testId): int
|
||||
{
|
||||
return $this->cachedAssertionsByTestId[$testId] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@ -1101,7 +1127,14 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
foreach ($results as $testId => $result) {
|
||||
$graph->setResult($this->branch, $testId, $result['status'], $result['message'], $result['time']);
|
||||
$graph->setResult(
|
||||
$this->branch,
|
||||
$testId,
|
||||
$result['status'],
|
||||
$result['message'],
|
||||
$result['time'],
|
||||
$result['assertions'],
|
||||
);
|
||||
}
|
||||
|
||||
$this->saveGraph($graph);
|
||||
|
||||
47
src/Plugins/Tia/Bootstrapper.php
Normal file
47
src/Plugins/Tia/Bootstrapper.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Contracts\Bootstrapper as BootstrapperContract;
|
||||
use Pest\Plugins\Tia\Contracts\State;
|
||||
use Pest\Support\Container;
|
||||
|
||||
/**
|
||||
* Plugin-level container registrations for TIA. Runs as part of Kernel's
|
||||
* bootstrapper chain so Tia's own service graph is set up without Kernel
|
||||
* having to know about any of its internals.
|
||||
*
|
||||
* Most Tia services (`Recorder`, `CoverageCollector`, `WatchPatterns`,
|
||||
* `ResultCollector`, `BaselineSync`) are auto-buildable — Pest's container
|
||||
* resolves them lazily via constructor reflection. The only service that
|
||||
* requires an explicit binding is the `State` contract, because the
|
||||
* filesystem implementation needs a root-directory string that reflection
|
||||
* can't infer.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class Bootstrapper implements BootstrapperContract
|
||||
{
|
||||
public function __construct(private Container $container) {}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->container->add(State::class, new FileState($this->tempDir()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Pest's `.temp/` directory relative to this file so TIA's
|
||||
* caches share the same location as the rest of Pest's transient
|
||||
* state (PHPUnit result cache, coverage PHP dumps, etc.).
|
||||
*/
|
||||
private function tempDir(): string
|
||||
{
|
||||
return __DIR__
|
||||
.DIRECTORY_SEPARATOR.'..'
|
||||
.DIRECTORY_SEPARATOR.'..'
|
||||
.DIRECTORY_SEPARATOR.'..'
|
||||
.DIRECTORY_SEPARATOR.'.temp';
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ final class Graph
|
||||
* @var array<string, array{
|
||||
* sha: ?string,
|
||||
* tree: array<string, string>,
|
||||
* results: array<string, array{status: int, message: string, time: float}>
|
||||
* results: array<string, array{status: int, message: string, time: float, assertions?: int}>
|
||||
* }>
|
||||
*/
|
||||
private array $baselines = [];
|
||||
@ -257,14 +257,36 @@ final class Graph
|
||||
$this->baselines[$branch]['sha'] = $sha;
|
||||
}
|
||||
|
||||
public function setResult(string $branch, string $testId, int $status, string $message, float $time): void
|
||||
public function setResult(string $branch, string $testId, int $status, string $message, float $time, int $assertions = 0): void
|
||||
{
|
||||
$this->ensureBaseline($branch);
|
||||
$this->baselines[$branch]['results'][$testId] = [
|
||||
'status' => $status, 'message' => $message, 'time' => $time,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'time' => $time,
|
||||
'assertions' => $assertions,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached assertion count for a test, or `null` if unknown.
|
||||
* Callers use this to feed `addToAssertionCount()` at replay time so
|
||||
* the "Tests: N passed (M assertions)" banner matches the recorded run
|
||||
* instead of defaulting to 1 assertion per test.
|
||||
*/
|
||||
public function getAssertions(string $branch, string $testId, string $fallbackBranch = 'main'): ?int
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
if (! isset($baseline['results'][$testId]['assertions'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $baseline['results'][$testId]['assertions'];
|
||||
|
||||
return is_int($value) ? $value : null;
|
||||
}
|
||||
|
||||
public function getResult(string $branch, string $testId, string $fallbackBranch = 'main'): ?TestStatus
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
@ -310,7 +332,7 @@ final class Graph
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{sha: ?string, tree: array<string, string>, results: array<string, array{status: int, message: string, time: float}>}
|
||||
* @return array{sha: ?string, tree: array<string, string>, results: array<string, array{status: int, message: string, time: float, assertions?: int}>}
|
||||
*/
|
||||
private function baselineFor(string $branch, string $fallbackBranch): array
|
||||
{
|
||||
|
||||
@ -118,6 +118,17 @@ final class ResultCollector
|
||||
$this->startTime = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the Finished subscriber after a test's outcome + assertion
|
||||
* events have all fired. Clears the "currently recording" pointer so
|
||||
* the next test's events don't get mis-attributed.
|
||||
*/
|
||||
public function finishTest(): void
|
||||
{
|
||||
$this->currentTestId = null;
|
||||
$this->startTime = null;
|
||||
}
|
||||
|
||||
private function record(int $status, string $message): void
|
||||
{
|
||||
if ($this->currentTestId === null) {
|
||||
@ -128,14 +139,17 @@ final class ResultCollector
|
||||
? round(microtime(true) - $this->startTime, 3)
|
||||
: 0.0;
|
||||
|
||||
// PHPUnit can fire more than one outcome event per test — the
|
||||
// canonical case is a risky pass (`Passed` then `ConsideredRisky`).
|
||||
// Last-wins semantics preserve the most specific status; the
|
||||
// existing assertion count (if any) survives the overwrite.
|
||||
$existing = $this->results[$this->currentTestId] ?? null;
|
||||
|
||||
$this->results[$this->currentTestId] = [
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'time' => $time,
|
||||
'assertions' => 0,
|
||||
'assertions' => $existing['assertions'] ?? 0,
|
||||
];
|
||||
|
||||
$this->currentTestId = null;
|
||||
$this->startTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,5 +30,11 @@ final class EnsureTiaAssertionsAreRecordedOnFinished implements FinishedSubscrib
|
||||
$event->numberOfAssertionsPerformed(),
|
||||
);
|
||||
}
|
||||
|
||||
// Close the "currently recording" window on Finished so the next
|
||||
// test's events don't get mis-attributed. Keeping the pointer open
|
||||
// through the outcome subscribers is what lets a late-firing
|
||||
// `ConsideredRisky` overwrite an earlier `Passed`.
|
||||
$this->collector->finishTest();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user