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)
}
}
}
|