mirror of
https://github.com/pestphp/pest.git
synced 2026-04-24 07:57:29 +02:00
feat(tia): continues to work on poc
This commit is contained in:
@ -28,9 +28,9 @@ final readonly class ChangedFiles
|
||||
|
||||
/**
|
||||
* @return array<int, string>|null `null` when git is unavailable, or when
|
||||
* the recorded SHA is no longer reachable
|
||||
* from HEAD (rebase / force-push) — in
|
||||
* that case the graph should be rebuilt.
|
||||
* the recorded SHA is no longer reachable
|
||||
* from HEAD (rebase / force-push) — in
|
||||
* that case the graph should be rebuilt.
|
||||
*/
|
||||
public function since(?string $sha): ?array
|
||||
{
|
||||
|
||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Support\Container;
|
||||
|
||||
/**
|
||||
* User-facing TIA configuration, returned by `pest()->tia()`.
|
||||
*
|
||||
@ -31,7 +33,9 @@ final class Configuration
|
||||
*/
|
||||
public function watch(array $patterns): self
|
||||
{
|
||||
WatchPatterns::instance()->add($patterns);
|
||||
/** @var WatchPatterns $watchPatterns */
|
||||
$watchPatterns = Container::getInstance()->get(WatchPatterns::class);
|
||||
$watchPatterns->add($patterns);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Support\Container;
|
||||
|
||||
/**
|
||||
* File-level Test Impact Analysis graph.
|
||||
*
|
||||
@ -127,7 +129,8 @@ final class Graph
|
||||
}
|
||||
|
||||
// 2. Watch-pattern lookup (non-PHP assets → test directories).
|
||||
$watchPatterns = WatchPatterns::instance();
|
||||
/** @var WatchPatterns $watchPatterns */
|
||||
$watchPatterns = Container::getInstance()->get(WatchPatterns::class);
|
||||
$normalised = [];
|
||||
|
||||
foreach ($changedFiles as $file) {
|
||||
@ -159,7 +162,7 @@ final class Graph
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string> All project-relative test files the graph knows.
|
||||
* @return array<int, string> All project-relative test files the graph knows.
|
||||
*/
|
||||
public function allTestFiles(): array
|
||||
{
|
||||
|
||||
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Captures per-test file coverage using the PCOV driver.
|
||||
@ -18,8 +17,6 @@ use ReflectionException;
|
||||
*/
|
||||
final class Recorder
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
|
||||
/**
|
||||
* Test file currently being recorded, or `null` when idle.
|
||||
*/
|
||||
@ -47,11 +44,6 @@ final class Recorder
|
||||
|
||||
private string $driver = 'none';
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
return self::$instance ??= new self;
|
||||
}
|
||||
|
||||
public function activate(): void
|
||||
{
|
||||
$this->active = true;
|
||||
|
||||
@ -1,159 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
/**
|
||||
* Shared TIA replay state consulted by Pest's `Testable` trait at runtime.
|
||||
*
|
||||
* Why a singleton: the plugin runs in `handleArguments` (before tests are
|
||||
* discovered), but the actual replay decision has to happen when each test
|
||||
* boots (`setUp` / `__runTest`). Those call sites are inside a trait that
|
||||
* has no easy way to inject dependencies, so they reach into this state
|
||||
* holder.
|
||||
*
|
||||
* Decision: a test file replays its previous pass iff
|
||||
* 1. TIA replay mode is active,
|
||||
* 2. the file is **known** to the dependency graph,
|
||||
* 3. the file is **not** in the affected set (its deps are unchanged),
|
||||
* 4. it was **not** in the previous run's defect list (only cached passes
|
||||
* replay; previously-failing tests rerun so users see current state).
|
||||
*
|
||||
* Points 1-3 live in this class. Point 4 uses PHPUnit's own
|
||||
* `DefaultResultCache`, queried at decision time.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class State
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
|
||||
private bool $replayMode = false;
|
||||
|
||||
/**
|
||||
* Keys are project-relative test file paths. Affected = must rerun.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $affectedFiles = [];
|
||||
|
||||
/**
|
||||
* Keys are project-relative test file paths. Known = recorded in graph.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $knownFiles = [];
|
||||
|
||||
/**
|
||||
* Test ids (class::method) that were in the previous run's defect list.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $previousDefects = [];
|
||||
|
||||
/**
|
||||
* Canonicalised project root used for relative-path calculations.
|
||||
*/
|
||||
private string $projectRoot = '';
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
return self::$instance ??= new self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on replay mode with the given graph + affected set.
|
||||
*
|
||||
* @param array<string, true> $affectedFiles
|
||||
* @param array<string, true> $previousDefects
|
||||
*/
|
||||
public function activate(string $projectRoot, Graph $graph, array $affectedFiles, array $previousDefects): void
|
||||
{
|
||||
$real = @realpath($projectRoot);
|
||||
|
||||
$this->projectRoot = $real !== false ? $real : $projectRoot;
|
||||
$this->replayMode = true;
|
||||
$this->affectedFiles = $affectedFiles;
|
||||
$this->previousDefects = $previousDefects;
|
||||
|
||||
// Pre-compute the known set from the graph so per-test lookups stay
|
||||
// O(1). Iterating edges once here beats calling `Graph::knowsTest`
|
||||
// from every test's `setUp`.
|
||||
$this->knownFiles = [];
|
||||
|
||||
foreach ($graph->allTestFiles() as $rel) {
|
||||
$this->knownFiles[$rel] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function isReplayMode(): bool
|
||||
{
|
||||
return $this->replayMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` when the given absolute test file should replay its
|
||||
* previous passing result instead of re-executing. `$testId` may be
|
||||
* `null` when the caller cannot cheaply determine it (e.g. early in
|
||||
* `setUp` before PHPUnit has published the name) — in that case we
|
||||
* replay iff the file is safe at the file level, and `__runTest` will
|
||||
* repeat the check with a proper id.
|
||||
*/
|
||||
public function shouldReplayFromCache(string $absoluteTestFile, ?string $testId = null): bool
|
||||
{
|
||||
if (! $this->replayMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rel = $this->relative($absoluteTestFile);
|
||||
|
||||
if ($rel === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! isset($this->knownFiles[$rel])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->affectedFiles[$rel])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($testId !== null && isset($this->previousDefects[$testId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->replayMode = false;
|
||||
$this->affectedFiles = [];
|
||||
$this->knownFiles = [];
|
||||
$this->previousDefects = [];
|
||||
$this->projectRoot = '';
|
||||
}
|
||||
|
||||
private function relative(string $path): ?string
|
||||
{
|
||||
if ($path === '' || $this->projectRoot === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$real = @realpath($path);
|
||||
|
||||
if ($real === false) {
|
||||
$real = $path;
|
||||
}
|
||||
|
||||
$root = rtrim($this->projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
|
||||
if (! str_starts_with($real, $root)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str_replace(DIRECTORY_SEPARATOR, '/', substr($real, strlen($root)));
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Contracts\TestCaseFilter;
|
||||
use Pest\Plugins\Tia\Graph;
|
||||
|
||||
/**
|
||||
* Accepts a test file in one of three cases:
|
||||
*
|
||||
* 1. The file falls outside the project root (we cannot reason about it, so
|
||||
* stay safe and run it).
|
||||
* 2. The graph has no record of the file — this is a new test that was
|
||||
* never part of a recording run, so we accept it by default. Skipping
|
||||
* unknown tests would be a correctness hazard (developers add tests and
|
||||
* TIA would silently not run them).
|
||||
* 3. The graph knows the file AND it is in the affected set.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class TiaTestCaseFilter implements TestCaseFilter
|
||||
{
|
||||
/**
|
||||
* @param array<string, true> $affectedTestFiles Keys are project-relative test file paths.
|
||||
*/
|
||||
public function __construct(
|
||||
private string $projectRoot,
|
||||
private Graph $graph,
|
||||
private array $affectedTestFiles,
|
||||
) {}
|
||||
|
||||
public function accept(string $testCaseFilename): bool
|
||||
{
|
||||
$rel = $this->relative($testCaseFilename);
|
||||
|
||||
if ($rel === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->graph->knowsTest($rel)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset($this->affectedTestFiles[$rel]);
|
||||
}
|
||||
|
||||
private function relative(string $path): ?string
|
||||
{
|
||||
$real = @realpath($path);
|
||||
|
||||
if ($real === false) {
|
||||
$real = $path;
|
||||
}
|
||||
|
||||
$root = rtrim($this->projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
|
||||
if (! str_starts_with($real, $root)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str_replace(DIRECTORY_SEPARATOR, '/', substr($real, strlen($root)));
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Browser\Support\BrowserTestIdentifier;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\TestSuite;
|
||||
|
||||
@ -72,7 +73,7 @@ final readonly class Browser implements WatchDefault
|
||||
// Scan TestRepository via BrowserTestIdentifier if pest-plugin-browser
|
||||
// is installed to find tests using `visit()` outside the conventional
|
||||
// Browser/ folder.
|
||||
if (class_exists(\Pest\Browser\Support\BrowserTestIdentifier::class)) {
|
||||
if (class_exists(BrowserTestIdentifier::class)) {
|
||||
$repo = TestSuite::getInstance()->tests;
|
||||
|
||||
foreach ($repo->getFilenames() as $filename) {
|
||||
@ -83,7 +84,7 @@ final readonly class Browser implements WatchDefault
|
||||
}
|
||||
|
||||
foreach ($factory->methods as $method) {
|
||||
if (\Pest\Browser\Support\BrowserTestIdentifier::isBrowserTest($method)) {
|
||||
if (BrowserTestIdentifier::isBrowserTest($method)) {
|
||||
$rel = $this->fileRelative($projectRoot, $filename);
|
||||
|
||||
if ($rel !== null) {
|
||||
|
||||
@ -22,7 +22,7 @@ interface WatchDefault
|
||||
public function applicable(): bool;
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>> glob → list of project-relative test dirs
|
||||
* @return array<string, array<int, string>> glob → list of project-relative test dirs
|
||||
*/
|
||||
public function defaults(string $projectRoot, string $testPath): array;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Plugins\Tia\WatchDefaults\WatchDefault;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
* Maps non-PHP file globs to the test directories they should invalidate.
|
||||
@ -37,17 +38,10 @@ final class WatchPatterns
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array<int, string>> glob → list of project-relative test dirs
|
||||
* @var array<string, array<int, string>> glob → list of project-relative test dirs
|
||||
*/
|
||||
private array $patterns = [];
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
return self::$instance ??= new self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Probes every registered `WatchDefault` and merges the patterns of
|
||||
* those that apply. Called once during Tia plugin boot, after BootFiles
|
||||
@ -56,7 +50,7 @@ final class WatchPatterns
|
||||
*/
|
||||
public function useDefaults(string $projectRoot): void
|
||||
{
|
||||
$testPath = \Pest\TestSuite::getInstance()->testPath;
|
||||
$testPath = TestSuite::getInstance()->testPath;
|
||||
|
||||
foreach (self::DEFAULTS as $class) {
|
||||
$default = new $class;
|
||||
@ -94,7 +88,7 @@ final class WatchPatterns
|
||||
*
|
||||
* @param string $projectRoot Absolute path.
|
||||
* @param array<int, string> $changedFiles Project-relative paths.
|
||||
* @return array<int, string> Project-relative test directories.
|
||||
* @return array<int, string> Project-relative test directories.
|
||||
*/
|
||||
public function matchedDirectories(string $projectRoot, array $changedFiles): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user