className = '__Twig_Tests_Cache_FilesystemTest_Template_'.$nonce; $this->directory = sys_get_temp_dir().'/twig-test'; $this->cache = new FilesystemCache($this->directory); } protected function tearDown(): void { if (file_exists($this->directory)) { FilesystemHelper::removeDir($this->directory); } } public function testLoad(): void { $key = $this->directory.'/cache/cachefile.php'; $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $this->cache->load($key); $this->assertTrue(class_exists($this->className, false)); } public function testLoadMissing(): void { $key = $this->directory.'/cache/cachefile.php'; $this->assertFalse(class_exists($this->className, false)); $this->cache->load($key); $this->assertFalse(class_exists($this->className, false)); } public function testLoadSkipsTheFileCheckWhenOpcacheHasTheFile(): void { $this->assertSame('loaded', $this->loadInOpcacheProcess(0, true)); } public function testLoadChecksTheFileWhenBytecodeInvalidationIsForced(): void { $this->assertSame('', $this->loadInOpcacheProcess(FilesystemCache::FORCE_BYTECODE_INVALIDATION, true)); } public function testLoadDoesNotWarnWhenTheOpcacheApiIsRestricted(): void { $this->assertSame('loaded', $this->loadInOpcacheProcess(0, false, '/nonexistent')); } public function testWrite(): void { $key = $this->directory.'/cache/cachefile.php'; $content = $this->generateSource(); $this->assertFileDoesNotExist($key); $this->assertFileDoesNotExist($this->directory); $this->cache->write($key, $content); $this->assertFileExists($this->directory); $this->assertFileExists($key); $this->assertSame(file_get_contents($key), $content); } public function testWriteFailMkdir(): void { if (\defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Read-only directories not possible on Windows.'); } $key = $this->directory.'/cache/cachefile.php'; $content = $this->generateSource(); $this->assertFileDoesNotExist($key); // Create read-only root directory. @mkdir($this->directory, 0555, true); $this->assertDirectoryExists($this->directory); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Unable to create the cache directory'); $this->cache->write($key, $content); } public function testWriteFailDirWritable(): void { if (\defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Read-only directories not possible on Windows.'); } $key = $this->directory.'/cache/cachefile.php'; $content = $this->generateSource(); $this->assertFileDoesNotExist($key); // Create root directory. @mkdir($this->directory, 0777, true); // Create read-only subdirectory. @mkdir($this->directory.'/cache', 0555); $this->assertDirectoryExists($this->directory.'/cache'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Unable to write in the cache directory'); $this->cache->write($key, $content); } public function testWriteFailWriteFile(): void { $key = $this->directory.'/cache/cachefile.php'; $content = $this->generateSource(); $this->assertFileDoesNotExist($key); // Create a directory in the place of the cache file. @mkdir($key, 0777, true); $this->assertDirectoryExists($key); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Failed to write cache file'); $this->cache->write($key, $content); } public function testGetTimestamp(): void { $key = $this->directory.'/cache/cachefile.php'; $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); // Create the file with a specific modification time. touch($key, 1234567890); $this->assertSame(1234567890, $this->cache->getTimestamp($key)); } public function testGetTimestampMissingFile(): void { $key = $this->directory.'/cache/cachefile.php'; $this->assertSame(0, $this->cache->getTimestamp($key)); } /** * Test file cache is tolerant towards trailing (back)slashes on the configured cache directory. * * @dataProvider provideDirectories */ #[DataProvider('provideDirectories')] public function testGenerateKey($expected, $input): void { $cache = new FilesystemCache($input); $this->assertMatchesRegularExpression($expected, $cache->generateKey('_test_', static::class)); } public static function provideDirectories() { $pattern = '#a/b/[a-zA-Z0-9]+/[a-zA-Z0-9]+.php$#'; return [ [$pattern, 'a/b'], [$pattern, 'a/b/'], [$pattern, 'a/b\\'], [$pattern, 'a/b\\/'], [$pattern, 'a/b\\//'], ['#/'.substr($pattern, 1), '/a/b'], ]; } private function generateSource() { return strtr(' $this->className, ]); } private function loadInOpcacheProcess(int $options, bool $removeCachedFile, string $restrictApi = ''): string { if (!\function_exists('opcache_compile_file')) { $this->markTestSkipped('OPcache is not loaded.'); } $code = <<<'PHP' require $argv[1]; $cache = new Twig\Cache\FilesystemCache($argv[2], (int) $argv[3]); $key = $cache->generateKey('index', 'Foo'); $cache->write($key, 'load($key); PHP; $process = proc_open([ \PHP_BINARY, '-d', 'opcache.enable=1', '-d', 'opcache.enable_cli=1', '-d', 'opcache.file_update_protection=0', '-d', 'opcache.validate_timestamps=0', '-d', 'opcache.restrict_api='.$restrictApi, '-d', 'display_errors=1', '-d', 'error_reporting=-1', '-r', $code, \dirname(__DIR__, 2).'/vendor/autoload.php', $this->directory, (string) $options, $removeCachedFile ? '1' : '0', ], [1 => ['pipe', 'w'], 2 => ['redirect', 1]], $pipes); $output = stream_get_contents($pipes[1]); proc_close($process); return $output; } }