-
Carlos Santana authoredCarlos Santana authored
PythonActionContainerTests.scala 13.93 KiB
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime.actionContainers
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import spray.json.DefaultJsonProtocol._
import spray.json._
import common.WskActorSystem
import actionContainers.{ActionContainer, BasicActionRunnerTests}
import actionContainers.ActionContainer.withContainer
import actionContainers.ResourceHelpers.{readAsBase64, ZipBuilder}
import common.TestUtils
import java.nio.file.Paths
@RunWith(classOf[JUnitRunner])
class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
lazy val imageName = "python3action"
/** 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)
}
behavior of imageName
override val testNoSourceOrExec = TestConfig("")
override val testNoSource = TestConfig("", hasCodeStub = true)
override val testNotReturningJson =
TestConfig("""
|def main(args):
| return "not a json object"
""".stripMargin)
override val testInitCannotBeCalledMoreThanOnce =
TestConfig("""
|def main(args):
| return args
""".stripMargin)
override val testEntryPointOtherThanMain =
TestConfig(
"""
|def niam(args):
| return args
""".stripMargin,
main = "niam")
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)
override val testUnicode =
TestConfig(if (pythonStringAsUnicode) {
"""
|def main(args):
| sep = args['delimiter']
| str = sep + " ☃ " + sep
| print(str)
| return {"winter" : str }
""".stripMargin.trim
} else {
"""
|def main(args):
| sep = args['delimiter']
| str = sep + " ☃ ".decode('utf-8') + sep
| print(str.encode('utf-8'))
| return {"winter" : str }
""".stripMargin.trim
})
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)
override val testLargeInput =
TestConfig("""
|def main(args):
| return args
""".stripMargin)
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)
val code = ZipBuilder.mkBase64Zip(srcs)
println(code)
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "niam"))
initCode should be(200)
val args = JsObject("msg" -> JsString("it works"))
val (runCode, runRes) = c.run(runPayload(args))
runCode should be(200)
runRes.get.fields.get("echo") shouldBe Some(args)
}
checkStreams(out, err, {
case (o, e) =>
o shouldBe empty
e shouldBe empty
})
}
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("workfile") -> "this is a test string")
val code = ZipBuilder.mkBase64Zip(srcs)
val (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code))
initCode should be(200)
val args = JsObject()
val (runCode, runRes) = c.run(runPayload(args))
runCode should be(200)
runRes.get.fields.get("file") shouldBe Some("this is a test string".toJson)
}
checkStreams(out, err, {
case (o, e) =>
o shouldBe empty
e shouldBe empty
})
}
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 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")
}
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 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 (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(200)
runRes.get.toString() should include("netmask")
}
checkStreams(out, err, {
case (o, e) =>
o should include("netmask")
e shouldBe empty
})
}
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 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 (out, err) = withActionContainer() { c =>
val (initCode, initRes) = c.init(initPayload(code, main = "naim"))
initCode should be(200)
val args = JsObject("msg" -> JsString("any"))
val (runCode, runRes) = c.run(runPayload(args))
runCode should be(200)
runRes.get.toString() should include("netmask")
}
checkStreams(out, err, {
case (o, e) =>
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 == "python2action") "python3_virtualenv.zip" else "python2_virtualenv.zip"
val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction)
val code = readAsBase64(Paths.get(zippedPythonActionName))
// 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")
}
})
}
}
it should "report error if zipped Python action has wrong main module name" in {
val zippedPythonActionWrongName = TestUtils.getTestActionFilename("python_virtualenv_name.zip")
val code = readAsBase64(Paths.get(zippedPythonActionWrongName))
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")
}
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 {
val zippedPythonActionWrongDir = TestUtils.getTestActionFilename("python_virtualenv_dir.zip")
val code = readAsBase64(Paths.get(zippedPythonActionWrongDir))
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")
}
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 (initCode, _) = c.init(initPayload(code))
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
/* 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
}
checkStreams(out, err, {
case (o, e) =>
o shouldBe empty
e should include("Traceback")
})
}
it should "log compilation errors" in {
val (out, err) = withActionContainer() { c =>
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)
}
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 (initCode, _) = c.init(initPayload(code))
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
runCode should be(200) // action writer returning an error is OK
runRes shouldBe defined
runRes should be(Some(JsObject("error" -> JsString("sorry"))))
}
checkStreams(out, err, {
case (o, e) =>
o shouldBe empty
e shouldBe empty
})
}
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
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")
}
}
if (initErrorsAreLogged)
checkStreams(out, err, {
case (o, e) =>
o shouldBe empty
e should include("Traceback")
})
}
}