From af32d116e4ca65e7e237ffd45ecdeb8c8bba5b68 Mon Sep 17 00:00:00 2001
From: Gustavo Freze 1, and it is read each time a command is called. While
+ * it is not enabled, a command skips the run entirely and returns null, so no Docker command is issued,
+ * neither for Flyway nor for its MySQL source.
Configures the JDBC URL, the credentials, the target schema (from MYSQL_DATABASE) and the
- * history table (schema_history), and waits until MySQL accepts connections before each command.
- * Call withSchema() or withTable() after this method to override the defaults.
The MySQL container is started by the first command that runs, through
+ * {@see MySQLDockerContainer::runIfNotExists()}, so it is reused when a container with its name is already
+ * running. Defining the source starts nothing. Flyway then connects with the JDBC URL of the started container,
+ * the root credentials (from MYSQL_ROOT_PASSWORD), the target schema (from
+ * MYSQL_DATABASE) and the history table (schema_history). withCredentials(),
+ * withSchema() and withTable() override these defaults, whether they are called before or after this method.
The variable is enabled when its value is 1, and it is read each time a command is called.
+ * While it is not enabled, a command returns null without issuing any Docker command, so neither
+ * Flyway nor its MySQL source is started. This keeps the migrations of a test run in the single process that
+ * sets the variable.
The mounted directory lives in memory, starts empty, and its content is discarded when the container + * stops.
+ * + * @param string $pathOnContainer The directory inside the container backed by memory. + * @return GenericDockerContainer The copy with the added tmpfs mount. + */ + public function withTmpfsMount(string $pathOnContainer): GenericDockerContainer + { + return new GenericDockerContainer( + waits: $this->waits, + client: $this->client, + definition: $this->definition->withTmpfsMount(pathOnContainer: $pathOnContainer) + ); + } + /** * Returns a copy of the GenericDockerContainer with a file or directory added to copy into the container. * diff --git a/src/Internal/Client/CommandLine.php b/src/Internal/Client/CommandLine.php index f30fc7a..bac8450 100644 --- a/src/Internal/Client/CommandLine.php +++ b/src/Internal/Client/CommandLine.php @@ -19,7 +19,7 @@ public function run(): Execution try { $this->process->run(); } catch (Throwable $exception) { - throw new DockerCommandExecutionFailed( + throw DockerCommandExecutionFailed::from( reason: $exception->getMessage(), command: $this->process->getCommandLine() ); diff --git a/src/Internal/Containers/Definitions/ContainerDefinition.php b/src/Internal/Containers/Definitions/ContainerDefinition.php index b9431cf..6cbe2d1 100644 --- a/src/Internal/Containers/Definitions/ContainerDefinition.php +++ b/src/Internal/Containers/Definitions/ContainerDefinition.php @@ -15,6 +15,7 @@ private function __construct( public Image $image, public Removal $removal, public Collection $networks, + public Collection $tmpfsMounts, public Collection $portMappings, public Collection $volumeMappings, public Collection $copyInstructions, @@ -29,6 +30,7 @@ public static function create(string $image): ContainerDefinition image: Image::from(image: $image), removal: Removal::AUTOMATIC, networks: Collection::createFromEmpty(), + tmpfsMounts: Collection::createFromEmpty(), portMappings: Collection::createFromEmpty(), volumeMappings: Collection::createFromEmpty(), copyInstructions: Collection::createFromEmpty(), @@ -43,6 +45,7 @@ public function withName(string $name): ContainerDefinition image: $this->image, removal: $this->removal, networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts, portMappings: $this->portMappings, volumeMappings: $this->volumeMappings, copyInstructions: $this->copyInstructions, @@ -55,6 +58,7 @@ public function toArguments(): array return $this->portMappings ->merge(other: $this->networks) ->merge(other: $this->volumeMappings) + ->merge(other: $this->tmpfsMounts) ->add($this->removal) ->merge(other: $this->environmentVariables) ->reduce( @@ -73,6 +77,22 @@ public function withNetwork(string $name): ContainerDefinition image: $this->image, removal: $this->removal, networks: Collection::createFrom(elements: [Network::from(name: $name)]), + tmpfsMounts: $this->tmpfsMounts, + portMappings: $this->portMappings, + volumeMappings: $this->volumeMappings, + copyInstructions: $this->copyInstructions, + environmentVariables: $this->environmentVariables + ); + } + + public function withTmpfsMount(string $pathOnContainer): ContainerDefinition + { + return new ContainerDefinition( + name: $this->name, + image: $this->image, + removal: $this->removal, + networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts->add(TmpfsMount::from(pathOnContainer: $pathOnContainer)), portMappings: $this->portMappings, volumeMappings: $this->volumeMappings, copyInstructions: $this->copyInstructions, @@ -87,6 +107,7 @@ public function withPortMapping(int $portOnHost, int $portOnContainer): Containe image: $this->image, removal: $this->removal, networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts, portMappings: $this->portMappings->add( PortMapping::from(portOnHost: $portOnHost, portOnContainer: $portOnContainer) ), @@ -103,6 +124,7 @@ public function withVolumeMapping(string $pathOnHost, string $pathOnContainer): image: $this->image, removal: $this->removal, networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts, portMappings: $this->portMappings, volumeMappings: $this->volumeMappings->add( VolumeMapping::from(pathOnHost: $pathOnHost, pathOnContainer: $pathOnContainer) @@ -119,6 +141,7 @@ public function withoutAutoRemove(): ContainerDefinition image: $this->image, removal: Removal::MANUAL, networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts, portMappings: $this->portMappings, volumeMappings: $this->volumeMappings, copyInstructions: $this->copyInstructions, @@ -133,6 +156,7 @@ public function withCopyInstruction(string $pathOnHost, string $pathOnContainer) image: $this->image, removal: $this->removal, networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts, portMappings: $this->portMappings, volumeMappings: $this->volumeMappings, copyInstructions: $this->copyInstructions->add( @@ -149,6 +173,7 @@ public function withEnvironmentVariable(string $key, string $value): ContainerDe image: $this->image, removal: $this->removal, networks: $this->networks, + tmpfsMounts: $this->tmpfsMounts, portMappings: $this->portMappings, volumeMappings: $this->volumeMappings, copyInstructions: $this->copyInstructions, diff --git a/src/Internal/Containers/Definitions/TmpfsMount.php b/src/Internal/Containers/Definitions/TmpfsMount.php new file mode 100644 index 0000000..3d6f9b9 --- /dev/null +++ b/src/Internal/Containers/Definitions/TmpfsMount.php @@ -0,0 +1,22 @@ +pathOnContainer]; + } +} diff --git a/src/Internal/Containers/Drivers/Flyway/FlywaySettings.php b/src/Internal/Containers/Drivers/Flyway/FlywaySettings.php new file mode 100644 index 0000000..9306eaf --- /dev/null +++ b/src/Internal/Containers/Drivers/Flyway/FlywaySettings.php @@ -0,0 +1,63 @@ +source, settings: [...$this->settings, $key => $value]); + } + + public function applyTo(GenericDockerContainer $container): GenericDockerContainer + { + $settings = [...$this->sourceSettings(), ...$this->settings]; + + return array_reduce( + array_keys($settings), + static fn(GenericDockerContainer $container, string $key): GenericDockerContainer => $container + ->withEnvironmentVariable(key: $key, value: $settings[$key]), + $container + ); + } + + public function withSource(MySQLDockerContainer $source): FlywaySettings + { + return new FlywaySettings(source: $source, settings: $this->settings); + } + + private function sourceSettings(): array + { + if (is_null($this->source)) { + return []; + } + + $sourceStarted = $this->source->runIfNotExists(); + $environmentVariables = $sourceStarted->getEnvironmentVariables(); + + return [ + 'FLYWAY_URL' => $sourceStarted->getJdbcUrl(), + 'FLYWAY_USER' => self::ROOT_USERNAME, + 'FLYWAY_TABLE' => self::HISTORY_TABLE, + 'FLYWAY_SCHEMAS' => $environmentVariables->getValueBy(key: 'MYSQL_DATABASE'), + 'FLYWAY_PASSWORD' => $environmentVariables->getValueBy(key: 'MYSQL_ROOT_PASSWORD') + ]; + } +} diff --git a/src/Internal/Containers/Drivers/MySQL/MySQLStartup.php b/src/Internal/Containers/Drivers/MySQL/MySQLStartup.php index 1b81d55..4fb40f2 100644 --- a/src/Internal/Containers/Drivers/MySQL/MySQLStartup.php +++ b/src/Internal/Containers/Drivers/MySQL/MySQLStartup.php @@ -11,16 +11,21 @@ final readonly class MySQLStartup { + private const array EPHEMERAL_SERVER_ARGUMENTS = ['--skip-log-bin']; private const int DEFAULT_READINESS_TIMEOUT_IN_SECONDS = 30; - private function __construct(private array $grantedHosts, private Duration $readinessTimeout) - { + private function __construct( + private array $grantedHosts, + private array $serverArguments, + private Duration $readinessTimeout + ) { } public static function create(): MySQLStartup { return new MySQLStartup( grantedHosts: [], + serverArguments: [], readinessTimeout: Duration::fromSeconds(seconds: self::DEFAULT_READINESS_TIMEOUT_IN_SECONDS) ); } @@ -44,13 +49,35 @@ public function complete(ContainerStarted $containerStarted): void } } + public function commandsFor(array $commands): array + { + return [...$commands, ...$this->serverArguments]; + } + public function withGrantedHosts(array $hosts): MySQLStartup { - return new MySQLStartup(grantedHosts: $hosts, readinessTimeout: $this->readinessTimeout); + return new MySQLStartup( + grantedHosts: $hosts, + serverArguments: $this->serverArguments, + readinessTimeout: $this->readinessTimeout + ); + } + + public function withEphemeralStorage(): MySQLStartup + { + return new MySQLStartup( + grantedHosts: $this->grantedHosts, + serverArguments: self::EPHEMERAL_SERVER_ARGUMENTS, + readinessTimeout: $this->readinessTimeout + ); } public function withReadinessTimeout(Duration $timeout): MySQLStartup { - return new MySQLStartup(grantedHosts: $this->grantedHosts, readinessTimeout: $timeout); + return new MySQLStartup( + grantedHosts: $this->grantedHosts, + serverArguments: $this->serverArguments, + readinessTimeout: $timeout + ); } } diff --git a/src/Internal/Containers/Environment/EnvironmentFlag.php b/src/Internal/Containers/Environment/EnvironmentFlag.php new file mode 100644 index 0000000..e500a47 --- /dev/null +++ b/src/Internal/Containers/Environment/EnvironmentFlag.php @@ -0,0 +1,24 @@ +variable) === self::ENABLED; + } +} diff --git a/src/MySQLDockerContainer.php b/src/MySQLDockerContainer.php index 0ec61b7..e05e4c7 100644 --- a/src/MySQLDockerContainer.php +++ b/src/MySQLDockerContainer.php @@ -22,6 +22,8 @@ */ final readonly class MySQLDockerContainer { + private const string DATA_DIRECTORY = '/var/lib/mysql'; + private function __construct(private MySQLStartup $startup, private GenericDockerContainer $container) { } @@ -61,7 +63,7 @@ public static function from( */ public function run(array $commands = []): MySQLContainerStarted&DisposableContainer { - $containerStarted = $this->container->run(commands: $commands); + $containerStarted = $this->container->run(commands: $this->startup->commandsFor(commands: $commands)); $this->startup->complete(containerStarted: $containerStarted); return MySQLStarted::from(containerStarted: $containerStarted); @@ -150,7 +152,9 @@ public function withUsername(string $user): MySQLDockerContainer */ public function runIfNotExists(array $commands = []): MySQLContainerStarted&SharedContainer { - $containerStarted = $this->container->runIfNotExists(commands: $commands); + $containerStarted = $this->container->runIfNotExists( + commands: $this->startup->commandsFor(commands: $commands) + ); $this->startup->complete(containerStarted: $containerStarted); return MySQLShared::from(containerStarted: $containerStarted); @@ -250,6 +254,23 @@ public function withoutAutoRemove(): MySQLDockerContainer return new MySQLDockerContainer(startup: $this->startup, container: $this->container->withoutAutoRemove()); } + /** + * Returns a copy of the MySQLDockerContainer that keeps its data in memory. + * + *The data directory is a tmpfs mount, so the data is discarded when the container stops, and the server runs + * without the binary log, which would otherwise grow in that memory. Meant for test databases, never for data + * that must outlive the container.
+ * + * @return MySQLDockerContainer The copy with ephemeral storage. + */ + public function withEphemeralStorage(): MySQLDockerContainer + { + return new MySQLDockerContainer( + startup: $this->startup->withEphemeralStorage(), + container: $this->container->withTmpfsMount(pathOnContainer: self::DATA_DIRECTORY) + ); + } + /** * Returns a copy of the MySQLDockerContainer with the readiness timeout replaced. * diff --git a/tests/Integration/DockerContainerTest.php b/tests/Integration/DockerContainerTest.php index d43bff0..dc593f7 100644 --- a/tests/Integration/DockerContainerTest.php +++ b/tests/Integration/DockerContainerTest.php @@ -18,13 +18,15 @@ final class DockerContainerTest extends TestCase private const string ROOT = 'xpto'; private const string DATABASE = 'test_adm'; + private static MySQLDockerContainer $mySQL; + private static MySQLContainerStarted $mySQLStarted; public static function setUpBeforeClass(): void { $template = 'test-database-%s'; - self::$mySQLStarted = MySQLDockerContainer::from(image: 'mysql:8.4') + self::$mySQL = MySQLDockerContainer::from(image: 'mysql:8.4') ->withName(name: sprintf($template, gethostname())) ->withNetwork(name: 'tiny-blocks') ->withDatabase(database: self::DATABASE) @@ -34,8 +36,9 @@ public static function setUpBeforeClass(): void ->withPortMapping(portOnHost: 0, portOnContainer: 3306) ->withGrantedHosts() ->withRootPassword(rootPassword: self::ROOT) - ->withReadinessTimeout(timeout: Duration::fromSeconds(seconds: 60)) - ->runIfNotExists(); + ->withEphemeralStorage(); + + self::$mySQLStarted = self::$mySQL->runIfNotExists(); } public function testMySQLWhenStartedThenAcceptsConnectionsToItsDatabase(): void @@ -122,10 +125,9 @@ public function testCleanAndMigrateWhenMigrationIsSlowThenWaitsForFlywayToFinish /** @And a Flyway container bound to the MySQL container with that slow migration */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine') - ->withSource(password: self::ROOT, username: self::ROOT, container: self::$mySQLStarted) + ->withSource(container: self::$mySQL) ->withNetwork(name: 'tiny-blocks') - ->withMigrations(pathOnHost: sprintf($template, __DIR__)) - ->withConnectRetries(retries: 15); + ->withMigrations(pathOnHost: sprintf($template, __DIR__)); /** @When Flyway cleans the schema and runs the slow migration */ $flyway->cleanAndMigrate(); @@ -144,10 +146,9 @@ public function testCleanAndMigrateWhenMigrationsAreGivenThenDatabaseIsPopulated /** @And a Flyway container bound to the MySQL container */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine') - ->withSource(password: self::ROOT, username: self::ROOT, container: self::$mySQLStarted) + ->withSource(container: self::$mySQL) ->withNetwork(name: 'tiny-blocks') ->withMigrations(pathOnHost: sprintf($template, __DIR__)) - ->withConnectRetries(retries: 15) ->withMigrationNamingValidation(); /** @When Flyway cleans the schema and migrates it */ diff --git a/tests/Unit/DefinedMySQLContainer.php b/tests/Unit/DefinedMySQLContainer.php new file mode 100644 index 0000000..6b3c534 --- /dev/null +++ b/tests/Unit/DefinedMySQLContainer.php @@ -0,0 +1,39 @@ +withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: $hostname, + environment: [ + sprintf($databaseTemplate, $database), + 'MYSQL_ROOT_PASSWORD=root' + ] + ) + ); + + $client->withDockerExecuteResponse(output: 'mysqld is alive'); + $client->withDockerExecuteResponse(output: ''); + + return MySQLDockerContainer::from(image: 'mysql:8.4', client: $client) + ->withName(name: $hostname) + ->withDatabase(database: $database) + ->withRootPassword(rootPassword: 'root'); + } +} diff --git a/tests/Unit/FlywayContainerGatedTest.php b/tests/Unit/FlywayContainerGatedTest.php new file mode 100644 index 0000000..b5e2f70 --- /dev/null +++ b/tests/Unit/FlywayContainerGatedTest.php @@ -0,0 +1,223 @@ +client = new ClientMock(); + } + + protected function tearDown(): void + { + putenv('RUN_MIGRATIONS'); + } + + public function testMigrateWhenVariableIsNotSetThenNothingRuns(): void + { + /** @Given the variable that enables the migrations is not set */ + putenv('RUN_MIGRATIONS'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @When migrate is called */ + $execution = $flyway->migrate(); + + /** @Then nothing should have run, and no Docker command should have been issued */ + self::assertSame([null, []], [$execution, $this->client->getExecutedArguments()]); + } + + public function testRepairWhenVariableIsDisabledThenNothingRuns(): void + { + /** @Given the variable that enables the migrations is set to 0 */ + putenv('RUN_MIGRATIONS=0'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @When repair is called */ + $execution = $flyway->repair(); + + /** @Then nothing should have run, and no Docker command should have been issued */ + self::assertSame([null, []], [$execution, $this->client->getExecutedArguments()]); + } + + public function testValidateWhenVariableIsDisabledThenNothingRuns(): void + { + /** @Given the variable that enables the migrations is set to 0 */ + putenv('RUN_MIGRATIONS=0'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @When validate is called */ + $execution = $flyway->validate(); + + /** @Then nothing should have run, and no Docker command should have been issued */ + self::assertSame([null, []], [$execution, $this->client->getExecutedArguments()]); + } + + public function testRepairWhenVariableIsEnabledThenRunsFlywayRepair(): void + { + /** @Given the variable that enables the migrations is set to 1 */ + putenv('RUN_MIGRATIONS=1'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @And Flyway exits successfully */ + $this->client->withDockerAttachedStartResponse(output: 'Successfully repaired schema history table'); + + /** @When repair is called */ + $execution = $flyway->repair(); + + /** @Then Flyway should have run the repair command and returned its output */ + self::assertStringEndsWith('flyway/flyway:12-alpine repair', $this->client->getExecutedCommandLines()[0]); + self::assertSame('Successfully repaired schema history table', $execution?->getOutput()); + } + + public function testMigrateWhenVariableIsEnabledThenRunsFlywayMigrate(): void + { + /** @Given the variable that enables the migrations is set to 1 */ + putenv('RUN_MIGRATIONS=1'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @And Flyway exits successfully */ + $this->client->withDockerAttachedStartResponse(output: 'Successfully applied 2 migrations'); + + /** @When migrate is called */ + $execution = $flyway->migrate(); + + /** @Then Flyway should have run the migrate command and returned its output */ + self::assertStringEndsWith('flyway/flyway:12-alpine migrate', $this->client->getExecutedCommandLines()[0]); + self::assertSame('Successfully applied 2 migrations', $execution?->getOutput()); + } + + public function testValidateWhenVariableIsEnabledThenRunsFlywayValidate(): void + { + /** @Given the variable that enables the migrations is set to 1 */ + putenv('RUN_MIGRATIONS=1'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @And Flyway exits successfully */ + $this->client->withDockerAttachedStartResponse(output: 'Successfully validated 2 migrations'); + + /** @When validate is called */ + $execution = $flyway->validate(); + + /** @Then Flyway should have run the validate command and returned its output */ + self::assertStringEndsWith('flyway/flyway:12-alpine validate', $this->client->getExecutedCommandLines()[0]); + self::assertSame('Successfully validated 2 migrations', $execution?->getOutput()); + } + + public function testMigrateWhenVariableIsOneWithLeadingZeroThenNothingRuns(): void + { + /** @Given the variable that enables the migrations is set to 01, which is not 1 */ + putenv('RUN_MIGRATIONS=01'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @When migrate is called */ + $execution = $flyway->migrate(); + + /** @Then nothing should have run, and no Docker command should have been issued */ + self::assertSame([null, []], [$execution, $this->client->getExecutedArguments()]); + } + + public function testCleanAndMigrateWhenVariableIsDisabledThenNeitherMySQLNorFlywayRuns(): void + { + /** @Given the variable that enables the migrations is set to 0 */ + putenv('RUN_MIGRATIONS=0'); + + /** @And a MySQL container definition */ + $mySQL = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client)->withName(name: 'skipped-db'); + + /** @And a Flyway container with the MySQL source that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->withSource(container: $mySQL) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @When cleanAndMigrate is called */ + $execution = $flyway->cleanAndMigrate(); + + /** @Then nothing should have run, and no Docker command should have been issued for MySQL or Flyway */ + self::assertSame([null, []], [$execution, $this->client->getExecutedArguments()]); + } + + public function testCleanAndMigrateWhenVariableIsEnabledThenRunsCleanFollowedByMigrate(): void + { + /** @Given the variable that enables the migrations is set to 1 */ + putenv('RUN_MIGRATIONS=1'); + + /** @And a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @And Flyway exits successfully */ + $this->client->withDockerAttachedStartResponse(output: 'Successfully applied 2 migrations'); + + /** @When cleanAndMigrate is called */ + $execution = $flyway->cleanAndMigrate(); + + /** @Then Flyway should have run the clean and migrate commands and returned its output */ + self::assertStringEndsWith( + 'flyway/flyway:12-alpine clean migrate', + $this->client->getExecutedCommandLines()[0] + ); + self::assertSame('Successfully applied 2 migrations', $execution?->getOutput()); + } + + public function testMigrateWhenVariableIsEnabledAfterTheDefinitionThenRunsFlywayMigrate(): void + { + /** @Given a Flyway container that runs only while the variable is enabled */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->onlyWhenEnabled(variable: 'RUN_MIGRATIONS'); + + /** @And the variable that enables the migrations is set to 1 afterwards */ + putenv('RUN_MIGRATIONS=1'); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @When migrate is called */ + $flyway->migrate(); + + /** @Then the variable should be read when the command is called, so Flyway should have run */ + self::assertStringEndsWith('flyway/flyway:12-alpine migrate', $this->client->getExecutedCommandLines()[0]); + } +} diff --git a/tests/Unit/FlywayDockerContainerTest.php b/tests/Unit/FlywayDockerContainerTest.php index b993687..f4da040 100644 --- a/tests/Unit/FlywayDockerContainerTest.php +++ b/tests/Unit/FlywayDockerContainerTest.php @@ -10,6 +10,7 @@ use TinyBlocks\DockerContainer\Exceptions\ContainerWaitTimeout; use TinyBlocks\DockerContainer\Exceptions\DockerCommandExecutionFailed; use TinyBlocks\DockerContainer\FlywayDockerContainer; +use TinyBlocks\DockerContainer\MySQLDockerContainer; use TinyBlocks\Time\Duration; final class FlywayDockerContainerTest extends TestCase @@ -59,17 +60,17 @@ public function testWithNetworkThenFlywayJoinsTheNetwork(): void public function testWithTableThenOverridesTheHistoryTable(): void { - /** @Given a running MySQL container */ - $mySQLStarted = RunningMySQLContainer::startWith( + /** @Given a MySQL container definition */ + $mySQL = DefinedMySQLContainer::definedWith( client: $this->client, database: 'test_db', hostname: 'custom-table-db' ); - /** @And a Flyway container with the MySQL source and a table override */ + /** @And a Flyway container with a table override and the MySQL source */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) - ->withSource(password: 'root', username: 'root', container: $mySQLStarted) - ->withTable(table: 'flyway_history'); + ->withTable(table: 'flyway_history') + ->withSource(container: $mySQL); /** @And the Docker daemon creates the Flyway container */ $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); @@ -77,11 +78,10 @@ public function testWithTableThenOverridesTheHistoryTable(): void /** @When migrate is called */ $flyway->migrate(); - /** @Then the overridden table should come after the default one, so it prevails */ - self::assertMatchesRegularExpression( - '/FLYWAY_TABLE=schema_history .*FLYWAY_TABLE=flyway_history/', - implode(PHP_EOL, $this->client->getExecutedCommandLines()) - ); + /** @Then the overridden table should replace the default one */ + $commandLines = implode(PHP_EOL, $this->client->getExecutedCommandLines()); + self::assertStringContainsString('FLYWAY_TABLE=flyway_history', $commandLines); + self::assertStringNotContainsString('FLYWAY_TABLE=schema_history', $commandLines); } public function testRepairThenRunsFlywayRepairToCompletion(): void @@ -130,17 +130,17 @@ public function testMigrateThenRunsFlywayMigrateToCompletion(): void public function testWithSchemaThenOverridesTheDetectedSchema(): void { - /** @Given a running MySQL container with database "original" */ - $mySQLStarted = RunningMySQLContainer::startWith( + /** @Given a MySQL container definition with database "original" */ + $mySQL = DefinedMySQLContainer::definedWith( client: $this->client, database: 'original', hostname: 'override-db' ); - /** @And a Flyway container with the MySQL source and a schema override */ + /** @And a Flyway container with a schema override and the MySQL source */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) - ->withSource(password: 'root', username: 'root', container: $mySQLStarted) - ->withSchema(schema: 'custom_schema'); + ->withSchema(schema: 'custom_schema') + ->withSource(container: $mySQL); /** @And the Docker daemon creates the Flyway container */ $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); @@ -148,17 +148,16 @@ public function testWithSchemaThenOverridesTheDetectedSchema(): void /** @When migrate is called */ $flyway->migrate(); - /** @Then the overridden schema should come after the detected one, so it prevails */ - self::assertMatchesRegularExpression( - '/FLYWAY_SCHEMAS=original .*FLYWAY_SCHEMAS=custom_schema/', - implode(PHP_EOL, $this->client->getExecutedCommandLines()) - ); + /** @Then the overridden schema should replace the detected one */ + $commandLines = implode(PHP_EOL, $this->client->getExecutedCommandLines()); + self::assertStringContainsString('FLYWAY_SCHEMAS=custom_schema', $commandLines); + self::assertStringNotContainsString('FLYWAY_SCHEMAS=original', $commandLines); } public function testWithSourceThenSetsTheDefaultHistoryTable(): void { - /** @Given a running MySQL container */ - $mySQLStarted = RunningMySQLContainer::startWith( + /** @Given a MySQL container definition */ + $mySQL = DefinedMySQLContainer::definedWith( client: $this->client, database: 'test_db', hostname: 'table-db' @@ -166,7 +165,7 @@ public function testWithSourceThenSetsTheDefaultHistoryTable(): void /** @And a Flyway container with the MySQL source */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) - ->withSource(password: 'root', username: 'root', container: $mySQLStarted); + ->withSource(container: $mySQL); /** @And the Docker daemon creates the Flyway container */ $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); @@ -219,32 +218,6 @@ public function testWithConnectRetriesThenSetsTheNumberOfRetries(): void self::assertStringContainsString('FLYWAY_CONNECT_RETRIES=10', $this->client->getExecutedCommandLines()[0]); } - public function testWithSourceThenConfiguresJdbcUrlAndCredentials(): void - { - /** @Given a running MySQL container */ - $mySQLStarted = RunningMySQLContainer::startWith( - client: $this->client, - database: 'app_database', - hostname: 'source-db' - ); - - /** @And a Flyway container with the MySQL source */ - $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) - ->withSource(password: 'secret', username: 'admin', container: $mySQLStarted); - - /** @And the Docker daemon creates the Flyway container */ - $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); - - /** @When migrate is called */ - $flyway->migrate(); - - /** @Then the Flyway container should carry the JDBC URL and the credentials */ - $commandLines = implode(PHP_EOL, $this->client->getExecutedCommandLines()); - self::assertStringContainsString('FLYWAY_URL=jdbc:mysql://source-db:3306/app_database', $commandLines); - self::assertStringContainsString('FLYWAY_USER=admin', $commandLines); - self::assertStringContainsString('FLYWAY_PASSWORD=secret', $commandLines); - } - public function testWithMigrationsThenCopiesThemBeforeFlywayStarts(): void { /** @Given a Flyway container with migrations */ @@ -286,18 +259,19 @@ public function testMigrateWhenFlywaySucceedsThenReturnsFlywayOutput(): void self::assertSame('Successfully applied 2 migrations', $execution->getOutput()); } - public function testWithSourceThenWaitsForMySQLBeforeFlywayIsCreated(): void + public function testWithCredentialsThenOverridesTheSourceCredentials(): void { - /** @Given a running MySQL container, pinged once while it started */ - $mySQLStarted = RunningMySQLContainer::startWith( + /** @Given a MySQL container definition */ + $mySQL = DefinedMySQLContainer::definedWith( client: $this->client, database: 'app_database', - hostname: 'ready-db' + hostname: 'credentials-db' ); - /** @And a Flyway container with the MySQL source */ + /** @And a Flyway container with the MySQL source and other credentials */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) - ->withSource(password: 'root', username: 'root', container: $mySQLStarted); + ->withSource(container: $mySQL) + ->withCredentials(password: 'secret', username: 'admin'); /** @And the Docker daemon creates the Flyway container */ $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); @@ -305,14 +279,11 @@ public function testWithSourceThenWaitsForMySQLBeforeFlywayIsCreated(): void /** @When migrate is called */ $flyway->migrate(); - /** @Then MySQL should have been pinged again right before the Flyway container was created */ - self::assertSame( - ['docker exec ready-db env MYSQL_PWD=root mysqladmin ping -h 127.0.0.1', 'docker create'], - [ - $this->client->getExecutedCommandLines()[5], - implode(' ', array_slice($this->client->getExecutedArguments()[6], 0, 2)) - ] - ); + /** @Then the overriding credentials should replace the root ones */ + $commandLines = implode(PHP_EOL, $this->client->getExecutedCommandLines()); + self::assertStringContainsString('FLYWAY_USER=admin', $commandLines); + self::assertStringContainsString('FLYWAY_PASSWORD=secret', $commandLines); + self::assertStringNotContainsString('FLYWAY_USER=root', $commandLines); } public function testMigrateWhenTimeoutIsGivenThenWaitsUpToThatTimeout(): void @@ -350,6 +321,47 @@ public function testWithEnvironmentVariableThenPassesTheFlywaySetting(): void ); } + public function testWithSourceThenConfiguresJdbcUrlAndRootCredentials(): void + { + /** @Given a MySQL container definition */ + $mySQL = DefinedMySQLContainer::definedWith( + client: $this->client, + database: 'app_database', + hostname: 'source-db' + ); + + /** @And a Flyway container with the MySQL source */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->withSource(container: $mySQL); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @When migrate is called */ + $flyway->migrate(); + + /** @Then the Flyway container should carry the JDBC URL and the root credentials of the MySQL container */ + $commandLines = implode(PHP_EOL, $this->client->getExecutedCommandLines()); + self::assertStringContainsString('FLYWAY_URL=jdbc:mysql://source-db:3306/app_database', $commandLines); + self::assertStringContainsString('FLYWAY_USER=root', $commandLines); + self::assertStringContainsString('FLYWAY_PASSWORD=root', $commandLines); + } + + public function testWithSourceThenMySQLIsNotStartedBeforeACommandRuns(): void + { + /** @Given a MySQL container definition */ + $mySQL = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client)->withName(name: 'lazy-db'); + + /** @And a Flyway container */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client); + + /** @When the MySQL container is set as the Flyway source */ + $flyway->withSource(container: $mySQL); + + /** @Then no Docker command should have been issued */ + self::assertSame([], $this->client->getExecutedArguments()); + } + public function testMigrateWhenFlywayFailsThenFlywayContainerIsRemoved(): void { /** @Given a Flyway container */ @@ -375,8 +387,8 @@ public function testMigrateWhenFlywayFailsThenFlywayContainerIsRemoved(): void public function testWithSourceThenDetectsTheSchemaFromTheMySQLDatabase(): void { - /** @Given a running MySQL container with database "products" */ - $mySQLStarted = RunningMySQLContainer::startWith( + /** @Given a MySQL container definition with database "products" */ + $mySQL = DefinedMySQLContainer::definedWith( client: $this->client, database: 'products', hostname: 'schema-db' @@ -384,7 +396,7 @@ public function testWithSourceThenDetectsTheSchemaFromTheMySQLDatabase(): void /** @And a Flyway container with the MySQL source */ $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) - ->withSource(password: 'root', username: 'root', container: $mySQLStarted); + ->withSource(container: $mySQL); /** @And the Docker daemon creates the Flyway container */ $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); @@ -451,6 +463,35 @@ public function testWithMigrationNamingValidationThenEnablesTheValidation(): voi ); } + public function testWithSourceThenFlywayIsCreatedWithoutPingingMySQLAgain(): void + { + /** @Given a MySQL container definition */ + $mySQL = DefinedMySQLContainer::definedWith( + client: $this->client, + database: 'app_database', + hostname: 'ready-db' + ); + + /** @And a Flyway container with the MySQL source */ + $flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine', client: $this->client) + ->withSource(container: $mySQL); + + /** @And the Docker daemon creates the Flyway container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @When migrate is called */ + $flyway->migrate(); + + /** @Then MySQL should be pinged once while it starts, with no second ping before Flyway is created */ + self::assertCount( + 1, + array_filter( + $this->client->getExecutedCommandLines(), + static fn(string $commandLine): bool => str_contains($commandLine, 'mysqladmin ping') + ) + ); + } + public function testMigrateThenFlywayContainerIsNotRemovedByDockerOnItsOwn(): void { /** @Given a Flyway container */ @@ -502,7 +543,7 @@ public function testMigrateWhenFlywayOutlastsTimeoutThenContainerWaitTimeout(): /** @And the attached run is interrupted when the timeout is reached */ $this->client->withDockerAttachedStartException( - exception: new DockerCommandExecutionFailed( + exception: DockerCommandExecutionFailed::from( reason: 'The process exceeded the timeout of 45 seconds.', command: 'docker start --attach 6acae5967be0' ) @@ -544,7 +585,7 @@ public function testMigrateWhenFlywayOutlastsTimeoutThenFlywayContainerIsRemoved /** @And the attached run is interrupted when the timeout is reached */ $this->client->withDockerAttachedStartException( - exception: new DockerCommandExecutionFailed(reason: 'timeout', command: 'docker start') + exception: DockerCommandExecutionFailed::from(reason: 'timeout', command: 'docker start') ); try { diff --git a/tests/Unit/GenericDockerContainerTest.php b/tests/Unit/GenericDockerContainerTest.php index 60e7a8d..b2783b9 100644 --- a/tests/Unit/GenericDockerContainerTest.php +++ b/tests/Unit/GenericDockerContainerTest.php @@ -429,6 +429,28 @@ public function testRunCommandLineIncludesPortMapping(): void self::assertStringContainsString('--publish 8080:80', $runCommand); } + public function testRunCommandLineIncludesTmpfsMounts(): void + { + /** @Given a container with two tmpfs mounts */ + $container = GenericDockerContainer::from(image: 'alpine:latest', client: $this->client) + ->withName(name: 'tmpfs-cmd') + ->withTmpfsMount(pathOnContainer: '/app/cache') + ->withTmpfsMount(pathOnContainer: '/app/sessions'); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse(inspectResult: InspectResponseFixture::build(hostname: 'tmpfs-cmd')); + + /** @When the container is started */ + $container->run(); + + /** @Then the docker create command should mount both directories in memory */ + self::assertStringContainsString( + '--tmpfs /app/cache --tmpfs /app/sessions', + $this->client->getExecutedCommandLines()[0] + ); + } + public function testRunContainerWithAutoGeneratedName(): void { /** @Given a container without a name */ @@ -1866,4 +1888,24 @@ public function testStopContainerWhenCustomTimeoutThenCommandCarriesTimeoutAsStr $stopArguments ); } + + public function testRunToCompletionWhenWaitBeforeRunIsRegisteredThenWaitsBeforeCreatingTheContainer(): void + { + /** @Given a condition that is immediately ready */ + $condition = $this->createMock(ContainerReady::class); + $condition->expects(self::once())->method('isReady')->willReturn(true); + + /** @And a container with a wait-before-run condition */ + $container = GenericDockerContainer::from(image: 'alpine:latest', client: $this->client) + ->withWaitBeforeRun(wait: ContainerWaitForDependency::untilReady(condition: $condition)); + + /** @And the Docker daemon creates the container */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + + /** @When the container runs to completion */ + $container->runToCompletion(timeout: Duration::fromSeconds(seconds: 10)); + + /** @Then the container should be created once the wait is satisfied */ + self::assertStringStartsWith('docker create', $this->client->getExecutedCommandLines()[0]); + } } diff --git a/tests/Unit/MySQLDockerContainerTest.php b/tests/Unit/MySQLDockerContainerTest.php index ef21e9b..f1ccb45 100644 --- a/tests/Unit/MySQLDockerContainerTest.php +++ b/tests/Unit/MySQLDockerContainerTest.php @@ -738,6 +738,33 @@ public function testRunIfNotExistsWhenContainerExistsThenStartedWasReused(): voi self::assertTrue($started->wasReused()); } + public function testRunWhenEphemeralStorageThenDataDirectoryIsATmpfsMount(): void + { + /** @Given a MySQL container with ephemeral storage */ + $container = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'ephemeral-db') + ->withRootPassword(rootPassword: 'root') + ->withEphemeralStorage(); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'ephemeral-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check succeeds */ + $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); + + /** @When the MySQL container is started */ + $container->run(); + + /** @Then the MySQL data directory should be a tmpfs mount */ + self::assertStringContainsString('--tmpfs /var/lib/mysql', $this->client->getExecutedCommandLines()[0]); + } + public function testExceptionWhenMySQLReadinessCheckAlwaysThrowsExceptions(): void { /** @Given a MySQL container with a very short readiness timeout */ @@ -758,7 +785,7 @@ public function testExceptionWhenMySQLReadinessCheckAlwaysThrowsExceptions(): vo /** @And the MySQL readiness check always throws exceptions */ for ($index = 0; $index < 100; $index++) { $this->client->withDockerExecuteException( - exception: new DockerCommandExecutionFailed(reason: 'container crashed', command: 'docker exec') + exception: DockerCommandExecutionFailed::from(reason: 'container crashed', command: 'docker exec') ); } @@ -802,6 +829,36 @@ public function testRunMySQLContainerRetriesReadinessCheckBeforeSucceeding(): vo self::assertSame('retry-db', $started->getName()); } + public function testRunWhenEphemeralStorageThenServerStartsWithoutBinaryLog(): void + { + /** @Given a MySQL container with ephemeral storage */ + $container = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'fast-db') + ->withRootPassword(rootPassword: 'root') + ->withEphemeralStorage(); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'fast-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check succeeds */ + $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); + + /** @When the MySQL container is started */ + $container->run(); + + /** @Then the MySQL server should start without the binary log */ + self::assertSame( + ['mysql:8.4', '--skip-log-bin'], + array_slice($this->client->getExecutedArguments()[0], -2) + ); + } + public function testWithGrantedHostsThenLeavesTheOriginalDefinitionUntouched(): void { /** @Given a MySQL container without granted hosts */ @@ -850,7 +907,7 @@ public function testRunMySQLContainerRetriesWhenReadinessCheckThrowsException(): /** @And the MySQL readiness check throws an exception first, then succeeds */ $this->client->withDockerExecuteException( - exception: new DockerCommandExecutionFailed(reason: 'container not running', command: 'docker exec') + exception: DockerCommandExecutionFailed::from(reason: 'container not running', command: 'docker exec') ); $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); @@ -1001,6 +1058,37 @@ public function testStopWhenMySQLContainerIsSharedThenStopsTheUnderlyingContaine ); } + public function testRunWhenGrantedHostsAndEphemeralStorageThenGrantsAreStillApplied(): void + { + /** @Given a MySQL container with granted hosts, then ephemeral storage */ + $container = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'grants-kept-db') + ->withGrantedHosts(hosts: ['10.%']) + ->withRootPassword(rootPassword: 'root') + ->withEphemeralStorage(); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'grants-kept-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check succeeds */ + $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); + + /** @When the MySQL container is started */ + $container->run(); + + /** @Then the granted host should still be created */ + self::assertStringContainsString( + "CREATE USER IF NOT EXISTS 'root'@'10.%'", + array_last($this->client->getExecutedCommandLines()) + ); + } + public function testRunIfNotExistsWhenMySQLNeverBecomesReadyThenContainerWaitTimeout(): void { /** @Given a MySQL container with a readiness timeout of one second */ @@ -1036,6 +1124,133 @@ public function testRunIfNotExistsWhenMySQLNeverBecomesReadyThenContainerWaitTim $container->runIfNotExists(); } + public function testRunIfNotExistsWhenEphemeralStorageThenServerStartsWithoutBinaryLog(): void + { + /** @Given a MySQL container with ephemeral storage that does not exist yet */ + $container = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'shared-fast-db') + ->withRootPassword(rootPassword: 'root') + ->withEphemeralStorage(); + + /** @And the Docker list returns empty (container does not exist) */ + $this->client->withDockerListResponse(output: ''); + + /** @And the Docker daemon returns valid run and inspect responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'shared-fast-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check succeeds */ + $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); + + /** @When runIfNotExists is called */ + $container->runIfNotExists(); + + /** @Then the MySQL server should start without the binary log */ + self::assertSame( + ['mysql:8.4', '--skip-log-bin'], + array_slice($this->client->getExecutedArguments()[1], -2) + ); + } + + public function testRunWhenEphemeralStorageAndGrantedHostsThenServerStillSkipsBinaryLog(): void + { + /** @Given a MySQL container with ephemeral storage, then granted hosts and a readiness timeout */ + $ephemeral = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'granted-fast-db') + ->withRootPassword(rootPassword: 'root') + ->withEphemeralStorage(); + $container = $ephemeral + ->withGrantedHosts(hosts: ['%']) + ->withReadinessTimeout(timeout: Duration::fromSeconds(seconds: 5)); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'granted-fast-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check and the grant statements succeed */ + $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); + + /** @When the MySQL container is started */ + $container->run(); + + /** @Then the MySQL server should still start without the binary log */ + self::assertSame( + ['mysql:8.4', '--skip-log-bin'], + array_slice($this->client->getExecutedArguments()[0], -2) + ); + } + + public function testRunWhenReadinessTimeoutAndEphemeralStorageThenTimeoutIsStillApplied(): void + { + /** @Given a MySQL container with a short readiness timeout, then ephemeral storage */ + $timed = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'timeout-kept-db') + ->withRootPassword(rootPassword: 'root') + ->withReadinessTimeout(timeout: Duration::fromSeconds(seconds: 1)); + $container = $timed->withEphemeralStorage(); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'timeout-kept-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check always fails */ + for ($index = 0; $index < 4; $index++) { + $this->client->withDockerExecuteResponse(output: 'mysqld is not ready', isSuccessful: false); + } + + /** @Then a ContainerWaitTimeout exception should be thrown after the short timeout */ + $this->expectException(ContainerWaitTimeout::class); + $this->expectExceptionMessage('Container readiness check timed out after <1> seconds.'); + + /** @When the MySQL container is started */ + $container->run(); + } + + public function testRunWhenEphemeralStorageAndCommandsThenServerArgumentsFollowTheCommands(): void + { + /** @Given a MySQL container with ephemeral storage */ + $container = MySQLDockerContainer::from(image: 'mysql:8.4', client: $this->client) + ->withName(name: 'command-db') + ->withRootPassword(rootPassword: 'root') + ->withEphemeralStorage(); + + /** @And the Docker daemon returns valid responses */ + $this->client->withDockerCreateResponse(output: InspectResponseFixture::containerId()); + $this->client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build( + hostname: 'command-db', + environment: ['MYSQL_ROOT_PASSWORD=root'] + ) + ); + + /** @And the MySQL readiness check succeeds */ + $this->client->withDockerExecuteResponse(output: 'mysqld is alive'); + + /** @When the MySQL container is started with an explicit server command */ + $container->run(commands: ['mysqld', '--character-set-server=utf8mb4']); + + /** @Then the explicit command should come first and the ephemeral server arguments after it */ + self::assertSame( + ['mysql:8.4', 'mysqld', '--character-set-server=utf8mb4', '--skip-log-bin'], + array_slice($this->client->getExecutedArguments()[0], -4) + ); + } + public function testGetAddressWhenMySQLContainerIsSharedThenIsTheAddressOfTheExistingContainer(): void { /** @Given a MySQL container reused from another process */ From bb4c73d59c5df57a2e215da333f1f2eaace7c1a1 Mon Sep 17 00:00:00 2001 From: Gustavo Freze