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
472 changes: 314 additions & 158 deletions README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
"php": "^8.5",
"symfony/process": "^8.1",
"tiny-blocks/collection": "^2.6",
"tiny-blocks/ksuid": "^2.0"
"tiny-blocks/ksuid": "^2.0",
"tiny-blocks/time": "^2.7"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.52",
"infection/infection": "^0.34",
"infection/infection": "^0.35",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^13.2",
"slevomat/coding-standard": "^8.31",
Expand Down Expand Up @@ -73,7 +74,7 @@
"test-file": "@php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage --filter",
"tests": [
"@php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests",
"@php ./vendor/bin/infection --threads=max --logger-html=reports/coverage/mutation-report.html --coverage=reports/coverage"
"@php ./vendor/bin/infection --threads=max --logger-html=reports/coverage/mutation-report.html"
]
}
}
62 changes: 37 additions & 25 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,60 @@ parameters:
- tests
tmpDir: reports/phpstan
ignoreErrors:
# Late static binding in extension-point container factories that consumers subclass.
- identifier: new.static
paths:
- src/FlywayDockerContainer.php
- src/GenericDockerContainer.php
- src/MySQLDockerContainer.php
# tiny-blocks/collection generics carry a mixed TValue on Internal command and definition types.
- identifier: missingType.generics
paths:
- src/Internal/Commands/DockerExecute.php
- src/Internal/Containers/ContainerInspection.php
- src/Internal/Commands/DockerRun.php
- src/Internal/Containers/Address/Ports.php
- src/Internal/Commands/DockerCreate.php
- src/Internal/Containers/ContainerWaits.php
- src/Internal/Containers/Definitions/ContainerDefinition.php
- src/Internal/Containers/Environment/EnvironmentVariables.php
# Internal collaborators take raw config arrays at the boundary, where PHPDoc is prohibited.
- identifier: missingType.iterableValue
paths:
- src/Internal/Containers/ContainerLauncher.php
- src/Internal/Containers/OneShotContainer.php
- src/Internal/Containers/Drivers/MySQL/MySQLStartup.php
- src/Internal/Commands/DockerExecute.php
- src/Internal/Commands/DockerRun.php
- src/Internal/Commands/DockerCreate.php
- src/Internal/Containers/Address/Ports.php
- src/Internal/Containers/ContainerInspection.php
- src/Internal/Containers/Definitions/ContainerDefinition.php
- src/Internal/Containers/Definitions/CopyInstruction.php
- src/Internal/Containers/Definitions/EnvironmentVariable.php
- src/Internal/Containers/Definitions/PortMapping.php
- src/Internal/Containers/Definitions/VolumeMapping.php
- src/Internal/Containers/Drivers/MySQL/JdbcUrl.php
- src/Internal/Containers/Drivers/MySQL/MySQLCommands.php
- src/Internal/Containers/Environment/EnvironmentVariables.php
# Internal command builders return argument lists assembled from collection mixed values.
- identifier: return.type
paths:
- src/Internal/Commands/DockerCopy.php
- src/Internal/Commands/DockerExecute.php
- src/Internal/Commands/DockerRun.php
- src/Internal/Commands/DockerCreate.php
- src/Internal/Containers/Address/Ports.php
# Collection each/reduce pass concrete closures against the mixed-typed variadic API.
- identifier: argument.type
paths:
- src/Internal/CommandHandler/ContainerCommandHandler.php
- src/Internal/Commands/DockerRun.php
- src/Internal/Containers/ContainerLauncher.php
- src/Internal/Containers/ContainerWaits.php
- src/Internal/Containers/Address/Ports.php
- src/Internal/Containers/Definitions/ContainerDefinition.php
# ContainerSharing starts a new container through a plain Closure, whose result is mixed to phpstan.
- identifier: argument.type
path: src/Internal/Containers/ContainerSharing.php
# docker inspect json_decode and sprintf feed mixed values across the system boundary.
- identifier: argument.type
paths:
- src/Internal/Containers/ContainerInspection.php
- src/Internal/Containers/Drivers/MySQL/MySQLCommands.php
# gethostname() returns string|false at the system boundary when the host name is unavailable.
# MySQLCommands assembles the setup argument list as a raw array, where PHPDoc is prohibited.
- identifier: argument.type
path: src/Internal/Containers/ContainerReaper.php
path: src/Internal/Containers/Drivers/MySQL/MySQLStartup.php
# docker inspect json_decode produces mixed offsets read in ContainerInspection.
- identifier: offsetAccess.nonOffsetAccessible
path: src/Internal/Containers/ContainerInspection.php
# docker inspect json_decode yields a mixed value cast to int at the boundary.
# docker inspect json_decode yields a mixed port binding cast to int at the boundary.
- identifier: cast.int
path: src/Internal/Containers/ContainerInspection.php
path: src/Internal/Containers/Address/Ports.php
# docker inspect json_decode yields a mixed value iterated at the boundary.
- identifier: foreach.nonIterable
path: src/Internal/Containers/ContainerInspection.php
Expand All @@ -79,15 +78,28 @@ parameters:
- tests/Unit/FlywayDockerContainerTest.php
- tests/Unit/GenericDockerContainerTest.php
- tests/Unit/MySQLDockerContainerTest.php
# ClientMock's match arms assemble output tuples from untyped recorded responses.
- identifier: return.type
# ClientMock destructures the untyped response tuple it answers a command with.
- identifier: offsetAccess.nonArray
path: tests/Unit/ClientMock.php
# ClientMock queues untyped response tuples under the command each one answers.
- identifier: offsetAccess.nonOffsetAccessible
path: tests/Unit/ClientMock.php
# ClientMock casts a mixed recorded value to string when building command output.
- identifier: cast.string
path: tests/Unit/ClientMock.php
# ClientMock indexes into a mixed recorded payload when replaying responses.
- identifier: offsetAccess.nonArray
path: tests/Unit/ClientMock.php
# Tests index into the untyped argument lists the ClientMock records.
- identifier: offsetAccess.nonOffsetAccessible
paths:
- tests/Unit/FlywayDockerContainerTest.php
- tests/Unit/GenericDockerContainerTest.php
# The shutdown spy records callbacks in an untyped global the test then invokes.
- identifier: callable.nonCallable
path: tests/Unit/GenericDockerContainerTest.php
# Immutability tests discard the copy a with* method returns, to show the receiver is untouched.
- identifier: method.resultUnused
paths:
- tests/Unit/FlywayDockerContainerTest.php
- tests/Unit/MySQLDockerContainerTest.php
# MySQLRepository reads a mixed PDO result as an object at the system boundary.
- identifier: method.nonObject
path: tests/Integration/MySQLRepository.php
Expand Down
43 changes: 11 additions & 32 deletions src/ContainerStarted.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,27 @@

namespace TinyBlocks\DockerContainer;

use TinyBlocks\DockerContainer\Internal\Exceptions\DockerCommandExecutionFailed;
use TinyBlocks\DockerContainer\Internal\Exceptions\StopTimeoutOutOfRange;
use TinyBlocks\DockerContainer\Exceptions\DockerCommandExecutionFailed;
use TinyBlocks\Time\Duration;

/**
* Represents a Docker container that has been started and is running.
*
* <p>A container this process started on its own is a {@see DisposableContainer}, which it may remove. A container
* shared among processes is a {@see SharedContainer}, which tells whether it was reused.</p>
*/
interface ContainerStarted
{
/**
* Default graceful period in whole seconds used when stopping the container.
*
* <p>After this period elapses, Docker forces the container to stop with SIGKILL.</p>
*/
public const int DEFAULT_TIMEOUT_IN_WHOLE_SECONDS = 10;

/**
* Stops the running container gracefully.
*
* @param int $timeoutInWholeSeconds The graceful period in seconds to wait before forcing the stop.
* <p>Docker forces the stop with SIGKILL once the grace period elapses.</p>
*
* @param Duration $gracePeriod The time to wait before forcing the stop.
* @return ExecutionCompleted The result of the stop command execution.
* @throws StopTimeoutOutOfRange If the timeout in whole seconds is negative.
* @throws DockerCommandExecutionFailed If the stop command fails.
* @throws DockerCommandExecutionFailed If the stop command cannot run.
*/
public function stop(int $timeoutInWholeSeconds = self::DEFAULT_TIMEOUT_IN_WHOLE_SECONDS): ExecutionCompleted;
public function stop(Duration $gracePeriod): ExecutionCompleted;

/**
* Returns the unique identifier of the container.
Expand All @@ -36,44 +33,26 @@ public function stop(int $timeoutInWholeSeconds = self::DEFAULT_TIMEOUT_IN_WHOLE
*/
public function getId(): string;

/**
* Forcefully removes the container and its anonymous volumes, then prunes
* unused networks created by the library.
*/
public function remove(): void;

/**
* Returns the name assigned to the container.
*
* @return string The container name.
*/
public function getName(): string;

/**
* Tells whether the container was reused from an already-running instance.
*
* @return bool True when an existing container was reused, false when a new one was started.
*/
public function wasReused(): bool;

/**
* Returns the network address of the container.
*
* @return Address The container's network address.
*/
public function getAddress(): Address;

/**
* Registers the container to be removed when the PHP process exits.
*/
public function stopOnShutdown(): void;

/**
* Executes commands inside the running container.
*
* @param array<int, string> $commands The commands to execute inside the container.
* @return ExecutionCompleted The result of the command execution.
* @throws DockerCommandExecutionFailed If the command execution fails.
* @throws DockerCommandExecutionFailed If the command execution cannot run.
*/
public function executeAfterStarted(array $commands): ExecutionCompleted;

Expand Down
26 changes: 26 additions & 0 deletions src/DisposableContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\DockerContainer;

use TinyBlocks\DockerContainer\Exceptions\DockerCommandExecutionFailed;

/**
* A started container this process owns alone, so it may remove the container once done with it.
*/
interface DisposableContainer extends ContainerStarted
{
/**
* Forcefully removes the container and its anonymous volumes, then prunes the unused networks created by the
* library.
*
* @throws DockerCommandExecutionFailed If a removal command cannot run.
*/
public function remove(): void;

/**
* Registers the container to be removed when the PHP process exits.
*/
public function stopOnShutdown(): void;
}
35 changes: 35 additions & 0 deletions src/DockerClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\DockerContainer;

use TinyBlocks\DockerContainer\Exceptions\DockerCommandExecutionFailed;

/**
* Executes the Docker commands the library issues, the single boundary between the library and the Docker engine.
*
* <p>Every container factory defaults to a client that invokes the local <code>docker</code> executable. A custom
* client replaces it, for example to route the commands to another Docker-compatible engine, or to stand in for
* the engine in the unit tests of code that builds containers.</p>
*/
interface DockerClient
{
/**
* Executes a Docker command within the default timeout of the client and returns its outcome.
*
* @param DockerCommand $command The Docker command to execute.
* @return ExecutionCompleted The outcome of the command.
* @throws DockerCommandExecutionFailed If the command cannot run or exceeds the default timeout.
*/
public function execute(DockerCommand $command): ExecutionCompleted;

/**
* Executes a Docker command within the timeout the command carries and returns its outcome.
*
* @param DockerCommandWithTimeout $command The Docker command to execute.
* @return ExecutionCompleted The outcome of the command.
* @throws DockerCommandExecutionFailed If the command cannot run or exceeds its timeout.
*/
public function executeWithTimeout(DockerCommandWithTimeout $command): ExecutionCompleted;
}
24 changes: 24 additions & 0 deletions src/DockerCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\DockerContainer;

/**
* A Docker command line the library issues through a {@see DockerClient}.
*
* <p>A command bounded by a timeout of its own is a {@see DockerCommandWithTimeout}. Any other command runs within
* the default timeout of the client.</p>
*/
interface DockerCommand
{
/**
* Returns the DockerCommand as an ordered argument list.
*
* <p>The first element is the executable and the remaining elements are its arguments. No shell interpretation
* happens between elements, so values are passed through verbatim.</p>
*
* @return array<int, string> The ordered argument list.
*/
public function toArguments(): array;
}
20 changes: 20 additions & 0 deletions src/DockerCommandWithTimeout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\DockerContainer;

use TinyBlocks\Time\Duration;

/**
* A Docker command that must finish within a timeout of its own, such as a container run until it exits.
*/
interface DockerCommandWithTimeout extends DockerCommand
{
/**
* Returns the longest time the command may run before it is interrupted.
*
* @return Duration The timeout.
*/
public function timeout(): Duration;
}
Loading