From 3a1af9fac00b7dcbbde6e8983979fefeb0f812c1 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 31 Aug 2026 18:20:12 +0200 Subject: [PATCH 1/2] Bugfix: Autoloaders require source, not the file --- .github/workflows/ci.yml | 5 + CHANGELOG.md | 18 ++ bin/ci_autoloader_smoke | 161 ++++++++++++++++++ composer.json | 1 + extension.neon | 19 +-- phpstan.neon | 30 ---- ...acheStorage.php => GeneratedFileCache.php} | 53 +++--- .../Magento/Autoload/ExtensionAutoloader.php | 13 +- .../Autoload/ExtensionInterfaceAutoloader.php | 13 +- .../Magento/Autoload/FactoryAutoloader.php | 13 +- .../Magento/Autoload/ProxyAutoloader.php | 13 +- ...est.php => GeneratedFileCacheUnitTest.php} | 30 ++-- .../Autoload/ExtensionAutoloaderUnitTest.php | 31 ++-- .../ExtensionInterfaceAutoloaderUnitTest.php | 35 ++-- .../Autoload/FactoryAutoloaderUnitTest.php | 90 +++++++--- .../Autoload/ProxyAutoloaderUnitTest.php | 34 ++-- .../Magento/Autoload/RegistrationUnitTest.php | 4 +- 17 files changed, 378 insertions(+), 185 deletions(-) create mode 100755 bin/ci_autoloader_smoke rename src/bitExpert/PHPStan/Magento/Autoload/Cache/{FileCacheStorage.php => GeneratedFileCache.php} (62%) rename tests/bitExpert/PHPStan/Magento/Autoload/Cache/{FileCacheStorageUnitTest.php => GeneratedFileCacheUnitTest.php} (67%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f0a8b6..0300903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: - name: Static code analysis run: composer analyze + - name: Autoloader smoke test + env: + REQUIRE_TURBO: '1' + run: composer smoke-test + - name: Unittests if: matrix.coveralls == false run: composer test diff --git a/CHANGELOG.md b/CHANGELOG.md index fb2c37a..663cf7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## Unreleased + +### Added + +- A CI smoke test (`composer smoke-test`) that runs a real analysis over a generated fixture project and fails when the generated-class autoloaders report an internal error. + +### Deprecated + +- Nothing. + +### Removed + +- `bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage`, replaced by `bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache`. The autoloaders now take that class in place of `PHPStan\Cache\Cache`, and the generated classes move from `%tmpDir%/cache/PHPStan` to `%tmpDir%/cache/bitExpert/PHPStanMagento` so that they no longer share a directory with PHPStan's own cache. + +### Fixed + +- Fix "Internal error: Failed opening required 'isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname()); + } + + rmdir($directory); +} + +function run(string $repoRoot, string $command, string $config): array +{ + $commandLine = sprintf( + '%s %s %s --configuration=%s 2>&1', + escapeshellarg(PHP_BINARY), + escapeshellarg($repoRoot . '/vendor/bin/phpstan'), + $command, + escapeshellarg($config) + ); + + $output = []; + $exitCode = 0; + exec($commandLine, $output, $exitCode); + + return [implode(PHP_EOL, $output), $exitCode]; +} + +/* + * The arena is what makes the cache round trip observable, and the extension being loaded does not + * mean the arena works: on macOS the extension loads but never hands out an arena. Probe it the way + * PHPStan does, with the binary PHPStan itself reports. + */ +function arenaIsUsable(string $diagnoseOutput): bool +{ + if (preg_match('#^Turbo worker binary: (\S+)#m', $diagnoseOutput, $matches) !== 1) { + return false; + } + + $probe = sprintf( + '%s -d %s -r %s 2>&1', + escapeshellarg(PHP_BINARY), + escapeshellarg('extension=' . $matches[1]), + escapeshellarg('echo \PHPStanTurbo\ArenaCache::create("probe") === null ? "no" : "yes";') + ); + + $output = []; + $exitCode = 0; + exec($probe, $output, $exitCode); + + return $exitCode === 0 && in_array('yes', $output, true); +} + +mkdir($fixtureRoot . '/src', 0777, true); + +for ($i = 1; $i <= $numberOfFiles; $i++) { + file_put_contents( + sprintf('%s/src/Consumer%d.php', $fixtureRoot, $i), + <<create(); + } + } + + PHP + ); +} + +// A cold tmpDir matters: with a warm cache the autoloaders find the generated file before they +// ever ask PHPStan to store anything, which hides the very problem this guard looks for. +file_put_contents($fixtureRoot . '/phpstan.neon', <<|MyUncachedExtension, string given~' path: tests/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloaderUnitTest.php - - - message: '~is not covered by backward compatibility promise.~' - path: tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php - message: '~Parameter #1 \$argument of class ReflectionClass constructor expects class-string|UncachedExtensionInterface, string given~' path: tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php diff --git a/src/bitExpert/PHPStan/Magento/Autoload/Cache/FileCacheStorage.php b/src/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCache.php similarity index 62% rename from src/bitExpert/PHPStan/Magento/Autoload/Cache/FileCacheStorage.php rename to src/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCache.php index 4d59462..67416e1 100644 --- a/src/bitExpert/PHPStan/Magento/Autoload/Cache/FileCacheStorage.php +++ b/src/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCache.php @@ -12,9 +12,15 @@ namespace bitExpert\PHPStan\Magento\Autoload\Cache; -use PHPStan\Cache\CacheStorage; - -class FileCacheStorage implements CacheStorage +/** + * Stores the source code the autoloaders generate and hands back the path of the file holding it. + * + * This deliberately is not a PHPStan\Cache\CacheStorage: the autoloaders need the path of the + * generated file to require it, while a CacheStorage has to return what was handed to save(). + * Mixing the two made the autoloaders require the source code itself once PHPStan started to + * serve cache entries from its shared memory arena. + */ +class GeneratedFileCache { /** * @var string @@ -27,9 +33,10 @@ class FileCacheStorage implements CacheStorage private $magentoRoot; /** - * FileCacheStorage constructor. + * GeneratedFileCache constructor. * * @param string $directory + * @param string $magentoRoot */ public function __construct(string $directory, string $magentoRoot) { @@ -38,39 +45,41 @@ public function __construct(string $directory, string $magentoRoot) } /** + * Returns the path of the file generated for the given key, null when nothing was generated yet. + * * @param string $key - * @param string $variableKey - * @return mixed + * @return string|null */ - public function load(string $key, string $variableKey) + public function getFile(string $key): ?string { - return (function (string $key): ?string { - $cacheDir = $this->getCacheDir($key); - $cacheFile = $this->getCacheFile($key); - if (!is_file($cacheDir . '/' . $cacheFile)) { - return null; - } + $cacheFile = $this->getCacheDir($key) . '/' . $this->getCacheFile($key); + if (!is_file($cacheFile)) { + return null; + } - return $cacheDir . '/' . $cacheFile; - })($key); + return $cacheFile; } /** + * Writes the given source code and returns the path of the file holding it. + * * @param string $key - * @param string $variableKey - * @param mixed $data + * @param string $contents + * @return string */ - public function save(string $key, string $variableKey, $data): void + public function putFile(string $key, string $contents): string { $cacheDir = $this->getCacheDir($key); - $cacheFile = $this->getCacheFile($key); + $cacheFile = $cacheDir . '/' . $this->getCacheFile($key); $this->makeDir($cacheDir); - $tmpSuccess = @file_put_contents($cacheDir . '/' . $cacheFile, $data); - if ($tmpSuccess === false) { + $written = @file_put_contents($cacheFile, $contents); + if ($written === false) { throw new \InvalidArgumentException( - sprintf('Could not write data to cache file %s.', $cacheDir . '/' . $cacheFile) + sprintf('Could not write data to cache file %s.', $cacheFile) ); } + + return $cacheFile; } /** diff --git a/src/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloader.php b/src/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloader.php index b97845f..cb67ee2 100644 --- a/src/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloader.php +++ b/src/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloader.php @@ -12,6 +12,7 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; use Laminas\Code\Generator\ClassGenerator; @@ -20,13 +21,12 @@ use Laminas\Code\Generator\DocBlockGenerator; use Laminas\Code\Generator\MethodGenerator; use Laminas\Code\Generator\ParameterGenerator; -use PHPStan\Cache\Cache; use ReflectionClass; class ExtensionAutoloader implements Autoloader { /** - * @var Cache + * @var GeneratedFileCache */ private $cache; /** @@ -41,12 +41,12 @@ class ExtensionAutoloader implements Autoloader /** * ExtensionAutoloader constructor. * - * @param Cache $cache + * @param GeneratedFileCache $cache * @param ClassLoaderProvider $classLoaderProvider * @param ExtensionAttributeDataProvider $attributeDataProvider */ public function __construct( - Cache $cache, + GeneratedFileCache $cache, ClassLoaderProvider $classLoaderProvider, ExtensionAttributeDataProvider $attributeDataProvider ) { @@ -65,10 +65,9 @@ public function autoload(string $class): void // local classes in your project. We need to check first if classes exists locally before generating them! $pathToLocalClass = $this->classLoaderProvider->findFile($class); if ($pathToLocalClass === false) { - $pathToLocalClass = $this->cache->load($class, ''); + $pathToLocalClass = $this->cache->getFile($class); if ($pathToLocalClass === null) { - $this->cache->save($class, '', $this->getFileContents($class)); - $pathToLocalClass = $this->cache->load($class, ''); + $pathToLocalClass = $this->cache->putFile($class, $this->getFileContents($class)); } } diff --git a/src/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloader.php b/src/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloader.php index e619323..15f8e12 100644 --- a/src/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloader.php +++ b/src/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloader.php @@ -12,6 +12,7 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; use Laminas\Code\Generator\DocBlock\Tag\ParamTag; @@ -20,12 +21,11 @@ use Laminas\Code\Generator\InterfaceGenerator; use Laminas\Code\Generator\MethodGenerator; use Laminas\Code\Generator\ParameterGenerator; -use PHPStan\Cache\Cache; class ExtensionInterfaceAutoloader implements Autoloader { /** - * @var Cache + * @var GeneratedFileCache */ private $cache; /** @@ -40,12 +40,12 @@ class ExtensionInterfaceAutoloader implements Autoloader /** * ExtensionInterfaceAutoloader constructor. * - * @param Cache $cache + * @param GeneratedFileCache $cache * @param ExtensionAttributeDataProvider $attributeDataProvider * @param ClassLoaderProvider $classLoaderProvider */ public function __construct( - Cache $cache, + GeneratedFileCache $cache, ClassLoaderProvider $classLoaderProvider, ExtensionAttributeDataProvider $attributeDataProvider ) { @@ -64,10 +64,9 @@ public function autoload(string $interfaceName): void // local classes in your project. We need to check first if classes exists locally before generating them! $pathToLocalInterface = $this->classLoaderProvider->findFile($interfaceName); if ($pathToLocalInterface === false) { - $pathToLocalInterface = $this->cache->load($interfaceName, ''); + $pathToLocalInterface = $this->cache->getFile($interfaceName); if ($pathToLocalInterface === null) { - $this->cache->save($interfaceName, '', $this->getFileContents($interfaceName)); - $pathToLocalInterface = $this->cache->load($interfaceName, ''); + $pathToLocalInterface = $this->cache->putFile($interfaceName, $this->getFileContents($interfaceName)); } } diff --git a/src/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloader.php b/src/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloader.php index 51630c2..1875aec 100644 --- a/src/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloader.php +++ b/src/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloader.php @@ -12,13 +12,13 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; -use PHPStan\Cache\Cache; class FactoryAutoloader implements Autoloader { /** - * @var Cache + * @var GeneratedFileCache */ private $cache; /** @@ -29,10 +29,10 @@ class FactoryAutoloader implements Autoloader /** * FactoryAutoloader constructor. * - * @param Cache $cache + * @param GeneratedFileCache $cache * @param ClassLoaderProvider $classLoaderProvider */ - public function __construct(Cache $cache, ClassLoaderProvider $classLoaderProvider) + public function __construct(GeneratedFileCache $cache, ClassLoaderProvider $classLoaderProvider) { $this->cache = $cache; $this->classLoaderProvider = $classLoaderProvider; @@ -48,10 +48,9 @@ public function autoload(string $class): void // local classes in your project. We need to check first if classes exists locally before generating them! $pathToLocalClass = $this->classLoaderProvider->findFile($class); if ($pathToLocalClass === false) { - $pathToLocalClass = $this->cache->load($class, ''); + $pathToLocalClass = $this->cache->getFile($class); if ($pathToLocalClass === null) { - $this->cache->save($class, '', $this->getFileContents($class)); - $pathToLocalClass = $this->cache->load($class, ''); + $pathToLocalClass = $this->cache->putFile($class, $this->getFileContents($class)); } } diff --git a/src/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloader.php b/src/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloader.php index c4f0131..f52165c 100644 --- a/src/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloader.php +++ b/src/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloader.php @@ -12,13 +12,13 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; -use PHPStan\Cache\Cache; class ProxyAutoloader implements Autoloader { /** - * @var Cache + * @var GeneratedFileCache */ private $cache; /** @@ -29,10 +29,10 @@ class ProxyAutoloader implements Autoloader /** * ProxyAutoloader constructor. * - * @param Cache $cache + * @param GeneratedFileCache $cache * @param ClassLoaderProvider $classLoaderProvider */ - public function __construct(Cache $cache, ClassLoaderProvider $classLoaderProvider) + public function __construct(GeneratedFileCache $cache, ClassLoaderProvider $classLoaderProvider) { $this->cache = $cache; $this->classLoaderProvider = $classLoaderProvider; @@ -48,10 +48,9 @@ public function autoload(string $class): void // local classes in your project. We need to check first if classes exists locally before generating them! $pathToLocalClass = $this->classLoaderProvider->findFile($class); if ($pathToLocalClass === false) { - $pathToLocalClass = $this->cache->load($class, ''); + $pathToLocalClass = $this->cache->getFile($class); if ($pathToLocalClass === null) { - $this->cache->save($class, '', $this->getFileContents($class)); - $pathToLocalClass = $this->cache->load($class, ''); + $pathToLocalClass = $this->cache->putFile($class, $this->getFileContents($class)); } } diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/Cache/FileCacheStorageUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php similarity index 67% rename from tests/bitExpert/PHPStan/Magento/Autoload/Cache/FileCacheStorageUnitTest.php rename to tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php index f1bd852..958b5b4 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/Cache/FileCacheStorageUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php @@ -17,21 +17,21 @@ use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\TestCase; -class FileCacheStorageUnitTest extends TestCase +class GeneratedFileCacheUnitTest extends TestCase { /** * @var vfsStreamDirectory */ private $root; /** - * @var FileCacheStorage + * @var GeneratedFileCache */ - private $storage; + private $cache; public function setUp(): void { $this->root = vfsStream::setup('tmp'); - $this->storage = new FileCacheStorage($this->root->url(), 'mage249'); + $this->cache = new GeneratedFileCache($this->root->url(), 'mage249'); } /** @@ -39,7 +39,7 @@ public function setUp(): void */ public function nullReturnedWhenLookingUpNonExistentFileInCache(): void { - $absFilename = $this->storage->load('test.txt', ''); + $absFilename = $this->cache->getFile('test.txt'); self::assertNull($absFilename); } @@ -54,7 +54,7 @@ public function absoluteFilenameReturnedWhenLookingUpExistentFileInCache(): void $this->root ); - $absFilename = $this->storage->load('test.txt', ''); + $absFilename = $this->cache->getFile('test.txt'); self::assertSame($absFilename, vfsStream::url('tmp/03/ef/4b6fcb2d521ef0fd442a5301e7932d16cc9f375a.php')); } @@ -64,10 +64,21 @@ public function absoluteFilenameReturnedWhenLookingUpExistentFileInCache(): void */ public function addingFileToCacheSucceeds(): void { - $this->storage->save('test.txt', '', 'Lorem ipsum'); - $absFilename = $this->storage->load('test.txt', ''); + $writtenFilename = $this->cache->putFile('test.txt', 'Lorem ipsum'); + $absFilename = $this->cache->getFile('test.txt'); self::assertSame($absFilename, vfsStream::url('tmp/03/ef/4b6fcb2d521ef0fd442a5301e7932d16cc9f375a.php')); + self::assertSame($absFilename, $writtenFilename); + } + + /** + * @test + */ + public function contentsAreWrittenToTheReturnedFile(): void + { + $writtenFilename = $this->cache->putFile('test.txt', 'Lorem ipsum'); + + self::assertSame('Lorem ipsum', file_get_contents($writtenFilename)); } /** @@ -80,7 +91,6 @@ public function addingFileToCacheFails(): void // simulate full disk vfsStream::setQuota(1); - $this->storage->save('test.txt', '', 'Lorem ipsum'); - $this->storage->load('test.txt', ''); + $this->cache->putFile('test.txt', 'Lorem ipsum'); } } diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloaderUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloaderUnitTest.php index 6fdebd6..7dc0b39 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloaderUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloaderUnitTest.php @@ -2,24 +2,18 @@ namespace bitExpert\PHPStan\Magento\Autoload; -use bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; use org\bovigo\vfs\vfsStream; -use PHPStan\Cache\Cache; -use PHPStan\Cache\CacheStorage; use PHPUnit\Framework\TestCase; class ExtensionAutoloaderUnitTest extends TestCase { /** - * @var Cache + * @var GeneratedFileCache&\PHPUnit\Framework\MockObject\MockObject */ private $cache; - /** - * @var CacheStorage&\PHPUnit\Framework\MockObject\MockObject - */ - private $cacheStorage; /** * @var ClassLoaderProvider&\PHPUnit\Framework\MockObject\MockObject */ @@ -35,8 +29,7 @@ class ExtensionAutoloaderUnitTest extends TestCase protected function setUp(): void { - $this->cacheStorage = $this->createMock(CacheStorage::class); - $this->cache = new Cache($this->cacheStorage); + $this->cache = $this->createMock(GeneratedFileCache::class); $this->classLoader = $this->createMock(ClassLoaderProvider::class); $this->extAttrDataProvider = $this->createMock(ExtensionAttributeDataProvider::class); $this->autoloader = new ExtensionAutoloader( @@ -53,8 +46,8 @@ public function autoloaderIgnoresClassesWithoutExtensionInterfacePostfix(): void { $this->classLoader->expects(self::never()) ->method('findFile'); - $this->cacheStorage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload('SomeClass'); } @@ -67,8 +60,8 @@ public function autoloaderPrefersLocalFile(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(__DIR__ . '/HelperExtension.php'); - $this->cacheStorage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload(HelperExtension::class); @@ -83,12 +76,12 @@ public function autoloaderUsesCachedFileWhenFound(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->cacheStorage->expects(self::once()) - ->method('load') + $this->cache->expects(self::once()) + ->method('getFile') ->willReturn(__DIR__ . '/HelperExtension.php'); - $this->cacheStorage->expects(self::never()) - ->method('save'); + $this->cache->expects(self::never()) + ->method('putFile'); $this->autoloader->autoload(HelperExtension::class); @@ -113,7 +106,7 @@ public function autoloadGeneratesInterfaceWhenNotCached(): void class_alias(HelperExtensionInterface::class, 'MyUncachedExtensionInterface'); $root = vfsStream::setup('test'); - $cache = new Cache(new FileCacheStorage($root->url() . '/tmp/cache/PHPStan', 'mage249')); + $cache = new GeneratedFileCache($root->url() . '/tmp/cache/bitExpert/PHPStanMagento', 'mage249'); $autoloader = new ExtensionAutoloader($cache, $this->classLoader, $this->extAttrDataProvider); $autoloader->autoload($className); diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php index 4386ef1..6c2740b 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php @@ -2,25 +2,19 @@ namespace bitExpert\PHPStan\Magento\Autoload; -use bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; use InvalidArgumentException; use org\bovigo\vfs\vfsStream; -use PHPStan\Cache\Cache; -use PHPStan\Cache\CacheStorage; use PHPUnit\Framework\TestCase; class ExtensionInterfaceAutoloaderUnitTest extends TestCase { /** - * @var Cache + * @var GeneratedFileCache&\PHPUnit\Framework\MockObject\MockObject */ private $cache; - /** - * @var CacheStorage&\PHPUnit\Framework\MockObject\MockObject - */ - private $cacheStorage; /** * @var ExtensionAttributeDataProvider&\PHPUnit\Framework\MockObject\MockObject */ @@ -36,8 +30,7 @@ class ExtensionInterfaceAutoloaderUnitTest extends TestCase protected function setUp(): void { - $this->cacheStorage = $this->createMock(CacheStorage::class); - $this->cache = new Cache($this->cacheStorage); + $this->cache = $this->createMock(GeneratedFileCache::class); $this->classLoader = $this->createMock(ClassLoaderProvider::class); $this->extAttrDataProvider = $this->createMock(ExtensionAttributeDataProvider::class); $this->autoloader = new ExtensionInterfaceAutoloader( @@ -54,8 +47,8 @@ public function autoloaderIgnoresClassesWithoutExtensionInterfacePostfix(): void { $this->classLoader->expects(self::never()) ->method('findFile'); - $this->cacheStorage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload('SomeClass'); } @@ -68,8 +61,8 @@ public function autoloaderPrefersLocalFile(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(__DIR__ . '/HelperExtensionInterface.php'); - $this->cacheStorage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload(HelperExtensionInterface::class); @@ -84,12 +77,12 @@ public function autoloaderUsesCachedFileWhenFound(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->cacheStorage->expects(self::once()) - ->method('load') + $this->cache->expects(self::once()) + ->method('getFile') ->willReturn(__DIR__ . '/HelperExtensionInterface.php'); - $this->cacheStorage->expects(self::never()) - ->method('save'); + $this->cache->expects(self::never()) + ->method('putFile'); $this->autoloader->autoload(HelperExtensionInterface::class); @@ -109,8 +102,8 @@ public function autoloadDoesNotGenerateInterfaceWhenNoAttributesExist(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->cacheStorage->expects(self::once()) - ->method('load') + $this->cache->expects(self::once()) + ->method('getFile') ->willReturn(null); $this->classLoader->expects(self::once()) @@ -128,7 +121,7 @@ public function autoloadGeneratesInterfaceWhenNotCached(): void $interfaceName = 'UncachedExtensionInterface'; $root = vfsStream::setup('test'); - $cache = new Cache(new FileCacheStorage($root->url() . '/tmp/cache/PHPStan', 'mage249')); + $cache = new GeneratedFileCache($root->url() . '/tmp/cache/bitExpert/PHPStanMagento', 'mage249'); $autoloader = new ExtensionInterfaceAutoloader($cache, $this->classLoader, $this->extAttrDataProvider); $this->classLoader->expects(self::once()) diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloaderUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloaderUnitTest.php index 677bce6..8af8abf 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloaderUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloaderUnitTest.php @@ -12,17 +12,16 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; -use PHPStan\Cache\Cache; -use PHPStan\Cache\CacheStorage; use PHPUnit\Framework\TestCase; class FactoryAutoloaderUnitTest extends TestCase { /** - * @var CacheStorage|\PHPUnit\Framework\MockObject\MockObject + * @var GeneratedFileCache|\PHPUnit\Framework\MockObject\MockObject */ - private $storage; + private $cache; /** * @var FactoryAutoloader */ @@ -31,13 +30,26 @@ class FactoryAutoloaderUnitTest extends TestCase * @var ClassLoaderProvider|\PHPUnit\Framework\MockObject\MockObject */ private $classLoader; + /** + * @var string[] + */ + private $tempFiles = []; public function setUp(): void { - $this->storage = $this->createMock(CacheStorage::class); + $this->cache = $this->createMock(GeneratedFileCache::class); $this->classLoader = $this->createMock(ClassLoaderProvider::class); - $this->autoloader = new FactoryAutoloader(new Cache($this->storage), $this->classLoader); + $this->autoloader = new FactoryAutoloader($this->cache, $this->classLoader); + } + + public function tearDown(): void + { + foreach ($this->tempFiles as $tempFile) { + @unlink($tempFile); + } + + $this->tempFiles = []; } /** @@ -47,8 +59,8 @@ public function autoloaderIgnoresClassesWithoutFactoryPostfix(): void { $this->classLoader->expects(self::never()) ->method('findFile'); - $this->storage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload('SomeClass'); } @@ -61,8 +73,8 @@ public function autoloaderPrefersLocalFile(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(__DIR__ . '/HelperFactory.php'); - $this->storage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload(HelperFactory::class); @@ -77,9 +89,11 @@ public function autoloaderUsesCachedFileWhenFound(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->storage->expects(self::once()) - ->method('load') + $this->cache->expects(self::once()) + ->method('getFile') ->willReturn(__DIR__ . '/HelperFactory.php'); + $this->cache->expects(self::never()) + ->method('putFile'); $this->autoloader->autoload(HelperFactory::class); @@ -94,17 +108,46 @@ public function autoloaderGeneratesCacheFileWhenNotFoundInCache(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->storage->expects(self::atMost(2)) - ->method('load') - ->willReturnOnConsecutiveCalls(null, __DIR__ . '/HelperFactory.php'); - $this->storage->expects(self::once()) - ->method('save'); + $this->cache->expects(self::once()) + ->method('getFile') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('putFile') + ->willReturn(__DIR__ . '/HelperFactory.php'); $this->autoloader->autoload(HelperFactory::class); self::assertTrue(class_exists(HelperFactory::class, false)); } + /** + * @test + */ + public function autoloaderRequiresTheGeneratedFileAndNotTheGeneratedSource(): void + { + $className = 'bitExpert\PHPStan\Magento\Autoload\PathNotSourceThingFactory'; + + $this->classLoader->expects(self::once()) + ->method('findFile') + ->willReturn(false); + $this->cache->expects(self::once()) + ->method('getFile') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('putFile') + ->willReturnCallback(function (string $key, string $contents): string { + $file = sys_get_temp_dir() . '/phpstan-magento-' . uniqid() . '.php'; + file_put_contents($file, $contents); + $this->tempFiles[] = $file; + + return $file; + }); + + $this->autoloader->autoload($className); + + self::assertTrue(class_exists($className, false)); + } + /** * @test */ @@ -113,14 +156,13 @@ public function autoloaderGeneratesFactoryForCorrectClassname(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->storage->expects(self::atMost(2)) - ->method('load') - ->willReturnOnConsecutiveCalls(null, __DIR__ . '/FactoryThingFactory.php'); - $this->storage->expects(self::once()) - ->method('save') + $this->cache->expects(self::once()) + ->method('getFile') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('putFile') ->with( 'bitExpert\PHPStan\Magento\Autoload\FactoryThingFactory', - static::isType('string'), static::stringContains(<<willReturn(__DIR__ . '/FactoryThingFactory.php'); $this->autoloader->autoload(FactoryThingFactory::class); } diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloaderUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloaderUnitTest.php index e2815f8..1aea8d7 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloaderUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/ProxyAutoloaderUnitTest.php @@ -12,17 +12,16 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; -use PHPStan\Cache\Cache; -use PHPStan\Cache\CacheStorage; use PHPUnit\Framework\TestCase; class ProxyAutoloaderUnitTest extends TestCase { /** - * @var CacheStorage|\PHPUnit\Framework\MockObject\MockObject + * @var GeneratedFileCache|\PHPUnit\Framework\MockObject\MockObject */ - private $storage; + private $cache; /** * @var ProxyAutoloader */ @@ -34,10 +33,10 @@ class ProxyAutoloaderUnitTest extends TestCase public function setUp(): void { - $this->storage = $this->createMock(CacheStorage::class); + $this->cache = $this->createMock(GeneratedFileCache::class); $this->classLoader = $this->createMock(ClassLoaderProvider::class); - $this->autoloader = new ProxyAutoloader(new Cache($this->storage), $this->classLoader); + $this->autoloader = new ProxyAutoloader($this->cache, $this->classLoader); } /** @@ -47,8 +46,8 @@ public function autoloaderIgnoresClassesWithoutProxyPostfix(): void { $this->classLoader->expects(self::never()) ->method('findFile'); - $this->storage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload('SomeClass'); } @@ -61,8 +60,8 @@ public function autoloaderPrefersLocalFile(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(__DIR__ . '/HelperProxy.php'); - $this->storage->expects(self::never()) - ->method('load'); + $this->cache->expects(self::never()) + ->method('getFile'); $this->autoloader->autoload('\bitExpert\PHPStan\Magento\Autoload\Helper\Proxy'); @@ -77,8 +76,8 @@ public function autoloaderUsesCachedFileWhenFound(): void $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->storage->expects(self::once()) - ->method('load') + $this->cache->expects(self::once()) + ->method('getFile') ->willReturn(__DIR__ . '/HelperProxy.php'); $this->autoloader->autoload('\bitExpert\PHPStan\Magento\Autoload\Helper\Proxy'); @@ -98,11 +97,12 @@ class_alias('\bitExpert\PHPStan\Magento\Autoload\HelperProxy', '\bitExpert\PHPSt $this->classLoader->expects(self::once()) ->method('findFile') ->willReturn(false); - $this->storage->expects(self::atMost(2)) - ->method('load') - ->willReturnOnConsecutiveCalls(null, __DIR__ . '/HelperProxy.php'); - $this->storage->expects(self::once()) - ->method('save'); + $this->cache->expects(self::once()) + ->method('getFile') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('putFile') + ->willReturn(__DIR__ . '/HelperProxy.php'); $this->autoloader->autoload('\bitExpert\PHPStan\Magento\Autoload\Helper\Proxy'); diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/RegistrationUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/RegistrationUnitTest.php index c14eea9..4b62478 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/RegistrationUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/RegistrationUnitTest.php @@ -12,9 +12,9 @@ namespace bitExpert\PHPStan\Magento\Autoload; +use bitExpert\PHPStan\Magento\Autoload\Cache\GeneratedFileCache; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider; use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; -use PHPStan\Cache\Cache; use PHPUnit\Framework\TestCase; class RegistrationUnitTest extends TestCase @@ -43,7 +43,7 @@ public function autoloadersCanRegisterAndUnregister(Autoloader $autoloader): voi */ public function provideAutoloaders(): array { - $cache = new Cache($this->getMockBuilder(\PHPStan\Cache\CacheStorage::class)->getMock()); + $cache = $this->createMock(GeneratedFileCache::class); $classLoader = $this->createMock(ClassLoaderProvider::class); return [ From 7e347077a8d12152e981fe863e7255cdad34e6de Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Wed, 2 Sep 2026 11:31:45 +0200 Subject: [PATCH 2/2] Cover makeDir branches in GeneratedFileCache --- .../Cache/GeneratedFileCacheUnitTest.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php b/tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php index 958b5b4..641bea3 100644 --- a/tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php +++ b/tests/bitExpert/PHPStan/Magento/Autoload/Cache/GeneratedFileCacheUnitTest.php @@ -93,4 +93,28 @@ public function addingFileToCacheFails(): void $this->cache->putFile('test.txt', 'Lorem ipsum'); } + + /** + * @test + */ + public function existingCacheDirectoryIsReused(): void + { + $this->cache->putFile('test.txt', 'Lorem ipsum'); + $writtenFilename = $this->cache->putFile('test.txt', 'Dolor sit amet'); + + self::assertSame('Dolor sit amet', file_get_contents($writtenFilename)); + } + + /** + * @test + */ + public function addingFileToCacheFailsWhenCacheDirectoryCannotBeCreated(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('#^Failed to create directory#'); + + $this->root->chmod(0000); + + $this->cache->putFile('test.txt', 'Lorem ipsum'); + } }