summaryrefslogtreecommitdiff
path: root/crashboxd/src/test/scala/io/crashbox/ci/DockerExecutorSpec.scala
blob: 23cbceda0515f720bc95e8e0e41e45e1cee818ad (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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)
    }
  }

}