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