mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 02:52:12 +02:00
wip
This commit is contained in:
@ -1250,22 +1250,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
// The collector occasionally hands us nothing usable: PHPUnit's
|
||||
// Prepared event can miss the file for Pest-generated classes,
|
||||
// and an eval'd class path (".../IndexTest.php(1) : eval()'d code")
|
||||
// would be rejected later by Graph::relative(). Reflect on the
|
||||
// class embedded in the test ID as a fallback so the failure
|
||||
// gets stored *with* a file — without it, filtered runs lose
|
||||
// the ability to re-run only the failing test next time and
|
||||
// bail out to the full suite.
|
||||
// would be rejected later by Graph::relative(). Recover the real
|
||||
// path from the class embedded in the test ID — without it,
|
||||
// filtered runs lose the ability to re-run only the failing test
|
||||
// next time.
|
||||
if ($file === null || (is_string($file) && str_contains($file, "eval()'d"))) {
|
||||
$class = strstr($testId, '::', true);
|
||||
|
||||
if (is_string($class) && $class !== '') {
|
||||
try {
|
||||
$reflected = (new \ReflectionClass($class))->getFileName();
|
||||
$file = $reflected === false ? null : $reflected;
|
||||
} catch (\ReflectionException) {
|
||||
$file = null;
|
||||
}
|
||||
}
|
||||
$file = self::resolveFailedTestFile($testId);
|
||||
}
|
||||
|
||||
$graph->setResult(
|
||||
@ -1283,6 +1273,63 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
$collector->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the source file for a Pest-generated test class.
|
||||
*
|
||||
* Pest synthesises a per-test class via `eval()` and writes the
|
||||
* original test file path to a `private static $__filename` property
|
||||
* (see `src/Factories/TestCaseFactory.php`). Reflecting on the class
|
||||
* with `getFileName()` would return the eval'd location, which
|
||||
* `Graph::relative()` rejects — losing the file mapping.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Read the `__filename` static if the class declares it (Pest
|
||||
* tests).
|
||||
* 2. Otherwise use `getFileName()` and skip eval'd frames by
|
||||
* walking up the parent class chain — a plain PHPUnit test
|
||||
* lives in a real file at the top of that chain.
|
||||
*/
|
||||
private static function resolveFailedTestFile(string $testId): ?string
|
||||
{
|
||||
$class = strstr($testId, '::', true);
|
||||
|
||||
if (! is_string($class) || $class === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
} catch (\ReflectionException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($reflection->hasProperty('__filename')) {
|
||||
try {
|
||||
$filename = $reflection->getStaticPropertyValue('__filename');
|
||||
} catch (\ReflectionException) {
|
||||
$filename = null;
|
||||
}
|
||||
|
||||
if (is_string($filename) && $filename !== '' && ! str_contains($filename, "eval()'d")) {
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
|
||||
$current = $reflection;
|
||||
|
||||
while ($current !== false) {
|
||||
$file = $current->getFileName();
|
||||
|
||||
if (is_string($file) && $file !== '' && ! str_contains($file, "eval()'d")) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$current = $current->getParentClass();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function coverageReportActive(): bool
|
||||
{
|
||||
try {
|
||||
|
||||
@ -150,14 +150,28 @@ final class Recorder
|
||||
\pcov\stop();
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = \pcov\collect(\pcov\all);
|
||||
|
||||
// pcov returns every executable line in every file it
|
||||
// tracked: positive values for executed lines, `-1` for
|
||||
// executable-but-not-run. A file with no positives was
|
||||
// loaded but nothing in it ran during this test's window
|
||||
// — typically a declaration-only file (Mailables, Enums,
|
||||
// DTOs) pulled in by some service-provider's static `use`
|
||||
// at framework boot. Including those attributes every
|
||||
// globally-bootstrapped class to whichever test triggered
|
||||
// the boot, blowing up the affected set on edits to those
|
||||
// files.
|
||||
$coveredFiles = self::filesWithExecutedLines($data);
|
||||
} else {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = \xdebug_get_code_coverage();
|
||||
// `true` resets Xdebug's buffer; without it the next start() accumulates prior test coverage.
|
||||
\xdebug_stop_code_coverage(true);
|
||||
|
||||
$coveredFiles = array_keys($data);
|
||||
}
|
||||
|
||||
foreach (array_keys($data) as $sourceFile) {
|
||||
foreach ($coveredFiles as $sourceFile) {
|
||||
$this->perTestFiles[$this->currentTestFile][$sourceFile] = true;
|
||||
}
|
||||
|
||||
@ -172,7 +186,7 @@ final class Recorder
|
||||
|
||||
// Walk covered classes' interfaces/traits/parents. Interfaces have no executable bytecode,
|
||||
// so a signature change would leave implementing-class tests stale without this walk.
|
||||
$this->linkSourceDependencies(array_keys($data));
|
||||
$this->linkSourceDependencies($coveredFiles);
|
||||
|
||||
$this->currentTestFile = null;
|
||||
$this->includedFilesAtTestStart = [];
|
||||
@ -641,6 +655,37 @@ final class Recorder
|
||||
return is_string($file) ? $file : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pcov's `file => line => executionCount` map to the files
|
||||
* that actually had at least one executed line. pcov reports `-1`
|
||||
* for "executable but not run" and a positive count for executed
|
||||
* lines; a file with no positives was loaded but contributed no
|
||||
* executed code to this test.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function filesWithExecutedLines(array $data): array
|
||||
{
|
||||
$out = [];
|
||||
|
||||
foreach ($data as $file => $lines) {
|
||||
if (! is_string($file) || ! is_array($lines)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($lines as $count) {
|
||||
if (is_int($count) && $count > 0) {
|
||||
$out[] = $file;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->currentTestFile = null;
|
||||
|
||||
Reference in New Issue
Block a user