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 433. Java class search path (CLASSPATH) of any Java/Javalike dependency 44 45Note: `pytest` cannot automatically determine the correct `PYTHONPATH` for `pyext` taskgens 46 because the extension might be part of a Python package or used standalone: 47 48 - When used as part of another `py` package, the `PYTHONPATH` is provided by 49 that taskgen so no additional action is required. 50 51 - When used as a standalone module, the user needs to specify the `PYTHONPATH` explicitly 52 via the `pytest_path` attribute on the `pyext` taskgen. 53 54 For details c.f. the pytest playground examples. 55 56 57For example:: 58 59 # A standalone Python C extension that demonstrates unit test environment population 60 # of PYTHONPATH and LD_LIBRARY_PATH/PATH/DYLD_LIBRARY_PATH. 61 # 62 # Note: `pytest_path` is provided here because pytest cannot automatically determine 63 # if the extension is part of another Python package or is used standalone. 64 bld(name = 'foo_ext', 65 features = 'c cshlib pyext', 66 source = 'src/foo_ext.c', 67 target = 'foo_ext', 68 pytest_path = [ bld.path.get_bld() ]) 69 70 # Python package under test that also depend on the Python module `foo_ext` 71 # 72 # Note: `install_from` is added automatically to `PYTHONPATH`. 73 bld(name = 'foo', 74 features = 'py', 75 use = 'foo_ext', 76 source = bld.path.ant_glob('src/foo/*.py'), 77 install_from = 'src') 78 79 # Unit test example using the built in module unittest and let that discover 80 # any test cases. 81 bld(name = 'foo_test', 82 features = 'pytest', 83 use = 'foo', 84 pytest_source = bld.path.ant_glob('test/*.py'), 85 ut_str = '${PYTHON} -B -m unittest discover') 86 87""" 88 89import os 90from waflib import Task, TaskGen, Errors, Utils, Logs 91from waflib.Tools import ccroot 92 93def _process_use_rec(self, name): 94 """ 95 Recursively process ``use`` for task generator with name ``name``.. 96 Used by pytest_process_use. 97 """ 98 if name in self.pytest_use_not or name in self.pytest_use_seen: 99 return 100 try: 101 tg = self.bld.get_tgen_by_name(name) 102 except Errors.WafError: 103 self.pytest_use_not.add(name) 104 return 105 106 self.pytest_use_seen.append(name) 107 tg.post() 108 109 for n in self.to_list(getattr(tg, 'use', [])): 110 _process_use_rec(self, n) 111 112 113@TaskGen.feature('pytest') 114@TaskGen.after_method('process_source', 'apply_link') 115def pytest_process_use(self): 116 """ 117 Process the ``use`` attribute which contains a list of task generator names and store 118 paths that later is used to populate the unit test runtime environment. 119 """ 120 self.pytest_use_not = set() 121 self.pytest_use_seen = [] 122 self.pytest_paths = [] # strings or Nodes 123 self.pytest_libpaths = [] # strings or Nodes 124 self.pytest_javapaths = [] # strings or Nodes 125 self.pytest_dep_nodes = [] 126 127 names = self.to_list(getattr(self, 'use', [])) 128 for name in names: 129 _process_use_rec(self, name) 130 131 def extend_unique(lst, varlst): 132 ext = [] 133 for x in varlst: 134 if x not in lst: 135 ext.append(x) 136 lst.extend(ext) 137 138 # Collect type specific info needed to construct a valid runtime environment 139 # for the test. 140 for name in self.pytest_use_seen: 141 tg = self.bld.get_tgen_by_name(name) 142 143 extend_unique(self.pytest_paths, Utils.to_list(getattr(tg, 'pytest_path', []))) 144 extend_unique(self.pytest_libpaths, Utils.to_list(getattr(tg, 'pytest_libpath', []))) 145 146 if 'py' in tg.features: 147 # Python dependencies are added to PYTHONPATH 148 pypath = getattr(tg, 'install_from', tg.path) 149 150 if 'buildcopy' in tg.features: 151 # Since buildcopy is used we assume that PYTHONPATH in build should be used, 152 # not source 153 extend_unique(self.pytest_paths, [pypath.get_bld().abspath()]) 154 155 # Add buildcopy output nodes to dependencies 156 extend_unique(self.pytest_dep_nodes, [o for task in getattr(tg, 'tasks', []) \ 157 for o in getattr(task, 'outputs', [])]) 158 else: 159 # If buildcopy is not used, depend on sources instead 160 extend_unique(self.pytest_dep_nodes, tg.source) 161 extend_unique(self.pytest_paths, [pypath.abspath()]) 162 163 if 'javac' in tg.features: 164 # If a JAR is generated point to that, otherwise to directory 165 if getattr(tg, 'jar_task', None): 166 extend_unique(self.pytest_javapaths, [tg.jar_task.outputs[0].abspath()]) 167 else: 168 extend_unique(self.pytest_javapaths, [tg.path.get_bld()]) 169 170 # And add respective dependencies if present 171 if tg.use_lst: 172 extend_unique(self.pytest_javapaths, tg.use_lst) 173 174 if getattr(tg, 'link_task', None): 175 # For tasks with a link_task (C, C++, D et.c.) include their library paths: 176 if not isinstance(tg.link_task, ccroot.stlink_task): 177 extend_unique(self.pytest_dep_nodes, tg.link_task.outputs) 178 extend_unique(self.pytest_libpaths, tg.link_task.env.LIBPATH) 179 180 if 'pyext' in tg.features: 181 # If the taskgen is extending Python we also want to add the interpreter libpath. 182 extend_unique(self.pytest_libpaths, tg.link_task.env.LIBPATH_PYEXT) 183 else: 184 # Only add to libpath if the link task is not a Python extension 185 extend_unique(self.pytest_libpaths, [tg.link_task.outputs[0].parent.abspath()]) 186 187 188@TaskGen.feature('pytest') 189@TaskGen.after_method('pytest_process_use') 190def make_pytest(self): 191 """ 192 Creates a ``utest`` task with a populated environment for Python if not specified in ``ut_env``: 193 194 - Paths in `pytest_paths` attribute are used to populate PYTHONPATH 195 - Paths in `pytest_libpaths` attribute are used to populate the system library path (e.g. LD_LIBRARY_PATH) 196 """ 197 nodes = self.to_nodes(self.pytest_source) 198 tsk = self.create_task('utest', nodes) 199 200 tsk.dep_nodes.extend(self.pytest_dep_nodes) 201 if getattr(self, 'ut_str', None): 202 self.ut_run, lst = Task.compile_fun(self.ut_str, shell=getattr(self, 'ut_shell', False)) 203 tsk.vars = lst + tsk.vars 204 205 if getattr(self, 'ut_cwd', None): 206 if isinstance(self.ut_cwd, str): 207 # we want a Node instance 208 if os.path.isabs(self.ut_cwd): 209 self.ut_cwd = self.bld.root.make_node(self.ut_cwd) 210 else: 211 self.ut_cwd = self.path.make_node(self.ut_cwd) 212 else: 213 if tsk.inputs: 214 self.ut_cwd = tsk.inputs[0].parent 215 else: 216 raise Errors.WafError("no valid input files for pytest task, check pytest_source value") 217 218 if not self.ut_cwd.exists(): 219 self.ut_cwd.mkdir() 220 221 if not hasattr(self, 'ut_env'): 222 self.ut_env = dict(os.environ) 223 def add_paths(var, lst): 224 # Add list of paths to a variable, lst can contain strings or nodes 225 lst = [ str(n) for n in lst ] 226 Logs.debug("ut: %s: Adding paths %s=%s", self, var, lst) 227 self.ut_env[var] = os.pathsep.join(lst) + os.pathsep + self.ut_env.get(var, '') 228 229 # Prepend dependency paths to PYTHONPATH, CLASSPATH and LD_LIBRARY_PATH 230 add_paths('PYTHONPATH', self.pytest_paths) 231 add_paths('CLASSPATH', self.pytest_javapaths) 232 233 if Utils.is_win32: 234 add_paths('PATH', self.pytest_libpaths) 235 elif Utils.unversioned_sys_platform() == 'darwin': 236 add_paths('DYLD_LIBRARY_PATH', self.pytest_libpaths) 237 add_paths('LD_LIBRARY_PATH', self.pytest_libpaths) 238 else: 239 add_paths('LD_LIBRARY_PATH', self.pytest_libpaths) 240 241