openwhisk-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From houshen...@apache.org
Subject [incubator-openwhisk] branch master updated: Re-implement the non-cli test cases with REST
Date Thu, 09 Nov 2017 23:04:09 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/master by this push:
     new 8bf2eef  Re-implement the non-cli test cases with REST
8bf2eef is described below

commit 8bf2eef01995951abb8ce389f777f868fd542a66
Author: Viola Gao <gaoqian126@126.com>
AuthorDate: Wed Nov 1 14:46:46 2017 -0400

    Re-implement the non-cli test cases with REST
    
    The test cases changed in this PR are non-CLI test cases, which should use
    the REST only to access the openwhisk services. This PR replaces the Wsk
    with WskRest in order to make it happen.
---
 tests/src/test/resources/application.conf          |  2 -
 tests/src/test/scala/common/WhiskProperties.java   | 21 ++++--
 tests/src/test/scala/common/rest/WskRest.scala     | 77 +++++++++++++++++-----
 tests/src/test/scala/ha/ShootComponentsTests.scala |  4 +-
 tests/src/test/scala/limits/ThrottleTests.scala    | 14 ++--
 tests/src/test/scala/services/HeadersTests.scala   |  4 +-
 .../test/scala/system/rest/ActionSchemaTests.scala |  4 +-
 .../scala/whisk/core/admin/WskAdminTests.scala     |  4 +-
 .../core/database/test/CacheConcurrencyTests.scala |  8 ++-
 .../whisk/core/limits/ActionLimitsTests.scala      | 14 ++--
 .../whisk/core/limits/MaxActionDurationTests.scala |  4 +-
 11 files changed, 105 insertions(+), 51 deletions(-)

diff --git a/tests/src/test/resources/application.conf b/tests/src/test/resources/application.conf
index 03b3e0d..39960d7 100644
--- a/tests/src/test/resources/application.conf
+++ b/tests/src/test/resources/application.conf
@@ -1,5 +1,3 @@
-akka.ssl-config.hostnameVerifierClass = common.rest.AcceptAllHostNameVerifier
-
 whisk.spi {
   SimpleSpi = whisk.spi.SimpleSpiImpl
   MissingSpi = whisk.spi.MissingImpl
diff --git a/tests/src/test/scala/common/WhiskProperties.java b/tests/src/test/scala/common/WhiskProperties.java
index 915ed6a..781f0bf 100644
--- a/tests/src/test/scala/common/WhiskProperties.java
+++ b/tests/src/test/scala/common/WhiskProperties.java
@@ -200,11 +200,20 @@ public class WhiskProperties {
         return whiskProperties.getProperty("router.host");
     }
 
+    public static String getApiProto() {
+        return whiskProperties.getProperty("whisk.api.host.proto");
+    }
+
+    public static String getApiHost() {
+        return whiskProperties.getProperty("whisk.api.host.name");
+    }
+
+    public static String getApiPort() {
+        return whiskProperties.getProperty("whisk.api.host.port");
+    }
+
     public static String getApiHostForAction() {
-        String proto = whiskProperties.getProperty("whisk.api.host.proto");
-        String port = whiskProperties.getProperty("whisk.api.host.port");
-        String host = whiskProperties.getProperty("whisk.api.host.name");
-        return proto + "://" + host + ":" + port;
+        return getApiProto() + "://" + getApiHost() + ":" + getApiPort();
     }
 
     public static String getApiHostForClient(String subdomain, boolean includeProtocol) {
@@ -235,11 +244,11 @@ public class WhiskProperties {
     }
 
     public static String getBaseControllerHost() {
-    	return getControllerHosts().split(",")[0];
+        return getControllerHosts().split(",")[0];
     }
 
     public static String getBaseControllerAddress() {
-    	return getBaseControllerHost() + ":" + getControllerBasePort();
+        return getBaseControllerHost() + ":" + getControllerBasePort();
     }
 
     public static int getMaxActionInvokesPerMinute() {
diff --git a/tests/src/test/scala/common/rest/WskRest.scala b/tests/src/test/scala/common/rest/WskRest.scala
index 199d7d6..2f620c2 100644
--- a/tests/src/test/scala/common/rest/WskRest.scala
+++ b/tests/src/test/scala/common/rest/WskRest.scala
@@ -35,7 +35,7 @@ import scala.collection.mutable.Buffer
 import scala.collection.immutable.Seq
 import scala.concurrent.duration.Duration
 import scala.concurrent.duration.DurationInt
-import scala.concurrent.Future
+import scala.concurrent.{Future, Promise}
 import scala.language.postfixOps
 import scala.util.Failure
 import scala.util.Success
@@ -62,8 +62,11 @@ import akka.http.scaladsl.model.HttpMethods.GET
 import akka.http.scaladsl.model.HttpMethods.POST
 import akka.http.scaladsl.model.HttpMethods.PUT
 import akka.http.scaladsl.HttpsConnectionContext
+import akka.http.scaladsl.settings.ConnectionPoolSettings
 
 import akka.stream.ActorMaterializer
+import akka.stream.scaladsl.{Keep, Sink, Source}
+import akka.stream.{OverflowStrategy, QueueOfferResult}
 
 import spray.json._
 import spray.json.DefaultJsonProtocol._
@@ -95,7 +98,7 @@ import javax.net.ssl.{HostnameVerifier, KeyManager, SSLContext, SSLSession,
X509
 import com.typesafe.sslconfig.akka.AkkaSSLConfig
 
 class AcceptAllHostNameVerifier extends HostnameVerifier {
-  def verify(s: String, sslSession: SSLSession) = true
+  override def verify(s: String, sslSession: SSLSession): Boolean = true
 }
 
 object SSL {
@@ -374,14 +377,24 @@ class WskRestAction
       }
     } else {
       bodyContent = bodyContent ++ Map("exec" -> exec.toJson, "parameters" -> params,
"annotations" -> annos)
+    }
 
-      bodyContent = bodyContent ++ {
-        timeout map { t =>
-          Map("limits" -> JsObject("timeout" -> t.toMillis.toJson))
-        } getOrElse Map[String, JsValue]()
-      }
+    val limits = Map[String, JsValue]() ++ {
+      timeout map { t =>
+        Map("timeout" -> t.toMillis.toJson)
+      } getOrElse Map[String, JsValue]()
+    } ++ {
+      logsize map { log =>
+        Map("logs" -> log.toMB.toJson)
+      } getOrElse Map[String, JsValue]()
+    } ++ {
+      memory map { m =>
+        Map("memory" -> m.toMB.toJson)
+      } getOrElse Map[String, JsValue]()
     }
 
+    if (!limits.isEmpty)
+      bodyContent = bodyContent ++ Map("limits" -> limits.toJson)
     val path = Path(s"$basePath/namespaces/$namespace/$noun/$actName")
     val resp =
       if (update) requestEntity(PUT, path, Map("overwrite" -> "true"), Some(JsObject(bodyContent).toString))
@@ -1138,16 +1151,21 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers
with ScalaFu
 
   implicit val config = PatienceConfig(100 seconds, 15 milliseconds)
   implicit val materializer = ActorMaterializer()
-  val whiskRestUrl = Uri(WhiskProperties.getApiHostForAction)
+  val queueSize = 10
+  val maxOpenRequest = 1024
   val basePath = Path("/api/v1")
 
   val sslConfig = AkkaSSLConfig().mapSettings { s =>
-    s.withLoose(s.loose.withAcceptAnyCertificate(true).withDisableHostnameVerification(true))
+    s.withHostnameVerifierClass(classOf[AcceptAllHostNameVerifier].asInstanceOf[Class[HostnameVerifier]])
   }
+
   val connectionContext = new HttpsConnectionContext(SSL.nonValidatingContext, Some(sslConfig))
 
   def isStatusCodeExpected(expectedExitCode: Int, statusCode: Int): Boolean = {
-    return statusCode == expectedExitCode
+    if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))
+      statusCode == expectedExitCode
+    else
+      true
   }
 
   def validateStatusCode(expectedExitCode: Int, statusCode: Int) = {
@@ -1175,15 +1193,38 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers
with ScalaFu
       HttpEntity(ContentTypes.`application/json`, b)
     } getOrElse HttpEntity(ContentTypes.`application/json`, "")
     val request = HttpRequest(method, uri, List(Authorization(creds)), entity = entity)
-
-    Http().singleRequest(request, connectionContext)
+    val connectionPoolSettings = ConnectionPoolSettings(actorSystem).withMaxOpenRequests(maxOpenRequest)
+    val pool = Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](
+      host = WhiskProperties.getApiHost,
+      connectionContext = connectionContext,
+      settings = connectionPoolSettings)
+    val queue = Source
+      .queue[(HttpRequest, Promise[HttpResponse])](queueSize, OverflowStrategy.dropNew)
+      .via(pool)
+      .toMat(Sink.foreach({
+        case ((Success(resp), p)) => p.success(resp)
+        case ((Failure(e), p))    => p.failure(e)
+      }))(Keep.left)
+      .run
+
+    val promise = Promise[HttpResponse]
+    val responsePromise = Promise[HttpResponse]()
+    queue.offer(request -> responsePromise).flatMap {
+      case QueueOfferResult.Enqueued => responsePromise.future
+      case QueueOfferResult.Dropped =>
+        Future.failed(new RuntimeException("Queue has overflowed. Please try again later."))
+      case QueueOfferResult.Failure(ex) => Future.failed(ex)
+      case QueueOfferResult.QueueClosed =>
+        Future.failed(
+          new RuntimeException("Queue was closed (pool shut down) while running the request.
Please try again later."))
+    }
   }
 
   def requestEntity(method: HttpMethod,
                     path: Path,
                     params: Map[String, String] = Map(),
                     body: Option[String] = None,
-                    whiskUrl: Uri = whiskRestUrl)(implicit wp: WskProps): HttpResponse =
{
+                    whiskUrl: Uri = Uri(""))(implicit wp: WskProps): HttpResponse = {
     val creds = getBasicHttpCredentials(wp)
     request(method, whiskUrl.withPath(path).withQuery(Uri.Query(params)), body, creds = creds).futureValue
   }
@@ -1298,7 +1339,7 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers
with ScalaFu
       Some(FileUtils.readFileToString(new File(pf)))
     } getOrElse Some(parameters.toJson.toString())
     val resp = requestEntity(POST, path, paramMap, input)
-    val r = new RestResult(resp.status.intValue, getRespData(resp))
+    val r = new RestResult(resp.status.intValue, getRespData(resp), blocking)
     // If the statusCode does not not equal to expectedExitCode, it is acceptable that the
statusCode
     // equals to 200 for the case that either blocking or result is set to true.
     if (!isStatusCodeExpected(expectedExitCode, r.statusCode.intValue)) {
@@ -1393,8 +1434,8 @@ object RestResult {
     obj.fields.get(key).map(_.convertTo[Vector[JsObject]]).getOrElse(Vector(JsObject()))
   }
 
-  def convertStausCodeToExitCode(statusCode: StatusCode): Int = {
-    if (statusCode == OK)
+  def convertStausCodeToExitCode(statusCode: StatusCode, blocking: Boolean = false): Int
= {
+    if ((statusCode == OK) || (!blocking && (statusCode == Accepted)))
       return 0
     if (statusCode.intValue < BadRequest.intValue) statusCode.intValue else statusCode.intValue
- codeConversion
   }
@@ -1408,9 +1449,9 @@ object RestResult {
   }
 }
 
-class RestResult(var statusCode: StatusCode, var respData: String = "")
+class RestResult(var statusCode: StatusCode, var respData: String = "", blocking: Boolean
= false)
     extends RunResult(
-      RestResult.convertStausCodeToExitCode(statusCode),
+      RestResult.convertStausCodeToExitCode(statusCode, blocking),
       respData,
       RestResult.convertHttpResponseToStderr(respData)) {
 
diff --git a/tests/src/test/scala/ha/ShootComponentsTests.scala b/tests/src/test/scala/ha/ShootComponentsTests.scala
index 4b28ded..fa1aba1 100644
--- a/tests/src/test/scala/ha/ShootComponentsTests.scala
+++ b/tests/src/test/scala/ha/ShootComponentsTests.scala
@@ -36,7 +36,7 @@ import akka.http.scaladsl.unmarshalling.Unmarshal
 import akka.stream.ActorMaterializer
 import common.TestUtils
 import common.WhiskProperties
-import common.Wsk
+import common.rest.WskRest
 import common.WskActorSystem
 import common.WskProps
 import common.WskTestHelpers
@@ -47,7 +47,7 @@ import whisk.utils.retry
 class ShootComponentsTests extends FlatSpec with Matchers with WskTestHelpers with ScalaFutures
with WskActorSystem {
 
   implicit val wskprops = WskProps()
-  val wsk = new Wsk
+  val wsk = new WskRest
   val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
 
   implicit val materializer = ActorMaterializer()
diff --git a/tests/src/test/scala/limits/ThrottleTests.scala b/tests/src/test/scala/limits/ThrottleTests.scala
index 8da66e8..511a1b3 100644
--- a/tests/src/test/scala/limits/ThrottleTests.scala
+++ b/tests/src/test/scala/limits/ThrottleTests.scala
@@ -19,6 +19,8 @@ package limits
 
 import java.time.Instant
 
+import akka.http.scaladsl.model.StatusCodes.TooManyRequests
+
 import scala.collection.parallel.immutable.ParSeq
 import scala.concurrent.Future
 import scala.concurrent.Promise
@@ -34,7 +36,7 @@ import common.TestHelpers
 import common.TestUtils
 import common.TestUtils._
 import common.WhiskProperties
-import common.Wsk
+import common.rest.WskRest
 import common.WskActorSystem
 import common.WskProps
 import common.WskTestHelpers
@@ -65,7 +67,7 @@ class ThrottleTests
 
   implicit val testConfig = PatienceConfig(5.minutes)
   implicit val wskprops = WskProps()
-  val wsk = new Wsk
+  val wsk = new WskRest
   val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
 
   val throttleWindow = 1.minute
@@ -296,7 +298,7 @@ class NamespaceSpecificThrottleTests
     with LocalHelper {
 
   val wskadmin = new RunWskAdminCmd {}
-  val wsk = new Wsk
+  val wsk = new WskRest
 
   val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
 
@@ -346,10 +348,10 @@ class NamespaceSpecificThrottleTests
       trigger.create(triggerName)
     }
 
-    wsk.action.invoke(actionName, expectedExitCode = TestUtils.THROTTLED).stderr should {
+    wsk.action.invoke(actionName, expectedExitCode = TooManyRequests.intValue).stderr should
{
       include(prefix(tooManyRequests(0, 0))) and include("allowed: 0")
     }
-    wsk.trigger.fire(triggerName, expectedExitCode = TestUtils.THROTTLED).stderr should {
+    wsk.trigger.fire(triggerName, expectedExitCode = TooManyRequests.intValue).stderr should
{
       include(prefix(tooManyRequests(0, 0))) and include("allowed: 0")
     }
   }
@@ -401,7 +403,7 @@ class NamespaceSpecificThrottleTests
       action.create(actionName, defaultAction)
     }
 
-    wsk.action.invoke(actionName, expectedExitCode = TestUtils.THROTTLED).stderr should {
+    wsk.action.invoke(actionName, expectedExitCode = TooManyRequests.intValue).stderr should
{
       include(prefix(tooManyConcurrentRequests(0, 0))) and include("allowed: 0")
     }
   }
diff --git a/tests/src/test/scala/services/HeadersTests.scala b/tests/src/test/scala/services/HeadersTests.scala
index 6d0b4c2..f3184c8 100644
--- a/tests/src/test/scala/services/HeadersTests.scala
+++ b/tests/src/test/scala/services/HeadersTests.scala
@@ -31,7 +31,7 @@ import org.scalatest.time.Span.convertDurationToSpan
 
 import common.TestUtils
 import common.WhiskProperties
-import common.Wsk
+import common.rest.WskRest
 import common.WskProps
 import common.WskTestHelpers
 
@@ -82,7 +82,7 @@ class HeadersTests extends FlatSpec with Matchers with ScalaFutures with
WskActo
 
   val basePath = Path("/api/v1")
   implicit val wskprops = WskProps()
-  val wsk = new Wsk
+  val wsk = new WskRest
 
   /**
    * Checks, if the required headers are in the list of all headers.
diff --git a/tests/src/test/scala/system/rest/ActionSchemaTests.scala b/tests/src/test/scala/system/rest/ActionSchemaTests.scala
index 9b0560a..b88b231 100644
--- a/tests/src/test/scala/system/rest/ActionSchemaTests.scala
+++ b/tests/src/test/scala/system/rest/ActionSchemaTests.scala
@@ -29,7 +29,7 @@ import com.jayway.restassured.RestAssured
 
 import common.TestUtils
 import common.WhiskProperties
-import common.Wsk
+import common.rest.WskRest
 import common.WskProps
 import common.WskTestHelpers
 import spray.json.JsArray
@@ -43,7 +43,7 @@ import spray.json.pimpString
 class ActionSchemaTests extends FlatSpec with Matchers with RestUtil with JsonSchema with
WskTestHelpers {
 
   implicit val wskprops = WskProps()
-  val wsk = new Wsk
+  val wsk = new WskRest
   val guestNamespace = wskprops.namespace
 
   it should "respond to GET /actions as documented in swagger" in withAssetCleaner(wskprops)
{ (wp, assetHelper) =>
diff --git a/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala b/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
index c42e2e8..b2bc3b4 100644
--- a/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
+++ b/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
@@ -25,7 +25,7 @@ import org.scalatest.junit.JUnitRunner
 
 import common.RunWskAdminCmd
 import common.TestHelpers
-import common.Wsk
+import common.rest.WskRest
 import common.WskAdmin
 import common.WskProps
 import whisk.core.entity.AuthKey
@@ -87,7 +87,7 @@ class WskAdminTests extends TestHelpers with Matchers {
   it should "verify guest account installed correctly" in {
     val wskadmin = new RunWskAdminCmd {}
     implicit val wskprops = WskProps()
-    val wsk = new Wsk
+    val wsk = new WskRest
     val ns = wsk.namespace.whois()
     wskadmin.cli(Seq("user", "get", ns)).stdout.trim should be(wskprops.authKey)
   }
diff --git a/tests/src/test/scala/whisk/core/database/test/CacheConcurrencyTests.scala b/tests/src/test/scala/whisk/core/database/test/CacheConcurrencyTests.scala
index 7bc56ba..1121522 100644
--- a/tests/src/test/scala/whisk/core/database/test/CacheConcurrencyTests.scala
+++ b/tests/src/test/scala/whisk/core/database/test/CacheConcurrencyTests.scala
@@ -17,6 +17,8 @@
 
 package whisk.core.database.test
 
+import akka.http.scaladsl.model.StatusCodes.NotFound
+
 import scala.collection.parallel._
 import scala.concurrent.forkjoin.ForkJoinPool
 
@@ -27,7 +29,7 @@ import org.scalatest.junit.JUnitRunner
 
 import common.TestUtils
 import common.TestUtils._
-import common.Wsk
+import common.rest.WskRest
 import common.WskProps
 import common.WskTestHelpers
 import spray.json.JsString
@@ -40,7 +42,7 @@ class CacheConcurrencyTests extends FlatSpec with WskTestHelpers with BeforeAndA
 
   implicit private val transId = TransactionId.testing
   implicit private val wp = WskProps()
-  private val wsk = new Wsk
+  private val wsk = new WskRest
 
   val nExternalIters = 1
   val nInternalIters = 5
@@ -97,7 +99,7 @@ class CacheConcurrencyTests extends FlatSpec with WskTestHelpers with BeforeAndA
       }
 
       run("get after delete") { name =>
-        wsk.action.get(name, expectedExitCode = NOT_FOUND)
+        wsk.action.get(name, expectedExitCode = NotFound.intValue)
       }
 
       run("recreate") { name =>
diff --git a/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala b/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala
index 4ac38f2..dab3da5 100644
--- a/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala
+++ b/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala
@@ -17,6 +17,9 @@
 
 package whisk.core.limits
 
+import akka.http.scaladsl.model.StatusCodes.RequestEntityTooLarge
+import akka.http.scaladsl.model.StatusCodes.BadGateway
+
 import java.io.File
 import java.io.PrintWriter
 
@@ -29,9 +32,8 @@ import org.scalatest.junit.JUnitRunner
 import common.ActivationResult
 import common.TestHelpers
 import common.TestUtils
-import common.TestUtils.TOO_LARGE
 import common.WhiskProperties
-import common.Wsk
+import common.rest.WskRest
 import common.WskProps
 import common.WskTestHelpers
 import spray.json._
@@ -47,7 +49,7 @@ import whisk.http.Messages
 class ActionLimitsTests extends TestHelpers with WskTestHelpers {
 
   implicit val wskprops = WskProps()
-  val wsk = new Wsk()
+  val wsk = new WskRest
 
   val defaultDosAction = TestUtils.getTestActionFilename("timeout.js")
   val allowedActionDuration = 10 seconds
@@ -148,10 +150,10 @@ class ActionLimitsTests extends TestHelpers with WskTestHelpers {
 
       // this tests an active ack failure to post from invoker
       val args = Map("size" -> (allowedSize + 1).toJson, "char" -> "a".toJson)
-      val code = if (blocking) TestUtils.APP_ERROR else TestUtils.SUCCESS_EXIT
+      val code = if (blocking) BadGateway.intValue else TestUtils.ACCEPTED
       val rr = wsk.action.invoke(name, args, blocking = blocking, expectedExitCode = code)
       if (blocking) {
-        checkResponse(wsk.parseJsonString(rr.stderr).convertTo[ActivationResult])
+        checkResponse(wsk.parseJsonString(rr.respData).convertTo[ActivationResult])
       } else {
         withActivation(wsk.activation, rr, totalWait = 120 seconds) { checkResponse(_) }
       }
@@ -187,7 +189,7 @@ class ActionLimitsTests extends TestHelpers with WskTestHelpers {
     pw.close
 
     assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>
-      action.create(name, Some(actionCode.getAbsolutePath), expectedExitCode = TOO_LARGE)
+      action.create(name, Some(actionCode.getAbsolutePath), expectedExitCode = RequestEntityTooLarge.intValue)
     }
 
     actionCode.delete
diff --git a/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala b/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala
index 261e300..dba2eeb 100644
--- a/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala
+++ b/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala
@@ -24,7 +24,7 @@ import org.scalatest.junit.JUnitRunner
 
 import common.TestHelpers
 import common.TestUtils
-import common.Wsk
+import common.rest.WskRest
 import common.WskProps
 import common.WskTestHelpers
 import whisk.core.entity._
@@ -40,7 +40,7 @@ import whisk.core.entity.TimeLimit
 class MaxActionDurationTests extends TestHelpers with WskTestHelpers {
 
   implicit val wskprops = WskProps()
-  val wsk = new Wsk
+  val wsk = new WskRest
 
   // swift is not tested, because it uses the same proxy like python
   "node-, python, and java-action" should "run up to the max allowed duration" in withAssetCleaner(wskprops)
{

-- 
To stop receiving notification emails like this one, please contact
['"commits@openwhisk.apache.org" <commits@openwhisk.apache.org>'].

Mime
View raw message