From 2b9a0b3e823de0177e461f38f364c82ad9ea5ef9 Mon Sep 17 00:00:00 2001 From: Troy Gibb Date: Fri, 30 Jan 2026 11:22:47 -0800 Subject: [PATCH 1/2] Remove mcap dep --- CMakeLists.txt | 2 -- cmake/install_mcap.cmake | 48 ---------------------------------------- 2 files changed, 50 deletions(-) delete mode 100644 cmake/install_mcap.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ae1b05..36c037b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,8 +20,6 @@ project(replay_testing) find_package(ament_cmake REQUIRED) find_package(ament_cmake_python REQUIRED) -include(cmake/install_mcap.cmake) - # Install Python modules and entry points ament_python_install_package(${PROJECT_NAME} SCRIPTS_DESTINATION lib/${PROJECT_NAME} diff --git a/cmake/install_mcap.cmake b/cmake/install_mcap.cmake deleted file mode 100644 index 5f864c0..0000000 --- a/cmake/install_mcap.cmake +++ /dev/null @@ -1,48 +0,0 @@ - -# Copyright (c) 2025-present Polymath Robotics, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -include(FetchContent) - -find_program(MCAP_BINARY mcap) -if(MCAP_BINARY) - message(STATUS "mcap binary found at ${MCAP_BINARY}, no need to download") -else() - message(STATUS "downloading mcap binary for arch ${CMAKE_HOST_SYSTEM_PROCESSOR}...") - - if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "x86_64") - set(MCAP_ARCH "amd64") - elseif(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "aarch64") - set(MCAP_ARCH "arm64") - else() - message(FATAL_ERROR "Unknown architecture ${CMAKE_HOST_SYSTEM_PROCESSOR}") - endif() - set(binary_name "mcap-linux-${MCAP_ARCH}") - - fetchcontent_declare( - mcap_binary - URL https://github.com/foxglove/mcap/releases/download/releases%2Fmcap-cli%2Fv0.0.47/${binary_name} - DOWNLOAD_NO_EXTRACT true - ) - fetchcontent_populate(mcap_binary) - message(STATUS "Successfully downloaded: " ${mcap_binary_SOURCE_DIR}) - - install( - PROGRAMS ${mcap_binary_SOURCE_DIR}/${binary_name} - DESTINATION bin - RENAME mcap - PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE - ) -endif() From f5df29484a961e9e11b5a410baf86627fed605f1 Mon Sep 17 00:00:00 2001 From: Troy Gibb Date: Fri, 30 Jan 2026 15:03:12 -0800 Subject: [PATCH 2/2] Replace filter utils --- replay_testing/filter.py | 49 +++++++++++++++++--------- test/test_filter.py | 75 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 test/test_filter.py diff --git a/replay_testing/filter.py b/replay_testing/filter.py index a91dde3..3df855d 100644 --- a/replay_testing/filter.py +++ b/replay_testing/filter.py @@ -13,23 +13,38 @@ # limitations under the License. # -import subprocess +import rosbag2_py def filter_mcap(input, output, output_topics): - command = [ - 'mcap', - 'filter', - input, - '-o', - output, - ] - - # Filter out topics that will be replayed - for topic in output_topics: - command.extend(['-n', topic]) - - try: - subprocess.run(command, check=True) - except subprocess.CalledProcessError as e: - print(f'Error filtering {input}: {e}') + """Filter out specified topics from an mcap file. + + Args: + input: Path to input mcap file + output: Path to output mcap file + output_topics: List of topic names to exclude from the output + """ + topics_to_exclude = set(output_topics) + + reader = rosbag2_py.SequentialReader() + reader.open( + rosbag2_py.StorageOptions(uri=str(input), storage_id='mcap'), + rosbag2_py.ConverterOptions(input_serialization_format='cdr', output_serialization_format='cdr'), + ) + + writer = rosbag2_py.SequentialWriter() + writer.open( + rosbag2_py.StorageOptions(uri=output, storage_id='mcap'), + rosbag2_py.ConverterOptions(input_serialization_format='cdr', output_serialization_format='cdr'), + ) + + # Create topics in writer, excluding filtered ones + for topic_metadata in reader.get_all_topics_and_types(): + if topic_metadata.name not in topics_to_exclude: + writer.create_topic(topic_metadata) + + # Copy messages, excluding filtered topics + while reader.has_next(): + topic_name, data, timestamp = reader.read_next() + if topic_name not in topics_to_exclude: + writer.write(topic_name, data, timestamp) diff --git a/test/test_filter.py b/test/test_filter.py new file mode 100644 index 0000000..66ad2f3 --- /dev/null +++ b/test/test_filter.py @@ -0,0 +1,75 @@ +# Copyright (c) 2025-present Polymath Robotics, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from pathlib import Path + +import rosbag2_py + +from replay_testing.filter import filter_mcap + +FIXTURES_DIR = Path(__file__).parent / 'fixtures' +CMD_VEL_MCAP = FIXTURES_DIR / 'cmd_vel_only.mcap' + + +def get_mcap_topics(mcap_path: Path) -> list[str]: + """Get list of topics from an mcap file.""" + reader = rosbag2_py.SequentialReader() + reader.open( + rosbag2_py.StorageOptions(uri=str(mcap_path), storage_id='mcap'), + rosbag2_py.ConverterOptions(input_serialization_format='cdr', output_serialization_format='cdr'), + ) + return [topic.name for topic in reader.get_all_topics_and_types()] + + +def test_filter_mcap_creates_output_file(tmp_path): + """Test that filter_mcap creates an output file.""" + output_path = tmp_path / 'filtered.mcap' + + filter_mcap( + input=str(CMD_VEL_MCAP), + output=str(output_path), + output_topics=['/vehicle/cmd_vel'], + ) + + assert output_path.exists() + + +def test_filter_mcap_preserves_matching_topic(tmp_path): + """Test that filter_mcap preserves messages matching the filter.""" + output_path = tmp_path / 'filtered.mcap' + + filter_mcap( + input=str(CMD_VEL_MCAP), + output=str(output_path), + output_topics=['/vehicle/cmd_vel'], + ) + + topics = get_mcap_topics(output_path) + assert '/vehicle/cmd_vel' not in topics + + +def test_filter_mcap_empty_topics_creates_empty_mcap(tmp_path): + """Test that filtering with non-matching topic creates mcap with no messages.""" + output_path = tmp_path / 'filtered.mcap' + + filter_mcap( + input=str(CMD_VEL_MCAP), + output=str(output_path), + output_topics=['/nonexistent/topic'], + ) + + assert output_path.exists() + topics = get_mcap_topics(output_path) + assert '/vehicle/cmd_vel' in topics