1#! /usr/bin/env python 2# encoding: utf-8 3# Calle Rosenquist, 2016-2018 (xbreak) 4 5""" 6Provides Python unit test support using :py:class:`waflib.Tools.waf_unit_test.utest` 7task via the **pytest** feature. 8 9To use pytest the following is needed: 10 111. Load `pytest` and the dependency `waf_unit_test` tools. 122. Create a task generator with feature `pytest` (not `test`) and customize behaviour with 13 the following attributes: 14 15 - `pytest_source`: Test input files. 16 - `ut_str`: Test runner command, e.g. ``${PYTHON} -B -m unittest discover`` or 17 if nose is used: ``${NOSETESTS} --no-byte-compile ${SRC}``. 18 - `ut_shell`: Determines if ``ut_str`` is executed in a shell. Default: False. 19 - `ut_cwd`: Working directory for test runner. Defaults to directory of 20 first ``pytest_source`` file. 21 22 Additionally the following `pytest` specific attributes are used in dependent taskgens: 23 24 - `pytest_path`: Node or string list of additional Python paths. 25 - `pytest_libpath`: Node or string list of additional library paths. 26 27The `use` dependencies are used for both update calculation and to populate 28the following environment variables for the `pytest` test runner: 29 301. `PYTHONPATH` (`sys.path`) of any dependent taskgen that has the feature `py`: 31 32 - `install_from` attribute is used to determine where the root of the Python sources 33 are located. If `install_from` is not specified the default is to use the taskgen path 34 as the root. 35 36 - `pytest_path` attribute is used to manually specify additional Python paths. 37 382. Dynamic linker search path variable (e.g. `LD_LIBRARY_PATH`) of any dependent taskgen with 39 non-static link_task. 40 41 - `pytest_libpath` attribute is used to manually specify additional linker paths. 42 43Note: `pytest` cannot automatically determine the correct `PYTHONPATH` for `pyext` taskgens 44 because the extension might be part of a Python package or used standalone: 45 46 - When used as part of another `py` package, the `PYTHONPATH` is provided by 47 that taskgen so no additional action is required. 48 49 - When used as a standalone module, the user needs to specify the `PYTHONPATH` explicitly 50 via the `pytest_path` attribute on the `pyext` taskgen. 51 52 For details c.f. the pytest playground examples. 53 54 55For example:: 56 57 # A standalone Python C extension that demonstrates unit test environment population 58 # of PYTHONPATH and LD_LIBRARY_PATH/PATH/DYLD_LIBRARY_PATH. 59 # 60 # Note: `pytest_path` is provided here because pytest cannot automatically determine 61 # if the extension is part of another Python package or is used standalone. 62 bld(name = 'foo_ext', 63 features = 'c cshlib pyext', 64 source = 'src/foo_ext.c', 65 target = 'foo_ext', 66 pytest_path = [ bld.path.get_bld() ]) 67 68 # Python package under test that also depend on the Python module `foo_ext` 69 # 70 # Note: `install_from` is added automatically to `PYTHONPATH`. 71 bld(name = 'foo', 72 features = 'py', 73 use = 'foo_ext', 74 source = bld.path.ant_glob('src/foo/*.py'), 75 install_from = 'src') 76 77 # Unit test example using the built in module unittest and let that discover 78 # any test cases. 79 bld(name = 'foo_test', 80 features = 'pytest', 81 use = 'foo', 82 pytest_source = bld.path.ant_glob('test/*.py'), 83 ut_str = '${PYTHON} -B -m unittest discover') 84 85""" 86 87import os 88from waflib import Task, TaskGen, Errors, Utils, Logs 89from waflib.Tools import ccroot 90 91def _process_use_rec(self, name): 92 """ 93 Recursively process ``use`` for task generator with name ``name``.. 94 Used by pytest_process_use. 95 """ 96 if name in self.pytest_use_not or name in self.pytest_use_seen: 97 return 98 try: 99 tg = self.bld.get_tgen_by_name(name) 100 except Errors.WafError: 101 self.pytest_use_not.add(name) 102 return 103 104 self.pytest_use_seen.append(name) 105 tg.post() 106 107 for n in self.to_list(getattr(tg, 'use', [])): 108 _process_use_rec(self, n) 109 110 111@TaskGen.feature('pytest') 112@TaskGen.after_method('process_source', 'apply_link') 113def pytest_process_use(self): 114 """ 115 Process the ``use`` attribute which contains a list of task generator names and store 116 paths that later is used to populate the unit test runtime environment. 117 """ 118 self.pytest_use_not = set() 119 self.pytest_use_seen = [] 120 self.pytest_paths = [] # strings or Nodes 121 self.pytest_libpaths = [] # strings or Nodes 122 self.pytest_dep_nodes = [] 123 124 names = self.to_list(getattr(self, 'use', [])) 125 for name in names: 126 _process_use_rec(self, name) 127 128 def extend_unique(lst, varlst): 129 ext = [] 130 for x in varlst: 131 if x not in lst: 132 ext.append(x) 133 lst.extend(ext) 134 135 # Collect type specific info needed to construct a valid runtime environment 136 # for the test. 137 for name in self.pytest_use_seen: 138 tg = self.bld.get_tgen_by_name(name) 139 140 extend_unique(self.pytest_paths, Utils.to_list(getattr(tg, 'pytest_path', []))) 141 extend_unique(self.pytest_libpaths, Utils.to_list(getattr(tg, 'pytest_libpath', []))) 142 143 if 'py' in tg.features: 144 # Python dependencies are added to PYTHONPATH 145 pypath = getattr(tg, 'install_from', tg.path) 146 147 if 'buildcopy' in tg.features: 148 # Since buildcopy is used we assume that PYTHONPATH in build should be used, 149 # not source 150 extend_unique(self.pytest_paths, [pypath.get_bld().abspath()]) 151 152 # Add buildcopy output nodes to dependencies 153 extend_unique(self.pytest_dep_nodes, [o for task in getattr(tg, 'tasks', []) \ 154 for o in getattr(task, 'outputs', [])]) 155 else: 156 # If buildcopy is not used, depend on sources instead 157 extend_unique(self.pytest_dep_nodes, tg.source) 158 extend_unique(self.pytest_paths, [pypath.abspath()]) 159 160 if getattr(tg, 'link_task', None): 161 # For tasks with a link_task (C, C++, D et.c.) include their library paths: 162 if not isinstance(tg.link_task, ccroot.stlink_task): 163 extend_unique(self.pytest_dep_nodes, tg.link_task.outputs) 164 extend_unique(self.pytest_libpaths, tg.link_task.env.LIBPATH) 165 166 if 'pyext' in tg.features: 167 # If the taskgen is extending Python we also want to add the interpreter libpath. 168 extend_unique(self.pytest_libpaths, tg.link_task.env.LIBPATH_PYEXT) 169 else: 170 # Only add to libpath if the link task is not a Python extension 171 extend_unique(self.pytest_libpaths, [tg.link_task.outputs[0].parent.abspath()]) 172 173 174@TaskGen.feature('pytest') 175@TaskGen.after_method('pytest_process_use') 176def make_pytest(self): 177 """ 178 Creates a ``utest`` task with a populated environment for Python if not specified in ``ut_env``: 179 180 - Paths in `pytest_paths` attribute are used to populate PYTHONPATH 181 - Paths in `pytest_libpaths` attribute are used to populate the system library path (e.g. LD_LIBRARY_PATH) 182 """ 183 nodes = self.to_nodes(self.pytest_source) 184 tsk = self.create_task('utest', nodes) 185 186 tsk.dep_nodes.extend(self.pytest_dep_nodes) 187 if getattr(self, 'ut_str', None): 188 self.ut_run, lst = Task.compile_fun(self.ut_str, shell=getattr(self, 'ut_shell', False)) 189 tsk.vars = lst + tsk.vars 190 191 if getattr(self, 'ut_cwd', None): 192 if isinstance(self.ut_cwd, str): 193 # we want a Node instance 194 if os.path.isabs(self.ut_cwd): 195 self.ut_cwd = self.bld.root.make_node(self.ut_cwd) 196 else: 197 self.ut_cwd = self.path.make_node(self.ut_cwd) 198 else: 199 if tsk.inputs: 200 self.ut_cwd = tsk.inputs[0].parent 201 else: 202 raise Errors.WafError("no valid input files for pytest task, check pytest_source value") 203 204 if not self.ut_cwd.exists(): 205 self.ut_cwd.mkdir() 206 207 if not hasattr(self, 'ut_env'): 208 self.ut_env = dict(os.environ) 209 def add_paths(var, lst): 210 # Add list of paths to a variable, lst can contain strings or nodes 211 lst = [ str(n) for n in lst ] 212 Logs.debug("ut: %s: Adding paths %s=%s", self, var, lst) 213 self.ut_env[var] = os.pathsep.join(lst) + os.pathsep + self.ut_env.get(var, '') 214 215 # Prepend dependency paths to PYTHONPATH and LD_LIBRARY_PATH 216 add_paths('PYTHONPATH', self.pytest_paths) 217 218 if Utils.is_win32: 219 add_paths('PATH', self.pytest_libpaths) 220 elif Utils.unversioned_sys_platform() == 'darwin': 221 add_paths('DYLD_LIBRARY_PATH', self.pytest_libpaths) 222 add_paths('LD_LIBRARY_PATH', self.pytest_libpaths) 223 else: 224 add_paths('LD_LIBRARY_PATH', self.pytest_libpaths) 225 226