blob: 91f1eb5179f652718d4dfcd0e60a3fc4cf4ea6de [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 invokerShoot
import java.io.File
import java.nio.charset.StandardCharsets
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import common._
import common.rest.WskRestOperations
import org.apache.openwhisk.core.entity.{Annotations}
import org.apache.commons.io.FileUtils
import spray.json._
import spray.json.DefaultJsonProtocol._
@RunWith(classOf[JUnitRunner])
class ShootInvokerTests extends TestHelpers with WskTestHelpers with JsHelpers with WskActorSystem {
implicit val wskprops = WskProps()
// wsk must have type WskOperations so that tests using CLI (class Wsk)
// instead of REST (WskRestOperations) still work.
val wsk: WskOperations = new WskRestOperations
val testString = "this is a test"
val testResult = JsObject("count" -> testString.split(" ").length.toJson)
val guestNamespace = wskprops.namespace
behavior of "Whisk actions"
it should "create an action with an empty file" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "empty"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("empty.js")))
}
}
it should "invoke an action returning a promise" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "hello promise"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("helloPromise.js")))
}
val run = wsk.action.invoke(name)
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
activation.response.result shouldBe Some(JsObject("done" -> true.toJson))
activation.logs.get.mkString(" ") shouldBe empty
}
}
it should "invoke an action with a space in the name" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "hello Async"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")))
}
val run = wsk.action.invoke(name, Map("payload" -> testString.toJson))
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
activation.response.result shouldBe Some(testResult)
activation.logs.get.mkString(" ") should include(testString)
}
}
it should "invoke an action that throws an uncaught exception and returns correct status code" in withAssetCleaner(
wskprops) { (wp, assetHelper) =>
val name = "throwExceptionAction"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("runexception.js")))
}
withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
val response = activation.response
activation.response.status shouldBe "action developer error"
activation.response.result shouldBe Some(
JsObject("error" -> "An error has occurred: Extraordinary exception".toJson))
}
}
it should "pass parameters bound on creation-time to the action" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "printParams"
val params = Map("param1" -> "test1", "param2" -> "test2")
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(
name,
Some(TestUtils.getTestActionFilename("printParams.js")),
parameters = params.mapValues(_.toJson))
}
val invokeParams = Map("payload" -> testString)
val run = wsk.action.invoke(name, invokeParams.mapValues(_.toJson))
withActivation(wsk.activation, run) { activation =>
val logs = activation.logs.get.mkString(" ")
(params ++ invokeParams).foreach {
case (key, value) =>
logs should include(s"params.$key: $value")
}
}
}
it should "copy an action and invoke it successfully" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "copied"
val packageName = "samples"
val actionName = "wordcount"
val fullQualifiedName = s"/$guestNamespace/$packageName/$actionName"
assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>
pkg.create(packageName, shared = Some(true))
}
assetHelper.withCleaner(wsk.action, fullQualifiedName) {
val file = Some(TestUtils.getTestActionFilename("wc.js"))
(action, _) =>
action.create(fullQualifiedName, file)
}
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(fullQualifiedName), Some("copy"))
}
val run = wsk.action.invoke(name, Map("payload" -> testString.toJson))
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
activation.response.result shouldBe Some(testResult)
activation.logs.get.mkString(" ") should include(testString)
}
}
it should "copy an action and ensure exec, parameters, and annotations copied" in withAssetCleaner(wskprops) {
(wp, assetHelper) =>
val origActionName = "origAction"
val copiedActionName = "copiedAction"
val params = Map("a" -> "A".toJson)
val annots = Map("b" -> "B".toJson)
assetHelper.withCleaner(wsk.action, origActionName) {
val file = Some(TestUtils.getTestActionFilename("wc.js"))
(action, _) =>
action.create(origActionName, file, parameters = params, annotations = annots)
}
assetHelper.withCleaner(wsk.action, copiedActionName) { (action, _) =>
action.create(copiedActionName, Some(origActionName), Some("copy"))
}
val copiedAction = wsk.parseJsonString(wsk.action.get(copiedActionName).stdout)
val origAction = wsk.parseJsonString(wsk.action.get(copiedActionName).stdout)
copiedAction.fields("annotations") shouldBe origAction.fields("annotations")
copiedAction.fields("parameters") shouldBe origAction.fields("parameters")
copiedAction.fields("exec") shouldBe origAction.fields("exec")
copiedAction.fields("version") shouldBe JsString("0.0.1")
}
it should "add new parameters and annotations while copying an action" in withAssetCleaner(wskprops) {
(wp, assetHelper) =>
val runtime = "nodejs:default"
val origName = "origAction"
val copiedName = "copiedAction"
val origParams = Map("origParam1" -> "origParamValue1".toJson, "origParam2" -> 999.toJson)
val copiedParams = Map("copiedParam1" -> "copiedParamValue1".toJson, "copiedParam2" -> 123.toJson)
val origAnnots = Map("origAnnot1" -> "origAnnotValue1".toJson, "origAnnot2" -> true.toJson)
val copiedAnnots = Map("copiedAnnot1" -> "copiedAnnotValue1".toJson, "copiedAnnot2" -> false.toJson)
val resParams = Seq(
JsObject("key" -> JsString("copiedParam1"), "value" -> JsString("copiedParamValue1")),
JsObject("key" -> JsString("copiedParam2"), "value" -> JsNumber(123)),
JsObject("key" -> JsString("origParam1"), "value" -> JsString("origParamValue1")),
JsObject("key" -> JsString("origParam2"), "value" -> JsNumber(999)))
val resAnnots = Seq(
JsObject("key" -> JsString("origAnnot1"), "value" -> JsString("origAnnotValue1")),
JsObject("key" -> JsString("copiedAnnot2"), "value" -> JsFalse),
JsObject("key" -> JsString("copiedAnnot1"), "value" -> JsString("copiedAnnotValue1")),
JsObject("key" -> JsString("origAnnot2"), "value" -> JsTrue),
JsObject("key" -> Annotations.ProvideApiKeyAnnotationName.toJson, "value" -> JsFalse))
assetHelper.withCleaner(wsk.action, origName) {
val file = Some(TestUtils.getTestActionFilename("echo.js"))
(action, _) =>
action.create(origName, file, parameters = origParams, annotations = origAnnots, kind = Some(runtime))
}
assetHelper.withCleaner(wsk.action, copiedName) { (action, _) =>
println("created copied ")
action.create(copiedName, Some(origName), Some("copy"), parameters = copiedParams, annotations = copiedAnnots)
}
val copiedAction = wsk.parseJsonString(wsk.action.get(copiedName).stdout)
// first we check the returned execution runtime for 'nodejs:*'
copiedAction
.fields("annotations")
.convertTo[Seq[JsObject]]
.find(_.fields("key").convertTo[String] == "exec")
.map(_.fields("value"))
.map(exec => { exec.convertTo[String] should startWith("nodejs:") })
.getOrElse(fail())
// CLI does not guarantee order of annotations and parameters so do a diff to compare the values
copiedAction.fields("parameters").convertTo[Seq[JsObject]] diff resParams shouldBe List.empty
// for the anotations we ignore the exec field here, since we already compared it above
copiedAction
.fields("annotations")
.convertTo[Seq[JsObject]]
.filter(annotation => annotation.fields("key").convertTo[String] != "exec") diff resAnnots shouldBe List.empty
}
it should "recreate and invoke a new action with different code" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "recreatedAction"
assetHelper.withCleaner(wsk.action, name, false) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("wc.js")))
}
val run1 = wsk.action.invoke(name, Map("payload" -> testString.toJson))
withActivation(wsk.activation, run1) { activation =>
activation.response.status shouldBe "success"
activation.logs.get.mkString(" ") should include(s"The message '$testString' has")
}
wsk.action.delete(name)
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))
}
val run2 = wsk.action.invoke(name, Map("payload" -> testString.toJson))
withActivation(wsk.activation, run2) { activation =>
activation.response.status shouldBe "success"
activation.logs.get.mkString(" ") should include(s"hello, $testString")
}
}
it should "fail to invoke an action with an empty file" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "empty"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("empty.js")))
}
val run = wsk.action.invoke(name)
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "action developer error"
activation.response.result shouldBe Some(JsObject("error" -> "Missing main/no code to execute.".toJson))
}
}
it should "blocking invoke of nested blocking actions" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "nestedBlockingAction"
val child = "wc"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(
name,
Some(TestUtils.getTestActionFilename("wcbin.js")),
annotations = Map(Annotations.ProvideApiKeyAnnotationName -> JsTrue))
}
assetHelper.withCleaner(wsk.action, child) { (action, _) =>
action.create(child, Some(TestUtils.getTestActionFilename("wc.js")))
}
val run = wsk.action.invoke(name, Map("payload" -> testString.toJson), blocking = true)
val activation = wsk.parseJsonString(run.stdout).convertTo[ActivationResult]
withClue(s"check failed for activation: $activation") {
val wordCount = testString.split(" ").length
activation.response.result.get shouldBe JsObject("binaryCount" -> s"${wordCount.toBinaryString} (base 2)".toJson)
}
}
it should "blocking invoke an asynchronous action" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "helloAsync"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")))
}
val run = wsk.action.invoke(name, Map("payload" -> testString.toJson), blocking = true)
val activation = wsk.parseJsonString(run.stdout).convertTo[ActivationResult]
withClue(s"check failed for activation: $activation") {
activation.response.status shouldBe "success"
activation.response.result shouldBe Some(testResult)
activation.logs shouldBe Some(List.empty)
}
}
it should "not be able to use 'ping' in an action" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "ping"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("ping.js")))
}
val run = wsk.action.invoke(name, Map("payload" -> "google.com".toJson))
withActivation(wsk.activation, run) { activation =>
val result = activation.response.result.get
result.getFields("stdout", "code") match {
case Seq(JsString(stdout), JsNumber(code)) =>
stdout should not include "bytes from"
code.intValue should not be 0
case _ => fail(s"fields 'stdout' or 'code' where not of the expected format, was $result")
}
}
}
it should "support UTF-8 as input and output format" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "utf8Test"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))
}
val utf8 = "«ταБЬℓσö»: 1<2 & 4+1>³, now 20%€§$ off!"
val run = wsk.action.invoke(name, Map("payload" -> utf8.toJson))
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
activation.logs.get.mkString(" ") should include(s"hello, $utf8")
}
}
it should "invoke action with large code" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "big-hello"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
val filePath = TestUtils.getTestActionFilename("hello.js")
val code = FileUtils.readFileToString(new File(filePath), StandardCharsets.UTF_8)
val largeCode = code + " " * (WhiskProperties.getMaxActionSizeMB * FileUtils.ONE_MB).toInt
val tmpFile = File.createTempFile("whisk", ".js")
FileUtils.write(tmpFile, largeCode, StandardCharsets.UTF_8)
val result = action.create(name, Some(tmpFile.getAbsolutePath))
tmpFile.delete()
result
}
val hello = "hello"
val run = wsk.action.invoke(name, Map("payload" -> hello.toJson))
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
activation.logs.get.mkString(" ") should include(s"hello, $hello")
}
}
}