1# -*- coding: utf-8 -*-
2
3import _pytest.tmpdir
4import distutils
5import os
6import os.path
7import pkg_resources
8
9try:
10    import pathlib
11except ImportError:
12    import pathlib2 as pathlib  # Python 2.7
13
14import py.path
15import re
16import requests
17import six
18import subprocess
19import sys
20
21from contextlib import contextmanager
22from mock import patch
23
24from skbuild.compat import which  # noqa: F401
25from skbuild.utils import push_dir
26from skbuild.platform_specifics import get_platform
27
28
29SAMPLES_DIR = os.path.join(
30    os.path.dirname(os.path.realpath(__file__)),
31    'samples',
32    )
33
34
35@contextmanager
36def push_argv(argv):
37    old_argv = sys.argv
38    sys.argv = argv
39    yield
40    sys.argv = old_argv
41
42
43@contextmanager
44def push_env(**kwargs):
45    """This context manager allow to set/unset environment variables.
46    """
47    saved_env = dict(os.environ)
48    for var, value in kwargs.items():
49        if value is not None:
50            os.environ[var] = value
51        elif var in os.environ:
52            del os.environ[var]
53    yield
54    os.environ.clear()
55    for (saved_var, saved_value) in saved_env.items():
56        os.environ[saved_var] = saved_value
57
58
59@contextmanager
60def prepend_sys_path(paths):
61    """This context manager allows to prepend paths to ``sys.path`` and restore the
62    original list.
63    """
64    saved_paths = list(sys.path)
65    sys.path = paths + saved_paths
66    yield
67    sys.path = saved_paths
68
69
70def _tmpdir(basename):
71    """This function returns a temporary directory similar to the one
72    returned by the ``tmpdir`` pytest fixture.
73    The difference is that the `basetemp` is not configurable using
74    the pytest settings."""
75
76    # Adapted from _pytest.tmpdir.tmpdir()
77    basename = re.sub(r"[\W]", "_", basename)
78    max_val = 30
79    if len(basename) > max_val:
80        basename = basename[:max_val]
81
82    # Adapted from _pytest.tmpdir.TempdirFactory.getbasetemp()
83    try:
84        basetemp = _tmpdir._basetemp
85    except AttributeError:
86        temproot = py.path.local.get_temproot()
87        user = _pytest.tmpdir.get_user()
88
89        if user:
90            # use a sub-directory in the temproot to speed-up
91            # make_numbered_dir() call
92            rootdir = temproot.join('pytest-of-%s' % user)
93        else:
94            rootdir = temproot
95
96        rootdir.ensure(dir=1)
97        basetemp = py.path.local.make_numbered_dir(prefix='pytest-',
98                                                   rootdir=rootdir)
99
100    # Adapted from _pytest.tmpdir.TempdirFactory.mktemp
101    return py.path.local.make_numbered_dir(prefix=basename,
102                                           keep=0, rootdir=basetemp,
103                                           lock_timeout=None)
104
105
106def _copy(src, target):
107    """
108    Copies a single entry (file, dir) named 'src' to 'target'. Softlinks are
109    processed properly as well.
110
111    Copied from pytest-datafiles/pytest_datafiles.py (MIT License)
112    """
113    if not src.exists():
114        raise ValueError("'%s' does not exist!" % src)
115
116    if src.isdir():
117        src.copy(target / src.basename)
118    elif src.islink():
119        (target / src.basename).mksymlinkto(src.realpath())
120    else:  # file
121        src.copy(target)
122
123
124def _copy_dir(target_dir, src_dir, on_duplicate='exception', keep_top_dir=False):
125    """
126    Copies all entries (files, dirs) from 'src_dir' to 'target_dir' taking
127    into account the 'on_duplicate' option (which defines what should happen if
128    an entry already exists: raise an exception, overwrite it or ignore it).
129
130    Adapted from pytest-datafiles/pytest_datafiles.py (MIT License)
131    """
132    src_files = []
133
134    if isinstance(src_dir, six.string_types):
135        src_dir = py.path.local(src_dir)
136
137    if keep_top_dir:
138        src_files = src_dir
139    else:
140        if src_dir.isdir():
141            src_files.extend(src_dir.listdir())
142        else:
143            src_files.append(src_dir)
144
145    for entry in src_files:
146        target_entry = target_dir / entry.basename
147        if not target_entry.exists() or on_duplicate == 'overwrite':
148            _copy(entry, target_dir)
149        elif on_duplicate == 'exception':
150            raise ValueError(
151                "'%s' already exists (src %s)" % (
152                    target_entry,
153                    entry,
154                )
155            )
156        else:  # ignore
157            continue
158
159
160def initialize_git_repo_and_commit(project_dir, verbose=True):
161    """Convenience function creating a git repository in ``project_dir``.
162
163    If ``project_dir`` does NOT contain a ``.git`` directory, a new
164    git repository with one commit containing all the directories and files
165    is created.
166    """
167    if isinstance(project_dir, six.string_types):
168        project_dir = py.path.local(project_dir)
169
170    if project_dir.join('.git').exists():
171        return
172
173    # If any, exclude virtualenv files
174    project_dir.join(".gitignore").write(".env")
175
176    with push_dir(str(project_dir)):
177        for cmd in [
178            ['git', 'init'],
179            ['git', 'config', 'user.name', 'scikit-build'],
180            ['git', 'config', 'user.email', 'test@test'],
181            ['git', 'config', 'commit.gpgsign', 'false'],
182            ['git', 'add', '-A'],
183            ['git', 'reset', '.gitignore'],
184            ['git', 'commit', '-m', 'Initial commit']
185        ]:
186            do_call = (subprocess.check_call
187                       if verbose else subprocess.check_output)
188            do_call(cmd)
189
190
191def prepare_project(project, tmp_project_dir, force=False):
192    """Convenience function setting up the build directory ``tmp_project_dir``
193    for the selected sample ``project``.
194
195    If ``tmp_project_dir`` does not exist, it is created.
196
197    If ``tmp_project_dir`` is empty, the sample ``project`` is copied into it.
198    Specifying ``force=True`` will copy the files even if ``tmp_project_dir``
199    is not empty.
200    """
201    if isinstance(tmp_project_dir, six.string_types):
202        tmp_project_dir = py.path.local(tmp_project_dir)
203
204    # Create project directory if it does not exist
205    if not tmp_project_dir.exists():
206        tmp_project_dir = _tmpdir(project)
207
208    # If empty or if force is True, copy project files and initialize git
209    if not tmp_project_dir.listdir() or force:
210        _copy_dir(tmp_project_dir, os.path.join(SAMPLES_DIR, project))
211
212
213@contextmanager
214def execute_setup_py(project_dir, setup_args, disable_languages_test=False):
215    """Context manager executing ``setup.py`` with the given arguments.
216
217    It yields after changing the current working directory
218    to ``project_dir``.
219    """
220
221    # See https://stackoverflow.com/questions/9160227/dir-util-copy-tree-fails-after-shutil-rmtree
222    distutils.dir_util._path_created = {}
223
224    # Clear _PYTHON_HOST_PLATFORM to ensure value sets in skbuild.setuptools_wrap.setup() does not
225    # influence other tests.
226    if '_PYTHON_HOST_PLATFORM' in os.environ:
227        del os.environ['_PYTHON_HOST_PLATFORM']
228
229    with push_dir(str(project_dir)), push_argv(["setup.py"] + setup_args), prepend_sys_path([str(project_dir)]):
230
231        # Restore master working set that is reset following call to "python setup.py test"
232        # See function "project_on_sys_path()" in setuptools.command.test
233        pkg_resources._initialize_master_working_set()
234
235        with open("setup.py", "r") as fp:
236            setup_code = compile(fp.read(), "setup.py", mode="exec")
237
238            if setup_code is not None:
239
240                if disable_languages_test:
241
242                    platform = get_platform()
243                    original_write_test_cmakelist = platform.write_test_cmakelist
244
245                    def write_test_cmakelist_no_languages(_self, _languages):
246                        original_write_test_cmakelist([])
247
248                    with patch.object(type(platform), 'write_test_cmakelist', new=write_test_cmakelist_no_languages):
249                        six.exec_(setup_code)
250
251                else:
252                    six.exec_(setup_code)
253
254        yield
255
256
257def project_setup_py_test(project, setup_args, tmp_dir=None, verbose_git=True, disable_languages_test=False):
258
259    def dec(fun):
260
261        @six.wraps(fun)
262        def wrapped(*iargs, **ikwargs):
263
264            if wrapped.tmp_dir is None:
265                wrapped.tmp_dir = _tmpdir(fun.__name__)
266                prepare_project(wrapped.project, wrapped.tmp_dir)
267                initialize_git_repo_and_commit(
268                    wrapped.tmp_dir, verbose=wrapped.verbose_git)
269
270            with execute_setup_py(wrapped.tmp_dir, wrapped.setup_args, disable_languages_test=disable_languages_test):
271                result2 = fun(*iargs, **ikwargs)
272
273            return wrapped.tmp_dir, result2
274
275        wrapped.project = project
276        wrapped.setup_args = setup_args
277        wrapped.tmp_dir = tmp_dir
278        wrapped.verbose_git = verbose_git
279
280        return wrapped
281
282    return dec
283
284
285def get_cmakecache_variables(cmakecache):
286    """Returns a dictionary of all variables found in given CMakeCache.txt.
287
288    Dictionary entries are tuple of the
289    form ``(variable_type, variable_value)``.
290
291    Possible `variable_type` are documented
292    `here <https://cmake.org/cmake/help/v3.7/prop_cache/TYPE.html>`_.
293    """
294    results = {}
295    cache_entry_pattern = re.compile(r"^([\w\d_-]+):([\w]+)=")
296    with open(cmakecache) as content:
297        for line in content.readlines():
298            line = line.strip()
299            result = cache_entry_pattern.match(line)
300            if result:
301                variable_name = result.group(1)
302                variable_type = result.group(2)
303                variable_value = line.split("=")[1]
304                results[variable_name] = (variable_type, variable_value)
305    return results
306
307
308def is_site_reachable(url):
309    """Return True if the given website can be accessed"""
310    try:
311        request = requests.get(url)
312        return request.status_code == 200
313    except requests.exceptions.ConnectionError:
314        return False
315
316
317def list_ancestors(path):
318    """Return logical ancestors of the path.
319    """
320    return [str(parent) for parent in pathlib.PurePosixPath(path).parents if str(parent) != "."]
321
322
323def get_ext_suffix():
324    """Return python extension suffix.
325    """
326    ext_suffix_var = 'SO'
327    if sys.version_info[:2] >= (3, 5):
328        ext_suffix_var = 'EXT_SUFFIX'
329    return distutils.sysconfig.get_config_var(ext_suffix_var)
330