From 93dc2ee2fec9ec500ea4f17877ae64aaaf4e9f10 Mon Sep 17 00:00:00 2001 From: rodric rabbah Date: Mon, 9 Jul 2018 11:29:52 -0400 Subject: [PATCH] Logs and tests (#63) 1. Emit log markers as expected by managed runtimes. 2. Handle missing code more graciously. 3. Adopt runtime tests upstream (which exposed the missing markers) 4. Move the proxy under core to match canonical structure of repo 5. Update CHANGELOG --- README.md | 4 +- {java8 => core/java8}/CHANGELOG.md | 5 + {java8 => core/java8}/Dockerfile | 0 {java8 => core/java8}/build.gradle | 2 +- {java8 => core/java8}/delete-build-run.sh | 0 {java8 => core/java8}/proxy/build.gradle | 0 .../java8}/proxy/compileClassCache.sh | 0 .../proxy/gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 {java8 => core/java8}/proxy/gradlew | 0 {java8 => core/java8}/proxy/gradlew.bat | 0 .../java/openwhisk/java/action/JarLoader.java | 0 .../java/openwhisk/java/action/Proxy.java | 48 ++- .../java/action/WhiskSecurityManager.java | 0 settings.gradle | 5 +- .../JavaActionContainerTests.scala | 383 ++++++++---------- 16 files changed, 215 insertions(+), 232 deletions(-) rename {java8 => core/java8}/CHANGELOG.md (93%) rename {java8 => core/java8}/Dockerfile (100%) rename {java8 => core/java8}/build.gradle (95%) rename {java8 => core/java8}/delete-build-run.sh (100%) rename {java8 => core/java8}/proxy/build.gradle (100%) rename {java8 => core/java8}/proxy/compileClassCache.sh (100%) rename {java8 => core/java8}/proxy/gradle/wrapper/gradle-wrapper.jar (100%) rename {java8 => core/java8}/proxy/gradle/wrapper/gradle-wrapper.properties (100%) rename {java8 => core/java8}/proxy/gradlew (100%) rename {java8 => core/java8}/proxy/gradlew.bat (100%) rename {java8 => core/java8}/proxy/src/main/java/openwhisk/java/action/JarLoader.java (100%) rename {java8 => core/java8}/proxy/src/main/java/openwhisk/java/action/Proxy.java (75%) rename {java8 => core/java8}/proxy/src/main/java/openwhisk/java/action/WhiskSecurityManager.java (100%) diff --git a/README.md b/README.md index a3cee098..c591aef5 100644 --- a/README.md +++ b/README.md @@ -95,14 +95,14 @@ wsk action invoke --result helloJava --param name World ## Local development ``` -./gradlew java8:distDocker +./gradlew core:java8:distDocker ``` This will produce the image `whisk/java8action` Build and Push image ``` docker login -./gradlew java8:distDocker -PdockerImagePrefix=$prefix-user -PdockerRegistry=docker.io +./gradlew core:java8:distDocker -PdockerImagePrefix=$prefix-user -PdockerRegistry=docker.io ``` Deploy OpenWhisk using ansible environment that contains the kind `java:8` diff --git a/java8/CHANGELOG.md b/core/java8/CHANGELOG.md similarity index 93% rename from java8/CHANGELOG.md rename to core/java8/CHANGELOG.md index 8404c78c..c7da0a14 100644 --- a/java8/CHANGELOG.md +++ b/core/java8/CHANGELOG.md @@ -20,6 +20,11 @@ # Java 8 OpenWhisk Runtime Container +## 1.1.1 +Changes: +- Adds log markers. +- Improve error handling for improper initialization. + ## 1.1.0 Changes: - Replaced oracle [jdk8u131-b11](http://download.oracle.com/otn-pub/java/jdk/"${VERSION}"u"${UPDATE}"-b"${BUILD}"/d54c1d3a095b4ff2b6607d096fa80163/server-jre-"${VERSION}"u"${UPDATE}"-linux-x64.tar.gz) with OpenJDK [adoptopenjdk/openjdk8-openj9:jdk8u162-b12_openj9-0.8.0](https://hub.docker.com/r/adoptopenjdk/openjdk8-openj9) diff --git a/java8/Dockerfile b/core/java8/Dockerfile similarity index 100% rename from java8/Dockerfile rename to core/java8/Dockerfile diff --git a/java8/build.gradle b/core/java8/build.gradle similarity index 95% rename from java8/build.gradle rename to core/java8/build.gradle index 3a4ae0f1..539efcf4 100644 --- a/java8/build.gradle +++ b/core/java8/build.gradle @@ -16,4 +16,4 @@ */ ext.dockerImageName = 'java8action' -apply from: '../gradle/docker.gradle' +apply from: '../../gradle/docker.gradle' diff --git a/java8/delete-build-run.sh b/core/java8/delete-build-run.sh similarity index 100% rename from java8/delete-build-run.sh rename to core/java8/delete-build-run.sh diff --git a/java8/proxy/build.gradle b/core/java8/proxy/build.gradle similarity index 100% rename from java8/proxy/build.gradle rename to core/java8/proxy/build.gradle diff --git a/java8/proxy/compileClassCache.sh b/core/java8/proxy/compileClassCache.sh similarity index 100% rename from java8/proxy/compileClassCache.sh rename to core/java8/proxy/compileClassCache.sh diff --git a/java8/proxy/gradle/wrapper/gradle-wrapper.jar b/core/java8/proxy/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from java8/proxy/gradle/wrapper/gradle-wrapper.jar rename to core/java8/proxy/gradle/wrapper/gradle-wrapper.jar diff --git a/java8/proxy/gradle/wrapper/gradle-wrapper.properties b/core/java8/proxy/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from java8/proxy/gradle/wrapper/gradle-wrapper.properties rename to core/java8/proxy/gradle/wrapper/gradle-wrapper.properties diff --git a/java8/proxy/gradlew b/core/java8/proxy/gradlew similarity index 100% rename from java8/proxy/gradlew rename to core/java8/proxy/gradlew diff --git a/java8/proxy/gradlew.bat b/core/java8/proxy/gradlew.bat similarity index 100% rename from java8/proxy/gradlew.bat rename to core/java8/proxy/gradlew.bat diff --git a/java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java b/core/java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java similarity index 100% rename from java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java rename to core/java8/proxy/src/main/java/openwhisk/java/action/JarLoader.java diff --git a/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java b/core/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java similarity index 75% rename from java8/proxy/src/main/java/openwhisk/java/action/Proxy.java rename to core/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java index fd803331..dc6c8618 100644 --- a/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java +++ b/core/java8/proxy/src/main/java/openwhisk/java/action/Proxy.java @@ -67,10 +67,19 @@ private static void writeError(HttpExchange t, String errorMessage) throws IOExc writeResponse(t, 502, message.toString()); } + private static void writeLogMarkers() { + System.out.println("XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX"); + System.err.println("XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX"); + System.out.flush(); + System.err.flush(); + } + private class InitHandler implements HttpHandler { public void handle(HttpExchange t) throws IOException { if (loader != null) { - Proxy.writeError(t, "Cannot initialize the action more than once."); + String errorMessage = "Cannot initialize the action more than once."; + System.err.println(errorMessage); + Proxy.writeError(t, errorMessage); return; } @@ -80,26 +89,34 @@ public void handle(HttpExchange t) throws IOException { JsonElement ie = parser.parse(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))); JsonObject inputObject = ie.getAsJsonObject(); - JsonObject message = inputObject.getAsJsonObject("value"); - String mainClass = message.getAsJsonPrimitive("main").getAsString(); - String base64Jar = message.getAsJsonPrimitive("code").getAsString(); + if (inputObject.has("value")) { + JsonObject message = inputObject.getAsJsonObject("value"); + if (message.has("main") && message.has("code")) { + String mainClass = message.getAsJsonPrimitive("main").getAsString(); + String base64Jar = message.getAsJsonPrimitive("code").getAsString(); - // FIXME: this is obviously not very useful. The idea is that we - // will implement/use a streaming parser for the incoming JSON object so that we - // can stream the contents of the jar straight to a file. - InputStream jarIs = new ByteArrayInputStream(base64Jar.getBytes(StandardCharsets.UTF_8)); + // FIXME: this is obviously not very useful. The idea is that we + // will implement/use a streaming parser for the incoming JSON object so that we + // can stream the contents of the jar straight to a file. + InputStream jarIs = new ByteArrayInputStream(base64Jar.getBytes(StandardCharsets.UTF_8)); - // Save the bytes to a file. - Path jarPath = JarLoader.saveBase64EncodedFile(jarIs); + // Save the bytes to a file. + Path jarPath = JarLoader.saveBase64EncodedFile(jarIs); - // Start up the custom classloader. This also checks that the - // main method exists. - loader = new JarLoader(jarPath, mainClass); + // Start up the custom classloader. This also checks that the + // main method exists. + loader = new JarLoader(jarPath, mainClass); + + Proxy.writeResponse(t, 200, "OK"); + return; + } + } - Proxy.writeResponse(t, 200, "OK"); + Proxy.writeError(t, "Missing main/no code to execute."); return; } catch (Exception e) { e.printStackTrace(System.err); + writeLogMarkers(); Proxy.writeError(t, "An error has occurred (see logs for details): " + e); return; } @@ -137,7 +154,7 @@ public void handle(HttpExchange t) throws IOException { JsonObject output = loader.invokeMain(inputObject, env); // User code finished running here. - if(output == null) { + if (output == null) { throw new NullPointerException("The action returned null"); } @@ -154,6 +171,7 @@ public void handle(HttpExchange t) throws IOException { e.printStackTrace(System.err); Proxy.writeError(t, "An error has occurred (see logs for details): " + e); } finally { + writeLogMarkers(); System.setSecurityManager(sm); Thread.currentThread().setContextClassLoader(cl); } diff --git a/java8/proxy/src/main/java/openwhisk/java/action/WhiskSecurityManager.java b/core/java8/proxy/src/main/java/openwhisk/java/action/WhiskSecurityManager.java similarity index 100% rename from java8/proxy/src/main/java/openwhisk/java/action/WhiskSecurityManager.java rename to core/java8/proxy/src/main/java/openwhisk/java/action/WhiskSecurityManager.java diff --git a/settings.gradle b/settings.gradle index 09c66ab8..facd081f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,9 +17,8 @@ include 'tests' -include 'java8' -include 'java8:proxy' - +include 'core:java8' +include 'core:java8:proxy' rootProject.name = 'runtime-java' diff --git a/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala b/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala index a3094a81..a990f33d 100644 --- a/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala +++ b/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala @@ -19,8 +19,6 @@ package actionContainers import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner -import org.scalatest.FlatSpec -import org.scalatest.Matchers import common.WskActorSystem import spray.json.DefaultJsonProtocol._ import spray.json._ @@ -28,182 +26,123 @@ import actionContainers.ResourceHelpers.JarBuilder import actionContainers.ActionContainer.withContainer @RunWith(classOf[JUnitRunner]) -class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSystem with ActionProxyContainerTestUtils { +class JavaActionContainerTests extends BasicActionRunnerTests with WskActorSystem { // Helpers specific to java actions - def withJavaContainer(code: ActionContainer => Unit, env: Map[String, String] = Map.empty) = - withContainer("java8action", env)(code) - - override def initPayload(mainClass: String, jar64: String) = - JsObject( - "value" -> JsObject("name" -> JsString("dummyAction"), "main" -> JsString(mainClass), "code" -> JsString(jar64))) + override def withActionContainer(env: Map[String, String] = Map.empty)( + code: ActionContainer => Unit): (String, String) = withContainer("java8action", env)(code) behavior of "Java action" - it should s"run a java snippet and confirm expected environment variables" in { - val props = Seq( - "api_host" -> "xyz", - "api_key" -> "abc", - "namespace" -> "zzz", - "action_name" -> "xxx", - "activation_id" -> "iii", - "deadline" -> "123") - val env = props.map { case (k, v) => s"__OW_${k.toUpperCase}" -> v } - val (out, err) = - withJavaContainer( - { c => - val jar = JarBuilder.mkBase64Jar( - Seq("example", "HelloWhisk.java") -> - """ - | package example; - | - | import com.google.gson.JsonObject; - | - | public class HelloWhisk { - | public static JsonObject main(JsonObject args) { - | JsonObject response = new JsonObject(); - | response.addProperty("api_host", System.getenv("__OW_API_HOST")); - | response.addProperty("api_key", System.getenv("__OW_API_KEY")); - | response.addProperty("namespace", System.getenv("__OW_NAMESPACE")); - | response.addProperty("action_name", System.getenv("__OW_ACTION_NAME")); - | response.addProperty("activation_id", System.getenv("__OW_ACTIVATION_ID")); - | response.addProperty("deadline", System.getenv("__OW_DEADLINE")); - | return response; - | } - | } - """.stripMargin.trim) - - val (initCode, _) = c.init(initPayload("example.HelloWhisk", jar)) - initCode should be(200) - - val (runCode, out) = c.run(runPayload(JsObject(), Some(props.toMap.toJson.asJsObject))) - runCode should be(200) - props.map { - case (k, v) => out.get.fields(k) shouldBe JsString(v) - - } - }, - env.take(1).toMap) + override val testNoSourceOrExec = { + TestConfig("") + } - out.trim shouldBe empty - err.trim shouldBe empty + override val testNotReturningJson = { + // skip this test since and add own below (see Nuller) + TestConfig("", skipTest = true) } - it should "support valid flows" in { - val (out, err) = withJavaContainer { c => - val jar = JarBuilder.mkBase64Jar( + override val testEnv = { + TestConfig( + JarBuilder.mkBase64Jar( Seq("example", "HelloWhisk.java") -> """ - | package example; - | - | import com.google.gson.JsonObject; - | - | public class HelloWhisk { - | public static JsonObject main(JsonObject args) { - | String name = args.getAsJsonPrimitive("name").getAsString(); - | JsonObject response = new JsonObject(); - | response.addProperty("greeting", "Hello " + name + "!"); - | return response; - | } - | } - """.stripMargin.trim) - - val (initCode, _) = c.init(initPayload("example.HelloWhisk", jar)) - initCode should be(200) - - val (runCode1, out1) = c.run(runPayload(JsObject("name" -> JsString("Whisk")))) - runCode1 should be(200) - out1 should be(Some(JsObject("greeting" -> JsString("Hello Whisk!")))) - - val (runCode2, out2) = c.run(runPayload(JsObject("name" -> JsString("ksihW")))) - runCode2 should be(200) - out2 should be(Some(JsObject("greeting" -> JsString("Hello ksihW!")))) - } - - out.trim shouldBe empty - err.trim shouldBe empty + | package example; + | + | import com.google.gson.JsonObject; + | + | public class HelloWhisk { + | public static JsonObject main(JsonObject args) { + | JsonObject response = new JsonObject(); + | response.addProperty("api_host", System.getenv("__OW_API_HOST")); + | response.addProperty("api_key", System.getenv("__OW_API_KEY")); + | response.addProperty("namespace", System.getenv("__OW_NAMESPACE")); + | response.addProperty("action_name", System.getenv("__OW_ACTION_NAME")); + | response.addProperty("activation_id", System.getenv("__OW_ACTIVATION_ID")); + | response.addProperty("deadline", System.getenv("__OW_DEADLINE")); + | return response; + | } + | } + """.stripMargin.trim), + main = "example.HelloWhisk") } - it should "not allow initialization twice" in { - val (out, err) = withJavaContainer { c => - val jar = JarBuilder.mkBase64Jar( + override val testEcho = { + TestConfig( + JarBuilder.mkBase64Jar( Seq("example", "HelloWhisk.java") -> """ - | package example; - | - | import com.google.gson.JsonObject; - | - | public class HelloWhisk { - | public static JsonObject main(JsonObject args) { - | return args; - | } - | } - """.stripMargin.trim) - - val (initCode, _) = c.init(initPayload("example.HelloWhisk", jar)) - initCode should be(200) - - val (initCode2, out2) = c.init(initPayload("example.HelloWhisk", jar)) - initCode2 should (be < 200 or be > 299) // the code does not matter, just cannot be 20x - out2 should be(Some(JsObject("error" -> JsString("Cannot initialize the action more than once.")))) - } - - out.trim shouldBe empty - err.trim shouldBe empty + | package example; + | + | import com.google.gson.JsonObject; + | + | public class HelloWhisk { + | public static JsonObject main(JsonObject args) { + | System.out.println("hello stdout"); + | System.err.println("hello stderr"); + | return args; + | } + | } + """.stripMargin.trim), + "example.HelloWhisk") } - it should "support valid actions with non 'main' names" in { - val (out, err) = withJavaContainer { c => - val jar = JarBuilder.mkBase64Jar( + override val testUnicode = { + TestConfig( + JarBuilder.mkBase64Jar( Seq("example", "HelloWhisk.java") -> """ - | package example; - | - | import com.google.gson.JsonObject; - | - | public class HelloWhisk { - | public static JsonObject hello(JsonObject args) { - | String name = args.getAsJsonPrimitive("name").getAsString(); - | JsonObject response = new JsonObject(); - | response.addProperty("greeting", "Hello " + name + "!"); - | return response; - | } - | } - """.stripMargin.trim) + | package example; + | + | import com.google.gson.JsonObject; + | + | public class HelloWhisk { + | public static JsonObject main(JsonObject args) { + | String delimiter = args.getAsJsonPrimitive("delimiter").getAsString(); + | JsonObject response = new JsonObject(); + | String str = delimiter + " ☃ " + delimiter; + | System.out.println(str); + | response.addProperty("winter", str); + | return response; + | } + | } + """.stripMargin), + "example.HelloWhisk") + } - val (initCode, _) = c.init(initPayload("example.HelloWhisk#hello", jar)) - initCode should be(200) + def echo(main: String = "main") = { + JarBuilder.mkBase64Jar( + Seq("example", "HelloWhisk.java") -> + s""" + | package example; + | + | import com.google.gson.JsonObject; + | + | public class HelloWhisk { + | public static JsonObject $main(JsonObject args) { + | return args; + | } + | } + """.stripMargin.trim) + } - val (runCode, out) = c.run(runPayload(JsObject("name" -> JsString("Whisk")))) - runCode should be(200) - out should be(Some(JsObject("greeting" -> JsString("Hello Whisk!")))) - } + override val testInitCannotBeCalledMoreThanOnce = { + TestConfig(echo(), "example.HelloWhisk") + } - out.trim shouldBe empty - err.trim shouldBe empty + override val testEntryPointOtherThanMain = { + TestConfig(echo("naim"), "example.HelloWhisk#naim") } - it should "report an error if explicit 'main' is not found" in { - val (out, err) = withJavaContainer { c => - val jar = JarBuilder.mkBase64Jar( - Seq("example", "HelloWhisk.java") -> - """ - | package example; - | - | import com.google.gson.JsonObject; - | - | public class HelloWhisk { - | public static JsonObject hello(JsonObject args) { - | String name = args.getAsJsonPrimitive("name").getAsString(); - | JsonObject response = new JsonObject(); - | response.addProperty("greeting", "Hello " + name + "!"); - | return response; - | } - | } - """.stripMargin.trim) + override val testLargeInput = { + TestConfig(echo(), "example.HelloWhisk") + } - Seq("", "x", "!", "#", "#main", "#bogus").foreach { m => - val (initCode, out) = c.init(initPayload(s"example.HelloWhisk$m", jar)) + Seq("", "x", "!", "#", "#main", "#bogus").foreach { m => + it should s"report an error if explicit 'main' is not found ($m)" in { + val (out, err) = withActionContainer() { c => + val (initCode, out) = c.init(initPayload(echo("hello"), s"example.HelloWhisk$m")) initCode shouldBe 502 out shouldBe { @@ -215,44 +154,17 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste Some(JsObject("error" -> s"An error has occurred (see logs for details): $error".toJson)) } } - } - - out.trim shouldBe empty - err.trim should not be empty - } - - it should "handle unicode in source, input params, logs, and result" in { - val (out, err) = withJavaContainer { c => - val jar = JarBuilder.mkBase64Jar( - Seq("example", "HelloWhisk.java") -> - """ - | package example; - | - | import com.google.gson.JsonObject; - | - | public class HelloWhisk { - | public static JsonObject main(JsonObject args) { - | String delimiter = args.getAsJsonPrimitive("delimiter").getAsString(); - | JsonObject response = new JsonObject(); - | String str = delimiter + " ☃ " + delimiter; - | System.out.println(str); - | response.addProperty("winter", str); - | return response; - | } - | } - """.stripMargin) - val (initCode, _) = c.init(initPayload("example.HelloWhisk", jar)) - val (runCode, runRes) = c.run(runPayload(JsObject("delimiter" -> JsString("❄")))) - runRes.get.fields.get("winter") shouldBe Some(JsString("❄ ☃ ❄")) + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should not be empty + }) } - - out should include("❄ ☃ ❄") - err.trim shouldBe empty } it should "fail to initialize with bad code" in { - val (out, err) = withJavaContainer { c => + val (out, err) = withActionContainer() { c => // This is valid zip file containing a single file, but not a valid // jar file. val brokenJar = ("UEsDBAoAAAAAAPxYbkhT4iFbCgAAAAoAAAANABwAbm90YWNsYXNzZmlsZVV" + @@ -261,17 +173,19 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste "GFzc2ZpbGVVVAUAA8zT5lZ1eAsAAQT1AQAABAAAAABQSwUGAAAAAAEAAQBT" + "AAAAUQAAAAAA") - val (initCode, _) = c.init(initPayload("example.Broken", brokenJar)) + val (initCode, _) = c.init(initPayload(brokenJar, "example.Broken")) initCode should not be (200) } // Somewhere, the logs should contain an exception. - val combined = out + err - combined.toLowerCase should include("exception") + checkStreams(out, err, { + case (o, e) => + (o + e).toLowerCase should include("exception") + }) } it should "return some error on action error" in { - val (out, err) = withJavaContainer { c => + val (out, err) = withActionContainer() { c => val jar = JarBuilder.mkBase64Jar( Seq("example", "HelloWhisk.java") -> """ @@ -286,22 +200,24 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste | } """.stripMargin.trim) - val (initCode, _) = c.init(initPayload("example.HelloWhisk", jar)) + val (initCode, _) = c.init(initPayload(jar, "example.HelloWhisk")) initCode should be(200) - val (runCode, runRes) = c.run(runPayload(JsObject())) + val (runCode, runRes) = c.run(runPayload(JsObject.empty)) runCode should not be (200) runRes shouldBe defined runRes.get.fields.get("error") shouldBe defined } - val combined = out + err - combined.toLowerCase should include("exception") + checkStreams(out, err, { + case (o, e) => + (o + e).toLowerCase should include("exception") + }) } it should "support application errors" in { - val (out, err) = withJavaContainer { c => + val (out, err) = withActionContainer() { c => val jar = JarBuilder.mkBase64Jar( Seq("example", "Error.java") -> """ @@ -318,22 +234,55 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste | } """.stripMargin.trim) - val (initCode, _) = c.init(initPayload("example.Error", jar)) + val (initCode, _) = c.init(initPayload(jar, "example.Error")) initCode should be(200) - val (runCode, runRes) = c.run(runPayload(JsObject())) + val (runCode, runRes) = c.run(runPayload(JsObject.empty)) runCode should be(200) // action writer returning an error is OK runRes shouldBe defined runRes.get.fields.get("error") shouldBe defined } - val combined = out + err - combined.trim shouldBe empty + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "support main in default package" in { + val (out, err) = withActionContainer() { c => + val jar = JarBuilder.mkBase64Jar( + Seq("", "HelloWhisk.java") -> + """ + | import com.google.gson.JsonObject; + | + | public class HelloWhisk { + | public static JsonObject main(JsonObject args) throws Exception { + | return args; + | } + | } + """.stripMargin.trim) + + val (initCode, _) = c.init(initPayload(jar, "HelloWhisk")) + initCode should be(200) + + val args = JsObject("a" -> "A".toJson) + val (runCode, runRes) = c.run(runPayload(args)) + runCode should be(200) + runRes shouldBe Some(args) + } + + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) } it should "survive System.exit" in { - val (out, err) = withJavaContainer { c => + val (out, err) = withActionContainer() { c => val jar = JarBuilder.mkBase64Jar( Seq("example", "Quitter.java") -> """ @@ -349,22 +298,24 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste | } """.stripMargin.trim) - val (initCode, _) = c.init(initPayload("example.Quitter", jar)) + val (initCode, _) = c.init(initPayload(jar, "example.Quitter")) initCode should be(200) - val (runCode, runRes) = c.run(runPayload(JsObject())) + val (runCode, runRes) = c.run(runPayload(JsObject.empty)) runCode should not be (200) runRes shouldBe defined runRes.get.fields.get("error") shouldBe defined } - val combined = out + err - combined.toLowerCase should include("system.exit") + checkStreams(out, err, { + case (o, e) => + (o + e).toLowerCase should include("system.exit") + }) } it should "enforce that the user returns an object" in { - withJavaContainer { c => + val (out, err) = withActionContainer() { c => val jar = JarBuilder.mkBase64Jar( Seq("example", "Nuller.java") -> """ @@ -379,15 +330,20 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste | } """.stripMargin.trim) - val (initCode, _) = c.init(initPayload("example.Nuller", jar)) + val (initCode, _) = c.init(initPayload(jar, "example.Nuller")) initCode should be(200) - val (runCode, runRes) = c.run(runPayload(JsObject())) + val (runCode, runRes) = c.run(runPayload(JsObject.empty)) runCode should not be (200) runRes shouldBe defined runRes.get.fields.get("error") shouldBe defined } + + checkStreams(out, err, { + case (o, e) => + (o + e).toLowerCase should include("the action returned null") + }) } val dynamicLoadingJar = JarBuilder.mkBase64Jar( @@ -433,8 +389,8 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste """.stripMargin.trim)) def classLoaderTest(param: String) = { - val (out, err) = withJavaContainer { c => - val (initCode, _) = c.init(initPayload("example.EntryPoint", dynamicLoadingJar)) + val (out, err) = withActionContainer() { c => + val (initCode, _) = c.init(initPayload(dynamicLoadingJar, "example.EntryPoint")) initCode should be(200) val (runCode, runRes) = c.run(runPayload(JsObject("classLoader" -> JsString(param)))) @@ -443,7 +399,12 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste runRes shouldBe defined runRes.get.fields.get("message") shouldBe Some(JsString("dynamic!")) } - (out ++ err).trim shouldBe empty + + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) } it should "support loading classes from the current classloader" in {