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