waf

FORK: waf with some random patches
git clone https://git.neptards.moe/neptards/waf.git
Log | Files | Refs | README

pytest.py (8866B)


      1 #! /usr/bin/env python
      2 # encoding: utf-8
      3 # Calle Rosenquist, 2016-2018 (xbreak)
      4 
      5 """
      6 Provides Python unit test support using :py:class:`waflib.Tools.waf_unit_test.utest`
      7 task via the **pytest** feature.
      8 
      9 To use pytest the following is needed:
     10 
     11 1. Load `pytest` and the dependency `waf_unit_test` tools.
     12 2. 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 
     27 The `use` dependencies are used for both update calculation and to populate
     28 the following environment variables for the `pytest` test runner:
     29 
     30 1. `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 
     38 2. 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 
     43 3. Java class search path (CLASSPATH) of any Java/Javalike dependency
     44 
     45 Note: `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 
     57 For 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 
     89 import os
     90 from waflib import Task, TaskGen, Errors, Utils, Logs
     91 from waflib.Tools import ccroot
     92 
     93 def _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')
    115 def 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')
    190 def 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