update nodejs to 8.10.0 and npm openwhisk to 3.14.0 (#31)


diff --git a/core/nodejs6Action/CHANGELOG.md b/core/nodejs6Action/CHANGELOG.md
index 079a4a2..16ceabb 100644
--- a/core/nodejs6Action/CHANGELOG.md
+++ b/core/nodejs6Action/CHANGELOG.md
@@ -1,5 +1,10 @@
 # NodeJS 6 OpenWhisk Runtime Container
 
+## 1.7.0
+Change: Update npm openwhisk package
+
+- [openwhisk v3.14.0](https://www.npmjs.com/package/openwhisk) - JavaScript client library for the OpenWhisk platform. Provides a wrapper around the OpenWhisk APIs.
+
 ## 1.6.0
 Change: Update npm openwhisk package
 
diff --git a/core/nodejs6Action/Dockerfile b/core/nodejs6Action/Dockerfile
index f8eef8c..9e6b834 100644
--- a/core/nodejs6Action/Dockerfile
+++ b/core/nodejs6Action/Dockerfile
@@ -46,7 +46,7 @@
 node-uuid@1.4.7 \
 nodemailer@2.6.4 \
 oauth2-server@2.4.1 \
-openwhisk@3.13.1 \
+openwhisk@3.14.0 \
 pkgcloud@1.4.0 \
 process@0.11.9 \
 pug@">=2.0.0-beta6 <2.0.1" \
diff --git a/core/nodejs8Action/CHANGELOG.md b/core/nodejs8Action/CHANGELOG.md
index 2ad64e9..e1b2d11 100644
--- a/core/nodejs8Action/CHANGELOG.md
+++ b/core/nodejs8Action/CHANGELOG.md
@@ -1,5 +1,12 @@
 # NodeJS 8 OpenWhisk Runtime Container
 
+## 1.4.0
+Change: Update nodejs and openwhisk npm package
+
+Node version = 8.10.0
+
+- [openwhisk v3.14.0](https://www.npmjs.com/package/openwhisk) - JavaScript client library for the OpenWhisk platform. Provides a wrapper around the OpenWhisk APIs.
+
 ## 1.3.0
 Change: Update npm openwhisk package
 
diff --git a/core/nodejs8Action/Dockerfile b/core/nodejs8Action/Dockerfile
index 43eafae..31dea72 100644
--- a/core/nodejs8Action/Dockerfile
+++ b/core/nodejs8Action/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:8.9.3
+FROM node:8.10.0
 RUN apt-get update && apt-get install -y \
     imagemagick \
     unzip \
diff --git a/core/nodejs8Action/package.json b/core/nodejs8Action/package.json
index 7eab06b..9eb2d84 100644
--- a/core/nodejs8Action/package.json
+++ b/core/nodejs8Action/package.json
@@ -8,7 +8,7 @@
   },
   "license": "Apache-2.0",
   "dependencies": {
-    "openwhisk": "3.13.1",
+    "openwhisk": "3.14.0",
     "body-parser": "1.18.2",
     "express": "4.16.2"
   }
diff --git a/tests/src/test/scala/actionContainers/ActionContainer.scala b/tests/src/test/scala/actionContainers/ActionContainer.scala
deleted file mode 100644
index 56aa131..0000000
--- a/tests/src/test/scala/actionContainers/ActionContainer.scala
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * 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 java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.PrintWriter
-
-import scala.concurrent.Await
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
-import scala.concurrent.blocking
-import scala.concurrent.duration.Duration
-import scala.concurrent.duration.DurationInt
-import scala.language.postfixOps
-import scala.sys.process.ProcessLogger
-import scala.sys.process.stringToProcess
-import scala.util.Random
-
-import org.apache.commons.lang3.StringUtils
-import org.scalatest.FlatSpec
-import org.scalatest.Matchers
-
-import akka.actor.ActorSystem
-import common.WhiskProperties
-import spray.json._
-import whisk.core.entity.Exec
-
-/**
- * For testing convenience, this interface abstracts away the REST calls to a
- * container as blocking method calls of this interface.
- */
-trait ActionContainer {
-  def init(value: JsValue): (Int, Option[JsObject])
-  def run(value: JsValue): (Int, Option[JsObject])
-}
-
-trait ActionProxyContainerTestUtils extends FlatSpec with Matchers {
-  import ActionContainer.{filterSentinel, sentinel}
-
-  def initPayload(code: String, main: String = "main") = {
-    JsObject(
-      "value" -> JsObject(
-        "code" -> { if (code != null) JsString(code) else JsNull },
-        "main" -> JsString(main),
-        "binary" -> JsBoolean(Exec.isBinaryCode(code))))
-  }
-
-  def runPayload(args: JsValue, other: Option[JsObject] = None) = {
-    JsObject(Map("value" -> args) ++ (other map { _.fields } getOrElse Map()))
-  }
-
-  def checkStreams(out: String, err: String, additionalCheck: (String, String) => Unit, sentinelCount: Int = 1) = {
-    withClue("expected number of stdout sentinels") {
-      sentinelCount shouldBe StringUtils.countMatches(out, sentinel)
-    }
-    withClue("expected number of stderr sentinels") {
-      sentinelCount shouldBe StringUtils.countMatches(err, sentinel)
-    }
-
-    val (o, e) = (filterSentinel(out), filterSentinel(err))
-    o should not include (sentinel)
-    e should not include (sentinel)
-    additionalCheck(o, e)
-  }
-}
-
-object ActionContainer {
-  private lazy val dockerBin: String = {
-    List("/usr/bin/docker", "/usr/local/bin/docker")
-      .find { bin =>
-        new File(bin).isFile()
-      }
-      .getOrElse(???) // This fails if the docker binary couldn't be located.
-  }
-
-  private lazy val dockerCmd: String = {
-    val version = WhiskProperties.getProperty("whisk.version.name")
-    // Check if we are running on docker-machine env.
-    val hostStr = if (version.toLowerCase().contains("mac")) {
-      s" --host tcp://${WhiskProperties.getMainDockerEndpoint()} "
-    } else {
-      " "
-    }
-    s"$dockerBin $hostStr"
-  }
-
-  private def docker(command: String): String = s"$dockerCmd $command"
-
-  // Runs a process asynchronously. Returns a future with (exitCode,stdout,stderr)
-  private def proc(cmd: String): Future[(Int, String, String)] = Future {
-    blocking {
-      val out = new ByteArrayOutputStream
-      val err = new ByteArrayOutputStream
-      val outW = new PrintWriter(out)
-      val errW = new PrintWriter(err)
-      val v = cmd ! (ProcessLogger(outW.println, errW.println))
-      outW.close()
-      errW.close()
-      (v, out.toString, err.toString)
-    }
-  }
-
-  // Tying it all together, we have a method that runs docker, waits for
-  // completion for some time then returns the exit code, the output stream
-  // and the error stream.
-  private def awaitDocker(cmd: String, t: Duration): (Int, String, String) = {
-    Await.result(proc(docker(cmd)), t)
-  }
-
-  // Filters out the sentinel markers inserted by the container (see relevant private code in Invoker.scala)
-  val sentinel = "XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX"
-  def filterSentinel(str: String) = str.replaceAll(sentinel, "").trim
-
-  def withContainer(imageName: String, environment: Map[String, String] = Map.empty)(code: ActionContainer => Unit)(
-    implicit actorSystem: ActorSystem): (String, String) = {
-    val rand = { val r = Random.nextInt; if (r < 0) -r else r }
-    val name = imageName.toLowerCase.replaceAll("""[^a-z]""", "") + rand
-    val envArgs = environment.toSeq.map {
-      case (k, v) => s"-e ${k}=${v}"
-    } mkString (" ")
-
-    // We create the container...
-    val runOut = awaitDocker(s"run --name $name $envArgs -d $imageName", 10 seconds)
-    assert(runOut._1 == 0, "'docker run' did not exit with 0: " + runOut)
-
-    // ...find out its IP address...
-    val ipOut = awaitDocker(s"""inspect --format '{{.NetworkSettings.IPAddress}}' $name""", 10 seconds)
-    assert(ipOut._1 == 0, "'docker inspect did not exit with 0")
-    val ip = ipOut._2.replaceAll("""[^0-9.]""", "")
-
-    // ...we create an instance of the mock container interface...
-    val mock = new ActionContainer {
-      def init(value: JsValue) = syncPost(ip, 8080, "/init", value)
-      def run(value: JsValue) = syncPost(ip, 8080, "/run", value)
-    }
-
-    try {
-      // ...and finally run the code with it.
-      code(mock)
-      // I'm told this is good for the logs.
-      Thread.sleep(100)
-      val (_, out, err) = awaitDocker(s"logs $name", 10 seconds)
-      (out, err)
-    } finally {
-      awaitDocker(s"kill $name", 10 seconds)
-      awaitDocker(s"rm $name", 10 seconds)
-    }
-  }
-
-  private def syncPost(host: String, port: Int, endPoint: String, content: JsValue): (Int, Option[JsObject]) = {
-    whisk.core.containerpool.HttpUtils.post(host, port, endPoint, content)
-  }
-
-  private class ActionContainerImpl() extends ActionContainer {
-    override def init(value: JsValue) = ???
-    override def run(value: JsValue) = ???
-  }
-}
diff --git a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala b/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
deleted file mode 100644
index 1471d12..0000000
--- a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * 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 java.io.File
-import java.util.Base64
-
-import org.apache.commons.io.FileUtils
-import org.junit.runner.RunWith
-import org.scalatest.junit.JUnitRunner
-
-import ActionContainer.withContainer
-import common.TestUtils
-import common.WskActorSystem
-import spray.json.DefaultJsonProtocol._
-import spray.json._
-
-@RunWith(classOf[JUnitRunner])
-class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSystem {
-
-  override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
-    withContainer("dockerskeleton", env)(code)
-  }
-
-  val codeNotReturningJson = """
-           |#!/bin/sh
-           |echo not a json object
-        """.stripMargin.trim
-
-  /** Standard code samples, should print 'hello' to stdout and echo the input args. */
-  val stdCodeSamples = {
-    val bash = """
-                |#!/bin/bash
-                |echo 'hello stdout'
-                |echo 'hello stderr' 1>&2
-                |if [[ -z $1 || $1 == '{}' ]]; then
-                |   echo '{ "msg": "Hello from bash script!" }'
-                |else
-                |   echo $1 # echo the arguments back as the result
-                |fi
-            """.stripMargin.trim
-
-    val python = """
-                |#!/usr/bin/env python
-                |from __future__ import print_function
-                |import sys
-                |print('hello stdout')
-                |print('hello stderr', file=sys.stderr)
-                |print(sys.argv[1])
-            """.stripMargin.trim
-
-    val perl = """
-                |#!/usr/bin/env perl
-                |print STDOUT "hello stdout\n";
-                |print STDERR "hello stderr\n";
-                |print $ARGV[0];
-            """.stripMargin.trim
-
-    // excluding perl as it not installed in alpine based image
-    Seq(("bash", bash), ("python", python))
-  }
-
-  val stdUnicodeSamples = {
-    // python 3 in base image
-    val python = """
-                |#!/usr/bin/env python
-                |import json, sys
-                |j = json.loads(sys.argv[1])
-                |sep = j["delimiter"]
-                |s = sep + " ☃ " + sep
-                |print(s)
-                |print(json.dumps({"winter": s}))
-            """.stripMargin.trim
-
-    Seq(("python", python))
-  }
-
-  /** Standard code samples, should print 'hello' to stdout and echo the input args. */
-  val stdEnvSamples = {
-    val bash = """
-                |#!/bin/bash
-                |echo "{ \
-                |\"api_host\": \"$__OW_API_HOST\", \"api_key\": \"$__OW_API_KEY\", \
-                |\"namespace\": \"$__OW_NAMESPACE\", \"action_name\": \"$__OW_ACTION_NAME\", \
-                |\"activation_id\": \"$__OW_ACTIVATION_ID\", \"deadline\": \"$__OW_DEADLINE\" }"
-            """.stripMargin.trim
-
-    val python =
-      """
-                |#!/usr/bin/env python
-                |import os
-                |
-                |print('{ "api_host": "%s", "api_key": "%s", "namespace": "%s", "action_name" : "%s", "activation_id": "%s", "deadline": "%s" }' % (
-                |  os.environ['__OW_API_HOST'], os.environ['__OW_API_KEY'],
-                |  os.environ['__OW_NAMESPACE'], os.environ['__OW_ACTION_NAME'],
-                |  os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE']))
-            """.stripMargin.trim
-
-    val perl =
-      """
-                |#!/usr/bin/env perl
-                |$a = $ENV{'__OW_API_HOST'};
-                |$b = $ENV{'__OW_API_KEY'};
-                |$c = $ENV{'__OW_NAMESPACE'};
-                |$d = $ENV{'__OW_ACTION_NAME'};
-                |$e = $ENV{'__OW_ACTIVATION_ID'};
-                |$f = $ENV{'__OW_DEADLINE'};
-                |print "{ \"api_host\": \"$a\", \"api_key\": \"$b\", \"namespace\": \"$c\", \"action_name\": \"$d\", \"activation_id\": \"$e\", \"deadline\": \"$f\" }";
-            """.stripMargin.trim
-
-    // excluding perl as it not installed in alpine based image
-    Seq(("bash", bash), ("python", python))
-  }
-
-  behavior of "openwhisk/dockerskeleton"
-
-  it should "run sample without init" in {
-    val (out, err) = withActionContainer() { c =>
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic."))))
-    }
-
-    checkStreams(out, err, {
-      case (o, _) => o should include("This is a stub action")
-    })
-  }
-
-  it should "run sample with 'null' init" in {
-    val (out, err) = withActionContainer() { c =>
-      val (initCode, _) = c.init(initPayload(null))
-      initCode should be(200)
-
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic."))))
-    }
-
-    checkStreams(out, err, {
-      case (o, _) => o should include("This is a stub action")
-    })
-  }
-
-  it should "run sample with init that does nothing" in {
-    val (out, err) = withActionContainer() { c =>
-      val (initCode, _) = c.init(JsObject())
-      initCode should be(200)
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic."))))
-    }
-
-    checkStreams(out, err, {
-      case (o, _) => o should include("This is a stub action")
-    })
-  }
-
-  it should "respond with 404 for bad run argument" in {
-    val (out, err) = withActionContainer() { c =>
-      val (runCode, out) = c.run(runPayload(JsString("A")))
-      runCode should be(404)
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        o shouldBe empty
-        e shouldBe empty
-    })
-  }
-
-  it should "fail to run a bad script" in {
-    val (out, err) = withActionContainer() { c =>
-      val (initCode, _) = c.init(initPayload(""))
-      initCode should be(200)
-      val (runCode, out) = c.run(JsNull)
-      runCode should be(502)
-      out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary."))))
-    }
-
-    checkStreams(out, err, {
-      case (o, _) => o should include("error")
-    })
-  }
-
-  it should "extract and run a compatible zip exec" in {
-    val zip = FileUtils.readFileToByteArray(new File(TestUtils.getTestActionFilename("blackbox.zip")))
-    val contents = Base64.getEncoder.encodeToString(zip)
-
-    val (out, err) = withActionContainer() { c =>
-      val (initCode, err) =
-        c.init(JsObject("value" -> JsObject("code" -> JsString(contents), "binary" -> JsBoolean(true))))
-      initCode should be(200)
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out.get should be(JsObject("msg" -> JsString("hello zip")))
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        o shouldBe "This is an example zip used with the docker skeleton action."
-        e shouldBe empty
-    })
-  }
-
-  testNotReturningJson(codeNotReturningJson, checkResultInLogs = true)
-  testEcho(stdCodeSamples)
-  testUnicode(stdUnicodeSamples)
-  testEnv(stdEnvSamples)
-}
-
-trait BasicActionRunnerTests extends ActionProxyContainerTestUtils {
-  def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit): (String, String)
-
-  /**
-   * Runs tests for actions which do not return a dictionary and confirms expected error messages.
-   * @param codeNotReturningJson code to execute, should not return a JSON object
-   * @param checkResultInLogs should be true iff the result of the action is expected to appear in stdout or stderr
-   */
-  def testNotReturningJson(codeNotReturningJson: String, checkResultInLogs: Boolean = true) = {
-    it should "run and report an error for script not returning a json object" in {
-      val (out, err) = withActionContainer() { c =>
-        val (initCode, _) = c.init(initPayload(codeNotReturningJson))
-        initCode should be(200)
-        val (runCode, out) = c.run(JsObject())
-        runCode should be(502)
-        out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary."))))
-      }
-
-      checkStreams(out, err, {
-        case (o, e) =>
-          if (checkResultInLogs) {
-            (o + e) should include("not a json object")
-          } else {
-            o shouldBe empty
-            e shouldBe empty
-          }
-      })
-    }
-  }
-
-  /**
-   * Runs tests for code samples which are expected to echo the input arguments
-   * and print hello [stdout, stderr].
-   */
-  def testEcho(stdCodeSamples: Seq[(String, String)]) = {
-    stdCodeSamples.foreach { s =>
-      it should s"run a ${s._1} script" in {
-        val argss = List(
-          JsObject("string" -> JsString("hello")),
-          JsObject("string" -> JsString("❄ ☃ ❄")),
-          JsObject("numbers" -> JsArray(JsNumber(42), JsNumber(1))),
-          // JsObject("boolean" -> JsBoolean(true)), // fails with swift3 returning boolean: 1
-          JsObject("object" -> JsObject("a" -> JsString("A"))))
-
-        val (out, err) = withActionContainer() { c =>
-          val (initCode, _) = c.init(initPayload(s._2))
-          initCode should be(200)
-
-          for (args <- argss) {
-            val (runCode, out) = c.run(runPayload(args))
-            runCode should be(200)
-            out should be(Some(args))
-          }
-        }
-
-        checkStreams(out, err, {
-          case (o, e) =>
-            o should include("hello stdout")
-            e should include("hello stderr")
-        }, argss.length)
-      }
-    }
-  }
-
-  def testUnicode(stdUnicodeSamples: Seq[(String, String)]) = {
-    stdUnicodeSamples.foreach { s =>
-      it should s"run a ${s._1} action and handle unicode in source, input params, logs, and result" in {
-        val (out, err) = withActionContainer() { c =>
-          val (initCode, _) = c.init(initPayload(s._2))
-          initCode should be(200)
-
-          val (runCode, runRes) = c.run(runPayload(JsObject("delimiter" -> JsString("❄"))))
-          runRes.get.fields.get("winter") shouldBe Some(JsString("❄ ☃ ❄"))
-        }
-
-        checkStreams(out, err, {
-          case (o, _) =>
-            o.toLowerCase should include("❄ ☃ ❄")
-        })
-      }
-    }
-  }
-
-  /** Runs tests for code samples which are expected to return the expected standard environment {auth, edge}. */
-  def testEnv(stdEnvSamples: Seq[(String, String)],
-              enforceEmptyOutputStream: Boolean = true,
-              enforceEmptyErrorStream: Boolean = true) = {
-    stdEnvSamples.foreach { s =>
-      it should s"run a ${s._1} script and confirm expected environment variables" in {
-        val props = Seq(
-          "api_host" -> "xyz",
-          "api_key" -> "abc",
-          "namespace" -> "zzz",
-          "action_name" -> "xxx",
-          "activation_id" -> "iii",
-          "deadline" -> "123")
-        val env = props.map { case (k, v) => s"__OW_${k.toUpperCase()}" -> v }
-
-        val (out, err) = withActionContainer(env.take(1).toMap) { c =>
-          val (initCode, _) = c.init(initPayload(s._2))
-          initCode should be(200)
-
-          val (runCode, out) = c.run(runPayload(JsObject(), Some(props.toMap.toJson.asJsObject)))
-          runCode should be(200)
-          out shouldBe defined
-          props.map {
-            case (k, v) =>
-              withClue(k) {
-                out.get.fields(k) shouldBe JsString(v)
-              }
-
-          }
-        }
-
-        checkStreams(out, err, {
-          case (o, e) =>
-            if (enforceEmptyOutputStream) o shouldBe empty
-            if (enforceEmptyErrorStream) e shouldBe empty
-        })
-      }
-    }
-  }
-}
diff --git a/tests/src/test/scala/actionContainers/ResourceHelpers.scala b/tests/src/test/scala/actionContainers/ResourceHelpers.scala
deleted file mode 100644
index 08f46e4..0000000
--- a/tests/src/test/scala/actionContainers/ResourceHelpers.scala
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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 java.net.URI
-import java.net.URLClassLoader
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.Paths
-import java.nio.file.SimpleFileVisitor
-import java.nio.file.FileVisitResult
-import java.nio.file.FileSystems
-import java.nio.file.attribute.BasicFileAttributes
-import java.nio.charset.StandardCharsets
-import java.util.Base64
-
-import javax.tools.ToolProvider
-
-import collection.JavaConverters._
-
-/**
- * A collection of utility objects to create ephemeral action resources based
- *  on file contents.
- */
-object ResourceHelpers {
-
-  /** Creates a zip file based on the contents of a top-level directory. */
-  object ZipBuilder {
-    def mkBase64Zip(sources: Seq[(Seq[String], String)]): String = {
-      val (tmpDir, _) = writeSourcesToTempDirectory(sources)
-      val archive = makeZipFromDir(tmpDir)
-      readAsBase64(archive)
-    }
-  }
-
-  /**
-   * A convenience object to compile and package Java sources into a JAR, and to
-   * encode that JAR as a base 64 string. The compilation options include the
-   * current classpath, which is why Google GSON is readily available (though not
-   * packaged in the JAR).
-   */
-  object JarBuilder {
-    def mkBase64Jar(sources: Seq[(Seq[String], String)]): String = {
-      // Note that this pipeline doesn't delete any of the temporary files.
-      val binDir = compile(sources)
-      val jarPath = makeJarFromDir(binDir)
-      val base64 = readAsBase64(jarPath)
-      base64
-    }
-
-    def mkBase64Jar(source: (Seq[String], String)): String = {
-      mkBase64Jar(Seq(source))
-    }
-
-    private def compile(sources: Seq[(Seq[String], String)]): Path = {
-      require(!sources.isEmpty)
-
-      // The absolute paths of the source file
-      val (srcDir, srcAbsPaths) = writeSourcesToTempDirectory(sources)
-
-      // A temporary directory for the destination files.
-      val binDir = Files.createTempDirectory("bin").toAbsolutePath()
-
-      // Preparing the compiler
-      val compiler = ToolProvider.getSystemJavaCompiler()
-      val fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)
-
-      // Collecting all files to be compiled
-      val compUnit = fileManager.getJavaFileObjectsFromFiles(srcAbsPaths.map(_.toFile).asJava)
-
-      // Setting the options
-      val compOptions = Seq("-d", binDir.toAbsolutePath().toString(), "-classpath", buildClassPath())
-      val compTask = compiler.getTask(null, fileManager, null, compOptions.asJava, null, compUnit)
-
-      // ...and off we go.
-      compTask.call()
-
-      binDir
-    }
-
-    private def buildClassPath(): String = {
-      val bcp = System.getProperty("java.class.path")
-
-      val list = this.getClass().getClassLoader() match {
-        case ucl: URLClassLoader =>
-          bcp :: ucl.getURLs().map(_.getFile().toString()).toList
-
-        case _ =>
-          List(bcp)
-      }
-
-      list.mkString(System.getProperty("path.separator"))
-    }
-  }
-
-  /**
-   * Creates a temporary directory and reproduces the desired file structure
-   * in it. Returns the path of the temporary directory and the path of each
-   * file as represented in it.
-   */
-  private def writeSourcesToTempDirectory(sources: Seq[(Seq[String], String)]): (Path, Seq[Path]) = {
-    // A temporary directory for the source files.
-    val srcDir = Files.createTempDirectory("src").toAbsolutePath()
-
-    val srcAbsPaths = for ((sourceName, sourceContent) <- sources) yield {
-      // The relative path of the source file
-      val srcRelPath = Paths.get(sourceName.head, sourceName.tail: _*)
-      // The absolute path of the source file
-      val srcAbsPath = srcDir.resolve(srcRelPath)
-      // Create parent directories if needed.
-      Files.createDirectories(srcAbsPath.getParent)
-      // Writing contents
-      Files.write(srcAbsPath, sourceContent.getBytes(StandardCharsets.UTF_8))
-
-      srcAbsPath
-    }
-
-    (srcDir, srcAbsPaths)
-  }
-
-  private def makeZipFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".zip")
-
-  private def makeJarFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".jar")
-
-  /**
-   * Compresses all files beyond a directory into a zip file.
-   * Note that Jar files are just zip files.
-   */
-  private def makeArchiveFromDir(dir: Path, extension: String): Path = {
-    // Any temporary file name for the archive.
-    val arPath = Files.createTempFile("output", extension).toAbsolutePath()
-
-    // We "mount" it as a filesystem, so we can just copy files into it.
-    val dstUri = new URI("jar:" + arPath.toUri().getScheme(), arPath.toAbsolutePath().toString(), null)
-    // OK, that's a hack. Doing this because newFileSystem wants to create that file.
-    arPath.toFile().delete()
-    val fs = FileSystems.newFileSystem(dstUri, Map(("create" -> "true")).asJava)
-
-    // Traversing all files in the bin directory...
-    Files.walkFileTree(
-      dir,
-      new SimpleFileVisitor[Path]() {
-        override def visitFile(path: Path, attributes: BasicFileAttributes) = {
-          // The path relative to the src dir
-          val relPath = dir.relativize(path)
-
-          // The corresponding path in the zip
-          val arRelPath = fs.getPath(relPath.toString())
-
-          // If this file is not top-level in the src dir...
-          if (relPath.getParent() != null) {
-            // ...create the directory structure if it doesn't exist.
-            if (!Files.exists(arRelPath.getParent())) {
-              Files.createDirectories(arRelPath.getParent())
-            }
-          }
-
-          // Finally we can copy that file.
-          Files.copy(path, arRelPath)
-
-          FileVisitResult.CONTINUE
-        }
-      })
-
-    fs.close()
-
-    arPath
-  }
-
-  /** Reads the contents of a (possibly binary) file into a base64-encoded String */
-  def readAsBase64(path: Path): String = {
-    val encoder = Base64.getEncoder()
-    new String(encoder.encode(Files.readAllBytes(path)), StandardCharsets.UTF_8)
-  }
-}
diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJs6ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJs6ActionContainerTests.scala
new file mode 100644
index 0000000..78f064e
--- /dev/null
+++ b/tests/src/test/scala/runtime/actionContainers/NodeJs6ActionContainerTests.scala
@@ -0,0 +1,28 @@
+/*
+ * 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 NodeJs6ActionContainerTests extends NodeJsActionContainerTests {
+
+  override lazy val nodejsContainerImageName = "nodejs6action"
+
+}
diff --git a/tests/src/test/scala/actionContainers/NodeJs8ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJs8ActionContainerTests.scala
similarity index 97%
rename from tests/src/test/scala/actionContainers/NodeJs8ActionContainerTests.scala
rename to tests/src/test/scala/runtime/actionContainers/NodeJs8ActionContainerTests.scala
index 8727a7f..ec57018 100644
--- a/tests/src/test/scala/actionContainers/NodeJs8ActionContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/NodeJs8ActionContainerTests.scala
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package actionContainers
+package runtime.actionContainers
 
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
diff --git a/tests/src/test/scala/actionContainers/NodeJsActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
similarity index 97%
rename from tests/src/test/scala/actionContainers/NodeJsActionContainerTests.scala
rename to tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
index be45a52..8898e22 100644
--- a/tests/src/test/scala/actionContainers/NodeJsActionContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
@@ -15,19 +15,15 @@
  * limitations under the License.
  */
 
-package actionContainers
-
-import org.junit.runner.RunWith
-import org.scalatest.junit.JUnitRunner
-
-import ActionContainer.withContainer
-import ResourceHelpers.ZipBuilder
+package runtime.actionContainers
 
 import common.WskActorSystem
+import actionContainers.{ActionContainer, BasicActionRunnerTests}
+import actionContainers.ActionContainer.withContainer
+import actionContainers.ResourceHelpers.ZipBuilder
 import spray.json._
 
-@RunWith(classOf[JUnitRunner])
-class NodeJsActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
+abstract class NodeJsActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
 
   lazy val nodejsContainerImageName = "nodejs6action"