diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07f01bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Ignore Eclipse/Zend Studio project files +/.buildpath +/.project +/.settings + +# Ignore PhpStorm files +/.idea/ + +# Ignore files under Mac +.DS_Store + +# Ignore 3rd party tools +/composer.lock +/composer.phar +/vendor/* + +# Ignore PHPUnit files +/.phpunit.result.cache + +# Ignore temporary files +/temp/ diff --git a/Dockerfile.twig b/Dockerfile.twig new file mode 100644 index 0000000..7593617 --- /dev/null +++ b/Dockerfile.twig @@ -0,0 +1,55 @@ +FROM {{ image_name }}:{{ php_version }}-cli + +ENV DEBIAN_FRONTEND noninteractive +ENV TERM xterm-color + +# @see https://www.nginx.com/resources/wiki/start/topics/tutorials/install/ +RUN \ + apt-get update && \ + apt-get install -y \ + apt-transport-https \ + dirmngr \ + gnupg \ + software-properties-common \ + supervisor \ + unzip \ + --no-install-recommends && \ + apt-key adv --no-tty --keyserver keyserver.ubuntu.com --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && \ + echo "deb http://nginx.org/packages/ubuntu/ bionic nginx" > /etc/apt/sources.list.d/nginx.list && \ + apt-get update && \ + apt-get install -y \ + nginx={{ nginx }} \ + --no-install-recommends && \ + {% if extensions is not empty %} + pecl update-channels && \ + {% for name, data in extensions %} + pecl install {{ name }}{% if data.version is not empty %}-{{ data.version }}{% endif %} && \ + {% endfor %} + {% for name, data in extensions %} + {% if data.enabled %} + docker-php-ext-enable {{ name }} && \ + {% endif %} + {% endfor %} + {% endif %} + curl \ + -sfL \ + --connect-timeout 5 \ + --max-time 15 \ + --retry 5 \ + --retry-delay 2 \ + --retry-max-time 60 \ + http://getcomposer.org/installer | php -- --install-dir="/usr/bin" --filename=composer && \ + chmod +x "/usr/bin/composer" && \ + composer self-update {{ composer.version }} && \ + mkdir -p /var/log/supervisor && \ + rm -r /var/lib/apt/lists/* && \ + mkdir -p /etc/nginx && \ + ln -sf /dev/stdout /var/log/nginx/access.log && \ + ln -sf /dev/stderr /var/log/nginx/error.log + +COPY ./rootfilesystem/ / + +ENTRYPOINT ["/entrypoint.sh"] +CMD [] + +WORKDIR "/var/www/" diff --git a/bin/generate-docker-files.php b/bin/generate-docker-files.php new file mode 100755 index 0000000..173f3b7 --- /dev/null +++ b/bin/generate-docker-files.php @@ -0,0 +1,15 @@ +#!/usr/bin/env php +render(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f9cfd9d --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "swoole/docker", + "description": "Official Docker image packing for Swoole.", + "homepage": "https://www.swoole.com", + "license": "Apache-2.0", + "require": { + "php": ">=7.1", + "crowdstar/reflection": "~1.0.0", + "overtrue/phplint": "~1.1.0", + "phpunit/phpunit": ">=8.0", + "squizlabs/php_codesniffer": ">=3.0", + "symfony/yaml": "~4.3.0", + "twig/twig": "~2.11.0" + }, + "autoload": { + "psr-4": { + "Swoole\\Docker\\": "src/" + } + } +} diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..882036e --- /dev/null +++ b/config.yml @@ -0,0 +1,30 @@ +# A sample configuration file for generating Dockerfiles. You may find configuration files under folder images/config/. + +# Base name of the configuration file must be of a specific version of image swoole/swoole, e.g., +# * 4.3.5 +# * 4.4.0 + +# Options for field "status": "under development", "released", "end-of-life". +# * "under development": +# Still under development. Please DO NOT use it in production. +# * "released": +# Released and can be used in production. The tag is frozen and won't be updated any more. +# * "end-of-life": +# There are new releases out and this tag should no longer being used in production. +status: "under development" +php: # List of PHP versions to build for. + - 7.1.29 + - 7.2.19 + - 7.3.6 +image: + # To find out available versions of Nginx, please run commands like + # docker run --rm php:7.3.6-cli bash -c "apt-get update && apt-cache policy nginx" + # or, + # docker run --rm php:7.3.6-cli bash -c "apt-get update && apt-cache showpkg nginx" + nginx: "1.10.3-1+deb9u2" + composer: + version: 1.8.6 # Composer version. + extensions: # List of PECL extensions to be installed. + zip: # An extension name. + version: 1.15.4 # Optional. + enabled: true # Enable the extension or not. By default it's disabled. diff --git a/config/4.3.5.yml b/config/4.3.5.yml new file mode 100644 index 0000000..87a8788 --- /dev/null +++ b/config/4.3.5.yml @@ -0,0 +1,12 @@ +# The YAML configuration file for generating Dockerfile of image swoole/swoole:4.3.5. +# For technical details of the configuration file, please check comments and sample configurations in file /config.yml. +# +status: "under development" +php: + - 7.1.29 + - 7.2.19 + - 7.3.6 +image: + nginx: "1.10.3-1+deb9u2" + composer: + version: 1.8.6 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f07b31f --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,6 @@ + + + + tests + + diff --git a/rootfilesystem/entrypoint.sh b/rootfilesystem/entrypoint.sh new file mode 100755 index 0000000..14c6737 --- /dev/null +++ b/rootfilesystem/entrypoint.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -e + +if [[ ! -z "$@" ]] ; then + BOOT_MODE=TASK +else + BOOT_MODE=SERVICE +fi + +for f in /usr/local/boot/*.sh ; do + BOOT_MODE=${BOOT_MODE} . "$f" +done + +# We use option "-c" here to suppress following warning message from console output: +# UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations... +if [[ -n "$(ls /etc/supervisor/conf.d/*.conf 2>/dev/null)" ]] ; then + if [[ "SERVICE" == "${BOOT_MODE}" ]] ; then + /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -n + else + /usr/bin/supervisord -c /etc/supervisor/supervisord.conf + fi +fi + +if [[ ! -z "$@" ]] ; then + if [[ "${1}" =~ ^(ba|)sh$ ]] ; then + # To support Docker commands like following: + # docker run --rm swoole/swoole:4.3.5 bash -c "composer --version" + # docker run --rm swoole/swoole:4.3.5 sh -c "composer --version" + exec "$@" + else + # To support Docker commands invoked in ECS (via command "aws ecs run-task"), kind of like following: + # docker run --rm swoole/swoole:4.3.5 "composer --version" + exec $@ + fi +fi diff --git a/rootfilesystem/etc/nginx/nginx.conf b/rootfilesystem/etc/nginx/nginx.conf new file mode 100644 index 0000000..7c72f95 --- /dev/null +++ b/rootfilesystem/etc/nginx/nginx.conf @@ -0,0 +1,40 @@ +user root root; +worker_processes 4; +pid /run/nginx.pid; +daemon off; + +events { + worker_connections 768; +} + +http { + sendfile off; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log off; + error_log /proc/self/fd/2; + + gzip on; + gzip_proxied any; + gzip_types application/json application/xml text/plain text/xml application/octet-stream; + # Compression level (1-9). + # 5 is a perfect compromise between size and cpu usage, offering about + # 75% reduction for most ascii files (almost identical to level 9). + gzip_comp_level 5; + # Don't compress anything that's already small and unlikely to shrink much + # if at all (the default is 20 bytes, which is bad as that usually leads to + # larger files after gzipping). + gzip_min_length 1280; + + # include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} diff --git a/rootfilesystem/etc/nginx/sites-enabled/default b/rootfilesystem/etc/nginx/sites-enabled/default new file mode 100644 index 0000000..78dc17d --- /dev/null +++ b/rootfilesystem/etc/nginx/sites-enabled/default @@ -0,0 +1,13 @@ +server { + listen 80 default_server; + root /var/www; + + location / { + proxy_http_version 1.1; + proxy_set_header Connection "keep-alive"; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:9501; + } +} diff --git a/rootfilesystem/etc/supervisor/available.d/README.md b/rootfilesystem/etc/supervisor/available.d/README.md new file mode 100644 index 0000000..4a49d4f --- /dev/null +++ b/rootfilesystem/etc/supervisor/available.d/README.md @@ -0,0 +1,8 @@ +Under this folder are _Supervisor_ configuration files that are available but not enabled by default. + +To enable a configuration file under this file, you may run commands like following: + +```bash +# Assuming there is a Supervisord configuration file "dnsmasq.conf" available. +enable-supervisord-program.sh dnsmasq +``` diff --git a/rootfilesystem/etc/supervisor/conf.d/README.md b/rootfilesystem/etc/supervisor/conf.d/README.md new file mode 100644 index 0000000..c8a0cf2 --- /dev/null +++ b/rootfilesystem/etc/supervisor/conf.d/README.md @@ -0,0 +1,7 @@ +Under this folder are _.conf_ configuration files included by the _Supervisor_ configuration file "/etc/supervisor/supervisord.conf". + +Besides the _.conf_ files under this folder, _.conf_ files from folder _../service.d_ or _../task.d_ will be copied to +here automatically when starting a Docker container. + +If there are two _.conf_ files of the same name there both under this folder and under folder _../service.d_ or _../task.d_, + the latter will be used. You may check file _/usr/local/boot/supervisor.sh_ in the image and see how that happens. diff --git a/rootfilesystem/etc/supervisor/service.d/README.md b/rootfilesystem/etc/supervisor/service.d/README.md new file mode 100644 index 0000000..36883a0 --- /dev/null +++ b/rootfilesystem/etc/supervisor/service.d/README.md @@ -0,0 +1,11 @@ +Under this folder are files included by the _Supervisor_ configuration file "/etc/supervisor/supervisord.conf" when +running under service mode, where the Docker container is to start some long-running webservices like Nginx. e.g., + +```bash +docker run --rm --name=app -p 80:80 swoole/swoole:4.3.5 +``` + +In this case, the list of programs defined under this folder (along with those already under folder _../conf.d/_) will +be started by _Supervisord_. + +For the list of programs defined under this folder, file extension must be "_.conf_". diff --git a/rootfilesystem/etc/supervisor/service.d/nginx.conf b/rootfilesystem/etc/supervisor/service.d/nginx.conf new file mode 100755 index 0000000..1dd881b --- /dev/null +++ b/rootfilesystem/etc/supervisor/service.d/nginx.conf @@ -0,0 +1,12 @@ +[supervisord] +user = root + +[program:nginx] +command = /usr/sbin/nginx +user = root +autostart = true +autorestart = true +stdout_logfile=/proc/self/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/self/fd/1 +stderr_logfile_maxbytes=0 diff --git a/rootfilesystem/etc/supervisor/service.d/swoole.conf b/rootfilesystem/etc/supervisor/service.d/swoole.conf new file mode 100644 index 0000000..3cd8909 --- /dev/null +++ b/rootfilesystem/etc/supervisor/service.d/swoole.conf @@ -0,0 +1,11 @@ +[supervisord] +user = root + +[program:swoole] +command = /var/www/server.php +user = root +autostart = true +stdout_logfile=/proc/self/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/self/fd/1 +stderr_logfile_maxbytes=0 diff --git a/rootfilesystem/etc/supervisor/task.d/README.md b/rootfilesystem/etc/supervisor/task.d/README.md new file mode 100644 index 0000000..4452925 --- /dev/null +++ b/rootfilesystem/etc/supervisor/task.d/README.md @@ -0,0 +1,11 @@ +Under this folder are files included by the _Supervisor_ configuration file "/etc/supervisor/supervisord.conf" when +running under task mode, where a specified command is executed in a Docker container launched. e.g., + +```bash +docker run --rm -t swoole/swoole:4.3.5 bash -c "php -v" +``` + +In this case, the list of programs defined under this folder (along with those already under folder _../conf.d/_) will +be started by _Supervisord_ first before executing command `php -v`. + +For the list of programs defined under this folder, file extension must be "_.conf_". diff --git a/rootfilesystem/usr/local/bin/disable-supervisord-program.sh b/rootfilesystem/usr/local/bin/disable-supervisord-program.sh new file mode 100755 index 0000000..f90d112 --- /dev/null +++ b/rootfilesystem/usr/local/bin/disable-supervisord-program.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# +# To disable a program in Supervisord. +# Once a program is disabled, you can't enable it with script enable-supervisord-program.sh directly. +# + +set -e + +for conf_dir in "available.d" "conf.d" "service.d" "task.d" ; do + conf_file="/etc/supervisor/${conf_dir}/${1}.conf" + if [[ -f "${conf_file}" ]] ; then + mv "${conf_file}" "${conf_file}.disabled" + fi +done diff --git a/rootfilesystem/usr/local/bin/enable-supervisord-program.sh b/rootfilesystem/usr/local/bin/enable-supervisord-program.sh new file mode 100755 index 0000000..9be159d --- /dev/null +++ b/rootfilesystem/usr/local/bin/enable-supervisord-program.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# +# To enable an available program in Supervisord. List of available program can be found under folder +# images/php-fpm/rootfilesystem/etc/supervisor/available.d/. +# + +set -e + +conf_file="/etc/supervisor/available.d/${1}.conf" + +if [[ -f "${conf_file}" ]] ; then + cp "${conf_file}" /etc/supervisor/conf.d/. +fi diff --git a/rootfilesystem/usr/local/bin/update_token.sh b/rootfilesystem/usr/local/bin/update_token.sh new file mode 100755 index 0000000..f43e37c --- /dev/null +++ b/rootfilesystem/usr/local/bin/update_token.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Update given token with an environment variable under given folders. +# +# Limitations: +# 1. The environment variable can not contain character "#" in it. +# +# Usage: +# ./bin/update_token.sh ENVIRONMENT_VARIABLE_NAME [FOLDER]... +# e.g., +# ./bin/update_token.sh PHP_VERSION /usr/local/etc/nginx /usr/local/etc/php +# + +set -e + +if [[ -z ${!1} ]] ; then + echo "Error: environment variable '{$1}' is empty." + exit 1 +fi + +for path in "${@:2}" ; do + if [[ ! -d "${path}" ]] ; then + echo "Error: Path '${path}' does not point to a folder." + exit 1 + fi +done +for path in "${@:2}" ; do + find "${path}" -type f -exec sed -i "s#%%${1}%%#${!1}#g" {} + +done diff --git a/rootfilesystem/usr/local/boot/sample.sh b/rootfilesystem/usr/local/boot/sample.sh new file mode 100755 index 0000000..97ef505 --- /dev/null +++ b/rootfilesystem/usr/local/boot/sample.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# +# Sample script showing how to write a script that will be executed automatically when a +# Docker container is started. +# + +set -e + +return # stop executing following sample scripts. + +if [[ "${BOOT_MODE}" == "SERVICE" ]] ; then + echo "Docker container is running in SERVICE mode." +fi + +if [[ "${BOOT_MODE}" == "TASK" ]] ; then + echo "Docker container is running in TASK mode." +fi + +echo "This line is printed out both in SERVICE mode and in TASK mode." diff --git a/rootfilesystem/usr/local/boot/set-github-token.sh b/rootfilesystem/usr/local/boot/set-github-token.sh new file mode 100755 index 0000000..7af1be7 --- /dev/null +++ b/rootfilesystem/usr/local/boot/set-github-token.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# +# Set a GitHub personal access token to the composer configuration at the global level. +# + +set -e + +if [[ ! -z "${GITHUB_PAT}" ]] ; then + composer config --global github-oauth.github.com "${GITHUB_PAT}" +fi diff --git a/rootfilesystem/usr/local/boot/supervisor.sh b/rootfilesystem/usr/local/boot/supervisor.sh new file mode 100755 index 0000000..d4b3f4d --- /dev/null +++ b/rootfilesystem/usr/local/boot/supervisor.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# +# Supervisor is a client/server system that allows its users to monitor and control a number of processes on UNIX-like +# operating systems. For more details, please check http://supervisord.org. +# + +set -e + +case "${BOOT_MODE}" in + "SERVICE") + echo "INFO: Supervisord configuration files copied from folder '/etc/supervisor/${BOOT_MODE,,}.d/'." + find /etc/supervisor/${BOOT_MODE,,}.d/ -mindepth 1 -type f -name *.conf -exec cp -t /etc/supervisor/conf.d/ {} + + ;; + "TASK") + find /etc/supervisor/${BOOT_MODE,,}.d/ -mindepth 1 -type f -name *.conf -exec cp -t /etc/supervisor/conf.d/ {} + + ;; + *) + echo "Error: BOOT_MODE in the Docker container must be either SERVICE or TASK." + exit 1 +esac diff --git a/rootfilesystem/usr/local/etc/php/conf.d/swoole.ini-suggested b/rootfilesystem/usr/local/etc/php/conf.d/swoole.ini-suggested new file mode 100644 index 0000000..8f82625 --- /dev/null +++ b/rootfilesystem/usr/local/etc/php/conf.d/swoole.ini-suggested @@ -0,0 +1 @@ +extension=swoole.so diff --git a/rootfilesystem/var/www/server.php b/rootfilesystem/var/www/server.php new file mode 100755 index 0000000..baaa840 --- /dev/null +++ b/rootfilesystem/var/www/server.php @@ -0,0 +1,17 @@ +#!/usr/bin/env php +bind("0.0.0.0", 9501); +$socket->listen(128); +go(function () use ($socket) { + while (true) { + $client = $socket->accept(); + go(function() use ($client) { + $client->send("Hello, World!\n"); + $client->close(); + }); + } +}); diff --git a/src/Dockerfile.php b/src/Dockerfile.php new file mode 100644 index 0000000..f69a774 --- /dev/null +++ b/src/Dockerfile.php @@ -0,0 +1,246 @@ + base image, + self::ARCH_AMD64 => 'php', + self::ARCH_ARM64V8 => 'arm64v8/php', + ]; + + /** + * @var string + */ + protected $basePath; + + /** + * @var string + */ + protected $swooleVersion; + + /** + * @var array + */ + protected $config; + + /** + * Dockerfile constructor. + * + * @param string $swooleVersion + * @throws Exception + */ + public function __construct(string $swooleVersion) + { + $this + ->setBasePath(dirname(__DIR__)) + ->setSwooleVersion($swooleVersion) + ->setConfig(Yaml::parseFile("{$this->getConfigFilePath()}")); + } + + /** + * @throws Exception + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + */ + public function render(): void + { + foreach ($this->getConfig()['php'] as $phpVersion) { + foreach (array_keys(self::BASE_IMAGES) as $architecture) { + $this->renderByArchitecture($phpVersion, $architecture, true); + } + } + } + + /** + * @param string $phpVersion + * @param string $architecture + * @param bool $save + * @return string + * @throws Exception + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + */ + public function renderByArchitecture(string $phpVersion, string $architecture, bool $save = false): string + { + $dockerFile = (new Environment(new FilesystemLoader($this->getBasePath()))) + ->load('Dockerfile.twig') + ->render($this->getContext($phpVersion, $architecture)); + + if ($save) { + $dockerFileDir = $this->getDockerFileDir($architecture); + if (!file_exists($dockerFileDir)) { + mkdir($dockerFileDir, 0777, true); + } + file_put_contents( + sprintf( + '%s/%s-php%s.Dockerfile', + $dockerFileDir, + $this->getSwooleVersion(), + $this->getPhpMajorVersion($phpVersion) + ), + $dockerFile + ); + } + + return $dockerFile; + } + + /** + * @return string + */ + public function getBasePath(): string + { + return $this->basePath; + } + + /** + * @param string $basePath + * @return Dockerfile + * @throws Exception + */ + public function setBasePath(string $basePath): self + { + if (!is_dir($basePath) || !is_readable($basePath)) { + throw new Exception("base path '{$basePath}' does not point to a directory or not readable"); + } + + $this->basePath = $basePath; + + return $this; + } + + /** + * @return string + */ + public function getSwooleVersion(): string + { + return $this->swooleVersion; + } + + /** + * @param string $swooleVersion + * @return Dockerfile + * @throws Exception + */ + public function setSwooleVersion(string $swooleVersion): self + { + if (!$this->isValidSwooleVersion($swooleVersion)) { + throw new Exception( + "Swoole version must be in the format of 'X.Y.Z'." + ); + } + + $this->swooleVersion = $swooleVersion; + + if (!is_file($this->getConfigFilePath()) || !is_readable($this->getConfigFilePath())) { + throw new Exception("Config file unreadable for Swoole version '{$swooleVersion}'."); + } + + return $this; + } + + /** + * @return array + */ + public function getConfig(): array + { + return $this->config; + } + + /** + * @param array $config + * @return Dockerfile + */ + public function setConfig(array $config): self + { + $this->config = $config; + + return $this; + } + + /** + * @param string $phpVersion + * @return string + */ + protected function getPhpMajorVersion(string $phpVersion): string + { + return preg_replace('/^(\d+\.\d+).*$/', '$1', $phpVersion); + } + + /** + * @param string $architecture + * @return string + */ + protected function getDockerFileDir(string $architecture): string + { + return "{$this->getBasePath()}/temp/dockerfiles/{$architecture}"; + } + + /** + * @return string + */ + protected function getConfigFilePath(): string + { + return $this->getConfigFilePathBySwooleVersion($this->getSwooleVersion()); + } + + /** + * @param string $swooleVersion + * @return string + */ + protected function getConfigFilePathBySwooleVersion(string $swooleVersion): string + { + return "{$this->getBasePath()}/config/{$swooleVersion}.yml"; + } + + /** + * @param string $swooleVersion + * @return bool + */ + protected function isValidSwooleVersion(string $swooleVersion): bool + { + return preg_match('/^[1-9]\d*\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/', $swooleVersion); + } + + /** + * @param string $phpVersion + * @param string $architecture + * @return array + * @throws Exception + */ + protected function getContext(string $phpVersion, string $architecture = self::ARCH_DEFAULT): array + { + if (!array_key_exists($architecture, self::BASE_IMAGES)) { + throw new Exception("Architecture '{$architecture}' not supported."); + } + + return array_merge( + $this->getConfig()['image'], + [ + 'image_name' => self::BASE_IMAGES[$architecture], + 'php_version' => $phpVersion, + ] + ); + } +} diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..f6639dd --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,12 @@ +getMockBuilder(Dockerfile::class)->disableOriginalConstructor()->getMock(), + 'getPhpMajorVersion', + [ + $phpVersion, + ] + ), + $message + ); + } + + /** + * @return array + */ + public function dataIsValidSwooleVersion(): array + { + return [ + [ + true, + '4.3.5', + 'a typical semantic version #', + ], + [ + true, + '701.301.201', + 'a typical semantic version # where each part is over 100', + ], + + [ + false, + '', + 'an empty string', + ], + [ + false, + ' ', + 'one space', + ], + [ + false, + 'a', + 'character "a"', + ], + [ + false, + '4.3', + 'no patch part included in the version #', + ], + [ + false, + ' 4.3.5', + 'leading space found', + ], + [ + false, + '4.3.5 ', + 'trailing space found', + ], + [ + false, + ' 4.3.5 ', + 'spaces around', + ], + [ + false, + ' 4.3.5a', + 'letter(s) found', + ], + [ + false, + '4.3.5-', + 'no image revision included', + ], + [ + false, + '4.3.5-@', + 'invalid character(s) in the revision part', + ], + [ + false, + '04.3.5', + 'leading zero(s) found in major version', + ], + [ + false, + '4.03.5', + 'leading zero(s) found in minor version', + ], + [ + false, + '4.3.05', + 'leading zero(s) found in the patch part', + ], + ]; + } + + /** + * @dataProvider dataIsValidSwooleVersion + * @covers Dockerfile::isValidSwooleVersion + * @param bool $expected + * @param string $imageTag + * @param string $message + * @throws ReflectionException + */ + public function testIsValidSwooleVersion(bool $expected, string $imageTag, string $message): void + { + self::assertSame( + $expected, + Reflection::callMethod( + $this->getMockBuilder(Dockerfile::class)->disableOriginalConstructor()->getMock(), + 'isValidSwooleVersion', + [ + $imageTag, + ] + ), + $message + ); + } +}