ROS系统python代码测试之rostest

ROS系统中提供了测试框架,可以实现python/c++代码的单元测试,python和C++通过不同的方式实现,

之后的两篇文档分别详细介绍各自的实现步骤,以及测试结果和覆盖率的获取。

ROS系统中python代码测试介绍

关于测试代码的写法细节请参考官方wiki文档,http://wiki.ros.org/unittest,本文主要说明使用中的坑。

ROS中python代码的测试可以有两种实现方式一是节点级的集成测试,可以测试节点创建和消息收发的整个过程;二是代码级的单元测试,在测试用例中导入被测代码进行测试。
python代码测试中可能遇到的问题及优化修改

1、创建启动测试的roslaunch文件

rostest相关的roslaunch请参考 http://wiki.ros.org/roslaunch/XML/test

 

<launch>
    <node name="nodename" pkg="pkgname" type="被测文件的py"/>
    <test test-name="testnodename" pkg="pkgname" time-limit="500.0" type="测试文件py" args="--cov"/>
</launch>
View Code

 

关注点1

time-limit这个标签限制了用例运行的最长时间,如果用例耗时超过这个时间,那么用例会自动以超时失败结束,默认值是60s。如果用例较多运行时间长的话,需要

设置合理的值;

关注点2

args 这个标签可以向测试程序中传入参数,--cov的作用是测试完成后生成覆盖率文件,下面详细介绍。

2、测试结果文件获取

参见上一篇介绍环境变量了文章,通过ROS_TEST_RESULTS_DIR这个变量可以修改测试结果输出的位置。

3、覆盖率统计

我使用的ros版本是indigo,这个版本中rostest对覆盖率统计的代码需要进一步优化。

优化点1rostest.rosrun(package_name, test_name, test_case_class, sysargs=None)

根据wiki中对rostest.rosrun的描述,该函数应该有第五个参数coverage_packages,该参数表示待测试package list.

优化后的函数rostest.rosrun(package_name, test_name, test_case_class, sysargs=None, coverage_packages=None)

优化点2:rostest覆盖率统计完善

覆盖率统计需要首先a安装python代码覆盖率工具coverge,参考http://coverage.readthedocs.org/en/latest/

修改rostest.rosrun代码,使代码能够输出xml_report,为什么要输出xml报告呢,因为The report is compatible with Cobertura reports.

这一点很关键,在jenkins持续集成环境中需要这一步骤的工作。jenkins中的Cobertura插件可以解析xml_report文件,然后将python代码的详细覆盖率信息显示在用例的测试结果中。

/opt/ros/indigo/lib/python2.7/dist-packages/rostest/__init__.py 中函数的修改

 

def rosrun(package, test_name, test, sysargs=None, coverage_packages=None):
    """
    Run a rostest/unittest-based integration test.
    
    @param package: name of package that test is in
    @type  package: str
    @param test_name: name of test that is being run
    @type  test_name: str
    @param test: test class 
    @type  test: unittest.TestCase
    @param sysargs: command-line args. If not specified, this defaults to sys.argv. rostest
      will look for the --text and --gtest_output parameters
    @type  sysargs: list
    """
    if sysargs is None:
        # lazy-init sys args
        import sys
        sysargs = sys.argv
        
    #parse sysargs
    result_file = None
    for arg in sysargs:
        if arg.startswith(XML_OUTPUT_FLAG):
            result_file = arg[len(XML_OUTPUT_FLAG):]
    text_mode = '--text' in sysargs
    coverage_mode = '--cov' in sysargs
    if coverage_mode:
        _start_coverage(coverage_packages)

    import unittest
    import rospy

    coverresult = os.getenv('ROS_TEST_RESULTS_DIR') + '/coverage/'
    
    suite = unittest.TestLoader().loadTestsFromTestCase(test)
    if text_mode:
        result = unittest.TextTestRunner(verbosity=2).run(suite)
    else:
        result = rosunit.create_xml_runner(package, test_name, result_file).run(suite)
    if coverage_mode:
        _stop_coverage(coverage_packages, coverresult)
    rosunit.print_unittest_summary(result)
    
    # shutdown any node resources in case test forgets to
    rospy.signal_shutdown('test complete')
    if not result.wasSuccessful():
        import sys
        sys.exit(1)

 

def _stop_coverage(packages, html=None):
    """
    @param packages: list of packages to generate coverage reports for
    @type  packages: [str]
    @param html: (optional) if not None, directory to generate html report to
    @type  html: str
    """
    if _cov is None:
        return
    import sys, os
    try:
        _cov.stop()
        # accumulate results
        _cov.save()
        
        # - update our own .coverage-modules file list for
        #   coverage-html tool. The reason we read and rewrite instead
        #   of append is that this does a uniqueness check to keep the
        #   file from growing unbounded
        if os.path.exists('.coverage-modules'):
            with open('.coverage-modules','r') as f:
                all_packages = set([x for x in f.read().split('\n') if x.strip()] + packages)
        else:
            all_packages = set(packages)
        with open('.coverage-modules','w') as f:
            f.write('\n'.join(all_packages)+'\n')
            
        try:
            # list of all modules for html report
            all_mods = []

            # iterate over packages to generate per-package console reports
            for package in packages:
                pkg = __import__(package)
                m = [v for v in sys.modules.values() if v and v.__name__.startswith(package)]
                all_mods.extend(m)

                # generate overall report and per module analysis
                _cov.report(m, show_missing=0)
                for mod in m:
                    res = _cov.analysis(mod)
                    print("\n%s:\nMissing lines: %s"%(res[0], res[3]))

            if html:
                
                print("="*80+"\ngenerating html coverage report to %s\n"%html+"="*80)
                _cov.html_report(all_mods, directory=html)
                _cov.xml_report(all_mods, outfile=html + 'cover.xml')
        except ImportError as e:
            print("WARNING: cannot import '%s', will not generate coverage report"%package, file=sys.stderr)
    except ImportError as e:
        print("""WARNING: cannot import python-coverage, coverage tests will not run.
To install coverage, run 'easy_install coverage'""", file=sys.stderr)

/opt/ros/indigo/lib/python2.7/dist-packages/rosunit/pyunit.py 文件修改

def unitrun(package, test_name, test, sysargs=None, coverage_packages=None):
    """
    Wrapper routine from running python unitttests with
    JUnit-compatible XML output.  This is meant for unittests that do
    not not need a running ROS graph (i.e. offline tests only).

    This enables JUnit-compatible test reporting so that
    test results can be reported to higher-level tools.

    WARNING: unitrun() will trigger a sys.exit() on test failure in
    order to properly exit with an error code. This routine is meant
    to be used as a main() routine, not as a library.
    
    @param package: name of ROS package that is running the test
    @type  package: str
    @param coverage_packages: list of Python package to compute coverage results for. Defaults to package
    @type  coverage_packages: [str]
    @param sysargs: (optional) alternate sys.argv
    @type  sysargs: [str]
    """
    if sysargs is None:
        # lazy-init sys args
        import sys
        sysargs = sys.argv

    import unittest
    
    if coverage_packages is None:
        coverage_packages = [package]
        
    #parse sysargs
    result_file = None
    for arg in sysargs:
        if arg.startswith(XML_OUTPUT_FLAG):
            result_file = arg[len(XML_OUTPUT_FLAG):]
    text_mode = '--text' in sysargs

    coverage_mode = '--cov' in sysargs or '--covhtml' in sysargs
    if coverage_mode:
        start_coverage(coverage_packages)

    # create and run unittest suite with our xmllrunner wrapper
    suite = unittest.TestLoader().loadTestsFromTestCase(test)
    if text_mode:
        result = unittest.TextTestRunner(verbosity=2).run(suite)
    else:
        result = create_xml_runner(package, test_name, result_file).run(suite)
    if coverage_mode:
        #cov_html_dir = 'covhtml_test' if '--covhtml' in sysargs else None
        cov_html_dir = os.getenv('ROS_TEST_RESULTS_DIR') + '/coverage/'
        stop_coverage(coverage_packages, html=cov_html_dir)

    # test over, summarize results and exit appropriately
    print_unittest_summary(result)
    
    if not result.wasSuccessful():
        import sys
        sys.exit(1)
def stop_coverage(packages, html=None):
    """
    @param packages: list of packages to generate coverage reports for
    @type  packages: [str]
    @param html: (optional) if not None, directory to generate html report to
    @type  html: str
    """
    if _cov is None:
        return
    import sys, os
    try:
        _cov.stop()
        # accumulate results
        _cov.save()
        
        # - update our own .coverage-modules file list for
        #   coverage-html tool. The reason we read and rewrite instead
        #   of append is that this does a uniqueness check to keep the
        #   file from growing unbounded
        if os.path.exists('.coverage-modules'):
            with open('.coverage-modules','r') as f:
                all_packages = set([x for x in f.read().split('\n') if x.strip()] + packages)
        else:
            all_packages = set(packages)
        with open('.coverage-modules','w') as f:
            f.write('\n'.join(all_packages)+'\n')
            
        try:
            # list of all modules for html report
            all_mods = []

            # iterate over packages to generate per-package console reports
            for package in packages:
                pkg = __import__(package)
                m = [v for v in sys.modules.values() if v and v.__name__.startswith(package)]
                all_mods.extend(m)

                # generate overall report and per module analysis
                _cov.report(m, show_missing=0)
                for mod in m:
                    res = _cov.analysis(mod)
                    print("\n%s:\nMissing lines: %s"%(res[0], res[3]))
                    
            if html:
                
                print("="*80+"\ngenerating html coverage report to %s\n"%html+"="*80)
                _cov.html_report(all_mods, directory=html)
                _cov.xml_report(all_mods, outfile=html + 'cover.xml')
        except ImportError as e:
            print("WARNING: cannot import '%s', will not generate coverage report"%package, file=sys.stderr)
    except ImportError as e:
        print("""WARNING: cannot import python-coverage, coverage tests will not run.
To install coverage, run 'easy_install coverage'""", file=sys.stderr)

 

4、测试代码例子

if __name__ == '__main__':
    for arg in sys.argv:
        print arg
    testfiles = []
    testfiles.append('被测试的python package')
    testfiles.append('被测试的python package')
    rostest.rosrun(PKG, NAME, 测试类名, sys.argv, testfiles)

 

posted @ 2015-12-15 20:21  machineLearning  阅读(2320)  评论(0编辑  收藏  举报