blob: 6622cc019214dc3e57bbf91038236319819d166a [file] [log] [blame]
/*
* 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 common.WskActorSystem
import actionContainers.{ActionContainer, BasicActionRunnerTests}
import actionContainers.ActionContainer.withContainer
import actionContainers.ResourceHelpers.ZipBuilder
import spray.json._
@RunWith(classOf[JUnitRunner])
class Ruby25ActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
// note: "out" will not be empty as the Webrick outputs a message during the boot and after the boot
val enforceEmptyOutputStream = false
lazy val ruby25ContainerImageName = "action-ruby-v2.5"
override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
withContainer(ruby25ContainerImageName, env)(code)
}
def withRuby25Container(code: ActionContainer => Unit) = withActionContainer()(code)
behavior of ruby25ContainerImageName
override val testNoSourceOrExec = TestConfig("")
override val testEcho = {
TestConfig("""
|def main(args)
| puts 'hello stdout'
| warn 'hello stderr'
| args
|end
""".stripMargin)
}
override val testNotReturningJson = {
TestConfig(
"""
|def main(args)
| "not a json object"
|end
""".stripMargin,
enforceEmptyOutputStream = enforceEmptyOutputStream,
enforceEmptyErrorStream = false)
}
override val testInitCannotBeCalledMoreThanOnce = {
TestConfig(
"""
|def main(args)
| args
|end
""".stripMargin,
enforceEmptyOutputStream = enforceEmptyOutputStream)
}
override val testEntryPointOtherThanMain = {
TestConfig(
"""
|def niam(args)
| args
|end
""".stripMargin,
main = "niam",
enforceEmptyOutputStream = enforceEmptyOutputStream)
}
override val testUnicode = {
TestConfig("""
|def main(args)
| str = args['delimiter'] + " ☃ " + args['delimiter']
| print str + "\n"
| {"winter" => str}
|end
""".stripMargin.trim)
}
override val testEnv = {
TestConfig(
"""
|def main(args)
| {
| "env" => ENV,
| "api_host" => ENV['__OW_API_HOST'],
| "api_key" => ENV['__OW_API_KEY'],
| "namespace" => ENV['__OW_NAMESPACE'],
| "action_name" => ENV['__OW_ACTION_NAME'],
| "activation_id" => ENV['__OW_ACTIVATION_ID'],
| "deadline" => ENV['__OW_DEADLINE']
| }
|end
""".stripMargin.trim,
enforceEmptyOutputStream = enforceEmptyOutputStream)
}
override val testLargeInput = {
TestConfig("""
|<?php
|function main(array $args) : array {
| echo 'hello stdout';
| error_log('hello stderr');
| return $args;
|}
""".stripMargin)
}
it should "fail to initialize with bad code" in {
val (out, err) = withRuby25Container { c =>
val code = """
| 10 PRINT "Hello world!"
| 20 GOTO 10
""".stripMargin
val (initCode, error) = c.init(initPayload(code))
initCode should not be (200)
error shouldBe a[Some[_]]
error.get shouldBe a[JsObject]
error.get.fields("error").toString should include("failed to parse the input code")
}
// Somewhere, the logs should mention an error occurred.
checkStreams(out, err, {
case (o, e) =>
(o + e).toLowerCase should include("invalid")
(o + e).toLowerCase should include("parse")
})
}
it should "return some error on action error" in {
val (out, err) = withRuby25Container { c =>
val code = """
| def main(args)
| raise Exception.new("nooooo")
| end
""".stripMargin
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
runCode should not be (200)
runRes shouldBe defined
runRes.get.fields.get("error") shouldBe defined
// runRes.get.fields("error").toString.toLowerCase should include("nooooo")
}
// Somewhere, the logs should be the error text
checkStreams(out, err, {
case (o, e) =>
(o + e).toLowerCase should include("nooooo")
})
}
it should "support application errors" in {
withRuby25Container { c =>
val code = """
| def main(args)
| { "error" => "sorry" }
| end
""".stripMargin;
val (initCode, error) = 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.get.fields.get("error") shouldBe defined
runRes.get.fields("error").toString.toLowerCase should include("sorry")
}
}
it should "fail gracefully when an action has a TypeError exception" in {
val (out, err) = withRuby25Container { c =>
val code = """
| def main(args)
| eval "class ENV\nend"
| { "hello" => "world" }
| end
""".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
runRes.get.fields("error").toString should include("An error occurred running the action")
}
// Somewhere, the logs should be the error text
checkStreams(out, err, {
case (o, e) =>
(o + e).toLowerCase should include("typeerror")
})
}
it should "support the documentation examples (1)" in {
val (out, err) = withRuby25Container { c =>
val code = """
| def main(params)
| if (params['payload'] == 0) then
| return {}
| elsif params['payload'] == 1 then
| return {'payload' => 'Hello, World!'} # indicates normal completion
| elsif params['payload'] == 2 then
| return {'error' => 'payload must be 0 or 1'} # indicates abnormal completion
| end
| end
""".stripMargin
c.init(initPayload(code))._1 should be(200)
val (c1, r1) = c.run(runPayload(JsObject("payload" -> JsNumber(0))))
val (c2, r2) = c.run(runPayload(JsObject("payload" -> JsNumber(1))))
val (c3, r3) = c.run(runPayload(JsObject("payload" -> JsNumber(2))))
c1 should be(200)
r1 should be(Some(JsObject()))
c2 should be(200)
r2 should be(Some(JsObject("payload" -> JsString("Hello, World!"))))
c3 should be(200) // application error, not container or system
r3.get.fields.get("error") shouldBe Some(JsString("payload must be 0 or 1"))
}
}
it should "have mechanize and activesupport gems available" in {
// GIVEN that it should "error when requiring a non-existent package" (see test above for this)
val (out, err) = withRuby25Container { c =>
val code = """
| require 'mechanize'
| require 'active_support'
| def main(args)
| Mechanize.class
| ActiveSupport.class
| {}
| end
""".stripMargin
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
// WHEN I run an action that calls a Guzzle & a Uuid method
val (runCode, out) = c.run(runPayload(JsObject()))
// THEN it should pass only when these packages are available
runCode should be(200)
}
}
it should "support large-ish actions" in {
val thought = " I took the one less traveled by, and that has made all the difference."
val assignment = " x = \"" + thought + "\";\n"
val code = """
| def main(args)
| x = "hello"
""".stripMargin + (assignment * 7000) + """
| x = "world"
| { "message" => x }
| end
""".stripMargin
// Lest someone should make it too easy.
code.length should be >= 500000
val (out, err) = withRuby25Container { c =>
c.init(initPayload(code))._1 should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
runCode should be(200)
runRes.get.fields.get("message") shouldBe defined
runRes.get.fields.get("message") shouldBe Some(JsString("world"))
}
}
val exampleOutputDotRuby: String = """
| def output(data)
| {'result' => data}
| end
""".stripMargin
it should "support zip-encoded packages" in {
val srcs = Seq(
Seq("output.rb") -> exampleOutputDotRuby,
Seq("main.rb") -> """
| require __dir__ + '/output.rb'
| def main(args)
| name = args['name'] || 'stranger'
| output(name)
| end
""".stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
val (out, err) = withRuby25Container { c =>
c.init(initPayload(code))._1 should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
runCode should be(200)
runRes.get.fields.get("result") shouldBe defined
runRes.get.fields.get("result") shouldBe Some(JsString("stranger"))
}
}
it should "fail gracefully on invalid zip files" in {
// Some text-file encoded to base64.
val code = "Q2VjaSBuJ2VzdCBwYXMgdW4gemlwLgo="
val (out, err) = withRuby25Container { c =>
val (initCode, error) = c.init(initPayload(code))
initCode should not be (200)
error shouldBe a[Some[_]]
error.get shouldBe a[JsObject]
error.get.fields("error").toString should include("failed to open zip file")
}
// Somewhere, the logs should mention the failure
checkStreams(out, err, {
case (o, e) =>
(o + e).toLowerCase should include("error")
(o + e).toLowerCase should include("failed to open zip file")
})
}
it should "fail gracefully on valid zip files that are not actions" in {
val srcs = Seq(Seq("hello") -> """
| Hello world!
""".stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
val (out, err) = withRuby25Container { c =>
c.init(initPayload(code))._1 should not be (200)
}
checkStreams(out, err, {
case (o, e) =>
(o + e).toLowerCase should include("error")
(o + e).toLowerCase should include("zipped actions must contain main.rb at the root.")
})
}
it should "fail gracefully on valid zip files with invalid code in main.rb" in {
val (out, err) = withRuby25Container { c =>
val srcs = Seq(Seq("main.rb") -> """
| 10 PRINT "Hello world!"
| 20 GOTO 10
""".stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
val (initCode, error) = c.init(initPayload(code))
initCode should not be (200)
error shouldBe a[Some[_]]
error.get shouldBe a[JsObject]
error.get.fields("error").toString should include("failed to parse the input code")
}
// Somewhere, the logs should mention an error occurred.
checkStreams(out, err, {
case (o, e) =>
(o + e).toLowerCase should include("invalid")
(o + e).toLowerCase should include("parse")
})
}
it should "support zipped actions using non-default entry point" in {
val srcs = Seq(Seq("main.rb") -> """
| def niam(args)
| { :result => "it works" }
| end
""".stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
withRuby25Container { c =>
c.init(initPayload(code, main = "niam"))._1 should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
}
}
}