creating now rosunit package as a low-level library for rostest that is also compliant with the ros stack

This commit is contained in:
Ken Conley 2010-10-27 01:14:39 +00:00
parent 0641434847
commit 3e611b7fb7
5 changed files with 405 additions and 0 deletions

View File

@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 2.4.6)
include($ENV{ROS_ROOT}/core/rosbuild/rosbuild.cmake)
# Set the build type. Options are:
# Coverage : w/ debug symbols, w/o optimization, w/ code-coverage
# Debug : w/ debug symbols, w/o optimization
# Release : w/o debug symbols, w/ optimization
# RelWithDebInfo : w/ debug symbols, w/ optimization
# MinSizeRel : w/o debug symbols, w/ optimization, stripped binaries
#set(ROS_BUILD_TYPE RelWithDebInfo)
rosbuild_init()
#set the default path for built executables to the "bin" directory
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
#set the default path for built libraries to the "lib" directory
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
#uncomment if you have defined messages
#rosbuild_genmsg()
#uncomment if you have defined services
#rosbuild_gensrv()
#common commands for building c++ executables and libraries
#rosbuild_add_library(${PROJECT_NAME} src/example.cpp)
#target_link_libraries(${PROJECT_NAME} another_library)
#rosbuild_add_boost_directories()
#rosbuild_link_boost(${PROJECT_NAME} thread)
#rosbuild_add_executable(example examples/example.cpp)
#target_link_libraries(example ${PROJECT_NAME})

1
tools/rosunit/Makefile Normal file
View File

@ -0,0 +1 @@
include $(shell rospack find mk)/cmake.mk

View File

@ -0,0 +1,15 @@
<package>
<description brief="Unit testing for ROS">
Unit-testing package for ROS. This is a lower-level library for rostest and handles unit tests, whereas rostest handles integration tests.
</description>
<author>Ken Conley</author>
<license>BSD</license>
<review status="unreviewed" notes=""/>
<url>http://ros.org/wiki/rosunit</url>
<depend package="roslib"/>
</package>

View File

@ -0,0 +1 @@
- builder: epydoc

View File

@ -0,0 +1,358 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: xmlresults.py 11858 2010-10-26 23:32:02Z kwc $
"""
Library for reading and manipulating Ant JUnit XML result files.
"""
import os
import sys
import string
import codecs
import re
from xml.dom.minidom import parse, parseString
from xml.dom import Node as DomNode
import roslib.rosenv
class TestInfo(object):
"""
Common container for 'error' and 'failure' results
"""
def __init__(self, type_, text):
"""
@param type_: type attribute from xml
@type type_: str
@param text: text property from xml
@type text: str
"""
self.type = type_
self.text = text
class TestError(TestInfo):
"""
'error' result container
"""
def xml(self):
return u'<error type="%s"><![CDATA[%s]]></error>'%(self.type, self.text)
class TestFailure(TestInfo):
"""
'failure' result container
"""
def xml(self):
return u'<failure type="%s"><![CDATA[%s]]></failure>'%(self.type, self.text)
class TestCaseResult(object):
"""
'testcase' result container
"""
def __init__(self, name):
"""
@param name: name of testcase
@type name: str
"""
self.name = name
self.failures = []
self.errors = []
self.time = 0.0
self.classname = ''
def _passed(self):
"""
@return: True if test passed
@rtype: bool
"""
return not self.errors and not self.failures
## bool: True if test passed without errors or failures
passed = property(_passed)
def _failure_description(self):
"""
@return: description of testcase failure
@rtype: str
"""
if self.failures:
tmpl = "[%s][FAILURE]"%self.name
tmpl = tmpl + '-'*(80-len(tmpl))
tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n"
return '\n'.join(tmpl%x.text for x in self.failures)
return ''
def _error_description(self):
"""
@return: description of testcase error
@rtype: str
"""
if self.errors:
tmpl = "[%s][ERROR]"%self.name
tmpl = tmpl + '-'*(80-len(tmpl))
tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n"
return '\n'.join(tmpl%x.text for x in self.errors)
return ''
def _description(self):
"""
@return: description of testcase result
@rtype: str
"""
if self.passed:
return "[%s][passed]\n"%self.name
else:
return self._failure_description()+\
self._error_description()
## str: printable description of testcase result
description = property(_description)
def add_failure(self, failure):
"""
@param failure TestFailure
"""
self.failures.append(failure)
def add_error(self, error):
"""
@param failure TestError
"""
self.errors.append(error)
def xml(self):
return u' <testcase classname="%s" name="%s" time="%s">\n'%(self.classname, self.name, self.time)+\
'\n '.join([f.xml() for f in self.failures])+\
'\n '.join([e.xml() for e in self.errors])+\
' </testcase>'
class Result(object):
__slots__ = ['name', 'num_errors', 'num_failures', 'num_tests', \
'test_case_results', 'system_out', 'system_err', 'time']
def __init__(self, name, num_errors, num_failures, num_tests):
self.name = name
self.num_errors = num_errors
self.num_failures = num_failures
self.num_tests = num_tests
self.test_case_results = []
self.system_out = ''
self.system_err = ''
self.time = 0.0
def accumulate(self, r):
"""
Add results from r to this result
@param r: results to aggregate with this result
@type r: Result
"""
self.num_errors += r.num_errors
self.num_failures += r.num_failures
self.num_tests += r.num_tests
self.test_case_results.extend(r.test_case_results)
if r.system_out:
self.system_out += '\n'+r.system_out
if r.system_err:
self.system_err += '\n'+r.system_err
def add_test_case_result(self, r):
"""
Add results from a testcase to this result container
@param r: TestCaseResult
@type r: TestCaseResult
"""
self.test_case_results.append(r)
def xml(self):
"""
@return: document as unicode (UTF-8 declared) XML according to Ant JUnit spec
"""
return u'<?xml version="1.0" encoding="utf-8"?>'+\
'<testsuite name="%s" tests="%s" errors="%s" failures="%s" time="%s">'%\
(self.name, self.num_tests, self.num_errors, self.num_failures, self.time)+\
'\n'.join([tc.xml() for tc in self.test_case_results])+\
' <system-out><![CDATA[%s]]></system-out>'%self.system_out+\
' <system-err><![CDATA[%s]]></system-err>'%self.system_err+\
'</testsuite>'
def _text(tag):
return reduce(lambda x, y: x + y, [c.data for c in tag.childNodes if c.nodeType in [DomNode.TEXT_NODE, DomNode.CDATA_SECTION_NODE]], "").strip()
def _load_suite_results(test_suite_name, test_suite, result):
nodes = [n for n in test_suite.childNodes \
if n.nodeType == DomNode.ELEMENT_NODE]
for node in nodes:
name = node.tagName
if name == 'testsuite':
# for now we flatten this hierarchy
_load_suite_results(test_suite_name, node, result)
elif name == 'system-out':
if _text(node):
system_out = "[%s] stdout"%test_suite_name + "-"*(71-len(test_suite_name))
system_out += '\n'+_text(node)
result.system_out += system_out
elif name == 'system-err':
if _text(node):
system_err = "[%s] stderr"%test_suite_name + "-"*(71-len(test_suite_name))
system_err += '\n'+_text(node)
result.system_err += system_err
elif name == 'testcase':
name = node.getAttribute('name') or 'unknown'
classname = node.getAttribute('classname') or 'unknown'
# mangle the classname for some sense of uniformity
# between rostest/unittest/gtest
if '__main__.' in classname:
classname = classname[classname.find('__main__.')+9:]
if classname == 'rostest.rostest.RosTest':
classname = 'rostest'
elif not classname.startswith(result.name):
classname = "%s.%s"%(result.name,classname)
time = node.getAttribute('time') or 0.0
tc_result = TestCaseResult("%s/%s"%(test_suite_name,name))
tc_result.classname = classname
tc_result.time = time
result.add_test_case_result(tc_result)
for d in [n for n in node.childNodes \
if n.nodeType == DomNode.ELEMENT_NODE]:
# convert 'message' attributes to text elements to keep
# python unittest and gtest consistent
if d.tagName == 'failure':
message = d.getAttribute('message') or ''
text = _text(d) or message
x = TestFailure(d.getAttribute('type') or '', text)
tc_result.add_failure(x)
elif d.tagName == 'error':
message = d.getAttribute('message') or ''
text = _text(d) or message
x = TestError(d.getAttribute('type') or '', text)
tc_result.add_error(x)
## #603: unit test suites are not good about screening out illegal
## unicode characters. This little recipe I from http://boodebr.org/main/python/all-about-python-and-unicode#UNI_XML
## screens these out
RE_XML_ILLEGAL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \
u'|' + \
u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \
(unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff))
_safe_xml_regex = re.compile(RE_XML_ILLEGAL)
def _read_file_safe_xml(test_file):
"""
read in file, screen out unsafe unicode characters
"""
try:
# this is ugly, but the files in question that are problematic
# do not declare unicode type.
try:
f = codecs.open(test_file, "r", "utf-8" )
x = f.read()
except:
f.close()
f = codecs.open(test_file, "r", "iso8859-1" )
x = f.read()
for match in _safe_xml_regex.finditer(x):
x = x[:match.start()] + "?" + x[match.end():]
return x.encode("utf-8")
finally:
f.close()
def read(test_file, test_name):
"""
Read in the test_result file
@param test_file: test file path
@type test_file: str
@param test_name: name of test
@type test_name: str
@return: test results
@rtype: Result
"""
try:
xml_str = _read_file_safe_xml(test_file)
test_suite = parseString(xml_str).getElementsByTagName('testsuite')
except Exception, e:
import traceback
traceback.print_exc()
print "WARN: cannot read test result file [%s]: %s"%(test_file, str(e))
return Result(test_name, 0, 0, 0)
if not test_suite:
print "WARN: test result file [%s] contains no results"%test_file
return Result(test_name, 0, 0, 0)
test_suite = test_suite[0]
vals = [test_suite.getAttribute(attr) for attr in ['errors', 'failures', 'tests']]
vals = [v or 0 for v in vals]
err, fail, tests = [string.atoi(val) for val in vals]
result = Result(test_name, err, fail, tests)
result.time = test_suite.getAttribute('time') or 0.0
# Create a prefix based on the test result filename. The idea is to
# disambiguate the case when tests of the same name are provided in
# different .xml files. We use the name of the parent directory
test_file_base = os.path.basename(os.path.dirname(test_file))
fname = os.path.basename(test_file)
if fname.startswith('TEST-'):
fname = fname[5:]
if fname.endswith('.xml'):
fname = fname[:-4]
test_file_base = "%s.%s"%(test_file_base, fname)
_load_suite_results(test_file_base, test_suite, result)
return result
def read_all(filter=[]):
"""
Read in the test_results and aggregate into a single Result object
@param filter: list of packages that should be processed
@type filter: [str]
@return: aggregated result
@rtype: L{Result}
"""
dir_ = roslib.rosenv.get_test_results_dir()
root_result = Result('ros', 0, 0, 0)
if not os.path.exists(dir_):
return root_result
for d in os.listdir(dir_):
if filter and not d in filter:
continue
subdir = os.path.join(dir_, d)
if os.path.isdir(subdir):
for file in os.listdir(subdir):
if file.endswith('.xml'):
file = os.path.join(subdir, file)
result = read(file, os.path.basename(subdir))
root_result.accumulate(result)
return root_result