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