🧪
Informe de Testing — sensor_fusion
GoogleTest 1.17.0 · CMake 3.29 · C++20 · Build:
Debug+Cov · 18 Jun 2026 14:32 UTC
41
Passed
3
Failed
2
Flaky
87.4%
Cobertura
1.24s
Duración
📊 Cobertura por Archivo (lcov + genhtml)
Archivo
Líneas cubiertas
%
Líneas
Ramas
kalman_filter.cpp
96%
144/150
91%
sensor_aggregator.cpp
84%
210/250
74%
collision_detector.cpp
82%
164/200
79%
imu_parser.cpp
91%
91/100
88%
gps_parser.cpp
65%
65/100
52%
TOTAL sensor_fusion/
87.4%
674/800
✓ Meta
🟢
KalmanFilterTest
12/12 passed
420ms
✓PredictState_UpdatesPositionCorrectly12ms
✓PredictState_ZeroNoise_StableCovariance8ms
✓Update_CorrectsMeasurementError14ms
✓Update_RejectsNaNMeasurement3ms
✓Reset_ClearsStateToInitial2ms
✓Parametrized_MultipleNoiseLevels/011ms
✓Parametrized_MultipleNoiseLevels/19ms
✓Parametrized_MultipleNoiseLevels/210ms
✓Covariance_PositiveDefiniteAfterInit5ms
✓LargeTimeStep_DoesNotDiverge18ms
✓SingularMatrix_ThrowsOrSaturates6ms
✓IMUIntegration_AccumulatesVelocity32ms
🔴
SensorAggregatorTest
16/18 passed
2 failed
580ms
✓RegisterSensor_AddsToActiveList3ms
✓Normalize_ScalesIMUToMetricUnits5ms
✓Normalize_ScalesGPSToMeters4ms
✓AggregateOnce_ProducesValidFusedFrame14ms
✓Aggregate_NoSensors_ReturnsEmptyFrame2ms
✓Mock_NotifierCalledOnFrameReady6ms
✓Mock_ErrorCallbackOnSensorTimeout8ms
✗ConcurrentAggregate_NoCrashUnderLoadFAIL
✗ConcurrentAggregate_DataConsistencyFAIL
✓DropLiDAR_FallsBackToIMUGPS11ms
✓TimestampAlignment_RejectsStaleFrames7ms
✓HighFrequencyIMU_BufferDoesNotOverflow44ms
✓GPSJitter_SmoothingFilterApplied9ms
✓Calibration_OffsetAppliedToAllChannels8ms
✓Serialize_DeserializeFrame_Roundtrip12ms
✓Metrics_IncrementOnEachAggregation5ms
~RealNetworkLatency_SkippedInCISKIP
~HardwareInterrupt_SkippedWithoutHWSKIP
✗ ConcurrentAggregate_NoCrashUnderLoad
[ RUN ] SensorAggregatorTest.ConcurrentAggregate_NoCrashUnderLoad
Sanitizer: ThreadSanitizer FATAL — data race detected
Thread T3 WRITE: sensor_aggregator.cpp:187 SensorAggregator::Aggregate()
Thread T1 READ: sensor_aggregator.cpp:143 SensorAggregator::GetLatestFrame()
Lock: std::mutex m_frameMutex — not held on write path
FIX: lock m_frameMutex in Aggregate() before writing m_latestFrame
🟡
CollisionDetectorTest
14/16 passed
1 failed · 1 flaky
240ms
✓DetectObstacle_PointWithinSafeZone_NoAlert8ms
✓DetectObstacle_PointBelowThreshold_Alert7ms
✓EmptyPointCloud_NoAlert2ms
✓SparseCloud_IgnoresNoise14ms
✓MockObstacleNotifier_CalledOnAlert5ms
✗Parametrized_DistanceThresholds/2FAIL
~TimingBased_FlakeyOnSlowCIFLAKY
✓VoxelGrid_ReducesDensityBy80Pct32ms
✓GroundPlane_Removed_Before_Detect18ms
✓Fake_PointCloudSource_ReturnsFixedData4ms
✓MaxRange_FiltersDistantPoints9ms
✓MultipleClusters_AllAlerted21ms
✓DynamicObstacle_VelocityEstimated27ms
✓Pose_TransformApplied_WorldFrame11ms
✓SafeZone_Polygon_4Vertices13ms
✓SafeZone_Polygon_6Vertices14ms
✗ Parametrized_DistanceThresholds/2
[ RUN ] CollisionDetectorTest.Parametrized_DistanceThresholds/2
tests/collision_detector_test.cpp:94
Expected: detector.IsObstacle({0.0f, 0.0f, 0.45f}) == true
Actual: false
Off-by-one in boundary condition: threshold comparison uses < instead of <=
FIX: collision_detector.cpp:67 — change 'dist < threshold_' to 'dist <= threshold_'
💻 Código de Tests Generado
kalman_filter_test.cpp
sensor_aggregator_test.cpp
CMakeLists.txt
ci.yml
#include <gtest/gtest.h> #include <cmath> #include "sensor_fusion/kalman_filter.hpp" namespace neuronav { namespace { // ── Fixture ──────────────────────────────────────────────── class KalmanFilterTest : public ::testing::Test { protected: void SetUp() override { KalmanFilter::Config cfg; cfg.process_noise = 1e-4f; cfg.measurement_noise = 1e-2f; cfg.initial_covariance = 1.0f; filter_ = std::make_unique<KalmanFilter>(cfg); } std::unique_ptr<KalmanFilter> filter_; }; // ── Tests básicos ────────────────────────────────────────── TEST_F(KalmanFilterTest, PredictState_UpdatesPositionCorrectly) { filter_->Init({0.0f, 0.0f, 0.0f}); filter_->Predict(dt_10ms); auto state = filter_->GetState(); // posición debe seguir en origen con cero velocidad inicial EXPECT_NEAR(state.x, 0.0f, 1e-5f); EXPECT_NEAR(state.y, 0.0f, 1e-5f); } TEST_F(KalmanFilterTest, Update_CorrectsMeasurementError) { filter_->Init({0.0f, 0.0f, 0.0f}); filter_->Predict(dt_10ms); filter_->Update({1.0f, 0.5f, 0.0f}); // medición GPS auto state = filter_->GetState(); EXPECT_GT(state.x, 0.0f); // corregido hacia la medición EXPECT_LT(state.x, 1.0f); // pero no igual (Kalman gain < 1) } TEST_F(KalmanFilterTest, Update_RejectsNaNMeasurement) { filter_->Init({0.0f, 0.0f, 0.0f}); EXPECT_THROW( filter_->Update({std::numeric_limits<float>::quiet_NaN(), 0.0f, 0.0f}), std::invalid_argument ); } // ── Parametrizado: múltiples niveles de ruido ────────────── struct NoiseParams { float q, r; }; class KalmanNoiseTest : public testing::TestWithParam<NoiseParams> {}; TEST_P(KalmanNoiseTest, Parametrized_MultipleNoiseLevels) { auto [q, r] = GetParam(); KalmanFilter::Config cfg{.process_noise=q, .measurement_noise=r}; KalmanFilter f(cfg); f.Init({0,0,0}); f.Predict(0.01f); f.Update({1,0,0}); EXPECT_TRUE(f.IsCovariancePositiveDefinite()); } INSTANTIATE_TEST_SUITE_P(NoiseMatrix, KalmanNoiseTest, testing::Values( NoiseParams{1e-5f, 1e-2f}, NoiseParams{1e-3f, 1e-2f}, NoiseParams{1e-1f, 1e-2f} )); } // namespace } // namespace neuronav
#include <gmock/gmock.h> #include <gtest/gtest.h> #include <thread> #include "sensor_fusion/sensor_aggregator.hpp" #include "sensor_fusion/frame_notifier.hpp" namespace neuronav { namespace { using ::testing::StrictMock; using ::testing::Exactly; // ── Mock del notificador de frames ───────────────────────── class MockFrameNotifier : public FrameNotifier { public: MOCK_METHOD(void, OnFrameReady, (const FusedFrame&), (override)); MOCK_METHOD(void, OnSensorError, (const std::string&), (override)); }; // ── Fake de fuente de sensor (estado fijo) ───────────────── class FakeIMUSource : public SensorSource { public: SensorReading Read() override { return {.accel={0.0f,0.0f,-9.81f}, .ts=1000}; } }; // ── Test: mock notificador llamado en OnFrameReady ───────── TEST(SensorAggregatorTest, Mock_NotifierCalledOnFrameReady) { StrictMock<MockFrameNotifier> notifier; SensorAggregator agg(notifier); agg.Register(std::make_unique<FakeIMUSource>(), SensorType::kIMU); EXPECT_CALL(notifier, OnFrameReady(::testing::_)) .Times(Exactly(1)); agg.Aggregate(); } // ── Test concurrencia (detecta race con TSan) ────────────── TEST(SensorAggregatorTest, ConcurrentAggregate_DataConsistency) { MockFrameNotifier notifier; ON_CALL(notifier, OnFrameReady(::testing::_)).WillByDefault(::testing::Return()); SensorAggregator agg(notifier); agg.Register(std::make_unique<FakeIMUSource>(), SensorType::kIMU); std::atomic<bool> stop{false}; std::thread reader([&]{ while(!stop) { auto f = agg.GetLatestFrame(); // READ sin lock → data race (void)f; } }); for(int i=0; i<100; ++i) agg.Aggregate(); stop = true; reader.join(); // TSan detectará el data race → FIX: lock m_frameMutex en Aggregate() } } // namespace } // namespace neuronav
## CMakeLists.txt — sensor_fusion tests cmake_minimum_required(VERSION 3.20) project(neuronav_sensor_fusion LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) ## ─── GoogleTest (pinned) ──────────────────────────────────── include(FetchContent) FetchContent_Declare(googletest URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip ) FetchContent_MakeAvailable(googletest) ## ─── Librería de producción ───────────────────────────────── add_library(sensor_fusion src/kalman_filter.cpp src/sensor_aggregator.cpp src/collision_detector.cpp src/imu_parser.cpp src/gps_parser.cpp ) target_include_directories(sensor_fusion PUBLIC include/) target_compile_options(sensor_fusion PRIVATE -Wall -Wextra -Wpedantic) ## ─── Ejecutable de tests ──────────────────────────────────── add_executable(neuronav_tests tests/unit/kalman_filter_test.cpp tests/unit/sensor_aggregator_test.cpp tests/unit/collision_detector_test.cpp tests/integration/full_pipeline_test.cpp ) target_link_libraries(neuronav_tests sensor_fusion GTest::gtest GTest::gmock GTest::gtest_main ) enable_testing() include(GoogleTest) gtest_discover_tests(neuronav_tests PROPERTIES LABELS "unit" DISCOVERY_TIMEOUT 30 ) ## ─── Sanitizers (opcionales) ──────────────────────────────── option(ENABLE_ASAN "AddressSanitizer" OFF) option(ENABLE_TSAN "ThreadSanitizer" OFF) option(ENABLE_UBSAN "UndefinedBehaviorSanitizer" OFF) option(ENABLE_COVERAGE "gcov coverage flags" OFF) if(ENABLE_ASAN) target_compile_options(neuronav_tests PRIVATE -fsanitize=address -fno-omit-frame-pointer) target_link_options(neuronav_tests PRIVATE -fsanitize=address) endif() if(ENABLE_TSAN) target_compile_options(neuronav_tests PRIVATE -fsanitize=thread) target_link_options(neuronav_tests PRIVATE -fsanitize=thread) endif() if(ENABLE_COVERAGE) target_compile_options(neuronav_tests PRIVATE --coverage) target_link_options(neuronav_tests PRIVATE --coverage) endif()
# .github/workflows/ci.yml name: C++ CI — sensor_fusion on: push: { branches: [main, develop, "feat/*"] } pull_request: { branches: [main] } jobs: build-and-test: runs-on: ubuntu-24.04 strategy: matrix: build_type: [Debug, Release] steps: - uses: actions/checkout@v4 - name: Configure run: | cmake -S . -B build \ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ -DENABLE_ASAN=ON -DENABLE_UBSAN=ON - name: Build run: cmake --build build -j$(nproc) - name: Run subset first (fast feedback) run: | ctest --test-dir build \ -L unit --output-on-failure -j4 - name: Run full suite run: | ctest --test-dir build \ --output-on-failure -j4 tsan: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Build with TSan run: | cmake -S . -B build-tsan \ -DENABLE_TSAN=ON -DCMAKE_BUILD_TYPE=Debug cmake --build build-tsan -j$(nproc) - name: Run TSan (race detector) run: | ctest --test-dir build-tsan \ -R "Concurrent" --output-on-failure coverage: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Build with coverage run: | cmake -S . -B build-cov \ -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug cmake --build build-cov -j$(nproc) ctest --test-dir build-cov lcov --capture --directory build-cov -o cov.info lcov --remove cov.info '/usr/*' '*/_deps/*' -o cov.info genhtml cov.info -o coverage-report - uses: actions/upload-artifact@v4 with: { name: coverage-report, path: coverage-report/ } - name: Coverage gate (≥85%) run: | PCT=$(lcov --summary cov.info 2>&1 | grep lines | grep -oP '\d+\.\d+') python3 -c "assert float('$PCT') >= 85, f'Coverage {PCT}% < 85%'"
🔬
Resultados de Sanitizers
TSan — DATA RACE DETECTADO
Race condition entre hilo escritor (Aggregate) y lector (GetLatestFrame) sobre m_latestFrame sin lock.
WARNING: ThreadSanitizer: data race (pid=18247)
Write of size 128 at 0x7f3a208c0120
#0 SensorAggregator::Aggregate() sensor_aggregator.cpp:187
#1 SensorAggregatorTest_Concurrent+0x1a8
Previous read of size 128 at 0x7f3a208c0120
#0 SensorAggregator::GetLatestFrame() sensor_aggregator.cpp:143
#1 test reader thread T3
FIX ▶ Add std::lock_guard<std::mutex> lg(m_frameMutex_) in Aggregate() write path
ASan + UBSan — CLEAN
Sin fugas de memoria, desbordamientos ni comportamiento indefinido en las 41 pruebas pasadas.
PASS: AddressSanitizer — 0 errors detected
PASS: UndefinedBehaviorSanitizer — 0 errors detected
Build type: Debug | -fsanitize=address,undefined -fno-omit-frame-pointer
Tests: 43 total | 41 passed | 2 sanitizer-triggered failures (TSan only)
🔄 Pipeline CI — Estado tras este PR
✓ Configure
cmake -S . -B build
→
✓ Build
cmake --build
→
✓ Subset
ctest -L unit
→
✗ Full Suite
3 failures
→
✗ TSan
race detected
→
✓ Coverage
87.4% ≥ 85%
→
→ Merge
bloqued: 2 gates
💻
ctest --test-dir build --output-on-failure
-- Running tests in /neuronav/build (46 total)
Test project /neuronav/build
Start 1: KalmanFilterTest.PredictState_UpdatesPositionCorrectly
1/46 Test #1: KalmanFilterTest.PredictState_UpdatesPositionCorrectly ..... Passed 12 ms
2/46 Test #2: KalmanFilterTest.PredictState_ZeroNoise_StableCovariance .. Passed 8 ms
3/46 Test #3: KalmanFilterTest.Update_CorrectsMeasurementError .......... Passed 14 ms
4/46 Test #4: KalmanFilterTest.Update_RejectsNaNMeasurement ............. Passed 3 ms
[... 37 more ...]
39/46 Test #39: SensorAggregatorTest.ConcurrentAggregate_NoCrashUnderLoad . FAILED (TSan)
40/46 Test #40: SensorAggregatorTest.ConcurrentAggregate_DataConsistency .. FAILED (TSan)
43/46 Test #43: CollisionDetectorTest.Parametrized_DistanceThresholds/2 ... FAILED off-by-one
45/46 Test #45: CollisionDetectorTest.TimingBased_FlakeyOnSlowCI .......... FLAKY (2/5 runs)
46/46 Test #46: CollisionDetectorTest.SafeZone_Polygon_6Vertices .......... Passed 14 ms
──────────────────────────────────────────────────────────────────────
41 tests passed, 3 tests failed, 2 flaky (46 total) in 1.24 sec
FAILED: ConcurrentAggregate_NoCrashUnderLoad, ConcurrentAggregate_DataConsistency
Parametrized_DistanceThresholds/2
FLAKY: TimingBased_FlakeyOnSlowCI (uses sleep-based sync → replace with condvar)
🎯 Plan de Acción — Próximos Pasos
-
FIX URGENTE — Race condition en SensorAggregator Añadir
std::lock_guard<std::mutex> lg(m_frameMutex_);al inicio deAggregate(). TSan confirmará clean en siguiente build. -
FIX — Off-by-one en CollisionDetector::IsObstacle() Cambiar
dist < threshold_→dist <= threshold_en línea 67. Parametrized/2 pasará inmediatamente. -
Eliminar flakiness — TimingBased_FlakeyOnSlowCI Reemplazar
std::this_thread::sleep_for()porstd::condition_variable+std::latch. -
Subir cobertura de gps_parser.cpp (65% → 85%) Añadir tests para paths de error: NMEA malformado, pérdida de señal, cambio de datum. Estimado: +4 tests.
-
Añadir fuzzing libFuzzer para CollisionDetector::ProcessCloud() Función pura sobre bytes arbitrarios → candidata ideal para harness libFuzzer en el pipeline de CI nocturno.