diff --git a/src/Internal/CacheManager.php b/src/Internal/CacheManager.php index 21735b34..8d48d750 100644 --- a/src/Internal/CacheManager.php +++ b/src/Internal/CacheManager.php @@ -27,7 +27,7 @@ public static function reset(): void } /** - * Returns the absolute path to the cache directory, isolating by system user if using temp dir. + * Returns the absolute path to the cache directory, isolating by system user and parallel test worker if using temp dir. */ public static function getCacheDir(): string { @@ -55,7 +55,20 @@ public static function getCacheDir(): string $user = (string) getmyuid(); } - $userHash = hash('xxh128', 'typephp_' . $user); + $testToken = getenv('TEST_TOKEN'); + $uniqueToken = getenv('UNIQUE_TEST_TOKEN'); + $pestWorkerId = getenv('PEST_PARALLEL_WORKER_ID'); + + $workerToken = '0'; + if (\is_string($testToken) && $testToken !== '') { + $workerToken = $testToken; + } elseif (\is_string($uniqueToken) && $uniqueToken !== '') { + $workerToken = $uniqueToken; + } elseif (\is_string($pestWorkerId) && $pestWorkerId !== '') { + $workerToken = $pestWorkerId; + } + + $userHash = hash('xxh128', 'typephp_' . $user . '_w' . $workerToken); return self::$resolvedCacheDir = sys_get_temp_dir() . '/typephp-cache-' . $userHash; } @@ -135,26 +148,59 @@ public static function writeCachedFileSafely(string $cachedFile, string $transfo } /** - * Clears all cached transformed files from the cache directory. + * Clears all cached transformed files from the cache directory, + * including all parallel worker directories (_w1, _w2, etc.). */ public static function clear(): int { - $cacheDir = self::getCacheDir(); + $config = Config::get(); + $customDir = $config['cache_dir'] ?? null; - if (! is_dir($cacheDir) || is_link($cacheDir)) { - return 0; - } + $dirsToClear = []; - $files = glob($cacheDir . '/*.php'); - if ($files === false || \count($files) === 0) { - return 0; + if (\is_string($customDir) && $customDir !== '') { + if (is_dir($customDir) && ! is_link($customDir)) { + $dirsToClear[] = $customDir; + } + } else { + $tempBase = sys_get_temp_dir(); + $matchedDirs = glob($tempBase . '/typephp-cache-*'); + + if ($matchedDirs !== false) { + foreach ($matchedDirs as $d) { + if (is_dir($d) && ! is_link($d)) { + if (\function_exists('posix_geteuid')) { + $owner = @fileowner($d); + if ($owner !== false && $owner !== posix_geteuid()) { + continue; + } + } + $dirsToClear[] = $d; + } + } + } } $count = 0; - foreach ($files as $file) { - if (is_file($file) && ! is_link($file)) { - @unlink($file); - $count++; + foreach ($dirsToClear as $dir) { + $files = glob($dir . '/*.php'); + if ($files !== false) { + foreach ($files as $file) { + if (is_file($file) && ! is_link($file)) { + @unlink($file); + $count++; + } + } + } + + // Also clean up any lingering temporary swap files + $tmpFiles = glob($dir . '/.tmp_*'); + if ($tmpFiles !== false) { + foreach ($tmpFiles as $tFile) { + if (is_file($tFile) && ! is_link($tFile)) { + @unlink($tFile); + } + } } } diff --git a/src/bootstrap.php b/src/bootstrap.php index 10f6491a..93e0060d 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -11,6 +11,7 @@ $isDisabledConst = \defined('TYPEPHP_DISABLE') && TYPEPHP_DISABLE; $isTooling = false; + $binary = ''; if (isset($_SERVER['argv']) && \is_array($_SERVER['argv']) && \count($_SERVER['argv']) > 0) { $candidate = $_SERVER['argv'][0]; @@ -36,13 +37,32 @@ 'composer' => true, 'deptrac' => true, 'phan' => true, + 'paratest' => true, ]; $isTooling = isset($toolingBinaries[$binary]); } } - if (! $isDisabledEnv && ! $isDisabledConst && ! $isTooling) { + $isParallelParent = false; + if (isset($_SERVER['argv']) && \is_array($_SERVER['argv'])) { + $hasParallelArg = false; + foreach ($_SERVER['argv'] as $arg) { + if (\is_string($arg) && (str_starts_with($arg, '--parallel') || $arg === '-p' || str_starts_with($arg, '--processes'))) { + $hasParallelArg = true; + + break; + } + } + + $isParallelParent = ($binary === 'paratest') || ($binary === 'pest' && $hasParallelArg); + + if (getenv('TEST_TOKEN') !== false || getenv('PARATEST') !== false || getenv('PEST_PARALLEL_WORKER_ID') !== false) { + $isParallelParent = false; + } + } + + if (! $isDisabledEnv && ! $isDisabledConst && ! $isTooling && ! $isParallelParent) { TypePHP::boot(); } } diff --git a/tests/TypeChecking/ArraysAndShapes/KeyOfValueOfTest.php b/tests/TypeChecking/ArraysAndShapes/KeyOfValueOfTest.php index e784c436..c14a2dcd 100644 --- a/tests/TypeChecking/ArraysAndShapes/KeyOfValueOfTest.php +++ b/tests/TypeChecking/ArraysAndShapes/KeyOfValueOfTest.php @@ -167,7 +167,8 @@ function testKeylessTupleKeyOf(int $index): int expect($conn->localAction(['action' => 'start']))->toBeTrue(); expect(fn () => $conn->localAction(['action' => 'pause'])) - ->toThrow(TypeError::class, "['action'] must be a key of TypePHP\Tests\Fixtures\Types\DoctrineLikeConnection::LOCAL_ACTIONS"); + ->toThrow(TypeError::class, "['action'] must be a key of TypePHP\Tests\Fixtures\Types\DoctrineLikeConnection::LOCAL_ACTIONS") + ; }); }); }); diff --git a/tests/TypeChecking/Boundaries/MultiBranchConditionalReturnsTest.php b/tests/TypeChecking/Boundaries/MultiBranchConditionalReturnsTest.php index 80a3b659..9d5fb481 100644 --- a/tests/TypeChecking/Boundaries/MultiBranchConditionalReturnsTest.php +++ b/tests/TypeChecking/Boundaries/MultiBranchConditionalReturnsTest.php @@ -136,7 +136,8 @@ public function formatValue(bool $asInt, mixed $value): mixed expect($service->formatByNegation(true, 100))->toBe(100); expect(fn () => $service->formatByNegation(true, -50)) - ->toThrow(TypeError::class, 'Return value must be of type positive-int'); + ->toThrow(TypeError::class, 'Return value must be of type positive-int') + ; }); }); }); diff --git a/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php b/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php index ccb9097e..8e63d393 100644 --- a/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php +++ b/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php @@ -100,6 +100,7 @@ public function count(): int expect(testTemplateWithIntersectionBoundClassString(ArrayObject::class))->toBe(ArrayObject::class); expect(fn () => testTemplateWithIntersectionBoundClassString(stdClass::class)) - ->toThrow(TypeError::class, 'must be a class-string of'); + ->toThrow(TypeError::class, 'must be a class-string of') + ; }); });