3 # Calle Rosenquist, 2016-2018 (xbreak)
6 Provides Python unit test support using :py:class:`waflib.Tools.waf_unit_test.utest`
7 task via the **pytest** feature.
9 To use pytest the following is needed:
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:
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.
22 Additionally the following `pytest` specific attributes are used in dependent taskgens:
24 - `pytest_path`: Node or string list of additional Python paths.
25 - `pytest_libpath`: Node or string list of additional library paths.
27 The `use` dependencies are used for both update calculation and to populate
28 the following environment variables for the `pytest` test runner:
30 1. `PYTHONPATH` (`sys.path`) of any dependent taskgen that has the feature `py`:
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
36 - `pytest_path` attribute is used to manually specify additional Python paths.
38 2. Dynamic linker search path variable (e.g. `LD_LIBRARY_PATH`) of any dependent taskgen with
41 - `pytest_libpath` attribute is used to manually specify additional linker paths.
43 3. Java class search path (CLASSPATH) of any Java/Javalike dependency
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:
48 - When used as part of another `py` package, the `PYTHONPATH` is provided by
49 that taskgen so no additional action is required.
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.
54 For details c.f. the pytest playground examples.
59 # A standalone Python C extension that demonstrates unit test environment population
60 # of PYTHONPATH and LD_LIBRARY_PATH/PATH/DYLD_LIBRARY_PATH.
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.
65 features = 'c cshlib pyext',
66 source = 'src/foo_ext.c',
68 pytest_path = [ bld.path.get_bld() ])
70 # Python package under test that also depend on the Python module `foo_ext`
72 # Note: `install_from` is added automatically to `PYTHONPATH`.
76 source = bld.path.ant_glob('src/foo/*.py'),
79 # Unit test example using the built in module unittest and let that discover
81 bld(name = 'foo_test',
84 pytest_source = bld.path.ant_glob('test/*.py'),
85 ut_str = '${PYTHON} -B -m unittest discover')
90 from waflib
import Task
, TaskGen
, Errors
, Utils
, Logs
91 from waflib
.Tools
import ccroot
93 def _process_use_rec(self
, name
):
95 Recursively process ``use`` for task generator with name ``name``..
96 Used by pytest_process_use.
98 if name
in self
.pytest_use_not
or name
in self
.pytest_use_seen
:
101 tg
= self
.bld
.get_tgen_by_name(name
)
102 except Errors
.WafError
:
103 self
.pytest_use_not
.add(name
)
106 self
.pytest_use_seen
.append(name
)
109 for n
in self
.to_list(getattr(tg
, 'use', [])):
110 _process_use_rec(self
, n
)
113 @TaskGen.feature('pytest')
114 @TaskGen.after_method('process_source', 'apply_link')
115 def pytest_process_use(self
):
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.
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
= []
127 names
= self
.to_list(getattr(self
, 'use', []))
129 _process_use_rec(self
, name
)
131 def extend_unique(lst
, varlst
):
138 # Collect type specific info needed to construct a valid runtime environment
140 for name
in self
.pytest_use_seen
:
141 tg
= self
.bld
.get_tgen_by_name(name
)
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', [])))
146 if 'py' in tg
.features
:
147 # Python dependencies are added to PYTHONPATH
148 pypath
= getattr(tg
, 'install_from', tg
.path
)
150 if 'buildcopy' in tg
.features
:
151 # Since buildcopy is used we assume that PYTHONPATH in build should be used,
153 extend_unique(self
.pytest_paths
, [pypath
.get_bld().abspath()])
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', [])])
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()])
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()])
168 extend_unique(self
.pytest_javapaths
, [tg
.path
.get_bld()])
170 # And add respective dependencies if present
172 extend_unique(self
.pytest_javapaths
, tg
.use_lst
)
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
)
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
)
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()])
188 @TaskGen.feature('pytest')
189 @TaskGen.after_method('pytest_process_use')
190 def make_pytest(self
):
192 Creates a ``utest`` task with a populated environment for Python if not specified in ``ut_env``:
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)
197 nodes
= self
.to_nodes(self
.pytest_source
)
198 tsk
= self
.create_task('utest', nodes
)
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
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
)
211 self
.ut_cwd
= self
.path
.make_node(self
.ut_cwd
)
214 self
.ut_cwd
= tsk
.inputs
[0].parent
216 raise Errors
.WafError("no valid input files for pytest task, check pytest_source value")
218 if not self
.ut_cwd
.exists():
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
, '')
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
)
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
)
239 add_paths('LD_LIBRARY_PATH', self
.pytest_libpaths
)