summaryrefslogblamecommitdiff
path: root/crashboxd/src/test/scala/io/crashbox/ci/DockerExecutorSpec.scala
blob: 23cbceda0515f720bc95e8e0e41e45e1cee818ad (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12

                      
                                                                 








                                            






























































                                                                                    










































                                                                               






                                                                   




























































                                                                         
package io.crashbox.ci

import com.spotify.docker.client.DockerClient.ListContainersParam
import java.io.{ByteArrayOutputStream, File}
import java.nio.file.Files

import scala.collection.JavaConverters._
import scala.concurrent.Await
import scala.concurrent.duration._

import akka.actor.ActorSystem
import org.scalatest._
import scala.util.Random


trait DockerSuite extends Suite with BeforeAndAfterAll { self =>

  private val name = self.toString()

  private def withTmp[A](action: File => A): A = {
    val dir = Files.createTempDirectory("crashbox-docker-test-" + name).toFile
    try action(dir)
    finally dir.delete()
  }

  val baseImage = "debian:jessie-backports"
  val dockerImage = "crashbox"
  val dockerTimeout = 30.seconds
  val dockerLabel = "test-" + Random.nextInt()

  implicit val system = ActorSystem("crashbox-docker-test-" + name)
  import system.dispatcher
  val executor = new DockerExecutor {
    override def label = dockerLabel
  }

  def buildImage(): Unit = {
    println("Pulling base docker image for running docker tests")
    executor.dockerClient.pull(baseImage)

    withTmp { dir =>
      println("Adapting base image for tests")
      val modifications = s"""|FROM $baseImage
                              |RUN adduser crashbox
                              |USER crashbox
                              |""".stripMargin
      Files.write((new File(dir, "Dockerfile")).toPath, modifications.getBytes)
      executor.dockerClient.build(dir.toPath, dockerImage)
    }
  }

  def runningDockers: Seq[String] = {
    val stale = executor.dockerClient
      .listContainers(
        ListContainersParam.withLabel("crashbox", dockerLabel)
      ).asScala
    stale.map(_.id())
  }

  override def beforeAll: Unit = {
    buildImage()
  }

  override def afterAll: Unit = {
    val running = runningDockers
    running.foreach { id =>
      executor.dockerClient.stopContainer(id, 0)
      executor.dockerClient.removeContainer(id)
    }
    require(running.isEmpty, "Docker containers were left running after unit tests")
    require(runningDockers.isEmpty, "Could not delete left over docker containers.")
  }

}


class DockerExecutorSpec
    extends FlatSpec
    with Matchers
    with BeforeAndAfterAll {

  val image = "crashbox"

  val timeout = 30.seconds

  implicit val system = ActorSystem("docker-test")
  import system.dispatcher
  val exec = new DockerExecutor

  override def beforeAll: Unit = {
    println("Pulling base docker image for running docker tests")
    val base = "debian:jessie-backports"
    exec.dockerClient.pull(base)

    withTmp { dir =>
      println("Adapting base image for tests")
      val modifications = s"""|FROM $base
                              |RUN adduser crashbox
                              |USER crashbox
                              |""".stripMargin
      Files.write((new File(dir, "Dockerfile")).toPath, modifications.getBytes)
      exec.dockerClient.build(dir.toPath, image)
    }

  }

  override def afterAll: Unit = {
    system.terminate()
  }

  def withTmp[A](action: File => A): A = {
    val dir = Files.createTempDirectory("crashbox-docker-test").toFile
    try action(dir)
    finally dir.delete()
  }

  def run[A](script: String)(tests: (Int, File, String) => A): A = withTmp {
    dir =>
    val out = new ByteArrayOutputStream(1024)
    val awaitable = for (id <- exec.start(image, script, dir, out);
      status <- exec.result(id)) yield {
      status
    }
    val status = Await.result(awaitable, timeout)
    tests(status, dir, new String(out.toByteArray()).trim())
  }

  "DockerExecutor" should "return expected exit codes" in {
    run("true") {
      case (status, _, _) =>
        assert(status == 0)
    }
    run("false") {
      case (status, _, _) =>
        assert(status == 1)
    }
    run("nonexistant") {
      case (status, _, _) =>
        assert(status == 127)
    }
  }

  it should "print the expected output" in {
    run("echo hello world") {
      case (_, _, out) =>
        assert(out == "hello world")
    }
    run("echo hello world >&2") {
      case (_, _, out) =>
        assert(out == "hello world")
    }
    run("echo hello world > /dev/null") {
      case (_, _, out) =>
        assert(out == "")
    }
  }

  it should "create expected files" in {
    run("echo hello world > data") {
      case (_, dir, _) =>
        val data = Files
          .lines((new File(dir, "data")).toPath)
          .iterator()
          .asScala
          .mkString("\n")
        assert(data == "hello world")
    }
  }

  it should "allow cancellation" in {
    withTmp { dir =>
      val script = "while true; do sleep 1; echo sleeping; done"
      val out = new ByteArrayOutputStream(1024)

      val id = Await.result(exec.start(image, script, dir, out), timeout)
      val check = exec.result(id).map { res =>
        assert(res == 137)
      }
      exec.stop(id)
      //TODO check if resoruces were cleaned up properly

      Await.result(check, timeout)
    }
  }

}