third_party:waf: update to upstream 2.0.4 release
[sfrench/samba-autobuild/.git] / third_party / waf / waflib / Tools / waf_unit_test.py
1 #! /usr/bin/env python
2 # encoding: utf-8
3 # WARNING! Do not edit! https://waf.io/book/index.html#_obtaining_the_waf_file
4
5 #!/usr/bin/env python
6 # encoding: utf-8
7 # Carlos Rafael Giani, 2006
8 # Thomas Nagy, 2010-2018 (ita)
9
10 """
11 Unit testing system for C/C++/D and interpreted languages providing test execution:
12
13 * in parallel, by using ``waf -j``
14 * partial (only the tests that have changed) or full (by using ``waf --alltests``)
15
16 The tests are declared by adding the **test** feature to programs::
17
18         def options(opt):
19                 opt.load('compiler_cxx waf_unit_test')
20         def configure(conf):
21                 conf.load('compiler_cxx waf_unit_test')
22         def build(bld):
23                 bld(features='cxx cxxprogram test', source='main.cpp', target='app')
24                 # or
25                 bld.program(features='test', source='main2.cpp', target='app2')
26
27 When the build is executed, the program 'test' will be built and executed without arguments.
28 The success/failure is detected by looking at the return code. The status and the standard output/error
29 are stored on the build context.
30
31 The results can be displayed by registering a callback function. Here is how to call
32 the predefined callback::
33
34         def build(bld):
35                 bld(features='cxx cxxprogram test', source='main.c', target='app')
36                 from waflib.Tools import waf_unit_test
37                 bld.add_post_fun(waf_unit_test.summary)
38
39 By passing --dump-test-scripts the build outputs corresponding python files
40 (with extension _run.py) that are useful for debugging purposes.
41 """
42
43 import os, shlex, sys
44 from waflib.TaskGen import feature, after_method, taskgen_method
45 from waflib import Utils, Task, Logs, Options
46 from waflib.Tools import ccroot
47 testlock = Utils.threading.Lock()
48
49 SCRIPT_TEMPLATE = """#! %(python)s
50 import subprocess, sys
51 cmd = %(cmd)r
52 # if you want to debug with gdb:
53 #cmd = ['gdb', '-args'] + cmd
54 env = %(env)r
55 status = subprocess.call(cmd, env=env, cwd=%(cwd)r, shell=isinstance(cmd, str))
56 sys.exit(status)
57 """
58
59 @taskgen_method
60 def handle_ut_cwd(self, key):
61         """
62         Task generator method, used internally to limit code duplication.
63         This method may disappear anytime.
64         """
65         cwd = getattr(self, key, None)
66         if cwd:
67                 if isinstance(cwd, str):
68                         # we want a Node instance
69                         if os.path.isabs(cwd):
70                                 self.ut_cwd = self.bld.root.make_node(cwd)
71                         else:
72                                 self.ut_cwd = self.path.make_node(cwd)
73
74 @feature('test_scripts')
75 def make_interpreted_test(self):
76         """Create interpreted unit tests."""
77         for x in ['test_scripts_source', 'test_scripts_template']:
78                 if not hasattr(self, x):
79                         Logs.warn('a test_scripts taskgen i missing %s' % x)
80                         return
81
82         self.ut_run, lst = Task.compile_fun(self.test_scripts_template, shell=getattr(self, 'test_scripts_shell', False))
83
84         script_nodes = self.to_nodes(self.test_scripts_source)
85         for script_node in script_nodes:
86                 tsk = self.create_task('utest', [script_node])
87                 tsk.vars = lst + tsk.vars
88                 tsk.env['SCRIPT'] = script_node.path_from(tsk.get_cwd())
89
90         self.handle_ut_cwd('test_scripts_cwd')
91
92         env = getattr(self, 'test_scripts_env', None)
93         if env:
94                 self.ut_env = env
95         else:
96                 self.ut_env = dict(os.environ)
97
98         paths = getattr(self, 'test_scripts_paths', {})
99         for (k,v) in paths.items():
100                 p = self.ut_env.get(k, '').split(os.pathsep)
101                 if isinstance(v, str):
102                         v = v.split(os.pathsep)
103                 self.ut_env[k] = os.pathsep.join(p + v)
104
105 @feature('test')
106 @after_method('apply_link', 'process_use')
107 def make_test(self):
108         """Create the unit test task. There can be only one unit test task by task generator."""
109         if not getattr(self, 'link_task', None):
110                 return
111
112         tsk = self.create_task('utest', self.link_task.outputs)
113         if getattr(self, 'ut_str', None):
114                 self.ut_run, lst = Task.compile_fun(self.ut_str, shell=getattr(self, 'ut_shell', False))
115                 tsk.vars = lst + tsk.vars
116
117         self.handle_ut_cwd('ut_cwd')
118
119         if not hasattr(self, 'ut_paths'):
120                 paths = []
121                 for x in self.tmp_use_sorted:
122                         try:
123                                 y = self.bld.get_tgen_by_name(x).link_task
124                         except AttributeError:
125                                 pass
126                         else:
127                                 if not isinstance(y, ccroot.stlink_task):
128                                         paths.append(y.outputs[0].parent.abspath())
129                 self.ut_paths = os.pathsep.join(paths) + os.pathsep
130
131         if not hasattr(self, 'ut_env'):
132                 self.ut_env = dct = dict(os.environ)
133                 def add_path(var):
134                         dct[var] = self.ut_paths + dct.get(var,'')
135                 if Utils.is_win32:
136                         add_path('PATH')
137                 elif Utils.unversioned_sys_platform() == 'darwin':
138                         add_path('DYLD_LIBRARY_PATH')
139                         add_path('LD_LIBRARY_PATH')
140                 else:
141                         add_path('LD_LIBRARY_PATH')
142
143         if not hasattr(self, 'ut_cmd'):
144                 self.ut_cmd = getattr(Options.options, 'testcmd', False)
145
146 @taskgen_method
147 def add_test_results(self, tup):
148         """Override and return tup[1] to interrupt the build immediately if a test does not run"""
149         Logs.debug("ut: %r", tup)
150         try:
151                 self.utest_results.append(tup)
152         except AttributeError:
153                 self.utest_results = [tup]
154         try:
155                 self.bld.utest_results.append(tup)
156         except AttributeError:
157                 self.bld.utest_results = [tup]
158
159 class utest(Task.Task):
160         """
161         Execute a unit test
162         """
163         color = 'PINK'
164         after = ['vnum', 'inst']
165         vars = []
166
167         def runnable_status(self):
168                 """
169                 Always execute the task if `waf --alltests` was used or no
170                 tests if ``waf --notests`` was used
171                 """
172                 if getattr(Options.options, 'no_tests', False):
173                         return Task.SKIP_ME
174
175                 ret = super(utest, self).runnable_status()
176                 if ret == Task.SKIP_ME:
177                         if getattr(Options.options, 'all_tests', False):
178                                 return Task.RUN_ME
179                 return ret
180
181         def get_test_env(self):
182                 """
183                 In general, tests may require any library built anywhere in the project.
184                 Override this method if fewer paths are needed
185                 """
186                 return self.generator.ut_env
187
188         def post_run(self):
189                 super(utest, self).post_run()
190                 if getattr(Options.options, 'clear_failed_tests', False) and self.waf_unit_test_results[1]:
191                         self.generator.bld.task_sigs[self.uid()] = None
192
193         def run(self):
194                 """
195                 Execute the test. The execution is always successful, and the results
196                 are stored on ``self.generator.bld.utest_results`` for postprocessing.
197
198                 Override ``add_test_results`` to interrupt the build
199                 """
200                 if hasattr(self.generator, 'ut_run'):
201                         return self.generator.ut_run(self)
202
203                 self.ut_exec = getattr(self.generator, 'ut_exec', [self.inputs[0].abspath()])
204                 ut_cmd = getattr(self.generator, 'ut_cmd', False)
205                 if ut_cmd:
206                         self.ut_exec = shlex.split(ut_cmd % ' '.join(self.ut_exec))
207
208                 return self.exec_command(self.ut_exec)
209
210         def exec_command(self, cmd, **kw):
211                 Logs.debug('runner: %r', cmd)
212                 if getattr(Options.options, 'dump_test_scripts', False):
213                         script_code = SCRIPT_TEMPLATE % {
214                                 'python': sys.executable,
215                                 'env': self.get_test_env(),
216                                 'cwd': self.get_cwd().abspath(),
217                                 'cmd': cmd
218                         }
219                         script_file = self.inputs[0].abspath() + '_run.py'
220                         Utils.writef(script_file, script_code)
221                         os.chmod(script_file, Utils.O755)
222                         if Logs.verbose > 1:
223                                 Logs.info('Test debug file written as %r' % script_file)
224
225                 proc = Utils.subprocess.Popen(cmd, cwd=self.get_cwd().abspath(), env=self.get_test_env(),
226                         stderr=Utils.subprocess.PIPE, stdout=Utils.subprocess.PIPE, shell=isinstance(cmd,str))
227                 (stdout, stderr) = proc.communicate()
228                 self.waf_unit_test_results = tup = (self.inputs[0].abspath(), proc.returncode, stdout, stderr)
229                 testlock.acquire()
230                 try:
231                         return self.generator.add_test_results(tup)
232                 finally:
233                         testlock.release()
234
235         def get_cwd(self):
236                 return getattr(self.generator, 'ut_cwd', self.inputs[0].parent)
237
238         def sig_explicit_deps(self):
239                 lst = [os.stat(node.abspath()).st_mtime for node in self.inputs]
240                 self.m.update(str(lst))
241                 return super(utest, self).sig_explicit_deps()
242
243 def summary(bld):
244         """
245         Display an execution summary::
246
247                 def build(bld):
248                         bld(features='cxx cxxprogram test', source='main.c', target='app')
249                         from waflib.Tools import waf_unit_test
250                         bld.add_post_fun(waf_unit_test.summary)
251         """
252         lst = getattr(bld, 'utest_results', [])
253         if lst:
254                 Logs.pprint('CYAN', 'execution summary')
255
256                 total = len(lst)
257                 tfail = len([x for x in lst if x[1]])
258
259                 Logs.pprint('GREEN', '  tests that pass %d/%d' % (total-tfail, total))
260                 for (f, code, out, err) in lst:
261                         if not code:
262                                 Logs.pprint('GREEN', '    %s' % f)
263
264                 Logs.pprint('GREEN' if tfail == 0 else 'RED', '  tests that fail %d/%d' % (tfail, total))
265                 for (f, code, out, err) in lst:
266                         if code:
267                                 Logs.pprint('RED', '    %s' % f)
268
269 def set_exit_code(bld):
270         """
271         If any of the tests fail waf will exit with that exit code.
272         This is useful if you have an automated build system which need
273         to report on errors from the tests.
274         You may use it like this:
275
276                 def build(bld):
277                         bld(features='cxx cxxprogram test', source='main.c', target='app')
278                         from waflib.Tools import waf_unit_test
279                         bld.add_post_fun(waf_unit_test.set_exit_code)
280         """
281         lst = getattr(bld, 'utest_results', [])
282         for (f, code, out, err) in lst:
283                 if code:
284                         msg = []
285                         if out:
286                                 msg.append('stdout:%s%s' % (os.linesep, out.decode('utf-8')))
287                         if err:
288                                 msg.append('stderr:%s%s' % (os.linesep, err.decode('utf-8')))
289                         bld.fatal(os.linesep.join(msg))
290
291
292 def options(opt):
293         """
294         Provide the ``--alltests``, ``--notests`` and ``--testcmd`` command-line options.
295         """
296         opt.add_option('--notests', action='store_true', default=False, help='Exec no unit tests', dest='no_tests')
297         opt.add_option('--alltests', action='store_true', default=False, help='Exec all unit tests', dest='all_tests')
298         opt.add_option('--clear-failed', action='store_true', default=False,
299                 help='Force failed unit tests to run again next time', dest='clear_failed_tests')
300         opt.add_option('--testcmd', action='store', default=False, dest='testcmd',
301                 help='Run the unit tests using the test-cmd string example "--testcmd="valgrind --error-exitcode=1 %s" to run under valgrind')
302         opt.add_option('--dump-test-scripts', action='store_true', default=False,
303                 help='Create python scripts to help debug tests', dest='dump_test_scripts')
304