diff --git a/composer.json b/composer.json index 271f81b9..726731a0 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,12 @@ ], "require": { - "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*", + "composer-runtime-api": "^2.2" }, "require-dev": { + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", "symfony/yaml": "^5.4 || ^6.4 || ^7.0", "phpunit/phpunit": "^10.5", "cucumber/cucumber": "dev-gherkin-24.1.0", diff --git a/src/Behat/Gherkin/Cache/FileCache.php b/src/Behat/Gherkin/Cache/FileCache.php index ea468569..5f381390 100644 --- a/src/Behat/Gherkin/Cache/FileCache.php +++ b/src/Behat/Gherkin/Cache/FileCache.php @@ -11,8 +11,8 @@ namespace Behat\Gherkin\Cache; use Behat\Gherkin\Exception\CacheException; -use Behat\Gherkin\Gherkin; use Behat\Gherkin\Node\FeatureNode; +use Composer\InstalledVersions; /** * File cache. @@ -24,6 +24,17 @@ class FileCache implements CacheInterface { private $path; + /** + * Used as part of the cache directory path to invalidate cache if the installed package version changes. + */ + private static function getGherkinVersionHash(): string + { + $version = InstalledVersions::getVersion('behat/gherkin'); + + // Composer version strings can contain arbitrary content so hash for filesystem safety + return md5($version); + } + /** * Initializes file cache. * @@ -33,7 +44,7 @@ class FileCache implements CacheInterface */ public function __construct($path) { - $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'v' . Gherkin::VERSION; + $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . self::getGherkinVersionHash(); if (!is_dir($this->path)) { @mkdir($this->path, 0777, true); diff --git a/src/Behat/Gherkin/Gherkin.php b/src/Behat/Gherkin/Gherkin.php index 577d0a9a..e13db611 100644 --- a/src/Behat/Gherkin/Gherkin.php +++ b/src/Behat/Gherkin/Gherkin.php @@ -23,6 +23,11 @@ */ class Gherkin { + /** + * @deprecated this constant will not be updated for releases after 4.8.0 and will be removed in the next major. + * You can use composer's runtime API to get the behat version if you need it. Note that composer's versions will + * not always be simple numeric values. + */ public const VERSION = '4.8.0'; /** diff --git a/tests/Behat/Gherkin/Cache/FileCacheTest.php b/tests/Behat/Gherkin/Cache/FileCacheTest.php index a8f0f466..a1d23cb0 100644 --- a/tests/Behat/Gherkin/Cache/FileCacheTest.php +++ b/tests/Behat/Gherkin/Cache/FileCacheTest.php @@ -12,10 +12,10 @@ use Behat\Gherkin\Cache\FileCache; use Behat\Gherkin\Exception\CacheException; -use Behat\Gherkin\Gherkin; use Behat\Gherkin\Node\FeatureNode; use Behat\Gherkin\Node\ScenarioNode; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; class FileCacheTest extends TestCase { @@ -58,9 +58,19 @@ public function testCacheAndRead(): void public function testBrokenCacheRead(): void { + // First, write a valid cache and find the file that was written + $this->cache->write( + 'broken_feature', + new FeatureNode(null, null, [], null, [], null, null, null, null), + ); + $files = glob($this->path . '/**/*.feature.cache'); + $this->assertCount(1, $files, 'Cache should have written a single file'); + + // Now simulate the file being corrupted and attempt to read it + file_put_contents($files[0], ''); + $this->expectException(CacheException::class); - touch($this->path . '/v' . Gherkin::VERSION . '/' . md5('broken_feature') . '.feature.cache'); $this->cache->read('broken_feature'); } @@ -77,13 +87,11 @@ public function testUnwriteableCacheDir(): void protected function setUp(): void { - $this->cache = new FileCache($this->path = sys_get_temp_dir() . '/gherkin-test'); + $this->cache = new FileCache($this->path = sys_get_temp_dir() . uniqid('/gherkin-test')); } protected function tearDown(): void { - foreach (glob($this->path . '/*.feature.cache') as $file) { - unlink((string) $file); - } + (new Filesystem())->remove($this->path); } }