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