diff --git a/src/Plugins/Tia/BaselineSync.php b/src/Plugins/Tia/BaselineSync.php index 77411f17..1f573aa6 100644 --- a/src/Plugins/Tia/BaselineSync.php +++ b/src/Plugins/Tia/BaselineSync.php @@ -312,6 +312,7 @@ final readonly class BaselineSync { $artifactSize = $this->artifactSize($repo, $runId); + $this->output->writeln(''); $this->renderChild($artifactSize !== null ? sprintf( 'Downloading TIA baseline (%s) from %s…', @@ -333,10 +334,11 @@ final readonly class BaselineSync $process->start(); $startedAt = microtime(true); + $tick = 0; while ($process->isRunning()) { - $this->renderDownloadProgress($runCacheDir, $artifactSize, $startedAt); - usleep(250_000); + $this->renderDownloadProgress($startedAt, $tick++); + usleep(120_000); } $process->wait(); @@ -402,30 +404,18 @@ final readonly class BaselineSync return is_numeric($size) ? (int) $size : null; } - private function renderDownloadProgress(string $dir, ?int $totalBytes, float $startedAt): void + private function renderDownloadProgress(float $startedAt, int $tick): void { - $current = $this->dirSize($dir); - $elapsed = max(0.001, microtime(true) - $startedAt); - $speed = (int) ($current / $elapsed); + static $frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; - if ($totalBytes !== null && $totalBytes > 0) { - $percent = min(99, (int) floor(($current / $totalBytes) * 100)); - $message = sprintf( - ' %s / %s (%d%%, %s/s)', - $this->formatSize($current), - $this->formatSize($totalBytes), - $percent, - $this->formatSize($speed), - ); - } else { - $message = sprintf( - ' %s (%s/s)', - $this->formatSize($current), - $this->formatSize($speed), - ); - } + $elapsed = max(0.0, microtime(true) - $startedAt); + $frame = $frames[$tick % count($frames)]; - $this->output->write("\r\033[K".$message); + $this->output->write(sprintf( + "\r\033[K %s %.1fs elapsed", + $frame, + $elapsed, + )); } private function clearProgressLine(): void diff --git a/src/Plugins/Tia/Fingerprint.php b/src/Plugins/Tia/Fingerprint.php index 1f54166e..da860b93 100644 --- a/src/Plugins/Tia/Fingerprint.php +++ b/src/Plugins/Tia/Fingerprint.php @@ -4,12 +4,14 @@ declare(strict_types=1); namespace Pest\Plugins\Tia; +use Symfony\Component\Finder\Finder; + /** * @internal */ final readonly class Fingerprint { - private const int SCHEMA_VERSION = 15; + private const int SCHEMA_VERSION = 17; /** * @return array{ @@ -23,8 +25,8 @@ final readonly class Fingerprint 'structural' => [ 'schema' => self::SCHEMA_VERSION, 'composer_lock' => self::composerLockHash($projectRoot), - 'phpunit_xml' => self::hashIfExists($projectRoot.'/phpunit.xml'), - 'phpunit_xml_dist' => self::hashIfExists($projectRoot.'/phpunit.xml.dist'), + 'phpunit_xml' => self::trackedHash($projectRoot, 'phpunit.xml'), + 'phpunit_xml_dist' => self::trackedHash($projectRoot, 'phpunit.xml.dist'), // 'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'), // 'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'), 'vite_config' => self::viteConfigHash($projectRoot), @@ -160,6 +162,10 @@ final readonly class Fingerprint $parts = []; foreach (JsModuleGraph::VITE_CONFIG_NAMES as $name) { + if (! self::isTrackedByGit($projectRoot, $name)) { + continue; + } + $hash = self::contentHashOrNull($projectRoot.'/'.$name); if ($hash !== null) { @@ -175,6 +181,10 @@ final readonly class Fingerprint $parts = []; foreach (['tsconfig.json', 'tsconfig.app.json', 'jsconfig.json'] as $name) { + if (! self::isTrackedByGit($projectRoot, $name)) { + continue; + } + $hash = self::hashIfExists($projectRoot.'/'.$name); if ($hash !== null) { @@ -230,7 +240,7 @@ final readonly class Fingerprint private static function composerLockHash(string $projectRoot): ?string { - return self::hashIfExists($projectRoot.'/composer.lock'); + return self::trackedHash($projectRoot, 'composer.lock'); } private static function packageLockHash(string $projectRoot): ?string @@ -238,7 +248,7 @@ final readonly class Fingerprint $parts = []; foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) { - $hash = self::hashIfExists($projectRoot.'/'.$name); + $hash = self::trackedHash($projectRoot, $name); if ($hash !== null) { $parts[] = $name.':'.$hash; @@ -248,6 +258,49 @@ final readonly class Fingerprint return $parts === [] ? null : hash('xxh128', implode("\n", $parts)); } + private static function trackedHash(string $projectRoot, string $relativePath): ?string + { + if (! self::isTrackedByGit($projectRoot, $relativePath)) { + return null; + } + + return self::hashIfExists($projectRoot.'/'.$relativePath); + } + + /** + * Returns true when the file exists and is not gitignored. + * + * Gitignored lockfiles (e.g. `package-lock.json` excluded from the repo) + * regenerate per-machine with OS-specific optional deps, which would + * otherwise force a fingerprint mismatch on every fetched baseline. + */ + private static function isTrackedByGit(string $projectRoot, string $relativePath): bool + { + if (! is_file($projectRoot.'/'.$relativePath)) { + return false; + } + + static $cache = []; + + $key = $projectRoot."\0".$relativePath; + + if (isset($cache[$key])) { + return $cache[$key]; + } + + if (! is_dir($projectRoot.'/.git') && ! is_file($projectRoot.'/.git')) { + return $cache[$key] = true; + } + + $finder = (new Finder()) + ->in($projectRoot) + ->depth('== 0') + ->name($relativePath) + ->ignoreVCSIgnored(true); + + return $cache[$key] = $finder->hasResults(); + } + private static function composerJsonHash(string $projectRoot): ?string { $path = $projectRoot.'/composer.json';