From b89ac6330c7606be7ca41bbdf1442b6970a1b799 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 1 May 2026 22:05:37 +0200 Subject: [PATCH] tests: add unit tests for TypeHelpers conversions Adds tests/test_type_helpers.cpp covering the free functions in TypeHelpers.hpp (dictToKwargs, kwargsToDict, metaRangeToRangeList, rangeListToMetaRange, metaRangeToRange, numberListToMetaRange, metaRangeToNumericList, rangeToMetaRange, sensorToArgInfo, argInfoToSensor). The harness is bare stdlib (cassert/iostream + a CHECK macro) to avoid pulling in a test framework dependency. 15 cases, all behavior-only, no hardware required. Wire-up: tests/ added via add_subdirectory; ctest invocation added to all three GitHub Actions jobs. Signed-off-by: tom --- .github/workflows/build.yml | 6 + CMakeLists.txt | 5 + tests/CMakeLists.txt | 19 ++++ tests/test_type_helpers.cpp | 213 ++++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_type_helpers.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62036f1..c57b26e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: run: BOOST_ROOT=/opt/local/libexec/boost/1.71 cmake -B build - name: Build run: cmake --build build + - name: Test + run: ctest --test-dir build --output-on-failure macos_homebrew_build_job: strategy: @@ -38,6 +40,8 @@ jobs: run: cmake -B build - name: Build run: cmake --build build + - name: Test + run: ctest --test-dir build --output-on-failure linux_build_job: strategy: @@ -57,3 +61,5 @@ jobs: run: cmake -GNinja -B build - name: Build run: cmake --build build + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 916687b..04f9bba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,6 +175,11 @@ if(APPLE) set(CMAKE_MACOSX_RPATH ON) endif() +######################################################################## +# Tests +######################################################################## +add_subdirectory(tests) + ######################################################################## # Print Summary ######################################################################## diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5cf2ada --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +######################################################################## +# Unit tests for TypeHelpers.hpp free functions +######################################################################## +add_executable(test_type_helpers test_type_helpers.cpp) + +target_include_directories(test_type_helpers PRIVATE + ${CMAKE_SOURCE_DIR} + ${SoapySDR_INCLUDE_DIRS} + ${UHD_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} +) + +target_link_libraries(test_type_helpers PRIVATE + ${SoapySDR_LIBRARIES} + ${UHD_LIBRARIES} + ${Boost_LIBRARIES} +) + +add_test(NAME type_helpers COMMAND test_type_helpers) diff --git a/tests/test_type_helpers.cpp b/tests/test_type_helpers.cpp new file mode 100644 index 0000000..1ac357b --- /dev/null +++ b/tests/test_type_helpers.cpp @@ -0,0 +1,213 @@ +// Copyright (c) 2026 SoapyUHD contributors +// SPDX-License-Identifier: GPL-3.0 +// +// Unit tests for TypeHelpers.hpp free functions. +// Bare-stdlib harness; no external test framework. + +#include "TypeHelpers.hpp" + +#include +#include +#include +#include + +static int g_failures = 0; + +#define CHECK(cond) do { \ + if (!(cond)) { \ + std::cerr << "FAIL: " #cond " at " << __FILE__ << ":" << __LINE__ << "\n"; \ + ++g_failures; \ + } \ +} while (0) + +#define CHECK_NEAR(a, b, eps) do { \ + if (std::fabs((a) - (b)) > (eps)) { \ + std::cerr << "FAIL: |" #a " - " #b "| > " #eps \ + << " (got " << (a) << " vs " << (b) << ")" \ + << " at " << __FILE__ << ":" << __LINE__ << "\n"; \ + ++g_failures; \ + } \ +} while (0) + +static void test_dict_kwargs_round_trip() +{ + SoapySDR::Kwargs kw; + kw["driver"] = "uhd"; + kw["type"] = "b200"; + kw["serial"] = "BADC10E"; + + uhd::device_addr_t addr = kwargsToDict(kw); + CHECK(addr.has_key("driver")); + CHECK(addr.has_key("type")); + CHECK(addr.has_key("serial")); + CHECK(addr["driver"] == "uhd"); + + SoapySDR::Kwargs kw2 = dictToKwargs(addr); + CHECK(kw2.size() == 3); + CHECK(kw2.at("driver") == "uhd"); + CHECK(kw2.at("type") == "b200"); + CHECK(kw2.at("serial") == "BADC10E"); +} + +static void test_dict_kwargs_empty() +{ + SoapySDR::Kwargs kw; + uhd::device_addr_t addr = kwargsToDict(kw); + CHECK(addr.size() == 0); + + SoapySDR::Kwargs kw2 = dictToKwargs(addr); + CHECK(kw2.empty()); +} + +static void test_meta_range_to_range_list_single() +{ + uhd::meta_range_t mr(0.0, 100.0, 1.0); + SoapySDR::RangeList rl = metaRangeToRangeList(mr); + CHECK(rl.size() == 1); + CHECK_NEAR(rl[0].minimum(), 0.0, 1e-9); + CHECK_NEAR(rl[0].maximum(), 100.0, 1e-9); +} + +static void test_meta_range_to_range_list_multi() +{ + uhd::meta_range_t mr; + mr.push_back(uhd::range_t(0.0, 10.0)); + mr.push_back(uhd::range_t(20.0, 30.0)); + SoapySDR::RangeList rl = metaRangeToRangeList(mr); + CHECK(rl.size() == 2); + CHECK_NEAR(rl[0].maximum(), 10.0, 1e-9); + CHECK_NEAR(rl[1].minimum(), 20.0, 1e-9); +} + +static void test_range_list_to_meta_range_round_trip() +{ + SoapySDR::RangeList rl; + rl.push_back(SoapySDR::Range(1.0, 2.0)); + rl.push_back(SoapySDR::Range(5.0, 10.0)); + uhd::meta_range_t mr = rangeListToMetaRange(rl); + CHECK(mr.size() == 2); + CHECK_NEAR(mr[0].start(), 1.0, 1e-9); + CHECK_NEAR(mr[1].stop(), 10.0, 1e-9); +} + +static void test_range_list_to_meta_range_empty_yields_zero() +{ + SoapySDR::RangeList rl; + uhd::meta_range_t mr = rangeListToMetaRange(rl); + CHECK(mr.size() == 1); + CHECK_NEAR(mr[0].start(), 0.0, 1e-9); + CHECK_NEAR(mr[0].stop(), 0.0, 1e-9); +} + +static void test_meta_range_to_range_top_level() +{ + uhd::meta_range_t mr(-50.0, 50.0, 0.5); + SoapySDR::Range r = metaRangeToRange(mr); + CHECK_NEAR(r.minimum(), -50.0, 1e-9); + CHECK_NEAR(r.maximum(), 50.0, 1e-9); +} + +static void test_number_list_to_meta_range_empty_yields_zero() +{ + std::vector v; + uhd::meta_range_t mr = numberListToMetaRange(v); + CHECK(mr.size() == 1); + CHECK_NEAR(mr[0].start(), 0.0, 1e-9); +} + +static void test_meta_range_to_numeric_list_single_returns_bounds() +{ + uhd::meta_range_t mr(10.0, 20.0); + std::vector v = metaRangeToNumericList(mr); + CHECK(v.size() == 2); + CHECK_NEAR(v[0], 10.0, 1e-9); + CHECK_NEAR(v[1], 20.0, 1e-9); +} + +static void test_meta_range_to_numeric_list_multi_returns_starts() +{ + uhd::meta_range_t mr; + mr.push_back(uhd::range_t(1.0)); + mr.push_back(uhd::range_t(2.0)); + mr.push_back(uhd::range_t(3.0)); + std::vector v = metaRangeToNumericList(mr); + CHECK(v.size() == 3); + CHECK_NEAR(v[0], 1.0, 1e-9); + CHECK_NEAR(v[2], 3.0, 1e-9); +} + +static void test_range_to_meta_range_default_step() +{ + SoapySDR::Range r(0.0, 100.0); + uhd::meta_range_t mr = rangeToMetaRange(r); + CHECK_NEAR(mr.start(), 0.0, 1e-9); + CHECK_NEAR(mr.stop(), 100.0, 1e-9); +} + +static void test_sensor_arginfo_round_trip_bool() +{ + uhd::sensor_value_t s("locked", true, "true", "false"); + SoapySDR::ArgInfo ai = sensorToArgInfo(s, "locked"); + CHECK(ai.key == "locked"); + CHECK(ai.type == SoapySDR::ArgInfo::BOOL); + + uhd::sensor_value_t s2 = argInfoToSensor(ai, "true"); + CHECK(s2.to_bool() == true); +} + +static void test_sensor_arginfo_round_trip_int() +{ + uhd::sensor_value_t s("count", 42, "items"); + SoapySDR::ArgInfo ai = sensorToArgInfo(s, "count"); + CHECK(ai.type == SoapySDR::ArgInfo::INT); + + uhd::sensor_value_t s2 = argInfoToSensor(ai, "42"); + CHECK(s2.to_int() == 42); +} + +static void test_sensor_arginfo_round_trip_real() +{ + uhd::sensor_value_t s("temp", 25.5, "C"); + SoapySDR::ArgInfo ai = sensorToArgInfo(s, "temp"); + CHECK(ai.type == SoapySDR::ArgInfo::FLOAT); + + uhd::sensor_value_t s2 = argInfoToSensor(ai, "25.5"); + CHECK_NEAR(s2.to_real(), 25.5, 1e-9); +} + +static void test_sensor_arginfo_round_trip_string() +{ + uhd::sensor_value_t s("status", std::string("active"), ""); + SoapySDR::ArgInfo ai = sensorToArgInfo(s, "status"); + CHECK(ai.type == SoapySDR::ArgInfo::STRING); + + uhd::sensor_value_t s2 = argInfoToSensor(ai, "active"); + CHECK(s2.value == "active"); +} + +int main() +{ + test_dict_kwargs_round_trip(); + test_dict_kwargs_empty(); + test_meta_range_to_range_list_single(); + test_meta_range_to_range_list_multi(); + test_range_list_to_meta_range_round_trip(); + test_range_list_to_meta_range_empty_yields_zero(); + test_meta_range_to_range_top_level(); + test_number_list_to_meta_range_empty_yields_zero(); + test_meta_range_to_numeric_list_single_returns_bounds(); + test_meta_range_to_numeric_list_multi_returns_starts(); + test_range_to_meta_range_default_step(); + test_sensor_arginfo_round_trip_bool(); + test_sensor_arginfo_round_trip_int(); + test_sensor_arginfo_round_trip_real(); + test_sensor_arginfo_round_trip_string(); + + if (g_failures == 0) + { + std::cout << "All tests passed.\n"; + return 0; + } + std::cerr << g_failures << " test(s) FAILED.\n"; + return 1; +}