diff --git a/tools/rosunit/CMakeLists.txt b/tools/rosunit/CMakeLists.txt
new file mode 100644
index 00000000..f8f1c9cc
--- /dev/null
+++ b/tools/rosunit/CMakeLists.txt
@@ -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})
diff --git a/tools/rosunit/Makefile b/tools/rosunit/Makefile
new file mode 100644
index 00000000..b75b928f
--- /dev/null
+++ b/tools/rosunit/Makefile
@@ -0,0 +1 @@
+include $(shell rospack find mk)/cmake.mk
\ No newline at end of file
diff --git a/tools/rosunit/manifest.xml b/tools/rosunit/manifest.xml
new file mode 100644
index 00000000..d735725c
--- /dev/null
+++ b/tools/rosunit/manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+ Unit-testing package for ROS. This is a lower-level library for rostest and handles unit tests, whereas rostest handles integration tests.
+
+
+ Ken Conley
+ BSD
+
+ http://ros.org/wiki/rosunit
+
+
+
+
+
diff --git a/tools/rosunit/rosdoc.yaml b/tools/rosunit/rosdoc.yaml
new file mode 100644
index 00000000..36870d2d
--- /dev/null
+++ b/tools/rosunit/rosdoc.yaml
@@ -0,0 +1 @@
+ - builder: epydoc
\ No newline at end of file
diff --git a/tools/rosunit/src/rosunit/junitxml.py b/tools/rosunit/src/rosunit/junitxml.py
new file mode 100644
index 00000000..21ce4b19
--- /dev/null
+++ b/tools/rosunit/src/rosunit/junitxml.py
@@ -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''%(self.type, self.text)
+
+class TestFailure(TestInfo):
+ """
+ 'failure' result container
+ """
+ def xml(self):
+ return u''%(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' \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])+\
+ ' '
+
+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''+\
+ ''%\
+ (self.name, self.num_tests, self.num_errors, self.num_failures, self.time)+\
+ '\n'.join([tc.xml() for tc in self.test_case_results])+\
+ ' '%self.system_out+\
+ ' '%self.system_err+\
+ ''
+
+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