Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/split.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ jobs:
uses: actions/cache@v5
with:
path: .monorepo-split-cache
key: dev-${GITHUB_REF##*/}
key: dev-${{ github.ref_name }}

- name: Install the dependencies
run: composer global require contao/monorepo-tools:^0.2

- name: Split the monorepo
run: $HOME/.composer/vendor/bin/monorepo-tools split ${GITHUB_REF##*/} ${{ github.event.forced && '--force-push' || '' }}
run: $HOME/.composer/vendor/bin/monorepo-tools split "$GITHUB_REF_NAME" ${{ github.event.forced && '--force-push' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.MONOREPO_SPLIT_TOKEN }}
60 changes: 54 additions & 6 deletions e2e-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,53 @@ $mariaDb = $config->withDatabase(DockerDatabaseConfig::mariaDb('mariadb:10.11'))
$mysql = $config->withDatabase(DockerDatabaseConfig::mysql('mysql:8.0'));
```

Different types and image versions use independent reusable containers and storage directories. This makes those configurations suitable for a PHPUnit data provider or separate CI jobs. A CI matrix can configure the same tests without changing PHP code:
Different types and image versions use independent reusable containers and storage directories. This makes those configurations suitable for a PHPUnit data provider or separate CI jobs.

Database warm-up is optional. Without the extension, each test starts its database variant on demand and waits until it is ready. When one PHPUnit run uses several database images, enable the PHPUnit extension in the E2E configuration to start them earlier:

```xml
<extensions>
<bootstrap class="Contao\E2eTesting\PhpUnit\DockerWarmUpExtension">
<parameter name="testsuites" value="e2e"/>
</bootstrap>
</extensions>
```

The comma-separated `testsuites` parameter selects the PHPUnit suites whose Docker services should be warmed. `ManagedEditionConfig::dockerServices()` exposes the services implied by the configuration, and `AbstractManagedEditionTestCase` implements the provider by returning that collection. The complete bundle example below uses this base class.

If a test already has another base class, it can continue using `ManagedEditionTestTrait` and advertise services directly by implementing `DockerServiceProviderInterface`:

```php
use Contao\E2eTesting\Docker\DockerServiceProviderInterface;
use Contao\E2eTesting\ManagedEdition\ManagedEditionConfig;
use Contao\E2eTesting\ManagedEdition\ManagedEditionTestTrait;
use Contao\InstallationRecipe\Composer\ComposerConfig;
use Contao\InstallationRecipe\Recipe\InstallationRecipe;
use PHPUnit\Framework\TestCase;

final class ManagedEditionSmokeTest extends TestCase implements DockerServiceProviderInterface
{
use ManagedEditionTestTrait;

protected static function createManagedEditionConfig(): ManagedEditionConfig
{
$bundleRoot = dirname(__DIR__, 2);
$composer = ComposerConfig::managedEdition('^5.7')
->withPathPackage('acme/example-bundle', $bundleRoot, '1.0.x-dev');

return ManagedEditionConfig::create(InstallationRecipe::create($composer), $bundleRoot);
}

public static function dockerServices(): iterable
{
return static::createManagedEditionConfig()->dockerServices();
}
}
```

When a selected suite starts, the extension collects and deduplicates its services before warming them without waiting for readiness. Tests still wait for a service when they first use it. A subclass can advertise additional services by overriding `dockerServices()`, yielding from `parent::dockerServices()`, and then yielding its own services. Other suites, such as `unit`, do not start Docker. The provider mechanism is independent of `ManagedEditionTestTrait`, and future service types such as Redis can implement `DockerServiceInterface` without changing the PHPUnit extension.

A CI matrix can configure the same tests without changing PHP code:

```shell
CONTAO_E2E_DATABASE_TYPE=mysql CONTAO_E2E_DATABASE_IMAGE=mysql:8.0 composer e2e-tests
Expand Down Expand Up @@ -70,16 +116,13 @@ The following example lives in a Contao bundle repository, not in this library.

declare(strict_types=1);

use Contao\E2eTesting\ManagedEdition\AbstractManagedEditionTestCase;
use Contao\E2eTesting\ManagedEdition\ManagedEditionConfig;
use Contao\E2eTesting\ManagedEdition\ManagedEditionTestTrait;
use Contao\InstallationRecipe\Composer\ComposerConfig;
use Contao\InstallationRecipe\Recipe\InstallationRecipe;
use PHPUnit\Framework\TestCase;

final class ManagedEditionSmokeTest extends TestCase
final class ManagedEditionSmokeTest extends AbstractManagedEditionTestCase
{
use ManagedEditionTestTrait;

protected static function createManagedEditionConfig(): ManagedEditionConfig
{
$bundleRoot = dirname(__DIR__, 2);
Expand Down Expand Up @@ -109,6 +152,11 @@ Add `tests/E2e` to your existing PHPUnit test suite, or use this minimal `phpuni
<directory>tests/E2e</directory>
</testsuite>
</testsuites>
<extensions>
<bootstrap class="Contao\E2eTesting\PhpUnit\DockerWarmUpExtension">
<parameter name="testsuites" value="e2e"/>
</bootstrap>
</extensions>
</phpunit>
```

Expand Down
12 changes: 8 additions & 4 deletions e2e-testing/src/Database/DatabaseReadinessProbe.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@

final readonly class DatabaseReadinessProbe
{
public function __construct(private float $timeout = 60)
{
}

public function wait(DatabaseServerConfig $config): void
{
$deadline = microtime(true) + 60;
$deadline = microtime(true) + $this->timeout;
$lastException = null;

do {
try {
$connection = DriverManager::getConnection(new DsnParser([
$connection = DriverManager::getConnection((new DsnParser([
'mysql' => 'pdo_mysql',
'pdo-mysql' => 'pdo_mysql',
'mysqli' => 'mysqli',
])->parse($config->url));
]))->parse($config->url));
$connection->executeQuery('SELECT 1');
$connection->close();

Expand All @@ -40,6 +44,6 @@ public function wait(DatabaseServerConfig $config): void
}
} while (microtime(true) < $deadline);

throw new DockerUnavailableException('The Docker MariaDB server did not become ready within 60 seconds: '.$lastException->getMessage(), 0, $lastException);
throw new DockerUnavailableException(\sprintf('The Docker MariaDB server did not become ready within %g seconds: %s', $this->timeout, $lastException->getMessage()), 0, $lastException);
}
}
9 changes: 8 additions & 1 deletion e2e-testing/src/Database/DockerClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@

private const DATABASE_CONFIG_LABEL = 'org.contao.e2e.database-config';

/**
* @param non-empty-list<string> $command
*/
public function __construct(private array $command = ['docker'])
{
}

public function isRunning(string $container): bool
{
return 'true' === $this->optional(['inspect', '--format', '{{.State.Running}}', $container]);
Expand Down Expand Up @@ -146,6 +153,6 @@ private function run(array $arguments): string
*/
private function process(array $arguments): Process
{
return new Process(['docker', ...$arguments]);
return new Process([...$this->command, ...$arguments]);
}
}
53 changes: 35 additions & 18 deletions e2e-testing/src/Database/DockerDatabaseServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Contao\E2eTesting\Database;

use Contao\E2eTesting\Cache\CacheConfig;
use Contao\E2eTesting\Cache\WorkspaceInitializer;
use Contao\E2eTesting\Exception\DockerUnavailableException;
use Contao\E2eTesting\Exception\E2eTestException;
use Symfony\Component\Filesystem\Filesystem;
Expand All @@ -25,10 +26,44 @@ public function __construct(
private DatabaseReadinessProbe $readinessProbe = new DatabaseReadinessProbe(),
private Filesystem $filesystem = new Filesystem(),
private DockerDatabaseLeaseRegistry $leaseRegistry = new DockerDatabaseLeaseRegistry(),
private WorkspaceInitializer $workspaceInitializer = new WorkspaceInitializer(),
) {
}

public function provide(CacheConfig $cache, DockerDatabaseConfig $database): DatabaseServerConfig
{
$config = $this->startContainer($cache, $database);
$this->readinessProbe->wait($config);

return $config;
}

public function warmUp(CacheConfig $cache, DockerDatabaseConfig ...$databases): void
{
$this->workspaceInitializer->initialize($cache);

foreach ($databases as $database) {
$this->startContainer($cache, $database);
}
}

public function stop(CacheConfig $cache, bool $force = false): void
{
$containers = $this->docker->find($this->containerPrefix($cache));
$locks = $force ? [] : $this->acquireStopLocks($cache, $containers);

try {
foreach ($containers as $container) {
$this->docker->stop($container);
}
} finally {
foreach ($locks as $lock) {
$lock->release();
}
}
}

private function startContainer(CacheConfig $cache, DockerDatabaseConfig $database): DatabaseServerConfig
{
$container = $this->containerName($cache, $database);
$leasePath = $this->leasePath($cache, $container);
Expand All @@ -52,27 +87,9 @@ public function provide(CacheConfig $cache, DockerDatabaseConfig $database): Dat
fclose($lock);
}

$this->readinessProbe->wait($config);

return $config;
}

public function stop(CacheConfig $cache, bool $force = false): void
{
$containers = $this->docker->find($this->containerPrefix($cache));
$locks = $force ? [] : $this->acquireStopLocks($cache, $containers);

try {
foreach ($containers as $container) {
$this->docker->stop($container);
}
} finally {
foreach ($locks as $lock) {
$lock->release();
}
}
}

private function start(CacheConfig $cache, DockerDatabaseConfig $database): DatabaseServerConfig
{
$container = $this->containerName($cache, $database);
Expand Down
41 changes: 41 additions & 0 deletions e2e-testing/src/Database/DockerDatabaseService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Database;

use Contao\E2eTesting\Cache\CacheConfig;
use Contao\E2eTesting\Docker\DockerServiceInterface;

final readonly class DockerDatabaseService implements DockerServiceInterface
{
public function __construct(
public CacheConfig $cache,
public DockerDatabaseConfig $config,
private DockerDatabaseServer $server = new DockerDatabaseServer(),
) {
}

public function fingerprint(): string
{
return hash('sha256', implode("\0", [
$this->cache->projectDirectory,
$this->cache->rootDirectory,
'database',
$this->config->fingerprint(),
]));
}

public function warmUp(): void
{
$this->server->warmUp($this->cache, $this->config);
}
}
20 changes: 20 additions & 0 deletions e2e-testing/src/Docker/DockerServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Docker;

interface DockerServiceInterface
{
public function fingerprint(): string;

public function warmUp(): void;
}
21 changes: 21 additions & 0 deletions e2e-testing/src/Docker/DockerServiceProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Docker;

interface DockerServiceProviderInterface
{
/**
* @return iterable<DockerServiceInterface>
*/
public static function dockerServices(): iterable;
}
26 changes: 26 additions & 0 deletions e2e-testing/src/ManagedEdition/AbstractManagedEditionTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\ManagedEdition;

use Contao\E2eTesting\Docker\DockerServiceProviderInterface;
use PHPUnit\Framework\TestCase;

abstract class AbstractManagedEditionTestCase extends TestCase implements DockerServiceProviderInterface
{
use ManagedEditionTestTrait;

public static function dockerServices(): iterable
{
return static::createManagedEditionConfig()->dockerServices();
}
}
17 changes: 17 additions & 0 deletions e2e-testing/src/ManagedEdition/ManagedEditionConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Contao\E2eTesting\Database\DatabaseResetMode;
use Contao\E2eTesting\Database\DatabaseServerConfig;
use Contao\E2eTesting\Database\DockerDatabaseConfig;
use Contao\E2eTesting\Database\DockerDatabaseService;
use Contao\E2eTesting\Docker\DockerServiceInterface;
use Contao\InstallationRecipe\File\FileMapping;
use Contao\InstallationRecipe\Recipe\InstallationRecipe;

Expand Down Expand Up @@ -47,6 +49,21 @@ public function withDatabase(DatabaseServerConfig|DockerDatabaseConfig $database
return $this->withEnvironment($this->environment->withDatabase($database));
}

/**
* @return list<DockerServiceInterface>
*/
public function dockerServices(): array
{
if ($this->environment->database instanceof DatabaseServerConfig) {
return [];
}

return [new DockerDatabaseService(
$this->environment->cache,
$this->environment->database ?? DockerDatabaseConfig::fromEnvironment(),
)];
}

public function withDcaFile(string $path): self
{
if (!is_file($path) || !str_ends_with($path, '.php')) {
Expand Down
Loading
Loading