Welcome, AI Agent! This file contains the architectural context and coding guidelines for the Mobile Robot Programming Toolkit (MRPT) 3.0. When generating, refactoring, or reviewing code in this repository, you must strictly adhere to the following rules.
- MRPT 3.0 is highly modular. It is designed to be built using colcon (similar to ROS 2 packages).
- Each module (e.g.,
mrpt_opengl,mrpt_math) lives in its own directory and functions as an independent CMake project. - Target OS/Compilers: Cross-platform (Linux, Windows, macOS, WebAssembly/Emscripten).
MRPT 3.0 abstracts away standard CMake boilerplate using mrpt_common. Use standard CMake commands but prefer mrpt_common cmake helpers when possible for consistency.
-
Compiling a module: Use
colcon build --packages-up-to mrpt_XXX, then. install/setup.bashthen you can run the executables or unit tests. There is already acolcon_defaults.yamlat the repo root defining symlink install, RelWithDebInfo builds, andCMAKE_EXPORT_COMPILE_COMMANDS. -
Minimum CMake Version:
cmake_minimum_required(VERSION 3.16) -
Always include the MRPT common scripts:
find_package(mrpt_common REQUIRED)- Target Definition: Use mrpt_add_library instead of add_library. This macro automatically handles C++ standard configurations, export targets, and installation steps.
mrpt_add_library(
TARGET ${PROJECT_NAME}
SOURCES ${LIB_SOURCES} ${LIB_PUBLIC_HEADERS}
PUBLIC_LINK_LIBRARIES mrpt::another_module
CMAKE_DEPENDENCIES another_module
)Dependency Management:
- Use find_package(mrpt_<module_name> REQUIRED) to find other MRPT modules.
Link against namespaced targets (e.g., mrpt::mrpt_poses, Eigen3::Eigen).
Standard: Use Modern C++ features where appropriate (C++17/C++20).
Namespaces: All core code must reside within the mrpt:: namespace or its sub-namespaces (e.g., mrpt::opengl::).
Unit tests: test files are in a "tests" subdirectory in each module, and their name must end in _unittest.cpp, then they will be catched automatically for inclusion as unit tests.
License Headers: Every new .cpp, .h, and CMakeLists.txt file must start with the standard MRPT SPDX-License-Identifier header:
/* _
| | Mobile Robot Programming Toolkit (MRPT)
_ __ ___ _ __ _ __ | |_
| '_ ` _ \| '__| '_ \| __| https://www.mrpt.org/
| | | | | | | | |_) | |_
|_| |_| |_|_| | .__/ \__| https://github.com/MRPT/mrpt/
| |
|_|
Copyright (c) 2005-2026, Individual contributors, see AUTHORS file
See: https://www.mrpt.org/Authors - All rights reserved.
SPDX-License-Identifier: BSD-3-Clause
*/Formatting: according to .clang-format and .clang-tidy. In particular: prefer if (x) {\n y;\n } instead of if(x) y;. Use [[nodiscard]] where applicable.
Do not manually configure .so versioning or write manual install() blocks for standard headers/libraries. mrpt_add_library does this.
Do not hardcode compiler flags (like -std=c++17 or -fPIC). The MRPT CMake wrappers handle these natively.
Do not use raw pointers for ownership. Default to std::shared_ptr or std::unique_ptr, and use MRPT's smart pointer macros where applicable.
Do not expose Eigen3 headers in public API files unless explicitly allowed by the user. Keep Eigen3 specific #include's in private "src/" files.
In mrpt 3.x, most MRPT libraries under ROOT/modules/mrpt_* now have their own pybind11 module, in a modular way so users can import
only the required modules.
modules/mrpt_core/python/mrpt/__init__.pyis the main root python file for all modules, in charge of trying to import the rest, if they exist.- Each module, for example
mrpt_img, has its ownmodules/mrpt_img/python/mrpt/img/__init__.pythat must be updated with new wrapped C++ classes. - Each module, for example
mrpt_img, has its ownmodules/mrpt_img/python_bindings/mrpt_img_py.cppfile with the specific wrapped C++ classes and python adaptors. - Python examples, demonstrating each module wrapped features, live under
mrpt_examples_py. They should be updated/extended or new examples created when appropriate as new classes are wrapped.
To add or extend Python bindings for a module mrpt_foo, three files are needed:
modules/mrpt_foo/python_bindings/mrpt_foo_py.cpp— The C++ pybind11 source. Must definePYBIND11_MODULE(_bindings, m) { ... }. The compiled shared library is always named_bindings.so.modules/mrpt_foo/python/mrpt/foo/__init__.py— Python re-exports:from . import _bindings as _b, thenClassName = _b.ClassNamefor each wrapped class/function, and an__all__list.modules/mrpt_foo/CMakeLists.txt— Must callmrpt_add_python_module(foo python_bindings/${PROJECT_NAME}_py.cpp). If this line is commented out, uncomment it.
The root mrpt/__init__.py (in mrpt_core) auto-imports all known submodules by name. If adding a new module name, add it to the MRPT_MODULES list there.
Includes: Always include <pybind11/pybind11.h> and <pybind11/stl.h>. Add <pybind11/eigen.h> for Eigen/matrix types, <pybind11/numpy.h> for py::array_t<T>, <pybind11/operators.h> for operator overloading, <pybind11/chrono.h> for time types, <pybind11/functional.h> for std::function callbacks.
Class binding with inheritance:
py::class_<Derived, Base1, Base2, std::shared_ptr<Derived>>(m, "Derived")Always use std::shared_ptr<T> holder for classes that are commonly used via smart pointers. Include CSerializable in the base list for serializable classes.
Properties: When C++ uses x() getter and x(double) setter (common in MRPT), use:
.def_property("x",
[](const T& p) { return p.x(); },
[](T& p, double val) { p.x(val); })For public member variables, use .def_readwrite("name", &T::name) or .def_readonly(...).
Overloaded methods: Resolve with py::overload_cast<ArgTypes...>(&Class::method) or static_cast<RetType(Class::*)(ArgTypes...)>(&Class::method).
Output-argument functions → return values: Wrap in lambda:
.def("compute", [](const T& self) {
ResultType result;
bool ok = self.compute(result);
return py::make_tuple(ok, result);
})Operator overloading: Use py::self + py::self, etc. Always add __str__ and __repr__.
NumPy integration:
- For Eigen matrices:
#include <pybind11/eigen.h>enables automatic conversion. For zero-copy, returnEigen::Map<const RowMajorMatrix>(ptr, rows, cols). - For
CImage: Usepy::array_t<uint8_t>with shape/strides and passself_obj(the Python object) as the buffer base for zero-copy. - Add
__array__protocol:.def("__array__", [](const T& self) { return self.as_numpy(); }).
Iterators: py::make_iterator(container.begin(), container.end()) with py::keep_alive<0, 1>().
Enums: py::enum_<T>(m, "Name").value("A", T::A).export_values(); Use py::arithmetic() for bitmask enums.
GIL management: When passing Python callables to C++ threads, acquire the GIL: py::gil_scoped_acquire gil; inside the C++ callback lambda.
Return value policies: Use py::return_value_policy::reference_internal for getters returning references to internal objects (e.g., getCamera() on a Viewport). Use py::return_value_policy::reference for singletons/static data.
Python __init__.py conventions:
- Re-export all bound classes:
ClassName = _b.ClassName - Monkey-patch
__array__for NumPy integration where appropriate - Add Pythonic conveniences (e.g.,
<<operator for scene building, color constants) - Include
__all__listing all public names
For ROS 2 packages that previously depended on mrpt_ros (MRPT 2.x wrappers), see the
"Porting ROS 2 nodes" section of the porting guide at
doc/source/doxygen-docs/port_mrpt3.md for:
package.xmldependency renaming (mrpt_lib*→ fine-grainedmrpt_<module>packages)CMakeLists.txtfind_packageand target renaming (mrpt::<X>→mrpt::mrpt_<X>)- Migrating away from the dropped
mrpt_libtclap/mrpt-tclapto CLI11
- Do not use the compound command "cd foo && git ...", instead, use "git -C foo ...".
Full procedure: doc/source/make_a_mrpt_release.rst. Quick summary:
- Each module/app has its own
package.xmlversion andCHANGELOG.rst, bumped together viacatkin_prepare_release(from thecatkin_pkgPython package), run from the repo root on branchdevelop. developis then merged intomaster, tagged, and packaged withpackaging/make_release.sh(produces signed.tar.gz/.zipin$HOME/mrpt_release/).packaging/release.pyautomates the whole flow end-to-end (version bump → changelog → merge to master → tarball + GPG signature →gh release createwith the tarball/zip/signature attached for Debian'suscan), pausing for explicit confirmation before pushingmasteror publishing the GitHub release. Run with--dry-runfirst to preview the commands.- Do not run
packaging/release.pyor any step that pushes/tags/publishes unless the user explicitly asks for an actual release to be cut.
ROS "*dev" buildfarm Jenkins jobs (e.g. Kdev__mrpt3__ubuntu_noble_amd64) run
colcon build -DBUILD_TESTING=0 once, then a second, fully clean
colcon build --cmake-clean-cache -DBUILD_TESTING=1 pass that also compiles
every module's gtest binaries. This doubles build time and was observed to
blow the 120-minute Jenkins timeout mid-way through the second pass, before
colcon test was ever invoked (no test results were produced at all). The
same test suite already runs on every push/PR via GitHub Actions CI
(.github/workflows/build-linux.yml), which never sources a ROS environment.
Fix: modules/mrpt_common/cmake/mrpt_cmake_functions.cmake now forces
BUILD_TESTING back to OFF (overriding the buildfarm's explicit
-DBUILD_TESTING=1) whenever both ROS_DISTRO (sourced ROS env) and
JENKINS_URL (any Jenkins job) are set in the environment — i.e. only on the
ROS build farm itself, not for a developer who merely has ROS sourced
locally. Escape hatch: -DMRPT_FORCE_TESTS_ON_ROS_BUILDFARM=ON re-enables
tests there if ever needed. Since both mrpt_add_test() and
mrpt_add_python_binding_test() already gate on BUILD_TESTING, no other
files needed changes.
A full rebuild of all 33 modules/* packages was done with coverage
instrumentation, followed by a full colcon test run (all tests passed) and a
gcovr line/branch report. Goal: 90% line coverage per module. Current
overall (2026-07-06): 51.3% lines / 38.1% branches — well short of goal.
Per-module rows below are still the 2026-07-03 baseline except where a row is
tagged with a newer date (e.g. mrpt_math, mrpt_graphs, after their
unit-test passes).
Gotcha (2026-07-06): the system gcov alias (/etc/alternatives/gcov) may
point to an unrelated binary (observed pointing to /usr/bin/gc), causing
gcovr to silently mis-decode gcov output as garbage/UnicodeDecodeErrors.
Pass --gcov-executable gcov-<major-version-matching-the-compiler> (e.g.
gcov-13) explicitly to gcovr rather than relying on the gcov PATH
lookup.
To reproduce:
colcon build --base-paths modules --cmake-args -DENABLE_COVERAGE=ON -DBUILD_TESTING=ON
colcon test --base-paths modules
gcovr --root . -j$(nproc) --gcov-ignore-parse-errors=all \
--exclude-unreachable-branches --exclude-throw-branches \
--exclude '.*/3rdparty/.*' --exclude '.*/stb/.*' --exclude '.*/tests/.*' \
--exclude '.*_unittest\.cpp' --exclude '.*/python_bindings/.*' --exclude '.*/samples/.*' \
--json-pretty -o coverage.json build(--gcov-ignore-parse-errors=all is needed, not just negative_hits.warn_once_per_file:
large repos also trip gcovr's "suspicious hits" detector, e.g. on
mrpt_maps/src/maps/COccupancyGridMap2D_likelihood.cpp, which otherwise aborts
the whole run with a SuspiciousHits exception.)
Gotcha: with symlink-install, the same header is reported twice by gcovr
(once under modules/<pkg>/include/..., once under the symlinked
install/<pkg>/include/...). Dedupe by stripping the leading modules/ or
install/ path segment and merging line hit-counts (max) before computing
per-file/per-module percentages, or numbers will be wrong in both directions.
scripts/coverage_module_report.py coverage.json <module1> [<module2> ...]
does this dedupe and prints per-file + aggregate line/branch % for the given
module(s), e.g. scripts/coverage_module_report.py coverage.json mrpt_math.
Gotcha (2026-07-09): re-running a coverage-instrumented test binary directly
(not via colcon test) without deleting old .gcda files first causes
libgcov profiling error: ... overwriting an existing profile data with a different checksum — safe to ignore for pass/fail, but before trusting a
gcovr report, find build -iname '*.gcda' -delete and re-run colcon test
once to get a clean, single-run coverage capture.
Gotcha (2026-07-09): mrpt_math only explicitly instantiates fixed-size
matrices/vectors for a handful of dimensions (square CMatrixFixed: 2,3,4,6,
7,12; CVectorFixed: 2,3,4,5,6,7,12 — see
mrpt_math/src/MatrixVectorBase_instantiate_{CMatrixFixed,CVectorFixed}.cpp).
Writing a test that instantiates a template (e.g. CKalmanFilterCapable<...>)
with a size outside that list, such as VEH_SIZE=1, compiles but fails to
link (undefined reference to MatrixVectorBase<...>::impl_op_...). Pick a
supported size (2 is the smallest) for any new fixed-size-matrix-based test.
Gotcha (2026-08-02): with gcovr 8.6 (vs. whatever earlier version this
baseline was first taken with), the reproduce command above can now abort
mid-run with GcovrMergeAssertionError: ... Got function mrpt::containers::yaml_ref::operator=(double) on multiple lines: 1036, 1042
(a header-only template function reported at two different line numbers by
different .gcda files, e.g. one from mrpt_opengl's and one from
mrpt_img's object files). Add --merge-mode-functions=merge-use-line-min
to the gcovr invocation to resolve the ambiguity instead of failing the
whole run.
When you only touched one module and want its updated number, it's tempting
to build+test just that module (colcon build --packages-up-to mrpt_XXX,
colcon test --packages-select mrpt_XXX) and run gcovr over just
build/mrpt_XXX. This badly undercounts modules with template-heavy public
headers (e.g. CMatrixFixed.h, CMatrixDynamic.h in mrpt_math): most of
those headers' instantiation coverage comes from other modules' tests
exercising them (mrpt_poses, mrpt_obs, mrpt_maps, ... all instantiate
matrix/point/pose templates too), not from the owning module's own tests. An
isolated single-package run only credits the owning module's own test binary,
which can read 30-40 points lower than the true, whole-repo number.
To get a number comparable to the table below, you must rebuild + retest
all packages (coverage instrumentation is a global CMake cache option,
so colcon build/colcon test with no --packages-* filter reuses existing
object files and is normally fast/incremental — a few minutes, not a full
rebuild from scratch), then run gcovr once over the whole build tree and
filter with coverage_module_report.py as above. There isn't a fast, cheap,
and accurate path — pick two.
| Module | Covered/Total lines | Line % | Branch % |
|---|---|---|---|
| mrpt_imgui | 0/53 | 0.0% | 0.0% |
| mrpt_gui | 22/4621 | 0.5% | 0.2% |
| mrpt_hwdrivers | 913/6592 | 13.9% | 9.7% |
| mrpt_comms | 237/1013 | 23.4% | 11.2% |
| mrpt_kinematics | 184/482 | 38.2% | 17.9% |
| mrpt_graphslam | 257/611 | 42.1% | 37.3% |
| mrpt_opengl | 2035/4234 | 48.1% | 30.1% |
| mrpt_system | 1012/1900 | 53.3% | 39.6% |
| mrpt_io | 726/1292 | 56.2% | 39.9% |
| mrpt_libapps_cli | 1130/1910 | 59.2% | 38.0% |
| mrpt_libapps_gui | 952/1567 | 60.8% | 42.6% |
| mrpt_nav | 3928/6234 | 63.0% | 45.3% |
| mrpt_slam | 2778/4299 | 64.6% | 43.7% |
| mrpt_viz (2026-08-02) | 6449/9426 | 68.4% | 50.1% |
| mrpt_rtti | 126/176 | 71.6% | 73.5% |
| mrpt_serialization | 511/708 | 72.2% | 52.5% |
| mrpt_maps (2026-08-03)§ | 10127/11696 | 86.6% | 62.5% |
| mrpt_math (2026-07-05) | 6914/8070 | 85.7% | 57.5% |
| mrpt_core | 541/628 | 86.1% | 64.8% |
| mrpt_obs (2026-07-10) | 4631/5324 | 87.0% | 56.1% |
| mrpt_img (2026-07-10)† | 2255/2495 | 90.4% | 69.2% |
| mrpt_graphs (2026-07-06) | 1022/1111 | 92.0% | 76.7% |
| mrpt_poses | 6263/6787 | 92.3% | 59.8% |
| mrpt_containers (2026-07-11) | 1146/1234 | 92.9% | 48.2%‡ |
| mrpt_expr | 93/100 | 93.0% | 60.2% |
| mrpt_random | 160/167 | 95.8% | 85.1% |
| mrpt_bayes (2026-07-09) | 1036/1078 | 96.1% | 77.4% |
| mrpt_config (2026-07-09) | 531/548 | 96.9% | 82.3% |
| mrpt_tfest (2026-07-07) | 633/652 | 97.1% | 73.3% |
| mrpt_topography (2026-07-10) | 373/375 | 99.5% | 91.2% |
| mrpt_typemeta | 57/57 | 100.0% | 85.1% |
† mrpt_img vendors third-party src/stb/*.h (stb_image/stb_image_resize2/
stb_image_write, public-domain), which is out of scope for tests like any
other 3rd-party code and is excluded via --exclude '.*/stb/.*', now part of
the standard reproduce command above (previously a module-scoped, one-off
filter just for this row). The 2026-07-10 pass also fixed
several bugs found via new tests: a bilinear-interpolation weight collapse and
an off-by-one edge read in CMappedImage::getPixel, a hue wrap-around sign
error in rgb2hsv(), a missing distortion-model assignment in one
camera_geometry::undistort_points() overload (silently a no-op), a
division-by-zero-to-NaN risk in CImage::cross_correlation_FFT(), an
unchecked negative-window read in CImage::KLT_response(), a
saveToFile()/doc mismatch (threw instead of returning false for unknown
extensions), a missing endpoint pixel in CCanvas::line(), and a missing
implementation of CImage::saveToStreamAsJPEG() (declared but never defined,
a link error for any caller). The CImage.SSE2.cpp/CImage.SSSE3.cpp SIMD
kernels are dead code as of the stb-based rewrite (no longer called from
CImage.cpp); they are covered by direct unit tests instead, which also
found and fixed a decimation remainder-loop bug and a swapped R/B luminance
weight.
‡ mrpt_containers line and branch % both come from
scripts/coverage_module_report.py (max hit-count per line, OR-merged
(line_number, branch_index) for branches, across duplicate
template-instantiation entries, e.g. CDynamicGrid<double> vs
CDynamicGrid<int> both mapping to the same header lines); raw un-deduped
gcovr CLI output for this module reads ~87% lines / ~45% branches instead.
This module-scoped run (not the whole-repo colcon test) is
adequate here since mrpt_containers' templated headers (CDynamicGrid,
circular_buffer, ts_hash_map, deepcopy_ptr) are almost entirely exercised
by this module's own tests, unlike mrpt_math's matrix templates. New tests
added on 2026-07-11 also fixed two test-only misunderstandings (not source
bugs) around yaml's null-node vs. null-scalar distinction: operator[]
throws for a null node only via the const overload (the non-const overload
auto-vivifies into a map), and a scalar node holding std::monostate (e.g.
parsed from YAML null) is also isNullNode() == true, which is why
node_t::typeName()'s scalar-visitor branch for std::monostate (yaml.cpp)
is unreachable dead code — isNullNode() intercepts first. The shared_ptr<yaml>
alternative in scalar_t remains unreachable via the public API (no
construction call sites), same as noted before; left in place as it is part of
the variant's public type and removing it would be an ABI-affecting change.
§ mrpt_maps was raised from 48.1%/32.7% (2026-07-03 baseline) to 83.0%/59.6%
on 2026-07-17, in two passes. Pass 1 (74.5%/53.9%) added new/extended tests
for CRandomFieldGridMap2D (35%→86%, all 5 map-type representations),
CPointsMap (51%→80%), CGasConcentrationGridMap2D (13%→84%, incl. the
wind-advection simulation path), CBeaconMap/CBeacon (76%→93% / new),
COccupancyGridMap2D_insert.cpp (30%→92%), the Voronoi/critical-points code,
CObservationPointCloud (new), and customizable_obs_viz.cpp (0%→97%).
Pass 2 (83.0%/59.6%) covered the remaining gaps: COctoMap/CColouredOctoMap
(21%/15%→76%/95%, incl. the shared COctoMapBase template — insertion for
2D/3D scans, castRay, getPointOccupancy, colour update via
SET/AVERAGE/INTEGRATE), CVoxelMap/CVoxelMapRGB/CVoxelMapOccupancyBase
(27%/22%/33%→85%/77%/94%, incl. remove_voxels_farther_than and the
range-image-based colour-scan path), COccupancyGridMap3D (51%→82%, incl.
the TInsertionOptions/TRenderingOptions structs and the determineMatching2D/
compute3DMatchingRatio/internal_computeObservationLikelihood stubs — see
below), CMultiMetricMap (75%→90%, determineMatching2D, getAsSimplePointsMap,
mapByIndex, serialization), and a new CPlanarLaserScan test file (0%→96%).
Real bugs found and fixed along the way: (1) CGasConcentrationGridMap2D:: getWindAs3DObject() and ::simulateAdvection() both dereferenced
CDynamicGrid::cellByPos/cellByIndex()'s result unchecked, segfaulting
whenever a wind-grid query point/index fell outside the grid (a boundary
point at exactly the grid edge, or index drift when the main grid resizes
but the wind sub-grids don't); (2) simulateAdvection()'s variance-update
loop treated the compressed m_stackedCov (N rows × small W-window
columns) as if it were a full N×N matrix, writing out-of-bounds columns and
corrupting the heap (double free or corruption) on any grid larger than
the window; (3) TInsertionOptions::loadFromConfigFile() marked
gasSensorLabel/gasSensorType/windSensorLabel/useWindInformation/
advectionFreq all failIfNotFound=true despite each having a documented
default (making the defaults dead code and any config file missing one of
these keys throw), and separately passed the string literal "false" where
read_bool() expects an actual bool default (a non-null pointer coerces
to true, silently inverting the intended default); (4)
COccupancyGridMap2D::findCriticalPoints() computed temp_x.size() - 1
with unsigned size_t arithmetic, underflowing to SIZE_MAX and reading
wildly out of bounds whenever zero or one critical-point candidates were
found (the common case for any corridor/room without branch points — i.e.
most maps); (5) COccupancyGridMap2D::getVoronoiClearance() dereferenced
cellByIndex()'s result unchecked, segfaulting for any negative or
out-of-range (cx,cy), reachable from findCriticalPoints()'s
x-2/y-2 neighbor scan. determineMatching2D()'s bounding-box overlap
pre-filter (not expanded by maxDistForCorrespondence/
maxAngularDistForCorrespondence) can also incorrectly early-reject valid
correspondences when the two maps' raw bounding boxes don't quite touch;
identified but not fixed (a hot, SIMD-optimized path used throughout
ICP/SLAM, too risky to patch without dedicated validation) — the affected
test was reshaped to avoid the case instead. Separately, MRPT's PLY point
cloud writer (mrpt::viz::PLY_import_export.cpp) only round-trips a single
grayscale "intensity" property ((R+G+B)/3, broadcast back to all three
channels on import), not full per-channel RGB — not a bug, just a
narrower-than-expected feature scope that a new test initially assumed
incorrectly. COccupancyGridMap3D::determineMatching2D(),
::compute3DMatchingRatio(), and ::internal_computeObservationLikelihood()
are all unimplemented stubs (THROW_EXCEPTION("Implement me!"), pre-existing,
self-documented) — new tests assert the current (throwing) behavior rather
than a working implementation; actually implementing them is a real
algorithmic task, not a bug fix, and was left for a dedicated session.
CVoxelMapRGB's 3D-scan colour path and CColouredOctoMap's 3D-scan path
both unproject via a range image + camera intrinsics (hasRangeImage), NOT
via hasPoints3D/points3D_x/y/z like most other 3D-scan consumers in this
module — a manually-constructed CObservation3DRangeScan for tests needs
cameraParams.setIntrinsicParamsFromValues(...) plus a filled rangeImage
matrix, or unprojectInto() silently yields zero points. A parallel 8-agent
test-writing fleet was used for the first half of this session; all 8 hit
the account's session usage limit mid-task, but each agent's already-applied
file edits survived intact (edits are atomic) — after registering the
surviving new files and fixing the crashes/config bugs above, a second,
solo pass covered the remaining gaps the fleet never reached
(CColouredOctoMap, COctoMap, CVoxelMap/CVoxelMapRGB/
CVoxelMapOccupancyBase, COccupancyGridMap3D, CMultiMetricMap extra
coverage, and a new CPlanarLaserScan test file).
mrpt_maps was further raised from 83.0%/59.6% to 86.6%/62.5% on 2026-08-03
(pass 3), targeting the lowest-coverage files remaining after pass 2:
COccupancyGridMap2D_getAs.cpp (49%→96%, all TGetAsImageParams combinations,
getAsImageFiltered()'s gaussian/median filter branches, getVisualizationInto()
enabled/disabled, getAsPointCloud() border-cell selection), _io.cpp
(55%→93%, bitmap save/load round-trip, loadFromBitmap()'s centered-origin
sentinel, saveAsBitmapTwoMapsWithCorrespondences(),
saveMetricMapRepresentationToFile(), and the ROS map-server YAML scale/
negate/missing-image/invalid-mode branches, using new fixture files
yaml_32_{scale,negate,badmode,missing_image}.yaml), _simulate.cpp
(53%→100%, out-of-range/short-max-range rays, noisy rays,
laserScanSimulator() decimation, laserScanSimulatorWithUncertainty() for
both sumUnscented/sumMonteCarlo and its unknown-method throw path),
_likelihood.cpp (80%→90%, a parameterized test now drives all 7
TLikelihoodMethod values through the real public dispatch instead of only
lmLikelihoodField_Thrun/_II), COctoMap.cpp (75%→94%, all 7
COctoMapVoxels::visualization_mode_t coloring branches plus
generateGridLines and the const getMetricSize/Min/Max overloads),
CVoxelMapRGB.cpp (76%→91%, serialization round-trip,
remove_voxels_farther_than, the ray_trace_free_space toggle, an empty-3D-scan
early return), CRandomFieldGridMap3D.cpp (79%→92%, TInsertionOptions
load/dump, saveAsCSV() with both mean+stddev files and its failure path,
serialization round-trip, insertIndividualReading(..., update_map=true)),
and CPointsMap_crtp_common.h (68%→97%, the CObservation3DRangeScan
overload of loadFromRangeScan() — insertInvalidPoints and the
no-points-3D early return — which no test in the module exercised at all
before this pass). A parallel 5-agent test-writing fleet was used for the
first half of this pass; all 5 hit the account's session usage limit
mid-task (same failure mode as the 8-agent fleet in pass 2), but each
agent's already-applied file edits survived intact — a solo pass then fixed
the resulting test failures and covered the remaining gaps (_io.cpp,
COctoMap.cpp's coloring-mode variants, CRandomFieldGridMap3D.cpp,
CPointsMap_crtp_common.h's 3D-scan overload, and a small voronoi
three-way-junction/isolated-obstacle addition). Two new _unittest.cpp
files (COccupancyGridMap2D_getAs_unittest.cpp, _io_unittest.cpp) had to
be added by hand to mrpt_maps/CMakeLists.txt's explicit
LIB_UNIT_TEST_SOURCES list — unlike some other modules, this list is not
glob-based, so a new *_unittest.cpp file silently never runs (and never
moves the coverage numbers) until it's registered there. Real bugs found
and fixed along the way: (1) CDynamicGrid3D::dyngridcommon_readFromStream()
(mrpt_containers, used by CRandomFieldGridMap3D and CLogOddsGridMap3D)
updated m_size_x/m_size_y/m_size_z from the stream but never recomputed
the cached m_size_x_times_y z-axis stride, so deserializing into an object
whose grid dimensions differ from what it had before (e.g. any freshly
default-constructed object) left cell-index lookups using a stale stride,
silently reading out-of-bounds heap memory instead of the intended cell — a
memory-safety bug, not just wrong data; (2)
COccupancyGridMap2D::computeObservationLikelihood_ConsensusOWA() had its
"is this a CObservation2DRangeScan?" branch condition inverted
(if (IS_CLASS(...)) instead of if (!IS_CLASS(...))), so the method
returned a constant 1e-3 fallback for its only supported observation type
and fell through to an unconditional dynamic_cast-triggered bad_cast for
every other type — lmConsensusOWA was completely non-functional before this
fix; (3) TLaserSimulUncertaintyParams::method's doc comment claimed the
default was sumMonteCarlo while the member initializer actually defaults to
sumUnscented (doc-only fix). computeObservationLikelihood_ConsensusOWA()
also has two more ASSERT_-guarded preconditions that are easy to trip by
accident rather than fix (a hot likelihood-evaluation path, same "too risky
to patch without dedicated validation" reasoning as determineMatching2D
above): OWA_weights.size() must not exceed the scan's point count (default
is 100 weights), and every scan point, when re-projected from whatever pose
is being evaluated, must land within the grid bounds or a nCells > 0 assert
fires — the affected tests use a generously oversized grid and a smaller
OWA_weights vector to stay clear of both, rather than patching the source.
mrpt_viz was raised from 29.5%/17.3% (2026-07-03 baseline) to 68.4%/50.1% on
2026-08-02. mrpt_viz has zero OpenGL dependency (it's the abstract scene-graph
description consumed by mrpt_opengl, not the other way round), so almost all
of the gain came from new, direct, non-rendering unit tests added under
modules/mrpt_viz/tests/: CPolyhedron_unittest.cpp (the session's single
biggest target, CPolyhedron.cpp going from a large uncovered file to
91.6%/82.8%, exercising every Create* platonic/Archimedean/Catalan/Johnson
solid factory plus getDual/truncate/cantellate/augment/rotate/scale),
PLY_import_export_unittest.cpp, COrbitCameraController_unittest.cpp,
Scene_Viewport_unittest.cpp, CVisualObject_unittest.cpp (base class +
the four VisualObjectParams_* mixins), CGeneralizedEllipsoid_unittest.cpp
(the CEllipsoid2D/3D, InverseDepth2D/3D, RangeBearing2D family),
pose_pdfs_unittest.cpp, StockObjects_unittest.cpp, and
MiscVisualObjects_unittest.cpp (CAxis, CVectorField2D/3D, CFrustum,
CSetOfObjects, CCamera, TTriangle, COctoMapVoxels, CSkyBox, CDisk,
CBox, CGridPlaneXY/XZ, CTexturedPlane). A smaller share came from
extending the mrpt_opengl reference-image suite with
CFBORender_ExtraEllipsoidsAndMesh_unittest.cpp (the inverse-depth/
range-bearing ellipsoid parameterizations, CGridPlaneXZ, CMesh3D), the
same offscreen-render/PNG-diff technique as the pre-existing CFBORender_*
tests — that file has to live under mrpt_opengl/tests/ rather than
mrpt_viz/tests/ given the one-way dependency, but the coverage it generates
is still credited to mrpt_viz's .gcda files since coverage instrumentation
is per-compiled-object, not per-test-binary. Real bugs found and fixed along
the way: (1) CPolyhedron::InitFromVertAndFaces() never assigned
m_Vertices/m_Faces from its parameters, only from whatever the member
variables already held — harmless for the two constructors that pre-populate
those members via their initializer list before calling it, but the
CPolyhedron(vertices, vector<vector<uint32_t>> faces) convenience
constructor left them empty, silently producing a 0-vertex/0-face polyhedron;
(2) PLY_Importer::loadFromPlyFile() segfaulted (null-pointer dereference in
ply_close()) instead of returning false for a nonexistent/unreadable
file, since ply_open_for_reading() returning nullptr on fopen() failure
was never checked; (3) CMesh3D::loadMesh()'s (raw-pointer overload)
triangle face-normal computation read vertex index slot [3], which for a
triangle (as opposed to a quad) is the unused -1 sentinel cast to
uint32_t (4294967295), producing an out-of-bounds std::vector read and
a segfault for any mesh with enableFaceNormals(true) and at least one
triangle face — a CMesh3D::loadMesh(is_quad, face_verts, vert_coords)
matrix-based overload right below it had the equivalent code correct, which
is what gave away the right fix. Also fixed: the CreateJohnsonSolidWithConstantBase()
API-doc example table in CPolyhedron.h listed "C+PRC-" for an 8-vertex
rhombicuboctahedron, but the parser requires the downward-cupola component
(C-) first and the upward one (C+) last (confirmed against
CreateRandomPolyhedron()'s own internal usage) — the doc string was
backwards and has been corrected to "C-PRC+". Remaining gaps below ~55%:
CAnimatedAssimpModel.cpp/CAssimpModel.cpp (0%, need an external 3D model
file plus system assimp — not attempted), CTextMessageCapable.cpp (0%,
on-screen text-overlay list, unexercised by any offscreen-render or unit
test), opengl_fonts.h (0%, embedded glyph bitmap data), and
CSetOfTexturedTriangles.cpp (2.1%, needs a rendering-based test since its
logic is mostly buffer-upload bookkeping).
-
Hardware drivers —
mrpt_hwdrivers(13.9%), most ofmrpt_comms(23.4%): inherently hard to unit-test since they talk to real serial ports/USB/GPS/ LIDAR/cameras (CHokuyoURG,CSickLaserSerial,COpenNI2Generic,CVelodyneScanner,CSerialPort,CNTRIPClient,CKinect, etc., all at 0%). Improving this needs a mockable transport layer (inject a fakeCStream/socket) rather than plain unit tests against hardware. -
GUI/rendering —
mrpt_gui(0.5%),mrpt_imgui(0%), and GUI-only files insidemrpt_viz/mrpt_opengl:mathplot.cpp,CDisplayWindow*.cpp,WxUtils.cpp,CWxGLCanvasBase.cpp,CQtGlCanvasBase.cpp,CImGuiSceneView.cppneed a live display/OpenGL context and are 0%. Realistic path to improvement is extracting non-UI logic into testable helpers, or headless/offscreen-context tests, not brute-force unit tests. -
CLI apps —
mrpt_libapps_cli(59.2% as of 2026-07-06, was 9.1%): somerawlog-edit_*.cpppaths remain untested. These are better suited to subprocess/golden-file integration tests (run the built binary against sample rawlogs, diff the output) than pure unit tests. -
Quick wins — pure-logic files at 0% with no hardware/GUI dependency (highest-value gaps, ordinary unit tests would work immediately):
mrpt_system/src/md5.cpp,mrpt_graphslam/src/{CEdgeCounter,TSlidingWindow, CWindowObserver}.cpp,mrpt_slam/src/slam/CRejectionSamplingRangeOnlyLocalization.cpp. (mrpt_img/src/CImage_loadXPM.cppcleared this bucket as of 2026-07-10, now ~90%;mrpt_obs/src/gnss_messages_novatel.cppandmrpt_obs/src/carmen_log_tools.cppalso cleared as of 2026-07-10, now at 100% and 88.2% respectively;mrpt_viz/src/PLY_import_export.cppandmrpt_viz/src/COrbitCameraController.cppcleared as of 2026-08-02, now at 54.7% and 100% respectively.) -
Biggest single-file impact (most uncovered lines, worth prioritizing for raw percentage gains):
mrpt_maps/src/maps/COccupancyGridMap2D_io.cpp(85, 56.0%),mrpt_maps/src/maps/COccupancyGridMap2D_simulate.cpp(49, 53.3%). (mrpt_viz/src/CPolyhedron.cpp, formerly the single biggest uncovered file in the whole repo at 1420 uncovered lines, cleared this bucket as of 2026-08-02, now at 91.6%.) (mrpt_obs/src/CObservation3DRangeScan.cppcleared this bucket as of 2026-07-10, now at 89.1%;mrpt_maps/src/maps/CRandomFieldGridMap2D.cpp,CPointsMap.cpp,CGasConcentrationGridMap2D.cpp,CColouredOctoMap.cpp,COctoMap.cpp, andCOccupancyGridMap3D.cppall cleared it as of 2026-07-17 — see § above.) -
Near-target modules (75-90%), smallest remaining gap to close first: none currently flagged. (
mrpt_graphsandmrpt_randomcleared this bucket as of 2026-07-06;mrpt_bayesandmrpt_configcleared it as of 2026-07-09, both now >96%;mrpt_obsimproved to 87.0% as of 2026-07-10 but is still within this range;mrpt_containerscleared it as of 2026-07-11, now at 92.9%, remaining gaps being mostly defensive "should never happen" throws and libfyaml parser error paths that are difficult to trigger without a malformed internal parser state.)
Branch coverage lags line coverage everywhere (often by 15-30 points), indicating error-handling and edge-case branches are the norm left untested even in files with decent line coverage — prioritize adding failure-path tests, not just more happy-path calls.