Skip to content
Snippets Groups Projects
PythonActionContainerTests.scala 10.3 KiB
Newer Older
/*
 * Copyright 2015-2016 IBM Corporation
 *
 * Licensed 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 actionContainers

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner

import ActionContainer.withContainer
import ResourceHelpers.ZipBuilder

import spray.json.DefaultJsonProtocol._
import spray.json._
import common.WskActorSystem

@RunWith(classOf[JUnitRunner])
class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
    override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
        withContainer("pythonaction", env)(code)
    behavior of "pythonaction"
    testNotReturningJson(
        """
        |def main(args):
        |    return "not a json object"
        """.stripMargin, checkResultInLogs = false)

    testEcho(Seq {
        ("python", """
          |from __future__ import print_function
          |def main(args):
          |    print('hello stdout')
          |    print('hello stderr', file=sys.stderr)
          |    return args
          """.stripMargin)
    })

    testEnv(Seq {
        ("python", """
         |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']
         |    }
    it should "support actions using non-default entry points" in {
        withActionContainer() { c =>
            val code = """
                |def niam(dict):
                |  return { "result": "it works" }
                |""".stripMargin

            val (initCode, initRes) = c.init(initPayload(code, main = "niam"))
            initCode should be(200)

            val (_, runRes) = c.run(runPayload(JsObject()))
            runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
        }
    }

    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)

        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 "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)
        }

        checkStreams(out, err, {
            case (o, e) =>
                o shouldBe empty
                e should include("Zip file does not include")
        })
    }

    it should "handle unicode in source, input params, logs, and result" in {
        val (out, err) = withActionContainer() { c =>
            val code = """
                |def main(dict):
                |    sep = dict['delimiter']
                |    str = sep + " ☃ ".decode('utf-8') + sep
                |    print(str.encode('utf-8'))
                |    return {"winter" : str }
            """.stripMargin

            val (initCode, _) = c.init(initPayload(code))
            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, e) =>
                o.toLowerCase should include("❄ ☃ ❄")
                e shouldBe empty
        })
    }

    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()))
            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)

        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

            val (initCode, res) = c.init(initPayload(code))
            initCode should be(200)

            val (runCode, runRes) = c.run(runPayload(JsObject()))
            runCode should be(502)
        }

        checkStreams(out, err, {
            case (o, e) =>
                o shouldBe empty
                e should include("Traceback")
        })
    }

    it should "be able to import additional packages as installed in the image" in {
        val (out, err) = withActionContainer() { c =>
            val code = """
                |from bs4 import BeautifulSoup
                |from dateutil.parser import *
                |import httplib2
                |from lxml import etree
                |import requests
                |from scrapy.item import Item, Field
                |import simplejson as json
                |from twisted.internet import protocol, reactor, endpoints
                |from kafka import BrokerConnection
                |def main(args):
                |    socket.setdefaulttimeout(120)
                |    b = BeautifulSoup('<html><head><title>python action test</title></head></html>', 'html.parser')
                |    h = httplib2.Http().request('https://openwhisk.ng.bluemix.net/api/v1')[0]
                |    t = parse('2016-02-22 11:59:00 EST')
                |    r = requests.get('https://openwhisk.ng.bluemix.net/api/v1')
                |    j = json.dumps({'foo':'bar'}, separators = (',', ':'))
                |    kafka = BrokerConnection("it works", 9093, None)
                |
                |    return {
                |       "bs4": str(b.title),
                |       "httplib2": h.status,
                |       "dateutil": t.strftime("%A"),
                |       "lxml": etree.Element("root").tag,
                |       "json": j,
                |       "request": r.status_code,
                |       "kafka_python": kafka.host
            """.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(
                "bs4" -> "<title>python action test</title>".toJson,
                "httplib2" -> 200.toJson,
                "dateutil" -> "Monday".toJson,
                "lxml" -> "root".toJson,
                "json" -> JsObject("foo" -> "bar".toJson).compactPrint.toJson,
                "request" -> 200.toJson,
                "kafka_python" -> "it works".toJson)))
        }

        checkStreams(out, err, {
            case (o, e) =>
                o shouldBe empty
                e shouldBe empty
        })
    }