Add PHP 7.2.6 runtime (#28)
diff --git a/README.md b/README.md
index 5d66dcc..1833a8e 100644
--- a/README.md
+++ b/README.md
@@ -22,33 +22,54 @@
[![Build Status](https://travis-ci.org/apache/incubator-openwhisk-runtime-php.svg?branch=master)](https://travis-ci.org/apache/incubator-openwhisk-runtime-php)
+## PHP versions
+
+This runtime provides PHP 7.2 and 7.1.
+
### Give it a try today
To use as a docker action
+
+PHP 7.2:
```
-wsk action update myAction myAction.php --docker openwhisk/action-php-v7.1:1.0.0
+wsk action update myAction myAction.php --docker openwhisk/action-php-v7.2:latest
```
+
+PHP 7.1:
+```
+wsk action update myAction myAction.php --docker openwhisk/action-php-v7.1:latest
+```
+
This works on any deployment of Apache OpenWhisk
### To use on deployment that contains the rutime as a kind
To use as a kind action
+
+PHP 7.2:
+```
+wsk action update myAction myAction.php --kind php:7.2
+```
+
+PHP 7.1:
```
wsk action update myAction myAction.php --kind php:7.1
```
### Local development
```
+./gradlew core:php7.2Action:distDocker
./gradlew core:php7.1Action:distDocker
```
-This will produce the image `whisk/action-php-v7.1`
+This will produce the images `whisk/action-php-v7.2` & `whisk/action-php-v7.1`
Build and Push image
```
docker login
+./gradlew core:php7.2Action:distDocker -PdockerImagePrefix=$prefix-user -PdockerRegistry=docker.io
./gradlew core:php7.1Action:distDocker -PdockerImagePrefix=$prefix-user -PdockerRegistry=docker.io
```
-Deploy OpenWhisk using ansible environment that contains the kind `php:7.1`
-Assuming you have OpenWhisk already deploy localy and `OPENWHISK_HOME` pointing to root directory of OpenWhisk core repository.
+Deploy OpenWhisk using ansible environment that contains the kinds `php:7.2` & `php:7.1`
+Assuming you have OpenWhisk already deploy locally and `OPENWHISK_HOME` pointing to root directory of OpenWhisk core repository.
Set `ROOTDIR` to the root directory of this repository.
@@ -71,12 +92,16 @@
To use as docker action push to your own dockerhub account
```
+docker tag whisk/php7.2Action $user_prefix/action-php-v7.2
+docker push $user_prefix/action-php-v7.2
+```
+```
docker tag whisk/php7.1Action $user_prefix/action-php-v7.1
docker push $user_prefix/action-php-v7.1
```
Then create the action using your the image from dockerhub
```
-wsk action update myAction myAction.php --docker $user_prefix/action-php-v7.1
+wsk action update myAction myAction.php --docker $user_prefix/action-php-v7.2
```
The `$user_prefix` is usually your dockerhub user id.
diff --git a/ansible/environments/local/group_vars/all b/ansible/environments/local/group_vars/all
index 1b3fe54..936316f 100755
--- a/ansible/environments/local/group_vars/all
+++ b/ansible/environments/local/group_vars/all
@@ -40,9 +40,15 @@
deprecated: false
php:
- kind: "php:7.1"
- default: true
+ default: false
image:
name: "action-php-v7.1"
deprecated: false
+ php:
+ - kind: "php:7.2"
+ default: true
+ image:
+ name: "action-php-v7.2"
+ deprecated: false
blackboxes:
- name: "dockerskeleton"
diff --git a/core/php7.2Action/CHANGELOG.md b/core/php7.2Action/CHANGELOG.md
new file mode 100644
index 0000000..702b708
--- /dev/null
+++ b/core/php7.2Action/CHANGELOG.md
@@ -0,0 +1,38 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## 1.0.0
+Initial release
+
+- Added: PHP: 7.2.6
+- Added: PHP extensions in addition to the standard ones:
+ - bcmath
+ - curl
+ - gd
+ - intl
+ - mbstring
+ - mysqli
+ - pdo_mysql
+ - pdo_pgsql
+ - pdo_sqlite
+ - soap
+ - zip
+- Added: Composer packages:
+ - [guzzlehttp/guzzle](https://packagist.org/packages/guzzlehttp/guzzle): 6.3.3
+ - [ramsey/uuid](https://packagist.org/packages/ramsey/uuid): 3.7.3
diff --git a/core/php7.2Action/Dockerfile b/core/php7.2Action/Dockerfile
new file mode 100644
index 0000000..fb08b24
--- /dev/null
+++ b/core/php7.2Action/Dockerfile
@@ -0,0 +1,63 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM php:7.2.6-alpine
+
+RUN \
+ apk update && apk upgrade && \
+ # install dependencies
+ apk add \
+ postgresql-dev \
+ icu \
+ icu-libs \
+ icu-dev \
+ freetype-dev \
+ libjpeg-turbo-dev \
+ libpng-dev \
+ libxml2-dev \
+ && \
+ # install useful PHP extensions
+ docker-php-ext-install \
+ opcache \
+ mysqli \
+ pdo_mysql \
+ pdo_pgsql \
+ intl \
+ bcmath \
+ zip \
+ gd \
+ soap
+
+# install composer
+RUN curl -s -f -L -o /tmp/installer.php https://getcomposer.org/installer \
+ && php /tmp/installer.php --no-ansi --install-dir=/usr/bin --filename=composer \
+ && composer --ansi --version --no-interaction
+
+# create src directory to store action files
+RUN mkdir -p /action/src
+
+# install Composer dependencies
+COPY composer.json /action
+RUN cd /action && /usr/bin/composer install --no-plugins --no-scripts --prefer-dist --no-dev -o && rm composer.lock
+
+# copy required files
+COPY router.php /action
+COPY php.ini /usr/local/etc/php
+
+# Run webserver on port 8080
+CMD [ "php", "-S", "0.0.0.0:8080", "/action/router.php" ]
+
diff --git a/core/php7.2Action/build.gradle b/core/php7.2Action/build.gradle
new file mode 100644
index 0000000..c168aa2
--- /dev/null
+++ b/core/php7.2Action/build.gradle
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ext.dockerImageName = 'action-php-v7.2'
+apply from: '../../gradle/docker.gradle'
diff --git a/core/php7.2Action/composer.json b/core/php7.2Action/composer.json
new file mode 100644
index 0000000..5819627
--- /dev/null
+++ b/core/php7.2Action/composer.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "platform": {
+ "php": "7.2"
+ }
+ },
+ "require": {
+ "guzzlehttp/guzzle": "6.3.3",
+ "ramsey/uuid": "3.7.3"
+ }
+}
diff --git a/core/php7.2Action/php.ini b/core/php7.2Action/php.ini
new file mode 100644
index 0000000..bee173d
--- /dev/null
+++ b/core/php7.2Action/php.ini
@@ -0,0 +1,37 @@
+; Licensed to the Apache Software Foundation (ASF) under one or more
+; contributor license agreements. See the NOTICE file distributed with
+; this work for additional information regarding copyright ownership.
+; The ASF licenses this file to You under the Apache License, Version 2.0
+; (the "License"); you may not use this file except in compliance with
+; the License. You may obtain a copy of the License at
+;
+; http://www.apache.org/licenses/LICENSE-2.0
+;
+; Unless required by applicable law or agreed to in writing, software
+; distributed under the License is distributed on an "AS IS" BASIS,
+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+; See the License for the specific language governing permissions and
+; limitations under the License.
+
+[PHP]
+short_open_tag = Off
+output_buffering = Off
+expose_php = Off
+max_execution_time = 0
+memory_limit = -1
+error_reporting = E_ALL
+display_errors = Off
+log_errors = On
+log_errors_max_len = 0
+html_errors = Off
+variables_order = "EGPCS"
+request_order = "GP"
+post_max_size = 0
+enable_dl = Off
+zend.assertions = -1
+
+[opcache]
+opcache.enable=1
+opcache.enable_cli=1
+opcache.max_accelerated_files=7963
+opcache.validate_timestamps=0
diff --git a/core/php7.2Action/router.php b/core/php7.2Action/router.php
new file mode 100644
index 0000000..6a2c570
--- /dev/null
+++ b/core/php7.2Action/router.php
@@ -0,0 +1,342 @@
+<?php
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * router.php
+ *
+ * This file is the API client for the action. The controller POSTs /init to set up the action and
+ * then POSTs to /run to invoke it.
+ */
+
+namespace OpenWhiskPhpRuntime;
+
+use RuntimeException;
+use Throwable;
+use ZipArchive;
+
+// set up an output buffer to redirect any script output to stdout, rather than the default
+// php://output, so that it goes to the logs, not the HTTP client.
+ob_start(function ($data) {
+ file_put_contents("php://stdout", $data);
+ return '';
+}, 1, PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_FLUSHABLE | PHP_OUTPUT_HANDLER_REMOVABLE);
+
+// Register a shutdown function so that we can fail gracefully when a fatal error occurs
+register_shutdown_function(function () {
+ $error = error_get_last();
+ if ($error && in_array($error["type"], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
+ $result = ['error' => 'An error occurred running the action.'];
+ $body = json_encode((object)$result);
+ header('HTTP/1.0 502 Bad Gateway');
+ header('Content-Type: application/json');
+ header("Content-Length: " . mb_strlen($body));
+
+ // write out sentinels as we've finished all log output
+ file_put_contents("php://stdout", "XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n");
+ file_put_contents("php://stderr", "XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n");
+
+ ob_end_clean();
+ echo $body;
+ exit;
+ }
+});
+
+const ACTION_SRC_FILENAME = 'index.php';
+const SRC_DIR = __DIR__ . '/src';
+const ACTION_CONFIG_FILE = __DIR__. '/config.json';
+const ACTION_SRC_FILE = SRC_DIR . '/' . ACTION_SRC_FILENAME;
+const TMP_ZIP_FILE = '/action.zip';
+
+// execute the revelant endpoint
+$result = route($_SERVER['REQUEST_URI']);
+sendResponse($result);
+exit;
+
+/**
+ * executes the relevant method for a given URL and return an array of data to send to the client
+ */
+function route(string $uri) : array
+{
+ try {
+ switch ($uri) {
+ case '/init':
+ return init();
+
+ case '/run':
+ $result = run();
+ writeSentinels();
+ return $result;
+
+ default:
+ throw new RuntimeException('Unexpected call to ' . $_SERVER["REQUEST_URI"], 500);
+ }
+ } catch (Throwable $e) {
+ $code = $e->getCode() < 400 ? 500 : $e->getCode();
+
+ if ($code != 502) {
+ writeTo("php://stdout", 'Error: ' . $e->getMessage());
+ }
+ writeSentinels();
+
+ http_response_code($code);
+ return ['error' => $e->getMessage()];
+ }
+}
+
+/**
+ * Send the response back
+ */
+function sendResponse(array $result) : void
+{
+ $body = json_encode((object)$result);
+ header('Content-Type: application/json');
+ header("Content-Length: " . mb_strlen($body));
+ ob_end_clean();
+ echo $body;
+}
+
+/**
+ * Handle the /init endpoint
+ *
+ * This end point is called once per container creation. It gives us the code we need
+ * to run and the name of the function within that code that's the entry point. As PHP
+ * has a setup/teardown model, we store the function name to a config file for retrieval
+ * in the /run end point.
+ *
+ * @return array Data to return to the client
+ */
+function init() : array
+{
+ // data is POSTed to us as a JSON string
+ $post = file_get_contents('php://input');
+ $data = json_decode($post, true)['value'] ?? [];
+
+ $name = $data['name'] ?? ''; // action name
+ $main = $data['main'] ?? 'main'; // function to call (default: main)
+ $code = trim($data['code'] ?? ''); // source code to run
+ $binary = $data['binary'] ?? false; // code is binary?
+
+ if (!$code) {
+ throw new RuntimeException("No code to execute");
+ }
+
+ if ($binary) {
+ // binary code is a zip file that's been base64 encoded, so unzip it
+ unzipString($code, SRC_DIR);
+
+ // if the zip file didn't contain a vendor directory, move our vendor into the src folder
+ if (! file_exists(SRC_DIR . '/vendor/autoload.php')) {
+ exec('mv ' . escapeshellarg(__DIR__ . '/vendor') . ' ' . escapeshellarg(SRC_DIR . '/vendor'));
+ }
+
+ // check that we have the expected action source file
+ if (! file_exists(ACTION_SRC_FILE)) {
+ throw new RuntimeException('Zipped actions must contain ' . ACTION_SRC_FILENAME . ' at the root.', 500);
+ }
+ } else {
+ // non-binary code is a text string, so save to disk
+ file_put_contents(ACTION_SRC_FILE, $code);
+
+ // move vendor folder into the src folder
+ exec('mv ' . escapeshellarg(__DIR__ . '/vendor') . ' ' . escapeshellarg(SRC_DIR . '/vendor'));
+ }
+
+ // is action file valid PHP? run `php -l` to find out
+ list($returnCode, $stdout, $stderr) = runPHP(['-l', '-f', ACTION_SRC_FILE]);
+ if ($returnCode != 0) {
+ writeTo("php://stderr", $stderr);
+ writeTo("php://stdout", $stdout);
+
+ $message = 'PHP syntax error in ' . ($binary ? ACTION_SRC_FILENAME : 'action.');
+ throw new RuntimeException($message, 500);
+ }
+
+ // does the action have the expected function name?
+ $testCode = 'require "' . ACTION_SRC_FILE . '"; exit((int)(! function_exists("' . $main .'")));';
+ list($returnCode, $stdout, $stderr) = runPHP(['-r', $testCode]);
+ if ($returnCode != 0) {
+ writeTo("php://stderr", $stderr);
+ writeTo("php://stdout", $stdout);
+ throw new RuntimeException("The function $main is missing.");
+ }
+
+ // write config file for use by /run
+ $config = [
+ 'file' => ACTION_SRC_FILE,
+ 'function' => $main,
+ 'name' => $name,
+ ];
+ file_put_contents(ACTION_CONFIG_FILE, '<?php return ' . var_export($config, true) . ';');
+
+ // reset OPcache
+ opcache_reset();
+
+ return ["OK" => true];
+}
+
+/**
+ * Handle the /run endpoint
+ *
+ * This end point is called once per action invocation. We load the function name from
+ * the config file and then invoke it. Note that as PHP writes to php://output, we
+ * capture in an output buffer and write the buffer to stdout for the OpenWhisk logs.
+ *
+ * @return array Data to return to the client
+ */
+function run() : array
+{
+ $config = require ACTION_CONFIG_FILE;
+ $_actionFile = $config['file'];
+ $_functionName = $config['function'];
+
+ // Extract the posted data
+ $post = json_decode(file_get_contents('php://input'), true);
+ if (!is_array($post)) {
+ $post = [];
+ }
+
+ // assign environment variables from the posted data
+ foreach (['api_key', 'namespace', 'action_name', 'activation_id', 'deadline'] as $param) {
+ if (array_key_exists($param, $post)) {
+ $_ENV['__OW_' . strtoupper($param)] = $post[$param];
+ }
+ }
+
+ // extract the function arguments from the posted data's "value" field
+ $args = [];
+ if (array_key_exists('value', $post) && is_array($post['value'])) {
+ $args = $post['value'];
+ }
+ $_ENV['WHISK_INPUT'] = json_encode($args);
+
+ // run the action
+ require __DIR__ . '/src/vendor/autoload.php';
+ require $_actionFile;
+ $result = $_functionName($args);
+ if (is_array($result)) {
+ return $result;
+ } elseif (is_scalar($result)) {
+ file_put_contents("php://stderr", 'Result must be an array but has type "'
+ . gettype($result) . '": ' . (string)$result . "\n");
+ file_put_contents("php://stdout", 'The action did not return a dictionary.');
+ throw new RuntimeException('The action did not return a dictionary.', 502);
+ } elseif (is_object($result)) {
+ if (method_exists($result, 'getArrayCopy')) {
+ return $result->getArrayCopy();
+ } elseif ($result instanceof \stdClass) {
+ return (array)$result;
+ }
+ }
+
+ return [];
+}
+
+/**
+ * Unzip a base64 encoded string to a directory
+ */
+function unzipString(string $b64Data, $dir): void
+{
+ file_put_contents(TMP_ZIP_FILE, base64_decode($b64Data));
+
+ $zip = new ZipArchive();
+ $res = $zip->open(TMP_ZIP_FILE);
+ if ($res !== true) {
+ $reasons = [
+ ZipArchive::ER_EXISTS => "File already exists.",
+ ZipArchive::ER_INCONS => "Zip archive inconsistent.",
+ ZipArchive::ER_INVAL => "Invalid argument.",
+ ZipArchive::ER_MEMORY => "Malloc failure.",
+ ZipArchive::ER_NOENT => "No such file.",
+ ZipArchive::ER_NOZIP => "Not a zip archive.",
+ ZipArchive::ER_OPEN => "Can't open file.",
+ ZipArchive::ER_READ => "Read error.",
+ ZipArchive::ER_SEEK => "Seek error.",
+ ];
+ $reason = $reasons[$res] ?? "Unknown error: $res.";
+ throw new RuntimeException("Failed to open zip file: $reason", 500);
+ }
+
+ $res = $zip->extractTo($dir . '/');
+ $zip->close();
+}
+
+/**
+ * Write the OpenWhisk sentinels to stdout and stderr so that it knows that we've finished
+ * writing data to them.
+ *
+ * @return void
+ */
+function writeSentinels() : void
+{
+ // write out sentinels as we've finished all log output
+ writeTo("php://stderr", "XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX");
+ writeTo("php://stdout", "XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX");
+}
+
+/**
+ * Run the PHP command in a separate process
+ *
+ * This ensures that if the action causes a fatal error, we can handle it.
+ *
+ * @param array $args arguments to the PHP executable
+ * @param string $stdin stdin to pass to the process
+ * @return array array containing [int return code, string stdout string stderr]
+ */
+function runPHP(array $args, string $stdin = '') : array
+{
+ $cmd = '/usr/local/bin/php ' . implode(' ', array_map('escapeshellarg', $args));
+
+ $process = proc_open(
+ $cmd,
+ [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ],
+ $pipes,
+ SRC_DIR
+ );
+
+ // write to the process' stdin
+ $bytes = fwrite($pipes[0], $stdin);
+ fclose($pipes[0]);
+
+ // read the process' stdout
+ $stdout = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+
+ // read the process' stderr
+ $stderr = stream_get_contents($pipes[2]);
+ fclose($pipes[2]);
+
+ // close process & get return code
+ $returnCode = proc_close($process);
+
+ // tidy up paths in any PHP stack traces
+ $stderr = str_replace(__DIR__ . '/', '', trim($stderr));
+ $stdout = str_replace(__DIR__ . '/', '', trim($stdout));
+
+ return [$returnCode, $stdout, $stderr];
+}
+
+function writeTo($pipe, $text)
+{
+ if ($text) {
+ file_put_contents($pipe, $text . PHP_EOL);
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 3802e56..39f191d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -18,6 +18,7 @@
include 'tests'
include 'core:php7.1Action'
+include 'core:php7.2Action'
rootProject.name = 'runtime-php'
diff --git a/tests/src/test/scala/runtime/actionContainers/Php71ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/Php71ActionContainerTests.scala
index d215725..03f42f3 100644
--- a/tests/src/test/scala/runtime/actionContainers/Php71ActionContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/Php71ActionContainerTests.scala
@@ -19,475 +19,9 @@
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
-import common.WskActorSystem
-import actionContainers.{ActionContainer, BasicActionRunnerTests}
-import actionContainers.ActionContainer.withContainer
-import actionContainers.ResourceHelpers.ZipBuilder
-import spray.json._
@RunWith(classOf[JUnitRunner])
-class Php71ActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
- // note: "out" will not be empty as the PHP web server outputs a message when
- // it starts up
- val enforceEmptyOutputStream = false
+class Php71ActionContainerTests extends Php7ActionContainerTests {
- lazy val php71ContainerImageName = "action-php-v7.1"
-
- override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
- withContainer(php71ContainerImageName, env)(code)
- }
-
- def withPhp71Container(code: ActionContainer => Unit) = withActionContainer()(code)
-
- behavior of php71ContainerImageName
-
- testEcho(Seq {
- (
- "PHP",
- """
- |<?php
- |function main(array $args) : array {
- | echo 'hello stdout';
- | error_log('hello stderr');
- | return $args;
- |}
- """.stripMargin)
- })
-
- testNotReturningJson("""
- |<?php
- |function main(array $args) {
- | return "not a json object";
- |}
- """.stripMargin)
-
- testUnicode(Seq {
- (
- "PHP",
- """
- |<?php
- |function main(array $args) : array {
- | $str = $args['delimiter'] . " ☃ " . $args['delimiter'];
- | echo $str . "\n";
- | return ["winter" => $str];
- |}
- """.stripMargin.trim)
- })
-
- testEnv(
- Seq {
- (
- "PHP",
- """
- |<?php
- |function main(array $args) : array {
- | return [
- | "env" => $_ENV,
- | "api_host" => $_ENV['__OW_API_HOST'],
- | "api_key" => $_ENV['__OW_API_KEY'],
- | "namespace" => $_ENV['__OW_NAMESPACE'],
- | "action_name" => $_ENV['__OW_ACTION_NAME'],
- | "activation_id" => $_ENV['__OW_ACTIVATION_ID'],
- | "deadline" => $_ENV['__OW_DEADLINE'],
- | ];
- |}
- """.stripMargin.trim)
- },
- enforceEmptyOutputStream)
-
- it should "fail to initialize with bad code" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- |<?php
- | 10 PRINT "Hello world!"
- | 20 GOTO 10
- """.stripMargin
-
- val (initCode, error) = c.init(initPayload(code))
- initCode should not be (200)
- error shouldBe a[Some[_]]
- error.get shouldBe a[JsObject]
- error.get.fields("error").toString should include("PHP syntax error")
- }
-
- // Somewhere, the logs should mention an error occurred.
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("error")
- (o + e).toLowerCase should include("syntax")
- })
- }
-
- it should "fail to initialize with no code" in {
- val (out, err) = withPhp71Container { c =>
- val code = ""
-
- val (initCode, error) = c.init(initPayload(code))
-
- initCode should not be (200)
- error shouldBe a[Some[_]]
- error.get shouldBe a[JsObject]
- error.get.fields("error").toString should include("No code to execute")
- }
- }
-
- it should "return some error on action error" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- |<?php
- | function main(array $args) : array {
- | throw new Exception ("nooooo");
- | }
- """.stripMargin
-
- val (initCode, _) = c.init(initPayload(code))
- initCode should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should not be (200)
-
- runRes shouldBe defined
- runRes.get.fields.get("error") shouldBe defined
- // runRes.get.fields("error").toString.toLowerCase should include("nooooo")
- }
-
- // Somewhere, the logs should be the error text
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("nooooo")
- })
-
- }
-
- it should "support application errors" in {
- withPhp71Container { c =>
- val code = """
- |<?php
- | function main(array $args) : array {
- | return [ "error" => "sorry" ];
- | }
- """.stripMargin;
-
- val (initCode, error) = c.init(initPayload(code))
- initCode should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should be(200) // action writer returning an error is OK
-
- runRes shouldBe defined
- runRes.get.fields.get("error") shouldBe defined
- runRes.get.fields("error").toString.toLowerCase should include("sorry")
- }
- }
-
- it should "fail gracefully when an action has a fatal error" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- | <?php
- | function main(array $args) : array {
- | eval("class Error {};");
- | return [ "hello" => "world" ];
- | }
- """.stripMargin;
-
- val (initCode, _) = c.init(initPayload(code))
- initCode should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should be(502)
-
- runRes shouldBe defined
- runRes.get.fields.get("error") shouldBe defined
- runRes.get.fields("error").toString should include("An error occurred running the action.")
- }
-
- // Somewhere, the logs should be the error text
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("fatal error")
- })
- }
-
- it should "suport returning a stdClass" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- | <?php
- | function main($params) {
- | $obj = new stdClass();
- | $obj->hello = 'world';
- | return $obj;
- | }
- """.stripMargin
-
- val (initCode, _) = c.init(initPayload(code))
- initCode should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should be(200) // action writer returning an error is OK
-
- runRes shouldBe defined
- runRes.get.fields.get("hello") shouldBe defined
- runRes.get.fields("hello").toString.toLowerCase should include("world")
- }
- }
-
- it should "support returning an object with a getArrayCopy() method" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- | <?php
- | function main($params) {
- | $obj = new ArrayObject();
- | $obj['hello'] = 'world';
- | return $obj;
- | }
- """.stripMargin
-
- val (initCode, _) = c.init(initPayload(code))
- initCode should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should be(200) // action writer returning an error is OK
-
- runRes shouldBe defined
- runRes.get.fields.get("hello") shouldBe defined
- runRes.get.fields.get("hello") shouldBe Some(JsString("world"))
- }
- }
-
- it should "support the documentation examples (1)" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- | <?php
- | function main($params) {
- | if ($params['payload'] == 0) {
- | return;
- | } else if ($params['payload'] == 1) {
- | return ['payload' => 'Hello, World!'] ; // indicates normal completion
- | } else if ($params['payload'] == 2) {
- | return ['error' => 'payload must be 0 or 1']; // indicates abnormal completion
- | }
- | }
- """.stripMargin
-
- c.init(initPayload(code))._1 should be(200)
-
- val (c1, r1) = c.run(runPayload(JsObject("payload" -> JsNumber(0))))
- val (c2, r2) = c.run(runPayload(JsObject("payload" -> JsNumber(1))))
- val (c3, r3) = c.run(runPayload(JsObject("payload" -> JsNumber(2))))
-
- c1 should be(200)
- r1 should be(Some(JsObject()))
-
- c2 should be(200)
- r2 should be(Some(JsObject("payload" -> JsString("Hello, World!"))))
-
- c3 should be(200) // application error, not container or system
- r3.get.fields.get("error") shouldBe Some(JsString("payload must be 0 or 1"))
- }
- }
-
- it should "have Guzzle and Uuid packages available" in {
- // GIVEN that it should "error when requiring a non-existent package" (see test above for this)
- val (out, err) = withPhp71Container { c =>
- val code = """
- | <?php
- | use Ramsey\Uuid\Uuid;
- | use GuzzleHttp\Client;
- | function main(array $args) {
- | Uuid::uuid4();
- | new Client();
- | }
- """.stripMargin
-
- val (initCode, _) = c.init(initPayload(code))
-
- initCode should be(200)
-
- // WHEN I run an action that calls a Guzzle & a Uuid method
- val (runCode, out) = c.run(runPayload(JsObject()))
-
- // THEN it should pass only when these packages are available
- runCode should be(200)
- }
- }
-
- it should "support large-ish actions" in {
- val thought = " I took the one less traveled by, and that has made all the difference."
- val assignment = " $x = \"" + thought + "\";\n"
-
- val code = """
- | <?php
- | function main(array $args) {
- | $x = "hello";
- """.stripMargin + (assignment * 7000) + """
- | $x = "world";
- | return [ "message" => $x ];
- | }
- """.stripMargin
-
- // Lest someone should make it too easy.
- code.length should be >= 500000
-
- val (out, err) = withPhp71Container { c =>
- c.init(initPayload(code))._1 should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
-
- runCode should be(200)
- runRes.get.fields.get("message") shouldBe defined
- runRes.get.fields.get("message") shouldBe Some(JsString("world"))
- }
- }
-
- val exampleOutputDotPhp: String = """
- | <?php
- | function output($data) {
- | return ['result' => $data];
- | }
- """.stripMargin
-
- it should "support zip-encoded packages" in {
- val srcs = Seq(
- Seq("output.php") -> exampleOutputDotPhp,
- Seq("index.php") -> """
- | <?php
- | require __DIR__ . '/output.php';
- | function main(array $args) {
- | $name = $args['name'] ?? 'stranger';
- | return output($name);
- | }
- """.stripMargin)
-
- val code = ZipBuilder.mkBase64Zip(srcs)
-
- val (out, err) = withPhp71Container { c =>
- c.init(initPayload(code))._1 should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
-
- runCode should be(200)
- runRes.get.fields.get("result") shouldBe defined
- runRes.get.fields.get("result") shouldBe Some(JsString("stranger"))
- }
- }
-
- it should "support replacing vendor in zip-encoded packages " in {
- val srcs = Seq(
- Seq("vendor/autoload.php") -> exampleOutputDotPhp,
- Seq("index.php") -> """
- | <?php
- | function main(array $args) {
- | $name = $args['name'] ?? 'stranger';
- | return output($name);
- | }
- """.stripMargin)
-
- val code = ZipBuilder.mkBase64Zip(srcs)
-
- val (out, err) = withPhp71Container { c =>
- c.init(initPayload(code))._1 should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
-
- runCode should be(200)
- runRes.get.fields.get("result") shouldBe defined
- runRes.get.fields.get("result") shouldBe Some(JsString("stranger"))
- }
- }
-
- it should "fail gracefully on invalid zip files" in {
- // Some text-file encoded to base64.
- val code = "Q2VjaSBuJ2VzdCBwYXMgdW4gemlwLgo="
-
- val (out, err) = withPhp71Container { c =>
- val (initCode, error) = c.init(initPayload(code))
- initCode should not be (200)
- error shouldBe a[Some[_]]
- error.get shouldBe a[JsObject]
- error.get.fields("error").toString should include("Failed to open zip file")
- }
-
- // Somewhere, the logs should mention the failure
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("error")
- (o + e).toLowerCase should include("failed to open zip file")
- })
- }
-
- it should "fail gracefully on valid zip files that are not actions" in {
- val srcs = Seq(Seq("hello") -> """
- | Hello world!
- """.stripMargin)
-
- val code = ZipBuilder.mkBase64Zip(srcs)
-
- val (out, err) = withPhp71Container { c =>
- c.init(initPayload(code))._1 should not be (200)
- }
-
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("error")
- (o + e).toLowerCase should include("zipped actions must contain index.php at the root.")
- })
- }
-
- it should "fail gracefully on valid zip files with invalid code in index.php" in {
- val (out, err) = withPhp71Container { c =>
- val srcs = Seq(Seq("index.php") -> """
- | <?php
- | 10 PRINT "Hello world!"
- | 20 GOTO 10
- """.stripMargin)
-
- val code = ZipBuilder.mkBase64Zip(srcs)
-
- val (initCode, error) = c.init(initPayload(code))
- initCode should not be (200)
- error shouldBe a[Some[_]]
- error.get shouldBe a[JsObject]
- error.get.fields("error").toString should include("PHP syntax error in index.php")
- }
-
- // Somewhere, the logs should mention an error occurred.
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("error")
- (o + e).toLowerCase should include("syntax")
- })
- }
-
- it should "support actions using non-default entry point" in {
- val (out, err) = withPhp71Container { c =>
- val code = """
- | <?php
- | function niam(array $args) {
- | return [result => "it works"];
- | }
- """.stripMargin
-
- c.init(initPayload(code, main = "niam"))._1 should be(200)
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
- }
- }
-
- it should "support zipped actions using non-default entry point" in {
- val srcs = Seq(Seq("index.php") -> """
- | <?php
- | function niam(array $args) {
- | return [result => "it works"];
- | }
- """.stripMargin)
-
- val code = ZipBuilder.mkBase64Zip(srcs)
-
- withPhp71Container { c =>
- c.init(initPayload(code, main = "niam"))._1 should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
- }
- }
+ override lazy val phpContainerImageName = "action-php-v7.1"
}
diff --git a/tests/src/test/scala/runtime/actionContainers/Php72ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/Php72ActionContainerTests.scala
new file mode 100644
index 0000000..7faed5e
--- /dev/null
+++ b/tests/src/test/scala/runtime/actionContainers/Php72ActionContainerTests.scala
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.actionContainers
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class Php72ActionContainerTests extends Php7ActionContainerTests {
+
+ override lazy val phpContainerImageName = "action-php-v7.2"
+}
diff --git a/tests/src/test/scala/runtime/actionContainers/Php7ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/Php7ActionContainerTests.scala
new file mode 100644
index 0000000..8ac62c7
--- /dev/null
+++ b/tests/src/test/scala/runtime/actionContainers/Php7ActionContainerTests.scala
@@ -0,0 +1,493 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.actionContainers
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import common.WskActorSystem
+import actionContainers.{ActionContainer, BasicActionRunnerTests}
+import actionContainers.ActionContainer.withContainer
+import actionContainers.ResourceHelpers.ZipBuilder
+import spray.json._
+
+@RunWith(classOf[JUnitRunner])
+abstract class Php7ActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
+ // note: "out" will not be empty as the PHP web server outputs a message when
+ // it starts up
+ val enforceEmptyOutputStream = false
+
+ lazy val phpContainerImageName = "action-php-v7.x"
+
+ override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
+ withContainer(phpContainerImageName, env)(code)
+ }
+
+ def withPhp7Container(code: ActionContainer => Unit) = withActionContainer()(code)
+
+ behavior of phpContainerImageName
+
+ testEcho(Seq {
+ (
+ "PHP",
+ """
+ |<?php
+ |function main(array $args) : array {
+ | echo 'hello stdout';
+ | error_log('hello stderr');
+ | return $args;
+ |}
+ """.stripMargin)
+ })
+
+ testNotReturningJson("""
+ |<?php
+ |function main(array $args) {
+ | return "not a json object";
+ |}
+ """.stripMargin)
+
+ testUnicode(Seq {
+ (
+ "PHP",
+ """
+ |<?php
+ |function main(array $args) : array {
+ | $str = $args['delimiter'] . " ☃ " . $args['delimiter'];
+ | echo $str . "\n";
+ | return ["winter" => $str];
+ |}
+ """.stripMargin.trim)
+ })
+
+ testEnv(
+ Seq {
+ (
+ "PHP",
+ """
+ |<?php
+ |function main(array $args) : array {
+ | return [
+ | "env" => $_ENV,
+ | "api_host" => $_ENV['__OW_API_HOST'],
+ | "api_key" => $_ENV['__OW_API_KEY'],
+ | "namespace" => $_ENV['__OW_NAMESPACE'],
+ | "action_name" => $_ENV['__OW_ACTION_NAME'],
+ | "activation_id" => $_ENV['__OW_ACTIVATION_ID'],
+ | "deadline" => $_ENV['__OW_DEADLINE'],
+ | ];
+ |}
+ """.stripMargin.trim)
+ },
+ enforceEmptyOutputStream)
+
+ it should "fail to initialize with bad code" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ |<?php
+ | 10 PRINT "Hello world!"
+ | 20 GOTO 10
+ """.stripMargin
+
+ val (initCode, error) = c.init(initPayload(code))
+ initCode should not be (200)
+ error shouldBe a[Some[_]]
+ error.get shouldBe a[JsObject]
+ error.get.fields("error").toString should include("PHP syntax error")
+ }
+
+ // Somewhere, the logs should mention an error occurred.
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("error")
+ (o + e).toLowerCase should include("syntax")
+ })
+ }
+
+ it should "fail to initialize with no code" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = ""
+
+ val (initCode, error) = c.init(initPayload(code))
+
+ initCode should not be (200)
+ error shouldBe a[Some[_]]
+ error.get shouldBe a[JsObject]
+ error.get.fields("error").toString should include("No code to execute")
+ }
+ }
+
+ it should "return some error on action error" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ |<?php
+ | function main(array $args) : array {
+ | throw new Exception ("nooooo");
+ | }
+ """.stripMargin
+
+ val (initCode, _) = c.init(initPayload(code))
+ initCode should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should not be (200)
+
+ runRes shouldBe defined
+ runRes.get.fields.get("error") shouldBe defined
+ // runRes.get.fields("error").toString.toLowerCase should include("nooooo")
+ }
+
+ // Somewhere, the logs should be the error text
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("nooooo")
+ })
+
+ }
+
+ it should "support application errors" in {
+ withPhp7Container { c =>
+ val code = """
+ |<?php
+ | function main(array $args) : array {
+ | return [ "error" => "sorry" ];
+ | }
+ """.stripMargin;
+
+ val (initCode, error) = c.init(initPayload(code))
+ initCode should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(200) // action writer returning an error is OK
+
+ runRes shouldBe defined
+ runRes.get.fields.get("error") shouldBe defined
+ runRes.get.fields("error").toString.toLowerCase should include("sorry")
+ }
+ }
+
+ it should "fail gracefully when an action has a fatal error" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ | <?php
+ | function main(array $args) : array {
+ | eval("class Error {};");
+ | return [ "hello" => "world" ];
+ | }
+ """.stripMargin;
+
+ val (initCode, _) = c.init(initPayload(code))
+ initCode should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(502)
+
+ runRes shouldBe defined
+ runRes.get.fields.get("error") shouldBe defined
+ runRes.get.fields("error").toString should include("An error occurred running the action.")
+ }
+
+ // Somewhere, the logs should be the error text
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("fatal error")
+ })
+ }
+
+ it should "suport returning a stdClass" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ | <?php
+ | function main($params) {
+ | $obj = new stdClass();
+ | $obj->hello = 'world';
+ | return $obj;
+ | }
+ """.stripMargin
+
+ val (initCode, _) = c.init(initPayload(code))
+ initCode should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(200) // action writer returning an error is OK
+
+ runRes shouldBe defined
+ runRes.get.fields.get("hello") shouldBe defined
+ runRes.get.fields("hello").toString.toLowerCase should include("world")
+ }
+ }
+
+ it should "support returning an object with a getArrayCopy() method" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ | <?php
+ | function main($params) {
+ | $obj = new ArrayObject();
+ | $obj['hello'] = 'world';
+ | return $obj;
+ | }
+ """.stripMargin
+
+ val (initCode, _) = c.init(initPayload(code))
+ initCode should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(200) // action writer returning an error is OK
+
+ runRes shouldBe defined
+ runRes.get.fields.get("hello") shouldBe defined
+ runRes.get.fields.get("hello") shouldBe Some(JsString("world"))
+ }
+ }
+
+ it should "support the documentation examples (1)" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ | <?php
+ | function main($params) {
+ | if ($params['payload'] == 0) {
+ | return;
+ | } else if ($params['payload'] == 1) {
+ | return ['payload' => 'Hello, World!'] ; // indicates normal completion
+ | } else if ($params['payload'] == 2) {
+ | return ['error' => 'payload must be 0 or 1']; // indicates abnormal completion
+ | }
+ | }
+ """.stripMargin
+
+ c.init(initPayload(code))._1 should be(200)
+
+ val (c1, r1) = c.run(runPayload(JsObject("payload" -> JsNumber(0))))
+ val (c2, r2) = c.run(runPayload(JsObject("payload" -> JsNumber(1))))
+ val (c3, r3) = c.run(runPayload(JsObject("payload" -> JsNumber(2))))
+
+ c1 should be(200)
+ r1 should be(Some(JsObject()))
+
+ c2 should be(200)
+ r2 should be(Some(JsObject("payload" -> JsString("Hello, World!"))))
+
+ c3 should be(200) // application error, not container or system
+ r3.get.fields.get("error") shouldBe Some(JsString("payload must be 0 or 1"))
+ }
+ }
+
+ it should "have Guzzle and Uuid packages available" in {
+ // GIVEN that it should "error when requiring a non-existent package" (see test above for this)
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ | <?php
+ | use Ramsey\Uuid\Uuid;
+ | use GuzzleHttp\Client;
+ | function main(array $args) {
+ | Uuid::uuid4();
+ | new Client();
+ | }
+ """.stripMargin
+
+ val (initCode, _) = c.init(initPayload(code))
+
+ initCode should be(200)
+
+ // WHEN I run an action that calls a Guzzle & a Uuid method
+ val (runCode, out) = c.run(runPayload(JsObject()))
+
+ // THEN it should pass only when these packages are available
+ runCode should be(200)
+ }
+ }
+
+ it should "support large-ish actions" in {
+ val thought = " I took the one less traveled by, and that has made all the difference."
+ val assignment = " $x = \"" + thought + "\";\n"
+
+ val code = """
+ | <?php
+ | function main(array $args) {
+ | $x = "hello";
+ """.stripMargin + (assignment * 7000) + """
+ | $x = "world";
+ | return [ "message" => $x ];
+ | }
+ """.stripMargin
+
+ // Lest someone should make it too easy.
+ code.length should be >= 500000
+
+ val (out, err) = withPhp7Container { c =>
+ c.init(initPayload(code))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+
+ runCode should be(200)
+ runRes.get.fields.get("message") shouldBe defined
+ runRes.get.fields.get("message") shouldBe Some(JsString("world"))
+ }
+ }
+
+ val exampleOutputDotPhp: String = """
+ | <?php
+ | function output($data) {
+ | return ['result' => $data];
+ | }
+ """.stripMargin
+
+ it should "support zip-encoded packages" in {
+ val srcs = Seq(
+ Seq("output.php") -> exampleOutputDotPhp,
+ Seq("index.php") -> """
+ | <?php
+ | require __DIR__ . '/output.php';
+ | function main(array $args) {
+ | $name = $args['name'] ?? 'stranger';
+ | return output($name);
+ | }
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (out, err) = withPhp7Container { c =>
+ c.init(initPayload(code))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+
+ runCode should be(200)
+ runRes.get.fields.get("result") shouldBe defined
+ runRes.get.fields.get("result") shouldBe Some(JsString("stranger"))
+ }
+ }
+
+ it should "support replacing vendor in zip-encoded packages " in {
+ val srcs = Seq(
+ Seq("vendor/autoload.php") -> exampleOutputDotPhp,
+ Seq("index.php") -> """
+ | <?php
+ | function main(array $args) {
+ | $name = $args['name'] ?? 'stranger';
+ | return output($name);
+ | }
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (out, err) = withPhp7Container { c =>
+ c.init(initPayload(code))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+
+ runCode should be(200)
+ runRes.get.fields.get("result") shouldBe defined
+ runRes.get.fields.get("result") shouldBe Some(JsString("stranger"))
+ }
+ }
+
+ it should "fail gracefully on invalid zip files" in {
+ // Some text-file encoded to base64.
+ val code = "Q2VjaSBuJ2VzdCBwYXMgdW4gemlwLgo="
+
+ val (out, err) = withPhp7Container { c =>
+ val (initCode, error) = c.init(initPayload(code))
+ initCode should not be (200)
+ error shouldBe a[Some[_]]
+ error.get shouldBe a[JsObject]
+ error.get.fields("error").toString should include("Failed to open zip file")
+ }
+
+ // Somewhere, the logs should mention the failure
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("error")
+ (o + e).toLowerCase should include("failed to open zip file")
+ })
+ }
+
+ it should "fail gracefully on valid zip files that are not actions" in {
+ val srcs = Seq(Seq("hello") -> """
+ | Hello world!
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (out, err) = withPhp7Container { c =>
+ c.init(initPayload(code))._1 should not be (200)
+ }
+
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("error")
+ (o + e).toLowerCase should include("zipped actions must contain index.php at the root.")
+ })
+ }
+
+ it should "fail gracefully on valid zip files with invalid code in index.php" in {
+ val (out, err) = withPhp7Container { c =>
+ val srcs = Seq(Seq("index.php") -> """
+ | <?php
+ | 10 PRINT "Hello world!"
+ | 20 GOTO 10
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (initCode, error) = c.init(initPayload(code))
+ initCode should not be (200)
+ error shouldBe a[Some[_]]
+ error.get shouldBe a[JsObject]
+ error.get.fields("error").toString should include("PHP syntax error in index.php")
+ }
+
+ // Somewhere, the logs should mention an error occurred.
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("error")
+ (o + e).toLowerCase should include("syntax")
+ })
+ }
+
+ it should "support actions using non-default entry point" in {
+ val (out, err) = withPhp7Container { c =>
+ val code = """
+ | <?php
+ | function niam(array $args) {
+ | return [result => "it works"];
+ | }
+ """.stripMargin
+
+ c.init(initPayload(code, main = "niam"))._1 should be(200)
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
+ }
+ }
+
+ it should "support zipped actions using non-default entry point" in {
+ val srcs = Seq(Seq("index.php") -> """
+ | <?php
+ | function niam(array $args) {
+ | return [result => "it works"];
+ | }
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ withPhp7Container { c =>
+ c.init(initPayload(code, main = "niam"))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
+ }
+ }
+}
diff --git a/tools/travis/test.sh b/tools/travis/test.sh
index 28da4fb..05d080e 100755
--- a/tools/travis/test.sh
+++ b/tools/travis/test.sh
@@ -28,7 +28,7 @@
cd ${ROOTDIR}
TERM=dumb ./gradlew :tests:checkScalafmtAll
-TERM=dumb ./gradlew :tests:test --tests *Php71*Tests
+TERM=dumb ./gradlew :tests:test