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 2.7")
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):
341

Samuel GAIST's avatar
Samuel GAIST committed
342
        tmp_folder = tempfile.gettempdir()
343
        program = pkg_resources.resource_filename(__name__, "cpu_stress.py")
Samuel GAIST's avatar
Samuel GAIST committed
344
        dst_name = os.path.join(tmp_folder, os.path.basename(program))
345

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

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

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

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

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

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

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

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

Philip ABBET's avatar
Philip ABBET committed
373 374 375 376
    @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)
377

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


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

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

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

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

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

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

Philip ABBET's avatar
Philip ABBET committed
403
        self.assertTrue(stop - start > 2.0)
404

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

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

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

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

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

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

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

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

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

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

Philip ABBET's avatar
Philip ABBET committed
435
        self.assertTrue(stop - start > 2.0)
436

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

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

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

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

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

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