libndr: Avoid assigning duplicate versions to symbols
[amitay/samba.git] / third_party / waf / waflib / extras / pytest.py
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