Blame third_party/waf/waflib/extras/pytest.py

rpm-build 95f51c
#! /usr/bin/env python
rpm-build 95f51c
# encoding: utf-8
rpm-build 95f51c
# Calle Rosenquist, 2016-2018 (xbreak)
rpm-build 95f51c
rpm-build 95f51c
"""
rpm-build 95f51c
Provides Python unit test support using :py:class:`waflib.Tools.waf_unit_test.utest`
rpm-build 95f51c
task via the **pytest** feature.
rpm-build 95f51c
rpm-build 95f51c
To use pytest the following is needed:
rpm-build 95f51c
rpm-build 95f51c
1. Load `pytest` and the dependency `waf_unit_test` tools.
rpm-build 95f51c
2. Create a task generator with feature `pytest` (not `test`) and customize behaviour with
rpm-build 95f51c
   the following attributes:
rpm-build 95f51c
rpm-build 95f51c
   - `pytest_source`: Test input files.
rpm-build 95f51c
   - `ut_str`: Test runner command, e.g. ``${PYTHON} -B -m unittest discover`` or
rpm-build 95f51c
               if nose is used: ``${NOSETESTS} --no-byte-compile ${SRC}``.
rpm-build 95f51c
   - `ut_shell`: Determines if ``ut_str`` is executed in a shell. Default: False.
rpm-build 95f51c
   - `ut_cwd`: Working directory for test runner. Defaults to directory of
rpm-build 95f51c
               first ``pytest_source`` file.
rpm-build 95f51c
rpm-build 95f51c
   Additionally the following `pytest` specific attributes are used in dependent taskgens:
rpm-build 95f51c
rpm-build 95f51c
   - `pytest_path`: Node or string list of additional Python paths.
rpm-build 95f51c
   - `pytest_libpath`: Node or string list of additional library paths.
rpm-build 95f51c
rpm-build 95f51c
The `use` dependencies are used for both update calculation and to populate
rpm-build 95f51c
the following environment variables for the `pytest` test runner:
rpm-build 95f51c
rpm-build 95f51c
1. `PYTHONPATH` (`sys.path`) of any dependent taskgen that has the feature `py`:
rpm-build 95f51c
rpm-build 95f51c
   - `install_from` attribute is used to determine where the root of the Python sources
rpm-build 95f51c
      are located. If `install_from` is not specified the default is to use the taskgen path
rpm-build 95f51c
      as the root.
rpm-build 95f51c
rpm-build 95f51c
   - `pytest_path` attribute is used to manually specify additional Python paths.
rpm-build 95f51c
rpm-build 95f51c
2. Dynamic linker search path variable (e.g. `LD_LIBRARY_PATH`) of any dependent taskgen with
rpm-build 95f51c
   non-static link_task.
rpm-build 95f51c
rpm-build 95f51c
   - `pytest_libpath` attribute is used to manually specify additional linker paths.
rpm-build 95f51c
rpm-build 95f51c
Note: `pytest` cannot automatically determine the correct `PYTHONPATH` for `pyext` taskgens
rpm-build 95f51c
      because the extension might be part of a Python package or used standalone:
rpm-build 95f51c
rpm-build 95f51c
      - When used as part of another `py` package, the `PYTHONPATH` is provided by
rpm-build 95f51c
      that taskgen so no additional action is required.
rpm-build 95f51c
rpm-build 95f51c
      - When used as a standalone module, the user needs to specify the `PYTHONPATH` explicitly
rpm-build 95f51c
      via the `pytest_path` attribute on the `pyext` taskgen.
rpm-build 95f51c
rpm-build 95f51c
      For details c.f. the pytest playground examples.
rpm-build 95f51c
rpm-build 95f51c
rpm-build 95f51c
For example::
rpm-build 95f51c
rpm-build 95f51c
    # A standalone Python C extension that demonstrates unit test environment population
rpm-build 95f51c
    # of PYTHONPATH and LD_LIBRARY_PATH/PATH/DYLD_LIBRARY_PATH.
rpm-build 95f51c
    #
rpm-build 95f51c
    # Note: `pytest_path` is provided here because pytest cannot automatically determine
rpm-build 95f51c
    # if the extension is part of another Python package or is used standalone.
rpm-build 95f51c
    bld(name         = 'foo_ext',
rpm-build 95f51c
        features     = 'c cshlib pyext',
rpm-build 95f51c
        source       = 'src/foo_ext.c',
rpm-build 95f51c
        target       = 'foo_ext',
rpm-build 95f51c
        pytest_path  = [ bld.path.get_bld() ])
rpm-build 95f51c
rpm-build 95f51c
    # Python package under test that also depend on the Python module `foo_ext`
rpm-build 95f51c
    #
rpm-build 95f51c
    # Note: `install_from` is added automatically to `PYTHONPATH`.
rpm-build 95f51c
    bld(name         = 'foo',
rpm-build 95f51c
        features     = 'py',
rpm-build 95f51c
        use          = 'foo_ext',
rpm-build 95f51c
        source       = bld.path.ant_glob('src/foo/*.py'),
rpm-build 95f51c
        install_from = 'src')
rpm-build 95f51c
rpm-build 95f51c
    # Unit test example using the built in module unittest and let that discover
rpm-build 95f51c
    # any test cases.
rpm-build 95f51c
    bld(name          = 'foo_test',
rpm-build 95f51c
        features      = 'pytest',
rpm-build 95f51c
        use           = 'foo',
rpm-build 95f51c
        pytest_source = bld.path.ant_glob('test/*.py'),
rpm-build 95f51c
        ut_str        = '${PYTHON} -B -m unittest discover')
rpm-build 95f51c
rpm-build 95f51c
"""
rpm-build 95f51c
rpm-build 95f51c
import os
rpm-build 95f51c
from waflib import Task, TaskGen, Errors, Utils, Logs
rpm-build 95f51c
from waflib.Tools import ccroot
rpm-build 95f51c
rpm-build 95f51c
def _process_use_rec(self, name):
rpm-build 95f51c
	"""
rpm-build 95f51c
	Recursively process ``use`` for task generator with name ``name``..
rpm-build 95f51c
	Used by pytest_process_use.
rpm-build 95f51c
	"""
rpm-build 95f51c
	if name in self.pytest_use_not or name in self.pytest_use_seen:
rpm-build 95f51c
		return
rpm-build 95f51c
	try:
rpm-build 95f51c
		tg = self.bld.get_tgen_by_name(name)
rpm-build 95f51c
	except Errors.WafError:
rpm-build 95f51c
		self.pytest_use_not.add(name)
rpm-build 95f51c
		return
rpm-build 95f51c
rpm-build 95f51c
	self.pytest_use_seen.append(name)
rpm-build 95f51c
	tg.post()
rpm-build 95f51c
rpm-build 95f51c
	for n in self.to_list(getattr(tg, 'use', [])):
rpm-build 95f51c
		_process_use_rec(self, n)
rpm-build 95f51c
rpm-build 95f51c
rpm-build 95f51c
@TaskGen.feature('pytest')
rpm-build 95f51c
@TaskGen.after_method('process_source', 'apply_link')
rpm-build 95f51c
def pytest_process_use(self):
rpm-build 95f51c
	"""
rpm-build 95f51c
	Process the ``use`` attribute which contains a list of task generator names and store
rpm-build 95f51c
	paths that later is used to populate the unit test runtime environment.
rpm-build 95f51c
	"""
rpm-build 95f51c
	self.pytest_use_not = set()
rpm-build 95f51c
	self.pytest_use_seen = []
rpm-build 95f51c
	self.pytest_paths = [] # strings or Nodes
rpm-build 95f51c
	self.pytest_libpaths = [] # strings or Nodes
rpm-build 95f51c
	self.pytest_dep_nodes = []
rpm-build 95f51c
rpm-build 95f51c
	names = self.to_list(getattr(self, 'use', []))
rpm-build 95f51c
	for name in names:
rpm-build 95f51c
		_process_use_rec(self, name)
rpm-build 95f51c
	
rpm-build 95f51c
	def extend_unique(lst, varlst):
rpm-build 95f51c
		ext = []
rpm-build 95f51c
		for x in varlst:
rpm-build 95f51c
			if x not in lst:
rpm-build 95f51c
				ext.append(x)
rpm-build 95f51c
		lst.extend(ext)
rpm-build 95f51c
rpm-build 95f51c
	# Collect type specific info needed to construct a valid runtime environment
rpm-build 95f51c
	# for the test.
rpm-build 95f51c
	for name in self.pytest_use_seen:
rpm-build 95f51c
		tg = self.bld.get_tgen_by_name(name)
rpm-build 95f51c
rpm-build 95f51c
		extend_unique(self.pytest_paths, Utils.to_list(getattr(tg, 'pytest_path', [])))
rpm-build 95f51c
		extend_unique(self.pytest_libpaths, Utils.to_list(getattr(tg, 'pytest_libpath', [])))
rpm-build 95f51c
rpm-build 95f51c
		if 'py' in tg.features:
rpm-build 95f51c
			# Python dependencies are added to PYTHONPATH
rpm-build 95f51c
			pypath = getattr(tg, 'install_from', tg.path)
rpm-build 95f51c
rpm-build 95f51c
			if 'buildcopy' in tg.features:
rpm-build 95f51c
				# Since buildcopy is used we assume that PYTHONPATH in build should be used,
rpm-build 95f51c
				# not source
rpm-build 95f51c
				extend_unique(self.pytest_paths, [pypath.get_bld().abspath()])
rpm-build 95f51c
rpm-build 95f51c
				# Add buildcopy output nodes to dependencies
rpm-build 95f51c
				extend_unique(self.pytest_dep_nodes, [o for task in getattr(tg, 'tasks', []) \
rpm-build 95f51c
														for o in getattr(task, 'outputs', [])])
rpm-build 95f51c
			else:
rpm-build 95f51c
				# If buildcopy is not used, depend on sources instead
rpm-build 95f51c
				extend_unique(self.pytest_dep_nodes, tg.source)
rpm-build 95f51c
				extend_unique(self.pytest_paths, [pypath.abspath()])
rpm-build 95f51c
rpm-build 95f51c
		if getattr(tg, 'link_task', None):
rpm-build 95f51c
			# For tasks with a link_task (C, C++, D et.c.) include their library paths:
rpm-build 95f51c
			if not isinstance(tg.link_task, ccroot.stlink_task):
rpm-build 95f51c
				extend_unique(self.pytest_dep_nodes, tg.link_task.outputs)
rpm-build 95f51c
				extend_unique(self.pytest_libpaths, tg.link_task.env.LIBPATH)
rpm-build 95f51c
rpm-build 95f51c
				if 'pyext' in tg.features:
rpm-build 95f51c
					# If the taskgen is extending Python we also want to add the interpreter libpath.
rpm-build 95f51c
					extend_unique(self.pytest_libpaths, tg.link_task.env.LIBPATH_PYEXT)
rpm-build 95f51c
				else:
rpm-build 95f51c
					# Only add to libpath if the link task is not a Python extension
rpm-build 95f51c
					extend_unique(self.pytest_libpaths, [tg.link_task.outputs[0].parent.abspath()])
rpm-build 95f51c
rpm-build 95f51c
rpm-build 95f51c
@TaskGen.feature('pytest')
rpm-build 95f51c
@TaskGen.after_method('pytest_process_use')
rpm-build 95f51c
def make_pytest(self):
rpm-build 95f51c
	"""
rpm-build 95f51c
	Creates a ``utest`` task with a populated environment for Python if not specified in ``ut_env``:
rpm-build 95f51c
rpm-build 95f51c
	- Paths in `pytest_paths` attribute are used to populate PYTHONPATH
rpm-build 95f51c
	- Paths in `pytest_libpaths` attribute are used to populate the system library path (e.g. LD_LIBRARY_PATH)
rpm-build 95f51c
	"""
rpm-build 95f51c
	nodes = self.to_nodes(self.pytest_source)
rpm-build 95f51c
	tsk = self.create_task('utest', nodes)
rpm-build 95f51c
	
rpm-build 95f51c
	tsk.dep_nodes.extend(self.pytest_dep_nodes)
rpm-build 95f51c
	if getattr(self, 'ut_str', None):
rpm-build 95f51c
		self.ut_run, lst = Task.compile_fun(self.ut_str, shell=getattr(self, 'ut_shell', False))
rpm-build 95f51c
		tsk.vars = lst + tsk.vars
rpm-build 95f51c
rpm-build 95f51c
	if getattr(self, 'ut_cwd', None):
rpm-build 95f51c
		if isinstance(self.ut_cwd, str):
rpm-build 95f51c
			# we want a Node instance
rpm-build 95f51c
			if os.path.isabs(self.ut_cwd):
rpm-build 95f51c
				self.ut_cwd = self.bld.root.make_node(self.ut_cwd)
rpm-build 95f51c
			else:
rpm-build 95f51c
				self.ut_cwd = self.path.make_node(self.ut_cwd)
rpm-build 95f51c
	else:
rpm-build 95f51c
		if tsk.inputs:
rpm-build 95f51c
			self.ut_cwd = tsk.inputs[0].parent
rpm-build 95f51c
		else:
rpm-build 95f51c
			raise Errors.WafError("no valid input files for pytest task, check pytest_source value")
rpm-build 95f51c
rpm-build 95f51c
	if not self.ut_cwd.exists():
rpm-build 95f51c
		self.ut_cwd.mkdir()
rpm-build 95f51c
rpm-build 95f51c
	if not hasattr(self, 'ut_env'):
rpm-build 95f51c
		self.ut_env = dict(os.environ)
rpm-build 95f51c
		def add_paths(var, lst):
rpm-build 95f51c
			# Add list of paths to a variable, lst can contain strings or nodes
rpm-build 95f51c
			lst = [ str(n) for n in lst ]
rpm-build 95f51c
			Logs.debug("ut: %s: Adding paths %s=%s", self, var, lst)
rpm-build 95f51c
			self.ut_env[var] = os.pathsep.join(lst) + os.pathsep + self.ut_env.get(var, '')
rpm-build 95f51c
rpm-build 95f51c
		# Prepend dependency paths to PYTHONPATH and LD_LIBRARY_PATH
rpm-build 95f51c
		add_paths('PYTHONPATH', self.pytest_paths)
rpm-build 95f51c
rpm-build 95f51c
		if Utils.is_win32:
rpm-build 95f51c
			add_paths('PATH', self.pytest_libpaths)
rpm-build 95f51c
		elif Utils.unversioned_sys_platform() == 'darwin':
rpm-build 95f51c
			add_paths('DYLD_LIBRARY_PATH', self.pytest_libpaths)
rpm-build 95f51c
			add_paths('LD_LIBRARY_PATH', self.pytest_libpaths)
rpm-build 95f51c
		else:
rpm-build 95f51c
			add_paths('LD_LIBRARY_PATH', self.pytest_libpaths)
rpm-build 95f51c