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
|
// The collector occasionally hands us nothing usable: PHPUnit's
|
||||||
// Prepared event can miss the file for Pest-generated classes,
|
// Prepared event can miss the file for Pest-generated classes,
|
||||||
// and an eval'd class path (".../IndexTest.php(1) : eval()'d code")
|
// and an eval'd class path (".../IndexTest.php(1) : eval()'d code")
|
||||||
// would be rejected later by Graph::relative(). Reflect on the
|
// would be rejected later by Graph::relative(). Recover the real
|
||||||
// class embedded in the test ID as a fallback so the failure
|
// path from the class embedded in the test ID — without it,
|
||||||
// gets stored *with* a file — without it, filtered runs lose
|
// filtered runs lose the ability to re-run only the failing test
|
||||||
// the ability to re-run only the failing test next time and
|
// next time.
|
||||||
// bail out to the full suite.
|
|
||||||
if ($file === null || (is_string($file) && str_contains($file, "eval()'d"))) {
|
if ($file === null || (is_string($file) && str_contains($file, "eval()'d"))) {
|
||||||
$class = strstr($testId, '::', true);
|
$file = self::resolveFailedTestFile($testId);
|
||||||
|
|
||||||
if (is_string($class) && $class !== '') {
|
|
||||||
try {
|
|
||||||
$reflected = (new \ReflectionClass($class))->getFileName();
|
|
||||||
$file = $reflected === false ? null : $reflected;
|
|
||||||
} catch (\ReflectionException) {
|
|
||||||
$file = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$graph->setResult(
|
$graph->setResult(
|
||||||
@ -1283,6 +1273,63 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
$collector->reset();
|
$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
|
private function coverageReportActive(): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -150,14 +150,28 @@ final class Recorder
|
|||||||
\pcov\stop();
|
\pcov\stop();
|
||||||
/** @var array<string, mixed> $data */
|
/** @var array<string, mixed> $data */
|
||||||
$data = \pcov\collect(\pcov\all);
|
$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 {
|
} else {
|
||||||
/** @var array<string, mixed> $data */
|
/** @var array<string, mixed> $data */
|
||||||
$data = \xdebug_get_code_coverage();
|
$data = \xdebug_get_code_coverage();
|
||||||
// `true` resets Xdebug's buffer; without it the next start() accumulates prior test coverage.
|
// `true` resets Xdebug's buffer; without it the next start() accumulates prior test coverage.
|
||||||
\xdebug_stop_code_coverage(true);
|
\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;
|
$this->perTestFiles[$this->currentTestFile][$sourceFile] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +186,7 @@ final class Recorder
|
|||||||
|
|
||||||
// Walk covered classes' interfaces/traits/parents. Interfaces have no executable bytecode,
|
// Walk covered classes' interfaces/traits/parents. Interfaces have no executable bytecode,
|
||||||
// so a signature change would leave implementing-class tests stale without this walk.
|
// 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->currentTestFile = null;
|
||||||
$this->includedFilesAtTestStart = [];
|
$this->includedFilesAtTestStart = [];
|
||||||
@ -641,6 +655,37 @@ final class Recorder
|
|||||||
return is_string($file) ? $file : null;
|
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
|
public function reset(): void
|
||||||
{
|
{
|
||||||
$this->currentTestFile = null;
|
$this->currentTestFile = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user