You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by dg...@apache.org on 2018/07/09 15:29:55 UTC

[incubator-openwhisk-runtime-java] branch master updated: Logs and tests (#63)

This is an automated email from the ASF dual-hosted git repository.

dgrove pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk-runtime-java.git


The following commit(s) were added to refs/heads/master by this push:
     new 93dc2ee  Logs and tests (#63)
93dc2ee is described below

commit 93dc2ee2fec9ec500ea4f17877ae64aaaf4e9f10
Author: rodric rabbah <ro...@gmail.com>
AuthorDate: Mon Jul 9 11:29:52 2018 -0400

    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 => core/java8}/proxy/compileClassCache.sh   |   0
 .../java8}/proxy/gradle/wrapper/gradle-wrapper.jar | Bin
 .../proxy/gradle/wrapper/gradle-wrapper.properties |   0
 {java8 => core/java8}/proxy/gradlew                |   0
 {java8 => core/java8}/proxy/gradlew.bat            |   0
 .../main/java/openwhisk/java/action/JarLoader.java |   0
 .../src/main/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(-)

diff --git a/README.md b/README.md
index a3cee09..c591aef 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 8404c78..c7da0a1 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 3a4ae0f..539efcf 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 fd80333..dc6c861 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 @@ public class Proxy {
         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 class Proxy {
                 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 class Proxy {
                 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 class Proxy {
                 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 09c66ab..facd081 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 a3094a8..a990f33 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 {