You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2018/03/07 04:30:36 UTC

[GitHub] paulcastro closed pull request #23: Add Codable Support for Swift 4.x

paulcastro closed pull request #23: Add Codable Support for Swift 4.x
URL: https://github.com/apache/incubator-openwhisk-runtime-swift/pull/23
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/.gitignore b/.gitignore
index 92a5d97..7a15b4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,4 +72,6 @@ ansible/roles/nginx/files/*cert.pem
 !tests/dat/build/swift4.0/SwiftyRequest.zip
 !tests/dat/build/swift4.1/HelloSwift4.zip
 !tests/dat/build/swift4.1/SwiftyRequest.zip
+!tests/dat/build/swift4.0/HelloSwift4Codable.zip
+!tests/dat/build/swift4.1/HelloSwift4Codable.zip
 
diff --git a/README.md b/README.md
index 82dd6f7..1062e48 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,76 @@ func main(args: [String:Any]) -> [String:Any] {
 ```
 
 ## Swift 4.x support
+
+Some examples of using Codable In and Out
+### Codable style function signature
+Create file `helloCodableAsync.swift`
+```swift
+// Domain model/entity
+struct Employee: Codable {
+  let id: Int?
+  let name: String?
+}
+// codable main function
+func main(input: Employee, respondWith: (Employee?, Error?) -> Void) -> Void {
+    // For simplicity, just passing same Employee instance forward
+    respondWith(input, nil)
+}
+```
+```
+wsk action update helloCodableAsync helloCodableAsync.swift swift:4.1
+```
+ok: updated action helloCodableAsync
+```
+wsk action invoke helloCodableAsync -r -p id 42 -p name Carlos
+```
+```json
+{
+    "id": 42,
+    "name": "Carlos"
+}
+```
+
+### Codable Error Handling
+Create file `helloCodableAsync.swift`
+```swift
+struct Employee: Codable {
+    let id: Int?
+    let name: String?
+}
+enum VendingMachineError: Error {
+    case invalidSelection
+    case insufficientFunds(coinsNeeded: Int)
+    case outOfStock
+}
+func main(input: Employee, respondWith: (Employee?, Error?) -> Void) -> Void {
+    // Return real error
+    do{
+        throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
+    } catch {
+        respondWith(nil, error)
+    } 
+}
+```
+```
+wsk action update helloCodableError helloCodableError.swift swift:4.1
+```
+ok: updated action helloCodableError
+```
+wsk action invoke helloCodableError -b -p id 42 -p name Carlos
+```
+```json
+{
+"name": "helloCodableError",
+"response": {
+  "result": {
+    "error": "insufficientFunds(5)"
+  },
+"status": "application error",
+"success": false
+}
+```
+
 ### Packaging an action as a Swift executable using Swift 4
 
 When you create an OpenWhisk Swift action with a Swift source file, it has to be compiled into a binary before the action is run. Once done, subsequent calls to the action are much faster until the container holding your action is purged. This delay is known as the cold-start delay.
@@ -136,7 +206,7 @@ let package = Package(
 
 ### Helper compile.sh helper script
 When compiling and packaging your swift 4 action, there are a couple of differences.
-All your source code needs to be copy to `/swift4Action/spm-build/Sources/Action/` instead of `/swift3Action/spm-build/`
+All your source code needs to be copied to `/swift4Action/spm-build/Sources/Action/` instead of `/swift3Action/spm-build/`
 You Package.swift needs to have the first line with a comment indicating swift4 tooling and format
 ```
 // swift-tools-version:4.0
@@ -158,7 +228,7 @@ This will produce a zip `build/swift4/Hello.zip`
 
 ### SwiftyJSON using single source action file
 If you have a swift:3.1.1 action not compile, just as source using the `SwiftyJSON` package, you need to precompile your action and specify the version of SwiftyJSON you wan to use for swift:4.0 kind action.
-Take into account that tarting with Swift 4 there is better support to manage JSON data natively.
+Take into account that starting with Swift 4 there is better support to manage JSON data natively.
 
 Note: This is only applicable to the base image provided for the Swift 4 runtime, other downstream such as IBM Cloud Functions extending this image might provide additional SDK and packages including `SwiftyJSON` and IBM Watson SDK, check the vendor documentation for more specific information about packages and versions.
 
@@ -179,6 +249,78 @@ We have a runtime for swift 4.1, is experimental as we are trying beta builds re
 Follow same insructions for Swift 4.0 above and replace the kind wih `swift:4.1` and image with `openwhisk/action-swift-v4.0`
 
 
+## Codable Suppor with Swift 4.x
+
+Some examples of using Codable In and Out
+
+### Codable style function signature
+Create file `helloCodableAsync.swift`
+```swift
+// Domain model/entity
+struct Employee: Codable {
+  let id: Int
+  let name: String
+}
+// codable main function
+func main(input: Employee, respondWith: (Employee?, Error?) -> Void) -> Void {
+    // For simplicity, just passing same Employee instance forward
+    respondWith(input, nil)
+}
+```
+```
+wsk action update helloCodableAsync helloCodableAsync.swift swift:4.1
+```
+ok: updated action helloCodableAsync
+```
+wsk action invoke helloCodableAsync -r -p id 42 -p name Carlos
+```
+```json
+{
+    "id": 42,
+    "name": "Carlos"
+}
+```
+
+### Codable Error Handling
+Create file `helloCodableAsync.swift`
+```swift
+struct Employee: Codable {
+    let id: Int
+    let name: String
+}
+enum VendingMachineError: Error {
+    case invalidSelection
+    case insufficientFunds(coinsNeeded: Int)
+    case outOfStock
+}
+func main(input: Employee, respondWith: (Employee?, Error?) -> Void) -> Void {
+    // Return real error
+    do{
+        throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
+    } catch {
+        respondWith(nil, error)
+    } 
+}
+```
+```
+wsk action update helloCodableError helloCodableError.swift swift:4.1
+```
+ok: updated action helloCodableError
+```
+wsk action invoke helloCodableError -b -p id 42 -p name Carlos
+```
+```json
+{
+"name": "helloCodableError",
+"response": {
+  "result": {
+    "error": "insufficientFunds(5)"
+  },
+"status": "application error",
+"success": false
+}
+```
+
 ### Using Swift 3.1.1
 To use as a docker action
 ```
@@ -232,13 +374,6 @@ Install dependencies from the root directory on $OPENWHISK_HOME repository
 ./gradlew :common:scala:install :core:controller:install :core:invoker:install :tests:install
 ```
 
-Using gradle for the ActionContainer tests you need to use a proxy if running on Mac, if Linux then don't use proxy options
-You can pass the flags `-Dhttp.proxyHost=localhost -Dhttp.proxyPort=3128` directly in gradle command.
-Or save in your `$HOME/.gradle/gradle.properties`
-```
-systemProp.http.proxyHost=localhost
-systemProp.http.proxyPort=3128
-```
 Using gradle to run all tests
 ```
 ./gradlew :tests:test
@@ -250,11 +385,7 @@ Using gradle to run some tests
 Using IntelliJ:
 - Import project as gradle project.
 - Make sure working directory is root of the project/repo
-- Add the following Java VM properties in ScalaTests Run Configuration, easiest is to change the Defaults for all ScalaTests to use this VM properties
-```
--Dhttp.proxyHost=localhost
--Dhttp.proxyPort=3128
-```
+
 
 #### Using container image to test
 To use as docker action push to your own dockerhub account
diff --git a/ansible/environments/local/group_vars/all b/ansible/environments/local/group_vars/all
index 3eb2358..1dfc339 100755
--- a/ansible/environments/local/group_vars/all
+++ b/ansible/environments/local/group_vars/all
@@ -1,8 +1,11 @@
 whisk_version_name: local
-config_root_dir: /tmp
-whisk_logs_dir: /tmp/wsklogs
+openwhisk_tmp_dir: "{{ lookup('env', 'OPENWHISK_TMP_DIR')|default('/tmp', true) }}"
+config_root_dir: "{{ openwhisk_tmp_dir }}/wskconf"
+whisk_logs_dir: "{{ openwhisk_tmp_dir }}/wsklogs"
 docker_registry: ""
 docker_dns: ""
+runtimes_bypass_pull_for_local_images: true
+invoker_use_runc: "{{ ansible_distribution != 'MacOSX' }}"
 
 db_prefix: whisk_local_
 
@@ -19,11 +22,11 @@ apigw_auth_user: ""
 apigw_auth_pwd: ""
 apigw_host_v2: "http://{{ groups['apigateway']|first }}:{{apigateway.port.api}}/v2"
 
-controller_arguments: '-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=1098'
-invoker_arguments: "{{ controller_arguments }}"
-
 invoker_allow_multiple_instances: true
 
+
+env_hosts_dir: "{{ playbook_dir }}/environments/local"
+
 runtimes_manifest:
   defaultImagePrefix: "openwhisk"
   defaultImageTag: "latest"
diff --git a/core/swift40Action/epilogue.swift b/core/swift40Action/epilogue.swift
index fcd52d1..9d768af 100644
--- a/core/swift40Action/epilogue.swift
+++ b/core/swift40Action/epilogue.swift
@@ -21,24 +21,100 @@ import Foundation
 let inputStr: String = readLine() ?? "{}"
 let json = inputStr.data(using: .utf8, allowLossyConversion: true)!
 
+func _whisk_print_error(message: String, error: Error?){
+    if let error = error {
+        print("{\"error\":\"\(message) \(error.localizedDescription)\"}")
+    } else {
+       print("{\"error\":\"\(message)\"}")
+    }
+}
 
 // snippet of code "injected" (wrapper code for invoking traditional main)
 func _run_main(mainFunction: ([String: Any]) -> [String: Any]) -> Void {
-    let parsed = try! JSONSerialization.jsonObject(with: json, options: []) as! [String: Any]
-    let result = mainFunction(parsed)
-    if JSONSerialization.isValidJSONObject(result) {
-        do {
-            let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
-            if let jsonStr = String(data: jsonData, encoding: String.Encoding.utf8) {
-                print("\(jsonStr)")
-            } else {
-                print("Error serializing data to JSON, data conversion returns nil string")
+    do {
+        let parsed = try JSONSerialization.jsonObject(with: json, options: []) as! [String: Any]
+        let result = mainFunction(parsed)
+        if JSONSerialization.isValidJSONObject(result) {
+            do {
+                let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
+                if let jsonStr = String(data: jsonData, encoding: String.Encoding.utf8) {
+                    print("\(jsonStr)")
+                } else {
+                    _whisk_print_error(message: "Error serializing data to JSON, data conversion returns nil string", error: nil)
+                }
+            } catch {
+                _whisk_print_error(message: "Failed to encode Dictionary type to JSON string:", error: error)
+            }
+        } else {
+            _whisk_print_error(message: "Error serializing JSON, data does not appear to be valid JSON", error: nil)
+        }
+    } catch {
+        _whisk_print_error(message: "Failed to execute action handler with error:", error: error)
+        return
+    }
+}
+
+// Codable main signature input Codable
+func _run_main<In: Codable, Out: Codable>(mainFunction: @escaping (In, (Out?, Error?) -> Void) -> Void) {
+    do {
+        let input = try Whisk.jsonDecoder.decode(In.self, from: json)
+        let resultHandler = { (out: Out?, error: Error?) in
+            if let error = error {
+                _whisk_print_error(message: "Action handler callback returned an error:", error: error)
+                return
+            }  
+            guard let out = out else {
+                _whisk_print_error(message: "Action handler callback did not return response or error.", error: nil)
+                return
+            }
+            do {
+                let jsonData = try Whisk.jsonEncoder.encode(out)
+                let jsonString = String(data: jsonData, encoding: .utf8)
+                print("\(jsonString!)")
+            } catch let error as EncodingError {
+                _whisk_print_error(message: "JSONEncoder failed to encode Codable type to JSON string:", error: error)
+                return
+            } catch {
+                _whisk_print_error(message: "Failed to execute action handler with error:", error: error)
+                return
             }
+        }
+        let _ = mainFunction(input, resultHandler)
+    } catch let error as DecodingError {
+        _whisk_print_error(message: "JSONDecoder failed to decode JSON string \(inputStr.replacingOccurrences(of: "\"", with: "\\\"")) to Codable type:", error: error)
+        return
+    } catch {
+        _whisk_print_error(message: "Failed to execute action handler with error:", error: error)
+        return
+    }
+}
+
+// Codable main signature no input
+func _run_main<Out: Codable>(mainFunction: @escaping ((Out?, Error?) -> Void) -> Void) {
+    let resultHandler = { (out: Out?, error: Error?) in
+        if let error = error {
+            _whisk_print_error(message: "Action handler callback returned an error:", error: error)
+            return
+        }
+        guard let out = out else {
+            _whisk_print_error(message: "Action handler callback did not return response or error.", error: nil)
+            return
+        }
+        do {
+            let jsonData = try Whisk.jsonEncoder.encode(out)
+            let jsonString = String(data: jsonData, encoding: .utf8)
+            print("\(jsonString!)")
+        } catch let error as EncodingError {
+            _whisk_print_error(message: "JSONEncoder failed to encode Codable type to JSON string:", error: error)
+            return
         } catch {
-            print(("\(error)"))
+            _whisk_print_error(message: "Failed to execute action handler with error:", error: error)
+            return
         }
-    } else {
-        print("Error serializing JSON, data does not appear to be valid JSON")
     }
+    let _ = mainFunction(resultHandler)
 }
 
+// snippets of code "injected", dependending on the type of function the developer
+// wants to use traditional vs codable
+
diff --git a/core/swift40Action/spm-build/_Whisk.swift b/core/swift40Action/spm-build/_Whisk.swift
index ede2a9f..fe43f21 100644
--- a/core/swift40Action/spm-build/_Whisk.swift
+++ b/core/swift40Action/spm-build/_Whisk.swift
@@ -22,6 +22,9 @@ class Whisk {
     
     static var baseUrl = ProcessInfo.processInfo.environment["__OW_API_HOST"]
     static var apiKey = ProcessInfo.processInfo.environment["__OW_API_KEY"]
+    // This will allow user to modify the default JSONDecoder and JSONEncoder used by epilogue
+    static var jsonDecoder = JSONDecoder()
+    static var jsonEncoder = JSONEncoder()
     
     class func invoke(actionNamed action : String, withParameters params : [String:Any], blocking: Bool = true) -> [String:Any] {
         let parsedAction = parseQualifiedName(name: action)
diff --git a/core/swift41Action/Dockerfile b/core/swift41Action/Dockerfile
index c7a336a..260b8d9 100644
--- a/core/swift41Action/Dockerfile
+++ b/core/swift41Action/Dockerfile
@@ -5,7 +5,7 @@ LABEL Description="Linux Ubuntu 14.04 image with the Swift binaries and tools."
 USER root
 
 # Set environment variables for image
-ENV SWIFT_SNAPSHOT swift-4.1-DEVELOPMENT-SNAPSHOT-2018-02-13-a
+ENV SWIFT_SNAPSHOT swift-4.1-DEVELOPMENT-SNAPSHOT-2018-03-06-a
 ENV SWIFT_SNAPSHOT_LOWERCASE swift-4.1-branch
 ENV UBUNTU_VERSION ubuntu14.04
 ENV UBUNTU_VERSION_NO_DOTS ubuntu1404
diff --git a/settings.gradle b/settings.gradle
index 6476232..dc7ecfe 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -12,7 +12,7 @@ gradle.ext.openwhisk = [
 ]
 
 gradle.ext.scala = [
-    version: '2.11.8',
+    version: '2.11.11',
     compileFlags: ['-feature', '-unchecked', '-deprecation', '-Xfatal-warnings', '-Ywarn-unused-import']
 ]
 
diff --git a/tests/dat/actions/HelloSwift4Codable/Package.swift b/tests/dat/actions/HelloSwift4Codable/Package.swift
new file mode 100644
index 0000000..b9f14f9
--- /dev/null
+++ b/tests/dat/actions/HelloSwift4Codable/Package.swift
@@ -0,0 +1,34 @@
+// swift-tools-version:4.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+/*
+ *
+ * Licensed 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.
+ */
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+    products: [
+      .executable(
+        name: "Action",
+        targets:  ["Action"]
+      )
+    ],
+    targets: [
+      .target(
+        name: "Action"
+      )
+    ]
+)
diff --git a/tests/dat/actions/HelloSwift4Codable/Sources/main.swift b/tests/dat/actions/HelloSwift4Codable/Sources/main.swift
new file mode 100644
index 0000000..04421af
--- /dev/null
+++ b/tests/dat/actions/HelloSwift4Codable/Sources/main.swift
@@ -0,0 +1,15 @@
+struct AnInput: Codable {
+    let name: String?
+}
+struct AnOutput: Codable {
+    let greeting: String?
+}
+ func main(input: AnInput, respondWith: (AnOutput?, Error?) -> Void) -> Void {
+     if let name = input.name {
+         let answer = AnOutput(greeting: "Hello \(name)!")
+         respondWith(answer, nil)
+     } else {
+         let answer = AnOutput(greeting: "Hello stranger!")
+         respondWith(answer, nil)
+     }
+  }
\ No newline at end of file
diff --git a/tests/dat/build.sh b/tests/dat/build.sh
index 1fbd0c1..97879b8 100755
--- a/tests/dat/build.sh
+++ b/tests/dat/build.sh
@@ -4,5 +4,8 @@ set -e
 ../../tools/build/compile.sh  HelloSwift3 swift:3.1.1 "-v"
 ../../tools/build/compile.sh  HelloSwift4 swift:4.0 "-v"
 ../../tools/build/compile.sh  SwiftyRequest swift:4.0 "-v"
+../../tools/build/compile.sh  HelloSwift4Codable swift:4.0 "-v"
+
 ../../tools/build/compile.sh  HelloSwift4 swift:4.1 "-v"
 ../../tools/build/compile.sh  SwiftyRequest swift:4.1 "-v"
+../../tools/build/compile.sh  HelloSwift4Codable swift:4.1 "-v"
diff --git a/tests/dat/build/swift4.0/HelloSwift4Codable.zip b/tests/dat/build/swift4.0/HelloSwift4Codable.zip
new file mode 100644
index 0000000..79cc30b
Binary files /dev/null and b/tests/dat/build/swift4.0/HelloSwift4Codable.zip differ
diff --git a/tests/dat/build/swift4.1/HelloSwift4Codable.zip b/tests/dat/build/swift4.1/HelloSwift4Codable.zip
new file mode 100644
index 0000000..6713b9c
Binary files /dev/null and b/tests/dat/build/swift4.1/HelloSwift4Codable.zip differ
diff --git a/tests/src/test/scala/actionContainers/ActionContainer.scala b/tests/src/test/scala/actionContainers/ActionContainer.scala
deleted file mode 100644
index 56aa131..0000000
--- a/tests/src/test/scala/actionContainers/ActionContainer.scala
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package runtime.actionContainers
-
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.PrintWriter
-
-import scala.concurrent.Await
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
-import scala.concurrent.blocking
-import scala.concurrent.duration.Duration
-import scala.concurrent.duration.DurationInt
-import scala.language.postfixOps
-import scala.sys.process.ProcessLogger
-import scala.sys.process.stringToProcess
-import scala.util.Random
-
-import org.apache.commons.lang3.StringUtils
-import org.scalatest.FlatSpec
-import org.scalatest.Matchers
-
-import akka.actor.ActorSystem
-import common.WhiskProperties
-import spray.json._
-import whisk.core.entity.Exec
-
-/**
- * For testing convenience, this interface abstracts away the REST calls to a
- * container as blocking method calls of this interface.
- */
-trait ActionContainer {
-  def init(value: JsValue): (Int, Option[JsObject])
-  def run(value: JsValue): (Int, Option[JsObject])
-}
-
-trait ActionProxyContainerTestUtils extends FlatSpec with Matchers {
-  import ActionContainer.{filterSentinel, sentinel}
-
-  def initPayload(code: String, main: String = "main") = {
-    JsObject(
-      "value" -> JsObject(
-        "code" -> { if (code != null) JsString(code) else JsNull },
-        "main" -> JsString(main),
-        "binary" -> JsBoolean(Exec.isBinaryCode(code))))
-  }
-
-  def runPayload(args: JsValue, other: Option[JsObject] = None) = {
-    JsObject(Map("value" -> args) ++ (other map { _.fields } getOrElse Map()))
-  }
-
-  def checkStreams(out: String, err: String, additionalCheck: (String, String) => Unit, sentinelCount: Int = 1) = {
-    withClue("expected number of stdout sentinels") {
-      sentinelCount shouldBe StringUtils.countMatches(out, sentinel)
-    }
-    withClue("expected number of stderr sentinels") {
-      sentinelCount shouldBe StringUtils.countMatches(err, sentinel)
-    }
-
-    val (o, e) = (filterSentinel(out), filterSentinel(err))
-    o should not include (sentinel)
-    e should not include (sentinel)
-    additionalCheck(o, e)
-  }
-}
-
-object ActionContainer {
-  private lazy val dockerBin: String = {
-    List("/usr/bin/docker", "/usr/local/bin/docker")
-      .find { bin =>
-        new File(bin).isFile()
-      }
-      .getOrElse(???) // This fails if the docker binary couldn't be located.
-  }
-
-  private lazy val dockerCmd: String = {
-    val version = WhiskProperties.getProperty("whisk.version.name")
-    // Check if we are running on docker-machine env.
-    val hostStr = if (version.toLowerCase().contains("mac")) {
-      s" --host tcp://${WhiskProperties.getMainDockerEndpoint()} "
-    } else {
-      " "
-    }
-    s"$dockerBin $hostStr"
-  }
-
-  private def docker(command: String): String = s"$dockerCmd $command"
-
-  // Runs a process asynchronously. Returns a future with (exitCode,stdout,stderr)
-  private def proc(cmd: String): Future[(Int, String, String)] = Future {
-    blocking {
-      val out = new ByteArrayOutputStream
-      val err = new ByteArrayOutputStream
-      val outW = new PrintWriter(out)
-      val errW = new PrintWriter(err)
-      val v = cmd ! (ProcessLogger(outW.println, errW.println))
-      outW.close()
-      errW.close()
-      (v, out.toString, err.toString)
-    }
-  }
-
-  // Tying it all together, we have a method that runs docker, waits for
-  // completion for some time then returns the exit code, the output stream
-  // and the error stream.
-  private def awaitDocker(cmd: String, t: Duration): (Int, String, String) = {
-    Await.result(proc(docker(cmd)), t)
-  }
-
-  // Filters out the sentinel markers inserted by the container (see relevant private code in Invoker.scala)
-  val sentinel = "XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX"
-  def filterSentinel(str: String) = str.replaceAll(sentinel, "").trim
-
-  def withContainer(imageName: String, environment: Map[String, String] = Map.empty)(code: ActionContainer => Unit)(
-    implicit actorSystem: ActorSystem): (String, String) = {
-    val rand = { val r = Random.nextInt; if (r < 0) -r else r }
-    val name = imageName.toLowerCase.replaceAll("""[^a-z]""", "") + rand
-    val envArgs = environment.toSeq.map {
-      case (k, v) => s"-e ${k}=${v}"
-    } mkString (" ")
-
-    // We create the container...
-    val runOut = awaitDocker(s"run --name $name $envArgs -d $imageName", 10 seconds)
-    assert(runOut._1 == 0, "'docker run' did not exit with 0: " + runOut)
-
-    // ...find out its IP address...
-    val ipOut = awaitDocker(s"""inspect --format '{{.NetworkSettings.IPAddress}}' $name""", 10 seconds)
-    assert(ipOut._1 == 0, "'docker inspect did not exit with 0")
-    val ip = ipOut._2.replaceAll("""[^0-9.]""", "")
-
-    // ...we create an instance of the mock container interface...
-    val mock = new ActionContainer {
-      def init(value: JsValue) = syncPost(ip, 8080, "/init", value)
-      def run(value: JsValue) = syncPost(ip, 8080, "/run", value)
-    }
-
-    try {
-      // ...and finally run the code with it.
-      code(mock)
-      // I'm told this is good for the logs.
-      Thread.sleep(100)
-      val (_, out, err) = awaitDocker(s"logs $name", 10 seconds)
-      (out, err)
-    } finally {
-      awaitDocker(s"kill $name", 10 seconds)
-      awaitDocker(s"rm $name", 10 seconds)
-    }
-  }
-
-  private def syncPost(host: String, port: Int, endPoint: String, content: JsValue): (Int, Option[JsObject]) = {
-    whisk.core.containerpool.HttpUtils.post(host, port, endPoint, content)
-  }
-
-  private class ActionContainerImpl() extends ActionContainer {
-    override def init(value: JsValue) = ???
-    override def run(value: JsValue) = ???
-  }
-}
diff --git a/tests/src/test/scala/actionContainers/ResourceHelpers.scala b/tests/src/test/scala/actionContainers/ResourceHelpers.scala
deleted file mode 100644
index 08f46e4..0000000
--- a/tests/src/test/scala/actionContainers/ResourceHelpers.scala
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package runtime.actionContainers
-
-import java.net.URI
-import java.net.URLClassLoader
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.Paths
-import java.nio.file.SimpleFileVisitor
-import java.nio.file.FileVisitResult
-import java.nio.file.FileSystems
-import java.nio.file.attribute.BasicFileAttributes
-import java.nio.charset.StandardCharsets
-import java.util.Base64
-
-import javax.tools.ToolProvider
-
-import collection.JavaConverters._
-
-/**
- * A collection of utility objects to create ephemeral action resources based
- *  on file contents.
- */
-object ResourceHelpers {
-
-  /** Creates a zip file based on the contents of a top-level directory. */
-  object ZipBuilder {
-    def mkBase64Zip(sources: Seq[(Seq[String], String)]): String = {
-      val (tmpDir, _) = writeSourcesToTempDirectory(sources)
-      val archive = makeZipFromDir(tmpDir)
-      readAsBase64(archive)
-    }
-  }
-
-  /**
-   * A convenience object to compile and package Java sources into a JAR, and to
-   * encode that JAR as a base 64 string. The compilation options include the
-   * current classpath, which is why Google GSON is readily available (though not
-   * packaged in the JAR).
-   */
-  object JarBuilder {
-    def mkBase64Jar(sources: Seq[(Seq[String], String)]): String = {
-      // Note that this pipeline doesn't delete any of the temporary files.
-      val binDir = compile(sources)
-      val jarPath = makeJarFromDir(binDir)
-      val base64 = readAsBase64(jarPath)
-      base64
-    }
-
-    def mkBase64Jar(source: (Seq[String], String)): String = {
-      mkBase64Jar(Seq(source))
-    }
-
-    private def compile(sources: Seq[(Seq[String], String)]): Path = {
-      require(!sources.isEmpty)
-
-      // The absolute paths of the source file
-      val (srcDir, srcAbsPaths) = writeSourcesToTempDirectory(sources)
-
-      // A temporary directory for the destination files.
-      val binDir = Files.createTempDirectory("bin").toAbsolutePath()
-
-      // Preparing the compiler
-      val compiler = ToolProvider.getSystemJavaCompiler()
-      val fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)
-
-      // Collecting all files to be compiled
-      val compUnit = fileManager.getJavaFileObjectsFromFiles(srcAbsPaths.map(_.toFile).asJava)
-
-      // Setting the options
-      val compOptions = Seq("-d", binDir.toAbsolutePath().toString(), "-classpath", buildClassPath())
-      val compTask = compiler.getTask(null, fileManager, null, compOptions.asJava, null, compUnit)
-
-      // ...and off we go.
-      compTask.call()
-
-      binDir
-    }
-
-    private def buildClassPath(): String = {
-      val bcp = System.getProperty("java.class.path")
-
-      val list = this.getClass().getClassLoader() match {
-        case ucl: URLClassLoader =>
-          bcp :: ucl.getURLs().map(_.getFile().toString()).toList
-
-        case _ =>
-          List(bcp)
-      }
-
-      list.mkString(System.getProperty("path.separator"))
-    }
-  }
-
-  /**
-   * Creates a temporary directory and reproduces the desired file structure
-   * in it. Returns the path of the temporary directory and the path of each
-   * file as represented in it.
-   */
-  private def writeSourcesToTempDirectory(sources: Seq[(Seq[String], String)]): (Path, Seq[Path]) = {
-    // A temporary directory for the source files.
-    val srcDir = Files.createTempDirectory("src").toAbsolutePath()
-
-    val srcAbsPaths = for ((sourceName, sourceContent) <- sources) yield {
-      // The relative path of the source file
-      val srcRelPath = Paths.get(sourceName.head, sourceName.tail: _*)
-      // The absolute path of the source file
-      val srcAbsPath = srcDir.resolve(srcRelPath)
-      // Create parent directories if needed.
-      Files.createDirectories(srcAbsPath.getParent)
-      // Writing contents
-      Files.write(srcAbsPath, sourceContent.getBytes(StandardCharsets.UTF_8))
-
-      srcAbsPath
-    }
-
-    (srcDir, srcAbsPaths)
-  }
-
-  private def makeZipFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".zip")
-
-  private def makeJarFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".jar")
-
-  /**
-   * Compresses all files beyond a directory into a zip file.
-   * Note that Jar files are just zip files.
-   */
-  private def makeArchiveFromDir(dir: Path, extension: String): Path = {
-    // Any temporary file name for the archive.
-    val arPath = Files.createTempFile("output", extension).toAbsolutePath()
-
-    // We "mount" it as a filesystem, so we can just copy files into it.
-    val dstUri = new URI("jar:" + arPath.toUri().getScheme(), arPath.toAbsolutePath().toString(), null)
-    // OK, that's a hack. Doing this because newFileSystem wants to create that file.
-    arPath.toFile().delete()
-    val fs = FileSystems.newFileSystem(dstUri, Map(("create" -> "true")).asJava)
-
-    // Traversing all files in the bin directory...
-    Files.walkFileTree(
-      dir,
-      new SimpleFileVisitor[Path]() {
-        override def visitFile(path: Path, attributes: BasicFileAttributes) = {
-          // The path relative to the src dir
-          val relPath = dir.relativize(path)
-
-          // The corresponding path in the zip
-          val arRelPath = fs.getPath(relPath.toString())
-
-          // If this file is not top-level in the src dir...
-          if (relPath.getParent() != null) {
-            // ...create the directory structure if it doesn't exist.
-            if (!Files.exists(arRelPath.getParent())) {
-              Files.createDirectories(arRelPath.getParent())
-            }
-          }
-
-          // Finally we can copy that file.
-          Files.copy(path, arRelPath)
-
-          FileVisitResult.CONTINUE
-        }
-      })
-
-    fs.close()
-
-    arPath
-  }
-
-  /** Reads the contents of a (possibly binary) file into a base64-encoded String */
-  def readAsBase64(path: Path): String = {
-    val encoder = Base64.getEncoder()
-    new String(encoder.encode(Files.readAllBytes(path)), StandardCharsets.UTF_8)
-  }
-}
diff --git a/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala
index bae51b1..3351717 100644
--- a/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala
@@ -25,7 +25,7 @@ import spray.json.JsString
 class Swift311ActionContainerTests extends SwiftActionContainerTests {
 
   override lazy val swiftContainerImageName = "action-swift-v3.1.1"
-  override lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/build/swift311/HelloSwift3.zip"
+  override lazy val swiftBinaryName = "tests/dat/build/swift311/HelloSwift3.zip"
 
   lazy val watsonCode = """
         | import AlchemyDataNewsV1
diff --git a/tests/src/test/scala/actionContainers/Swift40ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift40ActionContainerTests.scala
index f1a84ac..51ddca7 100644
--- a/tests/src/test/scala/actionContainers/Swift40ActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/Swift40ActionContainerTests.scala
@@ -22,12 +22,14 @@ import java.io.File
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
 import spray.json.{JsObject, JsString}
+import actionContainers.ResourceHelpers
+
 @RunWith(classOf[JUnitRunner])
 class Swift40ActionContainerTests extends SwiftActionContainerTests {
 
   override lazy val swiftContainerImageName = "action-swift-v4.0"
-  override lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/build/swift4.0/HelloSwift4.zip"
-  lazy val partyCompile = System.getProperty("user.dir") + "/dat/build/swift4.0/SwiftyRequest.zip"
+  override lazy val swiftBinaryName = "tests/dat/build/swift4.0/HelloSwift4.zip"
+  lazy val partyCompile = "tests/dat/build/swift4.0/SwiftyRequest.zip"
 
   val httpCode = """
        | import Dispatch
diff --git a/tests/src/test/scala/actionContainers/Swift40CodableActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift40CodableActionContainerTests.scala
new file mode 100644
index 0000000..8a71155
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/Swift40CodableActionContainerTests.scala
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.actionContainers
+
+class Swift40CodableActionContainerTests extends SwiftCodableActionContainerTests {
+  override lazy val swiftContainerImageName = "action-swift-v4.0"
+  override lazy val swiftBinaryName = "tests/dat/build/swift4.0/HelloSwift4Codable.zip"
+}
diff --git a/tests/src/test/scala/actionContainers/Swift41ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift41ActionContainerTests.scala
index 5530f64..04d44b6 100644
--- a/tests/src/test/scala/actionContainers/Swift41ActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/Swift41ActionContainerTests.scala
@@ -23,6 +23,6 @@ import org.scalatest.junit.JUnitRunner
 @RunWith(classOf[JUnitRunner])
 class Swift41ActionContainerTests extends Swift40ActionContainerTests {
   override lazy val swiftContainerImageName = "action-swift-v4.1"
-  override lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/build/swift4.1/HelloSwift4.zip"
-  override lazy val partyCompile = System.getProperty("user.dir") + "/dat/build/swift4.1/SwiftyRequest.zip"
+  override lazy val swiftBinaryName = "tests/dat/build/swift4.1/HelloSwift4.zip"
+  override lazy val partyCompile = "tests/dat/build/swift4.1/SwiftyRequest.zip"
 }
diff --git a/tests/src/test/scala/actionContainers/Swift41CodableActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift41CodableActionContainerTests.scala
new file mode 100644
index 0000000..0e4f6ef
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/Swift41CodableActionContainerTests.scala
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.actionContainers
+
+class Swift41CodableActionContainerTests extends SwiftCodableActionContainerTests {
+  override lazy val swiftContainerImageName = "action-swift-v4.1"
+  override lazy val swiftBinaryName = "tests/dat/build/swift4.1/HelloSwift4Codable.zip"
+}
diff --git a/tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala b/tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala
index aa3af7a..1754c83 100644
--- a/tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala
@@ -19,7 +19,8 @@ package runtime.actionContainers
 
 import java.io.File
 import common.WskActorSystem
-import ActionContainer.withContainer
+import actionContainers.{ActionContainer, ActionProxyContainerTestUtils, ResourceHelpers}
+import actionContainers.ActionContainer.withContainer
 import spray.json.DefaultJsonProtocol._
 import spray.json._
 
@@ -29,7 +30,7 @@ abstract class SwiftActionContainerTests extends BasicActionRunnerTests with Wsk
   // prints status messages and there doesn't seem to be a way to quiet them
   val enforceEmptyOutputStream = false
   lazy val swiftContainerImageName = "action-swift-v4.0"
-  lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/actions/swift4zip/build/Hello.zip"
+  lazy val swiftBinaryName = "tests/dat/actions/swift4zip/build/Hello.zip"
   val httpCode: String
 
   behavior of swiftContainerImageName
diff --git a/tests/src/test/scala/actionContainers/SwiftCodableActionContainerTests.scala b/tests/src/test/scala/actionContainers/SwiftCodableActionContainerTests.scala
new file mode 100644
index 0000000..82aab26
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/SwiftCodableActionContainerTests.scala
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.actionContainers
+
+import java.io.File
+import common.WskActorSystem
+import actionContainers.{ActionContainer, ResourceHelpers}
+import actionContainers.ActionContainer.withContainer
+import spray.json._
+
+abstract class SwiftCodableActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
+
+  // note: "out" will likely not be empty in some swift build as the compiler
+  // prints status messages and there doesn't seem to be a way to quiet them
+  val enforceEmptyOutputStream = false
+  lazy val swiftContainerImageName = "action-swift-v4.0"
+  lazy val swiftBinaryName = "tests/dat/build/swift4.0/HelloCodable.zip"
+
+  behavior of s"Codable $swiftContainerImageName"
+
+  testEcho(Seq {
+    (
+      "swift echo",
+      """
+        |
+        | extension FileHandle : TextOutputStream {
+        |     public func write(_ string: String) {
+        |         guard let data = string.data(using: .utf8) else { return }
+        |         self.write(data)
+        |     }
+        | }
+        |
+        | struct AnInput: Codable {
+        |  struct AnObject: Codable {
+        |   let a: String?
+        |  }
+        |  let string: String?
+        |  let numbers: [Int]?
+        |  let object: AnObject?
+        | }
+        | func main(input: AnInput, respondWith: (AnInput?, Error?) -> Void) -> Void {
+        |    print("hello stdout")
+        |    var standardError = FileHandle.standardError
+        |    print("hello stderr", to: &standardError)
+        |    respondWith(input, nil)
+        | }
+      """.stripMargin)
+  })
+
+  testUnicode(Seq {
+    (
+      "swift unicode",
+      """
+        | struct AnInputOutput: Codable {
+        |  let delimiter: String?
+        |  let winter: String?
+        |  let error: String?
+        | }
+        | func main(input: AnInputOutput, respondWith: (AnInputOutput?, Error?) -> Void) -> Void {
+        |    if let str = input.delimiter as? String {
+        |        let msg = "\(str) ? \(str)"
+        |        print(msg)
+        |        let answer = AnInputOutput(delimiter: nil, winter: msg, error: nil)
+        |        respondWith(answer, nil)
+        |    } else {
+        |        let answer = AnInputOutput(delimiter: "no delimiter", winter: nil, error: nil)
+        |        respondWith(answer, nil)
+        |    }
+        | }
+      """.stripMargin.trim)
+  })
+
+  testEnv(
+    Seq {
+      (
+        "swift environment",
+        """
+        | struct AnOutput: Codable {
+        |  let api_host: String
+        |  let api_key: String
+        |  let namespace: String
+        |  let action_name: String
+        |  let activation_id: String
+        |  let deadline: String
+        | }
+        | func main(respondWith: (AnOutput?, Error?) -> Void) -> Void {
+        |     let env = ProcessInfo.processInfo.environment
+        |     var a = "???"
+        |     var b = "???"
+        |     var c = "???"
+        |     var d = "???"
+        |     var e = "???"
+        |     var f = "???"
+        |     if let v : String = env["__OW_API_HOST"] {
+        |         a = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_API_KEY"] {
+        |         b = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_NAMESPACE"] {
+        |         c = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_ACTION_NAME"] {
+        |         d = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_ACTIVATION_ID"] {
+        |         e = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_DEADLINE"] {
+        |         f = "\(v)"
+        |     }
+        |     let result = AnOutput(api_host:a, api_key:b, namespace:c, action_name:d, activation_id:e, deadline: f)
+        |     respondWith(result, nil)
+        | }
+      """.stripMargin)
+    },
+    enforceEmptyOutputStream)
+
+  it should "support actions using non-default entry points" in {
+    withActionContainer() { c =>
+      val code = """
+                   | struct AnOutput: Codable {
+                   |   let result: String?
+                   | }
+                   | func niam(respondWith: (AnOutput?, Error?) -> Void) -> Void {
+                   |    respondWith(AnOutput(result: "it works"), nil)
+                   | }
+                   |""".stripMargin
+
+      val (initCode, initRes) = c.init(initPayload(code, main = "niam"))
+      initCode should be(200)
+
+      val (_, runRes) = c.run(runPayload(JsObject()))
+      runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
+    }
+  }
+
+  it should "return some error on action error" in {
+    val (out, err) = withActionContainer() { c =>
+      val code = """
+                   | // You need an indirection, or swiftc detects the div/0
+                   | // at compile-time. Smart.
+                   | func div(x: Int, y: Int) -> Int {
+                   |    return x/y
+                   | }
+                   | struct Result: Codable{
+                   |    let divBy0: Int?
+                   | }
+                   | func main(respondWith: (Result?, Error?) -> Void) -> Void {
+                   |    respondWith(Result(divBy0: div(x:5, y:0)), nil)
+                   | }
+                 """.stripMargin
+
+      val (initCode, _) = c.init(initPayload(code))
+      initCode should be(200)
+
+      val (runCode, runRes) = c.run(runPayload(JsObject()))
+      runCode should be(502)
+
+      runRes shouldBe defined
+      runRes.get.fields.get("error") shouldBe defined
+    }
+
+    checkStreams(out, err, {
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e shouldBe empty
+    })
+  }
+
+  it should "support application errors" in {
+    val (out, err) = withActionContainer() { c =>
+      val code = """
+                   | struct Result: Codable{
+                   |    let error: String?
+                   | }
+                   | func main(respondWith: (Result?, Error?) -> Void) -> Void {
+                   |    respondWith(Result(error: "sorry"), nil)
+                   | }
+                 """.stripMargin
+
+      val (initCode, _) = c.init(initPayload(code))
+      initCode should be(200)
+
+      val (runCode, runRes) = c.run(runPayload(JsObject()))
+      runCode should be(200) // action writer returning an error is OK
+
+      runRes shouldBe defined
+      runRes should be(Some(JsObject("error" -> JsString("sorry"))))
+    }
+
+    checkStreams(out, err, {
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e shouldBe empty
+    })
+  }
+
+  it should "support pre-compiled binary in a zip file" in {
+    val zip = new File(swiftBinaryName).toPath
+    val code = ResourceHelpers.readAsBase64(zip)
+
+    val (out, err) = withActionContainer() { c =>
+      val (initCode, initRes) = c.init(initPayload(code))
+      initCode should be(200)
+
+      val args = JsObject()
+      val (runCode, runRes) = c.run(runPayload(args))
+
+      runCode should be(200)
+      runRes.get shouldBe JsObject("greeting" -> (JsString("Hello stranger!")))
+    }
+
+    checkStreams(out, err, {
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e shouldBe empty
+    })
+  }
+
+  // Helpers specific to swift actions
+  override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
+    withContainer(swiftContainerImageName, env)(code)
+  }
+
+}
diff --git a/tests/src/test/scala/sdk/SwiftSDKTests.scala b/tests/src/test/scala/sdk/SwiftSDKTests.scala
index 14fcbee..d5437db 100644
--- a/tests/src/test/scala/sdk/SwiftSDKTests.scala
+++ b/tests/src/test/scala/sdk/SwiftSDKTests.scala
@@ -30,13 +30,12 @@ abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matche
 
   implicit val wskprops = WskProps()
   val wsk = new WskRest
-  val expectedDuration = 45 seconds
-  val activationPollDuration = 60 seconds
+  val activationPollDuration = 2.minutes
   lazy val actionKind = "swift:3.1.1"
   lazy val lang = actionKind.split(":")(0)
   lazy val majorVersion = actionKind.split(":")(1).split('.')(0)
   lazy val actionDir = s"$lang$majorVersion"
-  lazy val actionTypeDir: String = System.getProperty("user.dir") + "/dat/actions/sdk/" + actionDir
+  lazy val actionTypeDir: String = "tests/dat/actions/sdk/" + actionDir
   val controllerHost = WhiskProperties.getBaseControllerHost()
   val controllerPort = WhiskProperties.getControllerBasePort()
   val baseUrl = s"http://$controllerHost:$controllerPort"
@@ -58,7 +57,7 @@ abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matche
       params = params + ("baseUrl" -> JsString(baseUrl))
 
     val run = wsk.action.invoke(actionName, params)
-    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = activationPollDuration) { activation =>
       // should be successful
       activation.response.success shouldBe true
 
@@ -86,7 +85,7 @@ abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matche
         params = params + ("baseUrl" -> JsString(baseUrl))
 
       val run = wsk.action.invoke(actionName, params)
-      withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+      withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = activationPollDuration) { activation =>
         // should not have a "response"
         whisk.utils.JsHelpers.fieldPathExists(activation.response.result.get, "response") shouldBe false
 
@@ -127,7 +126,7 @@ abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matche
       params = params + ("baseUrl" -> JsString(baseUrl))
 
     val run = wsk.action.invoke(actionName, params)
-    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = activationPollDuration) { activation =>
       // should be successful
       activation.response.success shouldBe true
 
@@ -163,7 +162,7 @@ abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matche
       params = params + ("baseUrl" -> JsString(baseUrl))
 
     val run = wsk.action.invoke(actionName, params)
-    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = activationPollDuration) { activation =>
       // should be successful
       activation.response.success shouldBe true
 
@@ -206,7 +205,7 @@ abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matche
       params = params + ("baseUrl" -> JsString(baseUrl))
 
     val run = wsk.action.invoke("ActionThatCreatesRule", params)
-    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = activationPollDuration) { activation =>
       // should be successful
       activation.response.success shouldBe true
 
diff --git a/tests/tests/dat b/tests/tests/dat
new file mode 120000
index 0000000..735ed3b
--- /dev/null
+++ b/tests/tests/dat
@@ -0,0 +1 @@
+../dat
\ No newline at end of file


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services