pytest.py 8.1 KB

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