diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 1fdda42d..beb9b2f1 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -274,24 +274,11 @@ trait Testable self::$__latestIssues = $method->issues; self::$__latestPrs = $method->prs; - // TIA replay short-circuit. Runs AFTER dataset/description/ - // assignee metadata is populated so output and filtering still - // see the correct test name + tags on a cache hit, but BEFORE - // `parent::setUp()` and `beforeEach` so we skip the user's - // fixture setup (which is the whole point of replay — avoid - // paying for work whose outcome we already know). /** @var Tia $tia */ $tia = Container::getInstance()->get(Tia::class); $cached = $tia->getCachedResult(self::$__filename, $this::class.'::'.$this->name()); if ($cached !== null) { - // Risky has no public PHPUnit hook to replay as-risky, so we - // collapse it into Pass — the test doesn't misreport as a - // failure, at the cost of losing aggregate risky totals on - // replay (accepted until PHPUnit grows a programmatic - // risky-marker API). Skipped/Incomplete throw the matching - // PHPUnit exception so the runner marks the status exactly - // as it did on the recorded run. match (Replay::from($cached)) { Replay::Pass => $this->__shortCircuitCachedPass(), Replay::Skipped => $this->markTestSkipped($cached->message()), diff --git a/src/Plugins/Tia.php b/src/Plugins/Tia.php index ed8abbb6..b20b8781 100644 --- a/src/Plugins/Tia.php +++ b/src/Plugins/Tia.php @@ -357,14 +357,14 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $this->seedResultsInto($graph); if (! $this->saveGraph($graph)) { - $this->renderBadge('ERROR', 'TIA could not write the dependency graph.'); + $this->renderBadge('ERROR', 'Could not write the dependency graph.'); $recorder->reset(); return; } $this->renderBadge('INFO', sprintf( - 'TIA recorded the dependency graph (%d test file%s).', + 'Recorded the dependency graph (%d test file%s).', count($perTest), count($perTest) === 1 ? '' : 's', )); @@ -488,7 +488,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return $exitCode; } - $this->renderBadge('ERROR', 'TIA recorded zero edges — coverage driver likely missing.'); + $this->renderBadge('ERROR', 'Recorded zero edges — coverage driver likely missing.'); $this->renderDetail('Install / enable pcov or xdebug (mode: coverage) in the worker PHP and retry.'); return $exitCode; @@ -504,13 +504,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable } if (! $this->saveGraph($graph)) { - $this->renderBadge('ERROR', 'TIA could not write the dependency graph.'); + $this->renderBadge('ERROR', 'Could not write the dependency graph.'); return $exitCode; } $this->renderBadge('INFO', sprintf( - 'TIA recorded the dependency graph (%d test file%s, %d worker partial%s).', + 'Recorded the dependency graph (%d test file%s, %d worker partial%s).', count($finalised), count($finalised) === 1 ? '' : 's', count($partialKeys), @@ -536,8 +536,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable if (! Fingerprint::structuralMatches($stored, $current)) { $drift = Fingerprint::structuralDrift($stored, $current); - $this->renderBadge('WARN', sprintf( - 'TIA graph structure outdated (%s).', + $this->renderBadge('INFO', sprintf( + 'Graph structure outdated (%s).', $this->formatStructuralDrift($drift), )); @@ -560,7 +560,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return $this->reconcileFingerprint($rebuilt, $current); } - $this->renderBadge('WARN', 'TIA rebuilding graph from scratch.'); + $this->renderBadge('WARN', 'Rebuilding graph from scratch.'); $this->state->delete(self::KEY_GRAPH); $this->state->delete(self::KEY_COVERAGE_CACHE); @@ -572,7 +572,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable if ($drift !== []) { $this->renderBadge('WARN', sprintf( - 'TIA env differs from baseline (%s) — results dropped, edges reused.', + 'Env differs from baseline (%s) — results dropped, edges reused.', implode(', ', $drift), )); @@ -621,7 +621,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable if ($changedFiles->gitAvailable() && $branchSha !== null && $changedFiles->since($branchSha) === null) { - $this->renderBadge('WARN', 'TIA recorded commit is no longer reachable — graph will be rebuilt.'); + $this->renderBadge('WARN', 'Recorded commit is no longer reachable — graph will be rebuilt.'); $graph = null; } } @@ -794,7 +794,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $changedFiles = new ChangedFiles($projectRoot); if (! $changedFiles->gitAvailable()) { - $this->renderBadge('WARN', 'TIA git unavailable — running full suite.'); + $this->renderBadge('WARN', 'Git unavailable — running full suite.'); return $arguments; } @@ -811,9 +811,9 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $coverageAvailable = $this->piggybackCoverage || $this->recorder->driverAvailable(); if ($hasProjectPhpSourceChanges && ! $coverageAvailable) { - $this->renderBadge('WARN', 'TIA detected PHP source changes but no coverage driver is available.'); + $this->renderBadge('WARN', 'Detected PHP source changes but no coverage driver is available.'); $this->renderDetail('Running the full suite to avoid using a stale dependency graph.'); - $this->renderDetail('Install / enable pcov or xdebug (mode: coverage) so TIA can safely refresh edges after PHP refactors.'); + $this->renderDetail('Install / enable pcov or xdebug (mode: coverage) so edges can be safely refreshed after PHP refactors.'); return $arguments; } @@ -867,7 +867,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable } if (! $this->persistAffectedSet($affected)) { - $this->renderBadge('ERROR', 'TIA could not persist affected set — running full suite.'); + $this->renderBadge('ERROR', 'Could not persist affected set — running full suite.'); return $arguments; } @@ -1013,9 +1013,9 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable } $this->renderBadge('INFO', $this->piggybackCoverage - ? 'TIA recording dependency graph in parallel via --coverage (first run) — '. + ? 'Recording dependency graph in parallel via --coverage (first run) — '. 'subsequent --tia runs will only re-execute affected tests.' - : 'TIA recording dependency graph in parallel (first run) — '. + : 'Recording dependency graph in parallel (first run) — '. 'subsequent --tia runs will only re-execute affected tests.'); return $arguments; @@ -1024,7 +1024,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable if ($this->piggybackCoverage) { $this->recordingActive = true; - $this->renderBadge('INFO', 'TIA recording dependency graph via --coverage (first run) — '. + $this->renderBadge('INFO', 'Recording dependency graph via --coverage (first run) — '. 'subsequent --tia runs will only re-execute affected tests.'); return $arguments; @@ -1034,7 +1034,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $this->recordingActive = true; $this->renderBadge('INFO', sprintf( - 'TIA recording dependency graph via %s (first run) — '. + 'Recording dependency graph via %s (first run) — '. 'subsequent --tia runs will only re-execute affected tests.', $recorder->driver(), )); @@ -1044,8 +1044,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable private function emitCoverageDriverMissing(): void { - $this->renderBadge('WARN', 'No coverage driver is available — TIA skipped.'); - $this->renderDetail('TIA needs ext-pcov or Xdebug with coverage mode enabled to record the dependency graph.'); + $this->renderBadge('WARN', 'No coverage driver is available — skipped.'); + $this->renderDetail('Needs ext-pcov or Xdebug with coverage mode enabled to record the dependency graph.'); $this->renderDetail('Install or enable one and rerun with --tia.'); } @@ -1069,6 +1069,9 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $this->state->write($this->workerEdgesKey($this->workerToken()), $json); } + /** + * @return list + */ private function collectWorkerEdgesPartials(): array { return $this->state->keysWithPrefix(self::KEY_WORKER_EDGES_PREFIX); @@ -1128,6 +1131,9 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $this->state->write($this->workerResultsKey($this->workerToken()), $json); } + /** + * @return list + */ private function collectWorkerReplayPartials(): array { return $this->state->keysWithPrefix(self::KEY_WORKER_RESULTS_PREFIX); @@ -1367,7 +1373,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable // 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"))) { + if ($file === null || str_contains($file, "eval()'d")) { $file = $this->resolveFailedTestFile($testId); } @@ -1406,15 +1412,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable { $class = strstr($testId, '::', true); - if (! is_string($class) || $class === '') { + if (! is_string($class) || $class === '' || ! class_exists($class)) { return null; } - try { - $reflection = new \ReflectionClass($class); - } catch (\ReflectionException) { - return null; - } + $reflection = new \ReflectionClass($class); if ($reflection->hasProperty('__filename')) { try { @@ -1433,7 +1435,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable while ($current !== false) { $file = $current->getFileName(); - if (is_string($file) && $file !== '' && ! str_contains($file, "eval()'d")) { + if ($file !== false && ! str_contains($file, "eval()'d")) { return $file; } @@ -1466,10 +1468,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable private function hasProjectPhpSourceChanges(array $changedFiles): bool { foreach ($changedFiles as $rel) { - if (! is_string($rel)) { - continue; - } - if (! str_ends_with($rel, '.php')) { continue; } @@ -1523,12 +1521,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable } if (! Fingerprint::structuralMatches($fetched->fingerprint(), $current)) { - $this->renderBadge('WARN', 'TIA fetched baseline still drifts — discarding.'); + $this->renderBadge('WARN', 'Fetched baseline still drifts — discarding.'); return null; } - $this->renderBadge('SUCCESS', 'TIA fetched baseline matches — skipping local rebuild.'); + $this->renderBadge('SUCCESS', 'Fetched baseline matches — skipping local rebuild.'); return $fetched; } diff --git a/src/Plugins/Tia/BaselineSync.php b/src/Plugins/Tia/BaselineSync.php index 40028213..f552f246 100644 --- a/src/Plugins/Tia/BaselineSync.php +++ b/src/Plugins/Tia/BaselineSync.php @@ -67,7 +67,7 @@ final readonly class BaselineSync if (! $force && ($remaining = $this->cooldownRemaining()) !== null) { $this->renderBadge('WARN', sprintf( - 'TIA last fetch found no baseline — next auto-retry in %s. Override with --refetch.', + 'Last fetch found no baseline — next auto-retry in %s. Override with --refetch.', $this->formatDuration($remaining), )); @@ -102,7 +102,7 @@ final readonly class BaselineSync $this->clearCooldown(); $this->renderBadge('SUCCESS', sprintf( - 'TIA baseline ready (%s).', + 'Baseline ready (%s).', $this->formatSize(strlen($payload['graph']) + strlen($payload['coverage'] ?? '')), )); @@ -156,7 +156,7 @@ final readonly class BaselineSync private function emitPublishInstructions(string $repo): void { if ($this->isCi()) { - $this->renderBadge('INFO', 'TIA no baseline yet — this run will produce one.'); + $this->renderBadge('INFO', 'No baseline yet — this run will produce one.'); return; } @@ -165,7 +165,7 @@ final readonly class BaselineSync ? $this->laravelWorkflowYaml() : $this->genericWorkflowYaml(); - $this->renderBadge('WARN', 'TIA no baseline published yet — recording locally.'); + $this->renderBadge('WARN', 'No baseline published yet — recording locally.'); $this->renderDetail('To share the baseline with your team, add this workflow to the repo:'); $this->renderDetail('.github/workflows/tia-baseline.yml'); @@ -339,7 +339,7 @@ YAML; // Tier 2 — transient (network, rate-limit, unknown). Surface // the diagnostic but let the suite fall through to record mode. $this->renderBadge('WARN', sprintf( - 'TIA failed to query baseline runs — %s', + 'Failed to query baseline runs — %s', $listError['message'], )); @@ -364,7 +364,7 @@ YAML; @touch($runCacheDir); $this->renderBadge('INFO', sprintf( - 'TIA using cached baseline from %s (run %s).', + 'Using cached baseline from %s (run %s).', $repo, $runId, )); @@ -380,12 +380,12 @@ YAML; $this->renderBadge('INFO', $artifactSize !== null ? sprintf( - 'TIA fetching baseline (%s) from %s…', + 'Fetching baseline (%s) from %s…', $this->formatSize($artifactSize), $repo, ) : sprintf( - 'TIA fetching baseline from %s…', + 'Fetching baseline from %s…', $repo, )); @@ -424,7 +424,7 @@ YAML; // Tier 2 — transient. Diagnostic + fall through to record mode. $this->renderBadge('WARN', sprintf( - 'TIA baseline download failed — %s', + 'Baseline download failed — %s', $diagnosis['message'], )); @@ -491,7 +491,7 @@ YAML; // "100%" message naturally. $percent = min(99, (int) floor(($current / $totalBytes) * 100)); $message = sprintf( - ' TIA downloading %s / %s (%d%%, %s/s)', + ' Downloading %s / %s (%d%%, %s/s)', $this->formatSize($current), $this->formatSize($totalBytes), $percent, @@ -499,7 +499,7 @@ YAML; ); } else { $message = sprintf( - ' TIA downloading %s (%s/s)', + ' Downloading %s (%s/s)', $this->formatSize($current), $this->formatSize($speed), ); @@ -723,8 +723,7 @@ YAML; // Unknown — surface the first informative line so the user has // *something* to act on. - $first = strtok($output, "\n"); - $message = is_string($first) ? trim($first) : 'unknown error'; + $message = trim(strtok($output, "\n")); return ['kind' => 'unknown', 'message' => $message]; } diff --git a/src/Plugins/Tia/Graph.php b/src/Plugins/Tia/Graph.php index 8d516a66..01422a4f 100644 --- a/src/Plugins/Tia/Graph.php +++ b/src/Plugins/Tia/Graph.php @@ -234,7 +234,7 @@ final class Graph View::render('components.badge', [ 'type' => 'WARN', 'content' => sprintf( - 'TIA Vite resolver unavailable — falling back to watch pattern for %d new JS file(s).', + 'Vite resolver unavailable — falling back to watch pattern for %d new JS file(s).', count($newJsFiles), ), ]); @@ -469,7 +469,8 @@ final class Graph public function setResult(string $branch, string $testId, int $status, string $message, float $time, int $assertions = 0, ?string $file = null): void { $this->ensureBaseline($branch); - $this->baselines[$branch]['results'][$testId] = [ + + $entry = [ 'status' => $status, 'message' => $message, 'time' => $time, @@ -480,9 +481,11 @@ final class Graph $rel = $this->relative($file); if ($rel !== null) { - $this->baselines[$branch]['results'][$testId]['file'] = $rel; + $entry['file'] = $rel; } } + + $this->baselines[$branch]['results'][$testId] = $entry; } public function getAssertions(string $branch, string $testId, string $fallbackBranch = 'main'): ?int @@ -532,17 +535,12 @@ final class Graph $files = []; foreach ($baseline['results'] as $result) { - $status = $result['status'] ?? null; - - if ($status !== 7 && $status !== 8) { + if ($result['status'] !== 7 && $result['status'] !== 8) { continue; } $file = $result['file'] ?? null; - if (! is_string($file)) { - continue; - } - if ($file === '') { + if ($file === null || $file === '') { continue; } @@ -561,15 +559,13 @@ final class Graph $baseline = $this->baselineFor($branch, $fallbackBranch); foreach ($baseline['results'] as $result) { - $status = $result['status'] ?? null; - - if ($status !== 7 && $status !== 8) { + if ($result['status'] !== 7 && $result['status'] !== 8) { continue; } $file = $result['file'] ?? null; - if (! is_string($file) || $file === '' || $this->relative($file) === null) { + if ($file === null || $file === '' || $this->relative($file) === null) { return true; } } diff --git a/src/Plugins/Tia/Recorder.php b/src/Plugins/Tia/Recorder.php index 2695f1e1..6fcecdcf 100644 --- a/src/Plugins/Tia/Recorder.php +++ b/src/Plugins/Tia/Recorder.php @@ -473,7 +473,7 @@ final class Recorder } $parts = preg_split('/\s+as\s+/i', $import); - if ($parts === false || $parts === []) { + if ($parts === false) { return null; } @@ -488,9 +488,6 @@ final class Recorder if (! is_array($loader)) { continue; } - if (! isset($loader[0])) { - continue; - } if (! is_object($loader[0])) { continue; } @@ -689,9 +686,6 @@ final class Recorder $out = []; foreach ($data as $file => $lines) { - if (! is_string($file)) { - continue; - } if (! is_array($lines)) { continue; } @@ -709,7 +703,8 @@ final class Recorder // Skip files where the only "executed" line is the implicit // ZEND_RETURN at end-of-file (pcov artifact from being included // but never actually run). - if (count($covered) === 1 && max($covered) === max(array_keys($lines))) { + $lineKeys = array_keys($lines); + if ($lineKeys !== [] && count($covered) === 1 && $covered[0] === max($lineKeys)) { continue; } diff --git a/src/Plugins/Tia/SourceScope.php b/src/Plugins/Tia/SourceScope.php index 4459f66c..be377ae2 100644 --- a/src/Plugins/Tia/SourceScope.php +++ b/src/Plugins/Tia/SourceScope.php @@ -77,7 +77,7 @@ final readonly class SourceScope $includes = [self::normalise($projectRoot)]; } - return new self($projectRoot, $includes, $excludes); + return new self($includes, $excludes); } /** @@ -152,13 +152,7 @@ final readonly class SourceScope continue; } - $absolute = self::resolveRelative($value, $configDir); - - if ($absolute === null) { - continue; - } - - $out[] = $absolute; + $out[] = self::resolveRelative($value, $configDir); } return array_values(array_unique($out)); diff --git a/src/Restarters/PcovRestarter.php b/src/Restarters/PcovRestarter.php index 21d9539c..4bfc4144 100644 --- a/src/Restarters/PcovRestarter.php +++ b/src/Restarters/PcovRestarter.php @@ -79,9 +79,7 @@ final class PcovRestarter implements Restarter $env = []; foreach (getenv() as $name => $value) { - if (is_string($value)) { - $env[$name] = $value; - } + $env[$name] = $value; } return $env;