Merge pull request #2074 from docker/3.4.1-release
3.4.1 release
diff --git a/docker/api/container.py b/docker/api/container.py
index 05676f1..d4f75f5 100644
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -139,8 +139,9 @@
'changes': changes
}
u = self._url("/commit")
- return self._result(self._post_json(u, data=conf, params=params),
- json=True)
+ return self._result(
+ self._post_json(u, data=conf, params=params), json=True
+ )
def containers(self, quiet=False, all=False, trunc=False, latest=False,
since=None, before=None, limit=-1, size=False,
diff --git a/docker/auth.py b/docker/auth.py
index 0c0cb20..9635f93 100644
--- a/docker/auth.py
+++ b/docker/auth.py
@@ -270,7 +270,7 @@
"Couldn't find auth-related section ; attempting to interpret"
"as auth-only file"
)
- return parse_auth(config_dict)
+ return {'auths': parse_auth(config_dict)}
def _load_legacy_config(config_file):
@@ -287,14 +287,14 @@
)
username, password = decode_auth(data[0])
- return {
+ return {'auths': {
INDEX_NAME: {
'username': username,
'password': password,
'email': data[1],
'serveraddress': INDEX_URL,
}
- }
+ }}
except Exception as e:
log.debug(e)
pass
diff --git a/docker/utils/build.py b/docker/utils/build.py
index b644c9f..4fa5751 100644
--- a/docker/utils/build.py
+++ b/docker/utils/build.py
@@ -1,13 +1,13 @@
import io
import os
import re
-import six
import tarfile
import tempfile
+import six
+
+from .fnmatch import fnmatch
from ..constants import IS_WINDOWS_PLATFORM
-from fnmatch import fnmatch
-from itertools import chain
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
@@ -44,92 +44,9 @@
if dockerfile is None:
dockerfile = 'Dockerfile'
- def split_path(p):
- return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
-
- def normalize(p):
- # Leading and trailing slashes are not relevant. Yes,
- # "foo.py/" must exclude the "foo.py" regular file. "."
- # components are not relevant either, even if the whole
- # pattern is only ".", as the Docker reference states: "For
- # historical reasons, the pattern . is ignored."
- # ".." component must be cleared with the potential previous
- # component, regardless of whether it exists: "A preprocessing
- # step [...] eliminates . and .. elements using Go's
- # filepath.".
- i = 0
- split = split_path(p)
- while i < len(split):
- if split[i] == '..':
- del split[i]
- if i > 0:
- del split[i - 1]
- i -= 1
- else:
- i += 1
- return split
-
- patterns = (
- (True, normalize(p[1:]))
- if p.startswith('!') else
- (False, normalize(p))
- for p in patterns)
- patterns = list(reversed(list(chain(
- # Exclude empty patterns such as "." or the empty string.
- filter(lambda p: p[1], patterns),
- # Always include the Dockerfile and .dockerignore
- [(True, split_path(dockerfile)), (True, ['.dockerignore'])]))))
- return set(walk(root, patterns))
-
-
-def walk(root, patterns, default=True):
- """
- A collection of file lying below root that should be included according to
- patterns.
- """
-
- def match(p):
- if p[1][0] == '**':
- rec = (p[0], p[1][1:])
- return [p] + (match(rec) if rec[1] else [rec])
- elif fnmatch(f, p[1][0]):
- return [(p[0], p[1][1:])]
- else:
- return []
-
- for f in os.listdir(root):
- cur = os.path.join(root, f)
- # The patterns if recursing in that directory.
- sub = list(chain(*(match(p) for p in patterns)))
- # Whether this file is explicitely included / excluded.
- hit = next((p[0] for p in sub if not p[1]), None)
- # Whether this file is implicitely included / excluded.
- matched = default if hit is None else hit
- sub = list(filter(lambda p: p[1], sub))
- if os.path.isdir(cur) and not os.path.islink(cur):
- # Entirely skip directories if there are no chance any subfile will
- # be included.
- if all(not p[0] for p in sub) and not matched:
- continue
- # I think this would greatly speed up dockerignore handling by not
- # recursing into directories we are sure would be entirely
- # included, and only yielding the directory itself, which will be
- # recursively archived anyway. However the current unit test expect
- # the full list of subfiles and I'm not 100% sure it would make no
- # difference yet.
- # if all(p[0] for p in sub) and matched:
- # yield f
- # continue
- children = False
- for r in (os.path.join(f, p) for p in walk(cur, sub, matched)):
- yield r
- children = True
- # The current unit tests expect directories only under those
- # conditions. It might be simplifiable though.
- if (not sub or not children) and hit or hit is None and default:
- yield f
- elif matched:
- yield f
+ patterns.append('!' + dockerfile)
+ pm = PatternMatcher(patterns)
+ return set(pm.walk(root))
def build_file_list(root):
@@ -217,3 +134,122 @@
t.close()
f.seek(0)
return f
+
+
+def split_path(p):
+ return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
+
+
+def normalize_slashes(p):
+ if IS_WINDOWS_PLATFORM:
+ return '/'.join(split_path(p))
+ return p
+
+
+def walk(root, patterns, default=True):
+ pm = PatternMatcher(patterns)
+ return pm.walk(root)
+
+
+# Heavily based on
+# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
+class PatternMatcher(object):
+ def __init__(self, patterns):
+ self.patterns = list(filter(
+ lambda p: p.dirs, [Pattern(p) for p in patterns]
+ ))
+ self.patterns.append(Pattern('!.dockerignore'))
+
+ def matches(self, filepath):
+ matched = False
+ parent_path = os.path.dirname(filepath)
+ parent_path_dirs = split_path(parent_path)
+
+ for pattern in self.patterns:
+ negative = pattern.exclusion
+ match = pattern.match(filepath)
+ if not match and parent_path != '':
+ if len(pattern.dirs) <= len(parent_path_dirs):
+ match = pattern.match(
+ os.path.sep.join(parent_path_dirs[:len(pattern.dirs)])
+ )
+
+ if match:
+ matched = not negative
+
+ return matched
+
+ def walk(self, root):
+ def rec_walk(current_dir):
+ for f in os.listdir(current_dir):
+ fpath = os.path.join(
+ os.path.relpath(current_dir, root), f
+ )
+ if fpath.startswith('.' + os.path.sep):
+ fpath = fpath[2:]
+ match = self.matches(fpath)
+ if not match:
+ yield fpath
+
+ cur = os.path.join(root, fpath)
+ if not os.path.isdir(cur) or os.path.islink(cur):
+ continue
+
+ if match:
+ # If we want to skip this file and it's a directory
+ # then we should first check to see if there's an
+ # excludes pattern (e.g. !dir/file) that starts with this
+ # dir. If so then we can't skip this dir.
+ skip = True
+
+ for pat in self.patterns:
+ if not pat.exclusion:
+ continue
+ if pat.cleaned_pattern.startswith(
+ normalize_slashes(fpath)):
+ skip = False
+ break
+ if skip:
+ continue
+ for sub in rec_walk(cur):
+ yield sub
+
+ return rec_walk(root)
+
+
+class Pattern(object):
+ def __init__(self, pattern_str):
+ self.exclusion = False
+ if pattern_str.startswith('!'):
+ self.exclusion = True
+ pattern_str = pattern_str[1:]
+
+ self.dirs = self.normalize(pattern_str)
+ self.cleaned_pattern = '/'.join(self.dirs)
+
+ @classmethod
+ def normalize(cls, p):
+
+ # Leading and trailing slashes are not relevant. Yes,
+ # "foo.py/" must exclude the "foo.py" regular file. "."
+ # components are not relevant either, even if the whole
+ # pattern is only ".", as the Docker reference states: "For
+ # historical reasons, the pattern . is ignored."
+ # ".." component must be cleared with the potential previous
+ # component, regardless of whether it exists: "A preprocessing
+ # step [...] eliminates . and .. elements using Go's
+ # filepath.".
+ i = 0
+ split = split_path(p)
+ while i < len(split):
+ if split[i] == '..':
+ del split[i]
+ if i > 0:
+ del split[i - 1]
+ i -= 1
+ else:
+ i += 1
+ return split
+
+ def match(self, filepath):
+ return fnmatch(normalize_slashes(filepath), self.cleaned_pattern)
diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py
index 42461dd..cc940a2 100644
--- a/docker/utils/fnmatch.py
+++ b/docker/utils/fnmatch.py
@@ -111,4 +111,5 @@
res = '%s[%s]' % (res, stuff)
else:
res = res + re.escape(c)
+
return res + '$'
diff --git a/docker/version.py b/docker/version.py
index c504327..d451374 100644
--- a/docker/version.py
+++ b/docker/version.py
@@ -1,2 +1,2 @@
-version = "3.4.0"
+version = "3.4.1"
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 5a0d55a..2bd11a7 100644
--- a/docs/change-log.md
+++ b/docs/change-log.md
@@ -1,6 +1,18 @@
Change log
==========
+3.4.1
+-----
+
+[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/52?closed=1)
+
+### Bugfixes
+
+* Fixed a bug that caused auth values in config files written using one of the
+ legacy formats to be ignored
+* Fixed issues with handling of double-wildcard `**` patterns in
+ `.dockerignore` files
+
3.4.0
-----
diff --git a/tests/helpers.py b/tests/helpers.py
index b6b493b..b36d6d7 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -123,7 +123,12 @@
sock.sendall(b'make sure the socket is closed\n')
else:
sock.sendall(b"make sure the socket is closed\n")
- assert sock.recv(32) == b''
+ data = sock.recv(128)
+ # New in 18.06: error message is broadcast over the socket when reading
+ # after detach
+ assert data == b'' or data.startswith(
+ b'exec attach failed: error on attach stdin: read escape sequence'
+ )
def ctrl_with(char):
diff --git a/tests/integration/api_client_test.py b/tests/integration/api_client_test.py
index 05281f8..905e064 100644
--- a/tests/integration/api_client_test.py
+++ b/tests/integration/api_client_test.py
@@ -1,6 +1,3 @@
-import base64
-import os
-import tempfile
import time
import unittest
import warnings
@@ -24,43 +21,6 @@
assert 'Debug' in res
-class LoadConfigTest(BaseAPIIntegrationTest):
- def test_load_legacy_config(self):
- folder = tempfile.mkdtemp()
- self.tmp_folders.append(folder)
- cfg_path = os.path.join(folder, '.dockercfg')
- f = open(cfg_path, 'w')
- auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
- f.write('auth = {0}\n'.format(auth_))
- f.write('email = sakuya@scarlet.net')
- f.close()
- cfg = docker.auth.load_config(cfg_path)
- assert cfg[docker.auth.INDEX_NAME] is not None
- cfg = cfg[docker.auth.INDEX_NAME]
- assert cfg['username'] == 'sakuya'
- assert cfg['password'] == 'izayoi'
- assert cfg['email'] == 'sakuya@scarlet.net'
- assert cfg.get('Auth') is None
-
- def test_load_json_config(self):
- folder = tempfile.mkdtemp()
- self.tmp_folders.append(folder)
- cfg_path = os.path.join(folder, '.dockercfg')
- f = open(os.path.join(folder, '.dockercfg'), 'w')
- auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
- email_ = 'sakuya@scarlet.net'
- f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format(
- docker.auth.INDEX_URL, auth_, email_))
- f.close()
- cfg = docker.auth.load_config(cfg_path)
- assert cfg[docker.auth.INDEX_URL] is not None
- cfg = cfg[docker.auth.INDEX_URL]
- assert cfg['username'] == 'sakuya'
- assert cfg['password'] == 'izayoi'
- assert cfg['email'] == 'sakuya@scarlet.net'
- assert cfg.get('Auth') is None
-
-
class AutoDetectVersionTest(unittest.TestCase):
def test_client_init(self):
client = docker.APIClient(version='auto', **kwargs_from_env())
diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py
index ee32ca0..947d680 100644
--- a/tests/unit/auth_test.py
+++ b/tests/unit/auth_test.py
@@ -282,22 +282,64 @@
cfg = auth.load_config(folder)
assert cfg is not None
- def test_load_config(self):
+ def test_load_legacy_config(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)
- dockercfg_path = os.path.join(folder, '.dockercfg')
- with open(dockercfg_path, 'w') as f:
- auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
+ cfg_path = os.path.join(folder, '.dockercfg')
+ auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
+ with open(cfg_path, 'w') as f:
f.write('auth = {0}\n'.format(auth_))
f.write('email = sakuya@scarlet.net')
- cfg = auth.load_config(dockercfg_path)
- assert auth.INDEX_NAME in cfg
- assert cfg[auth.INDEX_NAME] is not None
- cfg = cfg[auth.INDEX_NAME]
+
+ cfg = auth.load_config(cfg_path)
+ assert auth.resolve_authconfig(cfg) is not None
+ assert cfg['auths'][auth.INDEX_NAME] is not None
+ cfg = cfg['auths'][auth.INDEX_NAME]
assert cfg['username'] == 'sakuya'
assert cfg['password'] == 'izayoi'
assert cfg['email'] == 'sakuya@scarlet.net'
- assert cfg.get('auth') is None
+ assert cfg.get('Auth') is None
+
+ def test_load_json_config(self):
+ folder = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, folder)
+ cfg_path = os.path.join(folder, '.dockercfg')
+ auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
+ email = 'sakuya@scarlet.net'
+ with open(cfg_path, 'w') as f:
+ json.dump(
+ {auth.INDEX_URL: {'auth': auth_, 'email': email}}, f
+ )
+ cfg = auth.load_config(cfg_path)
+ assert auth.resolve_authconfig(cfg) is not None
+ assert cfg['auths'][auth.INDEX_URL] is not None
+ cfg = cfg['auths'][auth.INDEX_URL]
+ assert cfg['username'] == 'sakuya'
+ assert cfg['password'] == 'izayoi'
+ assert cfg['email'] == email
+ assert cfg.get('Auth') is None
+
+ def test_load_modern_json_config(self):
+ folder = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, folder)
+ cfg_path = os.path.join(folder, 'config.json')
+ auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
+ email = 'sakuya@scarlet.net'
+ with open(cfg_path, 'w') as f:
+ json.dump({
+ 'auths': {
+ auth.INDEX_URL: {
+ 'auth': auth_, 'email': email
+ }
+ }
+ }, f)
+ cfg = auth.load_config(cfg_path)
+ assert auth.resolve_authconfig(cfg) is not None
+ assert cfg['auths'][auth.INDEX_URL] is not None
+ cfg = cfg['auths'][auth.INDEX_URL]
+ assert cfg['username'] == 'sakuya'
+ assert cfg['password'] == 'izayoi'
+ assert cfg['email'] == email
def test_load_config_with_random_name(self):
folder = tempfile.mkdtemp()
@@ -318,7 +360,7 @@
with open(dockercfg_path, 'w') as f:
json.dump(config, f)
- cfg = auth.load_config(dockercfg_path)
+ cfg = auth.load_config(dockercfg_path)['auths']
assert registry in cfg
assert cfg[registry] is not None
cfg = cfg[registry]
@@ -345,7 +387,7 @@
json.dump(config, f)
with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
- cfg = auth.load_config(None)
+ cfg = auth.load_config(None)['auths']
assert registry in cfg
assert cfg[registry] is not None
cfg = cfg[registry]
@@ -422,7 +464,7 @@
json.dump(config, f)
cfg = auth.load_config(dockercfg_path)
- assert cfg == {}
+ assert cfg == {'auths': {}}
def test_load_config_invalid_auth_dict(self):
folder = tempfile.mkdtemp()
diff --git a/tests/unit/utils_build_test.py b/tests/unit/utils_build_test.py
new file mode 100644
index 0000000..012f15b
--- /dev/null
+++ b/tests/unit/utils_build_test.py
@@ -0,0 +1,493 @@
+# -*- coding: utf-8 -*-
+
+import os
+import os.path
+import shutil
+import socket
+import tarfile
+import tempfile
+import unittest
+
+
+from docker.constants import IS_WINDOWS_PLATFORM
+from docker.utils import exclude_paths, tar
+
+import pytest
+
+from ..helpers import make_tree
+
+
+def convert_paths(collection):
+ return set(map(convert_path, collection))
+
+
+def convert_path(path):
+ return path.replace('/', os.path.sep)
+
+
+class ExcludePathsTest(unittest.TestCase):
+ dirs = [
+ 'foo',
+ 'foo/bar',
+ 'bar',
+ 'target',
+ 'target/subdir',
+ 'subdir',
+ 'subdir/target',
+ 'subdir/target/subdir',
+ 'subdir/subdir2',
+ 'subdir/subdir2/target',
+ 'subdir/subdir2/target/subdir'
+ ]
+
+ files = [
+ 'Dockerfile',
+ 'Dockerfile.alt',
+ '.dockerignore',
+ 'a.py',
+ 'a.go',
+ 'b.py',
+ 'cde.py',
+ 'foo/a.py',
+ 'foo/b.py',
+ 'foo/bar/a.py',
+ 'bar/a.py',
+ 'foo/Dockerfile3',
+ 'target/file.txt',
+ 'target/subdir/file.txt',
+ 'subdir/file.txt',
+ 'subdir/target/file.txt',
+ 'subdir/target/subdir/file.txt',
+ 'subdir/subdir2/file.txt',
+ 'subdir/subdir2/target/file.txt',
+ 'subdir/subdir2/target/subdir/file.txt',
+ ]
+
+ all_paths = set(dirs + files)
+
+ def setUp(self):
+ self.base = make_tree(self.dirs, self.files)
+
+ def tearDown(self):
+ shutil.rmtree(self.base)
+
+ def exclude(self, patterns, dockerfile=None):
+ return set(exclude_paths(self.base, patterns, dockerfile=dockerfile))
+
+ def test_no_excludes(self):
+ assert self.exclude(['']) == convert_paths(self.all_paths)
+
+ def test_no_dupes(self):
+ paths = exclude_paths(self.base, ['!a.py'])
+ assert sorted(paths) == sorted(set(paths))
+
+ def test_wildcard_exclude(self):
+ assert self.exclude(['*']) == set(['Dockerfile', '.dockerignore'])
+
+ def test_exclude_dockerfile_dockerignore(self):
+ """
+ Even if the .dockerignore file explicitly says to exclude
+ Dockerfile and/or .dockerignore, don't exclude them from
+ the actual tar file.
+ """
+ assert self.exclude(['Dockerfile', '.dockerignore']) == convert_paths(
+ self.all_paths
+ )
+
+ def test_exclude_custom_dockerfile(self):
+ """
+ If we're using a custom Dockerfile, make sure that's not
+ excluded.
+ """
+ assert self.exclude(['*'], dockerfile='Dockerfile.alt') == set(
+ ['Dockerfile.alt', '.dockerignore']
+ )
+
+ assert self.exclude(
+ ['*'], dockerfile='foo/Dockerfile3'
+ ) == convert_paths(set(['foo/Dockerfile3', '.dockerignore']))
+
+ # https://github.com/docker/docker-py/issues/1956
+ assert self.exclude(
+ ['*'], dockerfile='./foo/Dockerfile3'
+ ) == convert_paths(set(['foo/Dockerfile3', '.dockerignore']))
+
+ def test_exclude_dockerfile_child(self):
+ includes = self.exclude(['foo/'], dockerfile='foo/Dockerfile3')
+ 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(
+ self.all_paths - set(['a.py'])
+ )
+
+ def test_single_filename_leading_dot_slash(self):
+ assert self.exclude(['./a.py']) == convert_paths(
+ self.all_paths - set(['a.py'])
+ )
+
+ # As odd as it sounds, a filename pattern with a trailing slash on the
+ # end *will* result in that file being excluded.
+ def test_single_filename_trailing_slash(self):
+ assert self.exclude(['a.py/']) == convert_paths(
+ self.all_paths - set(['a.py'])
+ )
+
+ def test_wildcard_filename_start(self):
+ assert self.exclude(['*.py']) == convert_paths(
+ self.all_paths - set(['a.py', 'b.py', 'cde.py'])
+ )
+
+ def test_wildcard_with_exception(self):
+ assert self.exclude(['*.py', '!b.py']) == convert_paths(
+ self.all_paths - set(['a.py', 'cde.py'])
+ )
+
+ def test_wildcard_with_wildcard_exception(self):
+ assert self.exclude(['*.*', '!*.go']) == convert_paths(
+ self.all_paths - set([
+ 'a.py', 'b.py', 'cde.py', 'Dockerfile.alt',
+ ])
+ )
+
+ def test_wildcard_filename_end(self):
+ assert self.exclude(['a.*']) == convert_paths(
+ self.all_paths - set(['a.py', 'a.go'])
+ )
+
+ def test_question_mark(self):
+ assert self.exclude(['?.py']) == convert_paths(
+ self.all_paths - set(['a.py', 'b.py'])
+ )
+
+ def test_single_subdir_single_filename(self):
+ assert self.exclude(['foo/a.py']) == convert_paths(
+ self.all_paths - set(['foo/a.py'])
+ )
+
+ def test_single_subdir_single_filename_leading_slash(self):
+ assert self.exclude(['/foo/a.py']) == convert_paths(
+ self.all_paths - set(['foo/a.py'])
+ )
+
+ def test_exclude_include_absolute_path(self):
+ base = make_tree([], ['a.py', 'b.py'])
+ assert exclude_paths(
+ base,
+ ['/*', '!/*.py']
+ ) == set(['a.py', 'b.py'])
+
+ def test_single_subdir_with_path_traversal(self):
+ assert self.exclude(['foo/whoops/../a.py']) == convert_paths(
+ self.all_paths - set(['foo/a.py'])
+ )
+
+ def test_single_subdir_wildcard_filename(self):
+ assert self.exclude(['foo/*.py']) == convert_paths(
+ self.all_paths - set(['foo/a.py', 'foo/b.py'])
+ )
+
+ def test_wildcard_subdir_single_filename(self):
+ assert self.exclude(['*/a.py']) == convert_paths(
+ self.all_paths - set(['foo/a.py', 'bar/a.py'])
+ )
+
+ def test_wildcard_subdir_wildcard_filename(self):
+ assert self.exclude(['*/*.py']) == convert_paths(
+ self.all_paths - set(['foo/a.py', 'foo/b.py', 'bar/a.py'])
+ )
+
+ def test_directory(self):
+ assert self.exclude(['foo']) == convert_paths(
+ self.all_paths - set([
+ 'foo', 'foo/a.py', 'foo/b.py', 'foo/bar', 'foo/bar/a.py',
+ 'foo/Dockerfile3'
+ ])
+ )
+
+ def test_directory_with_trailing_slash(self):
+ assert self.exclude(['foo']) == convert_paths(
+ self.all_paths - set([
+ 'foo', 'foo/a.py', 'foo/b.py',
+ 'foo/bar', 'foo/bar/a.py', 'foo/Dockerfile3'
+ ])
+ )
+
+ def test_directory_with_single_exception(self):
+ assert self.exclude(['foo', '!foo/bar/a.py']) == convert_paths(
+ self.all_paths - set([
+ 'foo/a.py', 'foo/b.py', 'foo', 'foo/bar',
+ 'foo/Dockerfile3'
+ ])
+ )
+
+ def test_directory_with_subdir_exception(self):
+ assert self.exclude(['foo', '!foo/bar']) == convert_paths(
+ self.all_paths - set([
+ 'foo/a.py', 'foo/b.py', 'foo', 'foo/Dockerfile3'
+ ])
+ )
+
+ @pytest.mark.skipif(
+ not IS_WINDOWS_PLATFORM, reason='Backslash patterns only on Windows'
+ )
+ def test_directory_with_subdir_exception_win32_pathsep(self):
+ assert self.exclude(['foo', '!foo\\bar']) == convert_paths(
+ self.all_paths - set([
+ 'foo/a.py', 'foo/b.py', 'foo', 'foo/Dockerfile3'
+ ])
+ )
+
+ def test_directory_with_wildcard_exception(self):
+ assert self.exclude(['foo', '!foo/*.py']) == convert_paths(
+ self.all_paths - set([
+ 'foo/bar', 'foo/bar/a.py', 'foo', 'foo/Dockerfile3'
+ ])
+ )
+
+ def test_subdirectory(self):
+ assert self.exclude(['foo/bar']) == convert_paths(
+ self.all_paths - set(['foo/bar', 'foo/bar/a.py'])
+ )
+
+ @pytest.mark.skipif(
+ not IS_WINDOWS_PLATFORM, reason='Backslash patterns only on Windows'
+ )
+ def test_subdirectory_win32_pathsep(self):
+ assert self.exclude(['foo\\bar']) == convert_paths(
+ self.all_paths - set(['foo/bar', 'foo/bar/a.py'])
+ )
+
+ def test_double_wildcard(self):
+ assert self.exclude(['**/a.py']) == convert_paths(
+ self.all_paths - set(
+ ['a.py', 'foo/a.py', 'foo/bar/a.py', 'bar/a.py']
+ )
+ )
+
+ assert self.exclude(['foo/**/bar']) == convert_paths(
+ self.all_paths - set(['foo/bar', 'foo/bar/a.py'])
+ )
+
+ def test_single_and_double_wildcard(self):
+ assert self.exclude(['**/target/*/*']) == convert_paths(
+ self.all_paths - set(
+ ['target/subdir/file.txt',
+ 'subdir/target/subdir/file.txt',
+ 'subdir/subdir2/target/subdir/file.txt']
+ )
+ )
+
+ def test_trailing_double_wildcard(self):
+ assert self.exclude(['subdir/**']) == convert_paths(
+ self.all_paths - set(
+ ['subdir/file.txt',
+ 'subdir/target/file.txt',
+ 'subdir/target/subdir/file.txt',
+ 'subdir/subdir2/file.txt',
+ 'subdir/subdir2/target/file.txt',
+ 'subdir/subdir2/target/subdir/file.txt',
+ 'subdir/target',
+ 'subdir/target/subdir',
+ 'subdir/subdir2',
+ 'subdir/subdir2/target',
+ 'subdir/subdir2/target/subdir']
+ )
+ )
+
+ def test_double_wildcard_with_exception(self):
+ assert self.exclude(['**', '!bar', '!foo/bar']) == convert_paths(
+ set([
+ 'foo/bar', 'foo/bar/a.py', 'bar', 'bar/a.py', 'Dockerfile',
+ '.dockerignore',
+ ])
+ )
+
+ def test_include_wildcard(self):
+ # This may be surprising but it matches the CLI's behavior
+ # (tested with 18.05.0-ce on linux)
+ base = make_tree(['a'], ['a/b.py'])
+ assert exclude_paths(
+ base,
+ ['*', '!*/b.py']
+ ) == set()
+
+ def test_last_line_precedence(self):
+ base = make_tree(
+ [],
+ ['garbage.md',
+ 'trash.md',
+ 'README.md',
+ 'README-bis.md',
+ 'README-secret.md'])
+ assert exclude_paths(
+ base,
+ ['*.md', '!README*.md', 'README-secret.md']
+ ) == set(['README.md', 'README-bis.md'])
+
+ def test_parent_directory(self):
+ base = make_tree(
+ [],
+ ['a.py',
+ 'b.py',
+ 'c.py'])
+ # Dockerignore reference stipulates that absolute paths are
+ # equivalent to relative paths, hence /../foo should be
+ # equivalent to ../foo. It also stipulates that paths are run
+ # through Go's filepath.Clean, which explicitely "replace
+ # "/.." by "/" at the beginning of a path".
+ assert exclude_paths(
+ base,
+ ['../a.py', '/../b.py']
+ ) == set(['c.py'])
+
+
+class TarTest(unittest.TestCase):
+ def test_tar_with_excludes(self):
+ dirs = [
+ 'foo',
+ 'foo/bar',
+ 'bar',
+ ]
+
+ files = [
+ 'Dockerfile',
+ 'Dockerfile.alt',
+ '.dockerignore',
+ 'a.py',
+ 'a.go',
+ 'b.py',
+ 'cde.py',
+ 'foo/a.py',
+ 'foo/b.py',
+ 'foo/bar/a.py',
+ 'bar/a.py',
+ ]
+
+ exclude = [
+ '*.py',
+ '!b.py',
+ '!a.go',
+ 'foo',
+ 'Dockerfile*',
+ '.dockerignore',
+ ]
+
+ expected_names = set([
+ 'Dockerfile',
+ '.dockerignore',
+ 'a.go',
+ 'b.py',
+ 'bar',
+ 'bar/a.py',
+ ])
+
+ base = make_tree(dirs, files)
+ self.addCleanup(shutil.rmtree, base)
+
+ with tar(base, exclude=exclude) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert sorted(tar_data.getnames()) == sorted(expected_names)
+
+ def test_tar_with_empty_directory(self):
+ base = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, base)
+ for d in ['foo', 'bar']:
+ os.makedirs(os.path.join(base, d))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert sorted(tar_data.getnames()) == ['bar', 'foo']
+
+ @pytest.mark.skipif(
+ IS_WINDOWS_PLATFORM or os.geteuid() == 0,
+ reason='root user always has access ; no chmod on Windows'
+ )
+ def test_tar_with_inaccessible_file(self):
+ base = tempfile.mkdtemp()
+ full_path = os.path.join(base, 'foo')
+ self.addCleanup(shutil.rmtree, base)
+ with open(full_path, 'w') as f:
+ f.write('content')
+ os.chmod(full_path, 0o222)
+ with pytest.raises(IOError) as ei:
+ tar(base)
+
+ assert 'Can not read file in context: {}'.format(full_path) in (
+ ei.exconly()
+ )
+
+ @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
+ def test_tar_with_file_symlinks(self):
+ base = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, base)
+ with open(os.path.join(base, 'foo'), 'w') as f:
+ f.write("content")
+ os.makedirs(os.path.join(base, 'bar'))
+ os.symlink('../foo', os.path.join(base, 'bar/foo'))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert sorted(tar_data.getnames()) == ['bar', 'bar/foo', 'foo']
+
+ @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
+ def test_tar_with_directory_symlinks(self):
+ base = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, base)
+ for d in ['foo', 'bar']:
+ os.makedirs(os.path.join(base, d))
+ os.symlink('../foo', os.path.join(base, 'bar/foo'))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert sorted(tar_data.getnames()) == ['bar', 'bar/foo', 'foo']
+
+ @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
+ def test_tar_with_broken_symlinks(self):
+ base = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, base)
+ for d in ['foo', 'bar']:
+ os.makedirs(os.path.join(base, d))
+
+ os.symlink('../baz', os.path.join(base, 'bar/foo'))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert 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)
+ for d in ['foo', 'bar']:
+ os.makedirs(os.path.join(base, d))
+ sock = socket.socket(socket.AF_UNIX)
+ self.addCleanup(sock.close)
+ sock.bind(os.path.join(base, 'test.sock'))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert sorted(tar_data.getnames()) == ['bar', 'foo']
+
+ def tar_test_negative_mtime_bug(self):
+ base = tempfile.mkdtemp()
+ filename = os.path.join(base, 'th.txt')
+ self.addCleanup(shutil.rmtree, base)
+ with open(filename, 'w') as f:
+ f.write('Invisible Full Moon')
+ os.utime(filename, (12345, -3600.0))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ assert tar_data.getnames() == ['th.txt']
+ assert tar_data.getmember('th.txt').mtime == -3600
+
+ @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
+ def test_tar_directory_link(self):
+ dirs = ['a', 'b', 'a/c']
+ files = ['a/hello.py', 'b/utils.py', 'a/c/descend.py']
+ base = make_tree(dirs, files)
+ self.addCleanup(shutil.rmtree, base)
+ os.symlink(os.path.join(base, 'b'), os.path.join(base, 'a/c/b'))
+ with tar(base) as archive:
+ tar_data = tarfile.open(fileobj=archive)
+ names = tar_data.getnames()
+ for member in dirs + files:
+ assert member in names
+ assert 'a/c/b' in names
+ assert 'a/c/b/utils.py' not in names
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
index 00456e8..8880cfe 100644
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -5,29 +5,25 @@
import os
import os.path
import shutil
-import socket
import sys
-import tarfile
import tempfile
import unittest
-import pytest
-import six
from docker.api.client import APIClient
-from docker.constants import IS_WINDOWS_PLATFORM
from docker.errors import DockerException
from docker.utils import (
- parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
- parse_bytes, parse_env_file, exclude_paths, convert_volume_binds,
- decode_json_header, tar, split_command, parse_devices, update_headers,
+ convert_filters, convert_volume_binds, decode_json_header, kwargs_from_env,
+ parse_bytes, parse_devices, parse_env_file, parse_host,
+ parse_repository_tag, split_command, update_headers,
)
from docker.utils.ports import build_port_bindings, split_port
from docker.utils.utils import format_environment
-from ..helpers import make_tree
+import pytest
+import six
TEST_CERT_DIR = os.path.join(
os.path.dirname(__file__),
@@ -608,472 +604,6 @@
assert port_bindings["2000"] == [("127.0.0.1", "2000")]
-def convert_paths(collection):
- return set(map(convert_path, collection))
-
-
-def convert_path(path):
- return path.replace('/', os.path.sep)
-
-
-class ExcludePathsTest(unittest.TestCase):
- dirs = [
- 'foo',
- 'foo/bar',
- 'bar',
- 'target',
- 'target/subdir',
- 'subdir',
- 'subdir/target',
- 'subdir/target/subdir',
- 'subdir/subdir2',
- 'subdir/subdir2/target',
- 'subdir/subdir2/target/subdir'
- ]
-
- files = [
- 'Dockerfile',
- 'Dockerfile.alt',
- '.dockerignore',
- 'a.py',
- 'a.go',
- 'b.py',
- 'cde.py',
- 'foo/a.py',
- 'foo/b.py',
- 'foo/bar/a.py',
- 'bar/a.py',
- 'foo/Dockerfile3',
- 'target/file.txt',
- 'target/subdir/file.txt',
- 'subdir/file.txt',
- 'subdir/target/file.txt',
- 'subdir/target/subdir/file.txt',
- 'subdir/subdir2/file.txt',
- 'subdir/subdir2/target/file.txt',
- 'subdir/subdir2/target/subdir/file.txt',
- ]
-
- all_paths = set(dirs + files)
-
- def setUp(self):
- self.base = make_tree(self.dirs, self.files)
-
- def tearDown(self):
- shutil.rmtree(self.base)
-
- def exclude(self, patterns, dockerfile=None):
- return set(exclude_paths(self.base, patterns, dockerfile=dockerfile))
-
- def test_no_excludes(self):
- assert self.exclude(['']) == convert_paths(self.all_paths)
-
- def test_no_dupes(self):
- paths = exclude_paths(self.base, ['!a.py'])
- assert sorted(paths) == sorted(set(paths))
-
- def test_wildcard_exclude(self):
- assert self.exclude(['*']) == set(['Dockerfile', '.dockerignore'])
-
- def test_exclude_dockerfile_dockerignore(self):
- """
- Even if the .dockerignore file explicitly says to exclude
- Dockerfile and/or .dockerignore, don't exclude them from
- the actual tar file.
- """
- assert self.exclude(['Dockerfile', '.dockerignore']) == convert_paths(
- self.all_paths
- )
-
- def test_exclude_custom_dockerfile(self):
- """
- If we're using a custom Dockerfile, make sure that's not
- excluded.
- """
- assert self.exclude(['*'], dockerfile='Dockerfile.alt') == set(
- ['Dockerfile.alt', '.dockerignore']
- )
-
- assert self.exclude(
- ['*'], dockerfile='foo/Dockerfile3'
- ) == convert_paths(set(['foo/Dockerfile3', '.dockerignore']))
-
- # https://github.com/docker/docker-py/issues/1956
- assert self.exclude(
- ['*'], dockerfile='./foo/Dockerfile3'
- ) == convert_paths(set(['foo/Dockerfile3', '.dockerignore']))
-
- def test_exclude_dockerfile_child(self):
- includes = self.exclude(['foo/'], dockerfile='foo/Dockerfile3')
- 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(
- self.all_paths - set(['a.py'])
- )
-
- def test_single_filename_leading_dot_slash(self):
- assert self.exclude(['./a.py']) == convert_paths(
- self.all_paths - set(['a.py'])
- )
-
- # As odd as it sounds, a filename pattern with a trailing slash on the
- # end *will* result in that file being excluded.
- def test_single_filename_trailing_slash(self):
- assert self.exclude(['a.py/']) == convert_paths(
- self.all_paths - set(['a.py'])
- )
-
- def test_wildcard_filename_start(self):
- assert self.exclude(['*.py']) == convert_paths(
- self.all_paths - set(['a.py', 'b.py', 'cde.py'])
- )
-
- def test_wildcard_with_exception(self):
- assert self.exclude(['*.py', '!b.py']) == convert_paths(
- self.all_paths - set(['a.py', 'cde.py'])
- )
-
- def test_wildcard_with_wildcard_exception(self):
- assert self.exclude(['*.*', '!*.go']) == convert_paths(
- self.all_paths - set([
- 'a.py', 'b.py', 'cde.py', 'Dockerfile.alt',
- ])
- )
-
- def test_wildcard_filename_end(self):
- assert self.exclude(['a.*']) == convert_paths(
- self.all_paths - set(['a.py', 'a.go'])
- )
-
- def test_question_mark(self):
- assert self.exclude(['?.py']) == convert_paths(
- self.all_paths - set(['a.py', 'b.py'])
- )
-
- def test_single_subdir_single_filename(self):
- assert self.exclude(['foo/a.py']) == convert_paths(
- self.all_paths - set(['foo/a.py'])
- )
-
- def test_single_subdir_single_filename_leading_slash(self):
- assert self.exclude(['/foo/a.py']) == convert_paths(
- self.all_paths - set(['foo/a.py'])
- )
-
- def test_exclude_include_absolute_path(self):
- base = make_tree([], ['a.py', 'b.py'])
- assert exclude_paths(
- base,
- ['/*', '!/*.py']
- ) == set(['a.py', 'b.py'])
-
- def test_single_subdir_with_path_traversal(self):
- assert self.exclude(['foo/whoops/../a.py']) == convert_paths(
- self.all_paths - set(['foo/a.py'])
- )
-
- def test_single_subdir_wildcard_filename(self):
- assert self.exclude(['foo/*.py']) == convert_paths(
- self.all_paths - set(['foo/a.py', 'foo/b.py'])
- )
-
- def test_wildcard_subdir_single_filename(self):
- assert self.exclude(['*/a.py']) == convert_paths(
- self.all_paths - set(['foo/a.py', 'bar/a.py'])
- )
-
- def test_wildcard_subdir_wildcard_filename(self):
- assert self.exclude(['*/*.py']) == convert_paths(
- self.all_paths - set(['foo/a.py', 'foo/b.py', 'bar/a.py'])
- )
-
- def test_directory(self):
- assert self.exclude(['foo']) == convert_paths(
- self.all_paths - set([
- 'foo', 'foo/a.py', 'foo/b.py', 'foo/bar', 'foo/bar/a.py',
- 'foo/Dockerfile3'
- ])
- )
-
- def test_directory_with_trailing_slash(self):
- assert self.exclude(['foo']) == convert_paths(
- self.all_paths - set([
- 'foo', 'foo/a.py', 'foo/b.py',
- 'foo/bar', 'foo/bar/a.py', 'foo/Dockerfile3'
- ])
- )
-
- def test_directory_with_single_exception(self):
- assert self.exclude(['foo', '!foo/bar/a.py']) == convert_paths(
- self.all_paths - set([
- 'foo/a.py', 'foo/b.py', 'foo', 'foo/bar',
- 'foo/Dockerfile3'
- ])
- )
-
- def test_directory_with_subdir_exception(self):
- assert self.exclude(['foo', '!foo/bar']) == convert_paths(
- self.all_paths - set([
- 'foo/a.py', 'foo/b.py', 'foo', 'foo/Dockerfile3'
- ])
- )
-
- @pytest.mark.skipif(
- not IS_WINDOWS_PLATFORM, reason='Backslash patterns only on Windows'
- )
- def test_directory_with_subdir_exception_win32_pathsep(self):
- assert self.exclude(['foo', '!foo\\bar']) == convert_paths(
- self.all_paths - set([
- 'foo/a.py', 'foo/b.py', 'foo', 'foo/Dockerfile3'
- ])
- )
-
- def test_directory_with_wildcard_exception(self):
- assert self.exclude(['foo', '!foo/*.py']) == convert_paths(
- self.all_paths - set([
- 'foo/bar', 'foo/bar/a.py', 'foo', 'foo/Dockerfile3'
- ])
- )
-
- def test_subdirectory(self):
- assert self.exclude(['foo/bar']) == convert_paths(
- self.all_paths - set(['foo/bar', 'foo/bar/a.py'])
- )
-
- @pytest.mark.skipif(
- not IS_WINDOWS_PLATFORM, reason='Backslash patterns only on Windows'
- )
- def test_subdirectory_win32_pathsep(self):
- assert self.exclude(['foo\\bar']) == convert_paths(
- self.all_paths - set(['foo/bar', 'foo/bar/a.py'])
- )
-
- def test_double_wildcard(self):
- assert self.exclude(['**/a.py']) == convert_paths(
- self.all_paths - set(
- ['a.py', 'foo/a.py', 'foo/bar/a.py', 'bar/a.py']
- )
- )
-
- assert self.exclude(['foo/**/bar']) == convert_paths(
- self.all_paths - set(['foo/bar', 'foo/bar/a.py'])
- )
-
- def test_single_and_double_wildcard(self):
- assert self.exclude(['**/target/*/*']) == convert_paths(
- self.all_paths - set(
- ['target/subdir/file.txt',
- 'subdir/target/subdir/file.txt',
- 'subdir/subdir2/target/subdir/file.txt']
- )
- )
-
- def test_trailing_double_wildcard(self):
- assert self.exclude(['subdir/**']) == convert_paths(
- self.all_paths - set(
- ['subdir/file.txt',
- 'subdir/target/file.txt',
- 'subdir/target/subdir/file.txt',
- 'subdir/subdir2/file.txt',
- 'subdir/subdir2/target/file.txt',
- 'subdir/subdir2/target/subdir/file.txt',
- 'subdir/target',
- 'subdir/target/subdir',
- 'subdir/subdir2',
- 'subdir/subdir2/target',
- 'subdir/subdir2/target/subdir']
- )
- )
-
- def test_include_wildcard(self):
- base = make_tree(['a'], ['a/b.py'])
- assert exclude_paths(
- base,
- ['*', '!*/b.py']
- ) == convert_paths(['a/b.py'])
-
- def test_last_line_precedence(self):
- base = make_tree(
- [],
- ['garbage.md',
- 'thrash.md',
- 'README.md',
- 'README-bis.md',
- 'README-secret.md'])
- assert exclude_paths(
- base,
- ['*.md', '!README*.md', 'README-secret.md']
- ) == set(['README.md', 'README-bis.md'])
-
- def test_parent_directory(self):
- base = make_tree(
- [],
- ['a.py',
- 'b.py',
- 'c.py'])
- # Dockerignore reference stipulates that absolute paths are
- # equivalent to relative paths, hence /../foo should be
- # equivalent to ../foo. It also stipulates that paths are run
- # through Go's filepath.Clean, which explicitely "replace
- # "/.." by "/" at the beginning of a path".
- assert exclude_paths(
- base,
- ['../a.py', '/../b.py']
- ) == set(['c.py'])
-
-
-class TarTest(unittest.TestCase):
- def test_tar_with_excludes(self):
- dirs = [
- 'foo',
- 'foo/bar',
- 'bar',
- ]
-
- files = [
- 'Dockerfile',
- 'Dockerfile.alt',
- '.dockerignore',
- 'a.py',
- 'a.go',
- 'b.py',
- 'cde.py',
- 'foo/a.py',
- 'foo/b.py',
- 'foo/bar/a.py',
- 'bar/a.py',
- ]
-
- exclude = [
- '*.py',
- '!b.py',
- '!a.go',
- 'foo',
- 'Dockerfile*',
- '.dockerignore',
- ]
-
- expected_names = set([
- 'Dockerfile',
- '.dockerignore',
- 'a.go',
- 'b.py',
- 'bar',
- 'bar/a.py',
- ])
-
- base = make_tree(dirs, files)
- self.addCleanup(shutil.rmtree, base)
-
- with tar(base, exclude=exclude) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert sorted(tar_data.getnames()) == sorted(expected_names)
-
- def test_tar_with_empty_directory(self):
- base = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, base)
- for d in ['foo', 'bar']:
- os.makedirs(os.path.join(base, d))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert sorted(tar_data.getnames()) == ['bar', 'foo']
-
- @pytest.mark.skipif(
- IS_WINDOWS_PLATFORM or os.geteuid() == 0,
- reason='root user always has access ; no chmod on Windows'
- )
- def test_tar_with_inaccessible_file(self):
- base = tempfile.mkdtemp()
- full_path = os.path.join(base, 'foo')
- self.addCleanup(shutil.rmtree, base)
- with open(full_path, 'w') as f:
- f.write('content')
- os.chmod(full_path, 0o222)
- with pytest.raises(IOError) as ei:
- tar(base)
-
- assert 'Can not read file in context: {}'.format(full_path) in (
- ei.exconly()
- )
-
- @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
- def test_tar_with_file_symlinks(self):
- base = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, base)
- with open(os.path.join(base, 'foo'), 'w') as f:
- f.write("content")
- os.makedirs(os.path.join(base, 'bar'))
- os.symlink('../foo', os.path.join(base, 'bar/foo'))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert sorted(tar_data.getnames()) == ['bar', 'bar/foo', 'foo']
-
- @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
- def test_tar_with_directory_symlinks(self):
- base = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, base)
- for d in ['foo', 'bar']:
- os.makedirs(os.path.join(base, d))
- os.symlink('../foo', os.path.join(base, 'bar/foo'))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert sorted(tar_data.getnames()) == ['bar', 'bar/foo', 'foo']
-
- @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
- def test_tar_with_broken_symlinks(self):
- base = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, base)
- for d in ['foo', 'bar']:
- os.makedirs(os.path.join(base, d))
-
- os.symlink('../baz', os.path.join(base, 'bar/foo'))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert 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)
- for d in ['foo', 'bar']:
- os.makedirs(os.path.join(base, d))
- sock = socket.socket(socket.AF_UNIX)
- self.addCleanup(sock.close)
- sock.bind(os.path.join(base, 'test.sock'))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert sorted(tar_data.getnames()) == ['bar', 'foo']
-
- def tar_test_negative_mtime_bug(self):
- base = tempfile.mkdtemp()
- filename = os.path.join(base, 'th.txt')
- self.addCleanup(shutil.rmtree, base)
- with open(filename, 'w') as f:
- f.write('Invisible Full Moon')
- os.utime(filename, (12345, -3600.0))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- assert tar_data.getnames() == ['th.txt']
- assert tar_data.getmember('th.txt').mtime == -3600
-
- @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
- def test_tar_directory_link(self):
- dirs = ['a', 'b', 'a/c']
- files = ['a/hello.py', 'b/utils.py', 'a/c/descend.py']
- base = make_tree(dirs, files)
- self.addCleanup(shutil.rmtree, base)
- os.symlink(os.path.join(base, 'b'), os.path.join(base, 'a/c/b'))
- with tar(base) as archive:
- tar_data = tarfile.open(fileobj=archive)
- names = tar_data.getnames()
- for member in dirs + files:
- assert member in names
- assert 'a/c/b' in names
- assert 'a/c/b/utils.py' not in names
-
-
class FormatEnvironmentTest(unittest.TestCase):
def test_format_env_binary_unicode_value(self):
env_dict = {