This is an automated email from the ASF dual-hosted git repository.

msciabarra pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-runtime-nodejs.git


The following commit(s) were added to refs/heads/master by this push:
     new 912ee40  Actionloop based runtime for Typescript (#160)
912ee40 is described below

commit 912ee400d7be7ca4b428049418a1b5172e60ad32
Author: Michele Sciabarra <[email protected]>
AuthorDate: Tue Feb 25 07:49:02 2020 +0100

    Actionloop based runtime for Typescript (#160)
---
 .gitignore                                         |   3 +
 core/typescript37Action/Dockerfile                 |  59 ++++++++
 core/typescript37Action/Makefile                   |  32 +++++
 core/typescript37Action/bin/compile                | 153 +++++++++++++++++++++
 .../typescript37Action/build.gradle                |  27 +---
 core/typescript37Action/lib/launcher.ts            | 101 ++++++++++++++
 settings.gradle                                    |   2 +
 tests/dat/docker/typescript37docker/Dockerfile     |  20 +++
 .../dat/docker/typescript37docker/build.gradle     |  27 +---
 .../NodeJsActionContainerTests.scala               |  70 ++++++----
 .../actionContainers/NodeJsConcurrentTests.scala   |  55 ++++----
 .../NodeJsNonConcurrentTests.scala                 |  60 ++++----
 .../actionContainers/Typescript37BasicTests.scala  |  93 +++++++++++++
 .../actionContainers/Typescript37CommonTests.scala |  31 ++---
 14 files changed, 579 insertions(+), 154 deletions(-)

diff --git a/.gitignore b/.gitignore
index d5aef40..0f904f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,9 @@ results
 !/ansible/environments/local
 !/ansible/environments/mac
 
+# actionloop action compiler script
+!core/typescript37Action/bin/compile
+
 # Eclipse
 bin/
 **/.project
diff --git a/core/typescript37Action/Dockerfile 
b/core/typescript37Action/Dockerfile
new file mode 100644
index 0000000..e7135b2
--- /dev/null
+++ b/core/typescript37Action/Dockerfile
@@ -0,0 +1,59 @@
+#
+# 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 golang:1.12 as builder
+RUN env CGO_ENABLED=0 go get github.com/apache/openwhisk-runtime-go/main \
+    && mv /go/bin/main /bin/proxy
+FROM node:12.1.0-stretch
+ENV TYPESCRIPT_VERSION=3.7.4
+COPY --from=builder /bin/proxy /bin/proxy
+RUN apt-get update && apt-get install -y \
+    imagemagick \
+    graphicsmagick \
+    unzip \
+    wget \
+    && rm -rf /var/lib/apt/lists/* &&\
+    mkdir -p /app/action
+RUN cd /app ;\
+  npm install -g yarn ;\
+  npm install -g typescript@${TYPESCRIPT_VERSION} ;\
+  echo '{"private":true}' >package.json ;\
+  npm install --save --no-package-lock --production \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  @google-cloud/[email protected] \
+  @google-cloud/[email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  [email protected] \
+  && npm cache clean --force
+WORKDIR /app
+EXPOSE 8080
+COPY bin/compile /bin/compile
+COPY lib/launcher.ts /lib/launcher.ts
+ENV OW_COMPILER=/bin/compile
+ENV OW_LOG_INIT_ERROR=1
+ENV OW_WAIT_FOR_ACK=1
+ENV OW_EXECUTION_ENV=openwhisk/typescript3.7
+ENTRYPOINT ["/bin/proxy"]
diff --git a/core/typescript37Action/Makefile b/core/typescript37Action/Makefile
new file mode 100644
index 0000000..373f831
--- /dev/null
+++ b/core/typescript37Action/Makefile
@@ -0,0 +1,32 @@
+#
+# 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.
+#
+PREFIX=whisk
+IMG=action-typescript-v3.7
+
+start:
+       docker run -p 8080:8080 -ti -v $(PWD):/mnt $(IMG)
+
+debug:
+       docker run -p 8080:8080 -p 8081:8081 -ti --entrypoint=/bin/bash \
+       -v $(PWD):/mnt -e OW_COMPILER=/mnt/bin/compile $(IMG)
+
+build:
+       docker build . -t $(IMG)
+       docker tag $(IMG) whisk/$(IMG)
+       docker images | grep $(IMG)
+
+.PHONY: start debug build
diff --git a/core/typescript37Action/bin/compile 
b/core/typescript37Action/bin/compile
new file mode 100755
index 0000000..cf62d05
--- /dev/null
+++ b/core/typescript37Action/bin/compile
@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+/*
+#
+# 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.
+#
+*/
+const path = require("path")
+const fs = require("fs")
+const execFileSync = require('child_process').execFileSync;
+
+// write a file creating intermediate directories
+function write_file(file, body, executable) {
+    fs.mkdirSync(path.dirname(file), {recursive: true})
+    fs.writeFileSync(file, body)
+    if(executable)
+        fs.chmodSync(file, 0755)
+}
+
+// copy a file eventually replacing a substring
+function copy_replace(src, dst, match, replacement) {
+    var body = fs.readFileSync(src, "utf-8")
+    if(match)
+        body = body.replace(match, replacement)
+    write_file(dst, body)
+}
+
+function deext(filename) {
+    var pos = filename.lastIndexOf(".")
+    filename = pos > -1 ? filename.substring(0, pos) : filename
+    return filename
+}
+
+// resolve dependencies from package.json - return the main file
+function dependencies(src_dir) {
+  var pkg_config = src_dir+"/package.json"
+  var node_modules = src_dir+"/node_modules"
+  if(fs.existsSync(pkg_config)) {
+    if(!fs.existsSync(node_modules))
+        execFileSync("yarn", [], {
+            "cwd": src_dir
+        })
+     var config = JSON.parse(fs.readFileSync(pkg_config, "utf-8"))
+     //console.log(config)
+     if("main" in config) {
+         return deext(config["main"])
+     }
+  }
+  return "index"
+}
+
+// assemble sources
+function sources(launcher, main_file, main_func, src_dir) {
+    // init config
+    src_config = src_dir+"/tsconfig.json"
+    var config = {}
+    if(fs.existsSync(src_config)) {
+        config = JSON.parse(fs.readFileSync(src_config, "utf-8"))
+    }
+
+    if(!("files" in config))
+        config["files"] = []
+    if(!("compilerOptions" in config))
+        config["compilerOptions"] = {}
+    config["compilerOptions"]["inlineSourceMap"] = true
+    if("sourceMap" in config["compilerOptions"]) {
+        delete config["compilerOptions"]["sourceMap"]
+    }
+    if(!("outDir" in config["compilerOptions"]))
+        config["compilerOptions"]["outDir"] = "."
+
+    // copy main src file if any (and use it as main)
+    var src_file = src_dir+"/exec"
+    var tgt_file = src_dir+"/"+main_file+".ts"
+    if(fs.existsSync(src_file) && !fs.existsSync(tgt_file)){
+        var re = RegExp('(?<!export\\s+)function\\s+'+main_func)
+        copy_replace(src_file, tgt_file, re, "export function "+main_func)
+        config["files"].push(main_file+".ts")
+    }
+
+    // copy launcher and replace main
+    copy_replace(launcher,
+       src_dir+"/exec__.ts",
+      'require("./main__").main',
+      'require("./'+main_file+'").'+main_func)
+
+    // complete tsconfig.json
+    config["files"].push("exec__.ts")
+    write_file(src_config, JSON.stringify(config))
+}
+
+function build(src_dir, bin_dir) {
+    try {
+        fs.rmdirSync(bin_dir)
+        fs.renameSync(src_dir, bin_dir)
+        execFileSync("tsc", [], {
+            "cwd": bin_dir
+        })
+        write_file(bin_dir+"/exec",
+         '#!/bin/bash\n'+
+         'if [ "$(cat $0.env)" != "$__OW_EXECUTION_ENV" ]\n'+
+         'then cd "$(dirname $0)"\n'+
+         '     echo "Execution Environment Mismatch"\n'+
+         '     echo "Expected: $(cat $0.env)"\n'+
+         '     echo "Actual: $__OW_EXECUTION_ENV"\n'+
+         '     exit 1\n'+
+         'fi\n'+
+         'cd "$(dirname $0)"\n'+
+         'if [ -z "$__OW_DEBUG_PORT" ]\n' +
+         'then node exec__.js\n'+
+         'else node --inspect=":$__OW_DEBUG_PORT" exec__.js\n'+
+         'fi\n', true)
+         write_file(bin_dir+"/exec.env", process.env["__OW_EXECUTION_ENV"])
+    } catch(err) {
+        console.log("syntax error:", err.message)
+    }
+}
+
+function compile() {
+    if(process.argv.length<4) {
+        console.log("usage: <main-function> <source-dir> <target-dir>")
+        process.exit(1)
+    }
+    var launcher =  
path.dirname(path.dirname(process.argv[1]))+"/lib/launcher.ts"
+    var src_dir = path.resolve(process.argv[3])
+    var bin_dir = path.resolve(process.argv[4])
+    var main_func = process.argv[2]
+    var main_file = dependencies(src_dir)
+    var pieces =  main_func.split(".")
+    if(pieces.length >1) {
+        main_file = pieces.shift()
+        main_func = pieces.join(".")
+    }
+    //console.log(main_file, main_func)
+    sources(launcher, main_file, main_func, src_dir)
+    build(src_dir, bin_dir)
+}
+
+if(require.main === module) {
+    compile()
+}
diff --git a/settings.gradle b/core/typescript37Action/build.gradle
similarity index 56%
copy from settings.gradle
copy to core/typescript37Action/build.gradle
index da79f78..0476518 100644
--- a/settings.gradle
+++ b/core/typescript37Action/build.gradle
@@ -15,28 +15,5 @@
  * limitations under the License.
  */
 
-include 'tests'
-
-include 'core:nodejsActionBase'
-include 'core:nodejs8Action'
-include 'core:nodejs10Action'
-include 'core:nodejs12Action'
-include 'tests:dat:docker:nodejs8docker'
-include 'tests:dat:docker:nodejs10docker'
-include 'tests:dat:docker:nodejs12docker'
-
-rootProject.name = 'runtime-nodejs'
-
-gradle.ext.openwhisk = [
-        version: '1.0.0-SNAPSHOT'
-]
-
-gradle.ext.scala = [
-    version: '2.12.7',
-    compileFlags: ['-feature', '-unchecked', '-deprecation', 
'-Xfatal-warnings', '-Ywarn-unused-import']
-]
-
-gradle.ext.scalafmt = [
-    version: '1.5.0',
-    config: new File(rootProject.projectDir, '.scalafmt.conf')
-]
+ext.dockerImageName = 'action-typescript-v3.7'
+apply from: '../../gradle/docker.gradle'
diff --git a/core/typescript37Action/lib/launcher.ts 
b/core/typescript37Action/lib/launcher.ts
new file mode 100644
index 0000000..a575952
--- /dev/null
+++ b/core/typescript37Action/lib/launcher.ts
@@ -0,0 +1,101 @@
+/*
+#
+# 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.
+#
+*/
+try {
+const main = require("./main__").main
+const readline = require('readline');
+const fs = require("fs")
+const os = require("os")
+
+function vscodeDebug() {
+  let ifaces = os.networkInterfaces()
+  for(let iface of Object.keys(ifaces)) {
+    for(let ip of ifaces[iface]) {
+      if(!ip.internal) {
+        return {
+          "type": "node",
+          "request": "attach",
+          "name": process.env["__OW_ACTION_NAME"],
+          "address": ip.address,
+          "port": 8081,
+          "localRoot": "${workspaceFolder}",
+          "remoteRoot": __dirname
+        }
+      }
+    }
+  }
+  return {"error": "cannot find external interface"}
+}
+
+async function actionLoop() {
+  const out = fs.createWriteStream(null,
+    { fd: 3, encoding: "utf8" })
+    process.stdin.setEncoding('utf8');
+  const rl = readline.createInterface({
+    input: process.stdin
+  });
+  const debugging = "__OW_DEBUG_PORT" in process.env
+  out.write(JSON.stringify({"ok":true})+"\n");
+  for await (const line of rl) {
+    try {
+      let args = JSON.parse(line)
+      let value = args.value || {}
+      for (let key in args) {
+          if(key !== "value") {
+            let envar = "__OW_"+key.toUpperCase()
+            process.env[envar] = args[key]
+          }
+      }
+      let result = {}
+      if(debugging && "debugWith" in value) {
+        if(value["debugWith"]==="vscode")
+          result = vscodeDebug()
+        else
+          result = {"error": "requested unknown debugger"}
+      } else {
+        result = main(value)
+        if(typeof result === 'undefined') {
+          result = {}
+        }
+        if(Promise.resolve(result) == result) 
+          try {
+            result = await result
+          } catch(error) {
+            if(typeof error === 'undefined') {
+              error = {}
+            }
+            result = {"error": error }
+          }
+      }
+      out.write(JSON.stringify(result)+"\n");
+    } catch(err) {
+      console.log(err);
+      let message = err.message || err.toString()
+      let error = {"error": message}
+      out.write(JSON.stringify(error)+"\n");
+    }
+  }
+}
+actionLoop()
+} catch(e) {
+    if(e.code == "MODULE_NOT_FOUND") {
+        console.log("zipped actions must contain either package.json or 
index.js at the root.")
+    } 
+    console.log(e)
+    process.exit(1)
+}
diff --git a/settings.gradle b/settings.gradle
index da79f78..d688219 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -21,9 +21,11 @@ include 'core:nodejsActionBase'
 include 'core:nodejs8Action'
 include 'core:nodejs10Action'
 include 'core:nodejs12Action'
+include 'core:typescript37Action'
 include 'tests:dat:docker:nodejs8docker'
 include 'tests:dat:docker:nodejs10docker'
 include 'tests:dat:docker:nodejs12docker'
+include 'tests:dat:docker:typescript37docker'
 
 rootProject.name = 'runtime-nodejs'
 
diff --git a/tests/dat/docker/typescript37docker/Dockerfile 
b/tests/dat/docker/typescript37docker/Dockerfile
new file mode 100644
index 0000000..b6b31ec
--- /dev/null
+++ b/tests/dat/docker/typescript37docker/Dockerfile
@@ -0,0 +1,20 @@
+#
+# 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 action-typescript-v3.7
+RUN cd /app && \
+    npm add --save [email protected] && \
+    npm install --production
diff --git a/settings.gradle b/tests/dat/docker/typescript37docker/build.gradle
similarity index 56%
copy from settings.gradle
copy to tests/dat/docker/typescript37docker/build.gradle
index da79f78..0d894ee 100644
--- a/settings.gradle
+++ b/tests/dat/docker/typescript37docker/build.gradle
@@ -15,28 +15,5 @@
  * limitations under the License.
  */
 
-include 'tests'
-
-include 'core:nodejsActionBase'
-include 'core:nodejs8Action'
-include 'core:nodejs10Action'
-include 'core:nodejs12Action'
-include 'tests:dat:docker:nodejs8docker'
-include 'tests:dat:docker:nodejs10docker'
-include 'tests:dat:docker:nodejs12docker'
-
-rootProject.name = 'runtime-nodejs'
-
-gradle.ext.openwhisk = [
-        version: '1.0.0-SNAPSHOT'
-]
-
-gradle.ext.scala = [
-    version: '2.12.7',
-    compileFlags: ['-feature', '-unchecked', '-deprecation', 
'-Xfatal-warnings', '-Ywarn-unused-import']
-]
-
-gradle.ext.scalafmt = [
-    version: '1.5.0',
-    config: new File(rootProject.projectDir, '.scalafmt.conf')
-]
+ext.dockerImageName = 'typescript37docker'
+apply from: '../../../../gradle/docker.gradle'
diff --git 
a/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
 
b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
index 32fdd6e..4122309 100644
--- 
a/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
+++ 
b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
@@ -29,6 +29,7 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
 
   val nodejsContainerImageName: String
   val nodejsTestDockerImageName: String
+  val isTypeScript = false
 
   override def withActionContainer(env: Map[String, String] = Map.empty)(code: 
ActionContainer => Unit) = {
     withContainer(nodejsContainerImageName, env)(code)
@@ -137,7 +138,7 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
           | 20 GOTO 10
         """.stripMargin
 
-      val (initCode, _) = c.init(initPayload(code))
+      val (initCode, res) = c.init(initPayload(code))
 
       initCode should not be (200)
     }
@@ -176,7 +177,10 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
       initCode should be(200)
 
       val (runCode, runRes) = c.run(runPayload(JsObject()))
-      runCode should not be (200)
+      // actionloop proxy does not return a different error code when there is 
an error,
+      // because it communicates only through json
+      if (!isTypeScript)
+        runCode should not be (200)
 
       runRes shouldBe defined
       runRes.get.fields.get("error") shouldBe defined
@@ -293,18 +297,21 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
           | }
         """.stripMargin
 
-      c.init(initPayload(code))._1 should be(200)
-
-      val (runCode, result) = c.run(runPayload(JsObject("payload" -> 
JsString("test"))))
-      runCode should be(200)
-      result should be(Some(JsObject("payload" -> JsString("hello, test!"))))
+      if (isTypeScript)
+        c.init(initPayload(code))._1 should be(502)
+      else {
+        c.init(initPayload(code))._1 should be(200)
+        val (runCode, result) = c.run(runPayload(JsObject("payload" -> 
JsString("test"))))
+        runCode should be(200)
+        result should be(Some(JsObject("payload" -> JsString("hello, test!"))))
+      }
     }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        o shouldBe "hello, test!"
-        e shouldBe empty
-    })
+    if (!isTypeScript)
+      checkStreams(out, err, {
+        case (o, e) =>
+          o shouldBe "hello, test!"
+          e shouldBe empty
+      })
   }
 
   it should "support webpacked function" in {
@@ -317,18 +324,24 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
           |global.main = foo
         """.stripMargin
 
-      c.init(initPayload(code))._1 should be(200)
+      if (isTypeScript) {
+        c.init(initPayload(code))._1 should be(502)
+      } else {
+        c.init(initPayload(code))._1 should be(200)
 
-      val (runCode, result) = c.run(JsObject.empty)
-      runCode should be(200)
-      result should be(Some(JsObject("bar" -> JsTrue)))
+        val (runCode, result) = c.run(JsObject.empty)
+        runCode should be(200)
+        result should be(Some(JsObject("bar" -> JsTrue)))
+      }
     }
 
-    checkStreams(out, err, {
-      case (o, e) =>
-        o shouldBe empty
-        e shouldBe empty
-    })
+    if (!isTypeScript) {
+      checkStreams(out, err, {
+        case (o, e) =>
+          o shouldBe empty
+          e shouldBe empty
+      })
+    }
   }
 
   it should "error when requiring a non-existent package" in {
@@ -349,12 +362,16 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
 
       val (runCode, out) = c.run(runPayload(JsObject()))
 
-      runCode should not be (200)
+      if (isTypeScript)
+        out.get.fields should contain key ("error")
+      else
+        runCode should not be (200)
+
     }
 
     // Somewhere, the logs should mention an error occurred.
     checkStreams(out, err, {
-      case (o, e) => (o + e) should include("MODULE_NOT_FOUND")
+      case (o, e) => (o + e) should include regex ("Error|error")
     })
   }
 
@@ -365,6 +382,7 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
         """
           | function main(args) {
           |     require('openwhisk');
+          |     return { "result": true }
           | }
         """.stripMargin
 
@@ -672,7 +690,7 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
     checkStreams(out, err, {
       case (o, e) =>
         (o + e).toLowerCase should include("error")
-        (o + e).toLowerCase should include("uncompressing")
+        (o + e).toLowerCase should include regex ("syntax|uncompressing")
     })
   }
 
@@ -691,7 +709,7 @@ abstract class NodeJsActionContainerTests extends 
BasicActionRunnerTests with Ws
 
     checkStreams(out, err, {
       case (o, e) =>
-        (o + e).toLowerCase should include("error")
+        (o + e).toLowerCase should include regex ("error|exited")
         (o + e).toLowerCase should include("zipped actions must contain either 
package.json or index.js at the root.")
     })
   }
diff --git 
a/tests/src/test/scala/runtime/actionContainers/NodeJsConcurrentTests.scala 
b/tests/src/test/scala/runtime/actionContainers/NodeJsConcurrentTests.scala
index efc294e..47fc858 100644
--- a/tests/src/test/scala/runtime/actionContainers/NodeJsConcurrentTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/NodeJsConcurrentTests.scala
@@ -28,18 +28,19 @@ abstract class NodeJsConcurrentTests extends 
NodeJsActionContainerTests {
   override def withNodeJsContainer(code: ActionContainer => Unit) =
     withActionContainer(Map("__OW_ALLOW_CONCURRENT" -> "true"))(code)
 
-  it should "allow running activations concurrently" in {
+  if (!isTypeScript) {
+    it should "allow running activations concurrently" in {
 
-    val requestCount = 
actorSystem.settings.config.getInt("akka.http.host-connection-pool.max-connections")
-    require(requestCount > 100, "test requires that max-connections be set > 
100")
-    println(s"running $requestCount requests")
+      val requestCount = 
actorSystem.settings.config.getInt("akka.http.host-connection-pool.max-connections")
+      require(requestCount > 100, "test requires that max-connections be set > 
100")
+      println(s"running $requestCount requests")
 
-    val (out, err) = withNodeJsContainer { c =>
-      //this action will create a log entry, and only complete once all 
activations have arrived and emitted logg
-      //this forces all of the in-action logs to appear in a single portion of 
the stdout, and all of the sentinels to appear following that
+      val (out, err) = withNodeJsContainer { c =>
+        //this action will create a log entry, and only complete once all 
activations have arrived and emitted logg
+        //this forces all of the in-action logs to appear in a single portion 
of the stdout, and all of the sentinels to appear following that
 
-      val code =
-        s"""
+        val code =
+          s"""
            |
            | global.count = 0;
            | let requestCount = $requestCount;
@@ -69,29 +70,29 @@ abstract class NodeJsConcurrentTests extends 
NodeJsActionContainerTests {
            | }
         """.stripMargin
 
-      c.init(initPayload(code))._1 should be(200)
+        c.init(initPayload(code))._1 should be(200)
 
-      val payloads = (1 to requestCount).map({ i =>
-        JsObject(s"arg$i" -> JsString(s"value$i"))
-      })
+        val payloads = (1 to requestCount).map({ i =>
+          JsObject(s"arg$i" -> JsString(s"value$i"))
+        })
 
-      val responses = c.runMultiple(payloads.map {
-        runPayload(_)
-      })
-      payloads.foreach { a =>
-        responses should contain(200, Some(JsObject("args" -> a)))
+        val responses = c.runMultiple(payloads.map {
+          runPayload(_)
+        })
+        payloads.foreach { a =>
+          responses should contain(200, Some(JsObject("args" -> a)))
+        }
       }
-    }
 
-    checkStreams(out, err, {
-      case (o, e) =>
-        o.replaceAll("\n", "") shouldBe "interleave me" * requestCount
-        e shouldBe empty
-    }, requestCount)
+      checkStreams(out, err, {
+        case (o, e) =>
+          o.replaceAll("\n", "") shouldBe "interleave me" * requestCount
+          e shouldBe empty
+      }, requestCount)
 
-    withClue("expected grouping of stdout sentinels") {
-      out should include((ActionContainer.sentinel + "\n") * requestCount)
+      withClue("expected grouping of stdout sentinels") {
+        out should include((ActionContainer.sentinel + "\n") * requestCount)
+      }
     }
   }
-
 }
diff --git 
a/tests/src/test/scala/runtime/actionContainers/NodeJsNonConcurrentTests.scala 
b/tests/src/test/scala/runtime/actionContainers/NodeJsNonConcurrentTests.scala
index 85a4645..a63a523 100644
--- 
a/tests/src/test/scala/runtime/actionContainers/NodeJsNonConcurrentTests.scala
+++ 
b/tests/src/test/scala/runtime/actionContainers/NodeJsNonConcurrentTests.scala
@@ -27,9 +27,10 @@ abstract class NodeJsNonConcurrentTests extends 
NodeJsActionContainerTests {
     withActionContainer()(code)
 
   it should "NOT allow running activations concurrently (without proper env 
setup)" in {
-    val (out, err) = withNodeJsContainer { c =>
-      //this action will create a log entry, and only complete after 1s, to 
guarantee previous is still running
-      val code = """
+    if (!isTypeScript) {
+      val (out, err) = withNodeJsContainer { c =>
+        //this action will create a log entry, and only complete after 1s, to 
guarantee previous is still running
+        val code = """
                    | function main(args) {
                    |     console.log("no concurrency");
                    |     return new Promise(function(resolve, reject) {
@@ -40,38 +41,39 @@ abstract class NodeJsNonConcurrentTests extends 
NodeJsActionContainerTests {
                    | }
                  """.stripMargin
 
-      c.init(initPayload(code))._1 should be(200)
-      val requestCount = 2
+        c.init(initPayload(code))._1 should be(200)
+        val requestCount = 2
 
-      val payloads = (1 to requestCount).map({ i =>
-        JsObject(s"arg$i" -> JsString(s"value$i"))
-      })
+        val payloads = (1 to requestCount).map({ i =>
+          JsObject(s"arg$i" -> JsString(s"value$i"))
+        })
 
-      //run payloads concurrently
-      val responses = c.runMultiple(payloads.map {
-        runPayload(_)
-      })
+        //run payloads concurrently
+        val responses = c.runMultiple(payloads.map {
+          runPayload(_)
+        })
 
-      //one will fail, one will succeed - currently there is no way to 
guarantee which one succeeds, since both arrive "at the same time"
-      responses.count {
-        case (200, Some(JsObject(a))) if a.get("args").isDefined => true
-        case _                                                   => false
-      } shouldBe 1
+        //one will fail, one will succeed - currently there is no way to 
guarantee which one succeeds, since both arrive "at the same time"
+        responses.count {
+          case (200, Some(JsObject(a))) if a.get("args").isDefined => true
+          case _                                                   => false
+        } shouldBe 1
 
-      responses.count {
-        case (403, Some(JsObject(e)))
-            if e.getOrElse("error", JsString("")) == JsString("System not 
ready, status is running.") =>
-          true
-        case _ => false
-      } shouldBe 1
+        responses.count {
+          case (403, Some(JsObject(e)))
+              if e.getOrElse("error", JsString("")) == JsString("System not 
ready, status is running.") =>
+            true
+          case _ => false
+        } shouldBe 1
 
-    }
+      }
 
-    checkStreams(out, err, {
-      case (o, e) =>
-        o.replaceAll("\n", "") shouldBe "no concurrency"
-        e shouldBe "Internal system error: System not ready, status is 
running."
-    }, 1)
+      checkStreams(out, err, {
+        case (o, e) =>
+          o.replaceAll("\n", "") shouldBe "no concurrency"
+          e shouldBe "Internal system error: System not ready, status is 
running."
+      }, 1)
+    }
   }
 
 }
diff --git 
a/tests/src/test/scala/runtime/actionContainers/Typescript37BasicTests.scala 
b/tests/src/test/scala/runtime/actionContainers/Typescript37BasicTests.scala
new file mode 100644
index 0000000..4006cde
--- /dev/null
+++ b/tests/src/test/scala/runtime/actionContainers/Typescript37BasicTests.scala
@@ -0,0 +1,93 @@
+/*
+ * 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 actionContainers.ActionContainer.withContainer
+import actionContainers.{ActionContainer, BasicActionRunnerTests}
+import common.WskActorSystem
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class Typescript37BasicTests extends BasicActionRunnerTests with 
WskActorSystem {
+
+  val image = "action-typescript-v3.7"
+
+  override def withActionContainer(env: Map[String, String] = Map.empty)(code: 
ActionContainer => Unit) = {
+    withContainer(image, env)(code)
+  }
+
+  def withActionLoopContainer(code: ActionContainer => Unit) =
+    withContainer(image)(code)
+
+  behavior of image
+
+  override val testNoSourceOrExec = TestConfig("")
+
+  override val testNotReturningJson =
+    TestConfig("""
+                 |export function main(args) {
+                 |    return "not a json object"
+                 |}
+               """.stripMargin)
+
+  override val testEcho = TestConfig("""|export function main(args) {
+       |   console.log("hello stdout")
+       |   console.error("hello stderr")
+       |   return args
+       |}
+    """.stripMargin)
+
+  override val testUnicode = TestConfig("""|export function main(args) {
+       |  let delimiter = args['delimiter']
+       |  let msg = delimiter+" ☃ "+delimiter
+       |  console.log(msg)
+       |  return { "winter": msg }
+       |}
+    """.stripMargin)
+
+  override val testEnv = TestConfig("""|export function main(args) {
+       |  let env = process.env
+       |  return {
+       |    "api_host":      env["__OW_API_HOST"],
+       |    "api_key":       env["__OW_API_KEY"],
+       |    "namespace":     env["__OW_NAMESPACE"],
+       |    "activation_id": env["__OW_ACTIVATION_ID"],
+       |    "action_name":   env["__OW_ACTION_NAME"],
+       |    "deadline":      env["__OW_DEADLINE"],
+       |    "action_version": env["__OW_ACTION_VERSION"]
+       |  }
+       |}
+    """.stripMargin)
+
+  override val testInitCannotBeCalledMoreThanOnce = TestConfig(s"""|export 
function main(args) {
+        |  return args
+        |}
+    """.stripMargin)
+
+  override val testEntryPointOtherThanMain = TestConfig(
+    s"""|export function niam(args) {
+        |   return args
+        |}
+    """.stripMargin,
+    main = "niam")
+
+  override val testLargeInput = TestConfig(s"""|export function main(args) {
+        |  return args
+        |}
+    """.stripMargin)
+}
diff --git a/settings.gradle 
b/tests/src/test/scala/runtime/actionContainers/Typescript37CommonTests.scala
similarity index 56%
copy from settings.gradle
copy to 
tests/src/test/scala/runtime/actionContainers/Typescript37CommonTests.scala
index da79f78..e39e246 100644
--- a/settings.gradle
+++ 
b/tests/src/test/scala/runtime/actionContainers/Typescript37CommonTests.scala
@@ -15,28 +15,15 @@
  * limitations under the License.
  */
 
-include 'tests'
+package runtime.actionContainers
 
-include 'core:nodejsActionBase'
-include 'core:nodejs8Action'
-include 'core:nodejs10Action'
-include 'core:nodejs12Action'
-include 'tests:dat:docker:nodejs8docker'
-include 'tests:dat:docker:nodejs10docker'
-include 'tests:dat:docker:nodejs12docker'
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
 
-rootProject.name = 'runtime-nodejs'
+@RunWith(classOf[JUnitRunner])
+class Typescript37CommonTests extends NodeJsNonConcurrentTests {
 
-gradle.ext.openwhisk = [
-        version: '1.0.0-SNAPSHOT'
-]
-
-gradle.ext.scala = [
-    version: '2.12.7',
-    compileFlags: ['-feature', '-unchecked', '-deprecation', 
'-Xfatal-warnings', '-Ywarn-unused-import']
-]
-
-gradle.ext.scalafmt = [
-    version: '1.5.0',
-    config: new File(rootProject.projectDir, '.scalafmt.conf')
-]
+  override lazy val nodejsContainerImageName = "action-typescript-v3.7"
+  override lazy val nodejsTestDockerImageName = "typescript37docker"
+  override val isTypeScript = true
+}

Reply via email to