1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 """
36 rostest implementation of running bare (gtest-compatible) unit test
37 executables. These do not run in a ROS environment.
38 """
39
40 from __future__ import print_function
41
42 import os
43 import cStringIO
44 import unittest
45 import time
46 import signal
47 import subprocess
48 import traceback
49
50 import rospkg
51
52 from .core import xml_results_file, rostest_name_from_path, create_xml_runner, printlog, printerrlog, printlog_bold
53
54 from . import pmon
55 from . import junitxml
56
57 BARE_TIME_LIMIT = 60.
58 TIMEOUT_SIGINT = 15.0
59 TIMEOUT_SIGTERM = 2.0
60
62
64
65 - def __init__(self, exe, args, retry=0, time_limit=None, test_name=None, text_mode=False, package_name=None):
66 """
67 @param exe: path to executable to run
68 @type exe: str
69 @param args: arguments to exe
70 @type args: [str]
71 @type retry: int
72 @param time_limit: (optional) time limit for test. Defaults to BARE_TIME_LIMIT.
73 @type time_limit: float
74 @param test_name: (optional) override automatically generated test name
75 @type test_name: str
76 @param package_name: (optional) override automatically inferred package name
77 @type package_name: str
78 """
79 super(BareTestCase, self).__init__()
80 self.text_mode = text_mode
81 if package_name:
82 self.package = package_name
83 else:
84 self.package = rospkg.get_package_name(exe)
85 self.exe = os.path.abspath(exe)
86 if test_name is None:
87 self.test_name = os.path.basename(exe)
88 else:
89 self.test_name = test_name
90
91
92 if self.exe.endswith('.py'):
93 self.args = ['python', self.exe] + args
94 else:
95 self.args = [self.exe] + args
96 if text_mode:
97 self.args = self.args + ['--text']
98
99 self.retry = retry
100 self.time_limit = time_limit or BARE_TIME_LIMIT
101 self.pmon = None
102 self.results = junitxml.Result(self.test_name)
103
106
111
113 self.failIf(self.package is None, "unable to determine package of executable")
114
115 done = False
116 while not done:
117 test_name = self.test_name
118
119 printlog("Running test [%s]", test_name)
120
121
122
123 test_file = xml_results_file(self.package, test_name, False)
124 if os.path.exists(test_file):
125 printlog("removing previous test results file [%s]", test_file)
126 os.remove(test_file)
127
128 self.args.append('--gtest_output=xml:%s'%test_file)
129
130
131 printlog("running test %s"%test_name)
132 timeout_failure = False
133
134 run_id = None
135
136 process = LocalProcess(run_id, self.package, self.test_name, self.args, os.environ, False, cwd='cwd', is_node=False)
137
138 pm = self.pmon
139 pm.register(process)
140 success = process.start()
141 self.assert_(success, "test failed to start")
142
143
144 timeout_t = time.time() + self.time_limit
145 try:
146 while process.is_alive():
147
148 if time.time() > timeout_t:
149 raise TestTimeoutException("test max time allotted")
150 time.sleep(0.1)
151
152 except TestTimeoutException, e:
153 if self.retry:
154 timeout_failure = True
155 else:
156 raise
157
158 if not timeout_failure:
159 printlog("test [%s] finished"%test_name)
160 else:
161 printerrlog("test [%s] timed out"%test_name)
162
163
164 if self.text_mode:
165 results = self.results
166 elif not self.text_mode:
167
168 if not timeout_failure:
169 self.assert_(os.path.isfile(test_file), "test [%s] did not generate test results"%test_name)
170 printlog("test [%s] results are in [%s]", test_name, test_file)
171 results = junitxml.read(test_file, test_name)
172 test_fail = results.num_errors or results.num_failures
173 else:
174 test_fail = True
175
176 if self.retry > 0 and test_fail:
177 self.retry -= 1
178 printlog("test [%s] failed, retrying. Retries left: %s"%(test_name, self.retry))
179 else:
180 done = True
181 self.results = results
182 printlog("test [%s] results summary: %s errors, %s failures, %s tests",
183 test_name, results.num_errors, results.num_failures, results.num_tests)
184
185 printlog("[ROSTEST] test [%s] done", test_name)
186
187
188
190 """
191 Process launched on local machine
192 """
193
194 - def __init__(self, run_id, package, name, args, env, log_output, respawn=False, required=False, cwd=None, is_node=True):
195 """
196 @param run_id: unique run ID for this roslaunch. Used to
197 generate log directory location. run_id may be None if this
198 feature is not being used.
199 @type run_id: str
200 @param package: name of package process is part of
201 @type package: str
202 @param name: name of process
203 @type name: str
204 @param args: list of arguments to process
205 @type args: [str]
206 @param env: environment dictionary for process
207 @type env: {str : str}
208 @param log_output: if True, log output streams of process
209 @type log_output: bool
210 @param respawn: respawn process if it dies (default is False)
211 @type respawn: bool
212 @param cwd: working directory of process, or None
213 @type cwd: str
214 @param is_node: (optional) if True, process is ROS node and accepts ROS node command-line arguments. Default: True
215 @type is_node: False
216 """
217 super(LocalProcess, self).__init__(package, name, args, env, respawn, required)
218 self.run_id = run_id
219 self.popen = None
220 self.log_output = log_output
221 self.started = False
222 self.stopped = False
223 self.cwd = cwd
224 self.log_dir = None
225 self.pid = -1
226 self.is_node = is_node
227
228
230 """
231 Get all data about this process in dictionary form
232 """
233 info = super(LocalProcess, self).get_info()
234 info['pid'] = self.pid
235 if self.run_id:
236 info['run_id'] = self.run_id
237 info['log_output'] = self.log_output
238 if self.cwd is not None:
239 info['cwd'] = self.cwd
240 return info
241
287
289 """
290 Start the process.
291
292 @raise pmon.FatalProcessLaunch: if process cannot be started and it
293 is not likely to ever succeed
294 """
295 super(LocalProcess, self).start()
296 try:
297 self.lock.acquire()
298 self.started = self.stopped = False
299
300 full_env = self.env
301
302
303 try:
304 logfileout, logfileerr = self._configure_logging()
305 except Exception as e:
306 printerrlog("[%s] ERROR: unable to configure logging [%s]"%(self.name, str(e)))
307
308
309
310 logfileout, logfileerr = subprocess.PIPE, subprocess.PIPE
311
312 if self.cwd == 'node':
313 cwd = os.path.dirname(self.args[0])
314 elif self.cwd == 'cwd':
315 cwd = os.getcwd()
316 elif self.cwd == 'ros-root':
317 cwd = get_ros_root()
318 else:
319 cwd = rospkg.get_ros_home()
320
321 try:
322 self.popen = subprocess.Popen(self.args, cwd=cwd, stdout=logfileout, stderr=logfileerr, env=full_env, close_fds=True, preexec_fn=os.setsid)
323 except OSError, (errno, msg):
324 self.started = True
325 if errno == 8:
326 raise pmon.FatalProcessLaunch("Unable to launch [%s]. \nIf it is a script, you may be missing a '#!' declaration at the top."%self.name)
327 elif errno == 2:
328 raise pmon.FatalProcessLaunch("""Roslaunch got a '%s' error while attempting to run:
329
330 %s
331
332 Please make sure that all the executables in this command exist and have
333 executable permission. This is often caused by a bad launch-prefix."""%(msg, ' '.join(self.args)))
334 else:
335 raise pmon.FatalProcessLaunch("unable to launch [%s]: %s"%(' '.join(self.args), msg))
336
337 self.started = True
338
339
340
341 poll_result = self.popen.poll()
342 if poll_result is None or poll_result == 0:
343 self.pid = self.popen.pid
344 printlog_bold("process[%s]: started with pid [%s]"%(self.name, self.pid))
345 return True
346 else:
347 printerrlog("failed to start local process: %s"%(' '.join(self.args)))
348 return False
349 finally:
350 self.lock.release()
351
353 """
354 @return: True if process is still running
355 @rtype: bool
356 """
357 if not self.started:
358 return True
359 if self.stopped or self.popen is None:
360 return False
361 self.exit_code = self.popen.poll()
362 if self.exit_code is not None:
363 return False
364 return True
365
367 """
368 @return: human-readable description of exit state
369 @rtype: str
370 """
371
372 if self.exit_code is not None:
373 if self.exit_code:
374 if self.log_dir:
375 return 'process has died [pid %s, exit code %s].\nlog files: %s*.log'%(self.pid, self.exit_code, os.path.join(self.log_dir, self.name))
376 else:
377 return 'process has died [pid %s, exit code %s]'%(self.pid, self.exit_code)
378 else:
379 if self.log_dir:
380 return 'process has finished cleanly.\nlog file: %s*.log'%(os.path.join(self.log_dir, self.name))
381 else:
382 return 'process has finished cleanly'
383 else:
384 return 'process has died'
385
387 """
388 UNIX implementation of process killing
389
390 @param errors: error messages. stop() will record messages into this list.
391 @type errors: [str]
392 """
393 self.exit_code = self.popen.poll()
394 if self.exit_code is not None:
395
396 self.popen = None
397 self.stopped = True
398 return
399
400 pid = self.popen.pid
401 pgid = os.getpgid(pid)
402
403 try:
404
405 os.killpg(pgid, signal.SIGINT)
406 timeout_t = time.time() + TIMEOUT_SIGINT
407 retcode = self.popen.poll()
408 while time.time() < timeout_t and retcode is None:
409 time.sleep(0.1)
410 retcode = self.popen.poll()
411
412 if retcode is None:
413 printerrlog("[%s] escalating to SIGTERM"%self.name)
414 timeout_t = time.time() + TIMEOUT_SIGTERM
415 os.killpg(pgid, signal.SIGTERM)
416 retcode = self.popen.poll()
417 while time.time() < timeout_t and retcode is None:
418 time.sleep(0.2)
419 retcode = self.popen.poll()
420 if retcode is None:
421 printerrlog("[%s] escalating to SIGKILL"%self.name)
422 errors.append("process[%s, pid %s]: required SIGKILL. May still be running."%(self.name, pid))
423 try:
424 os.killpg(pgid, signal.SIGKILL)
425
426 except OSError as e:
427 if e.args[0] == 3:
428 printerrlog("no [%s] process with pid [%s]"%(self.name, pid))
429 else:
430 printerrlog("errors shutting down [%s]: %s"%(self.name, e))
431 finally:
432 self.popen = None
433
434 - def stop(self, errors=[]):
435 """
436 Stop the process. Record any significant error messages in the errors parameter
437
438 @param errors: error messages. stop() will record messages into this list.
439 @type errors: [str]
440 """
441 super(LocalProcess, self).stop(errors)
442 self.lock.acquire()
443 try:
444 try:
445 if self.popen is None:
446 return
447
448
449 self._stop_unix(errors)
450 except:
451 printerrlog("[%s] EXCEPTION %s"%(self.name, traceback.format_exc()))
452 finally:
453 self.stopped = True
454 self.lock.release()
455
457 """
458 Print summary of runner results and actual test results to
459 stdout. For rosunit and rostest, the test is wrapped in an
460 external runner. The results from this runner are important if the
461 runner itself has a failure.
462
463 @param runner_result: unittest runner result object
464 @type runner_result: _XMLTestResult
465 @param junit_results: Parsed JUnit test results
466 @type junit_results: rosunit.junitxml.Result
467 """
468
469
470
471
472
473 buff = cStringIO.StringIO()
474
475 buff.write("[%s]"%(runner_name)+'-'*71+'\n\n')
476 for tc_result in junit_results.test_case_results:
477 buff.write(tc_result.description)
478 for tc_result in runner_results.failures:
479 buff.write("[%s][failed]\n"%tc_result[0]._testMethodName)
480
481 buff.write('\nSUMMARY\n')
482 if runner_results.wasSuccessful() and (junit_results.num_errors + junit_results.num_failures) == 0:
483 buff.write("\033[32m * RESULT: SUCCESS\033[0m\n")
484 else:
485 buff.write("\033[1;31m * RESULT: FAIL\033[0m\n")
486
487
488
489
490
491 buff.write(" * TESTS: %s\n"%junit_results.num_tests)
492 num_errors = junit_results.num_errors+len(runner_results.errors)
493 if num_errors:
494 buff.write("\033[1;31m * ERRORS: %s\033[0m\n"%num_errors)
495 else:
496 buff.write(" * ERRORS: 0\n")
497 num_failures = junit_results.num_failures+len(runner_results.failures)
498 if num_failures:
499 buff.write("\033[1;31m * FAILURES: %s\033[0m\n"%num_failures)
500 else:
501 buff.write(" * FAILURES: 0\n")
502
503 if runner_results.failures:
504 buff.write("\nERROR: The following tests failed to run:\n")
505 for tc_result in runner_results.failures:
506 buff.write(" * " +tc_result[0]._testMethodName + "\n")
507
508 print(buff.getvalue())
509
520
522 """
523 Print summary of python unittest result to stdout
524 @param result: test results
525 """
526 buff = cStringIO.StringIO()
527 buff.write("-------------------------------------------------------------\nSUMMARY:\n")
528 if result.wasSuccessful():
529 buff.write("\033[32m * RESULT: SUCCESS\033[0m\n")
530 else:
531 buff.write(" * RESULT: FAIL\n")
532 buff.write(" * TESTS: %s\n"%result.testsRun)
533 buff.write(" * ERRORS: %s [%s]\n"%(len(result.errors), ', '.join(_format_errors(result.errors))))
534 buff.write(" * FAILURES: %s [%s]\n"%(len(result.failures), ', '.join(_format_errors(result.failures))))
535 print(buff.getvalue())
536