diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01230d0..6e84a57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,19 @@ jobs: docker run --rm -v $PWD:/app composer:2.8 sh -c \ "composer install --profile --ignore-platform-reqs && composer analyze" + refactor: + name: Refactor + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v6 + + - name: Run Refactor + run: | + docker run --rm -v $PWD:/app composer:2.8 sh -c \ + "composer install --profile --ignore-platform-reqs && composer refactor:check" + unit-tests: name: Unit Tests runs-on: ubuntu-latest diff --git a/app/controllers.php b/app/controllers.php index 104bafd..e1a5c85 100644 --- a/app/controllers.php +++ b/app/controllers.php @@ -25,7 +25,7 @@ ->param('timeout', '600', new Text(16), 'Maximum logs timeout.', true) ->inject('response') ->inject('runner') - ->action(function (string $runtimeId, string $timeoutStr, Response $response, Runner $runner) { + ->action(function (string $runtimeId, string $timeoutStr, Response $response, Runner $runner): void { $timeout = \intval($timeoutStr); $response->sendHeader('Content-Type', 'text/event-stream'); @@ -43,7 +43,7 @@ ->param('timeout', 600, new Integer(), 'Commands execution time in seconds.', true) ->inject('response') ->inject('runner') - ->action(function (string $runtimeId, string $command, int $timeout, Response $response, Runner $runner) { + ->action(function (string $runtimeId, string $command, int $timeout, Response $response, Runner $runner): void { $output = $runner->executeCommand($runtimeId, $command, $timeout); $response->setStatusCode(Response::STATUS_CODE_OK)->json([ 'output' => $output ]); }); @@ -68,7 +68,7 @@ ->param('restartPolicy', DockerAPI::RESTART_NO, new WhiteList([DockerAPI::RESTART_NO, DockerAPI::RESTART_ALWAYS, DockerAPI::RESTART_ON_FAILURE, DockerAPI::RESTART_UNLESS_STOPPED], true), 'Define restart policy for the runtime once an exit code is returned. Default value is "no". Possible values are "no", "always", "on-failure", "unless-stopped".', true) ->inject('response') ->inject('runner') - ->action(function (string $runtimeId, string $image, string $entrypoint, string $source, string $destination, string $outputDirectory, array $variables, string $runtimeEntrypoint, string $command, int $timeout, bool $remove, float $cpus, int $memory, string $version, string $restartPolicy, Response $response, Runner $runner) { + ->action(function (string $runtimeId, string $image, string $entrypoint, string $source, string $destination, string $outputDirectory, array $variables, string $runtimeEntrypoint, string $command, int $timeout, bool $remove, float $cpus, int $memory, string $version, string $restartPolicy, Response $response, Runner $runner): void { $secret = \bin2hex(\random_bytes(16)); /** @@ -90,7 +90,7 @@ default => [], }); - if (!empty($outputDirectory)) { + if ($outputDirectory !== '' && $outputDirectory !== '0') { $variables = \array_merge($variables, [ 'OPEN_RUNTIMES_OUTPUT_DIRECTORY' => $outputDirectory ]); @@ -100,7 +100,7 @@ 'CI' => 'true' ]); - $variables = array_map(fn ($v) => strval($v), $variables); + $variables = array_map(strval(...), $variables); $container = $runner->createRuntime($runtimeId, $secret, $image, $entrypoint, $source, $destination, $variables, $runtimeEntrypoint, $command, $timeout, $remove, $cpus, $memory, $version, $restartPolicy); $response->setStatusCode(Response::STATUS_CODE_CREATED)->json($container); @@ -111,7 +111,7 @@ ->desc("List currently active runtimes") ->inject('runner') ->inject('response') - ->action(function (Runner $runner, Response $response) { + ->action(function (Runner $runner, Response $response): void { $response->setStatusCode(Response::STATUS_CODE_OK)->json($runner->getRuntimes()); }); @@ -121,7 +121,7 @@ ->param('runtimeId', '', new Text(64), 'Runtime unique ID.') ->inject('runner') ->inject('response') - ->action(function (string $runtimeId, Runner $runner, Response $response) { + ->action(function (string $runtimeId, Runner $runner, Response $response): void { $runtimeName = System::getHostname() . '-' . $runtimeId; $response->setStatusCode(Response::STATUS_CODE_OK)->json($runner->getRuntime($runtimeName)); }); @@ -132,7 +132,7 @@ ->param('runtimeId', '', new Text(64), 'Runtime unique ID.') ->inject('response') ->inject('runner') - ->action(function (string $runtimeId, Response $response, Runner $runner) { + ->action(function (string $runtimeId, Response $response, Runner $runner): void { $runner->deleteRuntime($runtimeId); $response->setStatusCode(Response::STATUS_CODE_OK)->send(); }); @@ -183,39 +183,18 @@ function ( Response $response, Request $request, Runner $runner - ) { - // Extra parsers and validators to support both JSON and multipart - $intParams = ['timeout', 'memory']; - foreach ($intParams as $intParam) { - if (!empty($$intParam) && !is_numeric($$intParam)) { - $$intParam = \intval($$intParam); - } + ): void { + // Parse JSON strings for assoc params when coming from multipart + if (\is_string($headers)) { + $headers = \json_decode($headers, true) ?? []; } - $floatParams = ['cpus']; - foreach ($floatParams as $floatPram) { - if (!empty($$floatPram) && !is_numeric($$floatPram)) { - $$floatPram = \floatval($$floatPram); - } + if (\is_string($variables)) { + $variables = \json_decode($variables, true) ?? []; } - /** - * @var array $headers - * @var array $variables - */ - $assocParams = ['headers', 'variables']; - foreach ($assocParams as $assocParam) { - if (!empty($$assocParam) && !is_array($$assocParam)) { - $$assocParam = \json_decode($$assocParam, true); - } - } - - $booleanParams = ['logging']; - foreach ($booleanParams as $booleamParam) { - if (!empty($$booleamParam) && !is_bool($$booleamParam)) { - $$booleamParam = $$booleamParam === "true" ? true : false; - } - } + /** @var array $headers */ + /** @var array $variables */ // 'headers' validator $validator = new Assoc(); @@ -229,11 +208,11 @@ function ( throw new Exception(Exception::EXECUTION_BAD_REQUEST, $validator->getDescription()); } - if (empty($payload)) { + if (in_array($payload, [null, '', '0'], true)) { $payload = ''; } - $variables = array_map(fn ($v) => strval($v), $variables); + $variables = array_map(strval(...), $variables); $execution = $runner->createExecution( $runtimeId, @@ -303,16 +282,16 @@ function ( Http::get('/v1/health') ->desc("Get health status") ->inject('response') - ->action(function (Response $response) { + ->action(function (Response $response): void { $response->setStatusCode(Response::STATUS_CODE_OK)->text("OK"); }); Http::init() ->groups(['api']) ->inject('request') - ->action(function (Request $request) { + ->action(function (Request $request): void { $secretKey = \explode(' ', $request->getHeader('authorization', ''))[1] ?? ''; - if (empty($secretKey) || $secretKey !== System::getEnv('OPR_EXECUTOR_SECRET', '')) { + if ($secretKey === '' || $secretKey === '0' || $secretKey !== System::getEnv('OPR_EXECUTOR_SECRET', '')) { throw new Exception(Exception::GENERAL_UNAUTHORIZED, 'Missing executor key'); } }); diff --git a/app/error.php b/app/error.php index 1ab1105..de07fd5 100644 --- a/app/error.php +++ b/app/error.php @@ -8,7 +8,7 @@ Http::error() ->inject('error') ->inject('response') - ->action(function (Throwable $error, Response $response) { + ->action(function (Throwable $error, Response $response): void { // Show all Executor\Exceptions, or everything if in development $public = $error instanceof Exception || Http::isDevelopment(); $exception = $public ? $error : new Exception(Exception::GENERAL_UNKNOWN); diff --git a/app/http.php b/app/http.php index a4d8b9b..f1dc43d 100644 --- a/app/http.php +++ b/app/http.php @@ -32,7 +32,7 @@ ->inject('network') ->inject('imagePuller') ->inject('maintenance') - ->action(function (Orchestration $orchestration, Network $network, ImagePuller $imagePuller, Maintenance $maintenance) { + ->action(function (Orchestration $orchestration, Network $network, ImagePuller $imagePuller, Maintenance $maintenance): void { /* Fetch own container information */ $hostname = gethostname() ?: throw new \RuntimeException('Could not determine hostname'); $selfContainer = $orchestration->list(['name' => $hostname])[0] ?? throw new \RuntimeException('Own container not found'); @@ -58,11 +58,11 @@ Http::onRequest() ->inject('response') - ->action(function (Response $response) { + ->action(function (Response $response): void { $response->addHeader('Server', 'Executor'); }); -run(function () use ($settings) { +run(function () use ($settings): void { $server = new Server('0.0.0.0', '80', $settings); $http = new Http($server, 'UTC'); $http->start(); diff --git a/app/init.php b/app/init.php index 4058458..296aeae 100644 --- a/app/init.php +++ b/app/init.php @@ -14,13 +14,13 @@ use Utopia\Config\Config; const MAX_LOG_SIZE = 5 * 1024 * 1024; -const MAX_BUILD_LOG_SIZE = 1 * 1000 * 1000; +const MAX_BUILD_LOG_SIZE = 1000 * 1000; Config::load('errors', __DIR__ . '/config/errors.php'); $registry = new Registry(); -$registry->set('runtimes', fn () => new Runtimes()); +$registry->set('runtimes', fn (): \OpenRuntimes\Executor\Runner\Repository\Runtimes => new Runtimes()); Http::setResource('runtimes', fn () => $registry->get('runtimes')); diff --git a/composer.json b/composer.json index 0cc7411..43e10d8 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,11 @@ "scripts": { "format": "./vendor/bin/pint --config pint.json", "format:check": "./vendor/bin/pint --test --config pint.json", - "analyze": "./vendor/bin/phpstan analyse --level 8 --memory-limit=2G -c phpstan.neon app src tests", + "analyze": "./vendor/bin/phpstan analyse --memory-limit=1G -c phpstan.neon app src tests", "test:unit": "./vendor/bin/phpunit --configuration phpunit.xml --debug --testsuite=unit", - "test:e2e": "./vendor/bin/phpunit --configuration phpunit.xml --debug --testsuite=e2e" + "test:e2e": "./vendor/bin/phpunit --configuration phpunit.xml --debug --testsuite=e2e", + "refactor": "./vendor/bin/rector", + "refactor:check": "./vendor/bin/rector --dry-run" }, "require": { "php": ">=8.3.0", @@ -35,9 +37,10 @@ }, "require-dev": { "laravel/pint": "1.*", - "phpstan/phpstan": "1.*", + "phpstan/phpstan": "2.*", "phpunit/phpunit": "9.*", - "swoole/ide-helper": "5.1.2" + "swoole/ide-helper": "5.1.2", + "rector/rector": "2.3.1" }, "config": { "platform": { diff --git a/composer.lock b/composer.lock index 0f69fcd..82e570a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e210d16c714cdaf4f2628e234d2d52fe", + "content-hash": "b15d6c7bf4f50618ffb727a7631f49fb", "packages": [ { "name": "brick/math", @@ -145,16 +145,16 @@ }, { "name": "google/protobuf", - "version": "v4.33.2", + "version": "v4.33.4", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318" + "reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", - "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/22d28025cda0d223a2e48c2e16c5284ecc9f5402", + "reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402", "shasum": "" }, "require": { @@ -183,9 +183,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.4" }, - "time": "2025-12-05T22:12:22+00:00" + "time": "2026-01-12T17:58:43+00:00" }, { "name": "nyholm/psr7", @@ -462,16 +462,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d" + "reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d", - "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/62e680d587beb42e5247aa6ecd89ad1ca406e8ca", + "reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca", "shasum": "" }, "require": { @@ -522,7 +522,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-11-13T08:04:37+00:00" + "time": "2026-01-15T09:31:34+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -589,16 +589,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.10.0", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99" + "reference": "d91f21addcdb42da9a451c002777f8318432461a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", - "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/d91f21addcdb42da9a451c002777f8318432461a", + "reference": "d91f21addcdb42da9a451c002777f8318432461a", "shasum": "" }, "require": { @@ -682,7 +682,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-11-25T10:59:15+00:00" + "time": "2026-01-15T11:21:03+00:00" }, { "name": "open-telemetry/sem-conv", @@ -2259,16 +2259,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.19", + "version": "0.18.22", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "9c3f9a471250d22de7d405ee19e23e72b14181ca" + "reference": "c46bd78c1f52281df89f8921159782b20260ce31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/9c3f9a471250d22de7d405ee19e23e72b14181ca", - "reference": "9c3f9a471250d22de7d405ee19e23e72b14181ca", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/c46bd78c1f52281df89f8921159782b20260ce31", + "reference": "c46bd78c1f52281df89f8921159782b20260ce31", "shasum": "" }, "require": { @@ -2282,9 +2282,9 @@ "ext-zlib": "*", "ext-zstd": "*", "php": ">=8.1", - "utopia-php/framework": "0.*.*", "utopia-php/system": "0.*.*", - "utopia-php/telemetry": "0.2.*" + "utopia-php/telemetry": "0.2.*", + "utopia-php/validators": "0.1.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2311,9 +2311,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.19" + "source": "https://github.com/utopia-php/storage/tree/0.18.22" }, - "time": "2025-12-17T13:55:20+00:00" + "time": "2026-01-15T01:36:39+00:00" }, { "name": "utopia-php/system", @@ -2475,30 +2475,29 @@ "packages-dev": [ { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -2525,7 +2524,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -2541,20 +2540,20 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "laravel/pint", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", - "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", + "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", "shasum": "" }, "require": { @@ -2565,9 +2564,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.90.0", - "illuminate/view": "^12.40.1", - "larastan/larastan": "^3.8.0", + "friendsofphp/php-cs-fixer": "^3.92.4", + "illuminate/view": "^12.44.0", + "larastan/larastan": "^3.8.1", "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.3", @@ -2608,7 +2607,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-11-25T21:15:52+00:00" + "time": "2026-01-05T16:49:17+00:00" }, { "name": "myclabs/deep-copy", @@ -2848,15 +2847,15 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -2897,7 +2896,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3329,6 +3328,66 @@ ], "time": "2025-12-06T07:45:52+00:00" }, + { + "name": "rector/rector", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "9afc1bb43571b25629f353c61a9315b5ef31383a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/9afc1bb43571b25629f353c61a9315b5ef31383a", + "reference": "9afc1bb43571b25629f353c61a9315b5ef31383a", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.33" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-01-13T15:13:58+00:00" + }, { "name": "sebastian/cli-parser", "version": "1.0.2", diff --git a/phpstan.neon b/phpstan.neon index c41461e..e5fb08f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ parameters: + level: 8 scanDirectories: - vendor/swoole/ide-helper excludePaths: - - tests/resources \ No newline at end of file + - tests/resources diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..422a559 --- /dev/null +++ b/rector.php @@ -0,0 +1,23 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/app', + __DIR__ . '/tests' + ]) + ->withSkipPath(__DIR__ . '/tests/resources') + ->withPhpSets(php84: true) + ->withComposerBased(phpunit: true) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + phpunitCodeQuality: true, + earlyReturn: true + ); diff --git a/src/Executor/BodyMultipart.php b/src/Executor/BodyMultipart.php index 1951b29..c04401f 100644 --- a/src/Executor/BodyMultipart.php +++ b/src/Executor/BodyMultipart.php @@ -8,15 +8,12 @@ class BodyMultipart * @var array $parts */ private array $parts = []; + private string $boundary = ""; public function __construct(?string $boundary = null) { - if (is_null($boundary)) { - $this->boundary = self::generateBoundary(); - } else { - $this->boundary = $boundary; - } + $this->boundary = is_null($boundary) ? self::generateBoundary() : $boundary; } public static function generateBoundary(): string @@ -31,11 +28,15 @@ public function load(string $body): self $sections = \explode('--' . $this->boundary, $body); foreach ($sections as $section) { - if (empty($section)) { + if ($section === '') { continue; } - if (strpos($section, $eol) === 0) { + if ($section === '0') { + continue; + } + + if (str_starts_with($section, $eol)) { $section = substr($section, \strlen($eol)); } @@ -43,7 +44,7 @@ public function load(string $body): self $section = substr($section, 0, -1 * \strlen($eol)); } - if ($section == '--') { + if ($section === '--') { continue; } @@ -58,32 +59,31 @@ public function load(string $body): self $partName = ""; foreach ($partHeaders as $partHeader) { - if (!empty($partName)) { + if ($partName !== '' && $partName !== '0') { break; } $partHeaderArray = \explode(':', $partHeader, 2); - $partHeaderName = \strtolower($partHeaderArray[0] ?? ''); + $partHeaderName = \strtolower($partHeaderArray[0]); $partHeaderValue = $partHeaderArray[1] ?? ''; - if ($partHeaderName == "content-disposition") { + if ($partHeaderName === "content-disposition") { $dispositionChunks = \explode("; ", $partHeaderValue); foreach ($dispositionChunks as $dispositionChunk) { $dispositionChunkValues = \explode("=", $dispositionChunk, 2); - if (\count($dispositionChunkValues) >= 2) { - if ($dispositionChunkValues[0] === "name") { - $partName = \trim($dispositionChunkValues[1], "\""); - break; - } + if (\count($dispositionChunkValues) >= 2 && $dispositionChunkValues[0] === "name") { + $partName = \trim($dispositionChunkValues[1], '"'); + break; } } } } - if (!empty($partName)) { + if ($partName !== '' && $partName !== '0') { $this->parts[$partName] = $partBody; } } + return $this; } @@ -147,8 +147,6 @@ public function exportBody(): string $query .= '--' . $this->boundary; } - $query .= "--" . $eol; - - return $query; + return $query . ("--" . $eol); } } diff --git a/src/Executor/Exception.php b/src/Executor/Exception.php index e690354..cc7932d 100644 --- a/src/Executor/Exception.php +++ b/src/Executor/Exception.php @@ -13,28 +13,33 @@ class Exception extends \RuntimeException * _ */ public const string GENERAL_UNKNOWN = 'general_unknown'; + public const string GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found'; + public const string GENERAL_UNAUTHORIZED = 'general_unauthorized'; public const string EXECUTION_BAD_REQUEST = 'execution_bad_request'; + public const string EXECUTION_TIMEOUT = 'execution_timeout'; + public const string EXECUTION_BAD_JSON = 'execution_bad_json'; public const string RUNTIME_NOT_FOUND = 'runtime_not_found'; + public const string RUNTIME_CONFLICT = 'runtime_conflict'; + public const string RUNTIME_FAILED = 'runtime_failed'; + public const string RUNTIME_TIMEOUT = 'runtime_timeout'; public const string LOGS_TIMEOUT = 'logs_timeout'; public const string COMMAND_TIMEOUT = 'command_timeout'; + public const string COMMAND_FAILED = 'command_failed'; - /** - * Properties - */ - protected readonly string $type; protected readonly string $short; + protected readonly bool $publish; /** @@ -46,15 +51,13 @@ class Exception extends \RuntimeException * @param \Throwable|null $previous The previous exception. */ public function __construct( - string $type = Exception::GENERAL_UNKNOWN, + protected readonly string $type = Exception::GENERAL_UNKNOWN, ?string $message = null, ?int $code = null, ?\Throwable $previous = null ) { $errors = Config::getParam('errors'); - - $this->type = $type; - $error = $errors[$type] ?? []; + $error = $errors[$this->type] ?? []; $this->message = $message ?? $error['message']; $this->code = $code ?? $error['code'] ?: 500; @@ -67,8 +70,6 @@ public function __construct( /** * Get the type of the exception. - * - * @return string */ public function getType(): string { @@ -77,8 +78,6 @@ public function getType(): string /** * Get the short version of the exception. - * - * @return string */ public function getShort(): string { @@ -87,8 +86,6 @@ public function getShort(): string /** * Check whether the error message is publishable to logging systems (e.g. Sentry). - * - * @return bool */ public function isPublishable(): bool { diff --git a/src/Executor/Logs.php b/src/Executor/Logs.php index 8f3fe64..b323869 100644 --- a/src/Executor/Logs.php +++ b/src/Executor/Logs.php @@ -15,7 +15,7 @@ public static function get(string $containerId): array { $output = []; - $dir = "/tmp/$containerId/logging"; + $dir = sprintf('/tmp/%s/logging', $containerId); $logsFile = $dir . "/logs.txt"; $timingsFile = $dir . "/timings.txt"; @@ -66,7 +66,7 @@ public static function parseTiming(string $timing, ?DateTime $datetime = null): $datetime = new DateTime("now", new DateTimeZone("UTC")); // Date used for tracking absolute log timing } - if (empty($timing)) { + if ($timing === '' || $timing === '0') { return []; } @@ -74,7 +74,11 @@ public static function parseTiming(string $timing, ?DateTime $datetime = null): $rows = \explode("\n", $timing); foreach ($rows as $row) { - if (empty($row)) { + if ($row === '') { + continue; + } + + if ($row === '0') { continue; } @@ -105,7 +109,7 @@ public static function parseTiming(string $timing, ?DateTime $datetime = null): public static function getLogOffset(string $logs): int { $contentSplit = \explode("\n", $logs, 2); // Find first linebreak to identify prefix - $offset = \strlen($contentSplit[0] ?? ''); // Ignore script addition "Script started on..." + $offset = \strlen($contentSplit[0]); // Ignore script addition "Script started on..." $offset += 1; // Consider linebreak an intro too return $offset; diff --git a/src/Executor/Runner/Adapter.php b/src/Executor/Runner/Adapter.php index 0b9c923..13f8e42 100644 --- a/src/Executor/Runner/Adapter.php +++ b/src/Executor/Runner/Adapter.php @@ -6,39 +6,12 @@ abstract class Adapter { - /** - * @param string $runtimeId - * @param int $timeout - * @param Response $response - * @return void - */ abstract public function getLogs(string $runtimeId, int $timeout, Response $response): void; - /** - * @param string $runtimeId - * @param string $command - * @param int $timeout - * @return string - */ abstract public function executeCommand(string $runtimeId, string $command, int $timeout): string; /** - * @param string $runtimeId - * @param string $secret - * @param string $image - * @param string $entrypoint - * @param string $source - * @param string $destination * @param string[] $variables - * @param string $runtimeEntrypoint - * @param string $command - * @param int $timeout - * @param bool $remove - * @param float $cpus - * @param int $memory - * @param string $version - * @param string $restartPolicy - * @return mixed */ abstract public function createRuntime( string $runtimeId, @@ -59,31 +32,8 @@ abstract public function createRuntime( string $region = '', ): mixed; - /** - * @param string $runtimeId - * @return void - */ abstract public function deleteRuntime(string $runtimeId): void; - /** - * @param string $runtimeId - * @param string|null $payload - * @param string $path - * @param string $method - * @param mixed $headers - * @param int $timeout - * @param string $image - * @param string $source - * @param string $entrypoint - * @param mixed $variables - * @param float $cpus - * @param int $memory - * @param string $version - * @param string $runtimeEntrypoint - * @param bool $logging - * @param string $restartPolicy - * @return mixed - */ abstract public function createExecution( string $runtimeId, ?string $payload, diff --git a/src/Executor/Runner/Docker.php b/src/Executor/Runner/Docker.php index 340d9e1..6e81be5 100644 --- a/src/Executor/Runner/Docker.php +++ b/src/Executor/Runner/Docker.php @@ -23,8 +23,6 @@ class Docker extends Adapter { /** - * @param Orchestration $orchestration - * @param Runtimes $runtimes * @param string[] $networks */ public function __construct( @@ -34,18 +32,12 @@ public function __construct( ) { } - /** - * @param string $runtimeId - * @param int $timeout - * @param Response $response - * @return void - */ public function getLogs(string $runtimeId, int $timeout, Response $response): void { $runtimeName = System::getHostname() . '-' . $runtimeId; - $tmpFolder = "tmp/$runtimeName/"; - $tmpLogging = "/{$tmpFolder}logging"; // Build logs + $tmpFolder = sprintf('tmp/%s/', $runtimeName); + $tmpLogging = sprintf('/%slogging', $tmpFolder); // Build logs // TODO: Combine 3 checks below into one @@ -73,7 +65,7 @@ public function getLogs(string $runtimeId, int $timeout, Response $response): vo } $runtime = $this->runtimes->get($runtimeName); - if (!empty($runtime)) { + if ($runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { $version = $runtime->version; break; } @@ -94,14 +86,14 @@ public function getLogs(string $runtimeId, int $timeout, Response $response): vo if (\file_exists($tmpLogging . '/logs.txt') && \file_exists($tmpLogging . '/timings.txt')) { $timings = \file_get_contents($tmpLogging . '/timings.txt') ?: ''; - if (\strlen($timings) > 0) { + if ($timings !== '') { break; } } // Ensure runtime is still present $runtime = $this->runtimes->get($runtimeName); - if ($runtime === null) { + if (!$runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { return; } @@ -120,9 +112,9 @@ public function getLogs(string $runtimeId, int $timeout, Response $response): vo $streamInterval = 1000; // 1 second $activeRuntimes = $this->runtimes; - $timerId = Timer::tick($streamInterval, function () use (&$logsProcess, &$logsChunk, $response, $activeRuntimes, $runtimeName) { + $timerId = Timer::tick($streamInterval, function () use (&$logsProcess, &$logsChunk, $response, $activeRuntimes, $runtimeName): void { $runtime = $activeRuntimes->get($runtimeName); - if ($runtime === null) { + if (!$runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { return; } @@ -143,10 +135,8 @@ public function getLogs(string $runtimeId, int $timeout, Response $response): vo $write = $response->write($logsChunk); $logsChunk = ''; - if (!$write) { - if (!empty($logsProcess)) { - \proc_terminate($logsProcess, 9); - } + if (!$write && !empty($logsProcess)) { + \proc_terminate($logsProcess, 9); } }); @@ -157,13 +147,14 @@ public function getLogs(string $runtimeId, int $timeout, Response $response): vo $datetime = new \DateTime("now", new \DateTimeZone("UTC")); // Date used for tracking absolute log timing $output = ''; // Unused, just a refference for stdout - Console::execute('tail -F ' . $tmpLogging . '/timings.txt', '', $output, $timeout, function (string $timingChunk, mixed $process) use ($tmpLogging, &$logsChunk, &$logsProcess, &$datetime, &$offset, $introOffset) { + Console::execute('tail -F ' . $tmpLogging . '/timings.txt', '', $output, $timeout, function (string $timingChunk, mixed $process) use ($tmpLogging, &$logsChunk, &$logsProcess, &$datetime, &$offset, $introOffset): void { $logsProcess = $process; if (!\file_exists($tmpLogging . '/logs.txt')) { if (!empty($logsProcess)) { \proc_terminate($logsProcess, 9); } + return; } @@ -218,22 +209,7 @@ public function executeCommand(string $runtimeId, string $command, int $timeout) } /** - * @param string $runtimeId - * @param string $secret - * @param string $image - * @param string $entrypoint - * @param string $source - * @param string $destination * @param string[] $variables - * @param string $runtimeEntrypoint - * @param string $command - * @param int $timeout - * @param bool $remove - * @param float $cpus - * @param int $memory - * @param string $version - * @param string $restartPolicy - * @return mixed */ public function createRuntime( string $runtimeId, @@ -258,7 +234,7 @@ public function createRuntime( if ($this->runtimes->exists($runtimeName)) { $existingRuntime = $this->runtimes->get($runtimeName); - if ($existingRuntime !== null && $existingRuntime->status === 'pending') { + if ($existingRuntime instanceof \OpenRuntimes\Executor\Runner\Runtime && $existingRuntime->status === 'pending') { throw new Exception(Exception::RUNTIME_CONFLICT, 'A runtime with the same ID is already being created. Attempt a execution soon.'); } @@ -293,15 +269,15 @@ public function createRuntime( } $sourceFile = "code.tar.gz"; - if (!empty($source) && \pathinfo($source, PATHINFO_EXTENSION) === 'tar') { + if ($source !== '' && $source !== '0' && \pathinfo($source, PATHINFO_EXTENSION) === 'tar') { $sourceFile = "code.tar"; } - $tmpFolder = "tmp/$runtimeName/"; - $tmpSource = "/{$tmpFolder}src/$sourceFile"; - $tmpBuild = "/{$tmpFolder}builds/$buildFile"; - $tmpLogging = "/{$tmpFolder}logging"; // Build logs - $tmpLogs = "/{$tmpFolder}logs"; // Runtime logs + $tmpFolder = sprintf('tmp/%s/', $runtimeName); + $tmpSource = sprintf('/%ssrc/%s', $tmpFolder, $sourceFile); + $tmpBuild = sprintf('/%sbuilds/%s', $tmpFolder, $buildFile); + $tmpLogging = sprintf('/%slogging', $tmpFolder); // Build logs + $tmpLogs = sprintf('/%slogs', $tmpFolder); // Runtime logs $sourceDevice = StorageFactory::getDevice("/", System::getEnv('OPR_EXECUTOR_CONNECTION_STORAGE')); $localDevice = new Local(); @@ -310,10 +286,9 @@ public function createRuntime( /** * Copy code files from source to a temporary location on the executor */ - if (!empty($source)) { - if (!$sourceDevice->transfer($source, $tmpSource, $localDevice)) { - throw new \Exception('Failed to copy source code to temporary directory'); - }; + if ($source !== '' && $source !== '0' && !$sourceDevice->transfer($source, $tmpSource, $localDevice)) { + throw new \Exception('Failed to copy source code to temporary directory'); + ; } /** @@ -327,12 +302,8 @@ public function createRuntime( ->setCpus($cpus) ->setMemory($memory); - if (empty($runtimeEntrypoint)) { - if ($version === 'v2' && empty($command)) { - $runtimeEntrypointCommands = []; - } else { - $runtimeEntrypointCommands = ['tail', '-f', '/dev/null']; - } + if ($runtimeEntrypoint === '' || $runtimeEntrypoint === '0') { + $runtimeEntrypointCommands = $version === 'v2' && ($command === '' || $command === '0') ? [] : ['tail', '-f', '/dev/null']; } else { $runtimeEntrypointCommands = ['bash', '-c', $runtimeEntrypoint]; } @@ -369,14 +340,14 @@ public function createRuntime( restart: $restartPolicy ); - if (empty($containerId)) { + if ($containerId === '' || $containerId === '0') { throw new \Exception('Failed to create runtime'); } /** * Execute any commands if they were provided */ - if (!empty($command)) { + if ($command !== '' && $command !== '0') { if ($version === 'v2') { $commands = [ 'sh', @@ -401,7 +372,7 @@ public function createRuntime( ); if (!$status) { - throw new Exception(Exception::RUNTIME_FAILED, "Failed to create runtime: $stdout"); + throw new Exception(Exception::RUNTIME_FAILED, 'Failed to create runtime: ' . $stdout); } if ($version === 'v2') { @@ -421,7 +392,7 @@ public function createRuntime( /** * Move built code to expected build directory */ - if (!empty($destination)) { + if ($destination !== '' && $destination !== '0') { // Check if the build was successful by checking if file exists if (!$localDevice->exists($tmpBuild)) { throw new \Exception('Something went wrong when starting runtime.'); @@ -450,16 +421,16 @@ public function createRuntime( ]); $runtime = $this->runtimes->get($runtimeName); - if ($runtime !== null) { + if ($runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { $runtime->updated = \microtime(true); $runtime->status = 'Up ' . \round($duration, 2) . 's'; $runtime->initialised = 1; $this->runtimes->set($runtimeName, $runtime); } - } catch (Throwable $th) { + } catch (Throwable $throwable) { if ($version === 'v2') { - $message = !empty($output) ? $output : $th->getMessage(); + $message = $throwable->getMessage(); try { $logs = ''; $status = $this->orchestration->execute( @@ -469,24 +440,24 @@ public function createRuntime( timeout: 15 ); - if (!empty($logs)) { + if ($logs !== '' && $logs !== '0') { $message = $logs; } $message = \mb_substr($message, -MAX_BUILD_LOG_SIZE); // Limit to 1MB - } catch (Throwable $err) { + } catch (Throwable) { // Ignore, use fallback error message } - $output = [ + $output = [[ 'timestamp' => Logs::getTimestamp(), 'content' => $message - ]; + ]]; } else { $output = Logs::get($runtimeName); - $output = \count($output) > 0 ? $output : [[ + $output = $output !== [] ? $output : [[ 'timestamp' => Logs::getTimestamp(), - 'content' => $th->getMessage() + 'content' => $throwable->getMessage() ]]; } @@ -508,7 +479,7 @@ public function createRuntime( $message .= $chunk['content']; } - throw new \Exception($message, $th->getCode() ?: 500, $th); + throw new \Exception($message, $throwable->getCode() ?: 500, $throwable); } // Container cleanup @@ -527,7 +498,7 @@ public function createRuntime( // Remove weird symbol characters (for example from Next.js) if (\is_array($container['output'])) { - foreach ($container['output'] as $index => &$chunk) { + foreach ($container['output'] as &$chunk) { $chunk['content'] = \mb_convert_encoding($chunk['content'] ?? '', 'UTF-8', 'UTF-8'); } } @@ -535,10 +506,6 @@ public function createRuntime( return $container; } - /** - * @param string $runtimeId - * @return void - */ public function deleteRuntime(string $runtimeId): void { $runtimeName = System::getHostname() . '-' . $runtimeId; @@ -552,23 +519,6 @@ public function deleteRuntime(string $runtimeId): void } /** - * @param string $runtimeId - * @param string|null $payload - * @param string $path - * @param string $method - * @param mixed $headers - * @param int $timeout - * @param string $image - * @param string $source - * @param string $entrypoint - * @param mixed $variables - * @param float $cpus - * @param int $memory - * @param string $version - * @param string $runtimeEntrypoint - * @param bool $logging - * @param string $restartPolicy - * @return mixed * @throws Exception */ public function createExecution( @@ -600,12 +550,12 @@ public function createExecution( // Prepare runtime if (!$this->runtimes->exists($runtimeName)) { - if (empty($image) || empty($source)) { + if ($image === '' || $image === '0' || ($source === '' || $source === '0')) { throw new Exception(Exception::RUNTIME_NOT_FOUND, 'Runtime not found. Please start it first or provide runtime-related parameters.'); } // Prepare request to executor - $sendCreateRuntimeRequest = function () use ($runtimeId, $image, $source, $entrypoint, $variables, $cpus, $memory, $version, $restartPolicy, $runtimeEntrypoint) { + $sendCreateRuntimeRequest = function () use ($runtimeId, $image, $source, $entrypoint, $variables, $cpus, $memory, $version, $restartPolicy, $runtimeEntrypoint): array { $ch = \curl_init(); $body = \json_encode([ @@ -623,7 +573,7 @@ public function createExecution( \curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1/v1/runtimes"); \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?: ''); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); @@ -661,11 +611,7 @@ public function createExecution( ['errNo' => $errNo, 'error' => $error, 'statusCode' => $statusCode, 'executorResponse' => $executorResponse] = \call_user_func($sendCreateRuntimeRequest); if ($errNo === 0) { - if (\is_string($executorResponse)) { - $body = \json_decode($executorResponse, true); - } else { - $body = []; - } + $body = \is_string($executorResponse) ? \json_decode($executorResponse, true) : []; if ($statusCode >= 500) { // If the runtime has not yet attempted to start, it will return 500 @@ -692,7 +638,7 @@ public function createExecution( // Update runtimes $runtime = $this->runtimes->get($runtimeName); - if ($runtime !== null) { + if ($runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { $runtime->updated = \time(); $this->runtimes->set($runtimeName, $runtime); } @@ -706,7 +652,7 @@ public function createExecution( } $runtimeStatus = $this->runtimes->get($runtimeName); - if ($runtimeStatus === null) { + if (!$runtimeStatus instanceof \OpenRuntimes\Executor\Runner\Runtime) { throw new Exception(Exception::RUNTIME_NOT_FOUND, 'Runtime no longer exists.'); } @@ -722,12 +668,13 @@ public function createExecution( // Ensure we have secret $runtime = $this->runtimes->get($runtimeName); - if ($runtime === null) { + if (!$runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { throw new Exception(Exception::RUNTIME_NOT_FOUND, 'Runtime secret not found. Please re-create the runtime.', 500); } + $hostname = $runtime->hostname; $secret = $runtime->key; - if (empty($secret)) { + if ($secret === '' || $secret === '0') { throw new \Exception('Runtime secret not found. Please re-create the runtime.', 500); } @@ -746,9 +693,9 @@ public function createExecution( \curl_setopt($ch, CURLOPT_URL, "http://" . $hostname . ":3000/"); \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?: ''); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + \curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); \curl_setopt($ch, CURLOPT_HTTPHEADER, [ @@ -818,15 +765,15 @@ public function createExecution( } \curl_setopt($ch, CURLOPT_URL, "http://" . $hostname . ":3000" . $path); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method ?: 'GET'); \curl_setopt($ch, CURLOPT_NOBODY, \strtoupper($method) === 'HEAD'); - if (!empty($payload)) { + if (!in_array($payload, [null, '', '0'], true)) { \curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); } \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + \curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders): int { $len = strlen($header); $header = explode(':', $header, 2); if (count($header) < 2) { // ignore invalid headers @@ -836,7 +783,7 @@ public function createExecution( $key = strtolower(trim($header[0])); $value = trim($header[1]); - if (\in_array($key, ['x-open-runtimes-log-id'])) { + if ($key === 'x-open-runtimes-log-id') { $value = \urldecode($value); } @@ -853,13 +800,9 @@ public function createExecution( return $len; }); - \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout + 5); // Gives extra 5s after safe timeout to recieve response + \curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout + 5); // Gives extra 5s after safe timeout to recieve response \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); - if ($logging === true) { - $headers['x-open-runtimes-logging'] = 'enabled'; - } else { - $headers['x-open-runtimes-logging'] = 'disabled'; - } + $headers['x-open-runtimes-logging'] = $logging ? 'enabled' : 'disabled'; $headers['Authorization'] = 'Basic ' . \base64_encode('opr:' . $secret); $headers['x-open-runtimes-secret'] = $secret; @@ -898,11 +841,12 @@ public function createExecution( // Extract logs and errors from file based on fileId in header $fileId = $responseHeaders['x-open-runtimes-log-id'] ?? ''; if (\is_array($fileId)) { - $fileId = $fileId[0] ?? ''; + $fileId = $fileId[0]; } + $logs = ''; $errors = ''; - if (!empty($fileId)) { + if ($fileId !== '' && $fileId !== '0') { $logFile = '/tmp/' . $runtimeName . '/logs/' . $fileId . '_logs.log'; $errorFile = '/tmp/' . $runtimeName . '/logs/' . $fileId . '_errors.log'; @@ -956,7 +900,7 @@ public function createExecution( // From here we calculate billable duration of execution $startTime = \microtime(true); - if (empty($runtime->listening)) { + if ($runtime->listening === 0) { // Wait for cold-start to finish (app listening on port) $pingStart = \microtime(true); $validator = new TCP(); @@ -976,7 +920,7 @@ public function createExecution( // Update swoole table $runtime = $this->runtimes->get($runtimeName); - if ($runtime !== null) { + if ($runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { $runtime->listening = 1; $this->runtimes->set($runtimeName, $runtime); } @@ -1042,7 +986,7 @@ public function createExecution( // Update swoole table $runtime = $this->runtimes->get($runtimeName); - if ($runtime !== null) { + if ($runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { $runtime->updated = \microtime(true); $this->runtimes->set($runtimeName, $runtime); } @@ -1050,22 +994,19 @@ public function createExecution( return $execution; } - /** - * @return void - */ public function cleanup(): void { Console::log('Cleaning up containers and networks...'); $functionsToRemove = $this->orchestration->list(['label' => 'openruntimes-executor=' . System::getHostname()]); - if (\count($functionsToRemove) === 0) { + if ($functionsToRemove === []) { Console::info('No containers found to clean up.'); } $jobsRuntimes = []; foreach ($functionsToRemove as $container) { - $jobsRuntimes[] = function () use ($container) { + $jobsRuntimes[] = function () use ($container): void { try { $this->orchestration->remove($container->getId(), true); @@ -1076,12 +1017,13 @@ public function cleanup(): void } Console::success('Removed container ' . $container->getName()); - } catch (\Throwable $th) { + } catch (\Throwable $throwable) { Console::error('Failed to remove container: ' . $container->getName()); - Console::error($th); + Console::error($throwable); } }; } + batch($jobsRuntimes); Console::success('Cleanup finished.'); @@ -1093,13 +1035,14 @@ public function getRuntimes(): mixed foreach ($this->runtimes as $runtime) { $runtimes[] = $runtime->toArray(); } + return $runtimes; } public function getRuntime(string $name): mixed { $runtime = $this->runtimes->get($name); - if ($runtime === null) { + if (!$runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { throw new Exception(Exception::RUNTIME_NOT_FOUND); } diff --git a/src/Executor/Runner/ImagePuller.php b/src/Executor/Runner/ImagePuller.php index d53a380..18211e9 100644 --- a/src/Executor/Runner/ImagePuller.php +++ b/src/Executor/Runner/ImagePuller.php @@ -20,25 +20,25 @@ public function __construct(private Orchestration $orchestration) */ public function pull(array $images): void { - if (empty($images)) { + if ($images === []) { Console::log('[ImagePuller] No images to pull.'); return; } - $jobs = array_map(fn ($image) => function () use ($image) { + $jobs = array_map(fn ($image): \Closure => function () use ($image) { if (!$this->orchestration->pull($image)) { - Console::error("[ImagePuller] Failed to pull image $image"); + Console::error('[ImagePuller] Failed to pull image ' . $image); return; } return true; }, $images); - go(function () use ($jobs) { + go(function () use ($jobs): void { $results = batch($jobs); $success = \count(array_filter($results)); - Console::info("[ImagePuller] Pulled $success/". \count($jobs) . " images."); + Console::info(sprintf('[ImagePuller] Pulled %d/', $success). \count($jobs) . " images."); }); Console::info('[ImagePuller] Started pulling images.'); diff --git a/src/Executor/Runner/Maintenance.php b/src/Executor/Runner/Maintenance.php index 2b935de..3594de4 100644 --- a/src/Executor/Runner/Maintenance.php +++ b/src/Executor/Runner/Maintenance.php @@ -19,8 +19,8 @@ class Maintenance private int|false $timerId = false; public function __construct( - private Orchestration $orchestration, - private Runtimes $runtimes + private readonly Orchestration $orchestration, + private readonly Runtimes $runtimes ) { } @@ -35,7 +35,7 @@ public function start(int $intervalSeconds, int $inactiveSeconds): void $intervalMs = $intervalSeconds * 1000; $this->timerId = Timer::tick($intervalMs, fn () => $this->tick($inactiveSeconds)); - Console::info("[Maintenance] Started task on interval $intervalSeconds seconds."); + Console::info(sprintf('[Maintenance] Started task on interval %d seconds.', $intervalSeconds)); } /** @@ -57,12 +57,12 @@ public function stop(): void */ private function tick(int $inactiveSeconds): void { - Console::info("[Maintenance] Running task with threshold $inactiveSeconds seconds."); + Console::info(sprintf('[Maintenance] Running task with threshold %d seconds.', $inactiveSeconds)); $threshold = \time() - $inactiveSeconds; $candidates = array_filter( $this->runtimes->list(), - fn ($runtime) => $runtime->updated < $threshold + fn (\OpenRuntimes\Executor\Runner\Runtime $runtime): bool => $runtime->updated < $threshold ); // Remove from in-memory state before removing the container. @@ -71,15 +71,16 @@ private function tick(int $inactiveSeconds): void foreach ($keys as $key) { $this->runtimes->remove($key); } + // Then, remove forcefully terminate the associated running container. $jobs = array_map( - fn ($candidate) => fn () => $this->orchestration->remove($candidate->name, force: true), + fn (\OpenRuntimes\Executor\Runner\Runtime $candidate): \Closure => fn (): bool => $this->orchestration->remove($candidate->name, force: true), $candidates ); $results = batch($jobs); $removed = \count(array_filter($results)); - Console::info("[Maintenance] Removed {$removed}/" . \count($candidates) . " inactive runtimes."); + Console::info(sprintf('[Maintenance] Removed %d/', $removed) . \count($candidates) . " inactive runtimes."); $this->cleanupTmp(); } @@ -101,7 +102,7 @@ private function cleanupTmp(): void } if ($localDevice->deletePath($entry)) { - Console::info("[Maintenance] Removed {$entry}."); + Console::info(sprintf('[Maintenance] Removed %s.', $entry)); } } } diff --git a/src/Executor/Runner/Network.php b/src/Executor/Runner/Network.php index 17ced52..94bc169 100644 --- a/src/Executor/Runner/Network.php +++ b/src/Executor/Runner/Network.php @@ -27,16 +27,17 @@ public function setup(array $networks, string $container): void foreach ($networks as $network) { /* Ensure network exists */ if ($this->orchestration->networkExists($network)) { - Console::info("[Network] Network {$network} already exists"); + Console::info(sprintf('[Network] Network %s already exists', $network)); } else { try { $this->orchestration->createNetwork($network, false); - Console::success("[Network] Created network: {$network}"); + Console::success('[Network] Created network: ' . $network); } catch (\Throwable $e) { - Console::error("[Network] Failed to create network {$network}: {$e->getMessage()}"); + Console::error(sprintf('[Network] Failed to create network %s: %s', $network, $e->getMessage())); continue; } } + $this->available[] = $network; /* Add the container */ @@ -44,7 +45,7 @@ public function setup(array $networks, string $container): void $this->orchestration->networkConnect($container, $network); $this->container = $container; } catch (\Throwable $e) { - Console::error("[Network] Failed to connect container {$container} to network {$network}: {$e->getMessage()}"); + Console::error(sprintf('[Network] Failed to connect container %s to network %s: %s', $container, $network, $e->getMessage())); } } } @@ -60,19 +61,19 @@ public function cleanup(): void try { $this->orchestration->networkDisconnect($this->container, $network); } catch (\Throwable $e) { - Console::error("[Network] Failed to disconnect container {$this->container} from network {$network}: {$e->getMessage()}"); + Console::error(sprintf('[Network] Failed to disconnect container %s from network %s: %s', $this->container, $network, $e->getMessage())); } } /* Ensure network exists */ if ($this->orchestration->networkExists($network)) { - Console::info("[Network] Network {$network} already exists"); + Console::info(sprintf('[Network] Network %s already exists', $network)); } else { try { $this->orchestration->removeNetwork($network); - Console::success("[Network] Deleted network: {$network}"); + Console::success('[Network] Deleted network: ' . $network); } catch (\Throwable $e) { - Console::error("[Network] Failed to delete network {$network}: {$e->getMessage()}"); + Console::error(sprintf('[Network] Failed to delete network %s: %s', $network, $e->getMessage())); } } } diff --git a/src/Executor/Runner/Repository/Runtimes.php b/src/Executor/Runner/Repository/Runtimes.php index d1a46c3..d8bb199 100644 --- a/src/Executor/Runner/Repository/Runtimes.php +++ b/src/Executor/Runner/Repository/Runtimes.php @@ -9,7 +9,7 @@ /** * @implements Iterator */ -final class Runtimes implements Iterator +final readonly class Runtimes implements Iterator { private Table $runtimes; @@ -49,6 +49,7 @@ public function get(string $id): ?Runtime if ($runtime === false) { return null; } + return Runtime::fromArray($runtime); } @@ -68,7 +69,6 @@ public function exists(string $id): bool * * @param string $id The ID of the runtime to update. * @param Runtime $runtime The updated runtime object. - * @return void */ public function set(string $id, Runtime $runtime): void { @@ -97,6 +97,7 @@ public function list(): array foreach ($this->runtimes as $runtimeKey => $runtime) { $runtimes[$runtimeKey] = Runtime::fromArray($runtime); } + return $runtimes; } diff --git a/src/Executor/StorageFactory.php b/src/Executor/StorageFactory.php index 9166030..ba100c9 100644 --- a/src/Executor/StorageFactory.php +++ b/src/Executor/StorageFactory.php @@ -21,11 +21,10 @@ class StorageFactory * * @param string $root Root path for storage * @param ?string $connection DSN connection string. If empty or null, the local device will be used. - * @return Device */ public static function getDevice(string $root, ?string $connection = ''): Device { - $connection = $connection ?? ''; + $connection ??= ''; $acl = 'private'; $deviceType = Storage::DEVICE_LOCAL; $accessKey = ''; @@ -47,19 +46,21 @@ public static function getDevice(string $root, ?string $connection = ''): Device $insecure = $dsn->getParam('insecure', 'false') === 'true'; $url = $dsn->getParam('url', ''); - } catch (\Throwable $e) { - Console::warning($e->getMessage() . ' - Invalid DSN. Defaulting to Local device.'); + } catch (\Throwable $throwable) { + Console::warning($throwable->getMessage() . ' - Invalid DSN. Defaulting to Local device.'); } switch ($deviceType) { case Storage::DEVICE_S3: - if (!empty($url)) { + if ($url !== '' && $url !== '0') { return new S3($root, $accessKey, $accessSecret, $url, $dsnRegion, $acl); } - if (!empty($host)) { + + if ($host !== '' && $host !== '0') { $host = $insecure ? 'http://' . $host : $host; return new S3(root: $root, accessKey: $accessKey, secretKey: $accessSecret, host: $host, region: $dsnRegion, acl: $acl); } + return new AWS(root: $root, accessKey: $accessKey, secretKey: $accessSecret, bucket: $bucket, region: $dsnRegion, acl: $acl); case Storage::DEVICE_DO_SPACES: diff --git a/src/Executor/Validator/TCP.php b/src/Executor/Validator/TCP.php index a6c7106..eae32f4 100644 --- a/src/Executor/Validator/TCP.php +++ b/src/Executor/Validator/TCP.php @@ -37,7 +37,7 @@ public function isValid(mixed $value): bool [ $ip, $port ] = \explode(':', $value); $port = \intval($port); - if (empty($port) || empty($ip)) { + if ($port === 0 || ($ip === '' || $ip === '0')) { return false; } @@ -48,10 +48,10 @@ public function isValid(mixed $value): bool if (!$socket) { return false; - } else { - \fclose($socket); - return true; } + + \fclose($socket); + return true; } catch (\RuntimeException) { return false; } diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index a9daa60..f7a8bea 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -24,12 +24,8 @@ public function setKey(string $key): void /** * Wrapper method for client calls to make requests to the executor * - * @param string $method - * @param string $path * @param array $headers * @param array $params - * @param bool $decode - * @param ?callable $callback * @return array */ public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, ?callable $callback = null): array @@ -42,6 +38,7 @@ public function call(string $method, string $path = '', array $headers = [], arr foreach ($this->baseHeaders as $key => $value) { $client->addHeader($key, $value); } + foreach ($headers as $key => $value) { $client->addHeader($key, $value); } @@ -51,7 +48,7 @@ public function call(string $method, string $path = '', array $headers = [], arr method: $method, body: $method !== FetchClient::METHOD_GET ? $params : [], query: $method === FetchClient::METHOD_GET ? $params : [], - chunks: $callback ? function ($chunk) use ($callback) { + chunks: $callback ? function ($chunk) use ($callback): void { $callback($chunk->getData()); } : null ); @@ -83,14 +80,12 @@ public function call(string $method, string $path = '', array $headers = [], arr } } - $result = [ + return [ 'headers' => array_merge( $response->getHeaders(), ['status-code' => $response->getStatusCode()] ), 'body' => $body ]; - - return $result; } } diff --git a/tests/e2e/ExecutorTest.php b/tests/e2e/ExecutorTest.php index e34d2a6..5fe98ef 100644 --- a/tests/e2e/ExecutorTest.php +++ b/tests/e2e/ExecutorTest.php @@ -1,5 +1,7 @@ '0.11.0' // Enable array support for duplicate headers ]; - public function setUp(): void + protected function setUp(): void { $this->client = new Client($this->endpoint, $this->baseHeaders); $this->client->setKey($this->key); @@ -38,33 +42,37 @@ public function testLogStream(): void $runtimeId = \bin2hex(\random_bytes(4)); - Co\run(function () use (&$runtimeChunks, &$streamChunks, $runtimeId) { + Co\run(function () use (&$runtimeChunks, &$streamChunks, $runtimeId): void { /** Prepare build */ $output = ''; Console::execute('cd /app/tests/resources/functions/node && tar --exclude code.tar.gz -czf code.tar.gz .', '', $output); Co::join([ /** Watch logs */ - Co\go(function () use (&$streamChunks, $runtimeId) { - $this->client->call(Client::METHOD_GET, '/runtimes/test-log-stream-' . $runtimeId . '/logs', [], [], true, function ($data) use (&$streamChunks) { + Co\go(function () use (&$streamChunks, $runtimeId): void { + $this->client->call(Client::METHOD_GET, '/runtimes/test-log-stream-' . $runtimeId . '/logs', [], [], true, function ($data) use (&$streamChunks): void { // stream log parsing $data = \str_replace("\\n", "{OPR_LINEBREAK_PLACEHOLDER}", $data); foreach (\explode("\n", $data) as $chunk) { - if (empty($chunk)) { + if ($chunk === '') { + continue; + } + + if ($chunk === '0') { continue; } $chunk = \str_replace("{OPR_LINEBREAK_PLACEHOLDER}", "\n", $chunk); $parts = \explode(" ", $chunk, 2); $streamChunks[] = [ - 'timestamp' => $parts[0] ?? '', + 'timestamp' => $parts[0], 'content' => $parts[1] ?? '' ]; } }); }), /** Start runtime */ - Co\go(function () use (&$runtimeChunks, $runtimeId) { + Co\go(function () use (&$runtimeChunks, $runtimeId): void { $params = [ 'runtimeId' => 'test-log-stream-' . $runtimeId, 'source' => '/storage/functions/node/code.tar.gz', @@ -85,7 +93,7 @@ public function testLogStream(): void }); // Realtime logs are non-strict regarding exact end of logs - $validateLogs = function (array $logs, bool $strict) { + $validateLogs = function (array $logs, bool $strict): void { $content = ''; foreach ($logs as $log) { $content .= $log['content']; @@ -94,7 +102,7 @@ public function testLogStream(): void $this->assertStringContainsString('Environment preparation started', $content); for ($i = 1; $i <= 10; $i++) { - $this->assertStringContainsString("Step: $i", $content); + $this->assertStringContainsString('Step: ' . $i, $content); } if ($strict) { @@ -121,18 +129,19 @@ public function testLogStream(): void $previousTimestamp = $log['timestamp']; - if (!(\str_contains($log['content'], "echo -e"))) { - if (\str_contains($log['content'], "[33mOrange message") && \str_contains($log['content'], "[0m")) { + if (!(\str_contains((string) $log['content'], "echo -e"))) { + if (\str_contains((string) $log['content'], "[33mOrange message") && \str_contains((string) $log['content'], "[0m")) { $hasOrange = true; continue; } - if (\str_contains($log['content'], "[31mRed message") && \str_contains($log['content'], "[0m")) { + + if (\str_contains((string) $log['content'], "[31mRed message") && \str_contains((string) $log['content'], "[0m")) { $hasRed = true; continue; } } - if (\str_contains($log['content'], "Step: 5")) { + if (\str_contains((string) $log['content'], "Step: 5")) { $hasStep = true; } } @@ -164,7 +173,7 @@ public function testGetRuntimesEmpty(): void { $response = $this->client->call(Client::METHOD_GET, '/runtimes', [], []); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(0, count($response['body'])); + $this->assertCount(0, $response['body']); } public function testGetRuntimeUnknown(): void @@ -213,13 +222,13 @@ public function testBuild(): void /** Ensure build folder cleanup */ $tmpFolderPath = '/tmp/executor-test-build-' . $runtimeId; - $this->assertFalse(\is_dir($tmpFolderPath)); - $this->assertFalse(\file_exists($tmpFolderPath)); + $this->assertDirectoryNotExists($tmpFolderPath); + $this->assertFileNotExists($tmpFolderPath); /** List runtimes */ $response = $this->client->call(Client::METHOD_GET, '/runtimes', [], []); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(0, count($response['body'])); // Not 1, it was auto-removed + $this->assertCount(0, $response['body']); // Not 1, it was auto-removed /** Failure getRuntime */ $response = $this->client->call(Client::METHOD_GET, '/runtimes/test-build-selfdelete-' . $runtimeId, [], []); @@ -341,7 +350,7 @@ public function testBuildUncompressed(): void $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['body']['statusCode']); - $json = json_decode($response['body']['body'], true); + $json = json_decode((string) $response['body']['body'], true); $this->assertEquals("Hello Open Runtimes 👋", $json['message']); $response = $this->client->call(Client::METHOD_DELETE, '/runtimes/test-exec-' . $runtimeId, [], []); @@ -386,7 +395,7 @@ public function testExecute(): void $response = $this->client->call(Client::METHOD_POST, '/runtimes/test-exec/executions'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['body']['statusCode']); - $this->assertEquals('aValue', \json_decode($response['body']['headers'], true)['x-key']); + $this->assertEquals('aValue', \json_decode((string) $response['body']['headers'], true)['x-key']); /** Execute on cold-started runtime */ $response = $this->client->call(Client::METHOD_POST, '/runtimes/test-exec/executions', [], [ @@ -573,25 +582,25 @@ public function testSSRLogs(): void $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['body']['statusCode']); - $this->assertStringContainsString('

OK

', $response['body']['body']); + $this->assertStringContainsString('

OK

', (string) $response['body']['body']); - $setCookieList = \json_decode($response['body']['headers'], true)['set-cookie']; + $setCookieList = \json_decode((string) $response['body']['headers'], true)['set-cookie']; $this->assertEquals('astroCookie1=astroValue1; Max-Age=1800; HttpOnly', $setCookieList[0]); $this->assertEquals('astroCookie2=astroValue2; Max-Age=1800; HttpOnly', $setCookieList[1]); $this->assertNotEmpty($response['body']['logs']); - $this->assertStringContainsString('Open runtimes log', $response['body']['logs']); - $this->assertStringContainsString('A developer log', $response['body']['logs']); + $this->assertStringContainsString('Open runtimes log', (string) $response['body']['logs']); + $this->assertStringContainsString('A developer log', (string) $response['body']['logs']); $this->assertNotEmpty($response['body']['errors']); - $this->assertStringContainsString('Open runtimes error', $response['body']['errors']); + $this->assertStringContainsString('Open runtimes error', (string) $response['body']['errors']); $response = $this->client->call(Client::METHOD_POST, '/runtimes/test-ssr-exec/executions', [ 'x-executor-response-format' => '0.10.0' // Last version to report string header values only ], $params); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['body']['statusCode']); - $this->assertEquals('astroCookie2=astroValue2; Max-Age=1800; HttpOnly', \json_decode($response['body']['headers'], true)['set-cookie']); + $this->assertEquals('astroCookie2=astroValue2; Max-Age=1800; HttpOnly', \json_decode((string) $response['body']['headers'], true)['set-cookie']); /** Delete runtime */ $response = $this->client->call(Client::METHOD_DELETE, '/runtimes/test-ssr-exec', [], []); @@ -649,7 +658,7 @@ public function testRestartPolicy(): void \exec('docker logs executor-test-exec-restart-policy', $output); $output = \implode("\n", $output); $occurances = \substr_count($output, 'HTTP server successfully started!'); - $this->assertEquals(3, $occurances); + $this->assertSame(3, $occurances); /** Delete runtime */ $response = $this->client->call(Client::METHOD_DELETE, '/runtimes/test-exec-restart-policy', [], []); @@ -677,9 +686,9 @@ public function testBuildLogLimit(): void $response = $this->client->call(Client::METHOD_POST, '/runtimes', [], $params); $this->assertEquals(400, $response['headers']['status-code']); - $this->assertGreaterThanOrEqual($size128Kb * 7, \strlen($response['body']['message'])); - $this->assertStringContainsString('First log', $response['body']['message']); - $this->assertStringContainsString('Last log', $response['body']['message']); + $this->assertGreaterThanOrEqual($size128Kb * 7, \strlen((string) $response['body']['message'])); + $this->assertStringContainsString('First log', (string) $response['body']['message']); + $this->assertStringContainsString('Last log', (string) $response['body']['message']); $output = ''; Console::execute('cd /app/tests/resources/functions/php-build-logs && tar --exclude code.tar.gz -czf code.tar.gz .', '', $output); @@ -721,9 +730,9 @@ public function testBuildLogLimit(): void $response = $this->client->call(Client::METHOD_POST, '/runtimes', [], $params); $this->assertEquals(400, $response['headers']['status-code']); - $this->assertGreaterThanOrEqual(1000000, \strlen($response['body']['message'])); - $this->assertStringNotContainsString('Last log', $response['body']['message']); - $this->assertStringContainsString('First log', $response['body']['message']); + $this->assertGreaterThanOrEqual(1000000, \strlen((string) $response['body']['message'])); + $this->assertStringNotContainsString('Last log', (string) $response['body']['message']); + $this->assertStringContainsString('First log', (string) $response['body']['message']); $output = ''; Console::execute('cd /app/tests/resources/functions/php-build-logs && tar --exclude code.tar.gz -czf code.tar.gz .', '', $output); @@ -754,271 +763,253 @@ public function testBuildLogLimit(): void /** * - * @return array + * @return \Iterator<(int | string), mixed> */ - public function provideScenarios(): array + public function provideScenarios(): \Iterator { - return [ - [ - 'image' => 'openruntimes/node:v2-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-v2', - 'version' => 'v2', - 'startCommand' => '', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - $this->assertEquals('{"message":"Hello Open Runtimes 👋"}', $response['body']['body']); - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - } - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-empty-object', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - $this->assertEquals('{}', $response['body']['body']); - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - } - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-empty-array', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - $this->assertEquals('[]', $response['body']['body']); - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - } - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-timeout', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(500, $response['body']['statusCode']); - $this->assertStringContainsString('Execution timed out.', $response['body']['errors']); - $this->assertEmpty($response['body']['logs']); + yield [ + 'image' => 'openruntimes/node:v2-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-v2', + 'version' => 'v2', + 'startCommand' => '', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + $this->assertEquals('{"message":"Hello Open Runtimes 👋"}', $response['body']['body']); + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + } + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-empty-object', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + $this->assertEquals('{}', $response['body']['body']); + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + } + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-empty-array', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + $this->assertEquals('[]', $response['body']['body']); + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + } + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-timeout', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(500, $response['body']['statusCode']); + $this->assertStringContainsString('Execution timed out.', (string) $response['body']['errors']); + $this->assertEmpty($response['body']['logs']); + } + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-logs', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("OK", $response['body']['body']); + $this->assertSame(1024 * 1024, strlen((string) $response['body']['logs'])); + $this->assertEmpty($response['body']['errors']); + }, + 'body' => fn (): string => '1', + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-logs', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("OK", $response['body']['body']); + $this->assertSame(5 * 1024 * 1024, strlen((string) $response['body']['logs'])); + $this->assertEmpty($response['body']['errors']); + }, + 'body' => fn (): string => '5', + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-logs', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("OK", $response['body']['body']); + $this->assertGreaterThan(5 * 1024 * 1024, strlen((string) $response['body']['logs'])); + $this->assertLessThan(6 * 1024 * 1024, strlen((string) $response['body']['logs'])); + $this->assertStringContainsString('truncated', (string) $response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + }, + 'body' => fn (): string => '15', + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-logs', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("OK", $response['body']['body']); + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + }, + 'body' => fn (): string => '1', + 'logging' => false, + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-long-coldstart', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + $this->assertEquals('OK', $response['body']['body']); + $this->assertGreaterThan(10, $response['body']['duration']); // This is unsafe but important. If its flaky, inform @Meldiron + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + } + ]; + yield [ + 'image' => 'openruntimes/node:v5-21.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-binary-response', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + $bytes = unpack('C*byte', (string) $response['body']['body']); + if (!is_array($bytes)) { + $bytes = []; } - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-logs', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("OK", $response['body']['body']); - $this->assertEquals(1 * 1024 * 1024, strlen($response['body']['logs'])); - $this->assertEmpty($response['body']['errors']); - }, - 'body' => function () { - return '1'; - }, - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-logs', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("OK", $response['body']['body']); - $this->assertEquals(5 * 1024 * 1024, strlen($response['body']['logs'])); - $this->assertEmpty($response['body']['errors']); - }, - 'body' => function () { - return '5'; - }, - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-logs', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("OK", $response['body']['body']); - $this->assertGreaterThan(5 * 1024 * 1024, strlen($response['body']['logs'])); - $this->assertLessThan(6 * 1024 * 1024, strlen($response['body']['logs'])); - $this->assertStringContainsString('truncated', $response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - }, - 'body' => function () { - return '15'; - }, - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-logs', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("OK", $response['body']['body']); - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - }, - 'body' => function () { - return '1'; - }, - 'logging' => false, - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-long-coldstart', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - $this->assertEquals('OK', $response['body']['body']); - $this->assertGreaterThan(10, $response['body']['duration']); // This is unsafe but important. If its flaky, inform @Meldiron - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); + + $this->assertCount(3, $bytes); + $this->assertEquals(0, $bytes['byte1'] ?? 0); + $this->assertEquals(10, $bytes['byte2'] ?? 0); + $this->assertEquals(255, $bytes['byte3'] ?? 0); + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + }, + null, // body, + 'logging' => true, + 'mimeType' => 'multipart/form-data' + ]; + yield [ + 'image' => 'openruntimes/node:v5-21.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-binary-response', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringContainsString("JSON response does not allow binaries", (string) $response['body']['message']); + }, + null, // body, + 'logging' => true, + 'mimeType' => 'application/json' + ]; + yield [ + 'image' => 'openruntimes/node:v5-21.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-binary-request', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + $bytes = unpack('C*byte', (string) $response['body']['body']); + if (!is_array($bytes)) { + $bytes = []; } - ], - [ - 'image' => 'openruntimes/node:v5-21.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-binary-response', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - $bytes = unpack('C*byte', $response['body']['body']); - if (!is_array($bytes)) { - $bytes = []; - } - self::assertCount(3, $bytes); - self::assertEquals(0, $bytes['byte1'] ?? 0); - self::assertEquals(10, $bytes['byte2'] ?? 0); - self::assertEquals(255, $bytes['byte3'] ?? 0); - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - }, - null, // body, - 'logging' => true, - 'mimeType' => 'multipart/form-data' - ], - [ - 'image' => 'openruntimes/node:v5-21.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-binary-response', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(400, $response['headers']['status-code']); - $this->assertStringContainsString("JSON response does not allow binaries", $response['body']['message']); - }, - null, // body, - 'logging' => true, - 'mimeType' => 'application/json' - ], - [ - 'image' => 'openruntimes/node:v5-21.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-binary-request', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - $bytes = unpack('C*byte', $response['body']['body']); - if (!is_array($bytes)) { - $bytes = []; - } - self::assertCount(3, $bytes); - self::assertEquals(0, $bytes['byte1'] ?? 0); - self::assertEquals(10, $bytes['byte2'] ?? 0); - self::assertEquals(255, $bytes['byte3'] ?? 0); - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - }, - 'body' => function () { - return pack('C*', 0, 10, 255); - }, - 'logging' => true, - 'mimeType' => 'multipart/form-data' - ], - [ - 'image' => 'openruntimes/node:v5-18.0', - 'entrypoint' => 'index.js', - 'folder' => 'node-specs', - 'version' => 'v5', - 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', - 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i && npm run build"', - 'assertions' => function ($response) { - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(200, $response['body']['statusCode']); - - $this->assertIsString($response['body']['body']); - $this->assertNotEmpty($response['body']['body']); - $json = \json_decode($response['body']['body'], true); - $this->assertEquals("2.5", $json['cpus']); - $this->assertEquals("1024", $json['memory']); - - $this->assertEmpty($response['body']['logs']); - $this->assertEmpty($response['body']['errors']); - }, - 'body' => null, - 'logging' => true, - 'mimeType' => 'application/json', - 'cpus' => 2.5, - 'memory' => 1024, - 'buildAssertions' => function ($response) { - $output = ''; - foreach ($response['body']['output'] as $outputItem) { - $output .= $outputItem['content']; - } - $this->assertStringContainsString("cpus=2.5", $output); - $this->assertStringContainsString("memory=1024", $output); + $this->assertCount(3, $bytes); + $this->assertEquals(0, $bytes['byte1'] ?? 0); + $this->assertEquals(10, $bytes['byte2'] ?? 0); + $this->assertEquals(255, $bytes['byte3'] ?? 0); + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + }, + 'body' => fn (): string => pack('C*', 0, 10, 255), + 'logging' => true, + 'mimeType' => 'multipart/form-data' + ]; + yield [ + 'image' => 'openruntimes/node:v5-18.0', + 'entrypoint' => 'index.js', + 'folder' => 'node-specs', + 'version' => 'v5', + 'startCommand' => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "bash helpers/server.sh"', + 'buildCommand' => 'tar -zxf /tmp/code.tar.gz -C /mnt/code && bash helpers/build.sh "npm i && npm run build"', + 'assertions' => function (array $response): void { + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(200, $response['body']['statusCode']); + + $this->assertIsString($response['body']['body']); + $this->assertNotEmpty($response['body']['body']); + $json = \json_decode($response['body']['body'], true); + $this->assertEquals("2.5", $json['cpus']); + $this->assertEquals("1024", $json['memory']); + + $this->assertEmpty($response['body']['logs']); + $this->assertEmpty($response['body']['errors']); + }, + 'body' => null, + 'logging' => true, + 'mimeType' => 'application/json', + 'cpus' => 2.5, + 'memory' => 1024, + 'buildAssertions' => function (array $response): void { + $output = ''; + foreach ($response['body']['output'] as $outputItem) { + $output .= $outputItem['content']; } - ], + + $this->assertStringContainsString("cpus=2.5", $output); + $this->assertStringContainsString("memory=1024", $output); + } ]; } /** - * @param string $image - * @param string $entrypoint - * @param string $folder - * @param string $version - * @param string $startCommand - * @param string $buildCommand - * @param callable $assertions - * @param bool $logging * * @dataProvider provideScenarios */ @@ -1026,14 +1017,14 @@ public function testScenarios(string $image, string $entrypoint, string $folder, { /** Prepare deployment */ $output = ''; - Console::execute("cd /app/tests/resources/functions/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); + Console::execute(sprintf('cd /app/tests/resources/functions/%s && tar --exclude code.tar.gz -czf code.tar.gz .', $folder), '', $output); $runtimeId = \bin2hex(\random_bytes(4)); /** Build runtime */ $params = [ - 'runtimeId' => "scenario-build-{$folder}-{$runtimeId}", - 'source' => "/storage/functions/{$folder}/code.tar.gz", + 'runtimeId' => sprintf('scenario-build-%s-%s', $folder, $runtimeId), + 'source' => sprintf('/storage/functions/%s/code.tar.gz', $folder), 'destination' => '/storage/builds/test', 'version' => $version, 'entrypoint' => $entrypoint, @@ -1071,45 +1062,39 @@ public function testScenarios(string $image, string $entrypoint, string $folder, } /** Execute function */ - $response = $this->client->call(Client::METHOD_POST, "/runtimes/scenario-execute-{$folder}-{$runtimeId}/executions", [ + $response = $this->client->call(Client::METHOD_POST, sprintf('/runtimes/scenario-execute-%s-%s/executions', $folder, $runtimeId), [ 'content-type' => $mimeType, 'accept' => $mimeType ], $params); - $this->assertStringContainsString($mimeType, $response['headers']['content-type']); + $this->assertStringContainsString($mimeType, (string) $response['headers']['content-type']); call_user_func($assertions, $response); /** Delete runtime */ - $response = $this->client->call(Client::METHOD_DELETE, "/runtimes/scenario-execute-{$folder}-{$runtimeId}", [], []); + $response = $this->client->call(Client::METHOD_DELETE, sprintf('/runtimes/scenario-execute-%s-%s', $folder, $runtimeId), [], []); $this->assertEquals(200, $response['headers']['status-code']); } /** * - * @return array + * @return \Iterator<(int | string), mixed> */ - public function provideCustomRuntimes(): array + public function provideCustomRuntimes(): \Iterator { - return [ - [ 'folder' => 'php', 'image' => 'openruntimes/php:v5-8.1', 'entrypoint' => 'index.php', 'buildCommand' => 'composer install' ], - [ 'folder' => 'php-mock', 'image' => 'openruntimes/php:v5-8.1', 'entrypoint' => 'index.php', 'buildCommand' => 'composer install' ], - [ 'folder' => 'node', 'image' => 'openruntimes/node:v5-18.0', 'entrypoint' => 'index.js', 'buildCommand' => 'npm i'], - // [ 'folder' => 'deno', 'image' => 'openruntimes/deno:v5-1.24', 'entrypoint' => 'index.ts', 'buildCommand' => 'deno cache index.ts', 'startCommand' => 'denon start' ], - [ 'folder' => 'python', 'image' => 'openruntimes/python:v5-3.10', 'entrypoint' => 'index.py', 'buildCommand' => 'pip install -r requirements.txt'], - [ 'folder' => 'ruby', 'image' => 'openruntimes/ruby:v5-3.1', 'entrypoint' => 'index.rb', 'buildCommand' => ''], - [ 'folder' => 'cpp', 'image' => 'openruntimes/cpp:v5-17', 'entrypoint' => 'index.cc', 'buildCommand' => ''], - [ 'folder' => 'dart', 'image' => 'openruntimes/dart:v5-2.18', 'entrypoint' => 'lib/index.dart', 'buildCommand' => 'dart pub get'], - [ 'folder' => 'dotnet', 'image' => 'openruntimes/dotnet:v5-6.0', 'entrypoint' => 'Index.cs', 'buildCommand' => ''], - // Swift, Kotlin, Java missing on purpose - ]; + yield [ 'folder' => 'php', 'image' => 'openruntimes/php:v5-8.1', 'entrypoint' => 'index.php', 'buildCommand' => 'composer install' ]; + yield [ 'folder' => 'php-mock', 'image' => 'openruntimes/php:v5-8.1', 'entrypoint' => 'index.php', 'buildCommand' => 'composer install' ]; + yield [ 'folder' => 'node', 'image' => 'openruntimes/node:v5-18.0', 'entrypoint' => 'index.js', 'buildCommand' => 'npm i']; + // [ 'folder' => 'deno', 'image' => 'openruntimes/deno:v5-1.24', 'entrypoint' => 'index.ts', 'buildCommand' => 'deno cache index.ts', 'startCommand' => 'denon start' ], + yield [ 'folder' => 'python', 'image' => 'openruntimes/python:v5-3.10', 'entrypoint' => 'index.py', 'buildCommand' => 'pip install -r requirements.txt']; + yield [ 'folder' => 'ruby', 'image' => 'openruntimes/ruby:v5-3.1', 'entrypoint' => 'index.rb', 'buildCommand' => '']; + yield [ 'folder' => 'cpp', 'image' => 'openruntimes/cpp:v5-17', 'entrypoint' => 'index.cc', 'buildCommand' => '']; + yield [ 'folder' => 'dart', 'image' => 'openruntimes/dart:v5-2.18', 'entrypoint' => 'lib/index.dart', 'buildCommand' => 'dart pub get']; + yield [ 'folder' => 'dotnet', 'image' => 'openruntimes/dotnet:v5-6.0', 'entrypoint' => 'Index.cs', 'buildCommand' => '']; } /** - * @param string $folder - * @param string $image - * @param string $entrypoint * * @dataProvider provideCustomRuntimes */ @@ -1117,14 +1102,14 @@ public function testCustomRuntimes(string $folder, string $image, string $entryp { // Prepare tar.gz files $output = ''; - Console::execute("cd /app/tests/resources/functions/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); + Console::execute(sprintf('cd /app/tests/resources/functions/%s && tar --exclude code.tar.gz -czf code.tar.gz .', $folder), '', $output); $runtimeId = \bin2hex(\random_bytes(4)); // Build deployment $params = [ - 'runtimeId' => "custom-build-{$folder}-{$runtimeId}", - 'source' => "/storage/functions/{$folder}/code.tar.gz", + 'runtimeId' => sprintf('custom-build-%s-%s', $folder, $runtimeId), + 'source' => sprintf('/storage/functions/%s/code.tar.gz', $folder), 'destination' => '/storage/builds/test', 'entrypoint' => $entrypoint, 'image' => $image, @@ -1137,13 +1122,14 @@ public function testCustomRuntimes(string $folder, string $image, string $entryp if ($response['headers']['status-code'] !== 201) { \var_dump($response); } + $this->assertEquals(201, $response['headers']['status-code']); $this->assertIsString($response['body']['path']); $path = $response['body']['path']; // Execute function - $response = $this->client->call(Client::METHOD_POST, "/runtimes/custom-execute-{$folder}-{$runtimeId}/executions", [], [ + $response = $this->client->call(Client::METHOD_POST, sprintf('/runtimes/custom-execute-%s-%s/executions', $folder, $runtimeId), [], [ 'source' => $path, 'entrypoint' => $entrypoint, 'image' => $image, @@ -1167,7 +1153,7 @@ public function testCustomRuntimes(string $folder, string $image, string $entryp $body = $response['body']; $this->assertEquals(200, $body['statusCode']); $this->assertEmpty($body['errors']); - $this->assertStringContainsString('Sample Log', $body['logs']); + $this->assertStringContainsString('Sample Log', (string) $body['logs']); $this->assertIsString($body['body']); $this->assertNotEmpty($body['body']); $response = \json_decode($body['body'], true); @@ -1178,7 +1164,7 @@ public function testCustomRuntimes(string $folder, string $image, string $entryp $this->assertEquals(13, $response['todo']['userId']); /** Delete runtime */ - $response = $this->client->call(Client::METHOD_DELETE, "/runtimes/custom-execute-{$folder}-{$runtimeId}", [], []); + $response = $this->client->call(Client::METHOD_DELETE, sprintf('/runtimes/custom-execute-%s-%s', $folder, $runtimeId), [], []); $this->assertEquals(200, $response['headers']['status-code']); } @@ -1203,7 +1189,7 @@ public function testZipBuild(): void $response = $this->client->call(Client::METHOD_POST, '/runtimes', [], $params); $this->assertEquals(201, $response['headers']['status-code']); - $this->assertNotEmpty(201, $response['body']['path']); + $this->assertNotEmpty($response['body']['path']); $buildPath = $response['body']['path']; @@ -1244,20 +1230,20 @@ public function testCommands(): void 'command' => 'echo "Hello, World!"' ]); $this->assertEquals(200, $command['headers']['status-code']); - $this->assertStringContainsString('Hello, World!', $command['body']['output']); // not equals, because echo adds a newline + $this->assertStringContainsString('Hello, World!', (string) $command['body']['output']); // not equals, because echo adds a newline $command = $this->client->call(Client::METHOD_POST, '/runtimes/test-commands/commands', [], [ 'command' => 'sleep 5 && echo "Ok"', 'timeout' => 1 ]); $this->assertEquals(500, $command['headers']['status-code']); - $this->assertStringContainsString('Operation timed out', $command['body']['message']); + $this->assertStringContainsString('Operation timed out', (string) $command['body']['message']); $command = $this->client->call(Client::METHOD_POST, '/runtimes/test-commands/commands', [], [ 'command' => 'exit 1' ]); $this->assertEquals(500, $command['headers']['status-code']); - $this->assertStringContainsString('Failed to execute command', $command['body']['message']); + $this->assertStringContainsString('Failed to execute command', (string) $command['body']['message']); $response = $this->client->call(Client::METHOD_DELETE, "/runtimes/test-commands", [], []); $this->assertEquals(200, $response['headers']['status-code']); @@ -1279,16 +1265,16 @@ public function testLogStreamPersistent(): void $runtimeEnd = 0; $realtimeEnd = 0; - Co\run(function () use (&$runtimeEnd, &$realtimeEnd) { + Co\run(function () use (&$runtimeEnd, &$realtimeEnd): void { Co::join([ /** Watch logs */ - Co\go(function () use (&$realtimeEnd) { + Co\go(function () use (&$realtimeEnd): void { $this->client->call(Client::METHOD_GET, '/runtimes/test-log-stream-persistent/logs', [], [], true); $realtimeEnd = \microtime(true); }), /** Start runtime */ - Co\go(function () use (&$runtimeEnd) { + Co\go(function () use (&$runtimeEnd): void { $params = [ 'runtimeId' => 'test-log-stream-persistent', 'source' => '/storage/functions/node/code.tar.gz', diff --git a/tests/unit/Executor/Runner/Repository/RuntimesTest.php b/tests/unit/Executor/Runner/Repository/RuntimesTest.php index 89d09f4..7eb6d9c 100644 --- a/tests/unit/Executor/Runner/Repository/RuntimesTest.php +++ b/tests/unit/Executor/Runner/Repository/RuntimesTest.php @@ -1,12 +1,14 @@ assertNull($repository->get('missing')); + $this->assertNotInstanceOf(\OpenRuntimes\Executor\Runner\Runtime::class, $repository->get('missing')); } public function testRemove(): void @@ -61,7 +63,7 @@ public function testRemove(): void $this->assertTrue($repository->remove('rt-2')); $this->assertFalse($repository->exists('rt-2')); - $this->assertNull($repository->get('rt-2')); + $this->assertNotInstanceOf(\OpenRuntimes\Executor\Runner\Runtime::class, $repository->get('rt-2')); $this->assertFalse($repository->remove('rt-2')); } diff --git a/tests/unit/Executor/Runner/RuntimeTest.php b/tests/unit/Executor/Runner/RuntimeTest.php index b58bfa0..de71302 100644 --- a/tests/unit/Executor/Runner/RuntimeTest.php +++ b/tests/unit/Executor/Runner/RuntimeTest.php @@ -1,11 +1,13 @@