Back to Repositories

Testing GLONASS Navigation Message Parser in OpenPilot

This test suite validates the GLONASS navigation message parsing functionality in the OpenPilot system, focusing on the accurate decoding of different string types and their respective data fields. The tests ensure reliable processing of GLONASS satellite data for navigation purposes.

Test Coverage Overview

The test suite provides comprehensive coverage for GLONASS navigation message parsing, encompassing all five immediate data string types and non-immediate data strings. Key test areas include:
  • String type identification and validation
  • Data field extraction and conversion
  • Sign bit handling for numerical values
  • Padding and frame number verification
  • Hamming code validation

Implementation Analysis

The testing approach employs a structured pattern using Catch2 framework and custom data generation utilities. Each test case follows a consistent pattern of data generation, stream creation, parsing, and validation against expected values. The implementation leverages C++ templates and static casting for type-safe data handling.

Technical patterns include data stream manipulation, bitwise operations, and precise memory management for navigation data structures.

Technical Details

Testing tools and components include:
  • Catch2 testing framework
  • Kaitai Struct for binary parsing
  • Custom data generation utilities
  • Bitset operations for binary data handling
  • Vector-based test data structures
  • Random number generation for edge case testing

Best Practices Demonstrated

The test suite exemplifies several testing best practices including systematic test organization, comprehensive data validation, and robust error checking. Notable practices include:
  • Separation of test data generation from validation logic
  • Consistent error checking patterns
  • Comprehensive coverage of edge cases
  • Type-safe data handling
  • Clear test case organization by message type

commaai/openpilot

system/ubloxd/tests/test_glonass_kaitai.cc

            
#include <iostream>
#include <vector>
#include <bitset>
#include <cassert>
#include <cstdlib>
#include <ctime>

#include "catch2/catch.hpp"
#include "system/ubloxd/generated/glonass.h"

typedef std::vector<std::pair<int, int64_t>> string_data;

#define IDLE_CHIP_IDX 0
#define STRING_NUMBER_IDX 1
// string data 1-5
#define HC_IDX 0
#define PAD1_IDX 1
#define SUPERFRAME_IDX 2
#define PAD2_IDX 3
#define FRAME_IDX 4

// Indexes for string number 1
#define ST1_NU_IDX 2
#define ST1_P1_IDX 3
#define ST1_T_K_IDX 4
#define ST1_X_VEL_S_IDX 5
#define ST1_X_VEL_V_IDX 6
#define ST1_X_ACCEL_S_IDX 7
#define ST1_X_ACCEL_V_IDX 8
#define ST1_X_S_IDX 9
#define ST1_X_V_IDX 10
#define ST1_HC_OFF 11

// Indexes for string number 2
#define ST2_BN_IDX 2
#define ST2_P2_IDX 3
#define ST2_TB_IDX 4
#define ST2_NU_IDX 5
#define ST2_Y_VEL_S_IDX 6
#define ST2_Y_VEL_V_IDX 7
#define ST2_Y_ACCEL_S_IDX 8
#define ST2_Y_ACCEL_V_IDX 9
#define ST2_Y_S_IDX 10
#define ST2_Y_V_IDX 11
#define ST2_HC_OFF 12

// Indexes for string number 3
#define ST3_P3_IDX 2
#define ST3_GAMMA_N_S_IDX 3
#define ST3_GAMMA_N_V_IDX 4
#define ST3_NU_1_IDX 5
#define ST3_P_IDX 6
#define ST3_L_N_IDX 7
#define ST3_Z_VEL_S_IDX 8
#define ST3_Z_VEL_V_IDX 9
#define ST3_Z_ACCEL_S_IDX 10
#define ST3_Z_ACCEL_V_IDX 11
#define ST3_Z_S_IDX 12
#define ST3_Z_V_IDX 13
#define ST3_HC_OFF 14

// Indexes for string number 4
#define ST4_TAU_N_S_IDX 2
#define ST4_TAU_N_V_IDX 3
#define ST4_DELTA_TAU_N_S_IDX 4
#define ST4_DELTA_TAU_N_V_IDX 5
#define ST4_E_N_IDX 6
#define ST4_NU_1_IDX 7
#define ST4_P4_IDX 8
#define ST4_F_T_IDX 9
#define ST4_NU_2_IDX 10
#define ST4_N_T_IDX 11
#define ST4_N_IDX 12
#define ST4_M_IDX 13
#define ST4_HC_OFF 14

// Indexes for string number 5
#define ST5_N_A_IDX 2
#define ST5_TAU_C_IDX 3
#define ST5_NU_IDX 4
#define ST5_N_4_IDX 5
#define ST5_TAU_GPS_IDX 6
#define ST5_L_N_IDX 7
#define ST5_HC_OFF 8

// Indexes for non immediate
#define ST6_DATA_1_IDX 2
#define ST6_DATA_2_IDX 3
#define ST6_HC_OFF 4


std::string generate_inp_data(string_data& data) {
  std::string inp_data = "";
  for (auto& [b, v] : data) {
    std::string tmp = std::bitset<64>(v).to_string();
    inp_data += tmp.substr(64-b, b);
  }
  assert(inp_data.size() == 128);

  std::string string_data;
  string_data.reserve(16);
  for (int i = 0; i < 128; i+=8) {
    std::string substr = inp_data.substr(i, 8);
    string_data.push_back((uint8_t)std::stoi(substr.c_str(), 0, 2));
  }

  return string_data;
}

string_data generate_string_data(uint8_t string_number) {

  srand((unsigned)time(0));
  string_data data; //<bit length, value>
  data.push_back({1, 0}); // idle chip
  data.push_back({4, string_number}); // string number

  if (string_number == 1) {
    data.push_back({2, 3}); // not_used
    data.push_back({2, 1}); // p1
    data.push_back({12, 113}); // t_k
    data.push_back({1, rand() & 1}); // x_vel_sign
    data.push_back({23, 7122}); // x_vel_value
    data.push_back({1, rand() & 1}); // x_accel_sign
    data.push_back({4, 3}); // x_accel_value
    data.push_back({1, rand() & 1}); // x_sign
    data.push_back({26, 33554431}); // x_value
  } else if (string_number == 2) {
    data.push_back({3, 3}); // b_n
    data.push_back({1, 1}); // p2
    data.push_back({7, 123}); // t_b
    data.push_back({5, 31}); // not_used
    data.push_back({1, rand() & 1}); // y_vel_sign
    data.push_back({23, 7422}); // y_vel_value
    data.push_back({1, rand() & 1}); // y_accel_sign
    data.push_back({4, 3}); // y_accel_value
    data.push_back({1, rand() & 1}); // y_sign
    data.push_back({26, 67108863}); // y_value
  } else if (string_number == 3) {
    data.push_back({1, 0}); // p3
    data.push_back({1, 1}); // gamma_n_sign
    data.push_back({10, 123}); // gamma_n_value
    data.push_back({1, 0}); // not_used
    data.push_back({2, 2}); // p
    data.push_back({1, 1}); // l_n
    data.push_back({1, rand() & 1}); // z_vel_sign
    data.push_back({23, 1337}); // z_vel_value
    data.push_back({1, rand() & 1}); // z_accel_sign
    data.push_back({4, 9}); // z_accel_value
    data.push_back({1, rand() & 1}); // z_sign
    data.push_back({26, 100023}); // z_value
  } else if (string_number == 4) {
    data.push_back({1, rand() & 1}); // tau_n_sign
    data.push_back({21, 197152}); // tau_n_value
    data.push_back({1, rand() & 1}); // delta_tau_n_sign
    data.push_back({4, 4}); // delta_tau_n_value
    data.push_back({5, 0}); // e_n
    data.push_back({14, 2}); // not_used_1
    data.push_back({1, 1}); // p4
    data.push_back({4, 9}); // f_t
    data.push_back({3, 3}); // not_used_2
    data.push_back({11, 2047}); // n_t
    data.push_back({5, 2}); // n
    data.push_back({2, 1}); // m
  } else if (string_number == 5) {
    data.push_back({11, 2047}); // n_a
    data.push_back({32, 4294767295}); // tau_c
    data.push_back({1, 0}); // not_used_1
    data.push_back({5, 2}); // n_4
    data.push_back({22, 4114304}); // tau_gps
    data.push_back({1, 0}); // l_n
  } else { // non-immediate data is not parsed
    data.push_back({64, rand()}); // data_1
    data.push_back({8, 6}); // data_2
  }

  data.push_back({8, rand() & 0xFF}); // hamming code
  data.push_back({11, rand() & 0x7FF}); // pad
  data.push_back({16, rand() & 0xFFFF}); // superframe
  data.push_back({8, rand() & 0xFF}); // pad
  data.push_back({8, rand() & 0xFF}); // frame
  return data;
}

TEST_CASE("parse_string_number_1"){
  string_data data = generate_string_data(1);
  std::string inp_data = generate_inp_data(data);

  kaitai::kstream stream(inp_data);
  glonass_t gl_string(&stream);

  REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second);
  REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second);
  REQUIRE(gl_string.hamming_code() == data[ST1_HC_OFF + HC_IDX].second);
  REQUIRE(gl_string.pad_1() == data[ST1_HC_OFF + PAD1_IDX].second);
  REQUIRE(gl_string.superframe_number() == data[ST1_HC_OFF + SUPERFRAME_IDX].second);
  REQUIRE(gl_string.pad_2() == data[ST1_HC_OFF + PAD2_IDX].second);
  REQUIRE(gl_string.frame_number() == data[ST1_HC_OFF + FRAME_IDX].second);

  kaitai::kstream str1(inp_data);
  glonass_t str1_data(&str1);
  glonass_t::string_1_t* s1 = static_cast<glonass_t::string_1_t*>(str1_data.data());

  REQUIRE(s1->not_used() == data[ST1_NU_IDX].second);
  REQUIRE(s1->p1() == data[ST1_P1_IDX].second);
  REQUIRE(s1->t_k() == data[ST1_T_K_IDX].second);

  int mul = s1->x_vel_sign() ? (-1) : 1;
  REQUIRE(s1->x_vel() == (data[ST1_X_VEL_V_IDX].second * mul));
  mul = s1->x_accel_sign() ? (-1) : 1;
  REQUIRE(s1->x_accel() == (data[ST1_X_ACCEL_V_IDX].second * mul));
  mul = s1->x_sign() ? (-1) : 1;
  REQUIRE(s1->x() == (data[ST1_X_V_IDX].second * mul));
}

TEST_CASE("parse_string_number_2"){
  string_data data = generate_string_data(2);
  std::string inp_data = generate_inp_data(data);

  kaitai::kstream stream(inp_data);
  glonass_t gl_string(&stream);

  REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second);
  REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second);
  REQUIRE(gl_string.hamming_code() == data[ST2_HC_OFF + HC_IDX].second);
  REQUIRE(gl_string.pad_1() == data[ST2_HC_OFF + PAD1_IDX].second);
  REQUIRE(gl_string.superframe_number() == data[ST2_HC_OFF + SUPERFRAME_IDX].second);
  REQUIRE(gl_string.pad_2() == data[ST2_HC_OFF + PAD2_IDX].second);
  REQUIRE(gl_string.frame_number() == data[ST2_HC_OFF + FRAME_IDX].second);

  kaitai::kstream str2(inp_data);
  glonass_t str2_data(&str2);
  glonass_t::string_2_t* s2 = static_cast<glonass_t::string_2_t*>(str2_data.data());

  REQUIRE(s2->b_n() == data[ST2_BN_IDX].second);
  REQUIRE(s2->not_used() == data[ST2_NU_IDX].second);
  REQUIRE(s2->p2() == data[ST2_P2_IDX].second);
  REQUIRE(s2->t_b() == data[ST2_TB_IDX].second);
  int mul = s2->y_vel_sign() ? (-1) : 1;
  REQUIRE(s2->y_vel() == (data[ST2_Y_VEL_V_IDX].second * mul));
  mul = s2->y_accel_sign() ? (-1) : 1;
  REQUIRE(s2->y_accel() == (data[ST2_Y_ACCEL_V_IDX].second * mul));
  mul = s2->y_sign() ? (-1) : 1;
  REQUIRE(s2->y() == (data[ST2_Y_V_IDX].second * mul));
}

TEST_CASE("parse_string_number_3"){
  string_data data = generate_string_data(3);
  std::string inp_data = generate_inp_data(data);

  kaitai::kstream stream(inp_data);
  glonass_t gl_string(&stream);

  REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second);
  REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second);
  REQUIRE(gl_string.hamming_code() == data[ST3_HC_OFF + HC_IDX].second);
  REQUIRE(gl_string.pad_1() == data[ST3_HC_OFF + PAD1_IDX].second);
  REQUIRE(gl_string.superframe_number() == data[ST3_HC_OFF + SUPERFRAME_IDX].second);
  REQUIRE(gl_string.pad_2() == data[ST3_HC_OFF + PAD2_IDX].second);
  REQUIRE(gl_string.frame_number() == data[ST3_HC_OFF + FRAME_IDX].second);

  kaitai::kstream str3(inp_data);
  glonass_t str3_data(&str3);
  glonass_t::string_3_t* s3 = static_cast<glonass_t::string_3_t*>(str3_data.data());

  REQUIRE(s3->p3() == data[ST3_P3_IDX].second);
  int mul = s3->gamma_n_sign() ? (-1) : 1;
  REQUIRE(s3->gamma_n() == (data[ST3_GAMMA_N_V_IDX].second * mul));
  REQUIRE(s3->not_used() == data[ST3_NU_1_IDX].second);
  REQUIRE(s3->p() == data[ST3_P_IDX].second);
  REQUIRE(s3->l_n() == data[ST3_L_N_IDX].second);
  mul = s3->z_vel_sign() ? (-1) : 1;
  REQUIRE(s3->z_vel() == (data[ST3_Z_VEL_V_IDX].second * mul));
  mul = s3->z_accel_sign() ? (-1) : 1;
  REQUIRE(s3->z_accel() == (data[ST3_Z_ACCEL_V_IDX].second * mul));
  mul = s3->z_sign() ? (-1) : 1;
  REQUIRE(s3->z() == (data[ST3_Z_V_IDX].second * mul));
}

TEST_CASE("parse_string_number_4"){
  string_data data = generate_string_data(4);
  std::string inp_data = generate_inp_data(data);

  kaitai::kstream stream(inp_data);
  glonass_t gl_string(&stream);

  REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second);
  REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second);
  REQUIRE(gl_string.hamming_code() == data[ST4_HC_OFF + HC_IDX].second);
  REQUIRE(gl_string.pad_1() == data[ST4_HC_OFF + PAD1_IDX].second);
  REQUIRE(gl_string.superframe_number() == data[ST4_HC_OFF + SUPERFRAME_IDX].second);
  REQUIRE(gl_string.pad_2() == data[ST4_HC_OFF + PAD2_IDX].second);
  REQUIRE(gl_string.frame_number() == data[ST4_HC_OFF + FRAME_IDX].second);

  kaitai::kstream str4(inp_data);
  glonass_t str4_data(&str4);
  glonass_t::string_4_t* s4 = static_cast<glonass_t::string_4_t*>(str4_data.data());

  int mul = s4->tau_n_sign() ? (-1) : 1;
  REQUIRE(s4->tau_n() == (data[ST4_TAU_N_V_IDX].second * mul));
  mul = s4->delta_tau_n_sign() ? (-1) : 1;
  REQUIRE(s4->delta_tau_n() == (data[ST4_DELTA_TAU_N_V_IDX].second * mul));
  REQUIRE(s4->e_n() == data[ST4_E_N_IDX].second);
  REQUIRE(s4->not_used_1() == data[ST4_NU_1_IDX].second);
  REQUIRE(s4->p4() == data[ST4_P4_IDX].second);
  REQUIRE(s4->f_t() == data[ST4_F_T_IDX].second);
  REQUIRE(s4->not_used_2() == data[ST4_NU_2_IDX].second);
  REQUIRE(s4->n_t() == data[ST4_N_T_IDX].second);
  REQUIRE(s4->n() == data[ST4_N_IDX].second);
  REQUIRE(s4->m() == data[ST4_M_IDX].second);
}

TEST_CASE("parse_string_number_5"){
  string_data data = generate_string_data(5);
  std::string inp_data = generate_inp_data(data);

  kaitai::kstream stream(inp_data);
  glonass_t gl_string(&stream);

  REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second);
  REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second);
  REQUIRE(gl_string.hamming_code() == data[ST5_HC_OFF + HC_IDX].second);
  REQUIRE(gl_string.pad_1() == data[ST5_HC_OFF + PAD1_IDX].second);
  REQUIRE(gl_string.superframe_number() == data[ST5_HC_OFF + SUPERFRAME_IDX].second);
  REQUIRE(gl_string.pad_2() == data[ST5_HC_OFF + PAD2_IDX].second);
  REQUIRE(gl_string.frame_number() == data[ST5_HC_OFF + FRAME_IDX].second);

  kaitai::kstream str5(inp_data);
  glonass_t str5_data(&str5);
  glonass_t::string_5_t* s5 = static_cast<glonass_t::string_5_t*>(str5_data.data());

  REQUIRE(s5->n_a() == data[ST5_N_A_IDX].second);
  REQUIRE(s5->tau_c() == data[ST5_TAU_C_IDX].second);
  REQUIRE(s5->not_used() == data[ST5_NU_IDX].second);
  REQUIRE(s5->n_4() == data[ST5_N_4_IDX].second);
  REQUIRE(s5->tau_gps() == data[ST5_TAU_GPS_IDX].second);
  REQUIRE(s5->l_n() == data[ST5_L_N_IDX].second);
}

TEST_CASE("parse_string_number_NI"){
  string_data data = generate_string_data((rand() % 10) +  6);
  std::string inp_data = generate_inp_data(data);

  kaitai::kstream stream(inp_data);
  glonass_t gl_string(&stream);

  REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second);
  REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second);
  REQUIRE(gl_string.hamming_code() == data[ST6_HC_OFF + HC_IDX].second);
  REQUIRE(gl_string.pad_1() == data[ST6_HC_OFF + PAD1_IDX].second);
  REQUIRE(gl_string.superframe_number() == data[ST6_HC_OFF + SUPERFRAME_IDX].second);
  REQUIRE(gl_string.pad_2() == data[ST6_HC_OFF + PAD2_IDX].second);
  REQUIRE(gl_string.frame_number() == data[ST6_HC_OFF + FRAME_IDX].second);

  kaitai::kstream strni(inp_data);
  glonass_t strni_data(&strni);
  glonass_t::string_non_immediate_t* sni = static_cast<glonass_t::string_non_immediate_t*>(strni_data.data());

  REQUIRE(sni->data_1() == data[ST6_DATA_1_IDX].second);
  REQUIRE(sni->data_2() == data[ST6_DATA_2_IDX].second);
}