diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1fc77adc2..6d2dcc433 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,8 +22,14 @@ jobs: steps: - name: Checkout repository code uses: actions/checkout@v3 + with: + clean: false + - name: Clean repository excluding taped test data + run: git clean -ffdx -e external/taped_test_data - name: Import private extensions run: vcs import < extensions.repos + - name: Install taped test dependencies + run: ./setup.py --install-taped-test-deps load-env: uses: ./.github/workflows/load-env-subworkflow.yml @@ -42,7 +48,7 @@ jobs: needs: [checkout-repository, load-env] uses: ./.github/workflows/build-subworkflow.yml with: - build-command: './setup.py --with-pcl --build-dir build-pcl' + build-command: './setup.py --with-pcl --build-taped-test --build-dir build-pcl' self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} docker-image: localhost:5000/rgl:latest @@ -82,7 +88,7 @@ jobs: with: build-command: ' source /opt/ros/humble/setup.bash && - ./setup.py --with-pcl --with-ros2-standalone --with-udp --with-snow --build-dir build-all' + ./setup.py --with-pcl --with-ros2-standalone --with-udp --with-snow --build-taped-test --build-dir build-all' self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} docker-image: localhost:5000/rgl:latest @@ -99,7 +105,9 @@ jobs: needs: [ build-pcl ] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-pcl/test && ./RobotecGPULidar_test' + test-command: ' + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && + cd build-pcl/test && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' docker-image: localhost:5000/rgl:latest test-ros2-dev: @@ -141,9 +149,10 @@ jobs: source /opt/ros/humble/setup.bash && source /rgldep/radar_msgs/install/setup.bash && export RGL_TEST_VLP16_CALIB_FILE=$(pwd)/extensions/udp/test/resources/Ros2Vlp16Calib.yaml && + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && cd build-all/test && - export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && - export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test' + export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test && + export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' docker-image: localhost:5000/rgl:latest ###### TEST WITH CLEAN UBUNTU DOCKER IMAGE ###### @@ -161,7 +170,8 @@ jobs: # Additionally, install PCL extension dependent libraries for runtime test-command: ' apt update && apt install -y libxcursor1 libgl1 && - cd build-pcl/test && ./RobotecGPULidar_test' + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && + cd build-pcl/test && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 test-ros2-prod: @@ -200,8 +210,9 @@ jobs: # Run tests twice, each for different RMW implementation test-command: ' apt update && apt install -y libxcursor1 libgl1 && + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && cd build-all/test && cp -r ../ros2_standalone/*.so* ../ && - export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && - export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test' + export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test && + export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 diff --git a/CMakeLists.txt b/CMakeLists.txt index c11175537..12361b2d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,8 @@ set(RGL_AUTO_TAPE_PATH "" CACHE STRING # STRING prevents from expanding relativ # Test configuration set(RGL_BUILD_TESTS ON CACHE BOOL "Enables building test. GTest will be automatically downloaded") +set(RGL_BUILD_TAPED_TESTS OFF CACHE BOOL + "Enables building taped test.") # Tools configuration set(RGL_BUILD_TOOLS ON CACHE BOOL "Enables building RGL executable tools") @@ -176,7 +178,7 @@ if (NOT ("RGL_LOG_LEVEL_${RGL_LOG_LEVEL}" IN_LIST RGL_AVAILABLE_LOG_LEVELS)) message(FATAL_ERROR "Incorrect RGL_LOG_LEVEL value: ${RGL_LOG_LEVEL}") endif() -if (WIN32 AND RGL_AUTO_TAPE_PATH) +if (WIN32 AND (RGL_AUTO_TAPE_PATH OR RGL_BUILD_TAPED_TESTS)) message(FATAL_ERROR "(Auto)Tape not supported on Windows") endif() @@ -190,11 +192,19 @@ target_compile_definitions(RobotecGPULidar ) # Include tests -if (RGL_BUILD_TESTS) +if (RGL_BUILD_TESTS OR RGL_BUILD_TAPED_TESTS) enable_testing() - add_subdirectory(test) + + if (RGL_BUILD_TESTS) + add_subdirectory(test) + endif() + + if (RGL_BUILD_TAPED_TESTS) + add_subdirectory(test/taped_test) + endif() endif() + # Include tools if (RGL_BUILD_TOOLS) add_subdirectory(tools) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 57182e149..551cb9391 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -50,7 +50,7 @@ if (UNIX) set_target_properties(spdlog PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() -if (${RGL_BUILD_TESTS}) +if (${RGL_BUILD_TESTS} OR ${RGL_BUILD_TAPED_TESTS}) set(INSTALL_GTEST OFF CACHE INTERNAL "Disable installation of googletest") FetchContent_Declare( googletest diff --git a/setup.py b/setup.py index ef428a1c6..e6019a57f 100755 --- a/setup.py +++ b/setup.py @@ -23,6 +23,9 @@ class Config: VCPKG_EXEC = "vcpkg" VCPKG_BOOTSTRAP = "bootstrap-vcpkg.sh" VCPKG_TRIPLET = "x64-linux" + TAPED_TEST_DATA_DIR = os.path.join("external", "taped_test_data") + TAPED_TEST_DATA_REPO = "git@github.com:RobotecAI/RGL-blobs.git" + TAPED_TEST_DATA_BRANCH = "main" def __init__(self): # Platform-dependent configuration @@ -69,6 +72,10 @@ def main(): help="Pass arguments to make. Usage: --make=\"args...\". Defaults to \"-j \"") parser.add_argument("--lib-rpath", type=str, nargs='*', help="Add run-time search path(s) for RGL library. $ORIGIN (actual library path) is added by default.") + parser.add_argument("--install-taped-test-deps", action='store_true', + help="Install dependencies for taped test and exit (closed-source dependencies)") + parser.add_argument("--build-taped-test", action='store_true', + help="Build taped test") if on_windows(): parser.add_argument("--ninja", type=str, default=f"-j{os.cpu_count()}", dest="build_args", help="Pass arguments to ninja. Usage: --ninja=\"args...\". Defaults to \"-j \"") @@ -100,6 +107,17 @@ def main(): print('Installed ROS2 deps, exiting...') return 0 + # Install taped test dependencies + if args.install_taped_test_deps: + install_taped_test_deps(cfg) + print('Installed dependencies for taped test, exiting...') + return 0 + + # Check taped test requirements + if args.build_taped_test and not args.with_pcl: + raise RuntimeError( + "Taped test requires PCL extension to be built: run this script with --with-pcl flag") + # Check CUDA if not is_cuda_version_ok(cfg): raise RuntimeError( @@ -137,6 +155,7 @@ def main(): f"-DRGL_BUILD_ROS2_EXTENSION={'ON' if args.with_ros2 else 'OFF'}", f"-DRGL_BUILD_UDP_EXTENSION={'ON' if args.with_udp else 'OFF'}", f"-DRGL_BUILD_SNOW_EXTENSION={'ON' if args.with_snow else 'OFF'}", + f"-DRGL_BUILD_TAPED_TESTS={'ON' if args.build_taped_test else 'OFF'}" ] if on_linux(): @@ -233,15 +252,15 @@ def install_pcl_deps(cfg): f"{os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_EXEC)} install --clean-after-build pcl[core,visualization]:{cfg.VCPKG_TRIPLET}") -def has_colcon(): - process = subprocess.Popen("colcon --help", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) +def is_command_available(command): + process = subprocess.Popen(f"{command}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) process.wait() return process.returncode == 0 def install_ros2_deps(cfg): # Install colcon if needed - if not has_colcon(): + if not is_command_available("colcon --help"): if on_windows(): run_system_command("pip install colcon-common-extensions") elif not inside_docker(): # Linux; Inside docker already installed @@ -261,6 +280,47 @@ def install_ros2_deps(cfg): # TODO: cyclonedds rmw may be installed here (instead of manually in readme) +def ensure_git_lfs_installed(): + if not is_command_available("git-lfs --help"): + print("Installing git-lfs...") + run_subprocess_command( + "curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash") + run_subprocess_command("sudo apt install git-lfs") + + +def clone_taped_test_data_repo(cfg): + run_subprocess_command( + f"git clone -b {cfg.TAPED_TEST_DATA_BRANCH} --single-branch --depth 1 {cfg.TAPED_TEST_DATA_REPO} {cfg.TAPED_TEST_DATA_DIR}") + os.chdir(cfg.TAPED_TEST_DATA_DIR) + # Set up git-lfs for this repository + run_subprocess_command("git-lfs install && git-lfs pull") + + +def is_taped_data_up_to_date(cfg): + result = subprocess.Popen("git fetch --dry-run --verbose", shell=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + stdout, _ = result.communicate() + return f"[up to date] {cfg.TAPED_TEST_DATA_BRANCH}" in stdout.decode() + + +def update_taped_test_data_repo(cfg): + if not is_taped_data_up_to_date(cfg): + print("Updating taped test benchmark data repository...") + run_subprocess_command("git pull && git-lfs pull") + + +def install_taped_test_deps(cfg): + # Cloning and updating taped test benchmark data repo requires git-lfs to be installed + ensure_git_lfs_installed() + if not os.path.isdir(cfg.TAPED_TEST_DATA_DIR): + print("Cloning taped test benchmark data repository...") + clone_taped_test_data_repo(cfg) + else: + print("Checking for updates in taped test benchmark data repository...") + os.chdir(cfg.TAPED_TEST_DATA_DIR) + update_taped_test_data_repo(cfg) + + # Returns a dict with env variables visible for a command after running in a system shell # Used to capture effects of sourcing file such as ros2 setup def capture_environment(command="cd ."): diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 45ceec849..7131fcee5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -121,4 +121,4 @@ endif() include(GoogleTest) -gtest_discover_tests(RobotecGPULidar_test) +gtest_discover_tests(RobotecGPULidar_test) \ No newline at end of file diff --git a/test/taped_test/CMakeLists.txt b/test/taped_test/CMakeLists.txt new file mode 100644 index 000000000..b721c553c --- /dev/null +++ b/test/taped_test/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.16) + +if(WIN32) + message(FATAL_ERROR "Tape not supported on Windows") +endif() + +set(RGL_TAPED_TEST_FILES + src/AwsimMeshToPcdTest.cpp +) + +include(GoogleTest) + +add_executable(RobotecGPULidar_taped_test ${RGL_TAPED_TEST_FILES}) + +target_link_libraries(RobotecGPULidar_taped_test PRIVATE + gtest_main + gmock_main + spdlog + RobotecGPULidar +) + +target_include_directories(RobotecGPULidar_taped_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) + +gtest_discover_tests(RobotecGPULidar_taped_test) \ No newline at end of file diff --git a/test/taped_test/src/AwsimMeshToPcdTest.cpp b/test/taped_test/src/AwsimMeshToPcdTest.cpp new file mode 100644 index 000000000..8539b7f2a --- /dev/null +++ b/test/taped_test/src/AwsimMeshToPcdTest.cpp @@ -0,0 +1,63 @@ +#include +#include + +#include +#include + +#if RGL_BUILD_PCL_EXTENSION +#include +#include +#include +#endif + +class AwsimMeshToPcdTest : public RGLTest +{ +protected: + const std::string benchmarkDataDirEnvVariableName = "RGL_TAPED_TEST_DATA_DIR"; + + const std::string testTapeFileName = "awsim-mesh2pcd"; + const std::string expectedOutputDirName = "expected-output"; + const std::string expectedOutputFileName = testTapeFileName + ".pcd"; + + const float epsilon = 1e-2f; +}; + +TEST_F(AwsimMeshToPcdTest, validate_point_cloud_equivalence) +{ +#if RGL_BUILD_PCL_EXTENSION + + // It is necessary to set environment variable on benchmark data directory + if (std::getenv(benchmarkDataDirEnvVariableName.c_str()) == nullptr) { + const std::string msg = fmt::format( + "Skipping Taped Test - benchmark data directory must be provided in environment variable '{}'", + benchmarkDataDirEnvVariableName); + GTEST_SKIP() << msg; + } + + const std::string benchmarkDataDir = std::getenv(benchmarkDataDirEnvVariableName.c_str()); + const std::string testTapePath{(std::filesystem::path(benchmarkDataDir) / testTapeFileName).string()}; + const std::string expectedOutputPath{ + (std::filesystem::path(benchmarkDataDir) / expectedOutputDirName / expectedOutputFileName).string()}; + const std::string outputPath{(std::filesystem::current_path() / "output.pcd").string()}; + + ASSERT_RGL_SUCCESS(rgl_tape_play(testTapePath.c_str())); + + pcl::PointCloud::Ptr expectedCloud(new pcl::PointCloud); + pcl::PointCloud::Ptr outputCloud(new pcl::PointCloud); + + pcl::io::loadPCDFile(expectedOutputPath, *expectedCloud); + pcl::io::loadPCDFile(outputPath, *outputCloud); + + ASSERT_TRUE(expectedCloud->size() == outputCloud->size()); + + for (size_t i = 0; i < expectedCloud->size(); ++i) { + EXPECT_NEAR(expectedCloud->points[i].x, outputCloud->points[i].x, epsilon); + EXPECT_NEAR(expectedCloud->points[i].y, outputCloud->points[i].y, epsilon); + EXPECT_NEAR(expectedCloud->points[i].z, outputCloud->points[i].z, epsilon); + } + +#else + const std::string msg = fmt::format("Skipping Taped Test - RGL compiled without PCL extension."); + GTEST_SKIP() << msg; +#endif +}