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