diff --git a/README.md b/README.md index a59ee34..fecd853 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The dependency goes from `contao/e2e-testing` to `contao/installation-recipe`. T | Setting | Purpose | | --- | --- | | `monorepo_url` | The Git remote for this source repository. | -| `branch_filter` | The default branches eligible for splitting: `main` and numeric release branches such as `1.0`. | +| `branch_filter` | The branches eligible for splitting: `main`, numeric release branches such as `1.0`, and `feature/*`. | | `repositories` | Maps each package directory to its split repository: [`e2e-testing`](https://github.com/contao/e2e-testing) and [`installation-recipe`](https://github.com/contao/installation-recipe). | | `composer` | Extra settings for the combined root `composer.json`. Here, `bamarni/composer-bin-plugin` is a root development dependency. The empty `require` and `conflict` lists add no constraints. | diff --git a/e2e-testing/README.md b/e2e-testing/README.md index 3f8e23b..686bdab 100644 --- a/e2e-testing/README.md +++ b/e2e-testing/README.md @@ -107,6 +107,76 @@ vendor/bin/playwright-install --browsers Use `vendor/bin/playwright-install --with-deps` on a fresh Linux CI runner to install the required system libraries as well. Playwright caches matching Chromium, Firefox, and WebKit binaries outside the project and reuses them between runs. +## CI caches + +`cache:metadata` writes separate portable keys for Playwright browser binaries and reusable E2E setup data, then prints each opaque fingerprint and cache root as JSON: + +```shell +vendor/bin/contao-e2e cache:metadata +``` + +The keys are written to `.contao-e2e/cache-keys/playwright` and `.contao-e2e/cache-keys/e2e`. Any CI system can use their contents directly or hash the files. They remain separate because browser binaries and the rest of the E2E setup have different invalidation rules. + +The Playwright fingerprint uses the concrete version from the installed Node package, the browser revisions from Playwright's installed browser registry, operating system, OS release or Linux distribution version, and architecture. The E2E fingerprint covers the PHP major and minor version, operating system, architecture, the installed `contao/e2e-testing` and `contao/installation-recipe` versions, and Composer settings that can affect dependency resolution. + +The Playwright PHP package resolves the semver constraint in its bundled `package.json` through npm, pnpm, or Yarn. Its resolved Node package and browser registry must therefore exist before metadata can be calculated. Prepare those dependencies explicitly after Composer installation: + +```shell +vendor/bin/playwright-install +vendor/bin/contao-e2e cache:metadata +``` + +The first command may access the network to install Node packages. It does not install browser binaries without `--browsers`. `cache:metadata` never performs this preparation or accesses the network itself. + +A complete GitHub Actions job can keep every cache payload under `.contao-e2e/cache` and restore both groups independently: + +```yaml +jobs: + e2e: + runs-on: ubuntu-latest + env: + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.contao-e2e/cache/playwright + steps: + - uses: actions/checkout@v6 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: intl, mbstring, pdo_mysql, zip + coverage: none + + - name: Install Composer dependencies + run: composer install --no-interaction --no-progress + + - name: Prepare Playwright Node dependencies + run: vendor/bin/playwright-install + + - name: Calculate E2E cache metadata + run: vendor/bin/contao-e2e cache:metadata + + - name: Restore Playwright browsers + uses: actions/cache@v4 + with: + path: .contao-e2e/cache/playwright + key: playwright-${{ hashFiles('.contao-e2e/cache-keys/playwright') }} + + - name: Restore E2E setup cache + uses: actions/cache@v4 + with: + path: .contao-e2e/cache/e2e + key: contao-e2e-${{ hashFiles('.contao-e2e/cache-keys/e2e') }} + + - name: Install and verify Playwright browsers + run: vendor/bin/playwright-install --browsers + + - name: Run PHPUnit + run: vendor/bin/phpunit --configuration=phpunit.xml.dist +``` + +The cache root contains separate `playwright` and `e2e` groups. The package owns the contents of each group, so adding another reusable E2E setup cache does not require consuming projects to update their CI configuration. Database data, process locks, runtime files, and failure artifacts are deliberately excluded. The existing per-installation dependency and application fingerprints still validate restored installations, so project source files do not need to be part of the outer CI cache key. + +GitHub Actions restricts cache access by branch and ref. A pull request can restore caches created on its base branch, while caches created for a pull request's merge ref are only available to reruns of that pull request. Run this job on pushes to the default branch as well as pull requests so the default branch regularly creates a cache that different pull requests can reuse. + ### Test a bundle from its working tree The following example lives in a Contao bundle repository, not in this library. Install `contao/e2e-testing` as a development dependency as shown above, then put this test in `tests/E2e/ManagedEditionSmokeTest.php`. Replace `acme/example-bundle` with the `name` from your bundle's `composer.json` and choose a version that satisfies its Composer constraints. The version does not have to match the name of your current Git branch. @@ -375,7 +445,7 @@ $this->assertSame(200, $browser->getInternalResponse()->getStatusCode()); $this->assertSame('Example', trim($crawler->filterXPath('//head/title')->text())); ``` -Full Managed Editions are stored below `.contao-e2e/cache/installations///project`. The matching +Full Managed Editions are stored below `.contao-e2e/cache/e2e/installations///project`. The matching MySQL or MariaDB database runs in the configured server or a reusable Docker container. The default database files are stored below `.contao-e2e/database/data`; additional image variants use `.contao-e2e/database//data`. The `runtime/` directory only contains the lightweight webserver router and origin mapping. diff --git a/e2e-testing/src/Cache/CacheConfig.php b/e2e-testing/src/Cache/CacheConfig.php index f51a52f..8f1301c 100644 --- a/e2e-testing/src/Cache/CacheConfig.php +++ b/e2e-testing/src/Cache/CacheConfig.php @@ -37,4 +37,39 @@ public function withRootDirectory(string $rootDirectory): self { return new self($this->projectDirectory, Path::makeAbsolute($rootDirectory, $this->projectDirectory)); } + + public function cacheDirectory(): string + { + return Path::join($this->rootDirectory, 'cache'); + } + + public function e2eCacheDirectory(): string + { + return Path::join($this->cacheDirectory(), 'e2e'); + } + + public function composerCacheDirectory(): string + { + return Path::join($this->e2eCacheDirectory(), 'composer'); + } + + public function dependencyLocksDirectory(): string + { + return Path::join($this->e2eCacheDirectory(), 'dependency-locks'); + } + + public function installationsDirectory(): string + { + return Path::join($this->e2eCacheDirectory(), 'installations'); + } + + public function playwrightCacheDirectory(): string + { + return Path::join($this->cacheDirectory(), 'playwright'); + } + + public function cacheKeysDirectory(): string + { + return Path::join($this->rootDirectory, 'cache-keys'); + } } diff --git a/e2e-testing/src/Cache/CacheMetadataFactory.php b/e2e-testing/src/Cache/CacheMetadataFactory.php new file mode 100644 index 0000000..8ec5bf5 --- /dev/null +++ b/e2e-testing/src/Cache/CacheMetadataFactory.php @@ -0,0 +1,249 @@ + $compatibilityOverrides + */ + public function __construct( + private string|null $playwrightPackageDirectory = null, + private array $compatibilityOverrides = [], + private string|null $operatingSystem = null, + private string|null $architecture = null, + private string|null $operatingSystemVersion = null, + ) { + } + + /** + * @return array{ + * schema_version: int, + * playwright: array{fingerprint: string, path: string}, + * e2e: array{fingerprint: string, path: string} + * } + */ + public function create(CacheConfig $config): array + { + $compatibility = array_replace_recursive($this->compatibility(), $this->compatibilityOverrides); + + return [ + 'schema_version' => 1, + 'playwright' => [ + 'fingerprint' => $this->playwrightFingerprint(), + 'path' => $config->playwrightCacheDirectory(), + ], + 'e2e' => [ + 'fingerprint' => $this->hash($compatibility), + 'path' => $config->e2eCacheDirectory(), + ], + ]; + } + + private function playwrightFingerprint(): string + { + $playwrightDirectory = realpath(Path::join($this->packageDirectory(), 'bin/node_modules/playwright')); + + if (false === $playwrightDirectory) { + throw new E2eTestException('Playwright Node dependencies are not prepared. Run "vendor/bin/playwright-install" before "vendor/bin/contao-e2e cache:metadata". This preparation may download Node packages.'); + } + + $coreDirectory = realpath(Path::join(\dirname($playwrightDirectory), 'playwright-core')); + + if (false === $coreDirectory) { + throw new E2eTestException('Could not locate playwright-core in the prepared Playwright Node dependencies.'); + } + + $version = $this->readVersion(Path::join($playwrightDirectory, 'package.json')); + $browsers = $this->readBrowsers(Path::join($coreDirectory, 'browsers.json')); + $platform = [ + 'operating_system' => $this->operatingSystem ?? PHP_OS_FAMILY, + 'operating_system_version' => $this->operatingSystemVersion ?? $this->operatingSystemVersion(), + 'architecture' => $this->architecture ?? php_uname('m'), + ]; + + return $this->hash([$version, $browsers, $platform]); + } + + private function operatingSystemVersion(): string + { + if ('Linux' === PHP_OS_FAMILY && is_readable('/etc/os-release')) { + $release = parse_ini_file('/etc/os-release', scanner_mode: INI_SCANNER_RAW); + + if (false !== $release && isset($release['ID'], $release['VERSION_ID'])) { + return $release['ID'].'-'.$release['VERSION_ID']; + } + } + + return php_uname('r'); + } + + private function packageDirectory(): string + { + if (null !== $this->playwrightPackageDirectory) { + return $this->playwrightPackageDirectory; + } + + $directory = InstalledVersions::getInstallPath('playwright-php/playwright'); + + if (null === $directory) { + throw new E2eTestException('Could not locate the installed playwright-php/playwright package.'); + } + + return Path::canonicalize($directory); + } + + private function readVersion(string $path): string + { + $package = $this->readJson($path); + $version = $package['version'] ?? null; + + if (!\is_string($version) || '' === $version) { + throw new E2eTestException(\sprintf('The installed Playwright package "%s" has no version.', $path)); + } + + return $version; + } + + /** + * @return array}> + */ + private function readBrowsers(string $path): array + { + $document = $this->readJson($path); + $entries = $document['browsers'] ?? null; + + if (!\is_array($entries)) { + throw new E2eTestException(\sprintf('The installed Playwright browser registry "%s" is invalid.', $path)); + } + + $browsers = []; + + foreach ($entries as $entry) { + if (\is_array($entry) && true === ($entry['installByDefault'] ?? false)) { + $this->addBrowser($browsers, $entry, $path); + } + } + + ksort($browsers); + + return $browsers; + } + + /** + * @param array}> $browsers + * @param array $entry + */ + private function addBrowser(array &$browsers, array $entry, string $path): void + { + $name = $entry['name'] ?? null; + $revision = $entry['revision'] ?? null; + $overrides = $entry['revisionOverrides'] ?? []; + + if (!\is_string($name) || !\is_string($revision) || !\is_array($overrides)) { + throw new E2eTestException(\sprintf('The installed Playwright browser registry "%s" is invalid.', $path)); + } + + $revisionOverrides = []; + + foreach ($overrides as $platform => $override) { + if (\is_string($platform) && \is_string($override)) { + $revisionOverrides[$platform] = $override; + } + } + + ksort($revisionOverrides); + $browsers[$name] = ['revision' => $revision, 'revision_overrides' => $revisionOverrides]; + } + + /** + * @return array + */ + private function readJson(string $path): array + { + if (!is_file($path)) { + throw new E2eTestException(\sprintf('Could not find the installed Playwright metadata "%s".', $path)); + } + + $contents = file_get_contents($path); + + if (false === $contents) { + throw new E2eTestException(\sprintf('Could not read the installed Playwright metadata "%s".', $path)); + } + + return json_decode($contents, true, flags: JSON_THROW_ON_ERROR); + } + + /** + * @return array + */ + private function compatibility(): array + { + return [ + 'php' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, + 'operating_system' => $this->operatingSystem ?? PHP_OS_FAMILY, + 'architecture' => $this->architecture ?? php_uname('m'), + 'packages' => [ + 'contao/e2e-testing' => $this->packageVersion('contao/e2e-testing'), + 'contao/installation-recipe' => $this->packageVersion('contao/installation-recipe'), + ], + 'composer' => $this->composerConfiguration(), + ]; + } + + private function packageVersion(string $package): string + { + $version = InstalledVersions::getVersion($package); + + if (null !== $version) { + return $version.'@'.(InstalledVersions::getReference($package) ?? 'unknown'); + } + + $root = InstalledVersions::getRootPackage(); + + return ($root['version'] ?? 'unknown').'@'.($root['reference'] ?? 'unknown'); + } + + /** + * @return array + */ + private function composerConfiguration(): array + { + $configuration = []; + + foreach ([ + 'COMPOSER_IGNORE_PLATFORM_REQ', + 'COMPOSER_IGNORE_PLATFORM_REQS', + 'COMPOSER_MINIMAL_CHANGES', + 'COMPOSER_MIRROR_PATH_REPOS', + 'COMPOSER_PREFER_LOWEST', + 'COMPOSER_PREFER_STABLE', + 'COMPOSER_WITH_ALL_DEPENDENCIES', + 'COMPOSER_WITH_DEPENDENCIES', + ] as $name) { + $value = getenv($name); + $configuration[$name] = false === $value ? null : $value; + } + + return $configuration; + } + + private function hash(mixed $value): string + { + return hash('sha256', serialize($value)); + } +} diff --git a/e2e-testing/src/Cache/WorkspaceCleaner.php b/e2e-testing/src/Cache/WorkspaceCleaner.php index d3b3687..04ca098 100644 --- a/e2e-testing/src/Cache/WorkspaceCleaner.php +++ b/e2e-testing/src/Cache/WorkspaceCleaner.php @@ -25,7 +25,7 @@ public function __construct(private Filesystem $filesystem = new Filesystem()) public function clearCache(CacheConfig $config): void { $this->assertManaged($config); - $this->filesystem->remove(Path::join($config->rootDirectory, 'cache')); + $this->filesystem->remove([$config->cacheDirectory(), $config->cacheKeysDirectory()]); (new WorkspaceInitializer($this->filesystem))->initialize($config); } diff --git a/e2e-testing/src/Cache/WorkspaceInitializer.php b/e2e-testing/src/Cache/WorkspaceInitializer.php index b579a4c..18b969b 100644 --- a/e2e-testing/src/Cache/WorkspaceInitializer.php +++ b/e2e-testing/src/Cache/WorkspaceInitializer.php @@ -26,9 +26,11 @@ public function initialize(CacheConfig $config): void { $this->assertSafe($config); $this->filesystem->mkdir([ - Path::join($config->rootDirectory, 'cache/composer'), - Path::join($config->rootDirectory, 'cache/dependency-locks'), - Path::join($config->rootDirectory, 'cache/installations'), + $config->composerCacheDirectory(), + $config->dependencyLocksDirectory(), + $config->installationsDirectory(), + $config->playwrightCacheDirectory(), + $config->cacheKeysDirectory(), Path::join($config->rootDirectory, 'database'), Path::join($config->rootDirectory, 'failures'), Path::join($config->rootDirectory, 'locks'), diff --git a/e2e-testing/src/Command/CacheClearCommand.php b/e2e-testing/src/Command/CacheClearCommand.php index 58f1012..e6124ae 100644 --- a/e2e-testing/src/Command/CacheClearCommand.php +++ b/e2e-testing/src/Command/CacheClearCommand.php @@ -18,13 +18,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -#[AsCommand('cache:clear', 'Clear reusable dependency and Managed Edition caches')] +#[AsCommand('cache:clear', 'Clear reusable E2E setup caches')] final class CacheClearCommand extends AbstractWorkspaceCommand { protected function execute(InputInterface $input, OutputInterface $output): int { (new WorkspaceCleaner())->clearCache($this->cache()); - (new SymfonyStyle($input, $output))->success('E2E dependency and installation caches cleared.'); + (new SymfonyStyle($input, $output))->success('Reusable E2E setup caches cleared.'); return self::SUCCESS; } diff --git a/e2e-testing/src/Command/CacheMetadataCommand.php b/e2e-testing/src/Command/CacheMetadataCommand.php new file mode 100644 index 0000000..c50e2c4 --- /dev/null +++ b/e2e-testing/src/Command/CacheMetadataCommand.php @@ -0,0 +1,52 @@ +cache(); + (new WorkspaceInitializer())->initialize($config); + $metadata = ($this->metadataFactory ?? new CacheMetadataFactory())->create($config); + $filesystem = new Filesystem(); + $filesystem->dumpFile( + Path::join($config->cacheKeysDirectory(), 'playwright'), + $metadata['playwright']['fingerprint']."\n", + ); + $filesystem->dumpFile( + Path::join($config->cacheKeysDirectory(), 'e2e'), + $metadata['e2e']['fingerprint']."\n", + ); + $output->writeln(json_encode( + $metadata, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR, + )); + + return self::SUCCESS; + } +} diff --git a/e2e-testing/src/Command/E2eApplication.php b/e2e-testing/src/Command/E2eApplication.php index 5461744..f77d181 100644 --- a/e2e-testing/src/Command/E2eApplication.php +++ b/e2e-testing/src/Command/E2eApplication.php @@ -23,6 +23,7 @@ public function __construct() $this->addCommands([ new DoctorCommand(), new CacheClearCommand(), + new CacheMetadataCommand(), new DatabaseStopCommand(), new FailuresClearCommand(), ]); diff --git a/e2e-testing/src/Composer/ComposerInstaller.php b/e2e-testing/src/Composer/ComposerInstaller.php index cebbd2a..1573d29 100644 --- a/e2e-testing/src/Composer/ComposerInstaller.php +++ b/e2e-testing/src/Composer/ComposerInstaller.php @@ -43,7 +43,7 @@ public function install(ManagedEditionConfig $config, string $directory): void $command, $directory, [ - 'COMPOSER_CACHE_DIR' => Path::join($config->environment->cache->rootDirectory, 'cache/composer'), + 'COMPOSER_CACHE_DIR' => $config->environment->cache->composerCacheDirectory(), ], ); } diff --git a/e2e-testing/src/Installation/InstallationPool.php b/e2e-testing/src/Installation/InstallationPool.php index 434c0c5..5e1d0c9 100644 --- a/e2e-testing/src/Installation/InstallationPool.php +++ b/e2e-testing/src/Installation/InstallationPool.php @@ -38,7 +38,7 @@ public function acquire(CacheConfig $cache, string $fingerprint): InstallationLe continue; } - $directory = Path::join($cache->rootDirectory, 'cache/installations', $fingerprint, (string) $slot); + $directory = Path::join($cache->installationsDirectory(), $fingerprint, (string) $slot); $this->filesystem->mkdir($directory); return new InstallationLease($directory, $slot, $lock); diff --git a/e2e-testing/tests/CacheMetadataCommandTest.php b/e2e-testing/tests/CacheMetadataCommandTest.php new file mode 100644 index 0000000..ac00813 --- /dev/null +++ b/e2e-testing/tests/CacheMetadataCommandTest.php @@ -0,0 +1,89 @@ +directory = Path::join(\dirname(__DIR__, 2), '.contao-e2e/runtime/unit-tests/cache-command-'.bin2hex(random_bytes(6))); + $filesystem = new Filesystem(); + $filesystem->mkdir([ + $this->directory.'/package/bin/node_modules/playwright', + $this->directory.'/package/bin/node_modules/playwright-core', + ]); + $filesystem->dumpFile( + $this->directory.'/package/bin/node_modules/playwright/package.json', + '{"name":"playwright","version":"1.63.0"}', + ); + $filesystem->dumpFile( + $this->directory.'/package/bin/node_modules/playwright-core/browsers.json', + '{"browsers":[{"name":"firefox","revision":"1543","installByDefault":true}]}', + ); + } + + protected function tearDown(): void + { + (new Filesystem())->remove($this->directory); + } + + public function testWritesPortableCacheKeysAndProducesJsonOutput(): void + { + $cacheDirectory = $this->directory.'/workspace'; + putenv('CONTAO_E2E_DIRECTORY='.$cacheDirectory); + $tester = new CommandTester(new CacheMetadataCommand($this->metadataFactory())); + + try { + $this->assertSame(0, $tester->execute([])); + $metadata = json_decode($tester->getDisplay(), true, flags: JSON_THROW_ON_ERROR); + } finally { + putenv('CONTAO_E2E_DIRECTORY'); + } + + $this->assertSame(1, $metadata['schema_version']); + $this->assertSame( + $metadata['playwright']['fingerprint']."\n", + file_get_contents($cacheDirectory.'/cache-keys/playwright'), + ); + $this->assertSame( + $metadata['e2e']['fingerprint']."\n", + file_get_contents($cacheDirectory.'/cache-keys/e2e'), + ); + $this->assertSame( + $cacheDirectory.'/cache/playwright', + $metadata['playwright']['path'], + ); + $this->assertSame( + $cacheDirectory.'/cache/e2e', + $metadata['e2e']['path'], + ); + } + + private function metadataFactory(): CacheMetadataFactory + { + return new CacheMetadataFactory( + playwrightPackageDirectory: $this->directory.'/package', + operatingSystem: 'Linux', + architecture: 'x86_64', + ); + } +} diff --git a/e2e-testing/tests/CacheMetadataFactoryTest.php b/e2e-testing/tests/CacheMetadataFactoryTest.php new file mode 100644 index 0000000..4905392 --- /dev/null +++ b/e2e-testing/tests/CacheMetadataFactoryTest.php @@ -0,0 +1,163 @@ +directory = Path::join(\dirname(__DIR__, 2), '.contao-e2e/runtime/unit-tests/cache-metadata-'.bin2hex(random_bytes(6))); + $filesystem = new Filesystem(); + $filesystem->mkdir([ + $this->directory.'/package/bin/node_modules/playwright', + $this->directory.'/package/bin/node_modules/playwright-core', + ]); + $this->writePlaywrightVersion('1.63.0'); + $filesystem->dumpFile($this->directory.'/package/bin/node_modules/playwright-core/browsers.json', <<<'JSON' + { + "browsers": [ + {"name": "chromium", "revision": "1243", "installByDefault": true}, + {"name": "firefox", "revision": "1543", "installByDefault": true}, + {"name": "webkit", "revision": "2359", "revisionOverrides": {"mac14-arm64": "2251"}, "installByDefault": true}, + {"name": "android", "revision": "1001", "installByDefault": false} + ] + } + JSON); + } + + protected function tearDown(): void + { + (new Filesystem())->remove($this->directory); + } + + public function testProducesDeterministicSeparateFingerprintsAndStablePaths(): void + { + $factory = $this->factory(); + $config = CacheConfig::forProject($this->directory.'/project'); + $first = $factory->create($config); + $second = $factory->create($config); + + $this->assertSame($first['playwright']['fingerprint'], $second['playwright']['fingerprint']); + $this->assertSame($first['e2e']['fingerprint'], $second['e2e']['fingerprint']); + $this->assertNotSame($first['playwright']['fingerprint'], $first['e2e']['fingerprint']); + $this->assertSame($this->directory.'/project/.contao-e2e/cache/playwright', $first['playwright']['path']); + $this->assertSame( + $this->directory.'/project/.contao-e2e/cache/e2e', + $first['e2e']['path'], + ); + $this->assertSame(['fingerprint', 'path'], array_keys($first['playwright'])); + $this->assertSame(['fingerprint', 'path'], array_keys($first['e2e'])); + } + + public function testPlaywrightMetadataInvalidatesOnlyTheBrowserFingerprint(): void + { + $config = CacheConfig::forProject($this->directory.'/project'); + $initial = $this->factory()->create($config); + $this->writePlaywrightVersion('1.64.0'); + $versionChanged = $this->factory()->create($config); + + $this->assertNotSame($initial['playwright']['fingerprint'], $versionChanged['playwright']['fingerprint']); + $this->assertSame($initial['e2e']['fingerprint'], $versionChanged['e2e']['fingerprint']); + + (new Filesystem())->dumpFile( + $this->directory.'/package/bin/node_modules/playwright-core/browsers.json', + '{"browsers":[{"name":"firefox","revision":"1544","installByDefault":true}]}', + ); + $revisionChanged = $this->factory()->create($config); + + $this->assertNotSame($versionChanged['playwright']['fingerprint'], $revisionChanged['playwright']['fingerprint']); + $this->assertSame($versionChanged['e2e']['fingerprint'], $revisionChanged['e2e']['fingerprint']); + } + + public function testExplainsHowToPrepareMissingPlaywrightDependencies(): void + { + $this->expectException(E2eTestException::class); + $this->expectExceptionMessage('vendor/bin/playwright-install'); + + (new CacheMetadataFactory(playwrightPackageDirectory: $this->directory.'/missing-package')) + ->create(CacheConfig::forProject($this->directory.'/project')) + ; + } + + public function testOperatingSystemVersionInvalidatesOnlyTheBrowserFingerprint(): void + { + $config = CacheConfig::forProject($this->directory.'/project'); + $initial = $this->factory(operatingSystemVersion: 'ubuntu-22.04')->create($config); + $changed = $this->factory(operatingSystemVersion: 'ubuntu-24.04')->create($config); + $differentDistribution = $this->factory(operatingSystemVersion: 'debian-12')->create($config); + + $this->assertNotSame($initial['playwright']['fingerprint'], $changed['playwright']['fingerprint']); + $this->assertNotSame($changed['playwright']['fingerprint'], $differentDistribution['playwright']['fingerprint']); + $this->assertSame($initial['e2e']['fingerprint'], $changed['e2e']['fingerprint']); + $this->assertSame($changed['e2e']['fingerprint'], $differentDistribution['e2e']['fingerprint']); + } + + /** + * @param array $override + */ + #[DataProvider('e2eCompatibilityProvider')] + public function testRelevantCompatibilityChangesInvalidateOnlyTheE2eFingerprint(array $override): void + { + $config = CacheConfig::forProject($this->directory.'/project'); + $initial = $this->factory()->create($config); + $changed = $this->factory($override)->create($config); + + $this->assertSame($initial['playwright']['fingerprint'], $changed['playwright']['fingerprint']); + $this->assertNotSame($initial['e2e']['fingerprint'], $changed['e2e']['fingerprint']); + } + + /** + * @return iterable}> + */ + public static function e2eCompatibilityProvider(): iterable + { + yield 'PHP version' => [['php' => '9.0']]; + yield 'operating system' => [['operating_system' => 'ExampleOS']]; + yield 'architecture' => [['architecture' => 'example-architecture']]; + yield 'E2E package' => [['packages' => ['contao/e2e-testing' => '2.0.0.0']]]; + yield 'recipe package' => [['packages' => ['contao/installation-recipe' => '2.0.0.0']]]; + yield 'Composer configuration' => [['composer' => ['COMPOSER_PREFER_LOWEST' => '1']]]; + } + + /** + * @param array $compatibility + */ + private function factory(array $compatibility = [], string $operatingSystemVersion = 'ubuntu-24.04'): CacheMetadataFactory + { + return new CacheMetadataFactory( + playwrightPackageDirectory: $this->directory.'/package', + compatibilityOverrides: $compatibility, + operatingSystem: 'Linux', + architecture: 'x86_64', + operatingSystemVersion: $operatingSystemVersion, + ); + } + + private function writePlaywrightVersion(string $version): void + { + (new Filesystem())->dumpFile( + $this->directory.'/package/bin/node_modules/playwright/package.json', + json_encode(['name' => 'playwright', 'version' => $version], JSON_THROW_ON_ERROR), + ); + } +} diff --git a/e2e-testing/tests/E2eApplicationTest.php b/e2e-testing/tests/E2eApplicationTest.php index c70fc9d..1118df8 100644 --- a/e2e-testing/tests/E2eApplicationTest.php +++ b/e2e-testing/tests/E2eApplicationTest.php @@ -23,6 +23,7 @@ public function testRegistersTheWorkspaceCommands(): void $this->assertTrue($application->has('doctor')); $this->assertTrue($application->has('cache:clear')); + $this->assertTrue($application->has('cache:metadata')); $this->assertTrue($application->has('database:stop')); $this->assertTrue($application->has('failures:clear')); $this->assertTrue($application->find('database:stop')->getDefinition()->hasOption('force')); diff --git a/e2e-testing/tests/WorkspaceInitializerTest.php b/e2e-testing/tests/WorkspaceInitializerTest.php index e201974..a66d0f9 100644 --- a/e2e-testing/tests/WorkspaceInitializerTest.php +++ b/e2e-testing/tests/WorkspaceInitializerTest.php @@ -29,7 +29,9 @@ public function testCreatesTheProjectLocalWorkspace(): void $this->assertFileExists($project.'/.contao-e2e/.managed-by-contao-e2e'); $this->assertSame("*\n!.gitignore\n", file_get_contents($project.'/.contao-e2e/.gitignore')); - $this->assertDirectoryExists($project.'/.contao-e2e/cache/installations'); + $this->assertDirectoryExists($project.'/.contao-e2e/cache/e2e/installations'); + $this->assertDirectoryExists($project.'/.contao-e2e/cache/playwright'); + $this->assertDirectoryExists($project.'/.contao-e2e/cache-keys'); $this->assertDirectoryExists($project.'/.contao-e2e/database'); } } diff --git a/monorepo.yml b/monorepo.yml index afa3884..3738edb 100644 --- a/monorepo.yml +++ b/monorepo.yml @@ -1,5 +1,5 @@ monorepo_url: https://github.com/contao/setup-and-testing.git -branch_filter: /^(main|\d+\.\d+)$/ +branch_filter: /^(main|\d+\.\d+|feature\/.+)$/ composer: require: []