diff --git a/.travis.yml b/.travis.yml
index 52d92d7..74beae5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,11 @@ scala:
- 2.12.7
services:
- docker
+# required to support multi-stage build
+addons:
+ apt:
+ packages:
+ - docker-ce
before_install:
- "./tools/travis/setup.sh"
install: true
@@ -21,7 +26,7 @@ deploy:
all_branches: true
repo: apache/incubator-openwhisk-runtime-python
- provider: script
- script: "./tools/travis/publish.sh openwhisk 2 latest &&
./tools/travis/publish.sh openwhisk 3 latest && ./tools/travis/publish.sh
openwhisk 3-ai latest"
+ script: "./tools/travis/publish.sh openwhisk 2 latest &&
./tools/travis/publish.sh openwhisk 3 latest && ./tools/travis/publish.sh
openwhisk 3-ai latest && ./tools/travis/publish.sh openwhisk 3-loop latest"
on:
branch: master
repo: apache/incubator-openwhisk-runtime-python
@@ -39,3 +44,4 @@ notifications:
urls:
# travis2slack webhook to enable DMs on openwhisk-team.slack.com to PR
authors with TravisCI results
secure:
"jhiMGpQ6kJFWjjsO68RmgD2Lga7jgNE+EKwND0dMOvzf5llMLFDKcY5J3tgtrqYaslQdXeuYeru/9qJrTTjFEu+vz3iCwoJ/eme+D0TtTIFGlPr7oa9tZlWrkPM/0zFLq7KjJauIIX2+6qrGVrNJJ6ENfr4U8Ir8q51oLIk44bsCeB8EmkahPOlNG6kcNqgpxHWKYUdUIg3B0GxqCKida/76dXDTRHCV2dZuT2bXz2oSJYog/lybomsjQIUZj0+HqxecgWTzag3Y6rTpK+m+vywazHP91hE+oU4e7YrxCH6v9+ukoWaljFqO5ZEKXcpx6tzx8Q0FvoTP8vGOO9b/t1loVcA8OxSJDrtOAztfoz/u0HJN6vnVt+maqnrYAD1F4pxA63JA6/+a7firmtADP7A/WQMZg6RgEkGUr+amFn303dTvgjDDkZ4oH8MAr0EPsneGUA2MZgB3i1MEcnCrYzT7KpYmDmFLoFhS9OX8f1H3zi5DLZZbZ1jbW/Ay4BgvjdoC8vmhAsDfVvyY9P240+nQ9NrnjaAUMD4XI/6JAKekfoxvsnc9W8gKBGTNfzi55AVe7HbzB/wCd58c2CV3Ev3RRwKQpH67jLBROpg2ocRQr0BUeHmfOT7NV4BCqdw1eVkZWBw4oxVaCHelDdICwgPn696W5t/1UVl4tLTt1rk="
+
diff --git a/core/pythonActionLoop/Dockerfile b/core/pythonActionLoop/Dockerfile
new file mode 100644
index 0000000..b2daade
--- /dev/null
+++ b/core/pythonActionLoop/Dockerfile
@@ -0,0 +1,42 @@
+#
+# 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 openwhisk/actionloop:latest as builder
+
+FROM python:3.7-stretch
+
+# Install common modules for python
+RUN pip install \
+ beautifulsoup4==4.6.3 \
+ httplib2==0.11.3 \
+ kafka_python==1.4.3 \
+ lxml==4.2.5 \
+ python-dateutil==2.7.3 \
+ requests==2.19.1 \
+ scrapy==1.5.1 \
+ simplejson==3.16.0 \
+ virtualenv==16.0.0 \
+ twisted==18.7.0
+
+RUN mkdir -p /action
+WORKDIR /
+COPY --from=builder /bin/proxy /bin/proxy
+ADD pythonbuild.py /bin/compile
+ADD pythonbuild.py.launcher.py /bin/compile.launcher.py
+ENV OW_COMPILER=/bin/compile
+ENTRYPOINT []
+CMD ["/bin/proxy"]
+
diff --git a/core/pythonActionLoop/build.gradle
b/core/pythonActionLoop/build.gradle
new file mode 100644
index 0000000..2e4226b
--- /dev/null
+++ b/core/pythonActionLoop/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 = 'actionloop-python-v3.7'
+apply from: '../../gradle/docker.gradle'
diff --git a/core/pythonActionLoop/pythonbuild.py
b/core/pythonActionLoop/pythonbuild.py
new file mode 100755
index 0000000..30802c2
--- /dev/null
+++ b/core/pythonActionLoop/pythonbuild.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+"""Python Action Compiler
+#
+# 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 __future__ import print_function
+import os
+import sys
+import codecs
+import subprocess
+
+
+def copy(src, dst):
+ with codecs.open(src, 'r', 'utf-8') as s:
+ body = s.read()
+ with codecs.open(dst, 'w', 'utf-8') as d:
+ d.write(body)
+
+# if there is an exec copy to main__.py
+# else if there is a __main__.py copy to main__.py
+# (exec prevails over __main__.py)
+# then copy the launcher in exec__.py replacing the main function
+def sources(launcher, source_dir, main):
+ # source and dest
+ src = "%s/exec" % source_dir
+ dst = "%s/main__.py" % source_dir
+ # copy exec to main__.py
+ if os.path.isfile(src):
+ copy(src,dst)
+ else:
+ # renaming __main__ to main__
+ src = "%s/__main__.py" % source_dir
+ if os.path.isfile(src):
+ copy(src, dst)
+
+ # copy a launcher
+ starter = "%s/exec__.py" % source_dir
+ with codecs.open(launcher, 'r', 'utf-8') as s:
+ with codecs.open(starter, 'w', 'utf-8') as d:
+ body = s.read()
+ body = body.replace("from main__ import main as main",
+ "from main__ import %s as main" % main)
+ d.write(body)
+ return starter
+
+# build the launcher but only if there is the main
+def build(source_dir, target_file, launcher):
+ main = "%s/main__.py" % source_dir
+ cmd = "#!/bin/bash"
+ if os.path.isfile(main):
+ cmd += """
+cd %s
+exec python %s "$@"
+""" % (source_dir, launcher)
+ else:
+ cmd += """
+echo "Zip file does not include mandatory files."
+"""
+ with codecs.open(target_file, 'w', 'utf-8') as d:
+ d.write(cmd)
+ os.chmod(target_file, 0o755)
+
+def compile(argv):
+ if len(argv) < 4:
+ sys.stdout.write("usage: <main-function> <source-dir> <target-dir>\n")
+ sys.exit(1)
+
+ main = argv[1]
+ source_dir = os.path.abspath(argv[2])
+ target_file = os.path.abspath("%s/exec" % argv[3])
+ launcher = os.path.abspath(argv[0]+".launcher.py")
+ starter = sources(launcher, source_dir, main)
+ build(source_dir, target_file, starter)
+ sys.stdout.flush()
+ sys.stderr.flush()
+ return target_file
+
+
+if __name__ == '__main__':
+ p = subprocess.Popen([compile(sys.argv), "exit"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (o, e) = p.communicate()
+ if isinstance(o, bytes) and not isinstance(o, str):
+ o = o.decode('utf-8')
+ if isinstance(e, bytes) and not isinstance(e, str):
+ e = e.decode('utf-8')
+ if o:
+ sys.stdout.write(o)
+ sys.stdout.flush()
+
+ if e:
+ sys.stderr.write(e)
+ sys.stderr.flush()
+
diff --git a/core/pythonActionLoop/pythonbuild.py.launcher.py
b/core/pythonActionLoop/pythonbuild.py.launcher.py
new file mode 100755
index 0000000..b7007c9
--- /dev/null
+++ b/core/pythonActionLoop/pythonbuild.py.launcher.py
@@ -0,0 +1,72 @@
+#
+# 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 __future__ import print_function
+from sys import stdin
+from sys import stdout
+from sys import stderr
+from os import fdopen
+import sys, os, json, traceback
+
+try:
+ # if the directory 'virtualenv' is extracted out of a zip file
+ path_to_virtualenv = os.path.abspath('./virtualenv')
+ if os.path.isdir(path_to_virtualenv):
+ # activate the virtualenv using activate_this.py contained in the
virtualenv
+ activate_this_file = path_to_virtualenv + '/bin/activate_this.py'
+ if os.path.exists(activate_this_file):
+ with open(activate_this_file) as f:
+ code = compile(f.read(), activate_this_file, 'exec')
+ exec(code, dict(__file__=activate_this_file))
+ else:
+ sys.stderr.write('Invalid virtualenv. Zip file does not include
/virtualenv/bin/' + os.path.basename(activate_this_file) + '\n')
+ sys.exit(1)
+except Exception:
+ traceback.print_exc(file=sys.stderr, limit=0)
+ sys.exit(1)
+
+# now import the action as process input/output
+from main__ import main as main
+
+# if there are some arguments exit immediately
+if len(sys.argv) >1:
+ sys.stderr.flush()
+ sys.stdout.flush()
+ sys.exit(0)
+
+env = os.environ
+out = fdopen(3, "wb")
+while True:
+ line = stdin.readline()
+ if not line: break
+ args = json.loads(line)
+ payload = {}
+ for key in args:
+ if key == "value":
+ payload = args["value"]
+ else:
+ env["__OW_%s" % key.upper()]= args[key]
+ res = {}
+ try:
+ res = main(payload)
+ except Exception as ex:
+ print(traceback.format_exc(), file=stderr)
+ res = {"error": str(ex)}
+ out.write(json.dumps(res, ensure_ascii=False).encode('utf-8'))
+ out.write(b'\n')
+ stdout.flush()
+ stderr.flush()
+ out.flush()
diff --git a/settings.gradle b/settings.gradle
index cec472b..7bec58b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -20,6 +20,8 @@ include 'tests'
include 'core:pythonAction'
include 'core:python2Action'
include 'core:python3AiAction'
+include 'core:pythonActionLoop'
+
rootProject.name = 'runtime-python'
diff --git
a/tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
b/tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
index a706938..edc376c 100644
---
a/tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
+++
b/tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
@@ -36,6 +36,9 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
/** indicates if strings in python are unicode by default (i.e., python3 ->
true, python2.7 -> false) */
lazy val pythonStringAsUnicode = true
+ /** indicates if errors are logged or returned in the answer */
+ lazy val initErrorsAreLogged = true
+
override def withActionContainer(env: Map[String, String] = Map.empty)(code:
ActionContainer => Unit) = {
withContainer(imageName, env)(code)
}
@@ -47,15 +50,15 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
override val testNotReturningJson =
TestConfig("""
- |def main(args):
- | return "not a json object"
- """.stripMargin)
+ |def main(args):
+ | return "not a json object"
+ """.stripMargin)
override val testInitCannotBeCalledMoreThanOnce =
TestConfig("""
- |def main(args):
- | return args
- """.stripMargin)
+ |def main(args):
+ | return args
+ """.stripMargin)
override val testEntryPointOtherThanMain =
TestConfig(
@@ -67,13 +70,13 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
override val testEcho =
TestConfig("""
- |from __future__ import print_function
- |import sys
- |def main(args):
- | print('hello stdout')
- | print('hello stderr', file=sys.stderr)
- | return args
- """.stripMargin)
+ |from __future__ import print_function
+ |import sys
+ |def main(args):
+ | print('hello stdout')
+ | print('hello stderr', file=sys.stderr)
+ | return args
+ """.stripMargin)
override val testUnicode =
TestConfig(if (pythonStringAsUnicode) {
@@ -96,17 +99,17 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
override val testEnv =
TestConfig("""
- |import os
- |def main(dict):
- | return {
- | "api_host": os.environ['__OW_API_HOST'],
- | "api_key": os.environ['__OW_API_KEY'],
- | "namespace": os.environ['__OW_NAMESPACE'],
- | "action_name": os.environ['__OW_ACTION_NAME'],
- | "activation_id": os.environ['__OW_ACTIVATION_ID'],
- | "deadline": os.environ['__OW_DEADLINE']
- | }
- """.stripMargin.trim)
+ |import os
+ |def main(dict):
+ | return {
+ | "api_host": os.environ['__OW_API_HOST'],
+ | "api_key": os.environ['__OW_API_KEY'],
+ | "namespace": os.environ['__OW_NAMESPACE'],
+ | "action_name": os.environ['__OW_ACTION_NAME'],
+ | "activation_id": os.environ['__OW_ACTIVATION_ID'],
+ | "deadline": os.environ['__OW_DEADLINE']
+ | }
+ """.stripMargin.trim)
override val testLargeInput =
TestConfig("""
@@ -116,17 +119,20 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
it should "support zip-encoded action using non-default entry points" in {
val srcs = Seq(
- Seq("__main__.py") -> """
- |from echo import echo
- |def niam(args):
- | return echo(args)
- """.stripMargin,
- Seq("echo.py") -> """
- |def echo(args):
- | return { "echo": args }
- """.stripMargin)
+ Seq("__main__.py") ->
+ """
+ |from echo import echo
+ |def niam(args):
+ | return echo(args)
+ """.stripMargin,
+ Seq("echo.py") ->
+ """
+ |def echo(args):
+ | return { "echo": args }
+ """.stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
+ println(code)
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "niam"))
@@ -148,11 +154,12 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
it should "support zip-encoded action which can read from relative paths" in
{
val srcs = Seq(
- Seq("__main__.py") -> """
- |def main(args):
- | f = open('workfile', 'r')
- | return {'file': f.read()}
- """.stripMargin,
+ Seq("__main__.py") ->
+ """
+ |def main(args):
+ | f = open('workfile', 'r')
+ | return {'file': f.read()}
+ """.stripMargin,
Seq("workfile") -> "this is a test string")
val code = ZipBuilder.mkBase64Zip(srcs)
@@ -176,27 +183,35 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
}
it should "report error if zip-encoded action does not include required
file" in {
- val srcs = Seq(Seq("echo.py") -> """
- |def echo(args):
- | return { "echo": args }
- """.stripMargin)
+ val srcs = Seq(
+ Seq("echo.py") ->
+ """
+ |def echo(args):
+ | return { "echo": args }
+ """.stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "echo"))
initCode should be(502)
+ if (!initErrorsAreLogged)
+ initRes.get.fields.get("error").get.toString() should include("Zip
file does not include")
}
- checkStreams(out, err, {
- case (o, e) =>
- o shouldBe empty
- e should include("Zip file does not include")
- })
+ if (initErrorsAreLogged)
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e should include("Zip file does not include")
+ })
}
it should "run zipped Python action containing a virtual environment" in {
- val zippedPythonAction = if (imageName == "python2action")
"python2_virtualenv.zip" else "python3_virtualenv.zip"
+ val zippedPythonAction =
+ if (imageName == "python2action") "python2_virtualenv.zip"
+ else if (imageName == "actionloop-python-v3.7") "python37_virtualenv.zip"
+ else "python3_virtualenv.zip"
val zippedPythonActionName =
TestUtils.getTestActionFilename(zippedPythonAction)
val code = readAsBase64(Paths.get(zippedPythonActionName))
@@ -208,6 +223,7 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
runCode should be(200)
runRes.get.toString() should include("netmask")
}
+
checkStreams(out, err, {
case (o, e) =>
o should include("netmask")
@@ -216,10 +232,13 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
}
it should "run zipped Python action containing a virtual environment with
non-standard entry point" in {
- val zippedPythonAction = if (imageName == "python2action")
"python2_virtualenv.zip" else "python3_virtualenv.zip"
+ val zippedPythonAction =
+ if (imageName == "python2action") "python2_virtualenv.zip"
+ else if (imageName == "actionloop-python-v3.7") "python37_virtualenv.zip"
+ else "python3_virtualenv.zip"
val zippedPythonActionName =
TestUtils.getTestActionFilename(zippedPythonAction)
- val code = readAsBase64(Paths.get(zippedPythonActionName))
+ val code = readAsBase64(Paths.get(zippedPythonActionName))
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "naim"))
initCode should be(200)
@@ -233,28 +252,46 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
o should include("netmask")
e shouldBe empty
})
+
}
it should "report error if zipped Python action containing a virtual
environment for wrong python version" in {
- val zippedPythonAction = if (imageName.contains("python3"))
"python2_virtualenv.zip" else "python3_virtualenv.zip"
+ val zippedPythonAction = if (imageName == "python2action")
"python3_virtualenv.zip" else "python2_virtualenv.zip"
val zippedPythonActionName =
TestUtils.getTestActionFilename(zippedPythonAction)
+
val code = readAsBase64(Paths.get(zippedPythonActionName))
- val (out, err) = withActionContainer() { c =>
- val (initCode, initRes) = c.init(initPayload(code, main = "main"))
- initCode should be(200)
- val args = JsObject("msg" -> JsString("any"))
- val (runCode, runRes) = c.run(runPayload(args))
- runCode should be {
- if (imageName == "python3aiaction") 200 else 502
+ // temporary guard to comment out this test for python3aiaction
+ // until it is fixed (it does not detect the wrong virtual env)
+ if (imageName != "python3aiaction") {
+ val (out, err) = withActionContainer() { c =>
+ val (initCode, initRes) = c.init(initPayload(code, main = "main"))
+ if (initErrorsAreLogged) {
+ initCode should be(200)
+ val args = JsObject("msg" -> JsString("any"))
+ val (runCode, runRes) = c.run(runPayload(args))
+ runCode should be(502)
+ } else {
+ // it actually means it is actionloop
+ // it checks the error at init time
+ initCode should be(502)
+ initRes.get.fields.get("error").get.toString() should include("No
module")
+ }
}
+ if (initErrorsAreLogged)
+ checkStreams(
+ out,
+ err, {
+ case (o, e) =>
+ o shouldBe empty
+ if (imageName == "python2action") {
+ e should include("ImportError")
+ }
+ if (imageName == "python3action") {
+ e should include("ModuleNotFoundError")
+ }
+ })
}
- checkStreams(out, err, {
- case (o, e) =>
- if (imageName != "python3aiaction") { o shouldBe empty }
- if (imageName == "python2action") { e should include("ImportError") }
- if (imageName == "python3action") { e should
include("ModuleNotFoundError") }
- })
}
it should "report error if zipped Python action has wrong main module name"
in {
@@ -265,12 +302,15 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "main"))
initCode should be(502)
+ if (!initErrorsAreLogged)
+ initRes.get.fields.get("error").get.toString() should include("Zip
file does not include mandatory files")
}
- checkStreams(out, err, {
- case (o, e) =>
- o shouldBe empty
- e should include("Zip file does not include __main__.py")
- })
+ if (initErrorsAreLogged)
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e should include("Zip file does not include __main__.py")
+ })
}
it should "report error if zipped Python action has invalid virtualenv
directory" in {
@@ -280,29 +320,38 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "main"))
initCode should be(502)
+ if (!initErrorsAreLogged)
+ initRes.get.fields.get("error").get.toString() should include("Invalid
virtualenv. Zip file does not include")
}
- checkStreams(out, err, {
- case (o, e) =>
- o shouldBe empty
- e should include("Zip file does not include /virtualenv/bin/")
- })
+ if (initErrorsAreLogged)
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e should include("Zip file does not include /virtualenv/bin/")
+ })
}
it should "return on action error when action fails" in {
val (out, err) = withActionContainer() { c =>
- val code = """
- |def div(x, y):
- | return x/y
- |
- |def main(dict):
- | return {"divBy0": div(5,0)}
- """.stripMargin
+ val code =
+ """
+ |def div(x, y):
+ | return x/y
+ |
+ |def main(dict):
+ | return {"divBy0": div(5,0)}
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should be(502)
+ /* ActionLoop does not set 502 if there are application errors
+ * Since it only receive a string from the application
+ * it should parse the entire string in JSON just to find it is an
"error"
+ */
+ if (initErrorsAreLogged)
+ runCode should be(502)
runRes shouldBe defined
runRes.get.fields.get("error") shouldBe defined
@@ -317,29 +366,31 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
it should "log compilation errors" in {
val (out, err) = withActionContainer() { c =>
- val code = """
- | 10 PRINT "Hello!"
- | 20 GOTO 10
- """.stripMargin
+ val code =
+ """
+ | 10 PRINT "Hello!"
+ | 20 GOTO 10
+ """.stripMargin
val (initCode, res) = c.init(initPayload(code))
// init checks whether compilation was successful, so return 502
initCode should be(502)
}
-
- checkStreams(out, err, {
- case (o, e) =>
- o shouldBe empty
- e should include("Traceback")
- })
+ if (initErrorsAreLogged)
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e should include("Traceback")
+ })
}
it should "support application errors" in {
val (out, err) = withActionContainer() { c =>
- val code = """
- |def main(args):
- | return { "error": "sorry" }
- """.stripMargin
+ val code =
+ """
+ |def main(args):
+ | return { "error": "sorry" }
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
@@ -360,23 +411,31 @@ class PythonActionContainerTests extends
BasicActionRunnerTests with WskActorSys
it should "error when importing a not-supported package" in {
val (out, err) = withActionContainer() { c =>
- val code = """
- |import iamnotsupported
- |def main(args):
- | return { "error": "not reaching here" }
- """.stripMargin
-
- val (initCode, res) = c.init(initPayload(code))
- initCode should be(200)
-
- val (runCode, runRes) = c.run(runPayload(JsObject()))
- runCode should be(502)
+ val code =
+ """
+ |import iamnotsupported
+ |def main(args):
+ | return { "error": "not reaching here" }
+ """.stripMargin
+
+ if (initErrorsAreLogged) {
+ val (initCode, res) = c.init(initPayload(code))
+ initCode should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(502)
+ } else {
+ // action loop detects those errors at init time
+ val (initCode, initRes) = c.init(initPayload(code))
+ initCode should be(502)
+ initRes.get.fields.get("error").get.toString() should
include("Traceback")
+ }
}
-
- checkStreams(out, err, {
- case (o, e) =>
- o shouldBe empty
- e should include("Traceback")
- })
+ if (initErrorsAreLogged)
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e should include("Traceback")
+ })
}
}
diff --git
a/tests/src/test/scala/runtime/actionContainers/PythonActionLoopContainerTests.scala
b/tests/src/test/scala/runtime/actionContainers/PythonActionLoopContainerTests.scala
new file mode 100644
index 0000000..56a2dce
--- /dev/null
+++
b/tests/src/test/scala/runtime/actionContainers/PythonActionLoopContainerTests.scala
@@ -0,0 +1,36 @@
+/*
+ * 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 common.WskActorSystem
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class PythonActionLoopContainerTests extends PythonActionContainerTests with
WskActorSystem {
+
+ override lazy val imageName = "actionloop-python-v3.7"
+
+ override val testNoSource = TestConfig("", hasCodeStub = false)
+
+ /** indicates if strings in python are unicode by default (i.e., python3 ->
true, python2.7 -> false) */
+ override lazy val pythonStringAsUnicode = true
+
+ /** actionloop based image does not log init errors - return the error in
the body */
+ override lazy val initErrorsAreLogged = false
+}
diff --git a/tools/travis/publish.sh b/tools/travis/publish.sh
index 9368b7a..e18795d 100755
--- a/tools/travis/publish.sh
+++ b/tools/travis/publish.sh
@@ -36,6 +36,8 @@ elif [ ${RUNTIME_VERSION} == "3" ]; then
RUNTIME="pythonAction"
elif [ ${RUNTIME_VERSION} == "3-ai" ]; then
RUNTIME="python3AiAction"
+elif [ ${RUNTIME_VERSION} == "3-loop" ]; then
+ RUNTIME="pythonActionLoop"
fi
if [[ ! -z ${DOCKER_USER} ]] && [[ ! -z ${DOCKER_PASSWORD} ]]; then
With regards,
Apache Git Services