diff --git a/Jenkinsfile b/Jenkinsfile index b5af1bada2..a61e6d5165 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,7 +7,7 @@ def images = [:] // Note: Swarm in dind seem notoriously flimsy with 1.12.1+, which is why we're // sticking with 1.12.0 for the 1.12 series -def dockerVersions = ["1.12.0", "1.13.1"] +def dockerVersions = ["1.12.0", "1.13.1", "17.04.0-ce-rc1"] def buildImage = { name, buildargs, pyTag -> img = docker.image(name) @@ -35,8 +35,8 @@ def buildImages = { -> } def getAPIVersion = { engineVersion -> - def versionMap = ['1.12': '1.24', '1.13': '1.26'] - return versionMap[engineVersion.substring(0, 4)] + def versionMap = ['1.12.': '1.24', '1.13.': '1.26', '17.04': '1.27'] + return versionMap[engineVersion.substring(0, 5)] } def runTests = { Map settings -> diff --git a/Makefile b/Makefile index 411795848e..e4c64e71e7 100644 --- a/Makefile +++ b/Makefile @@ -41,14 +41,17 @@ integration-test: build integration-test-py3: build-py3 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test tests/integration/${file} +TEST_API_VERSION ?= 1.27 +TEST_ENGINE_VERSION ?= 17.04.0-ce-rc1 + .PHONY: integration-dind integration-dind: build build-py3 docker rm -vf dpy-dind || : - docker run -d --name dpy-dind --privileged dockerswarm/dind:1.13.1 docker daemon\ + docker run -d --name dpy-dind --privileged dockerswarm/dind:${TEST_ENGINE_VERSION} docker daemon\ -H tcp://0.0.0.0:2375 --experimental - docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=1.26"\ + docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind:docker docker-sdk-python py.test tests/integration - docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=1.26"\ + docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind:docker docker-sdk-python3 py.test tests/integration docker rm -vf dpy-dind @@ -57,14 +60,14 @@ integration-dind-ssl: build-dind-certs build build-py3 docker run -d --name dpy-dind-certs dpy-dind-certs docker run -d --env="DOCKER_HOST=tcp://localhost:2375" --env="DOCKER_TLS_VERIFY=1"\ --env="DOCKER_CERT_PATH=/certs" --volumes-from dpy-dind-certs --name dpy-dind-ssl\ - -v /tmp --privileged dockerswarm/dind:1.13.1 docker daemon --tlsverify\ + -v /tmp --privileged dockerswarm/dind:${TEST_ENGINE_VERSION} docker daemon --tlsverify\ --tlscacert=/certs/ca.pem --tlscert=/certs/server-cert.pem\ --tlskey=/certs/server-key.pem -H tcp://0.0.0.0:2375 --experimental docker run --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\ - --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=1.26"\ + --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind-ssl:docker docker-sdk-python py.test tests/integration docker run --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\ - --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=1.26"\ + --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind-ssl:docker docker-sdk-python3 py.test tests/integration docker rm -vf dpy-dind-ssl dpy-dind-certs diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..1fc67cc024 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,12 @@ +version: '{branch}-{build}' + +install: + - "SET PATH=C:\\Python27-x64;C:\\Python27-x64\\Scripts;%PATH%" + - "python --version" + - "pip install tox==2.1.1 virtualenv==13.1.2" + +# Build the binary after tests +build: false + +test_script: + - "tox" diff --git a/docker/api/client.py b/docker/api/client.py index 99d7879cb8..749b061dce 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -75,7 +75,7 @@ class APIClient( base_url (str): URL to the Docker server. For example, ``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``. version (str): The version of the API to use. Set to ``auto`` to - automatically detect the server's version. Default: ``1.24`` + automatically detect the server's version. Default: ``1.26`` timeout (int): Default timeout for API calls, in seconds. tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass ``True`` to enable it with default options, or pass a diff --git a/docker/api/container.py b/docker/api/container.py index 4e7364b6db..85e5e90ae7 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -473,8 +473,8 @@ def create_host_config(self, *args, **kwargs): cap_add (list of str): Add kernel capabilities. For example, ``["SYS_ADMIN", "MKNOD"]``. cap_drop (list of str): Drop kernel capabilities. - cpu_group (int): The length of a CPU period in microseconds. - cpu_period (int): Microseconds of CPU time that the container can + cpu_period (int): The length of a CPU period in microseconds. + cpu_quota (int): Microseconds of CPU time that the container can get in a CPU period. cpu_shares (int): CPU shares (relative weight). cpuset_cpus (str): CPUs in which to allow execution (``0-3``, diff --git a/docker/api/daemon.py b/docker/api/daemon.py index 00367bc309..91c777f092 100644 --- a/docker/api/daemon.py +++ b/docker/api/daemon.py @@ -68,9 +68,10 @@ def events(self, since=None, until=None, filters=None, decode=None): 'until': until, 'filters': filters } + url = self._url('/events') return self._stream_helper( - self._get(self._url('/events'), params=params, stream=True), + self._get(url, params=params, stream=True, timeout=None), decode=decode ) diff --git a/docker/api/network.py b/docker/api/network.py index 9652228de1..74f4cd2b30 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -168,15 +168,23 @@ def remove_network(self, net_id): self._raise_for_status(res) @minimum_version('1.21') - def inspect_network(self, net_id): + def inspect_network(self, net_id, verbose=None): """ Get detailed information about a network. Args: net_id (str): ID of network + verbose (bool): Show the service details across the cluster in + swarm mode. """ + params = {} + if verbose is not None: + if version_lt(self._version, '1.28'): + raise InvalidVersion('verbose was introduced in API 1.28') + params['verbose'] = verbose + url = self._url("/networks/{0}", net_id) - res = self._get(url) + res = self._get(url, params=params) return self._result(res, json=True) @check_resource diff --git a/docker/client.py b/docker/client.py index 151e1944ef..09abd63322 100644 --- a/docker/client.py +++ b/docker/client.py @@ -24,7 +24,7 @@ class DockerClient(object): base_url (str): URL to the Docker server. For example, ``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``. version (str): The version of the API to use. Set to ``auto`` to - automatically detect the server's version. Default: ``1.24`` + automatically detect the server's version. Default: ``1.26`` timeout (int): Default timeout for API calls, in seconds. tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass ``True`` to enable it with default options, or pass a @@ -58,7 +58,7 @@ def from_env(cls, **kwargs): Args: version (str): The version of the API to use. Set to ``auto`` to - automatically detect the server's version. Default: ``1.24`` + automatically detect the server's version. Default: ``1.26`` timeout (int): Default timeout for API calls, in seconds. ssl_version (int): A valid `SSL version`_. assert_hostname (bool): Verify the hostname of the server. diff --git a/docker/errors.py b/docker/errors.py index d9b197d1a3..03b89c11e3 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -59,7 +59,7 @@ def __str__(self): @property def status_code(self): - if self.response: + if self.response is not None: return self.response.status_code def is_client_error(self): diff --git a/docker/models/containers.py b/docker/models/containers.py index 0d328e72a9..f6bd906546 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -456,10 +456,13 @@ def run(self, image, command=None, stdout=True, stderr=False, cap_add (list of str): Add kernel capabilities. For example, ``["SYS_ADMIN", "MKNOD"]``. cap_drop (list of str): Drop kernel capabilities. - cpu_group (int): The length of a CPU period in microseconds. - cpu_period (int): Microseconds of CPU time that the container can + cpu_count (int): CPU count (Windows only). + cpu_quota (int): Microseconds of CPU time that the container can get in a CPU period. + cpu_percent (int): CPU percent (Windows only). + cpu_period (int): The length of a CPU period in microseconds. cpu_shares (int): CPU shares (relative weight). + cpus (float): Number of CPUs. cpuset_cpus (str): CPUs in which to allow execution (``0-3``, ``0,1``). detach (bool): Run container in the background and return a @@ -510,14 +513,12 @@ def run(self, image, command=None, stdout=True, stderr=False, driver. mac_address (str): MAC address to assign to the container. - mem_limit (float or str): Memory limit. Accepts float values + mem_limit (int or str): Memory limit. Accepts float values (which represent the memory limit of the created container in bytes) or a string with a units identification char (``100000b``, ``1000k``, ``128m``, ``1g``). If a string is specified without a units character, bytes are assumed as an intended unit. - mem_limit (str or int): Maximum amount of memory container is - allowed to consume. (e.g. ``1G``). mem_swappiness (int): Tune a container's memory swappiness behavior. Accepts number between 0 and 100. memswap_limit (str or int): Maximum amount of memory + swap a @@ -585,7 +586,7 @@ def run(self, image, command=None, stdout=True, stderr=False, stdin_open (bool): Keep ``STDIN`` open even if not attached. stdout (bool): Return logs from ``STDOUT`` when ``detach=False``. Default: ``True``. - stdout (bool): Return logs from ``STDERR`` when ``detach=False``. + stderr (bool): Return logs from ``STDERR`` when ``detach=False``. Default: ``False``. stop_signal (str): The stop signal to use to stop the container (e.g. ``SIGINT``). @@ -803,9 +804,12 @@ def prune(self, filters=None): 'cap_add', 'cap_drop', 'cgroup_parent', + 'cpu_count', + 'cpu_percent', 'cpu_period', 'cpu_quota', 'cpu_shares', + 'cpus', 'cpuset_cpus', 'device_read_bps', 'device_read_iops', diff --git a/docker/types/containers.py b/docker/types/containers.py index 5a5079a81a..d6fff4a0e5 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -118,7 +118,8 @@ def __init__(self, version, binds=None, port_bindings=None, tmpfs=None, oom_score_adj=None, dns_opt=None, cpu_shares=None, cpuset_cpus=None, userns_mode=None, pids_limit=None, isolation=None, auto_remove=False, storage_opt=None, - init=None, init_path=None): + init=None, init_path=None, volume_driver=None, + cpu_count=None, cpu_percent=None, cpus=None): if mem_limit is not None: self['Memory'] = parse_bytes(mem_limit) @@ -428,6 +429,35 @@ def __init__(self, version, binds=None, port_bindings=None, raise host_config_version_error('init_path', '1.25') self['InitPath'] = init_path + if volume_driver is not None: + if version_lt(version, '1.21'): + raise host_config_version_error('volume_driver', '1.21') + self['VolumeDriver'] = volume_driver + if cpu_count: + if not isinstance(cpu_count, int): + raise host_config_type_error('cpu_count', cpu_count, 'int') + if version_lt(version, '1.25'): + raise host_config_version_error('cpu_count', '1.25') + + self['CpuCount'] = cpu_count + + if cpu_percent: + if not isinstance(cpu_percent, int): + raise host_config_type_error('cpu_percent', cpu_percent, 'int') + if version_lt(version, '1.25'): + raise host_config_version_error('cpu_percent', '1.25') + + self['CpuPercent'] = cpu_percent + + if cpus: + if not isinstance(cpus, (float, int)): + raise host_config_type_error('cpus', cpus, 'float') + if version_lt(version, '1.25'): + raise host_config_version_error('cpus', '1.25') + + self['NanoCpus'] = int(1000000000 * cpus) + + def host_config_type_error(param, param_value, expected): error_msg = 'Invalid type for {0} param: expected {1} but found {2}' @@ -456,43 +486,27 @@ def __init__( stop_signal=None, networking_config=None, healthcheck=None, stop_timeout=None ): - if isinstance(command, six.string_types): - command = split_command(command) - - if isinstance(entrypoint, six.string_types): - entrypoint = split_command(entrypoint) - - if isinstance(environment, dict): - environment = format_environment(environment) - - if labels is not None and version_lt(version, '1.18'): - raise errors.InvalidVersion( - 'labels were only introduced in API version 1.18' - ) + if version_gte(version, '1.10'): + message = ('{0!r} parameter has no effect on create_container().' + ' It has been moved to host_config') + if dns is not None: + raise errors.InvalidVersion(message.format('dns')) + if volumes_from is not None: + raise errors.InvalidVersion(message.format('volumes_from')) - if cpuset is not None or cpu_shares is not None: - if version_gte(version, '1.18'): + if version_lt(version, '1.18'): + if labels is not None: + raise errors.InvalidVersion( + 'labels were only introduced in API version 1.18' + ) + else: + if cpuset is not None or cpu_shares is not None: warnings.warn( 'The cpuset_cpus and cpu_shares options have been moved to' ' host_config in API version 1.18, and will be removed', DeprecationWarning ) - if stop_signal is not None and version_lt(version, '1.21'): - raise errors.InvalidVersion( - 'stop_signal was only introduced in API version 1.21' - ) - - if stop_timeout is not None and version_lt(version, '1.25'): - raise errors.InvalidVersion( - 'stop_timeout was only introduced in API version 1.25' - ) - - if healthcheck is not None and version_lt(version, '1.24'): - raise errors.InvalidVersion( - 'Health options were only introduced in API version 1.24' - ) - if version_lt(version, '1.19'): if volume_driver is not None: raise errors.InvalidVersion( @@ -513,6 +527,38 @@ def __init__( 'version 1.19' ) + if version_lt(version, '1.21'): + if stop_signal is not None: + raise errors.InvalidVersion( + 'stop_signal was only introduced in API version 1.21' + ) + else: + if volume_driver is not None: + warnings.warn( + 'The volume_driver option has been moved to' + ' host_config in API version 1.21, and will be removed', + DeprecationWarning + ) + + if stop_timeout is not None and version_lt(version, '1.25'): + raise errors.InvalidVersion( + 'stop_timeout was only introduced in API version 1.25' + ) + + if healthcheck is not None and version_lt(version, '1.24'): + raise errors.InvalidVersion( + 'Health options were only introduced in API version 1.24' + ) + + if isinstance(command, six.string_types): + command = split_command(command) + + if isinstance(entrypoint, six.string_types): + entrypoint = split_command(entrypoint) + + if isinstance(environment, dict): + environment = format_environment(environment) + if isinstance(labels, list): labels = dict((lbl, six.text_type('')) for lbl in labels) @@ -566,14 +612,6 @@ def __init__( attach_stdin = True stdin_once = True - if version_gte(version, '1.10'): - message = ('{0!r} parameter has no effect on create_container().' - ' It has been moved to host_config') - if dns is not None: - raise errors.InvalidVersion(message.format('dns')) - if volumes_from is not None: - raise errors.InvalidVersion(message.format('volumes_from')) - self.update({ 'Hostname': hostname, 'Domainname': domainname, diff --git a/docker/utils/build.py b/docker/utils/build.py index 6ba47b39fb..79b72495d9 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -1,5 +1,6 @@ import os +from ..constants import IS_WINDOWS_PLATFORM from .fnmatch import fnmatch from .utils import create_archive @@ -39,7 +40,7 @@ def exclude_paths(root, patterns, dockerfile=None): # If the Dockerfile is in a subdirectory that is excluded, get_paths # will not descend into it and the file will be skipped. This ensures # it doesn't happen. - set([dockerfile]) + set([dockerfile.replace('/', os.path.sep)]) if os.path.exists(os.path.join(root, dockerfile)) else set() ) @@ -130,9 +131,12 @@ def match_path(path, pattern): if pattern: pattern = os.path.relpath(pattern) + pattern_components = pattern.split(os.path.sep) + if len(pattern_components) == 1 and IS_WINDOWS_PLATFORM: + pattern_components = pattern.split('/') + if '**' not in pattern: - pattern_components = pattern.split(os.path.sep) path_components = path.split(os.path.sep)[:len(pattern_components)] else: path_components = path.split(os.path.sep) - return fnmatch('/'.join(path_components), pattern) + return fnmatch('/'.join(path_components), '/'.join(pattern_components)) diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py index 80bdf77329..e95b63ceb1 100644 --- a/docker/utils/fnmatch.py +++ b/docker/utils/fnmatch.py @@ -39,15 +39,13 @@ def fnmatch(name, pat): If you don't want this, use fnmatchcase(FILENAME, PATTERN). """ - import os - name = os.path.normcase(name) - pat = os.path.normcase(pat) + name = name.lower() + pat = pat.lower() return fnmatchcase(name, pat) def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. - This is a version of fnmatch() which doesn't case-normalize its arguments. """ @@ -67,7 +65,6 @@ def translate(pat): There is no way to quote meta-characters. """ - recursive_mode = False i, n = 0, len(pat) res = '' @@ -100,7 +97,7 @@ def translate(pat): stuff = '\\' + stuff res = '%s[%s]' % (res, stuff) elif recursive_mode and c == '/': - res = res + '/?' + res = res + re.escape(c) + '?' else: res = res + re.escape(c) return res + '\Z(?ms)' diff --git a/docker/version.py b/docker/version.py index b2474bd739..320dae622d 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.2.0" +version = "2.3.0-dev" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) diff --git a/docs/change-log.md b/docs/change-log.md index c04cd4be8f..5d9b05b37d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,19 @@ Change log ========== +2.2.1 +----- + +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/32?closed=1) + +### Bugfixes + +* Fixed a bug where the `status_code` attribute of `APIError` exceptions would + not reflect the expected value. +* Fixed an issue where the `events` method would time out unexpectedly if no + data was sent by the engine for a given amount of time. + + 2.2.0 ----- diff --git a/docs/containers.rst b/docs/containers.rst index 9b27a306b8..20529b0eff 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -40,6 +40,7 @@ Container objects .. automethod:: logs .. automethod:: pause .. automethod:: put_archive + .. automethod:: reload .. automethod:: remove .. automethod:: rename .. automethod:: resize diff --git a/docs/secrets.rst b/docs/secrets.rst index 49e149847d..d1c39f1a16 100644 --- a/docs/secrets.rst +++ b/docs/secrets.rst @@ -26,4 +26,5 @@ Secret objects The raw representation of this object from the server. + .. automethod:: reload .. automethod:: remove diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 3bfabe91dd..6858ad0e54 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -363,7 +363,7 @@ def test_create_service_with_secret(self): self.tmp_secrets.append(secret_id) secret_ref = docker.types.SecretReference(secret_id, secret_name) container_spec = docker.types.ContainerSpec( - 'busybox', ['top'], secrets=[secret_ref] + 'busybox', ['sleep', '999'], secrets=[secret_ref] ) task_tmpl = docker.types.TaskTemplate(container_spec) name = self.get_service_name() @@ -388,7 +388,7 @@ def test_create_service_with_unicode_secret(self): self.tmp_secrets.append(secret_id) secret_ref = docker.types.SecretReference(secret_id, secret_name) container_spec = docker.types.ContainerSpec( - 'busybox', ['top'], secrets=[secret_ref] + 'busybox', ['sleep', '999'], secrets=[secret_ref] ) task_tmpl = docker.types.TaskTemplate(container_spec) name = self.get_service_name() diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index 51d6678151..15f19f2e0b 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -407,11 +407,8 @@ def test_create_container_with_volumes_from(self): {'Content-Type': 'application/json'}) def test_create_container_empty_volumes_from(self): - self.client.create_container('busybox', 'true', volumes_from=[]) - - args = fake_request.call_args - data = json.loads(args[1]['data']) - self.assertTrue('VolumesFrom' not in data) + with pytest.raises(docker.errors.InvalidVersion): + self.client.create_container('busybox', 'true', volumes_from=[]) def test_create_named_container(self): self.client.create_container('busybox', 'true', @@ -978,11 +975,11 @@ def test_create_container_with_named_volume(self): self.client.create_container( 'busybox', 'true', host_config=self.client.create_host_config( + volume_driver='foodriver', binds={volume_name: { "bind": mount_dest, "ro": False }}), - volume_driver='foodriver', ) args = fake_request.call_args @@ -990,8 +987,8 @@ def test_create_container_with_named_volume(self): args[0][1], url_prefix + 'containers/create' ) expected_payload = self.base_create_payload() - expected_payload['VolumeDriver'] = 'foodriver' expected_payload['HostConfig'] = self.client.create_host_config() + expected_payload['HostConfig']['VolumeDriver'] = 'foodriver' expected_payload['HostConfig']['Binds'] = ["name:/mnt:rw"] self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], @@ -1155,6 +1152,37 @@ def test_create_container_with_unicode_envvars(self): self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data'])['Env'], expected) + @requires_api_version('1.25') + def test_create_container_with_host_config_cpus(self): + self.client.create_container( + 'busybox', 'ls', host_config=self.client.create_host_config( + cpu_count=1, + cpu_percent=20, + cpus=10 + ) + ) + + args = fake_request.call_args + self.assertEqual(args[0][1], + url_prefix + 'containers/create') + + self.assertEqual(json.loads(args[1]['data']), + json.loads(''' + {"Tty": false, "Image": "busybox", + "Cmd": ["ls"], "AttachStdin": false, + "AttachStderr": true, + "AttachStdout": true, "OpenStdin": false, + "StdinOnce": false, + "NetworkDisabled": false, + "HostConfig": { + "CpuCount": 1, + "CpuPercent": 20, + "NanoCpus": 10000000000, + "NetworkMode": "default" + }}''')) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + class ContainerTest(BaseAPIClientTest): def test_list_containers(self): diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index b632d209be..83848c524a 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -229,7 +229,7 @@ def test_events(self): url_prefix + 'events', params={'since': None, 'until': None, 'filters': None}, stream=True, - timeout=DEFAULT_TIMEOUT_SECONDS + timeout=None ) def test_events_with_since_until(self): @@ -249,7 +249,7 @@ def test_events_with_since_until(self): 'filters': None }, stream=True, - timeout=DEFAULT_TIMEOUT_SECONDS + timeout=None ) def test_events_with_filters(self): @@ -268,7 +268,7 @@ def test_events_with_filters(self): 'filters': expected_filters }, stream=True, - timeout=DEFAULT_TIMEOUT_SECONDS + timeout=None ) def _socket_path_for_client_session(self, client): diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py index 5c470ffa2f..4ec8dff397 100644 --- a/tests/unit/dockertypes_test.py +++ b/tests/unit/dockertypes_test.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- import unittest +import warnings import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION from docker.errors import InvalidArgument, InvalidVersion from docker.types import ( - EndpointConfig, HostConfig, IPAMConfig, IPAMPool, LogConfig, Mount, - ServiceMode, Ulimit, + ContainerConfig, EndpointConfig, HostConfig, IPAMConfig, IPAMPool, + LogConfig, Mount, ServiceMode, Ulimit, ) try: @@ -165,6 +166,59 @@ def test_create_host_config_invalid_mem_swappiness(self): with pytest.raises(TypeError): create_host_config(version='1.24', mem_swappiness='40') + def test_create_host_config_with_volume_driver(self): + with pytest.raises(InvalidVersion): + create_host_config(version='1.20', volume_driver='local') + + config = create_host_config(version='1.21', volume_driver='local') + assert config.get('VolumeDriver') == 'local' + + def test_create_host_config_invalid_cpu_count_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpu_count='1') + + def test_create_host_config_with_cpu_count(self): + config = create_host_config(version='1.25', cpu_count=2) + self.assertEqual(config.get('CpuCount'), 2) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpu_count=1)) + + def test_create_host_config_invalid_cpu_percent_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpu_percent='1') + + def test_create_host_config_with_cpu_percent(self): + config = create_host_config(version='1.25', cpu_percent=15) + self.assertEqual(config.get('CpuPercent'), 15) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpu_percent=10)) + + def test_create_host_config_invalid_cpus_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpus='0') + + def test_create_host_config_with_cpus(self): + config = create_host_config(version='1.25', cpus=100) + self.assertEqual(config.get('NanoCpus'), 100000000000) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpus=1)) + + +class ContainerConfigTest(unittest.TestCase): + def test_create_container_config_volume_driver_warning(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + ContainerConfig( + version='1.21', image='scratch', command=None, + volume_driver='local' + ) + + assert len(w) == 1 + assert 'The volume_driver option has been moved' in str(w[0].message) + class UlimitTest(unittest.TestCase): def test_create_host_config_dict_ulimit(self): diff --git a/tests/unit/errors_test.py b/tests/unit/errors_test.py index 876ede3b5f..b78af4e109 100644 --- a/tests/unit/errors_test.py +++ b/tests/unit/errors_test.py @@ -1,5 +1,7 @@ import unittest +import requests + from docker.errors import (APIError, DockerException, create_unexpected_kwargs_error) @@ -11,6 +13,69 @@ def test_api_error_is_caught_by_dockerexception(self): except DockerException: pass + def test_status_code_200(self): + """The status_code property is present with 200 response.""" + resp = requests.Response() + resp.status_code = 200 + err = APIError('', response=resp) + assert err.status_code == 200 + + def test_status_code_400(self): + """The status_code property is present with 400 response.""" + resp = requests.Response() + resp.status_code = 400 + err = APIError('', response=resp) + assert err.status_code == 400 + + def test_status_code_500(self): + """The status_code property is present with 500 response.""" + resp = requests.Response() + resp.status_code = 500 + err = APIError('', response=resp) + assert err.status_code == 500 + + def test_is_server_error_200(self): + """Report not server error on 200 response.""" + resp = requests.Response() + resp.status_code = 200 + err = APIError('', response=resp) + assert err.is_server_error() is False + + def test_is_server_error_300(self): + """Report not server error on 300 response.""" + resp = requests.Response() + resp.status_code = 300 + err = APIError('', response=resp) + assert err.is_server_error() is False + + def test_is_server_error_400(self): + """Report not server error on 400 response.""" + resp = requests.Response() + resp.status_code = 400 + err = APIError('', response=resp) + assert err.is_server_error() is False + + def test_is_server_error_500(self): + """Report server error on 500 response.""" + resp = requests.Response() + resp.status_code = 500 + err = APIError('', response=resp) + assert err.is_server_error() is True + + def test_is_client_error_500(self): + """Report not client error on 500 response.""" + resp = requests.Response() + resp.status_code = 500 + err = APIError('', response=resp) + assert err.is_client_error() is False + + def test_is_client_error_400(self): + """Report client error on 400 response.""" + resp = requests.Response() + resp.status_code = 400 + err = APIError('', response=resp) + assert err.is_client_error() is True + class CreateUnexpectedKwargsErrorTest(unittest.TestCase): def test_create_unexpected_kwargs_error_single(self): diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 4c3c3664a2..25ed0f9b7f 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -618,9 +618,11 @@ def test_build_port_bindings_with_nonmatching_internal_port_ranges(self): def convert_paths(collection): - if not IS_WINDOWS_PLATFORM: - return collection - return set(map(lambda x: x.replace('/', '\\'), collection)) + return set(map(convert_path, collection)) + + +def convert_path(path): + return path.replace('/', os.path.sep) class ExcludePathsTest(unittest.TestCase): @@ -685,12 +687,12 @@ def test_exclude_custom_dockerfile(self): set(['Dockerfile.alt', '.dockerignore']) assert self.exclude(['*'], dockerfile='foo/Dockerfile3') == \ - set(['foo/Dockerfile3', '.dockerignore']) + convert_paths(set(['foo/Dockerfile3', '.dockerignore'])) def test_exclude_dockerfile_child(self): includes = self.exclude(['foo/'], dockerfile='foo/Dockerfile3') - assert 'foo/Dockerfile3' in includes - assert 'foo/a.py' not in includes + assert convert_path('foo/Dockerfile3') in includes + assert convert_path('foo/a.py') not in includes def test_single_filename(self): assert self.exclude(['a.py']) == convert_paths( @@ -917,6 +919,7 @@ def test_tar_with_directory_symlinks(self): sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo'] ) + @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No UNIX sockets on Win32') def test_tar_socket_file(self): base = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base) @@ -945,62 +948,53 @@ class ShouldCheckDirectoryTest(unittest.TestCase): ] def test_should_check_directory_not_excluded(self): - self.assertTrue( - should_check_directory('not_excluded', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + 'not_excluded', self.exclude_patterns, self.include_patterns ) - - self.assertTrue( - should_check_directory('dir/with', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + convert_path('dir/with'), self.exclude_patterns, + self.include_patterns ) def test_shoud_check_parent_directories_of_excluded(self): - self.assertTrue( - should_check_directory('dir', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + 'dir', self.exclude_patterns, self.include_patterns ) - self.assertTrue( - should_check_directory('dir/with', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + convert_path('dir/with'), self.exclude_patterns, + self.include_patterns ) def test_should_not_check_excluded_directories_with_no_exceptions(self): - self.assertFalse( - should_check_directory('exclude_rather_large_directory', - self.exclude_patterns, self.include_patterns - ) + assert not should_check_directory( + 'exclude_rather_large_directory', self.exclude_patterns, + self.include_patterns ) - self.assertFalse( - should_check_directory('dir/with/subdir_excluded', - self.exclude_patterns, self.include_patterns - ) + assert not should_check_directory( + convert_path('dir/with/subdir_excluded'), self.exclude_patterns, + self.include_patterns ) def test_should_check_excluded_directory_with_exceptions(self): - self.assertTrue( - should_check_directory('dir/with/exceptions', - self.exclude_patterns, self.include_patterns - ) + assert should_check_directory( + convert_path('dir/with/exceptions'), self.exclude_patterns, + self.include_patterns ) - self.assertTrue( - should_check_directory('dir/with/exceptions/in', - self.exclude_patterns, self.include_patterns - ) + assert should_check_directory( + convert_path('dir/with/exceptions/in'), self.exclude_patterns, + self.include_patterns ) def test_should_not_check_siblings_of_exceptions(self): - self.assertFalse( - should_check_directory('dir/with/exceptions/but_not_here', - self.exclude_patterns, self.include_patterns - ) + assert not should_check_directory( + convert_path('dir/with/exceptions/but_not_here'), + self.exclude_patterns, self.include_patterns ) def test_should_check_subdirectories_of_exceptions(self): - self.assertTrue( - should_check_directory('dir/with/exceptions/like_this_one/subdir', - self.exclude_patterns, self.include_patterns - ) + assert should_check_directory( + convert_path('dir/with/exceptions/like_this_one/subdir'), + self.exclude_patterns, self.include_patterns ) diff --git a/tox.ini b/tox.ini index 1a41c6edac..5a5e5415ad 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skipsdist=True [testenv] usedevelop=True commands = - py.test --cov=docker {posargs:tests/unit} + py.test -v --cov=docker {posargs:tests/unit} deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt