diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000000000000000000000000000000000000..0dc41ec0b2739252b7b521e30fb166e4e7ec6c1e --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,6 @@ +style = intellij +danglingParentheses = false +maxColumn = 120 +docstrings = JavaDoc +rewrite.rules = [SortImports] +project.git = true \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..300a1ce98215f2395c502b69d50843e385d161e1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,13 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "cz.alenkacz:gradle-scalafmt:${gradle.scalafmt.version}" + } +} + +subprojects { + apply plugin: 'scalafmt' + scalafmt.configFilePath = gradle.scalafmt.config +} diff --git a/settings.gradle b/settings.gradle index 375b6de15a08610a56b0a6728ac06b3dd231b9d7..9a27b319b4d55f2a2ff80b290f2e9f3c3b3d139e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,3 +26,8 @@ gradle.ext.scala = [ version: '2.11.8', compileFlags: ['-feature', '-unchecked', '-deprecation', '-Xfatal-warnings', '-Ywarn-unused-import'] ] + +gradle.ext.scalafmt = [ + version: '1.5.0', + config: new File(rootProject.projectDir, '.scalafmt.conf') +] diff --git a/tests/src/test/scala/actionContainers/ActionContainer.scala b/tests/src/test/scala/actionContainers/ActionContainer.scala index e9cc9392ed9b78204d044a21e44d12e51b59c580..63da8c7e5fc5e3e6b125c0dcd88a204cd78da3b8 100644 --- a/tests/src/test/scala/actionContainers/ActionContainer.scala +++ b/tests/src/test/scala/actionContainers/ActionContainer.scala @@ -46,124 +46,127 @@ import whisk.core.entity.Exec * container as blocking method calls of this interface. */ trait ActionContainer { - def init(value: JsValue): (Int, Option[JsObject]) - def run(value: JsValue): (Int, Option[JsObject]) + 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)))) + 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) } - - def runPayload(args: JsValue, other: Option[JsObject] = None) = { - JsObject(Map("value" -> args) ++ (other map { _.fields } getOrElse Map())) + withClue("expected number of stderr sentinels") { + sentinelCount shouldBe StringUtils.countMatches(err, sentinel) } - 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) - } + 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 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 hostStr = if (WhiskProperties.onMacOSX()) { + s" --host tcp://${WhiskProperties.getMainDockerEndpoint()} " + } else { + " " } - - private lazy val dockerCmd: String = { - val hostStr = if (WhiskProperties.onMacOSX()) { - s" --host tcp://${WhiskProperties.getMainDockerEndpoint()} " - } else { - " " - } - s"$dockerBin $hostStr" + 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) } - - 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) } - // 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) + 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) } + } - // 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.docker.HttpUtils.post(host, port, endPoint, content) - } + private def syncPost(host: String, port: Int, endPoint: String, content: JsValue): (Int, Option[JsObject]) = { + whisk.core.containerpool.docker.HttpUtils.post(host, port, endPoint, content) + } - private class ActionContainerImpl() extends ActionContainer { - override def init(value: JsValue) = ??? - override def run(value: JsValue) = ??? - } + private class ActionContainerImpl() extends ActionContainer { + override def init(value: JsValue) = ??? + override def run(value: JsValue) = ??? + } } diff --git a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala b/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala index dabe5cb2bfe8d1906d9ae93f6e96b9e65a705eab..1ec8e9f89b129ee7c264188df870fdb49a52ec61 100644 --- a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala +++ b/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala @@ -33,18 +33,18 @@ import spray.json._ @RunWith(classOf[JUnitRunner]) class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSystem { - override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = { - withContainer("dockerskeleton", env)(code) - } + override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = { + withContainer("dockerskeleton", env)(code) + } - val codeNotReturningJson = """ + val codeNotReturningJson = """ |#!/bin/sh |echo not a json object """.stripMargin.trim - /** Standard code samples, should print 'hello' to stdout and echo the input args. */ - val stdCodeSamples = { - val bash = """ + /** Standard code samples, should print 'hello' to stdout and echo the input args. */ + val stdCodeSamples = { + val bash = """ |#!/bin/bash |echo 'hello stdout' |echo 'hello stderr' 1>&2 @@ -55,7 +55,7 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst |fi """.stripMargin.trim - val python = """ + val python = """ |#!/usr/bin/env python |from __future__ import print_function |import sys @@ -64,20 +64,20 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst |print(sys.argv[1]) """.stripMargin.trim - val perl = """ + val perl = """ |#!/usr/bin/env perl |print STDOUT "hello stdout\n"; |print STDERR "hello stderr\n"; |print $ARGV[0]; """.stripMargin.trim - // excluding perl as it not installed in alpine based image - Seq(("bash", bash), ("python", python)) - } + // excluding perl as it not installed in alpine based image + Seq(("bash", bash), ("python", python)) + } - val stdUnicodeSamples = { - // python 3 in base image - val python = """ + val stdUnicodeSamples = { + // python 3 in base image + val python = """ |#!/usr/bin/env python |import json, sys |j = json.loads(sys.argv[1]) @@ -87,12 +87,12 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst |print(json.dumps({"winter": s})) """.stripMargin.trim - Seq(("python", python)) - } + Seq(("python", python)) + } - /** Standard code samples, should print 'hello' to stdout and echo the input args. */ - val stdEnvSamples = { - val bash = """ + /** Standard code samples, should print 'hello' to stdout and echo the input args. */ + val stdEnvSamples = { + val bash = """ |#!/bin/bash |echo "{ \ |\"api_host\": \"$__OW_API_HOST\", \"api_key\": \"$__OW_API_KEY\", \ @@ -100,7 +100,8 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst |\"activation_id\": \"$__OW_ACTIVATION_ID\", \"deadline\": \"$__OW_DEADLINE\" }" """.stripMargin.trim - val python = """ + val python = + """ |#!/usr/bin/env python |import os | @@ -110,7 +111,8 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst | os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE'])) """.stripMargin.trim - val perl = """ + val perl = + """ |#!/usr/bin/env perl |$a = $ENV{'__OW_API_HOST'}; |$b = $ENV{'__OW_API_KEY'}; @@ -121,222 +123,226 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst |print "{ \"api_host\": \"$a\", \"api_key\": \"$b\", \"namespace\": \"$c\", \"action_name\": \"$d\", \"activation_id\": \"$e\", \"deadline\": \"$f\" }"; """.stripMargin.trim - // excluding perl as it not installed in alpine based image - Seq(("bash", bash), ("python", python)) - } - - behavior of "openwhisk/dockerskeleton" + // excluding perl as it not installed in alpine based image + Seq(("bash", bash), ("python", python)) + } - it should "run sample without init" in { - val (out, err) = withActionContainer() { c => - val (runCode, out) = c.run(JsObject()) - runCode should be(200) - out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) - } + behavior of "openwhisk/dockerskeleton" - checkStreams(out, err, { - case (o, _) => o should include("This is a stub action") - }) + it should "run sample without init" in { + val (out, err) = withActionContainer() { c => + val (runCode, out) = c.run(JsObject()) + runCode should be(200) + out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) } - it should "run sample with 'null' init" in { - val (out, err) = withActionContainer() { c => - val (initCode, _) = c.init(initPayload(null)) - initCode should be(200) + checkStreams(out, err, { + case (o, _) => o should include("This is a stub action") + }) + } - val (runCode, out) = c.run(JsObject()) - runCode should be(200) - out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) - } + it should "run sample with 'null' init" in { + val (out, err) = withActionContainer() { c => + val (initCode, _) = c.init(initPayload(null)) + initCode should be(200) - checkStreams(out, err, { - case (o, _) => o should include("This is a stub action") - }) + val (runCode, out) = c.run(JsObject()) + runCode should be(200) + out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) } - it should "run sample with init that does nothing" in { - val (out, err) = withActionContainer() { c => - val (initCode, _) = c.init(JsObject()) - initCode should be(200) - val (runCode, out) = c.run(JsObject()) - runCode should be(200) - out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) - } - - checkStreams(out, err, { - case (o, _) => o should include("This is a stub action") - }) + checkStreams(out, err, { + case (o, _) => o should include("This is a stub action") + }) + } + + it should "run sample with init that does nothing" in { + val (out, err) = withActionContainer() { c => + val (initCode, _) = c.init(JsObject()) + initCode should be(200) + val (runCode, out) = c.run(JsObject()) + runCode should be(200) + out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) } - it should "respond with 404 for bad run argument" in { - val (out, err) = withActionContainer() { c => - val (runCode, out) = c.run(runPayload(JsString("A"))) - runCode should be(404) - } + checkStreams(out, err, { + case (o, _) => o should include("This is a stub action") + }) + } - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e shouldBe empty - }) + it should "respond with 404 for bad run argument" in { + val (out, err) = withActionContainer() { c => + val (runCode, out) = c.run(runPayload(JsString("A"))) + runCode should be(404) } - it should "fail to run a bad script" in { - val (out, err) = withActionContainer() { c => - val (initCode, _) = c.init(initPayload("")) - initCode should be(200) - val (runCode, out) = c.run(JsNull) - runCode should be(502) - out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary.")))) - } + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "fail to run a bad script" in { + val (out, err) = withActionContainer() { c => + val (initCode, _) = c.init(initPayload("")) + initCode should be(200) + val (runCode, out) = c.run(JsNull) + runCode should be(502) + out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary.")))) + } - checkStreams(out, err, { - case (o, _) => o should include("error") - }) + checkStreams(out, err, { + case (o, _) => o should include("error") + }) + } + + it should "extract and run a compatible zip exec" in { + val zip = FileUtils.readFileToByteArray(new File(TestUtils.getTestActionFilename("blackbox.zip"))) + val contents = Base64.getEncoder.encodeToString(zip) + + val (out, err) = withActionContainer() { c => + val (initCode, err) = + c.init(JsObject("value" -> JsObject("code" -> JsString(contents), "binary" -> JsBoolean(true)))) + initCode should be(200) + val (runCode, out) = c.run(JsObject()) + runCode should be(200) + out.get should be(JsObject("msg" -> JsString("hello zip"))) } - it should "extract and run a compatible zip exec" in { - val zip = FileUtils.readFileToByteArray(new File(TestUtils.getTestActionFilename("blackbox.zip"))) - val contents = Base64.getEncoder.encodeToString(zip) + checkStreams(out, err, { + case (o, e) => + o shouldBe "This is an example zip used with the docker skeleton action." + e shouldBe empty + }) + } + + testNotReturningJson(codeNotReturningJson, checkResultInLogs = true) + testEcho(stdCodeSamples) + testUnicode(stdUnicodeSamples) + testEnv(stdEnvSamples) +} + +trait BasicActionRunnerTests extends ActionProxyContainerTestUtils { + def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit): (String, String) + + /** + * Runs tests for actions which do not return a dictionary and confirms expected error messages. + * @param codeNotReturningJson code to execute, should not return a JSON object + * @param checkResultInLogs should be true iff the result of the action is expected to appear in stdout or stderr + */ + def testNotReturningJson(codeNotReturningJson: String, checkResultInLogs: Boolean = true) = { + it should "run and report an error for script not returning a json object" in { + val (out, err) = withActionContainer() { c => + val (initCode, _) = c.init(initPayload(codeNotReturningJson)) + initCode should be(200) + val (runCode, out) = c.run(JsObject()) + runCode should be(502) + out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary.")))) + } + + checkStreams(out, err, { + case (o, e) => + if (checkResultInLogs) { + (o + e) should include("not a json object") + } else { + o shouldBe empty + e shouldBe empty + } + }) + } + } + + /** + * Runs tests for code samples which are expected to echo the input arguments + * and print hello [stdout, stderr]. + */ + def testEcho(stdCodeSamples: Seq[(String, String)]) = { + stdCodeSamples.foreach { s => + it should s"run a ${s._1} script" in { + val argss = List( + JsObject("string" -> JsString("hello")), + JsObject("string" -> JsString("℠☃ â„")), + JsObject("numbers" -> JsArray(JsNumber(42), JsNumber(1))), + // JsObject("boolean" -> JsBoolean(true)), // fails with swift3 returning boolean: 1 + JsObject("object" -> JsObject("a" -> JsString("A")))) val (out, err) = withActionContainer() { c => - val (initCode, err) = c.init(JsObject("value" -> JsObject("code" -> JsString(contents), "binary" -> JsBoolean(true)))) - initCode should be(200) - val (runCode, out) = c.run(JsObject()) + val (initCode, _) = c.init(initPayload(s._2)) + initCode should be(200) + + for (args <- argss) { + val (runCode, out) = c.run(runPayload(args)) runCode should be(200) - out.get should be(JsObject("msg" -> JsString("hello zip"))) + out should be(Some(args)) + } } checkStreams(out, err, { - case (o, e) => - o shouldBe "This is an example zip used with the docker skeleton action." - e shouldBe empty - }) + case (o, e) => + o should include("hello stdout") + e should include("hello stderr") + }, argss.length) + } } + } - testNotReturningJson(codeNotReturningJson, checkResultInLogs = true) - testEcho(stdCodeSamples) - testUnicode(stdUnicodeSamples) - testEnv(stdEnvSamples) -} + def testUnicode(stdUnicodeSamples: Seq[(String, String)]) = { + stdUnicodeSamples.foreach { s => + it should s"run a ${s._1} action and handle unicode in source, input params, logs, and result" in { + val (out, err) = withActionContainer() { c => + val (initCode, _) = c.init(initPayload(s._2)) + initCode should be(200) -trait BasicActionRunnerTests extends ActionProxyContainerTestUtils { - def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit): (String, String) - - /** - * Runs tests for actions which do not return a dictionary and confirms expected error messages. - * @param codeNotReturningJson code to execute, should not return a JSON object - * @param checkResultInLogs should be true iff the result of the action is expected to appear in stdout or stderr - */ - def testNotReturningJson(codeNotReturningJson: String, checkResultInLogs: Boolean = true) = { - it should "run and report an error for script not returning a json object" in { - val (out, err) = withActionContainer() { c => - val (initCode, _) = c.init(initPayload(codeNotReturningJson)) - initCode should be(200) - val (runCode, out) = c.run(JsObject()) - runCode should be(502) - out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary.")))) - } - - checkStreams(out, err, { - case (o, e) => - if (checkResultInLogs) { - (o + e) should include("not a json object") - } else { - o shouldBe empty - e shouldBe empty - } - }) + val (runCode, runRes) = c.run(runPayload(JsObject("delimiter" -> JsString("â„")))) + runRes.get.fields.get("winter") shouldBe Some(JsString("℠☃ â„")) } - } - /** - * Runs tests for code samples which are expected to echo the input arguments - * and print hello [stdout, stderr]. - */ - def testEcho(stdCodeSamples: Seq[(String, String)]) = { - stdCodeSamples.foreach { s => - it should s"run a ${s._1} script" in { - val argss = List( - JsObject("string" -> JsString("hello")), - JsObject("string" -> JsString("℠☃ â„")), - JsObject("numbers" -> JsArray(JsNumber(42), JsNumber(1))), - // JsObject("boolean" -> JsBoolean(true)), // fails with swift3 returning boolean: 1 - JsObject("object" -> JsObject("a" -> JsString("A")))) - - val (out, err) = withActionContainer() { c => - val (initCode, _) = c.init(initPayload(s._2)) - initCode should be(200) - - for (args <- argss) { - val (runCode, out) = c.run(runPayload(args)) - runCode should be(200) - out should be(Some(args)) - } - } - - checkStreams(out, err, { - case (o, e) => - o should include("hello stdout") - e should include("hello stderr") - }, argss.length) - } - } + checkStreams(out, err, { + case (o, _) => + o.toLowerCase should include("℠☃ â„") + }) + } } - - def testUnicode(stdUnicodeSamples: Seq[(String, String)]) = { - stdUnicodeSamples.foreach { s => - it should s"run a ${s._1} action and handle unicode in source, input params, logs, and result" in { - val (out, err) = withActionContainer() { c => - val (initCode, _) = c.init(initPayload(s._2)) - initCode should be(200) - - val (runCode, runRes) = c.run(runPayload(JsObject("delimiter" -> JsString("â„")))) - runRes.get.fields.get("winter") shouldBe Some(JsString("℠☃ â„")) - } - - checkStreams(out, err, { - case (o, _) => - o.toLowerCase should include("℠☃ â„") - }) - } + } + + /** Runs tests for code samples which are expected to return the expected standard environment {auth, edge}. */ + def testEnv(stdEnvSamples: Seq[(String, String)], + enforceEmptyOutputStream: Boolean = true, + enforceEmptyErrorStream: Boolean = true) = { + stdEnvSamples.foreach { s => + it should s"run a ${s._1} script and confirm expected environment variables" in { + val props = Seq( + "api_host" -> "xyz", + "api_key" -> "abc", + "namespace" -> "zzz", + "action_name" -> "xxx", + "activation_id" -> "iii", + "deadline" -> "123") + val env = props.map { case (k, v) => s"__OW_${k.toUpperCase()}" -> v } + + val (out, err) = withActionContainer(env.take(1).toMap) { c => + val (initCode, _) = c.init(initPayload(s._2)) + initCode should be(200) + + val (runCode, out) = c.run(runPayload(JsObject(), Some(props.toMap.toJson.asJsObject))) + runCode should be(200) + out shouldBe defined + props.map { + case (k, v) => + withClue(k) { + out.get.fields(k) shouldBe JsString(v) + } + + } } - } - /** Runs tests for code samples which are expected to return the expected standard environment {auth, edge}. */ - def testEnv(stdEnvSamples: Seq[(String, String)], enforceEmptyOutputStream: Boolean = true, enforceEmptyErrorStream: Boolean = true) = { - stdEnvSamples.foreach { s => - it should s"run a ${s._1} script and confirm expected environment variables" in { - val props = Seq( - "api_host" -> "xyz", - "api_key" -> "abc", - "namespace" -> "zzz", - "action_name" -> "xxx", - "activation_id" -> "iii", - "deadline" -> "123") - val env = props.map { case (k, v) => s"__OW_${k.toUpperCase()}" -> v } - - val (out, err) = withActionContainer(env.take(1).toMap) { c => - val (initCode, _) = c.init(initPayload(s._2)) - initCode should be(200) - - val (runCode, out) = c.run(runPayload(JsObject(), Some(props.toMap.toJson.asJsObject))) - runCode should be(200) - out shouldBe defined - props.map { - case (k, v) => withClue(k) { - out.get.fields(k) shouldBe JsString(v) - } - - } - } - - checkStreams(out, err, { - case (o, e) => - if (enforceEmptyOutputStream) o shouldBe empty - if (enforceEmptyErrorStream) e shouldBe empty - }) - } - } + checkStreams(out, err, { + case (o, e) => + if (enforceEmptyOutputStream) o shouldBe empty + if (enforceEmptyErrorStream) e shouldBe empty + }) + } } + } } diff --git a/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala index e88977879d2d8f2c8989d55e91e9f47453ddae6c..1740401f35d92a0a5a762e0174520ed4db6c8b65 100644 --- a/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala +++ b/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala @@ -24,8 +24,8 @@ import common.WskActorSystem @RunWith(classOf[JUnitRunner]) class Python2ActionContainerTests extends PythonActionContainerTests with WskActorSystem { - override lazy val imageName = "python2action" + override lazy val imageName = "python2action" - /** indicates if strings in python are unicode by default (i.e., python3 -> true, python2.7 -> false) */ - override lazy val pythonStringAsUnicode = false + /** indicates if strings in python are unicode by default (i.e., python3 -> true, python2.7 -> false) */ + override lazy val pythonStringAsUnicode = false } diff --git a/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala b/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala index d80b4ca121f24941d8ba598ebfa62aba2e7ce41e..1e4f1bc480bc02add0c4a938bee644cbbc408aa5 100644 --- a/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala +++ b/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala @@ -20,7 +20,7 @@ package actionContainers import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner import ActionContainer.withContainer -import ResourceHelpers.{ ZipBuilder, readAsBase64 } +import ResourceHelpers.{readAsBase64, ZipBuilder} import spray.json.DefaultJsonProtocol._ import spray.json._ import common.WskActorSystem @@ -30,25 +30,28 @@ import java.nio.file.Paths @RunWith(classOf[JUnitRunner]) class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSystem { - lazy val imageName = "python3action" + lazy val imageName = "python3action" - /** indicates if strings in python are unicode by default (i.e., python3 -> true, python2.7 -> false) */ - lazy val pythonStringAsUnicode = true + /** indicates if strings in python are unicode by default (i.e., python3 -> true, python2.7 -> false) */ + lazy val pythonStringAsUnicode = true - override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = { - withContainer(imageName, env)(code) - } + override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = { + withContainer(imageName, env)(code) + } - behavior of imageName + behavior of imageName - testNotReturningJson( - """ + testNotReturningJson( + """ |def main(args): | return "not a json object" - """.stripMargin, checkResultInLogs = false) + """.stripMargin, + checkResultInLogs = false) - testEcho(Seq { - ("python", """ + testEcho(Seq { + ( + "python", + """ |from __future__ import print_function |import sys |def main(args): @@ -56,30 +59,36 @@ class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSys | print('hello stderr', file=sys.stderr) | return args """.stripMargin) - }) + }) - testUnicode(Seq { - if (pythonStringAsUnicode) { - ("python", """ + testUnicode(Seq { + if (pythonStringAsUnicode) { + ( + "python", + """ |def main(args): | sep = args['delimiter'] | str = sep + " ☃ " + sep | print(str) | return {"winter" : str } """.stripMargin.trim) - } else { - ("python", """ + } else { + ( + "python", + """ |def main(args): | sep = args['delimiter'] | str = sep + " ☃ ".decode('utf-8') + sep | print(str.encode('utf-8')) | return {"winter" : str } """.stripMargin.trim) - } - }) + } + }) - testEnv(Seq { - ("python", """ + testEnv(Seq { + ( + "python", + """ |import os |def main(dict): | return { @@ -91,199 +100,198 @@ class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSys | "deadline": os.environ['__OW_DEADLINE'] | } """.stripMargin.trim) - }) + }) - it should "support actions using non-default entry points" in { - withActionContainer() { c => - val code = """ + it should "support actions using non-default entry points" in { + withActionContainer() { c => + val code = """ |def niam(dict): | return { "result": "it works" } |""".stripMargin - val (initCode, initRes) = c.init(initPayload(code, main = "niam")) - initCode should be(200) + 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")) - } + val (_, runRes) = c.run(runPayload(JsObject())) + runRes.get.fields.get("result") shouldBe Some(JsString("it works")) } + } - it should "support zip-encoded action using non-default entry points" in { - val srcs = Seq( - Seq("__main__.py") -> """ + it should "support zip-encoded action using non-default entry points" in { + val srcs = Seq( + Seq("__main__.py") -> """ |from echo import echo |def niam(args): | return echo(args) """.stripMargin, - Seq("echo.py") -> """ + Seq("echo.py") -> """ |def echo(args): | return { "echo": args } """.stripMargin) - val code = ZipBuilder.mkBase64Zip(srcs) + val code = ZipBuilder.mkBase64Zip(srcs) - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "niam")) - initCode should be(200) + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "niam")) + initCode should be(200) - val args = JsObject("msg" -> JsString("it works")) - val (runCode, runRes) = c.run(runPayload(args)) + val args = JsObject("msg" -> JsString("it works")) + val (runCode, runRes) = c.run(runPayload(args)) - runCode should be(200) - runRes.get.fields.get("echo") shouldBe Some(args) - } - - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e shouldBe empty - }) + runCode should be(200) + runRes.get.fields.get("echo") shouldBe Some(args) } - it should "support zip-encoded action which can read from relative paths" in { - val srcs = Seq( - Seq("__main__.py") -> """ + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "support zip-encoded action which can read from relative paths" in { + val srcs = Seq( + Seq("__main__.py") -> """ |def main(args): | f = open('workfile', 'r') | return {'file': f.read()} """.stripMargin, - Seq("workfile") -> "this is a test string") - - val code = ZipBuilder.mkBase64Zip(srcs) + Seq("workfile") -> "this is a test string") - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code)) - initCode should be(200) + val code = ZipBuilder.mkBase64Zip(srcs) - val args = JsObject() - val (runCode, runRes) = c.run(runPayload(args)) + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code)) + initCode should be(200) - runCode should be(200) - runRes.get.fields.get("file") shouldBe Some("this is a test string".toJson) - } + val args = JsObject() + val (runCode, runRes) = c.run(runPayload(args)) - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e shouldBe empty - }) + runCode should be(200) + runRes.get.fields.get("file") shouldBe Some("this is a test string".toJson) } - it should "report error if zip-encoded action does not include required file" in { - val srcs = Seq( - Seq("echo.py") -> """ + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "report error if zip-encoded action does not include required file" in { + val srcs = Seq(Seq("echo.py") -> """ |def echo(args): | return { "echo": args } """.stripMargin) - val code = ZipBuilder.mkBase64Zip(srcs) - - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "echo")) - initCode should be(502) - } + val code = ZipBuilder.mkBase64Zip(srcs) - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e should include("Zip file does not include") - }) + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "echo")) + initCode should be(502) } - it should "run zipped Python action containing a virtual environment" in { - val zippedPythonAction = if (imageName == "python2action") "python2_virtualenv.zip" else "python3_virtualenv.zip" - val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction) - val code = readAsBase64(Paths.get(zippedPythonActionName)) - - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "main")) - initCode should be(200) - val args = JsObject("msg" -> JsString("any")) - val (runCode, runRes) = c.run(runPayload(args)) - runCode should be(200) - runRes.get.toString() should include("netmask") - } - checkStreams(out, err, { - case (o, e) => - o should include("netmask") - e shouldBe empty - }) + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should include("Zip file does not include") + }) + } + + it should "run zipped Python action containing a virtual environment" in { + val zippedPythonAction = if (imageName == "python2action") "python2_virtualenv.zip" else "python3_virtualenv.zip" + val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction) + val code = readAsBase64(Paths.get(zippedPythonActionName)) + + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "main")) + initCode should be(200) + val args = JsObject("msg" -> JsString("any")) + val (runCode, runRes) = c.run(runPayload(args)) + runCode should be(200) + runRes.get.toString() should include("netmask") } - - it should "run zipped Python action containing a virtual environment with non-standard entry point" in { - val zippedPythonAction = if (imageName == "python2action") "python2_virtualenv.zip" else "python3_virtualenv.zip" - val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction) - val code = readAsBase64(Paths.get(zippedPythonActionName)) - - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "naim")) - initCode should be(200) - val args = JsObject("msg" -> JsString("any")) - val (runCode, runRes) = c.run(runPayload(args)) - runCode should be(200) - runRes.get.toString() should include("netmask") - } - checkStreams(out, err, { - case (o, e) => - o should include("netmask") - e shouldBe empty - }) + checkStreams(out, err, { + case (o, e) => + o should include("netmask") + e shouldBe empty + }) + } + + it should "run zipped Python action containing a virtual environment with non-standard entry point" in { + val zippedPythonAction = if (imageName == "python2action") "python2_virtualenv.zip" else "python3_virtualenv.zip" + val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction) + val code = readAsBase64(Paths.get(zippedPythonActionName)) + + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "naim")) + initCode should be(200) + val args = JsObject("msg" -> JsString("any")) + val (runCode, runRes) = c.run(runPayload(args)) + runCode should be(200) + runRes.get.toString() should include("netmask") } - - it should "report error if zipped Python action containing a virtual environment for wrong python version" in { - val zippedPythonAction = if (imageName == "python3action") "python2_virtualenv.zip" else "python3_virtualenv.zip" - val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction) - val code = readAsBase64(Paths.get(zippedPythonActionName)) - - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "main")) - initCode should be(200) - val args = JsObject("msg" -> JsString("any")) - val (runCode, runRes) = c.run(runPayload(args)) - runCode should be(502) - } - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - if (imageName == "python2action") { e should include("ImportError") } - if (imageName == "python3action") { e should include("ModuleNotFoundError") } - }) + checkStreams(out, err, { + case (o, e) => + o should include("netmask") + e shouldBe empty + }) + } + + it should "report error if zipped Python action containing a virtual environment for wrong python version" in { + val zippedPythonAction = if (imageName == "python3action") "python2_virtualenv.zip" else "python3_virtualenv.zip" + val zippedPythonActionName = TestUtils.getTestActionFilename(zippedPythonAction) + val code = readAsBase64(Paths.get(zippedPythonActionName)) + + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "main")) + initCode should be(200) + val args = JsObject("msg" -> JsString("any")) + val (runCode, runRes) = c.run(runPayload(args)) + runCode should be(502) } + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + if (imageName == "python2action") { e should include("ImportError") } + if (imageName == "python3action") { e should include("ModuleNotFoundError") } + }) + } - it should "report error if zipped Python action has wrong main module name" in { - val zippedPythonActionWrongName = TestUtils.getTestActionFilename("python_virtualenv_name.zip") + it should "report error if zipped Python action has wrong main module name" in { + val zippedPythonActionWrongName = TestUtils.getTestActionFilename("python_virtualenv_name.zip") - val code = readAsBase64(Paths.get(zippedPythonActionWrongName)) + val code = readAsBase64(Paths.get(zippedPythonActionWrongName)) - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "main")) - initCode should be(502) - } - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e should include("Zip file does not include __main__.py") - }) + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "main")) + initCode should be(502) } + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should include("Zip file does not include __main__.py") + }) + } - it should "report error if zipped Python action has invalid virtualenv directory" in { - val zippedPythonActionWrongDir = TestUtils.getTestActionFilename("python_virtualenv_dir.zip") - - val code = readAsBase64(Paths.get(zippedPythonActionWrongDir)) - val (out, err) = withActionContainer() { c => - val (initCode, initRes) = c.init(initPayload(code, main = "main")) - initCode should be(502) - } - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e should include("Zip file does not include /virtualenv/bin/") - }) + it should "report error if zipped Python action has invalid virtualenv directory" in { + val zippedPythonActionWrongDir = TestUtils.getTestActionFilename("python_virtualenv_dir.zip") + + val code = readAsBase64(Paths.get(zippedPythonActionWrongDir)) + val (out, err) = withActionContainer() { c => + val (initCode, initRes) = c.init(initPayload(code, main = "main")) + initCode should be(502) } + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should include("Zip file does not include /virtualenv/bin/") + }) + } - it should "return on action error when action fails" in { - val (out, err) = withActionContainer() { c => - val code = """ + it should "return on action error when action fails" in { + val (out, err) = withActionContainer() { c => + val code = """ |def div(x, y): | return x/y | @@ -291,91 +299,91 @@ class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSys | return {"divBy0": div(5,0)} """.stripMargin - val (initCode, _) = c.init(initPayload(code)) - initCode should be(200) + val (initCode, _) = c.init(initPayload(code)) + initCode should be(200) - val (runCode, runRes) = c.run(runPayload(JsObject())) - runCode should be(502) + 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) => - o shouldBe empty - e should include("Traceback") - }) + runRes shouldBe defined + runRes.get.fields.get("error") shouldBe defined } - it should "log compilation errors" in { - val (out, err) = withActionContainer() { c => - val code = """ + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should include("Traceback") + }) + } + + it should "log compilation errors" in { + val (out, err) = withActionContainer() { c => + val code = """ | 10 PRINT "Hello!" | 20 GOTO 10 """.stripMargin - val (initCode, res) = c.init(initPayload(code)) - // init checks whether compilation was successful, so return 502 - initCode should be(502) - } - - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e should include("Traceback") - }) + val (initCode, res) = c.init(initPayload(code)) + // init checks whether compilation was successful, so return 502 + initCode should be(502) } - it should "support application errors" in { - val (out, err) = withActionContainer() { c => - val code = """ + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should include("Traceback") + }) + } + + it should "support application errors" in { + val (out, err) = withActionContainer() { c => + val code = """ |def main(args): | return { "error": "sorry" } """.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 + val (initCode, _) = c.init(initPayload(code)) + initCode should be(200) - runRes shouldBe defined - runRes should be(Some(JsObject("error" -> JsString("sorry")))) - } + val (runCode, runRes) = c.run(runPayload(JsObject())) + runCode should be(200) // action writer returning an error is OK - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e shouldBe empty - }) + runRes shouldBe defined + runRes should be(Some(JsObject("error" -> JsString("sorry")))) } - it should "error when importing a not-supported package" in { - val (out, err) = withActionContainer() { c => - val code = """ + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "error when importing a not-supported package" in { + val (out, err) = withActionContainer() { c => + val code = """ |import iamnotsupported |def main(args): | return { "error": "not reaching here" } """.stripMargin - val (initCode, res) = c.init(initPayload(code)) - initCode should be(200) - - val (runCode, runRes) = c.run(runPayload(JsObject())) - runCode should be(502) - } + val (initCode, res) = c.init(initPayload(code)) + initCode should be(200) - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e should include("Traceback") - }) + val (runCode, runRes) = c.run(runPayload(JsObject())) + runCode should be(502) } - it should "be able to import additional packages as installed in the image" in { - val (out, err) = withActionContainer() { c => - val code = """ + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e should include("Traceback") + }) + } + + it should "be able to import additional packages as installed in the image" in { + val (out, err) = withActionContainer() { c => + val code = """ |from bs4 import BeautifulSoup |from dateutil.parser import * |import httplib2 @@ -407,27 +415,28 @@ class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSys | } """.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( - "bs4" -> "<title>python action test</title>".toJson, - "httplib2" -> 200.toJson, - "dateutil" -> "Monday".toJson, - "lxml" -> "root".toJson, - "json" -> JsObject("foo" -> "bar".toJson).compactPrint.toJson, - "request" -> 200.toJson, - "kafka_python" -> "it works".toJson))) - } - - checkStreams(out, err, { - case (o, e) => - o shouldBe empty - e shouldBe empty - }) + 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( + "bs4" -> "<title>python action test</title>".toJson, + "httplib2" -> 200.toJson, + "dateutil" -> "Monday".toJson, + "lxml" -> "root".toJson, + "json" -> JsObject("foo" -> "bar".toJson).compactPrint.toJson, + "request" -> 200.toJson, + "kafka_python" -> "it works".toJson))) } + + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } } diff --git a/tests/src/test/scala/actionContainers/ResourceHelpers.scala b/tests/src/test/scala/actionContainers/ResourceHelpers.scala index 39d963be93db8b1e05d2ee1ef167993ce67961e7..3041e0ea4aecd594be023107f88fd412987a16be 100644 --- a/tests/src/test/scala/actionContainers/ResourceHelpers.scala +++ b/tests/src/test/scala/actionContainers/ResourceHelpers.scala @@ -38,152 +38,153 @@ import collection.JavaConverters._ * 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 - } + /** 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)) - } + def mkBase64Jar(source: (Seq[String], String)): String = { + mkBase64Jar(Seq(source)) + } - private def compile(sources: Seq[(Seq[String], String)]): Path = { - require(!sources.isEmpty) + private def compile(sources: Seq[(Seq[String], String)]): Path = { + require(!sources.isEmpty) - // The absolute paths of the source file - val (srcDir, srcAbsPaths) = writeSourcesToTempDirectory(sources) + // 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() + // 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) + // 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) + // 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) + // 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() + // ...and off we go. + compTask.call() - binDir - } + binDir + } - private def buildClassPath(): String = { - val bcp = System.getProperty("java.class.path") + 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 + val list = this.getClass().getClassLoader() match { + case ucl: URLClassLoader => + bcp :: ucl.getURLs().map(_.getFile().toString()).toList - case _ => - List(bcp) - } + case _ => + List(bcp) + } - list.mkString(System.getProperty("path.separator")) - } + 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) + } + + /** + * 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 } - 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 + (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()) } - }) + } - fs.close() + // Finally we can copy that file. + Files.copy(path, arRelPath) - arPath - } + FileVisitResult.CONTINUE + } + }) - /** 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) - } + 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) + } }