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

ningyougang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new 62b8a50d4 Support array result for common action and sequence action 
(#5290)
62b8a50d4 is described below

commit 62b8a50d447c066a4e579ed1d8c85eef02e37e41
Author: ningyougang <[email protected]>
AuthorDate: Mon Aug 1 17:32:12 2022 +0800

    Support array result for common action and sequence action (#5290)
    
    * Support array result
    
    * Make controller accept json array
    
    * Make elasticsearch support json array
    
    Couchdb already suports
    
    * Make go runtime test cases due to depend on this
    
    * Add test case for array result for nodejs runtime
    
    * Make sequence action to support array result
    
    * Optimize sequence action to support array result
    
    * Fix test case for sequence action feature
    
    * Add test case for sequence action
    
    This test case is just for nodejs
    
    * Add extra method runForJsArray for runtime tests
    
    * Fix build error
    
    * Fix review comment
---
 .../apache/openwhisk/core/connector/Message.scala  | 12 +++--
 .../core/containerpool/AkkaContainerClient.scala   | 33 ++++++++++++
 .../openwhisk/core/containerpool/Container.scala   |  4 +-
 .../ElasticSearchActivationStore.scala             | 13 +++--
 .../openwhisk/core/entity/ActivationResult.scala   | 15 +++++-
 .../org/apache/openwhisk/http/ErrorResponse.scala  |  2 +-
 .../apache/openwhisk/core/controller/Actions.scala |  8 ++-
 .../controller/actions/PostActionActivation.scala  |  2 +-
 .../core/controller/actions/PrimitiveActions.scala | 32 ++++++++----
 .../core/controller/actions/SequenceActions.scala  | 20 ++++---
 .../core/containerpool/ContainerProxy.scala        | 25 +++++----
 tests/dat/actions/helloArray.js                    | 24 +++++++++
 tests/dat/actions/sort-array.js                    | 28 ++++++++++
 tests/dat/actions/split-array.js                   | 31 +++++++++++
 .../scala/actionContainers/ActionContainer.scala   | 13 +++++
 .../actionContainers/BasicActionRunnerTests.scala  |  2 +-
 tests/src/test/scala/common/WskTestHelpers.scala   |  2 +-
 .../scala/invokerShoot/ShootInvokerTests.scala     |  2 +-
 .../core/cli/test/WskRestBasicUsageTests.scala     | 10 ++--
 .../containerpool/test/ContainerProxyTests.scala   | 10 ++--
 .../test/FunctionPullingContainerProxyTests.scala  |  8 +--
 .../core/controller/test/ConductorsApiTests.scala  | 61 ++++++++++++++--------
 .../core/controller/test/WebActionsApiTests.scala  |  8 ++-
 .../openwhisk/core/limits/ActionLimitsTests.scala  | 10 ++--
 .../test/scala/system/basic/WskActionTests.scala   | 16 +++++-
 .../scala/system/basic/WskConductorTests.scala     |  4 +-
 .../test/scala/system/basic/WskSequenceTests.scala | 51 +++++++++++++-----
 .../test/scala/system/basic/WskUnicodeTests.scala  |  4 +-
 28 files changed, 342 insertions(+), 108 deletions(-)

diff --git 
a/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala 
b/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala
index 11749bed0..d3c550eb2 100644
--- 
a/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala
+++ 
b/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala
@@ -57,7 +57,7 @@ case class ActivationMessage(override val transid: 
TransactionId,
                              activationId: ActivationId,
                              rootControllerIndex: ControllerInstanceId,
                              blocking: Boolean,
-                             content: Option[JsObject],
+                             content: Option[JsValue],
                              initArgs: Set[String] = Set.empty,
                              lockedArgs: Map[String, String] = Map.empty,
                              cause: Option[ActivationId] = None,
@@ -380,9 +380,13 @@ object Activation extends DefaultJsonProtocol {
 
   /** Get "StatusCode" from result response set by action developer * */
   def userDefinedStatusCode(result: Option[JsValue]): Option[Int] = {
-    val statusCode = JsHelpers
-      .getFieldPath(result.get.asJsObject, ERROR_FIELD, "statusCode")
-      .orElse(JsHelpers.getFieldPath(result.get.asJsObject, "statusCode"))
+    val statusCode: Option[JsValue] = result match {
+      case Some(JsObject(fields)) =>
+        JsHelpers
+          .getFieldPath(JsObject(fields), ERROR_FIELD, "statusCode")
+          .orElse(JsHelpers.getFieldPath(JsObject(fields), "statusCode"))
+      case _ => None
+    }
     statusCode.map {
       case value => 
Try(value.convertTo[BigInt].intValue).toOption.getOrElse(BadRequest.intValue)
     }
diff --git 
a/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/AkkaContainerClient.scala
 
b/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/AkkaContainerClient.scala
index 4e06d6111..ea5978bb7 100644
--- 
a/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/AkkaContainerClient.scala
+++ 
b/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/AkkaContainerClient.scala
@@ -201,6 +201,19 @@ object AkkaContainerClient {
     result
   }
 
+  /** A helper method to post one single request to a connection. Used for 
container tests. */
+  def postForJsArray(host: String, port: Int, endPoint: String, content: 
JsValue, timeout: FiniteDuration)(
+    implicit logging: Logging,
+    as: ActorSystem,
+    ec: ExecutionContext,
+    tid: TransactionId): (Int, Option[JsArray]) = {
+    val connection = new AkkaContainerClient(host, port, timeout, 1.MB, 1.MB, 
1)
+    val response = executeRequestForJsArray(connection, endPoint, content)
+    val result = Await.result(response, timeout + 10.seconds) //additional 
timeout to complete futures
+    connection.close()
+    result
+  }
+
   /** A helper method to post multiple concurrent requests to a single 
connection. Used for container tests. */
   def concurrentPost(host: String, port: Int, endPoint: String, contents: 
Seq[JsValue], timeout: FiniteDuration)(
     implicit logging: Logging,
@@ -233,4 +246,24 @@ object AkkaContainerClient {
 
     res
   }
+
+  private def executeRequestForJsArray(connection: AkkaContainerClient, 
endpoint: String, content: JsValue)(
+    implicit logging: Logging,
+    as: ActorSystem,
+    ec: ExecutionContext,
+    tid: TransactionId): Future[(Int, Option[JsArray])] = {
+
+    val res = connection
+      .post(endpoint, content, true)
+      .map({
+        case Right(r)                   => (r.statusCode, 
Try(r.entity.parseJson.convertTo[JsArray]).toOption)
+        case Left(NoResponseReceived()) => throw new IllegalStateException("no 
response from container")
+        case Left(Timeout(_))           => throw new 
java.util.concurrent.TimeoutException()
+        case Left(ConnectionError(t: java.net.SocketTimeoutException)) =>
+          throw new java.util.concurrent.TimeoutException()
+        case Left(ConnectionError(t)) => throw new 
IllegalStateException(t.getMessage)
+      })
+
+    res
+  }
 }
diff --git 
a/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/Container.scala
 
b/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/Container.scala
index 6b557f3a3..5290f3e4a 100644
--- 
a/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/Container.scala
+++ 
b/common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/Container.scala
@@ -26,7 +26,7 @@ import akka.util.ByteString
 import pureconfig._
 import pureconfig.generic.auto._
 import spray.json.DefaultJsonProtocol._
-import spray.json.JsObject
+import spray.json.{JsObject, JsValue}
 import org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}
 import org.apache.openwhisk.core.ConfigKeys
 import 
org.apache.openwhisk.core.entity.ActivationResponse.{ContainerConnectionError, 
ContainerResponse}
@@ -159,7 +159,7 @@ trait Container {
   }
 
   /** Runs code in the container. Thread-safe - caller may invoke concurrently 
for concurrent activation processing. */
-  def run(parameters: JsObject,
+  def run(parameters: JsValue,
           environment: JsObject,
           timeout: FiniteDuration,
           maxConcurrent: Int,
diff --git 
a/common/scala/src/main/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStore.scala
 
b/common/scala/src/main/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStore.scala
index 06de2af41..097b26b39 100644
--- 
a/common/scala/src/main/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStore.scala
+++ 
b/common/scala/src/main/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStore.scala
@@ -388,8 +388,8 @@ class ElasticSearchActivationStore(
     
restoreAnnotations(restoreResponse(hit.sourceAsString.parseJson.asJsObject)).convertTo[WhiskActivation]
   }
 
-  private def restoreAnnotations(js: JsObject): JsObject = {
-    val annotations = js.fields
+  private def restoreAnnotations(js: JsValue): JsObject = {
+    val annotations = js.asJsObject.fields
       .get("annotations")
       .map { anno =>
         Try {
@@ -399,10 +399,10 @@ class ElasticSearchActivationStore(
         }.getOrElse(JsArray.empty)
       }
       .getOrElse(JsArray.empty)
-    JsObject(js.fields.updated("annotations", annotations))
+    JsObject(js.asJsObject.fields.updated("annotations", annotations))
   }
 
-  private def restoreResponse(js: JsObject): JsObject = {
+  private def restoreResponse(js: JsObject): JsValue = {
     val response = js.fields
       .get("response")
       .map { res =>
@@ -412,7 +412,10 @@ class ElasticSearchActivationStore(
             .get("result")
             .map { r =>
               val JsString(data) = r
-              data.parseJson.asJsObject
+              data.parseJson match {
+                case JsArray(elements) => JsArray(elements)
+                case _                 => data.parseJson.asJsObject
+              }
             }
             .getOrElse(JsObject.empty)
           JsObject(temp.updated("result", result))
diff --git 
a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationResult.scala
 
b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationResult.scala
index c8a1ddfb2..42961425f 100644
--- 
a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationResult.scala
+++ 
b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationResult.scala
@@ -111,7 +111,7 @@ protected[core] object ActivationResponse extends 
DefaultJsonProtocol {
    * NOTE: the code is application error (since this response could be used as 
a response for the sequence
    * if the payload contains an error)
    */
-  protected[core] def payloadPlaceholder(payload: Option[JsObject]) = 
ActivationResponse(ApplicationError, payload)
+  protected[core] def payloadPlaceholder(payload: Option[JsValue]) = 
ActivationResponse(ApplicationError, payload)
 
   /**
    * Class of errors for invoker-container communication.
@@ -203,7 +203,7 @@ protected[core] object ActivationResponse extends 
DefaultJsonProtocol {
         truncated match {
           case None =>
             val sizeOpt = Option(str).map(_.length)
-            Try { str.parseJson.asJsObject } match {
+            Try { str.parseJson } match {
               case scala.util.Success(result @ JsObject(fields)) =>
                 // If the response is a JSON object container an error field, 
accept it as the response error.
                 val errorOpt = fields.get(ERROR_FIELD)
@@ -222,6 +222,17 @@ protected[core] object ActivationResponse extends 
DefaultJsonProtocol {
                   developerError(errorContent, sizeOpt)
                 }
 
+              case scala.util.Success(result @ JsArray(_)) =>
+                if (res.okStatus) {
+                  success(Some(result), sizeOpt)
+                } else {
+                  // Any non-200 code is treated as a container failure. We 
still need to check whether
+                  // there was a useful error message in there.
+                  val errorContent = invalidRunResponse(str).toJson
+                  //developerErrorWithLog(errorContent, sizeOpt, None)
+                  developerError(errorContent, sizeOpt)
+                }
+
               case scala.util.Success(notAnObj) =>
                 // This should affect only blackbox containers, since our own 
containers should already test for that.
                 developerError(invalidRunResponse(str), sizeOpt)
diff --git 
a/common/scala/src/main/scala/org/apache/openwhisk/http/ErrorResponse.scala 
b/common/scala/src/main/scala/org/apache/openwhisk/http/ErrorResponse.scala
index 35af54fa8..07818418e 100644
--- a/common/scala/src/main/scala/org/apache/openwhisk/http/ErrorResponse.scala
+++ b/common/scala/src/main/scala/org/apache/openwhisk/http/ErrorResponse.scala
@@ -204,7 +204,7 @@ object Messages {
   }
 
   def invalidRunResponse(actualResponse: String) = {
-    "The action did not produce a valid JSON response" + {
+    "The action did not produce a valid JSON or JSON Array response" + {
       Option(actualResponse) filter { _.nonEmpty } map { s =>
         s": $s"
       } getOrElse "."
diff --git 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Actions.scala
 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Actions.scala
index a6e4554b0..7820dfa63 100644
--- 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Actions.scala
+++ 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Actions.scala
@@ -289,8 +289,12 @@ trait WhiskActionsApi extends WhiskCollectionAPI with 
PostActionActivation with
           complete(Accepted, activationId.toJsObject)
         }
       case Success(Right(activation)) =>
-        val response = if (result) activation.resultAsJson else 
activation.toExtendedJson()
-
+        val response = activation.response.result match {
+          case Some(JsArray(elements)) =>
+            JsArray(elements)
+          case _ =>
+            if (result) activation.resultAsJson else 
activation.toExtendedJson()
+        }
         respondWithActivationIdHeader(activation.activationId) {
           if (activation.response.isSuccess) {
             complete(OK, response)
diff --git 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PostActionActivation.scala
 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PostActionActivation.scala
index 1b14fc3a5..db9a38d42 100644
--- 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PostActionActivation.scala
+++ 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PostActionActivation.scala
@@ -47,7 +47,7 @@ protected[core] trait PostActionActivation extends 
PrimitiveActions with Sequenc
   protected[controller] def invokeAction(
     user: Identity,
     action: WhiskActionMetaData,
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForResponse: Option[FiniteDuration],
     cause: Option[ActivationId])(implicit transid: TransactionId): 
Future[Either[ActivationId, WhiskActivation]] = {
     action.toExecutableWhiskAction match {
diff --git 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
index 19ca12e9f..694ef7fa0 100644
--- 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
+++ 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
@@ -80,7 +80,7 @@ protected[actions] trait PrimitiveActions {
     user: Identity,
     action: WhiskActionMetaData,
     components: Vector[FullyQualifiedEntityName],
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForOutermostResponse: Option[FiniteDuration],
     cause: Option[ActivationId],
     topmost: Boolean,
@@ -109,7 +109,7 @@ protected[actions] trait PrimitiveActions {
   protected[actions] def invokeSingleAction(
     user: Identity,
     action: ExecutableWhiskActionMetaData,
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForResponse: Option[FiniteDuration],
     cause: Option[ActivationId])(implicit transid: TransactionId): 
Future[Either[ActivationId, WhiskActivation]] = {
 
@@ -152,12 +152,16 @@ protected[actions] trait PrimitiveActions {
   private def invokeSimpleAction(
     user: Identity,
     action: ExecutableWhiskActionMetaData,
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForResponse: Option[FiniteDuration],
     cause: Option[ActivationId])(implicit transid: TransactionId): 
Future[Either[ActivationId, WhiskActivation]] = {
 
     // merge package parameters with action (action parameters supersede), 
then merge in payload
-    val args = action.parameters merge payload
+    val args: Option[JsValue] = payload match {
+      case Some(JsObject(fields))  => action.parameters merge 
Some(JsObject(fields))
+      case Some(JsArray(elements)) => Some(JsArray(elements))
+      case _                       => Some(action.parameters.toJsObject)
+    }
     val activationId = activationIdFactory.make()
 
     val startActivation = transid.started(
@@ -169,6 +173,10 @@ protected[actions] trait PrimitiveActions {
     val startLoadbalancer =
       transid.started(this, LoggingMarkers.CONTROLLER_LOADBALANCER, s"action 
activation id: ${activationId}")
 
+    val keySet = payload match {
+      case Some(JsObject(fields)) => Some(fields.keySet)
+      case _                      => None
+    }
     val message = ActivationMessage(
       transid,
       FullyQualifiedEntityName(action.namespace, action.name, 
Some(action.version), action.binding),
@@ -179,7 +187,7 @@ protected[actions] trait PrimitiveActions {
       waitForResponse.isDefined,
       args,
       action.parameters.initParameters,
-      
action.parameters.lockedParameters(payload.map(_.fields.keySet).getOrElse(Set.empty)),
+      action.parameters.lockedParameters(keySet.getOrElse(Set.empty)),
       cause = cause,
       WhiskTracerProvider.tracer.getTraceContext(transid))
 
@@ -271,7 +279,7 @@ protected[actions] trait PrimitiveActions {
    */
   private def invokeComposition(user: Identity,
                                 action: ExecutableWhiskActionMetaData,
-                                payload: Option[JsObject],
+                                payload: Option[JsValue],
                                 waitForResponse: Option[FiniteDuration],
                                 cause: Option[ActivationId],
                                 accounting: Option[CompositionAccounting] = 
None)(
@@ -319,7 +327,7 @@ protected[actions] trait PrimitiveActions {
    * @param parentTid a parent transaction id
    */
   private def invokeConductor(user: Identity,
-                              payload: Option[JsObject],
+                              payload: Option[JsValue],
                               session: Session,
                               parentTid: TransactionId): 
Future[ActivationResponse] = {
 
@@ -330,9 +338,13 @@ protected[actions] trait PrimitiveActions {
       
Future.successful(ActivationResponse.applicationError(compositionIsTooLong))
     } else {
       // inject state into payload if any
-      val params = session.state
-        .map(state => Some(JsObject(payload.getOrElse(JsObject.empty).fields 
++ state.fields)))
-        .getOrElse(payload)
+      val params: Option[JsValue] = payload match {
+        case Some(JsObject(fields)) =>
+          session.state
+            .map(state => Some(JsObject(JsObject(fields).fields ++ 
state.fields)))
+            .getOrElse(payload)
+        case _ => None
+      }
 
       // invoke conductor action
       session.accounting.conductors += 1
diff --git 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/SequenceActions.scala
 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/SequenceActions.scala
index 0bc082bf3..4490cf158 100644
--- 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/SequenceActions.scala
+++ 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/SequenceActions.scala
@@ -71,7 +71,7 @@ protected[actions] trait SequenceActions {
   protected[actions] def invokeAction(
     user: Identity,
     action: WhiskActionMetaData,
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForResponse: Option[FiniteDuration],
     cause: Option[ActivationId])(implicit transid: TransactionId): 
Future[Either[ActivationId, WhiskActivation]]
 
@@ -93,7 +93,7 @@ protected[actions] trait SequenceActions {
     user: Identity,
     action: WhiskActionMetaData,
     components: Vector[FullyQualifiedEntityName],
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForOutermostResponse: Option[FiniteDuration],
     cause: Option[ActivationId],
     topmost: Boolean,
@@ -266,7 +266,7 @@ protected[actions] trait SequenceActions {
     user: Identity,
     seqAction: WhiskActionMetaData,
     seqActivationId: ActivationId,
-    inputPayload: Option[JsObject],
+    inputPayload: Option[JsValue],
     components: Vector[FullyQualifiedEntityName],
     cause: Option[ActivationId],
     atomicActionCnt: Int)(implicit transid: TransactionId): 
Future[SequenceAccounting] = {
@@ -347,7 +347,12 @@ protected[actions] trait SequenceActions {
       // the accounting no longer needs to hold a reference to it once the 
action is
       // invoked, so previousResponse.getAndSet(null) drops the reference at 
this point
       // which prevents dragging the previous response for the lifetime of the 
next activation
-      val inputPayload = 
accounting.previousResponse.getAndSet(null).result.map(_.asJsObject)
+      val previousResult = accounting.previousResponse.getAndSet(null).result
+      val inputPayload: Option[JsValue] = previousResult match {
+        case Some(JsObject(fields))  => Some(JsObject(fields))
+        case Some(JsArray(elements)) => Some(JsArray(elements))
+        case _                       => None
+      }
 
       // invoke the action by calling the right method depending on whether 
it's an atomic action or a sequence
       val futureWhiskActivationTuple = action.toExecutableWhiskAction match {
@@ -460,9 +465,10 @@ protected[actions] case class 
SequenceAccounting(atomicActionCnt: Int,
     // check conditions on payload that may lead to interrupting the execution 
of the sequence
     //     short-circuit the execution of the sequence iff the payload 
contains an error field
     //     and is the result of an action return, not the initial payload
-    val outputPayload = activation.response.result.map(_.asJsObject)
-    val payloadContent = outputPayload getOrElse JsObject.empty
-    val errorField = payloadContent.fields.get(ActivationResponse.ERROR_FIELD)
+    val errorField: Option[JsValue] = activation.response.result match {
+      case Some(JsObject(fields)) => fields.get(ActivationResponse.ERROR_FIELD)
+      case _                      => None
+    }
     val withinSeqLimit = newCnt <= maxSequenceCnt
 
     if (withinSeqLimit && errorField.isEmpty) {
diff --git 
a/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
 
b/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
index 1f8476c29..d5ae131b1 100644
--- 
a/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
+++ 
b/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
@@ -1082,25 +1082,28 @@ object ContainerProxy {
    * @param initArgs set of parameters to treat as initialization arguments
    * @return A partition of the arguments into an environment variables map 
and the JsObject argument to the action
    */
-  def partitionArguments(content: Option[JsObject], initArgs: Set[String]): 
(Map[String, JsValue], JsObject) = {
+  def partitionArguments(content: Option[JsValue], initArgs: Set[String]): 
(Map[String, JsValue], JsValue) = {
     content match {
-      case None                         => (Map.empty, JsObject.empty)
-      case Some(js) if initArgs.isEmpty => (Map.empty, js)
-      case Some(js) =>
-        val (env, args) = js.fields.partition(k => initArgs.contains(k._1))
+      case None                                       => (Map.empty, 
JsObject.empty)
+      case Some(JsArray(elements))                    => (Map.empty, 
JsArray(elements))
+      case Some(JsObject(fields)) if initArgs.isEmpty => (Map.empty, 
JsObject(fields))
+      case Some(JsObject(fields)) =>
+        val (env, args) = fields.partition(k => initArgs.contains(k._1))
         (env, JsObject(args))
     }
   }
 
-  def unlockArguments(content: Option[JsObject],
+  def unlockArguments(content: Option[JsValue],
                       lockedArgs: Map[String, String],
-                      decoder: ParameterEncryption): Option[JsObject] = {
-    content.map {
-      case JsObject(fields) =>
-        JsObject(fields.map {
+                      decoder: ParameterEncryption): Option[JsValue] = {
+    content match {
+      case Some(JsObject(fields)) =>
+        Some(JsObject(fields.map {
           case (k, v: JsString) if lockedArgs.contains(k) => (k -> 
decoder.encryptor(lockedArgs(k)).decrypt(v))
           case p                                          => p
-        })
+        }))
+      // keep the original for other type(e.g. JsArray)
+      case contentValue => contentValue
     }
   }
 }
diff --git a/tests/dat/actions/helloArray.js b/tests/dat/actions/helloArray.js
new file mode 100644
index 000000000..a112c5db4
--- /dev/null
+++ b/tests/dat/actions/helloArray.js
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * helloArray action
+ */
+
+function main(params) {
+    return [{"key1":"value1"},{"key2":"value2"}];
+}
diff --git a/tests/dat/actions/sort-array.js b/tests/dat/actions/sort-array.js
new file mode 100644
index 000000000..f031ca0b3
--- /dev/null
+++ b/tests/dat/actions/sort-array.js
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+/**
+ * Sort a set of lines.
+ * @param lines An array of strings to sort.
+ */
+function main(msg) {
+    var lines = msg || [];
+    console.log('sort before: ' + lines);
+    lines.sort();
+    console.log('sort after: ' + lines);
+    return lines;
+}
diff --git a/tests/dat/actions/split-array.js b/tests/dat/actions/split-array.js
new file mode 100644
index 000000000..f3f29fd23
--- /dev/null
+++ b/tests/dat/actions/split-array.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+/**
+ * Splits a string into an array of strings
+ * Return lines in an array.
+ * @param payload A string.
+ * @param separator The character, or the regular expression, to use for 
splitting the string
+ */
+function main(msg) {
+    var separator = msg.separator || /\r?\n/;
+    var payload = msg.payload.toString();
+    var lines = payload.split(separator);
+    // return array as next action's input
+    return lines;
+}
+
diff --git a/tests/src/test/scala/actionContainers/ActionContainer.scala 
b/tests/src/test/scala/actionContainers/ActionContainer.scala
index 28c6937d3..4e7b4e7fa 100644
--- a/tests/src/test/scala/actionContainers/ActionContainer.scala
+++ b/tests/src/test/scala/actionContainers/ActionContainer.scala
@@ -52,6 +52,7 @@ import org.apache.openwhisk.core.containerpool.Container
 trait ActionContainer {
   def init(value: JsValue): (Int, Option[JsObject])
   def run(value: JsValue): (Int, Option[JsObject])
+  def runForJsArray(value: JsValue): (Int, Option[JsArray])
   def runMultiple(values: Seq[JsValue])(implicit ec: ExecutionContext): 
Seq[(Int, Option[JsObject])]
 }
 
@@ -227,6 +228,7 @@ object ActionContainer {
     val mock = new ActionContainer {
       def init(value: JsValue): (Int, Option[JsObject]) = syncPost(ip, port, 
"/init", value)
       def run(value: JsValue): (Int, Option[JsObject]) = syncPost(ip, port, 
"/run", value)
+      def runForJsArray(value: JsValue): (Int, Option[JsArray]) = 
syncPostForJsArray(ip, port, "/run", value)
       def runMultiple(values: Seq[JsValue])(implicit ec: ExecutionContext): 
Seq[(Int, Option[JsObject])] =
         concurrentSyncPost(ip, port, "/run", values)
     }
@@ -252,6 +254,17 @@ object ActionContainer {
 
     org.apache.openwhisk.core.containerpool.AkkaContainerClient.post(host, 
port, endPoint, content, 30.seconds)
   }
+
+  private def syncPostForJsArray(host: String, port: Int, endPoint: String, 
content: JsValue)(
+    implicit logging: Logging,
+    as: ActorSystem): (Int, Option[JsArray]) = {
+
+    implicit val transid = TransactionId.testing
+
+    org.apache.openwhisk.core.containerpool.AkkaContainerClient
+      .postForJsArray(host, port, endPoint, content, 30.seconds)
+  }
+
   private def concurrentSyncPost(host: String, port: Int, endPoint: String, 
contents: Seq[JsValue])(
     implicit logging: Logging,
     as: ActorSystem): Seq[(Int, Option[JsObject])] = {
diff --git a/tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala 
b/tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala
index 547ceebf6..17727704e 100644
--- a/tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala
+++ b/tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala
@@ -173,7 +173,7 @@ trait BasicActionRunnerTests extends 
ActionProxyContainerTestUtils {
         initCode should be(200)
         val (runCode, out) = c.run(JsObject.empty)
         runCode should not be (200)
-        out should be(Some(JsObject("error" -> JsString("The action did not 
return a dictionary."))))
+        out should be(Some(JsObject("error" -> JsString("The action did not 
return a dictionary or array."))))
       }
 
       checkStreams(out, err, {
diff --git a/tests/src/test/scala/common/WskTestHelpers.scala 
b/tests/src/test/scala/common/WskTestHelpers.scala
index 419fc673a..69b01c6f4 100644
--- a/tests/src/test/scala/common/WskTestHelpers.scala
+++ b/tests/src/test/scala/common/WskTestHelpers.scala
@@ -76,7 +76,7 @@ object FullyQualifiedNames {
  * @param status a string used to indicate the status of the action
  * @param success a boolean value used to indicate whether the action is 
executed successfully or not
  */
-case class ActivationResponse(result: Option[JsObject], status: String, 
success: Boolean)
+case class ActivationResponse(result: Option[JsValue], status: String, 
success: Boolean)
 
 object ActivationResponse extends DefaultJsonProtocol {
   implicit val serdes = jsonFormat3(ActivationResponse.apply)
diff --git a/tests/src/test/scala/invokerShoot/ShootInvokerTests.scala 
b/tests/src/test/scala/invokerShoot/ShootInvokerTests.scala
index 4330695f5..28d7dea47 100644
--- a/tests/src/test/scala/invokerShoot/ShootInvokerTests.scala
+++ b/tests/src/test/scala/invokerShoot/ShootInvokerTests.scala
@@ -307,7 +307,7 @@ class ShootInvokerTests extends TestHelpers with 
WskTestHelpers with JsHelpers w
     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 {
+      result.asJsObject.getFields("stdout", "code") match {
         case Seq(JsString(stdout), JsNumber(code)) =>
           stdout should not include "bytes from"
           code.intValue should not be 0
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestBasicUsageTests.scala
 
b/tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestBasicUsageTests.scala
index 6e0cbb04f..8df5ad7ba 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestBasicUsageTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestBasicUsageTests.scala
@@ -224,7 +224,7 @@ class WskRestBasicUsageTests extends TestHelpers with 
WskTestHelpers with WskAct
 
       withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
         val response = activation.response
-        response.result.get.fields("error") shouldBe 
Messages.abnormalInitialization.toJson
+        response.result.get.asJsObject.fields("error") shouldBe 
Messages.abnormalInitialization.toJson
         response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.DeveloperError)
       }
   }
@@ -238,7 +238,7 @@ class WskRestBasicUsageTests extends TestHelpers with 
WskTestHelpers with WskAct
 
       withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
         val response = activation.response
-        response.result.get.fields("error") shouldBe 
Messages.timedoutActivation(3 seconds, true).toJson
+        response.result.get.asJsObject.fields("error") shouldBe 
Messages.timedoutActivation(3 seconds, true).toJson
         response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.DeveloperError)
       }
   }
@@ -252,7 +252,7 @@ class WskRestBasicUsageTests extends TestHelpers with 
WskTestHelpers with WskAct
 
       withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
         val response = activation.response
-        response.result.get.fields("error") shouldBe 
Messages.abnormalRun.toJson
+        response.result.get.asJsObject.fields("error") shouldBe 
Messages.abnormalRun.toJson
         response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.DeveloperError)
       }
   }
@@ -326,7 +326,7 @@ class WskRestBasicUsageTests extends TestHelpers with 
WskTestHelpers with WskAct
       val run = wsk.action.invoke(name)
       withActivation(wsk.activation, run) { activation =>
         activation.response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.DeveloperError)
-        activation.response.result.get
+        activation.response.result.get.asJsObject
           .fields("error") shouldBe s"Failed to pull container image 
'$containerName'.".toJson
         activation.annotations shouldBe defined
         val limits = 
activation.annotations.get.filter(_.fields("key").convertTo[String] == "limits")
@@ -775,7 +775,7 @@ class WskRestBasicUsageTests extends TestHelpers with 
WskTestHelpers with WskAct
         withActivation(wsk.activation, ruleActivation.activationId) { 
actionActivation =>
           actionActivation.response.result match {
             case Some(result) =>
-              result.fields.get("args") map { headers =>
+              result.asJsObject.fields.get("args") map { headers =>
                 headers.asJsObject.fields.get("__ow_headers") map { params =>
                   params.asJsObject.fields.get("foo") map { foo =>
                     foo shouldBe JsString("Bar")
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
 
b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
index 193f9f66a..1b4567a2d 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
@@ -1338,7 +1338,7 @@ class ContainerProxyTests
     timeout) {
     val container = new TestContainer {
       override def run(
-        parameters: JsObject,
+        parameters: JsValue,
         environment: JsObject,
         timeout: FiniteDuration,
         concurrent: Int,
@@ -1519,7 +1519,7 @@ class ContainerProxyTests
     timeout) {
     val container = new TestContainer {
       override def run(
-        parameters: JsObject,
+        parameters: JsValue,
         environment: JsObject,
         timeout: FiniteDuration,
         concurrent: Int,
@@ -1688,7 +1688,7 @@ class ContainerProxyTests
   it should "resend the job to the parent if /run fails connection after 
Paused -> Running" in within(timeout) {
     val container = new TestContainer {
       override def run(
-        parameters: JsObject,
+        parameters: JsValue,
         environment: JsObject,
         timeout: FiniteDuration,
         concurrent: Int,
@@ -1740,7 +1740,7 @@ class ContainerProxyTests
   it should "resend the job to the parent if /run fails connection after Ready 
-> Running" in within(timeout) {
     val container = new TestContainer {
       override def run(
-        parameters: JsObject,
+        parameters: JsValue,
         environment: JsObject,
         timeout: FiniteDuration,
         concurrent: Int,
@@ -2160,7 +2160,7 @@ class ContainerProxyTests
       initPromise.map(_.future).getOrElse(Future.successful(initInterval))
     }
     override def run(
-      parameters: JsObject,
+      parameters: JsValue,
       environment: JsObject,
       timeout: FiniteDuration,
       concurrent: Int,
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/FunctionPullingContainerProxyTests.scala
 
b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/FunctionPullingContainerProxyTests.scala
index e2e553e78..8f6561218 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/FunctionPullingContainerProxyTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/FunctionPullingContainerProxyTests.scala
@@ -1737,7 +1737,7 @@ class FunctionPullingContainerProxyTests
     val dataManagementService = TestProbe()
 
     val container = new TestContainer {
-      override def run(parameters: JsObject,
+      override def run(parameters: JsValue,
                        environment: JsObject,
                        timeout: FiniteDuration,
                        concurrent: Int,
@@ -1810,7 +1810,7 @@ class FunctionPullingContainerProxyTests
     val dataManagementService = TestProbe()
 
     val container = new TestContainer {
-      override def run(parameters: JsObject,
+      override def run(parameters: JsValue,
                        environment: JsObject,
                        timeout: FiniteDuration,
                        concurrent: Int,
@@ -2418,7 +2418,7 @@ class FunctionPullingContainerProxyTests
     val dataManagementService = TestProbe()
 
     val container = new TestContainer {
-      override def run(parameters: JsObject,
+      override def run(parameters: JsValue,
                        environment: JsObject,
                        timeout: FiniteDuration,
                        concurrent: Int,
@@ -2816,7 +2816,7 @@ class FunctionPullingContainerProxyTests
     }
 
     override def run(
-      parameters: JsObject,
+      parameters: JsValue,
       environment: JsObject,
       timeout: FiniteDuration,
       concurrent: Int,
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ConductorsApiTests.scala
 
b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ConductorsApiTests.scala
index 1645b887e..7c87e1121 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ConductorsApiTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ConductorsApiTests.scala
@@ -322,10 +322,16 @@ class ConductorsApiTests extends ControllerTestCommon 
with WhiskActionsApi {
   class FakeLoadBalancerService(config: WhiskConfig)(implicit ec: 
ExecutionContext)
       extends DegenerateLoadBalancerService(config) {
 
-    private def respond(action: ExecutableWhiskActionMetaData, msg: 
ActivationMessage, result: JsObject) = {
-      val response =
-        if (result.fields.get("error") isDefined) 
ActivationResponse(ActivationResponse.ApplicationError, Some(result))
-        else ActivationResponse.success(Some(result))
+    private def respond(action: ExecutableWhiskActionMetaData, msg: 
ActivationMessage, result: JsValue) = {
+      val response = {
+        result match {
+          case JsObject(fields) =>
+            if (fields.get("error") isDefined)
+              ActivationResponse(ActivationResponse.ApplicationError, 
Some(result))
+            else ActivationResponse.success(Some(result))
+          case _ => ActivationResponse.success(Some(result))
+        }
+      }
       val start = Instant.now
       WhiskActivation(
         action.namespace,
@@ -345,26 +351,39 @@ class ConductorsApiTests extends ControllerTestCommon 
with WhiskActionsApi {
             case `echo` => // echo action
               Future(Right(respond(action, msg, args)))
             case `conductor` => // see tests/dat/actions/conductor.js
-              val result =
-                if (args.fields.get("error") isDefined) args
-                else {
-                  val action = args.fields.get("action") map { action =>
-                    Map("action" -> action)
-                  } getOrElse Map.empty
-                  val state = args.fields.get("state") map { state =>
-                    Map("state" -> state)
-                  } getOrElse Map.empty
-                  val wrappedParams = args.fields.getOrElse("params", 
JsObject.empty).asJsObject.fields
-                  val escapedParams = args.fields - "action" - "state" - 
"params"
-                  val params = Map("params" -> JsObject(wrappedParams ++ 
escapedParams))
-                  JsObject(params ++ action ++ state)
+              val result = {
+                args match {
+                  case JsObject(fields) =>
+                    if (fields.get("error") isDefined) args
+                    else {
+                      val action = fields.get("action") map { action =>
+                        Map("action" -> action)
+                      } getOrElse Map.empty
+                      val state = fields.get("state") map { state =>
+                        Map("state" -> state)
+                      } getOrElse Map.empty
+                      val wrappedParams = fields.getOrElse("params", 
JsObject.empty).asJsObject.fields
+                      val escapedParams = fields - "action" - "state" - 
"params"
+                      val params = Map("params" -> JsObject(wrappedParams ++ 
escapedParams))
+                      JsObject(params ++ action ++ state)
+                    }
+                  case _ => JsObject.empty
                 }
+
+              }
               Future(Right(respond(action, msg, result)))
             case `step` => // see tests/dat/actions/step.js
-              val result = args.fields.get("n") map { n =>
-                JsObject("n" -> (n.convertTo[BigDecimal] + 1).toJson)
-              } getOrElse {
-                JsObject("error" -> "missing parameter".toJson)
+              val result = {
+                args match {
+                  case JsObject(fields) =>
+                    fields.get("n") map { n =>
+                      JsObject("n" -> (n.convertTo[BigDecimal] + 1).toJson)
+                    } getOrElse {
+                      JsObject("error" -> "missing parameter".toJson)
+                    }
+                  case _ => JsObject.empty
+                }
+
               }
               Future(Right(respond(action, msg, result)))
             case _ =>
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
 
b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
index ddf7e3827..f0eb668df 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
@@ -261,7 +261,7 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
   override protected[controller] def invokeAction(
     user: Identity,
     action: WhiskActionMetaData,
-    payload: Option[JsObject],
+    payload: Option[JsValue],
     waitForResponse: Option[FiniteDuration],
     cause: Option[ActivationId])(implicit transid: TransactionId): 
Future[Either[ActivationId, WhiskActivation]] = {
     invocationCount = invocationCount + 1
@@ -271,10 +271,14 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
       // 1. the package name for the action (to confirm that this resolved to 
systemId)
       // 2. the action name (to confirm that this resolved to the expected 
action)
       // 3. the payload received by the action which consists of the 
action.params + payload
+      val content = payload match {
+        case Some(JsObject(fields)) => 
action.parameters.merge(Some(JsObject(fields)))
+        case _                      => Some(action.parameters.toJsObject)
+      }
       val result = actionResult getOrElse JsObject(
         "pkg" -> action.namespace.toJson,
         "action" -> action.name.toJson,
-        "content" -> action.parameters.merge(payload).get)
+        "content" -> content.get)
 
       val activation = WhiskActivation(
         action.namespace,
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/limits/ActionLimitsTests.scala 
b/tests/src/test/scala/org/apache/openwhisk/core/limits/ActionLimitsTests.scala
index 3617d0c3f..989096e55 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/limits/ActionLimitsTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/limits/ActionLimitsTests.scala
@@ -225,7 +225,7 @@ class ActionLimitsTests extends TestHelpers with 
WskTestHelpers with WskActorSys
       withActivation(wsk.activation, run) { result =>
         withClue("Activation result not as expected:") {
           result.response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.DeveloperError)
-          result.response.result.get.fields("error") shouldBe {
+          result.response.result.get.asJsObject.fields("error") shouldBe {
             Messages.timedoutActivation(allowedActionDuration, init = 
false).toJson
           }
           result.duration.toInt should be >= 
allowedActionDuration.toMillis.toInt
@@ -314,7 +314,7 @@ class ActionLimitsTests extends TestHelpers with 
WskTestHelpers with WskActorSys
         val response = activation.response
         response.success shouldBe false
         response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
-        val msg = 
response.result.get.fields(ActivationResponse.ERROR_FIELD).convertTo[String]
+        val msg = 
response.result.get.asJsObject.fields(ActivationResponse.ERROR_FIELD).convertTo[String]
         val expected = Messages.truncatedResponse((allowedSize + 10).B, 
allowedSize.B)
         withClue(s"is: ${msg.take(expected.length)}\nexpected: $expected") {
           msg.startsWith(expected) shouldBe true
@@ -404,7 +404,7 @@ class ActionLimitsTests extends TestHelpers with 
WskTestHelpers with WskActorSys
     withActivation(wsk.activation, run) { activation =>
       activation.response.success shouldBe false
 
-      val error = activation.response.result.get.fields("error").asJsObject
+      val error = 
activation.response.result.get.asJsObject.fields("error").asJsObject
       error.fields("filesToOpen") shouldBe (openFileLimit + 1).toJson
 
       error.fields("message") shouldBe {
@@ -471,7 +471,7 @@ class ActionLimitsTests extends TestHelpers with 
WskTestHelpers with WskActorSys
     val payload = MemoryLimit.MIN_MEMORY.toMB * 2
     val run = wsk.action.invoke(name, Map("payload" -> payload.toJson))
     withActivation(wsk.activation, run) {
-      _.response.result.get.fields("error") shouldBe 
Messages.memoryExhausted.toJson
+      _.response.result.get.asJsObject.fields("error") shouldBe 
Messages.memoryExhausted.toJson
     }
   }
 
@@ -494,7 +494,7 @@ class ActionLimitsTests extends TestHelpers with 
WskTestHelpers with WskActorSys
       withActivation(wsk.activation, run) { result =>
         withClue("Activation result not as expected:") {
           result.response.status shouldBe 
ActivationResponse.messageForCode(ActivationResponse.DeveloperError)
-          result.response.result.get
+          result.response.result.get.asJsObject
             .fields("error") shouldBe 
Messages.timedoutActivation(allowedActionDuration, init = false).toJson
           val logs = result.logs.get
           logs.last should include(Messages.logWarningDeveloperError)
diff --git a/tests/src/test/scala/system/basic/WskActionTests.scala 
b/tests/src/test/scala/system/basic/WskActionTests.scala
index 4b4d60376..a7a5cd89a 100644
--- a/tests/src/test/scala/system/basic/WskActionTests.scala
+++ b/tests/src/test/scala/system/basic/WskActionTests.scala
@@ -313,7 +313,7 @@ class WskActionTests extends TestHelpers with 
WskTestHelpers with JsHelpers with
     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 {
+      result.asJsObject.getFields("stdout", "code") match {
         case Seq(JsString(stdout), JsNumber(code)) =>
           stdout should not include "bytes from"
           code.intValue should not be 0
@@ -392,4 +392,18 @@ class WskActionTests extends TestHelpers with 
WskTestHelpers with JsHelpers with
       }
   }
 
+  it should "invoke an action with a array result" in 
withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    val name = "helloArray"
+    assetHelper.withCleaner(wsk.action, name) { (action, _) =>
+      action.create(name, 
Some(TestUtils.getTestActionFilename("helloArray.js")))
+    }
+
+    val run = wsk.action.invoke(name)
+    withActivation(wsk.activation, run) { activation =>
+      activation.response.status shouldBe "success"
+      activation.response.result shouldBe Some(
+        JsArray(JsObject("key1" -> JsString("value1")), JsObject("key2" -> 
JsString("value2"))))
+    }
+  }
+
 }
diff --git a/tests/src/test/scala/system/basic/WskConductorTests.scala 
b/tests/src/test/scala/system/basic/WskConductorTests.scala
index c45f053fe..7f4406d24 100644
--- a/tests/src/test/scala/system/basic/WskConductorTests.scala
+++ b/tests/src/test/scala/system/basic/WskConductorTests.scala
@@ -102,7 +102,7 @@ class WskConductorTests extends TestHelpers with 
WskTestHelpers with JsHelpers w
         wsk.action.invoke(echo, Map("payload" -> testString.toJson, "action" 
-> invalid.toJson))
       withActivation(wsk.activation, invalidrun) { activation =>
         activation.response.status shouldBe "application error"
-        activation.response.result.get.fields.get("error") shouldBe Some(
+        activation.response.result.get.asJsObject.fields.get("error") shouldBe 
Some(
           JsString(compositionComponentInvalid(JsString(invalid))))
         checkConductorLogsAndAnnotations(activation, 2) // echo
       }
@@ -113,7 +113,7 @@ class WskConductorTests extends TestHelpers with 
WskTestHelpers with JsHelpers w
 
       withActivation(wsk.activation, undefinedrun) { activation =>
         activation.response.status shouldBe "application error"
-        activation.response.result.get.fields.get("error") shouldBe Some(
+        activation.response.result.get.asJsObject.fields.get("error") shouldBe 
Some(
           JsString(compositionComponentNotFound(s"$namespace/$missing")))
         checkConductorLogsAndAnnotations(activation, 2) // echo
       }
diff --git a/tests/src/test/scala/system/basic/WskSequenceTests.scala 
b/tests/src/test/scala/system/basic/WskSequenceTests.scala
index 73728b92e..d89017662 100644
--- a/tests/src/test/scala/system/basic/WskSequenceTests.scala
+++ b/tests/src/test/scala/system/basic/WskSequenceTests.scala
@@ -73,9 +73,9 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
         checkSequenceLogsAndAnnotations(activation, 4) // 4 activations in 
this sequence
         activation.cause shouldBe None // topmost sequence
         val result = activation.response.result.get
-        result.fields.get("payload") shouldBe defined
-        result.fields.get("length") should not be defined
-        result.fields.get("lines") shouldBe Some(JsArray(Vector(now.toJson)))
+        result.asJsObject.fields.get("payload") shouldBe defined
+        result.asJsObject.fields.get("length") should not be defined
+        result.asJsObject.fields.get("lines") shouldBe 
Some(JsArray(Vector(now.toJson)))
       }
 
       // update action sequence and run it with normal payload
@@ -90,8 +90,8 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
       withActivation(wsk.activation, secondrun, totalWait = 2 * 
allowedActionDuration) { activation =>
         checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in 
this sequence
         val result = activation.response.result.get
-        result.fields.get("length") shouldBe Some(2.toJson)
-        result.fields.get("lines") shouldBe Some(args.sortWith(_.compareTo(_) 
< 0).toArray.toJson)
+        result.asJsObject.fields.get("length") shouldBe Some(2.toJson)
+        result.asJsObject.fields.get("lines") shouldBe 
Some(args.sortWith(_.compareTo(_) < 0).toArray.toJson)
       }
 
       // run sequence with error in the payload
@@ -102,8 +102,8 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
       withActivation(wsk.activation, thirdrun, totalWait = 2 * 
allowedActionDuration) { activation =>
         checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in 
this sequence
         val result = activation.response.result.get
-        result.fields.get("length") shouldBe Some(2.toJson)
-        result.fields.get("lines") shouldBe Some(args.sortWith(_.compareTo(_) 
< 0).toArray.toJson)
+        result.asJsObject.fields.get("length") shouldBe Some(2.toJson)
+        result.asJsObject.fields.get("lines") shouldBe 
Some(args.sortWith(_.compareTo(_) < 0).toArray.toJson)
       }
   }
 
@@ -141,9 +141,9 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
       checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this 
sequence
       activation.cause shouldBe None // topmost sequence
       val result = activation.response.result.get
-      result.fields.get("payload") shouldBe defined
-      result.fields.get("length") should not be defined
-      result.fields.get("lines") shouldBe Some(JsArray(Vector(now.toJson)))
+      result.asJsObject.fields.get("payload") shouldBe defined
+      result.asJsObject.fields.get("length") should not be defined
+      result.asJsObject.fields.get("lines") shouldBe 
Some(JsArray(Vector(now.toJson)))
     }
   }
 
@@ -186,7 +186,7 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
     withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) 
{ activation =>
       checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this 
sequence
       val result = activation.response.result.get
-      result.fields.get("payload") shouldBe Some(argsJson)
+      result.asJsObject.fields.get("payload") shouldBe Some(argsJson)
     }
     // update x with limit echo
     val limit: Int = {
@@ -203,7 +203,7 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
       activation.response.status shouldBe ("application error")
       checkSequenceLogsAndAnnotations(activation, 2)
       val result = activation.response.result.get
-      result.fields.get("error") shouldBe Some(JsString(sequenceIsTooLong))
+      result.asJsObject.fields.get("error") shouldBe 
Some(JsString(sequenceIsTooLong))
       // check that inner sequence had only (limit - 1) activations
       val innerSeq = activation.logs.get(1) // the id of the inner sequence 
activation
       val getInnerSeq = wsk.activation.get(Some(innerSeq))
@@ -541,6 +541,31 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
       }
   }
 
+  it should "invoke a sequence which supports array result" in 
withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    val name = "sequence-array"
+    val actions = Seq("split-array", "sort-array")
+    for (actionName <- actions) {
+      val file = TestUtils.getTestActionFilename(s"$actionName.js")
+      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+        action.create(name = actionName, artifact = Some(file), timeout = 
Some(allowedActionDuration))
+      }
+    }
+
+    assetHelper.withCleaner(wsk.action, name) {
+      val sequence = actions.mkString(",")
+      (action, _) =>
+        action.create(name, Some(sequence), kind = Some("sequence"), timeout = 
Some(allowedActionDuration))
+    }
+
+    val args = Array("bbb", "aaa", "ccc")
+    val run = wsk.action.invoke(name, Map("payload" -> 
args.mkString("\n").toJson))
+    withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) 
{ activation =>
+      checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in this 
sequence
+      activation.cause shouldBe None // topmost sequence
+      activation.response.result shouldBe Some(JsArray(JsString("aaa"), 
JsString("bbb"), JsString("ccc")))
+    }
+  }
+
   /**
    * checks the result of an echo sequence connected to a trigger through a 
rule
    * @param triggerFireRun the run result of firing the trigger
@@ -553,7 +578,7 @@ class WskSequenceTests extends TestHelpers with 
WskTestHelpers with StreamLoggin
       withActivation(wsk.activation, ruleActivation.activationId) { 
actionActivation =>
         actionActivation.response.result match {
           case Some(result) =>
-            val (_, part2) = result.fields partition (p => p._1 == 
"__ow_headers") // excluding headers
+            val (_, part2) = result.asJsObject.fields partition (p => p._1 == 
"__ow_headers") // excluding headers
             JsObject(part2) shouldBe triggerPayload
           case others =>
             fail(s"no result found: $others")
diff --git a/tests/src/test/scala/system/basic/WskUnicodeTests.scala 
b/tests/src/test/scala/system/basic/WskUnicodeTests.scala
index a217b9a1b..67a2a749d 100644
--- a/tests/src/test/scala/system/basic/WskUnicodeTests.scala
+++ b/tests/src/test/scala/system/basic/WskUnicodeTests.scala
@@ -88,8 +88,8 @@ class WskUnicodeTests extends TestHelpers with WskTestHelpers 
with JsHelpers wit
           wsk.action.invoke(name, parameters = Map("delimiter" -> 
JsString("❄"))),
           totalWait = activationPollDuration) { activation =>
           val response = activation.response
-          response.result.get.fields.get("error") shouldBe empty
-          response.result.get.fields.get("winter") should be(Some(JsString("❄ 
☃ ❄")))
+          response.result.get.asJsObject.fields.get("error") shouldBe empty
+          response.result.get.asJsObject.fields.get("winter") should 
be(Some(JsString("❄ ☃ ❄")))
 
           activation.logs.toList.flatten.mkString(" ") should include("❄ ☃ ❄")
         }

Reply via email to