test_docker.py 14.3 KB
Newer Older
André Anjos's avatar
André Anjos committed
1 2 3
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

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
###################################################################################
#                                                                                 #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
# Contact: beat.support@idiap.ch                                                  #
#                                                                                 #
# Redistribution and use in source and binary forms, with or without              #
# modification, are permitted provided that the following conditions are met:     #
#                                                                                 #
# 1. Redistributions of source code must retain the above copyright notice, this  #
# list of conditions and the following disclaimer.                                #
#                                                                                 #
# 2. Redistributions in binary form must reproduce the above copyright notice,    #
# this list of conditions and the following disclaimer in the documentation       #
# and/or other materials provided with the distribution.                          #
#                                                                                 #
# 3. Neither the name of the copyright holder nor the names of its contributors   #
# may be used to endorse or promote products derived from this software without   #
# specific prior written permission.                                              #
#                                                                                 #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
#                                                                                 #
###################################################################################


"""Asynchronous process I/O with the Subprocess module"""
André Anjos's avatar
André Anjos committed
38 39 40

import os
import time
Samuel GAIST's avatar
Samuel GAIST committed
41
import tempfile
42
import unittest
André Anjos's avatar
André Anjos committed
43 44
import pkg_resources

45 46
from tempfile import TemporaryDirectory

47
from ..dock import Host
48

49
from . import tmp_prefix
50
from .utils import slow
51 52
from .utils import skipif

53
from . import network_name
54
from . import DOCKER_NETWORK_TEST_ENABLED
55

56

57 58 59 60
class NoDiscoveryTests(unittest.TestCase):
    """Test cases that don't require the discovery of database and runtime
    environments.
    """
61

Philip ABBET's avatar
Philip ABBET committed
62 63
    @classmethod
    def setUpClass(cls):
64
        cls.host = Host(raise_on_errors=False, discover=False)
65

Philip ABBET's avatar
Philip ABBET committed
66 67 68
    @classmethod
    def tearDownClass(cls):
        cls.host.teardown()
69

Philip ABBET's avatar
Philip ABBET committed
70 71
    def tearDown(self):
        self.host.teardown()
Samuel GAIST's avatar
Samuel GAIST committed
72
        self.assertFalse(self.host.containers)  # All containers are gone
73 74


75 76
class NetworkTest(NoDiscoveryTests):
    @slow
77
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
78 79
    def test_network(self):
        string = "hello world"
80
        container = self.host.create_container("debian:8.4", ["echo", string])
81 82 83 84 85
        container.network_name = network_name

        try:
            self.host.start(container)
            status = self.host.wait(container)
Samuel GAIST's avatar
Samuel GAIST committed
86 87 88
        except Exception:
            from . import network

89 90 91 92
            network.remove()
            raise

        self.assertEqual(status, 0)
93
        self.assertEqual(self.host.logs(container), string + "\n")
94 95

    @slow
96
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
97 98 99
    def test_non_existing_network(self):

        string = "hello world"
100 101
        network_name = "beat.core.fake"
        container = self.host.create_container("debian:8.4", ["echo", string])
102 103 104 105 106
        container.network_name = network_name

        try:
            self.host.start(container)
        except RuntimeError as e:
107
            self.assertTrue(str(e).find("network %s not found" % network_name) >= 0)
108 109 110 111 112 113 114 115


class UserTest(NoDiscoveryTests):
    @slow
    def test_user(self):
        """Test that the uid property is correctly used.
        """

116
        container = self.host.create_container("debian:8.4", ["id"])
117 118 119 120 121 122
        container.uid = 10000

        self.host.start(container)
        status = self.host.wait(container)

        self.assertEqual(status, 0)
123 124 125 126 127
        self.assertTrue(
            self.host.logs(container).startswith(
                "uid={0} gid={0}".format(container.uid)
            )
        )
128 129


130 131 132 133 134 135
class EnvironmentVariableTest(NoDiscoveryTests):
    @slow
    def test_environment_variable(self):
        """Test that the uid property is correctly used.
        """

136 137
        container = self.host.create_container("debian:8.4", ["env"])
        container.add_environment_variable("DOCKER_TEST", "good")
138 139 140 141 142

        self.host.start(container)
        status = self.host.wait(container)

        self.assertEqual(status, 0)
143
        self.assertTrue("DOCKER_TEST=good" in self.host.logs(container))
144 145


146 147 148 149 150 151 152 153
class WorkdirTest(NoDiscoveryTests):
    @slow
    def test_workdir(self):
        """Test that the workdir property is correctly used.
        """

        with TemporaryDirectory() as tmp_folder:
            test_file = "test.txt"
154 155 156 157 158
            container = self.host.create_container(
                "debian:8.4", ["cp", "/etc/debian_version", test_file]
            )
            container.add_volume(tmp_folder, "/test_workdir", read_only=False)
            container.set_workdir("/test_workdir")
159 160 161 162 163 164 165 166 167 168 169 170 171

            self.host.start(container)
            status = self.host.wait(container)
            if status != 0:
                print(self.host.logs(container))

            self.assertEqual(status, 0)

            with open(os.path.join(tmp_folder, test_file), "rt") as file:
                content = file.read()
                self.assertEqual(content, "8.4\n")


172 173 174 175 176 177
class EntrypointTest(NoDiscoveryTests):
    @slow
    def test_entrypoint(self):
        """Test that the entrypoint property is correctly used.
        """

178 179
        container = self.host.create_container("debian:8.4", ["42"])
        container.set_entrypoint("echo")
180 181 182 183 184 185 186 187 188 189 190

        self.host.start(container)
        status = self.host.wait(container)
        logs = self.host.logs(container)
        if status != 0:
            print(logs)

        self.assertEqual(status, 0)
        self.assertEqual(logs, "42\n")


191
class AsyncTest(NoDiscoveryTests):
Philip ABBET's avatar
Philip ABBET committed
192 193
    @slow
    def test_echo(self):
194

Philip ABBET's avatar
Philip ABBET committed
195
        string = "hello, world"
196

197
        container = self.host.create_container("debian:8.4", ["echo", string])
Philip ABBET's avatar
Philip ABBET committed
198 199
        self.host.start(container)
        status = self.host.wait(container)
200

Philip ABBET's avatar
Philip ABBET committed
201
        self.assertEqual(status, 0)
202
        self.assertEqual(self.host.logs(container), string + "\n")
203

Philip ABBET's avatar
Philip ABBET committed
204 205
    @slow
    def test_non_existing(self):
206

207
        container = self.host.create_container("debian:8.4", ["sdfsdfdsf329909092"])
208

209 210
        try:
            self.host.start(container)
211
        except Exception as e:
212
            self.assertTrue(str(e).find("Failed to create the container") >= 0)
213

214
        self.assertFalse(self.host.containers)  # All containers are gone
215

Philip ABBET's avatar
Philip ABBET committed
216 217
    @slow
    def test_timeout(self):
218

219
        sleep_for = 100  # seconds
220

221
        container = self.host.create_container("debian:8.4", ["sleep", str(sleep_for)])
Philip ABBET's avatar
Philip ABBET committed
222
        self.host.start(container)
223

224 225
        retval = self.host.wait(container, timeout=0.5)
        self.assertTrue(retval is None)
226

Philip ABBET's avatar
Philip ABBET committed
227
        self.host.kill(container)
228

229
        retval = self.host.wait(container)
230

231
        self.assertEqual(self.host.status(container), "exited")
232
        self.assertEqual(retval, 137)
233
        self.assertEqual(self.host.logs(container), "")
234

Philip ABBET's avatar
Philip ABBET committed
235 236
    @slow
    def test_does_not_timeout(self):
237

238
        sleep_for = 0.5  # seconds
239

240
        container = self.host.create_container("debian:8.4", ["sleep", str(sleep_for)])
Philip ABBET's avatar
Philip ABBET committed
241
        self.host.start(container)
242

243
        status = self.host.wait(container, timeout=5)  # Should not timeout
244

245
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
246
        self.assertEqual(status, 0)
247
        self.assertEqual(self.host.logs(container), "")
248

249 250 251 252 253

class AsyncWithEnvironmentTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.host = Host(raise_on_errors=False)
254
        cls.test_environment = cls.host.full_environment_name("Python for tests")
255 256 257 258 259 260 261

    @classmethod
    def tearDownClass(cls):
        cls.host.teardown()

    def tearDown(self):
        self.host.teardown()
Samuel GAIST's avatar
Samuel GAIST committed
262
        self.assertFalse(self.host.containers)  # All containers are gone
263

Philip ABBET's avatar
Philip ABBET committed
264 265
    @slow
    def test_memory_limit(self):
266

267 268 269 270 271 272 273 274 275 276 277 278
        cmd = [
            "python",
            "-c",
            "; ".join(
                [
                    "print('Before')",
                    "import sys; sys.stdout.flush()",
                    "d = '0' * (40 * 1024 * 1024)",
                    "import time; time.sleep(5)",
                    "print('After')",
                ]
            ),
Philip ABBET's avatar
Philip ABBET committed
279
        ]
280

Philip ABBET's avatar
Philip ABBET committed
281
        container = self.host.create_container(self.test_environment, cmd)
282 283 284 285 286 287
        # The amount of memory in megabytes should be greater than whatever
        # the docker process is started with (see:
        # https://unix.stackexchange.com/questions/412040/cgroups-memory-limit-write-error-device-or-resource-busy)
        # If you start seeing EBUSY (device or resource busy errors) from
        # docker, then try increasing a bit this value such that it still
        # triggers the memory allocation error for the array defined above.
288
        self.host.start(container, virtual_memory_in_megabytes=20)
289

Philip ABBET's avatar
Philip ABBET committed
290
        time.sleep(2)
291

Samuel GAIST's avatar
Samuel GAIST committed
292
        self.host.statistics(container)
293

Philip ABBET's avatar
Philip ABBET committed
294
        status = self.host.wait(container)
295

296
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
297
        self.assertEqual(status, 137)
298
        self.assertEqual(self.host.logs(container).strip(), "Before")
299

Philip ABBET's avatar
Philip ABBET committed
300 301
    @slow
    def test_memory_limit2(self):
302

303 304 305 306 307 308 309 310 311 312 313 314
        cmd = [
            "python",
            "-c",
            "; ".join(
                [
                    "print('Before')",
                    "import sys; sys.stdout.flush()",
                    "d = '0' * (10 * 1024 * 1024)",
                    "import time; time.sleep(5)",
                    "print('After')",
                ]
            ),
Philip ABBET's avatar
Philip ABBET committed
315
        ]
316

Philip ABBET's avatar
Philip ABBET committed
317 318
        container = self.host.create_container(self.test_environment, cmd)
        self.host.start(container, virtual_memory_in_megabytes=100)
319

Philip ABBET's avatar
Philip ABBET committed
320
        time.sleep(2)
321

Philip ABBET's avatar
Philip ABBET committed
322
        stats = self.host.statistics(container)
323

Philip ABBET's avatar
Philip ABBET committed
324
        status = self.host.wait(container)
325

Samuel GAIST's avatar
Samuel GAIST committed
326 327 328
        self.assertTrue(
            stats["memory"]["percent"] > 10,
            ("Memory check failed, " "%d%% <= 10%%" % stats["memory"]["percent"]),
329
        )
330

Samuel GAIST's avatar
Samuel GAIST committed
331 332 333
        self.assertTrue(
            stats["memory"]["percent"] < 20,
            ("Memory check failed, " "%d%% >= 15%%" % stats["memory"]["percent"]),
334
        )
335

336
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
337
        self.assertEqual(status, 0)
338
        self.assertEqual(self.host.logs(container).strip(), "Before\nAfter")
339

Philip ABBET's avatar
Philip ABBET committed
340
    def _run_cpulimit(self, processes, max_cpu_percent, sleep_time):
Samuel GAIST's avatar
Samuel GAIST committed
341
        tmp_folder = tempfile.gettempdir()
342
        program = pkg_resources.resource_filename(__name__, "cpu_stress.py")
Samuel GAIST's avatar
Samuel GAIST committed
343
        dst_name = os.path.join(tmp_folder, os.path.basename(program))
344

345 346 347
        container = self.host.create_container(
            self.test_environment, ["python", dst_name, str(processes)]
        )
348

Samuel GAIST's avatar
Samuel GAIST committed
349
        container.add_volume(program, os.path.join(tmp_folder, "cpu_stress.py"))
350

Philip ABBET's avatar
Philip ABBET committed
351
        self.host.start(container, max_cpu_percent=max_cpu_percent)
352

Philip ABBET's avatar
Philip ABBET committed
353
        time.sleep(sleep_time)
354

Philip ABBET's avatar
Philip ABBET committed
355
        stats = self.host.statistics(container)
356

357
        self.assertEqual(self.host.status(container), "running")
358

359
        percent = stats["cpu"]["percent"]
Samuel GAIST's avatar
Samuel GAIST committed
360 361 362 363 364 365
        self.assertTrue(
            percent < (1.1 * max_cpu_percent),
            (
                "%.2f%% is more than 20%% off the expected ceiling at %d%%!"
                % (percent, max_cpu_percent)
            ),
366
        )
367

Philip ABBET's avatar
Philip ABBET committed
368 369 370
        # make sure nothing is there anymore
        self.host.kill(container)
        self.assertEqual(self.host.wait(container), 137)
371

Philip ABBET's avatar
Philip ABBET committed
372 373 374 375
    @slow
    def test_cpulimit_at_20percent(self):
        # runs 1 process that should consume at most 20% of the host CPU
        self._run_cpulimit(1, 20, 3)
376

Philip ABBET's avatar
Philip ABBET committed
377 378 379 380
    @slow
    def test_cpulimit_at_100percent(self):
        # runs 4 processes that should consume 50% of the host CPU
        self._run_cpulimit(4, 100, 3)
381 382 383


class HostTest(unittest.TestCase):
Philip ABBET's avatar
Philip ABBET committed
384 385
    def setUp(self):
        Host.images_cache = {}
386

Philip ABBET's avatar
Philip ABBET committed
387 388 389
    @slow
    def test_images_cache(self):
        self.assertEqual(len(Host.images_cache), 0)
390

Philip ABBET's avatar
Philip ABBET committed
391 392
        # Might take some time
        start = time.time()
393

Philip ABBET's avatar
Philip ABBET committed
394 395
        host = Host(raise_on_errors=False)
        host.teardown()
396

Philip ABBET's avatar
Philip ABBET committed
397
        stop = time.time()
398

Philip ABBET's avatar
Philip ABBET committed
399 400
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
401

402
        self.assertTrue(stop - start < 2.0)
403

Philip ABBET's avatar
Philip ABBET committed
404 405
        # Should be instantaneous
        start = time.time()
406

Philip ABBET's avatar
Philip ABBET committed
407 408
        host = Host(raise_on_errors=False)
        host.teardown()
409

Philip ABBET's avatar
Philip ABBET committed
410
        stop = time.time()
411

Philip ABBET's avatar
Philip ABBET committed
412
        self.assertEqual(len(Host.images_cache), nb_images)
413

Philip ABBET's avatar
Philip ABBET committed
414
        self.assertTrue(stop - start < 1.0)
415

Philip ABBET's avatar
Philip ABBET committed
416 417 418
    @slow
    def test_images_cache_file(self):
        self.assertEqual(len(Host.images_cache), 0)
419

Philip ABBET's avatar
Philip ABBET committed
420 421
        # Might take some time
        start = time.time()
422

423 424 425 426
        host = Host(
            images_cache=os.path.join(tmp_prefix, "images_cache.json"),
            raise_on_errors=False,
        )
Philip ABBET's avatar
Philip ABBET committed
427
        host.teardown()
428

Philip ABBET's avatar
Philip ABBET committed
429
        stop = time.time()
430

Philip ABBET's avatar
Philip ABBET committed
431 432
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
433

434
        self.assertTrue(stop - start < 2.0)
435

Philip ABBET's avatar
Philip ABBET committed
436
        Host.images_cache = {}
437

Philip ABBET's avatar
Philip ABBET committed
438 439
        # Should be instantaneous
        start = time.time()
440

441 442 443 444
        host = Host(
            images_cache=os.path.join(tmp_prefix, "images_cache.json"),
            raise_on_errors=False,
        )
Philip ABBET's avatar
Philip ABBET committed
445
        host.teardown()
446

Philip ABBET's avatar
Philip ABBET committed
447
        stop = time.time()
448

Philip ABBET's avatar
Philip ABBET committed
449
        self.assertEqual(len(Host.images_cache), nb_images)
450

Philip ABBET's avatar
Philip ABBET committed
451
        self.assertTrue(stop - start < 1.0)