You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2018/06/20 14:38:48 UTC

[GitHub] csantanapr closed pull request #28: Add PHP 7.2.6 runtime

csantanapr closed pull request #28: Add PHP 7.2.6 runtime
URL: https://github.com/apache/incubator-openwhisk-runtime-php/pull/28
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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 @@ wskdev fresh -t local-php
 
 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 @@ runtimes_manifest:
       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 @@ 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])
-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 @@ export OPENWHISK_HOME=$WHISKDIR
 
 cd ${ROOTDIR}
 TERM=dumb ./gradlew :tests:checkScalafmtAll
-TERM=dumb ./gradlew :tests:test --tests *Php71*Tests
+TERM=dumb ./gradlew :tests:test
 
 
 


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services