1.使用Catkin发布的完整指南
说明:
- 本教程试图总结在现代发行版(Indigo)上发布基于Catkin的软件包应该满足的所有步骤。 它不仅涵盖代码完成后需要执行的操作,还包括代码和catkin设置的外观,以便能够成功发布。
- 将发布的是您的存储库中找到的所有软件包。 如果此存储库中的软件包依赖于您希望发布的另一个存储库中的软件包,则应将它们一起发布。
1.本地编译
- "catkin_make -DCMAKE_BUILD_TYPE=RelWithDebInfo -j1"
应该编译,请看 catkin. 在运行之前 删除devil和build 目录。
2.本地测试
- “catkin_make -DCMAKE_BUILD_TYPE = Debug run_tests”
运行时,结果不会失败。
2.1代码级测试
C++
- 在package.xml中,如果依赖项尚未在build_depend中,则除了在test_depend中添加依赖项外,不执行任何操作。
- 在CMakeLists.txt中添加类似的内容
- catkin_add_gtest(mypackage-utest test/utest.cpp)
See gtest, catkin, format 2 or catkin, legacy format.
Python
- In package.xml: <test_depend>python-nose</test_depend>
- In CMakeLists.txt: catkin_add_nosetests(path/to/my_test.py)
Python脚本应该不可执行
See unittest#Code-level_Python_Unit_Tests, catkin documentation, format 2, or catkin documentation, legacy format.
2.2节点级测试
使用rostest测试ROS主服务器和可能的其他节点。
C++
- In package.xml: <build_depend>rostest</build_depend>
- In CMakeLists.txt, contrary to what the official documentation states (as of 2015-01), add:
if (CATKIN_ENABLE_TESTING)
find_package(rostest REQUIRED)
add_rostest_gtest(mynode_test test/mynode.test test/test_mynode.cpp [more cpp files])
target_link_libraries(mynode_test ${catkin_LIBRARIES})
endif()
Python
- In package.xml: <build_depend>rostest</build_depend>
- In CMakeLists.txt, contrary to what the official documentation states (as of 2015-01), add:
if (CATKIN_ENABLE_TESTING)
find_package(rostest REQUIRED)
add_rostest(test/mytest.test)
endif()
在mytest.test中调用的Python脚本必须是可执行的。
2.3用数据文件测试
在原目录中:See http://answers.ros.org/question/59779/data-files-for-catkin-gtest/.
外部:See catkin documentation, format 2 or catkin documentation, legacy format.
3.检查依赖项
- “rosdep check mypackage”
不应报告任何错误。请注意,可以忽略有关将成为发行版一部分的程序包的未满足依赖项的错误。
如果依赖关系未满足,即如果它不在https://github.com/ros/rosdistro/tree/master/rosdep中的适当文件中,则必须按照以下说明将其添加到那里:rosdep documentation。
4.添加安装说明
See catkin documentation, format 2 or catkin documentation, legacy format.
5.准备文档
用Doxygen格式编写代码注释。 使用doc:部分将您的存储库添加到distribution.yaml中,以自动生成在线文档(消息API,代码API)。 有关文档生成,. See rosdistro/Tutorials/Indexing Your ROS Repository for Documentation Generation.。 请注意,这可以在发布步骤中更容易地完成。
6.进行预发布测试
6.1本地
See jenkins_tools#run_chroot_local.
6.2在buildfarm上
See bloom/Tutorials/PrereleaseTest.
6.3发布
See bloom/Tutorials/FirstTimeRelease.
2.gtest
1.获取gtest
2.谷歌测试(gtest)
我们使用GoogleTest或gtest在C ++中编写单元测试。 gtest的官方文档在这里:https://github.com/google/googletest
也可参考: http://www.ibm.com/developerworks/aix/library/au-googletestingframework.html
这些页面提供了一些提示,以及在ROS代码中编写和调用单元测试的示例。
对于与语言无关的策略和测试策略,see UnitTesting.
3.代码结构:
按照惯例,包的测试程序进入测试子目录。对于一个简单的包,通常写入一个测试文件就足够了,比如test / utest.cpp。
4.写tests
测试的基本结构如下所示:
// Bring in my package's API, which is what I'm testing
#include "foo/foo.h"
// Bring in gtest
#include <gtest/gtest.h>
// Declare a test
TEST(TestSuite, testCase1)
{
<test things here, calling EXPECT_* and/or ASSERT_* macros as needed>
}
// Declare another test
TEST(TestSuite, testCase2)
{
<test things here, calling EXPECT_* and/or ASSERT_* macros as needed>
}
// Run all the tests that were declared with TEST()
int main(int argc, char **argv){
testing::InitGoogleTest(&argc, argv);
ros::init(argc, argv, "tester");
ros::NodeHandle nh;
return RUN_ALL_TESTS();
}
请注意,如果您的测试使用ROS,则需要初始化ROS。
4.1测试命名约定
每个测试都是“测试用例”,测试用例分为“测试套件”。由您来适当地声明和使用测试套件。许多软件包只需要一个测试套件,但如果有意义,您可以使用更多软件包。
- Test suites are CamelCased, like C++ types
- Test cases are camelCased, like C++ functions
5.编译和运行测试
将测试添加到包的CMakeLists.txt中,如下所示:
- catkin_add_gtest(utest test/utest.cpp)
这些调用将导致在主构建期间构建utest可执行文件(一个简单的make),并将它放在TBD中。请注意,与rosbuild不同,不允许在目标声明中指定目录层次结构。
您可以使用make test运行测试。您也可以直接运行测试可执行文件,例如:
- ./bin/test/utest
请记住,roscore需要在您的计算机上运行。 否则,测试将默默等待直到它。
有关catkin_add_gtest()宏的更多信息,请参阅 catkin/CMakeLists.txt.
- 注意:不要在包清单中声明对gtest的依赖。 catkin_add_gtest()宏将引入必要的标志。
- 注意:如果要针对gtest构建可执行文件,但不要将其声明为自己的测试(例如,当您打算通过rostest运行可执行文件时),请使用catkin_add_executable_with_gtest()。
- 注意:使用Ubuntu 11.10,您将收到“对'pthread_getspecific'的未定义引用”错误。 要解决此问题,您必须通过在catkin_add_gtest调用上方添加以下行来添加“-pthread”编译器标志:
- SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
6.测试输出
测试的控制台输出看起来像这样: ~~~
当作为make test运行时,每个测试也将生成一个XML文件:
7.例子:
- 以下示例取自math_utils包。 它显示了如何测试各种函数调用的正确结果。
- 注意使用EXPECT_ *宏而不是ASSERT_ *宏。 ASSERT_ *宏记录失败并立即退出测试,而EXPECT_ *版本记录失败并继续。 后一种行为通常是你想要的,因为你想要运行每一个测试。 例外情况是一次测试取决于先前测试的成功执行。
简单案例:调用函数
#include "math_utils/MathExpression.h"
#include "math_utils/math_utils.h"
#include <gtest/gtest.h>
#define TEST_EXPRESSION(a) EXPECT_EQ((a), meval::EvaluateMathExpression(#a))
TEST(MathExpressions, operatorRecognition){
EXPECT_TRUE(meval::ContainsOperators("+"));
EXPECT_TRUE(meval::ContainsOperators("-"));
EXPECT_TRUE(meval::ContainsOperators("/"));
EXPECT_TRUE(meval::ContainsOperators("*"));
EXPECT_FALSE(meval::ContainsOperators("1234567890qwertyuiop[]asdfghjkl;'zxcvbnm,._=?8"));
}
TEST(MathExpressions, basicOperations){
EXPECT_EQ(5, meval::EvaluateMathExpression("2+3"));
EXPECT_EQ(5, meval::EvaluateMathExpression("2 + 3"));
EXPECT_EQ(10, meval::EvaluateMathExpression("20/2"));
EXPECT_EQ(-4, meval::EvaluateMathExpression("6 - 10"));
EXPECT_EQ(24, meval::EvaluateMathExpression("6 * 4"));
}
TEST(MathExpressions, complexOperations){
TEST_EXPRESSION(((3 + 4) / 2.0) + 10);
TEST_EXPRESSION(7 * (1 + 2 + 3 - 2 + 3.4) / 12.7);
TEST_EXPRESSION((1 + 2 + 3) - (8.0 / 10));
}
TEST(MathExpressions, UnaryMinus){
TEST_EXPRESSION(-5);
}
TEST(MathExpressions, badInput){
//TODO - figure out what good error behavior is and test for it properly
//EXPECT_EQ(0, meval::EvaluateMathExpression("4.1.3 - 4.1"));
//EXPECT_EQ(0, meval::EvaluateMathExpression("4.1.3"));
}
TEST(MathUtils, basicOperations){
EXPECT_EQ(math_utils::clamp<int>(-10, 10, 20), 10);
EXPECT_EQ(math_utils::clamp<int>(15, 10, 20), 15);
EXPECT_EQ(math_utils::clamp<int>(25, 10, 20), 20);
}
int main(int argc, char **argv){
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
处理异常
- 由于Google不使用例外,因此gtest无法处理它们。
- 如果您正在测试可以抛出异常的代码,那么您需要自己尝试/捕获它们,然后根据需要使用ADD_FAILURE和/或FAIL宏。 ADD_FAILURE记录非致命故障,而FAIL记录致命故障。
- 以下示例取自map_server包。 它展示了如何测试可以抛出异常的库。 注意我们如何测试意外的异常(如果我们捕获它就失败)和预期的异常(如果我们没有捕获它就失败)。
- 另请注意,SUCCEED宏仅仅是纪录片。 如果要在中间成功终止测试,则还必须显式调用return。
#include <stdexcept> // for std::runtime_error
#include <gtest/gtest.h>
#include "map_server/image_loader.h"
#include "test_constants.h"
/* Try to load a valid PNG file. Succeeds if no exception is thrown, and if
* the loaded image matches the known dimensions and content of the file.
*
* This test can fail on OS X, due to an apparent limitation of the
* underlying SDL_Image library. */
TEST(MapServer, loadValidPNG)
{
try
{
std_srvs::StaticMap::response map_resp;
map_server::loadMapFromFile(&map_resp, g_valid_png_file, g_valid_image_res, false);
EXPECT_FLOAT_EQ(map_resp.map.resolution, g_valid_image_res);
EXPECT_EQ(map_resp.map.width, g_valid_image_width);
EXPECT_EQ(map_resp.map.height, g_valid_image_height);
for(unsigned int i=0; i < map_resp.map.width * map_resp.map.height; i++)
EXPECT_EQ(g_valid_image_content[i], map_resp.map.data[i]);
}
catch(...)
{
ADD_FAILURE() << "Uncaught exception : " << "This is OK on OS X";
}
}
/* Try to load a valid BMP file. Succeeds if no exception is thrown, and if
* the loaded image matches the known dimensions and content of the file. */
TEST(MapServer, loadValidBMP)
{
try
{
std_srvs::StaticMap::response map_resp;
map_server::loadMapFromFile(&map_resp, g_valid_bmp_file, g_valid_image_res, false);
EXPECT_FLOAT_EQ(map_resp.map.resolution, g_valid_image_res);
EXPECT_EQ(map_resp.map.width, g_valid_image_width);
EXPECT_EQ(map_resp.map.height, g_valid_image_height);
for(unsigned int i=0; i < map_resp.map.width * map_resp.map.height; i++)
EXPECT_EQ(g_valid_image_content[i], map_resp.map.data[i]);
}
catch(...)
{
ADD_FAILURE() << "Uncaught exception";
}
}
/* Try to load a valid BMP file. Succeeds if no exception is thrown, and if
* the loaded image matches the known dimensions and content of the file. */
TEST(MapServer, loadValidBMP)
{
try
{
std_srvs::StaticMap::response map_resp;
map_server::loadMapFromFile(&map_resp, g_valid_bmp_file, g_valid_image_res, false);
EXPECT_FLOAT_EQ(map_resp.map.resolution, g_valid_image_res);
EXPECT_EQ(map_resp.map.width, g_valid_image_width);
EXPECT_EQ(map_resp.map.height, g_valid_image_height);
for(unsigned int i=0; i < map_resp.map.width * map_resp.map.height; i++)
EXPECT_EQ(g_valid_image_content[i], map_resp.map.data[i]);
}
catch(...)
{
ADD_FAILURE() << "Uncaught exception";
}
}
/* Try to load an invalid file. Succeeds if a std::runtime_error exception
* is thrown. */
TEST(MapServer, loadInvalidFile)
{
try
{
std_srvs::StaticMap::response map_resp;
map_server::loadMapFromFile(&map_resp, "foo", 0.1, false);
}
catch(std::runtime_error &e)
{
SUCCEED();
return;
}
catch(...)
{
FAIL() << "Uncaught exception";
}
ADD_FAILURE() << "Didn't throw exception as expected";
}
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
使用测试夹具获取持久数据
- 您可以通过创建“测试夹具”来创建持久性数据对象,该测试夹具是从test :: Test继承的类。 使用测试夹具的测试使用TEST_F宏而不是TEST宏声明。 这篇谷歌测试入门是强烈推荐阅读,以了解测试夹具的强大用途。
- 使用测试夹具的最常见原因是,需要设置变量和条件的繁琐过程来测试可由不同测试用例重用的功能。 下面是一个可以添加/乘以Vector3几何消息的类的示例。
#ifndef TALKER
#define TALKER
#include <ros/ros.h>
#include "geometry_msgs/Vector3.h"
class Talker{
ros::NodeHandle nh;
ros::Publisher pub;
public:
Talker();
void pubMsg(geometry_msgs::Vector3&& v);
geometry_msgs::Vector3 add(const geometry_msgs::Vector3& a, const geometry_msgs::Vector3& b);
geometry_msgs::Vector3 mult(const geometry_msgs::Vector3& a, const geometry_msgs::Vector3& b);
};
# endif // TALKER_H
因为我们想用相同的参数分别测试两个函数,所以我们可以利用测试夹具来设置和拆除每个测试之间的变量。
#include <gtest/gtest.h>
#include "talker/talker.h"
// helper function to create a geometry_msg::Vector3
auto createVec = [](double x, double y, double z) {
geometry_msgs::Vector3 v;
v.x = x;
v.y = y;
v.z = z;
return v;
};
class TalkerFixture : public ::testing::Test {
protected:
geometry_msgs::Vector3 a;
geometry_msgs::Vector3 b;
// Setup
TalkerTest() {
a = createVec(1,2,3);
b = createVec(2,2,2);
}
};
TEST_F(TalkerFixture, TestAdder) {
Talker talk;
// a and b are inherited from TalkerTest
geometry_msgs::Vector3 vec = talk.add(a,b);
geometry_msgs::Vector3 ans = createVec(3,4,5);
ASSERT_EQ(ans.x, vec.x) << "Talker add x's failed";
ASSERT_EQ(ans.y, vec.y) << "Talker add y's failed";
ASSERT_EQ(ans.z, vec.z) << "Talker add z's failed";
}
TEST_F(TalkerFixture, TestMult) {
Talker talk;
// a and b are inherited from TalkerTest
geometry_msgs::Vector3 vec = talk.mult(a,b);
geometry_msgs::Vector3 ans = createVec(2,4,6);
ASSERT_EQ(ans.x, vec.x) << "Talker mult x's failed";
ASSERT_EQ(ans.y, vec.y) << "Talker mult y's failed";
ASSERT_EQ(ans.z, vec.z) << "Talker mult z's failed";
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
ros::init(argc, argv, "test_talker");
ros::NodeHandle nh;
return RUN_ALL_TESTS();
}
更新Package.xml
<package format="2">
<test_depend>rosunit</test_depend>
</package>
更新CMakeLists.txt
注意gtest是如何像任何其他必须链接到其依赖项的可执行文件一样添加为可执行文件,即Talker类/库。
## Add gtest based cpp test target and link libraries
catkin_add_gtest(${PROJECT_NAME}-test test/talker_tests.cpp)
if(TARGET ${PROJECT_NAME}-test)
target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME})
endif()
运行测试:
如果你的gtest通过rosinit()初始化rosnode,你必须单独初始化一个roscore,然后运行所有测试:
- roscore
- catkin_make run_tests
或者运行特定的包只需完成查找包找您要运行的特定包测试
- roscore
- catkin_make run_tests<TAB><TAB>