diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc089d329..6e059e777 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,8 @@ jobs: ROBOT_MODEL: ${{matrix.env.ROBOT_MODEL}} URSIM_VERSION: ${{matrix.env.URSIM_VERSION}} PROGRAM_FOLDER: ${{matrix.env.PROGRAM_FOLDER}} + - name: install-pips + run: pip install pandas lxml - name: configure run: mkdir build && cd build && cmake .. -DBUILDING_TESTS=1 -DINTEGRATION_TESTS=1 -DWITH_ASAN=ON env: @@ -52,6 +54,8 @@ jobs: run: mkdir -p test_artifacts - name: test run: cd build && ctest --output-on-failure --output-junit junit.xml + env: + URSIM_VERSION: ${{matrix.env.URSIM_VERSION}} - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 diff --git a/.gitignore b/.gitignore index 32f3e4f11..696674064 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ install_manifest.txt .idea .directory .vscode -build/ \ No newline at end of file +build/ +tests/resources/docs_rtde_output_recipe.txt diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 67d2b159d..4ba47c9a7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,7 +21,12 @@ option(INTEGRATION_TESTS "Build the integration tests that require a running rob if (INTEGRATION_TESTS) # Integration tests require a robot reachable at 192.168.56.101. Therefore, they have to be # activated separately. + + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + add_custom_target(generate_outputs ALL COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/resources/generate_rtde_outputs.py) add_executable(rtde_tests test_rtde_client.cpp) + add_dependencies(rtde_tests generate_outputs) target_link_libraries(rtde_tests PRIVATE ur_client_library::urcl GTest::gtest_main) gtest_add_tests(TARGET rtde_tests WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} @@ -111,6 +116,8 @@ target_link_libraries(primary_parser_tests PRIVATE ur_client_library::urcl GTest gtest_add_tests(TARGET primary_parser_tests ) + + add_executable(rtde_data_package_tests test_rtde_data_package.cpp) target_link_libraries(rtde_data_package_tests PRIVATE ur_client_library::urcl GTest::gtest_main) gtest_add_tests(TARGET rtde_data_package_tests diff --git a/tests/resources/exhaustive_rtde_output_recipe.txt b/tests/resources/exhaustive_rtde_output_recipe.txt index e0df0aa2a..ea3d5c7ef 100644 --- a/tests/resources/exhaustive_rtde_output_recipe.txt +++ b/tests/resources/exhaustive_rtde_output_recipe.txt @@ -1,3 +1,165 @@ +input_bit_registers0_to_31 +input_bit_registers32_to_63 +input_bit_register_64 +input_bit_register_65 +input_bit_register_66 +input_bit_register_67 +input_bit_register_68 +input_bit_register_69 +input_bit_register_70 +input_bit_register_71 +input_bit_register_72 +input_bit_register_73 +input_bit_register_74 +input_bit_register_75 +input_bit_register_76 +input_bit_register_77 +input_bit_register_78 +input_bit_register_79 +input_bit_register_80 +input_bit_register_81 +input_bit_register_82 +input_bit_register_83 +input_bit_register_84 +input_bit_register_85 +input_bit_register_86 +input_bit_register_87 +input_bit_register_88 +input_bit_register_89 +input_bit_register_90 +input_bit_register_91 +input_bit_register_92 +input_bit_register_93 +input_bit_register_94 +input_bit_register_95 +input_bit_register_96 +input_bit_register_97 +input_bit_register_98 +input_bit_register_99 +input_bit_register_100 +input_bit_register_101 +input_bit_register_102 +input_bit_register_103 +input_bit_register_104 +input_bit_register_105 +input_bit_register_106 +input_bit_register_107 +input_bit_register_108 +input_bit_register_109 +input_bit_register_110 +input_bit_register_111 +input_bit_register_112 +input_bit_register_113 +input_bit_register_114 +input_bit_register_115 +input_bit_register_116 +input_bit_register_117 +input_bit_register_118 +input_bit_register_119 +input_bit_register_120 +input_bit_register_121 +input_bit_register_122 +input_bit_register_123 +input_bit_register_124 +input_bit_register_125 +input_bit_register_126 +input_bit_register_127 +input_int_register_0 +input_int_register_1 +input_int_register_2 +input_int_register_3 +input_int_register_4 +input_int_register_5 +input_int_register_6 +input_int_register_7 +input_int_register_8 +input_int_register_9 +input_int_register_10 +input_int_register_11 +input_int_register_12 +input_int_register_13 +input_int_register_14 +input_int_register_15 +input_int_register_16 +input_int_register_17 +input_int_register_18 +input_int_register_19 +input_int_register_20 +input_int_register_21 +input_int_register_22 +input_int_register_23 +input_int_register_24 +input_int_register_25 +input_int_register_26 +input_int_register_27 +input_int_register_28 +input_int_register_29 +input_int_register_30 +input_int_register_31 +input_int_register_32 +input_int_register_33 +input_int_register_34 +input_int_register_35 +input_int_register_36 +input_int_register_37 +input_int_register_38 +input_int_register_39 +input_int_register_40 +input_int_register_41 +input_int_register_42 +input_int_register_43 +input_int_register_44 +input_int_register_45 +input_int_register_46 +input_int_register_47 +input_double_register_0 +input_double_register_1 +input_double_register_2 +input_double_register_3 +input_double_register_4 +input_double_register_5 +input_double_register_6 +input_double_register_7 +input_double_register_8 +input_double_register_9 +input_double_register_10 +input_double_register_11 +input_double_register_12 +input_double_register_13 +input_double_register_14 +input_double_register_15 +input_double_register_16 +input_double_register_17 +input_double_register_18 +input_double_register_19 +input_double_register_20 +input_double_register_21 +input_double_register_22 +input_double_register_23 +input_double_register_24 +input_double_register_25 +input_double_register_26 +input_double_register_27 +input_double_register_28 +input_double_register_29 +input_double_register_30 +input_double_register_31 +input_double_register_32 +input_double_register_33 +input_double_register_34 +input_double_register_35 +input_double_register_36 +input_double_register_37 +input_double_register_38 +input_double_register_39 +input_double_register_40 +input_double_register_41 +input_double_register_42 +input_double_register_43 +input_double_register_44 +input_double_register_45 +input_double_register_46 +input_double_register_47 timestamp target_q target_qd @@ -48,34 +210,6 @@ standard_analog_input1 standard_analog_output0 standard_analog_output1 io_current -actual_robot_energy_consumed -actual_robot_braking_energy_dissipated -encoder0_raw -encoder1_raw -euromap67_input_bits -euromap67_output_bits -euromap67_24V_voltage -euromap67_24V_current -tool_mode -tool_analog_input_types -tool_analog_input0 -tool_analog_input1 -tool_output_voltage -tool_output_current -tool_temperature -tool_output_mode -tool_digital_output0_mode -tool_digital_output1_mode -tcp_force_scalar -joint_position_deviation_ratio -collision_detection_ratio -ft_raw_wrench -wrench_calc_from_currents -payload -payload_cog -payload_inertia -script_control_line -time_scale_source output_bit_registers0_to_31 output_bit_registers32_to_63 output_bit_register_64 @@ -238,165 +372,31 @@ output_double_register_44 output_double_register_45 output_double_register_46 output_double_register_47 -input_bit_registers0_to_31 -input_bit_registers32_to_63 -input_bit_register_64 -input_bit_register_65 -input_bit_register_66 -input_bit_register_67 -input_bit_register_68 -input_bit_register_69 -input_bit_register_70 -input_bit_register_71 -input_bit_register_72 -input_bit_register_73 -input_bit_register_74 -input_bit_register_75 -input_bit_register_76 -input_bit_register_77 -input_bit_register_78 -input_bit_register_79 -input_bit_register_80 -input_bit_register_81 -input_bit_register_82 -input_bit_register_83 -input_bit_register_84 -input_bit_register_85 -input_bit_register_86 -input_bit_register_87 -input_bit_register_88 -input_bit_register_89 -input_bit_register_90 -input_bit_register_91 -input_bit_register_92 -input_bit_register_93 -input_bit_register_94 -input_bit_register_95 -input_bit_register_96 -input_bit_register_97 -input_bit_register_98 -input_bit_register_99 -input_bit_register_100 -input_bit_register_101 -input_bit_register_102 -input_bit_register_103 -input_bit_register_104 -input_bit_register_105 -input_bit_register_106 -input_bit_register_107 -input_bit_register_108 -input_bit_register_109 -input_bit_register_110 -input_bit_register_111 -input_bit_register_112 -input_bit_register_113 -input_bit_register_114 -input_bit_register_115 -input_bit_register_116 -input_bit_register_117 -input_bit_register_118 -input_bit_register_119 -input_bit_register_120 -input_bit_register_121 -input_bit_register_122 -input_bit_register_123 -input_bit_register_124 -input_bit_register_125 -input_bit_register_126 -input_bit_register_127 -input_int_register_0 -input_int_register_1 -input_int_register_2 -input_int_register_3 -input_int_register_4 -input_int_register_5 -input_int_register_6 -input_int_register_7 -input_int_register_8 -input_int_register_9 -input_int_register_10 -input_int_register_11 -input_int_register_12 -input_int_register_13 -input_int_register_14 -input_int_register_15 -input_int_register_16 -input_int_register_17 -input_int_register_18 -input_int_register_19 -input_int_register_20 -input_int_register_21 -input_int_register_22 -input_int_register_23 -input_int_register_24 -input_int_register_25 -input_int_register_26 -input_int_register_27 -input_int_register_28 -input_int_register_29 -input_int_register_30 -input_int_register_31 -input_int_register_32 -input_int_register_33 -input_int_register_34 -input_int_register_35 -input_int_register_36 -input_int_register_37 -input_int_register_38 -input_int_register_39 -input_int_register_40 -input_int_register_41 -input_int_register_42 -input_int_register_43 -input_int_register_44 -input_int_register_45 -input_int_register_46 -input_int_register_47 -input_double_register_0 -input_double_register_1 -input_double_register_2 -input_double_register_3 -input_double_register_4 -input_double_register_5 -input_double_register_6 -input_double_register_7 -input_double_register_8 -input_double_register_9 -input_double_register_10 -input_double_register_11 -input_double_register_12 -input_double_register_13 -input_double_register_14 -input_double_register_15 -input_double_register_16 -input_double_register_17 -input_double_register_18 -input_double_register_19 -input_double_register_20 -input_double_register_21 -input_double_register_22 -input_double_register_23 -input_double_register_24 -input_double_register_25 -input_double_register_26 -input_double_register_27 -input_double_register_28 -input_double_register_29 -input_double_register_30 -input_double_register_31 -input_double_register_32 -input_double_register_33 -input_double_register_34 -input_double_register_35 -input_double_register_36 -input_double_register_37 -input_double_register_38 -input_double_register_39 -input_double_register_40 -input_double_register_41 -input_double_register_42 -input_double_register_43 -input_double_register_44 -input_double_register_45 -input_double_register_46 -input_double_register_47 +actual_robot_energy_consumed +actual_robot_braking_energy_dissipated +encoder0_raw +encoder1_raw +euromap67_input_bits +euromap67_output_bits +euromap67_24V_voltage +euromap67_24V_current +tool_mode +tool_analog_input_types +tool_analog_input0 +tool_analog_input1 +tool_output_voltage +tool_output_current +tool_temperature +tool_output_mode +tool_digital_output0_mode +tool_digital_output1_mode +tcp_force_scalar +joint_position_deviation_ratio +collision_detection_ratio +ft_raw_wrench +wrench_calc_from_currents +payload +payload_cog +payload_inertia +script_control_line +time_scale_source diff --git a/tests/resources/generate_rtde_outputs.py b/tests/resources/generate_rtde_outputs.py new file mode 100644 index 000000000..cf4f527b9 --- /dev/null +++ b/tests/resources/generate_rtde_outputs.py @@ -0,0 +1,79 @@ +# -- BEGIN LICENSE BLOCK ---------------------------------------------- +# Copyright 2025 Universal Robots A/S +# +# 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 the {copyright_holder} 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 HOLDER 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. +# -- END LICENSE BLOCK ------------------------------------------------ + +import pathlib +import pandas as pd +import re +URCL_PATH = pathlib.Path(__file__).parent.parent.parent.resolve() + +PKG_PATH = [i for i in pathlib.Path(URCL_PATH.as_posix()).glob("**/data_package.cpp")] + +assert len(PKG_PATH) == 1 + +PKG_PATH = PKG_PATH[0].resolve() + +OUTPUT_PATH = pathlib.Path(__file__).parent.resolve().as_posix() + "/exhaustive_rtde_output_recipe.txt" +WEB_OUTPUT_PATH = pathlib.Path(__file__).parent.resolve().as_posix() + "/docs_rtde_output_recipe.txt" + +with open(PKG_PATH) as pkg_file: + save_outputs = False + outputs = [] + while True: + line = pkg_file.readline() + if not line: + break + if "// INPUT / OUTPUT" in line: + save_outputs = True + if save_outputs: + if "//" not in line and len(line) > 1: + outputs.append(line.split('"')[1] + "\n") + if "// NOT IN OFFICIAL DOCS" in line: + break + + with open(OUTPUT_PATH, "w") as output_file: + output_file.writelines(outputs) + +# Get outputs from official docs +page = pd.read_html("https://docs.universal-robots.com/tutorials/communication-protocol-tutorials/rtde-guide.html") +table = page[1] +outputs = [] +for _, row in table.iterrows(): + name = row["Name"] + if "X" in name: + numbers = re.findall(r'\d+', row["Comment"]) + amount = int(numbers[0]) + base_addr = int(numbers[1]) + for i in range(amount): + outputs.append(name[:-1]+str(base_addr+i) + "\n") + else: + outputs.append(name + "\n") + +with open(WEB_OUTPUT_PATH, "w") as web_output_file: + web_output_file.writelines(outputs) diff --git a/tests/test_rtde_client.cpp b/tests/test_rtde_client.cpp index cd87c2355..0280682b4 100644 --- a/tests/test_rtde_client.cpp +++ b/tests/test_rtde_client.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "ur_client_library/exceptions.h" #include @@ -59,6 +60,7 @@ class RTDEClientTest : public ::testing::Test std::string output_recipe_file_ = "resources/rtde_output_recipe.txt"; std::string exhaustive_output_recipe_file_ = "resources/exhaustive_rtde_output_recipe.txt"; + std::string docs_output_recipe_file_ = "resources/docs_rtde_output_recipe.txt"; std::string input_recipe_file_ = "resources/rtde_input_recipe.txt"; comm::INotifier notifier_; std::unique_ptr client_; @@ -375,11 +377,26 @@ TEST_F(RTDEClientTest, connect_non_running_robot) TEST_F(RTDEClientTest, check_all_rtde_output_variables_exist) { + const char* env_var = std::getenv("URSIM_VERSION"); + if (env_var == nullptr) + { + std::cout << "No URSIM_VERSION environment variable set, skipping test." << std::endl; + GTEST_SKIP(); + } + const std::string env_ursim_version(env_var); + + if (env_ursim_version != "latest") + { + std::cout << "Not using the latest URSIM version, skipping test. URSIM_VERSION is set to '" << env_ursim_version + << "'" << std::endl; + GTEST_SKIP(); + } + client_->init(); // Ignore unknown output variables to account for variables not available in old urcontrol versions. client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, exhaustive_output_recipe_file_, - input_recipe_file_, 0.0, true)); + input_recipe_file_, 0.0, false)); EXPECT_TRUE(client_->init()); client_->start(); @@ -400,6 +417,51 @@ TEST_F(RTDEClientTest, check_all_rtde_output_variables_exist) client_->pause(); } +TEST_F(RTDEClientTest, check_rtde_data_fields_match_docs) +{ + std::ifstream docs_file(docs_output_recipe_file_); + std::ifstream pkg_file(exhaustive_output_recipe_file_); + std::vector docs_outputs; + std::string line; + while (std::getline(docs_file, line)) + { + docs_outputs.push_back(line); + } + std::vector pkg_outputs; + while (std::getline(pkg_file, line)) + { + pkg_outputs.push_back(line); + } + std::sort(docs_outputs.begin(), docs_outputs.end()); + std::sort(pkg_outputs.begin(), pkg_outputs.end()); + if (!std::is_permutation(docs_outputs.begin(), docs_outputs.end(), pkg_outputs.begin(), pkg_outputs.end())) + { + std::cout << "Data package output fields do not match output fields in documentation" << std::endl; + std::unordered_map diff; + std::cout << "Differences: " << std::endl; + for (auto name : docs_outputs) + { + diff[name] += 1; + } + for (auto name : pkg_outputs) + { + diff[name] -= 1; + } + for (auto elem : diff) + { + if (elem.second > 0) + { + std::cout << elem.first << " exists in documentation, but not in data package dict." << std::endl; + } + if (elem.second < 0) + { + std::cout << elem.first << " exists in data package dict, but not in documentation." << std::endl; + } + } + GTEST_FAIL(); + } +} + TEST_F(RTDEClientTest, check_unknown_rtde_output_variable) { client_->init();