From e65725c15cac44b300fe874ea53c20971b87a53f Mon Sep 17 00:00:00 2001 From: Heath Henley Date: Fri, 13 Feb 2026 10:41:26 -0500 Subject: [PATCH 1/3] DRY tests - start splitting out duplicated conversions It was easy to get started but makes more sense - moving all the tests into a single place next - subscriber and request tests will eventually test more of the actual functionality instead of conversion internals. --- CMakeLists.txt | 1 + src/conversions.cpp | 276 ++++++++++++++++++++++++++ src/conversions_internal.hpp | 33 +++ src/requests.cpp | 152 +------------- src/requests_internal.hpp | 10 - src/subscriber.cpp | 231 +-------------------- src/subscriber_internal.hpp | 23 +-- tests/CMakeLists.txt | 1 + tests/conversions_unit_tests.cpp | 143 +++++++++++++ tests/requests_conversion_tests.cpp | 72 ++++++- tests/subscriber_conversion_tests.cpp | 75 ++++++- 11 files changed, 604 insertions(+), 413 deletions(-) create mode 100644 src/conversions.cpp create mode 100644 src/conversions_internal.hpp create mode 100644 tests/conversions_unit_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 778f20b..d3c667c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ target_link_libraries(farsounder_proto # ============================================================================= add_library(farsounder SHARED src/config.cpp + src/conversions.cpp src/requests.cpp src/subscriber.cpp ) diff --git a/src/conversions.cpp b/src/conversions.cpp new file mode 100644 index 0000000..1c38119 --- /dev/null +++ b/src/conversions.cpp @@ -0,0 +1,276 @@ +#include "conversions_internal.hpp" + +#include +#include + +namespace farsounder::detail { + +Timestamp convert_timestamp(const proto::time::Time& t) { + std::tm tm = {}; + tm.tm_year = static_cast(t.year()) - 1900; + tm.tm_mon = static_cast(t.month()) - 1; + tm.tm_mday = static_cast(t.day()); + tm.tm_hour = static_cast(t.hour()); + tm.tm_min = static_cast(t.minute()); + tm.tm_sec = static_cast(t.second()); +#ifdef _WIN32 + auto epoch = _mkgmtime(&tm); +#else + auto epoch = timegm(&tm); +#endif + return Timestamp{static_cast(epoch) + t.millisecond() / 1000.0}; +} + +ResultCode convert_result_code(proto::nav_api::RequestResult::ResultCode code) { + switch (code) { + case proto::nav_api::RequestResult::kSuccess: + return ResultCode::Success; + case proto::nav_api::RequestResult::kUnknownError: + return ResultCode::UnknownError; + case proto::nav_api::RequestResult::kOperationUnavailable: + return ResultCode::OperationUnavailable; + case proto::nav_api::RequestResult::kParameterOutOfRange: + return ResultCode::ParameterOutOfRange; + case proto::nav_api::RequestResult::kParameterMissing: + return ResultCode::ParameterMissing; + case proto::nav_api::RequestResult::kInvalidRequest: + return ResultCode::InvalidRequest; + default: + return ResultCode::UnknownError; + } +} + +RequestResult convert_result(const proto::nav_api::RequestResult& r) { + RequestResult result; + if (r.has_time()) { + result.time = convert_timestamp(r.time()); + } + result.code = convert_result_code(r.code()); + result.detail = r.result_detail(); + return result; +} + +SystemType convert_system_type( + proto::nav_api::ProcessorSettings::SystemType t) { + switch (t) { + case proto::nav_api::ProcessorSettings::kFS500: + return SystemType::kFS500; + case proto::nav_api::ProcessorSettings::kFS1000: + return SystemType::kFS1000; + case proto::nav_api::ProcessorSettings::kFS350: + return SystemType::kFS350; + default: + return SystemType::kFS500; + } +} + +FieldOfView convert_fov(proto::nav_api::FieldOfView fov) { + switch (fov) { + case proto::nav_api::k120d100m: + return FieldOfView::k120d100m; + case proto::nav_api::k120d200m: + return FieldOfView::k120d200m; + case proto::nav_api::k90d500m: + return FieldOfView::k90d500m; + case proto::nav_api::k60d1000m: + return FieldOfView::k60d1000m; + case proto::nav_api::k90d100m: + return FieldOfView::k90d100m; + case proto::nav_api::k90d200m: + return FieldOfView::k90d200m; + case proto::nav_api::k90d350m: + return FieldOfView::k90d350m; + case proto::nav_api::kStandby: + return FieldOfView::kStandby; + default: + return FieldOfView::k90d500m; + } +} + +proto::nav_api::FieldOfView convert_fov_to_proto(FieldOfView fov) { + switch (fov) { + case FieldOfView::k120d100m: + return proto::nav_api::k120d100m; + case FieldOfView::k120d200m: + return proto::nav_api::k120d200m; + case FieldOfView::k90d500m: + return proto::nav_api::k90d500m; + case FieldOfView::k60d1000m: + return proto::nav_api::k60d1000m; + case FieldOfView::k90d100m: + return proto::nav_api::k90d100m; + case FieldOfView::k90d200m: + return proto::nav_api::k90d200m; + case FieldOfView::k90d350m: + return proto::nav_api::k90d350m; + case FieldOfView::kStandby: + return proto::nav_api::kStandby; + default: + return proto::nav_api::k90d500m; + } +} + +ArrayDataType convert_array_type(proto::array::ArrayData::Type type) { + switch (type) { + case proto::array::ArrayData::BYTE: + return ArrayDataType::kByte; + case proto::array::ArrayData::INT16: + return ArrayDataType::kInt16; + case proto::array::ArrayData::UINT16: + return ArrayDataType::kUInt16; + case proto::array::ArrayData::INT32: + return ArrayDataType::kInt32; + case proto::array::ArrayData::UINT32: + return ArrayDataType::kUInt32; + case proto::array::ArrayData::INT64: + return ArrayDataType::kInt64; + case proto::array::ArrayData::UINT64: + return ArrayDataType::kUInt64; + case proto::array::ArrayData::FLOAT32: + return ArrayDataType::kFloat32; + case proto::array::ArrayData::FLOAT64: + return ArrayDataType::kFloat64; + case proto::array::ArrayData::COMPLEX64: + return ArrayDataType::kComplex64; + case proto::array::ArrayData::COMPLEX128: + return ArrayDataType::kComplex128; + case proto::array::ArrayData::BOOL: + return ArrayDataType::kBool; + default: + return ArrayDataType::kByte; + } +} + +ArrayDataOrder convert_array_order(proto::array::ArrayData::Order order) { + switch (order) { + case proto::array::ArrayData::ROW_MAJOR: + return ArrayDataOrder::kRowMajor; + case proto::array::ArrayData::COLUMN_MAJOR: + return ArrayDataOrder::kColumnMajor; + default: + return ArrayDataOrder::kRowMajor; + } +} + +Bin convert_bin(const proto::nav_api::Bin& b) { + Bin bin; + bin.hor_index = b.hor_index(); + bin.ver_index = b.ver_index(); + bin.range_index = b.range_index(); + bin.cross_range = b.cross_range(); + bin.down_range = b.down_range(); + bin.depth = b.depth(); + bin.strength = b.strength(); + return bin; +} + +GridDescription convert_grid_description( + const proto::grid_description::GridDescription& g) { + GridDescription grid; + if (g.has_mode()) { + grid.mode = static_cast(g.mode()); + } + grid.hor_angles.reserve(static_cast(g.hor_angles_size())); + for (const auto angle : g.hor_angles()) { + grid.hor_angles.push_back(angle); + } + grid.ver_angles.reserve(static_cast(g.ver_angles_size())); + for (const auto angle : g.ver_angles()) { + grid.ver_angles.push_back(angle); + } + if (g.has_max_range()) { + grid.max_range = g.max_range(); + } + return grid; +} + +HydrophoneData convert_hydrophone_data(const proto::nav_api::HydrophoneData& h) { + HydrophoneData data; + if (h.has_time()) { + data.time = convert_timestamp(h.time()); + } + data.serial = h.serial(); + data.transmit_id = h.transmit_id(); + data.num_hor_phones = h.num_hor_phones(); + data.num_ver_phones = h.num_ver_phones(); + + if (h.has_raw_timeseries()) { + const auto& ts = h.raw_timeseries(); + data.dims.reserve(static_cast(ts.dims_size())); + for (int i = 0; i < ts.dims_size(); ++i) { + data.dims.push_back(ts.dims(i)); + } + if (ts.has_type()) { + data.type = convert_array_type(ts.type()); + } + if (ts.has_order()) { + data.order = convert_array_order(ts.order()); + } + if (ts.has_data()) { + data.raw_timeseries = ts.data(); + } + } + return data; +} + +TargetData convert_target_data(const proto::nav_api::TargetData& t) { + TargetData data; + if (t.has_time()) { + data.time = convert_timestamp(t.time()); + } + data.serial = t.serial(); + + if (t.has_heading()) { + data.heading = Heading{t.heading().heading()}; + } + if (t.has_position()) { + data.position = Position{t.position().lat(), t.position().lon()}; + } + + data.bottom.reserve(static_cast(t.bottom_size())); + for (const auto& bin : t.bottom()) { + data.bottom.push_back(convert_bin(bin)); + } + + data.groups.reserve(static_cast(t.groups_size())); + for (const auto& group : t.groups()) { + TargetGroup tg; + tg.bins.reserve(static_cast(group.bins_size())); + for (const auto& bin : group.bins()) { + tg.bins.push_back(convert_bin(bin)); + } + data.groups.push_back(std::move(tg)); + } + + if (t.has_grid_description()) { + data.grid_description = convert_grid_description(t.grid_description()); + } + data.max_depth = t.max_depth(); + data.max_range_index = t.max_range_index(); + return data; +} + +ProcessorSettings convert_processor_settings( + const proto::nav_api::ProcessorSettings& s) { + ProcessorSettings settings; + if (s.has_time()) { + settings.time = convert_timestamp(s.time()); + } + settings.min_inwater_squelch = s.min_inwater_squelch(); + settings.max_inwater_squelch = s.max_inwater_squelch(); + settings.inwater_squelch = s.inwater_squelch(); + settings.squelchless_inwater_detector = s.squelchless_inwater_detector(); + settings.detect_bottom = s.detect_bottom(); + settings.system_type = convert_system_type(s.system_type()); + settings.fov = convert_fov(s.fov()); + return settings; +} + +VesselInfo convert_vessel_info(const proto::nav_api::VesselInfo& v) { + VesselInfo info; + info.draft = v.draft(); + info.keel_offset = v.keel_offset(); + return info; +} + +} // namespace farsounder::detail diff --git a/src/conversions_internal.hpp b/src/conversions_internal.hpp new file mode 100644 index 0000000..10507ef --- /dev/null +++ b/src/conversions_internal.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "farsounder/types.hpp" +#include "proto/array.pb.h" +#include "proto/grid_description.pb.h" +#include "proto/nav_api.pb.h" + +namespace farsounder::detail { + +Timestamp convert_timestamp(const proto::time::Time& t); + +ResultCode convert_result_code(proto::nav_api::RequestResult::ResultCode code); +RequestResult convert_result(const proto::nav_api::RequestResult& r); + +SystemType convert_system_type( + proto::nav_api::ProcessorSettings::SystemType t); +FieldOfView convert_fov(proto::nav_api::FieldOfView fov); +proto::nav_api::FieldOfView convert_fov_to_proto(FieldOfView fov); + +ArrayDataType convert_array_type(proto::array::ArrayData::Type type); +ArrayDataOrder convert_array_order(proto::array::ArrayData::Order order); + +Bin convert_bin(const proto::nav_api::Bin& b); +GridDescription convert_grid_description( + const proto::grid_description::GridDescription& g); +HydrophoneData convert_hydrophone_data(const proto::nav_api::HydrophoneData& h); +TargetData convert_target_data(const proto::nav_api::TargetData& t); + +ProcessorSettings convert_processor_settings( + const proto::nav_api::ProcessorSettings& s); +VesselInfo convert_vessel_info(const proto::nav_api::VesselInfo& v); + +} // namespace farsounder::detail diff --git a/src/requests.cpp b/src/requests.cpp index 774b818..644978c 100644 --- a/src/requests.cpp +++ b/src/requests.cpp @@ -2,7 +2,6 @@ #include -#include #include #include #include @@ -10,6 +9,7 @@ #include #include +#include "conversions_internal.hpp" #include "proto/nav_api.pb.h" #include "requests_internal.hpp" @@ -74,140 +74,8 @@ std::string rest_base_url(const config::ClientConfig& cfg) { } // namespace -// ============================================================================= -// Proto to wrapper type conversions -// ============================================================================= - namespace detail { -Timestamp convert_timestamp(const proto::time::Time& t) { - std::tm tm = {}; - tm.tm_year = static_cast(t.year()) - 1900; - tm.tm_mon = static_cast(t.month()) - 1; - tm.tm_mday = static_cast(t.day()); - tm.tm_hour = static_cast(t.hour()); - tm.tm_min = static_cast(t.minute()); - tm.tm_sec = static_cast(t.second()); -#ifdef _WIN32 - auto epoch = _mkgmtime(&tm); -#else - auto epoch = timegm(&tm); -#endif - return Timestamp{static_cast(epoch) + t.millisecond() / 1000.0}; -} - -ResultCode convert_result_code(proto::nav_api::RequestResult::ResultCode code) { - switch (code) { - case proto::nav_api::RequestResult::kSuccess: - return ResultCode::Success; - case proto::nav_api::RequestResult::kUnknownError: - return ResultCode::UnknownError; - case proto::nav_api::RequestResult::kOperationUnavailable: - return ResultCode::OperationUnavailable; - case proto::nav_api::RequestResult::kParameterOutOfRange: - return ResultCode::ParameterOutOfRange; - case proto::nav_api::RequestResult::kParameterMissing: - return ResultCode::ParameterMissing; - case proto::nav_api::RequestResult::kInvalidRequest: - return ResultCode::InvalidRequest; - default: - return ResultCode::UnknownError; - } -} - -RequestResult convert_result(const proto::nav_api::RequestResult& r) { - RequestResult result; - if (r.has_time()) { - result.time = convert_timestamp(r.time()); - } - result.code = convert_result_code(r.code()); - result.detail = r.result_detail(); - return result; -} - -SystemType convert_system_type( - proto::nav_api::ProcessorSettings::SystemType t) { - switch (t) { - case proto::nav_api::ProcessorSettings::kFS500: - return SystemType::kFS500; - case proto::nav_api::ProcessorSettings::kFS1000: - return SystemType::kFS1000; - case proto::nav_api::ProcessorSettings::kFS350: - return SystemType::kFS350; - default: - return SystemType::kFS500; - } -} - -FieldOfView convert_fov(proto::nav_api::FieldOfView fov) { - switch (fov) { - case proto::nav_api::k120d100m: - return FieldOfView::k120d100m; - case proto::nav_api::k120d200m: - return FieldOfView::k120d200m; - case proto::nav_api::k90d500m: - return FieldOfView::k90d500m; - case proto::nav_api::k60d1000m: - return FieldOfView::k60d1000m; - case proto::nav_api::k90d100m: - return FieldOfView::k90d100m; - case proto::nav_api::k90d200m: - return FieldOfView::k90d200m; - case proto::nav_api::k90d350m: - return FieldOfView::k90d350m; - case proto::nav_api::kStandby: - return FieldOfView::kStandby; - default: - return FieldOfView::k90d500m; - } -} - -proto::nav_api::FieldOfView convert_fov_to_proto(FieldOfView fov) { - switch (fov) { - case FieldOfView::k120d100m: - return proto::nav_api::k120d100m; - case FieldOfView::k120d200m: - return proto::nav_api::k120d200m; - case FieldOfView::k90d500m: - return proto::nav_api::k90d500m; - case FieldOfView::k60d1000m: - return proto::nav_api::k60d1000m; - case FieldOfView::k90d100m: - return proto::nav_api::k90d100m; - case FieldOfView::k90d200m: - return proto::nav_api::k90d200m; - case FieldOfView::k90d350m: - return proto::nav_api::k90d350m; - case FieldOfView::kStandby: - return proto::nav_api::kStandby; - default: - return proto::nav_api::k90d500m; - } -} - -ProcessorSettings convert_processor_settings( - const proto::nav_api::ProcessorSettings& s) { - ProcessorSettings settings; - if (s.has_time()) { - settings.time = convert_timestamp(s.time()); - } - settings.min_inwater_squelch = s.min_inwater_squelch(); - settings.max_inwater_squelch = s.max_inwater_squelch(); - settings.inwater_squelch = s.inwater_squelch(); - settings.squelchless_inwater_detector = s.squelchless_inwater_detector(); - settings.detect_bottom = s.detect_bottom(); - settings.system_type = convert_system_type(s.system_type()); - settings.fov = convert_fov(s.fov()); - return settings; -} - -VesselInfo convert_vessel_info(const proto::nav_api::VesselInfo& v) { - VesselInfo info; - info.draft = v.draft(); - info.keel_offset = v.keel_offset(); - return info; -} - // ============================================================================= // History data parsing (REST/JSON) // ============================================================================= @@ -287,9 +155,9 @@ GetProcessorSettingsResponse get_processor_settings( config, config::ReqRepEndpoint::GetProcessorSettings, request); GetProcessorSettingsResponse response; - response.result = detail::convert_result(proto_response.result()); + response.result = farsounder::detail::convert_result(proto_response.result()); response.settings = - detail::convert_processor_settings(proto_response.settings()); + farsounder::detail::convert_processor_settings(proto_response.settings()); return response; } @@ -302,13 +170,13 @@ std::future get_processor_settings_async( SetFieldOfViewResponse set_field_of_view(const config::ClientConfig& config, FieldOfView fov) { proto::nav_api::SetFieldOfViewRequest request; - request.set_fov(detail::convert_fov_to_proto(fov)); + request.set_fov(farsounder::detail::convert_fov_to_proto(fov)); auto proto_response = send_request( config, config::ReqRepEndpoint::SetFieldOfView, request); SetFieldOfViewResponse response; - response.result = detail::convert_result(proto_response.result()); + response.result = farsounder::detail::convert_result(proto_response.result()); return response; } @@ -329,7 +197,7 @@ SetBottomDetectionResponse set_bottom_detection( config, config::ReqRepEndpoint::SetBottomDetection, request); SetBottomDetectionResponse response; - response.result = detail::convert_result(proto_response.result()); + response.result = farsounder::detail::convert_result(proto_response.result()); return response; } @@ -350,7 +218,7 @@ SetInWaterSquelchResponse set_inwater_squelch( config, config::ReqRepEndpoint::SetInWaterSquelch, request); SetInWaterSquelchResponse response; - response.result = detail::convert_result(proto_response.result()); + response.result = farsounder::detail::convert_result(proto_response.result()); return response; } @@ -372,7 +240,7 @@ SetSquelchlessInWaterDetectorResponse set_squelchless_inwater_detector( request); SetSquelchlessInWaterDetectorResponse response; - response.result = detail::convert_result(proto_response.result()); + response.result = farsounder::detail::convert_result(proto_response.result()); return response; } @@ -393,8 +261,8 @@ GetVesselInfoResponse get_vessel_info(const config::ClientConfig& config) { config, config::ReqRepEndpoint::GetVesselInfo, request); GetVesselInfoResponse response; - response.result = detail::convert_result(proto_response.result()); - response.info = detail::convert_vessel_info(proto_response.info()); + response.result = farsounder::detail::convert_result(proto_response.result()); + response.info = farsounder::detail::convert_vessel_info(proto_response.info()); return response; } diff --git a/src/requests_internal.hpp b/src/requests_internal.hpp index b658468..3e3070c 100644 --- a/src/requests_internal.hpp +++ b/src/requests_internal.hpp @@ -8,16 +8,6 @@ namespace farsounder::requests::detail { -Timestamp convert_timestamp(const proto::time::Time& t); -ResultCode convert_result_code(proto::nav_api::RequestResult::ResultCode code); -RequestResult convert_result(const proto::nav_api::RequestResult& r); -SystemType convert_system_type(proto::nav_api::ProcessorSettings::SystemType t); -FieldOfView convert_fov(proto::nav_api::FieldOfView fov); -proto::nav_api::FieldOfView convert_fov_to_proto(FieldOfView fov); -ProcessorSettings convert_processor_settings( - const proto::nav_api::ProcessorSettings& s); -VesselInfo convert_vessel_info(const proto::nav_api::VesselInfo& v); - history::GriddedBottomDetection parse_gridded_bottom_detection( const nlohmann::json& payload); history::GriddedInwaterDetection parse_gridded_inwater_detection( diff --git a/src/subscriber.cpp b/src/subscriber.cpp index 10d8d74..cfc05a0 100644 --- a/src/subscriber.cpp +++ b/src/subscriber.cpp @@ -3,12 +3,12 @@ #include #include #include -#include #include #include #include #include +#include "conversions_internal.hpp" #include "proto/array.pb.h" #include "proto/nav_api.pb.h" #include "subscriber_internal.hpp" @@ -86,235 +86,6 @@ std::string pubsub_address(const config::ClientConfig& cfg, } // namespace -// ============================================================================= -// Proto to wrapper type conversions -// ============================================================================= - -namespace detail { - -Timestamp convert_timestamp(const proto::time::Time& t) { - // Convert year/month/day/hour/minute/second/millisecond to epoch - std::tm tm = {}; - tm.tm_year = static_cast(t.year()) - 1900; - tm.tm_mon = static_cast(t.month()) - 1; - tm.tm_mday = static_cast(t.day()); - tm.tm_hour = static_cast(t.hour()); - tm.tm_min = static_cast(t.minute()); - tm.tm_sec = static_cast(t.second()); -#ifdef _WIN32 - auto epoch = _mkgmtime(&tm); -#else - auto epoch = timegm(&tm); -#endif - return Timestamp{static_cast(epoch) + t.millisecond() / 1000.0}; -} - -SystemType convert_system_type( - proto::nav_api::ProcessorSettings::SystemType t) { - switch (t) { - case proto::nav_api::ProcessorSettings::kFS500: - return SystemType::kFS500; - case proto::nav_api::ProcessorSettings::kFS1000: - return SystemType::kFS1000; - case proto::nav_api::ProcessorSettings::kFS350: - return SystemType::kFS350; - default: - return SystemType::kFS500; - } -} - -FieldOfView convert_fov(proto::nav_api::FieldOfView fov) { - switch (fov) { - case proto::nav_api::k120d100m: - return FieldOfView::k120d100m; - case proto::nav_api::k120d200m: - return FieldOfView::k120d200m; - case proto::nav_api::k90d500m: - return FieldOfView::k90d500m; - case proto::nav_api::k60d1000m: - return FieldOfView::k60d1000m; - case proto::nav_api::k90d100m: - return FieldOfView::k90d100m; - case proto::nav_api::k90d200m: - return FieldOfView::k90d200m; - case proto::nav_api::k90d350m: - return FieldOfView::k90d350m; - case proto::nav_api::kStandby: - return FieldOfView::kStandby; - default: - return FieldOfView::k90d500m; - } -} - -ArrayDataType convert_array_type(proto::array::ArrayData::Type type) { - switch (type) { - case proto::array::ArrayData::BYTE: - return ArrayDataType::kByte; - case proto::array::ArrayData::INT16: - return ArrayDataType::kInt16; - case proto::array::ArrayData::UINT16: - return ArrayDataType::kUInt16; - case proto::array::ArrayData::INT32: - return ArrayDataType::kInt32; - case proto::array::ArrayData::UINT32: - return ArrayDataType::kUInt32; - case proto::array::ArrayData::INT64: - return ArrayDataType::kInt64; - case proto::array::ArrayData::UINT64: - return ArrayDataType::kUInt64; - case proto::array::ArrayData::FLOAT32: - return ArrayDataType::kFloat32; - case proto::array::ArrayData::FLOAT64: - return ArrayDataType::kFloat64; - case proto::array::ArrayData::COMPLEX64: - return ArrayDataType::kComplex64; - case proto::array::ArrayData::COMPLEX128: - return ArrayDataType::kComplex128; - case proto::array::ArrayData::BOOL: - return ArrayDataType::kBool; - default: - return ArrayDataType::kByte; - } -} - -ArrayDataOrder convert_array_order(proto::array::ArrayData::Order order) { - switch (order) { - case proto::array::ArrayData::ROW_MAJOR: - return ArrayDataOrder::kRowMajor; - case proto::array::ArrayData::COLUMN_MAJOR: - return ArrayDataOrder::kColumnMajor; - default: - return ArrayDataOrder::kRowMajor; - } -} - -Bin convert_bin(const proto::nav_api::Bin& b) { - Bin bin; - bin.hor_index = b.hor_index(); - bin.ver_index = b.ver_index(); - bin.range_index = b.range_index(); - bin.cross_range = b.cross_range(); - bin.down_range = b.down_range(); - bin.depth = b.depth(); - bin.strength = b.strength(); - return bin; -} - -GridDescription convert_grid_description( - const proto::grid_description::GridDescription& g) { - GridDescription grid; - if (g.has_mode()) { - grid.mode = static_cast(g.mode()); - } - grid.hor_angles.reserve(static_cast(g.hor_angles_size())); - for (const auto angle : g.hor_angles()) { - grid.hor_angles.push_back(angle); - } - grid.ver_angles.reserve(static_cast(g.ver_angles_size())); - for (const auto angle : g.ver_angles()) { - grid.ver_angles.push_back(angle); - } - if (g.has_max_range()) { - grid.max_range = g.max_range(); - } - return grid; -} - -HydrophoneData convert_hydrophone_data( - const proto::nav_api::HydrophoneData& h) { - HydrophoneData data; - if (h.has_time()) { - data.time = detail::convert_timestamp(h.time()); - } - data.serial = h.serial(); - data.transmit_id = h.transmit_id(); - data.num_hor_phones = h.num_hor_phones(); - data.num_ver_phones = h.num_ver_phones(); - - // Convert raw timeseries if present - if (h.has_raw_timeseries()) { - const auto& ts = h.raw_timeseries(); - data.dims.reserve(static_cast(ts.dims_size())); - for (int i = 0; i < ts.dims_size(); ++i) { - data.dims.push_back(ts.dims(i)); - } - if (ts.has_type()) { - data.type = detail::convert_array_type(ts.type()); - } - if (ts.has_order()) { - data.order = detail::convert_array_order(ts.order()); - } - if (ts.has_data()) { - data.raw_timeseries = ts.data(); - } - } - return data; -} - -TargetData convert_target_data(const proto::nav_api::TargetData& t) { - TargetData data; - if (t.has_time()) { - data.time = detail::convert_timestamp(t.time()); - } - data.serial = t.serial(); - - if (t.has_heading()) { - data.heading = Heading{t.heading().heading()}; - } - if (t.has_position()) { - data.position = Position{t.position().lat(), t.position().lon()}; - } - - // Convert bottom bins - data.bottom.reserve(static_cast(t.bottom_size())); - for (const auto& bin : t.bottom()) { - data.bottom.push_back(detail::convert_bin(bin)); - } - - // Convert target groups - data.groups.reserve(static_cast(t.groups_size())); - for (const auto& group : t.groups()) { - TargetGroup tg; - tg.bins.reserve(static_cast(group.bins_size())); - for (const auto& bin : group.bins()) { - tg.bins.push_back(detail::convert_bin(bin)); - } - data.groups.push_back(std::move(tg)); - } - - if (t.has_grid_description()) { - data.grid_description = convert_grid_description(t.grid_description()); - } - data.max_depth = t.max_depth(); - data.max_range_index = t.max_range_index(); - return data; -} - -ProcessorSettings convert_processor_settings( - const proto::nav_api::ProcessorSettings& s) { - ProcessorSettings settings; - if (s.has_time()) { - settings.time = detail::convert_timestamp(s.time()); - } - settings.min_inwater_squelch = s.min_inwater_squelch(); - settings.max_inwater_squelch = s.max_inwater_squelch(); - settings.inwater_squelch = s.inwater_squelch(); - settings.squelchless_inwater_detector = s.squelchless_inwater_detector(); - settings.detect_bottom = s.detect_bottom(); - settings.system_type = detail::convert_system_type(s.system_type()); - settings.fov = detail::convert_fov(s.fov()); - return settings; -} - -VesselInfo convert_vessel_info(const proto::nav_api::VesselInfo& v) { - VesselInfo info; - info.draft = v.draft(); - info.keel_offset = v.keel_offset(); - return info; -} - -} // namespace detail - // ============================================================================= // Subscriber implementation (PIMPL) // ============================================================================= diff --git a/src/subscriber_internal.hpp b/src/subscriber_internal.hpp index 16fef1e..754d2e2 100644 --- a/src/subscriber_internal.hpp +++ b/src/subscriber_internal.hpp @@ -1,24 +1,3 @@ #pragma once -#include - -#include "farsounder/types.hpp" -#include "proto/array.pb.h" -#include "proto/nav_api.pb.h" - -namespace farsounder::detail { - -Timestamp convert_timestamp(const proto::time::Time& t); -SystemType convert_system_type( - proto::nav_api::ProcessorSettings::SystemType t); -FieldOfView convert_fov(proto::nav_api::FieldOfView fov); -ArrayDataType convert_array_type(proto::array::ArrayData::Type type); -ArrayDataOrder convert_array_order(proto::array::ArrayData::Order order); -Bin convert_bin(const proto::nav_api::Bin& b); -HydrophoneData convert_hydrophone_data(const proto::nav_api::HydrophoneData& h); -TargetData convert_target_data(const proto::nav_api::TargetData& t); -ProcessorSettings convert_processor_settings( - const proto::nav_api::ProcessorSettings& s); -VesselInfo convert_vessel_info(const proto::nav_api::VesselInfo& v); - -} // namespace farsounder::detail +#include "conversions_internal.hpp" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 150e298..0a8bf7b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,7 @@ include(GoogleTest) add_executable(farsounder_tests + conversions_unit_tests.cpp requests_conversion_tests.cpp history_parse_tests.cpp subscriber_conversion_tests.cpp diff --git a/tests/conversions_unit_tests.cpp b/tests/conversions_unit_tests.cpp new file mode 100644 index 0000000..e1e3157 --- /dev/null +++ b/tests/conversions_unit_tests.cpp @@ -0,0 +1,143 @@ +#include + +#include "conversions_internal.hpp" + +namespace { + +using farsounder::ArrayDataOrder; +using farsounder::ArrayDataType; +using farsounder::FieldOfView; +using farsounder::ResultCode; +using farsounder::SystemType; +using farsounder::detail::convert_array_order; +using farsounder::detail::convert_array_type; +using farsounder::detail::convert_bin; +using farsounder::detail::convert_fov; +using farsounder::detail::convert_fov_to_proto; +using farsounder::detail::convert_hydrophone_data; +using farsounder::detail::convert_processor_settings; +using farsounder::detail::convert_result; +using farsounder::detail::convert_result_code; +using farsounder::detail::convert_system_type; +using farsounder::detail::convert_target_data; +using farsounder::detail::convert_timestamp; +using farsounder::detail::convert_vessel_info; + +TEST(SharedConversions, CoversRequestSpecificConverters) { + EXPECT_EQ(convert_result_code(proto::nav_api::RequestResult::kSuccess), + ResultCode::Success); + EXPECT_EQ(convert_result_code( + static_cast(999)), + ResultCode::UnknownError); + + proto::nav_api::RequestResult result; + result.set_code(proto::nav_api::RequestResult::kInvalidRequest); + result.set_result_detail("bad request"); + const auto converted_result = convert_result(result); + EXPECT_DOUBLE_EQ(converted_result.time.seconds_since_epoch, 0.0); + EXPECT_EQ(converted_result.code, ResultCode::InvalidRequest); + EXPECT_EQ(converted_result.detail, "bad request"); + + EXPECT_EQ(convert_fov_to_proto(FieldOfView::k120d100m), + proto::nav_api::k120d100m); +} + +TEST(SharedConversions, CoversSharedEnumConvertersWithDefaults) { + EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS1000), + SystemType::kFS1000); + EXPECT_EQ(convert_system_type( + static_cast( + 999)), + SystemType::kFS500); + + EXPECT_EQ(convert_fov(proto::nav_api::k120d200m), FieldOfView::k120d200m); + EXPECT_EQ(convert_fov(static_cast(999)), + FieldOfView::k90d500m); + + EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT64), + ArrayDataType::kFloat64); + EXPECT_EQ(convert_array_type(static_cast(999)), + ArrayDataType::kByte); + + EXPECT_EQ(convert_array_order(proto::array::ArrayData::COLUMN_MAJOR), + ArrayDataOrder::kColumnMajor); + EXPECT_EQ( + convert_array_order(static_cast(999)), + ArrayDataOrder::kRowMajor); +} + +TEST(SharedConversions, CoversTimestampAndStructConverters) { + proto::time::Time time; + time.set_year(1970); + time.set_month(1); + time.set_day(1); + time.set_hour(0); + time.set_minute(0); + time.set_second(3); + time.set_millisecond(500); + EXPECT_DOUBLE_EQ(convert_timestamp(time).seconds_since_epoch, 3.5); + + proto::nav_api::Bin bin; + bin.set_hor_index(8); + bin.set_strength(1.25f); + const auto converted_bin = convert_bin(bin); + EXPECT_EQ(converted_bin.hor_index, 8); + EXPECT_FLOAT_EQ(converted_bin.strength, 1.25f); + + proto::nav_api::ProcessorSettings settings; + settings.set_system_type(proto::nav_api::ProcessorSettings::kFS350); + settings.set_fov(proto::nav_api::k90d350m); + const auto converted_settings = convert_processor_settings(settings); + EXPECT_EQ(converted_settings.system_type, SystemType::kFS350); + EXPECT_EQ(converted_settings.fov, FieldOfView::k90d350m); + EXPECT_DOUBLE_EQ(converted_settings.time.seconds_since_epoch, 0.0); + + proto::nav_api::VesselInfo vessel; + vessel.set_draft(3.5f); + vessel.set_keel_offset(0.25f); + const auto converted_vessel = convert_vessel_info(vessel); + EXPECT_FLOAT_EQ(converted_vessel.draft, 3.5f); + EXPECT_FLOAT_EQ(converted_vessel.keel_offset, 0.25f); +} + +TEST(SharedConversions, CoversSubscriberPayloadConverters) { + proto::nav_api::HydrophoneData hydrophone; + hydrophone.set_serial("h"); + hydrophone.set_transmit_id("tx"); + auto hydrophone_converted = convert_hydrophone_data(hydrophone); + EXPECT_TRUE(hydrophone_converted.dims.empty()); + EXPECT_EQ(hydrophone_converted.type, ArrayDataType::kByte); + + auto* raw_timeseries = hydrophone.mutable_raw_timeseries(); + raw_timeseries->add_dims(2); + raw_timeseries->set_type(proto::array::ArrayData::INT16); + raw_timeseries->set_order(proto::array::ArrayData::COLUMN_MAJOR); + raw_timeseries->set_data(std::string("\x01\x02", 2)); + hydrophone_converted = convert_hydrophone_data(hydrophone); + ASSERT_EQ(hydrophone_converted.dims.size(), 1u); + EXPECT_EQ(hydrophone_converted.type, ArrayDataType::kInt16); + EXPECT_EQ(hydrophone_converted.order, ArrayDataOrder::kColumnMajor); + + proto::nav_api::TargetData target; + target.set_serial("t"); + target.set_max_depth(4.0); + target.set_max_range_index(2); + auto target_converted = convert_target_data(target); + EXPECT_FALSE(target_converted.heading.has_value()); + EXPECT_TRUE(target_converted.bottom.empty()); + EXPECT_FALSE(target_converted.grid_description.has_value()); + + auto* heading = target.mutable_heading(); + heading->set_heading(1.0); + auto* bottom = target.add_bottom(); + bottom->set_hor_index(10); + auto* grid = target.mutable_grid_description(); + grid->set_mode(proto::grid_description::GridDescription::kFixed); + target_converted = convert_target_data(target); + EXPECT_TRUE(target_converted.heading.has_value()); + ASSERT_EQ(target_converted.bottom.size(), 1u); + EXPECT_EQ(target_converted.bottom[0].hor_index, 10); + EXPECT_TRUE(target_converted.grid_description.has_value()); +} + +} // namespace diff --git a/tests/requests_conversion_tests.cpp b/tests/requests_conversion_tests.cpp index f7f152e..384cf28 100644 --- a/tests/requests_conversion_tests.cpp +++ b/tests/requests_conversion_tests.cpp @@ -1,19 +1,20 @@ #include -#include "requests_internal.hpp" +#include "conversions_internal.hpp" namespace { using farsounder::FieldOfView; using farsounder::ResultCode; using farsounder::SystemType; -using farsounder::requests::detail::convert_fov; -using farsounder::requests::detail::convert_fov_to_proto; -using farsounder::requests::detail::convert_processor_settings; -using farsounder::requests::detail::convert_result; -using farsounder::requests::detail::convert_result_code; -using farsounder::requests::detail::convert_system_type; -using farsounder::requests::detail::convert_timestamp; +using farsounder::detail::convert_fov; +using farsounder::detail::convert_fov_to_proto; +using farsounder::detail::convert_processor_settings; +using farsounder::detail::convert_result; +using farsounder::detail::convert_result_code; +using farsounder::detail::convert_system_type; +using farsounder::detail::convert_timestamp; +using farsounder::detail::convert_vessel_info; TEST(ResultCodeMapping, MapsAllValues) { EXPECT_EQ(convert_result_code(proto::nav_api::RequestResult::kSuccess), @@ -34,6 +35,12 @@ TEST(ResultCodeMapping, MapsAllValues) { ResultCode::InvalidRequest); } +TEST(ResultCodeMapping, UnknownDefaultsToUnknownError) { + const auto unknown_code = + static_cast(99); + EXPECT_EQ(convert_result_code(unknown_code), ResultCode::UnknownError); +} + TEST(FieldOfViewMapping, MapsAllValues) { EXPECT_EQ(convert_fov(proto::nav_api::k120d100m), FieldOfView::k120d100m); EXPECT_EQ(convert_fov(proto::nav_api::k120d200m), FieldOfView::k120d200m); @@ -56,6 +63,14 @@ TEST(FieldOfViewMapping, RoundTripsThroughProto) { } } +TEST(FieldOfViewMapping, UnknownDefaultsToNinetyDegreeFiveHundredMeters) { + const auto unknown_fov = static_cast(999); + EXPECT_EQ(convert_fov(unknown_fov), FieldOfView::k90d500m); + + const auto unknown_wrapper = static_cast(999); + EXPECT_EQ(convert_fov_to_proto(unknown_wrapper), proto::nav_api::k90d500m); +} + TEST(SystemTypeMapping, MapsAllValues) { EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS500), SystemType::kFS500); @@ -65,6 +80,12 @@ TEST(SystemTypeMapping, MapsAllValues) { SystemType::kFS350); } +TEST(SystemTypeMapping, UnknownDefaultsToFs500) { + const auto unknown_system = + static_cast(999); + EXPECT_EQ(convert_system_type(unknown_system), SystemType::kFS500); +} + TEST(TimestampConversion, ConvertsEpochWithMilliseconds) { proto::time::Time time; time.set_year(1970); @@ -112,6 +133,17 @@ TEST(RequestResultConversion, CopiesFields) { EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 1.0); } +TEST(RequestResultConversion, MissingTimeKeepsDefaultTimestamp) { + proto::nav_api::RequestResult result; + result.set_code(proto::nav_api::RequestResult::kSuccess); + result.set_result_detail("ok"); + + const auto converted = convert_result(result); + EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); + EXPECT_EQ(converted.code, ResultCode::Success); + EXPECT_EQ(converted.detail, "ok"); +} + TEST(ProcessorSettingsConversion, CopiesAllFields) { proto::nav_api::ProcessorSettings settings; settings.set_min_inwater_squelch(1.0f); @@ -142,4 +174,28 @@ TEST(ProcessorSettingsConversion, CopiesAllFields) { EXPECT_EQ(converted.fov, FieldOfView::k90d350m); } +TEST(ProcessorSettingsConversion, MissingTimeKeepsDefaultTimestamp) { + proto::nav_api::ProcessorSettings settings; + settings.set_min_inwater_squelch(1.0f); + settings.set_max_inwater_squelch(2.0f); + settings.set_inwater_squelch(1.5f); + settings.set_squelchless_inwater_detector(false); + settings.set_detect_bottom(true); + settings.set_system_type(proto::nav_api::ProcessorSettings::kFS500); + settings.set_fov(proto::nav_api::k90d500m); + + const auto converted = convert_processor_settings(settings); + EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); +} + +TEST(VesselInfoConversion, CopiesFields) { + proto::nav_api::VesselInfo vessel_info; + vessel_info.set_draft(2.5f); + vessel_info.set_keel_offset(-0.5f); + + const auto converted = convert_vessel_info(vessel_info); + EXPECT_FLOAT_EQ(converted.draft, 2.5f); + EXPECT_FLOAT_EQ(converted.keel_offset, -0.5f); +} + } // namespace diff --git a/tests/subscriber_conversion_tests.cpp b/tests/subscriber_conversion_tests.cpp index c0bee4c..3fa08dc 100644 --- a/tests/subscriber_conversion_tests.cpp +++ b/tests/subscriber_conversion_tests.cpp @@ -2,7 +2,7 @@ #include -#include "subscriber_internal.hpp" +#include "conversions_internal.hpp" namespace { @@ -58,6 +58,46 @@ TEST(SubscriberConversions, EnumMappings) { ArrayDataOrder::kColumnMajor); } +TEST(SubscriberConversions, EnumMappingsUseDefaultsForUnknownValues) { + const auto unknown_system = + static_cast(999); + const auto unknown_fov = static_cast(999); + const auto unknown_type = static_cast(999); + const auto unknown_order = static_cast(999); + + EXPECT_EQ(convert_system_type(unknown_system), SystemType::kFS500); + EXPECT_EQ(convert_fov(unknown_fov), FieldOfView::k90d500m); + EXPECT_EQ(convert_array_type(unknown_type), ArrayDataType::kByte); + EXPECT_EQ(convert_array_order(unknown_order), ArrayDataOrder::kRowMajor); +} + +TEST(SubscriberConversions, ArrayTypeMappingsCoverAllValues) { + EXPECT_EQ(convert_array_type(proto::array::ArrayData::BYTE), + ArrayDataType::kByte); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT16), + ArrayDataType::kInt16); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT16), + ArrayDataType::kUInt16); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT32), + ArrayDataType::kInt32); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT32), + ArrayDataType::kUInt32); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT64), + ArrayDataType::kInt64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT64), + ArrayDataType::kUInt64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT32), + ArrayDataType::kFloat32); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT64), + ArrayDataType::kFloat64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::COMPLEX64), + ArrayDataType::kComplex64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::COMPLEX128), + ArrayDataType::kComplex128); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::BOOL), + ArrayDataType::kBool); +} + TEST(SubscriberConversions, BinConversionCopiesFields) { proto::nav_api::Bin bin; bin.set_hor_index(1); @@ -114,6 +154,22 @@ TEST(SubscriberConversions, HydrophoneDataConversionCopiesArrayData) { EXPECT_EQ(converted.raw_timeseries, std::string("\x01\x02\x03\x04", 4)); } +TEST(SubscriberConversions, HydrophoneDataConversionHandlesMissingRawTimeseries) { + proto::nav_api::HydrophoneData proto_data; + proto_data.set_serial("serial-missing-array"); + proto_data.set_transmit_id("tx-missing-array"); + proto_data.set_num_hor_phones(1); + proto_data.set_num_ver_phones(2); + + const auto converted = convert_hydrophone_data(proto_data); + EXPECT_EQ(converted.serial, "serial-missing-array"); + EXPECT_EQ(converted.transmit_id, "tx-missing-array"); + EXPECT_TRUE(converted.dims.empty()); + EXPECT_EQ(converted.type, ArrayDataType::kByte); + EXPECT_EQ(converted.order, ArrayDataOrder::kRowMajor); + EXPECT_TRUE(converted.raw_timeseries.empty()); +} + TEST(SubscriberConversions, TargetDataConversionCopiesFields) { proto::nav_api::TargetData proto_data; proto_data.set_serial("serial-2"); @@ -188,6 +244,23 @@ TEST(SubscriberConversions, TargetDataConversionCopiesFields) { EXPECT_EQ(converted.max_range_index, 7); } +TEST(SubscriberConversions, TargetDataConversionHandlesMissingOptionalFields) { + proto::nav_api::TargetData proto_data; + proto_data.set_serial("serial-minimal"); + proto_data.set_max_depth(10.0); + proto_data.set_max_range_index(3); + + const auto converted = convert_target_data(proto_data); + EXPECT_EQ(converted.serial, "serial-minimal"); + EXPECT_FALSE(converted.heading.has_value()); + EXPECT_FALSE(converted.position.has_value()); + EXPECT_TRUE(converted.bottom.empty()); + EXPECT_TRUE(converted.groups.empty()); + EXPECT_FALSE(converted.grid_description.has_value()); + EXPECT_DOUBLE_EQ(converted.max_depth, 10.0); + EXPECT_EQ(converted.max_range_index, 3); +} + TEST(SubscriberConversions, ProcessorSettingsConversionCopiesFields) { proto::nav_api::ProcessorSettings proto_settings; proto_settings.set_min_inwater_squelch(1.0f); From 2741235ffe9d54a7ef1c997da294f2541a688ddb Mon Sep 17 00:00:00 2001 From: Heath Henley Date: Fri, 13 Feb 2026 10:54:33 -0500 Subject: [PATCH 2/3] DRY tests / conversion more Move all conversion tests to one place Autoformat with clang format --- examples/basic_client.cpp | 22 +- src/conversions.cpp | 7 +- src/requests.cpp | 25 +- tests/CMakeLists.txt | 2 - tests/conversions_unit_tests.cpp | 439 +++++++++++++++++++++----- tests/requests_conversion_tests.cpp | 201 ------------ tests/subscriber_conversion_tests.cpp | 303 ------------------ 7 files changed, 390 insertions(+), 609 deletions(-) delete mode 100644 tests/requests_conversion_tests.cpp delete mode 100644 tests/subscriber_conversion_tests.cpp diff --git a/examples/basic_client.cpp b/examples/basic_client.cpp index 8e25804..b55ec47 100644 --- a/examples/basic_client.cpp +++ b/examples/basic_client.cpp @@ -22,19 +22,23 @@ void handle_sigint(int) { } } // namespace -int main( - int argc, char** argv -) { - +int main(int argc, char** argv) { if (argc > 2 || (argc > 1 && std::string(argv[1]) == kHelpFlag)) { std::cout << "Usage: " << argv[0] << " [host]" << '\n'; - std::cout << " host: the host to connect to (default: " << kDefaultHost << ")" << std::endl; - std::cout << " This should point at the machine running the" << std::endl + std::cout << " host: the host to connect to (default: " << kDefaultHost + << ")" << std::endl; + std::cout << " This should point at the machine running the" + << std::endl << " SonaSoft demo or SonaSoft nav software." << std::endl; - std::cout << " On WSL, you can use the ip address of the WSL host." << std::endl; - std::cout << " One way to get this ip is to run this inside the WSL terminal:" << std::endl; + std::cout << " On WSL, you can use the ip address of the WSL host." + << std::endl; + std::cout << " One way to get this ip is to run this inside the WSL " + "terminal:" + << std::endl; std::cout << " $ ip route | awk '/default/ {print $3}'" << std::endl; - std::cout << " For example, if the ip address is 172.30.64.1, you can use:" << std::endl; + std::cout + << " For example, if the ip address is 172.30.64.1, you can use:" + << std::endl; std::cout << " " << argv[0] << " 172.30.64.1" << std::endl; return 1; } diff --git a/src/conversions.cpp b/src/conversions.cpp index 1c38119..86d2f34 100644 --- a/src/conversions.cpp +++ b/src/conversions.cpp @@ -1,8 +1,8 @@ -#include "conversions_internal.hpp" - #include #include +#include "conversions_internal.hpp" + namespace farsounder::detail { Timestamp convert_timestamp(const proto::time::Time& t) { @@ -184,7 +184,8 @@ GridDescription convert_grid_description( return grid; } -HydrophoneData convert_hydrophone_data(const proto::nav_api::HydrophoneData& h) { +HydrophoneData convert_hydrophone_data( + const proto::nav_api::HydrophoneData& h) { HydrophoneData data; if (h.has_time()) { data.time = convert_timestamp(h.time()); diff --git a/src/requests.cpp b/src/requests.cpp index 644978c..89890ac 100644 --- a/src/requests.cpp +++ b/src/requests.cpp @@ -155,9 +155,10 @@ GetProcessorSettingsResponse get_processor_settings( config, config::ReqRepEndpoint::GetProcessorSettings, request); GetProcessorSettingsResponse response; - response.result = farsounder::detail::convert_result(proto_response.result()); - response.settings = - farsounder::detail::convert_processor_settings(proto_response.settings()); + response.result = + farsounder::detail::convert_result(proto_response.result()); + response.settings = farsounder::detail::convert_processor_settings( + proto_response.settings()); return response; } @@ -176,7 +177,8 @@ SetFieldOfViewResponse set_field_of_view(const config::ClientConfig& config, config, config::ReqRepEndpoint::SetFieldOfView, request); SetFieldOfViewResponse response; - response.result = farsounder::detail::convert_result(proto_response.result()); + response.result = + farsounder::detail::convert_result(proto_response.result()); return response; } @@ -197,7 +199,8 @@ SetBottomDetectionResponse set_bottom_detection( config, config::ReqRepEndpoint::SetBottomDetection, request); SetBottomDetectionResponse response; - response.result = farsounder::detail::convert_result(proto_response.result()); + response.result = + farsounder::detail::convert_result(proto_response.result()); return response; } @@ -218,7 +221,8 @@ SetInWaterSquelchResponse set_inwater_squelch( config, config::ReqRepEndpoint::SetInWaterSquelch, request); SetInWaterSquelchResponse response; - response.result = farsounder::detail::convert_result(proto_response.result()); + response.result = + farsounder::detail::convert_result(proto_response.result()); return response; } @@ -240,7 +244,8 @@ SetSquelchlessInWaterDetectorResponse set_squelchless_inwater_detector( request); SetSquelchlessInWaterDetectorResponse response; - response.result = farsounder::detail::convert_result(proto_response.result()); + response.result = + farsounder::detail::convert_result(proto_response.result()); return response; } @@ -261,8 +266,10 @@ GetVesselInfoResponse get_vessel_info(const config::ClientConfig& config) { config, config::ReqRepEndpoint::GetVesselInfo, request); GetVesselInfoResponse response; - response.result = farsounder::detail::convert_result(proto_response.result()); - response.info = farsounder::detail::convert_vessel_info(proto_response.info()); + response.result = + farsounder::detail::convert_result(proto_response.result()); + response.info = + farsounder::detail::convert_vessel_info(proto_response.info()); return response; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0a8bf7b..82b6093 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,9 +2,7 @@ include(GoogleTest) add_executable(farsounder_tests conversions_unit_tests.cpp - requests_conversion_tests.cpp history_parse_tests.cpp - subscriber_conversion_tests.cpp ) target_include_directories(farsounder_tests diff --git a/tests/conversions_unit_tests.cpp b/tests/conversions_unit_tests.cpp index e1e3157..693d260 100644 --- a/tests/conversions_unit_tests.cpp +++ b/tests/conversions_unit_tests.cpp @@ -1,5 +1,7 @@ #include +#include + #include "conversions_internal.hpp" namespace { @@ -7,6 +9,7 @@ namespace { using farsounder::ArrayDataOrder; using farsounder::ArrayDataType; using farsounder::FieldOfView; +using farsounder::GridMode; using farsounder::ResultCode; using farsounder::SystemType; using farsounder::detail::convert_array_order; @@ -23,121 +26,393 @@ using farsounder::detail::convert_target_data; using farsounder::detail::convert_timestamp; using farsounder::detail::convert_vessel_info; -TEST(SharedConversions, CoversRequestSpecificConverters) { +TEST(ResultCodeMapping, MapsAllValues) { EXPECT_EQ(convert_result_code(proto::nav_api::RequestResult::kSuccess), ResultCode::Success); + EXPECT_EQ(convert_result_code(proto::nav_api::RequestResult::kUnknownError), + ResultCode::UnknownError); + EXPECT_EQ(convert_result_code( + proto::nav_api::RequestResult::kOperationUnavailable), + ResultCode::OperationUnavailable); + EXPECT_EQ(convert_result_code( + proto::nav_api::RequestResult::kParameterOutOfRange), + ResultCode::ParameterOutOfRange); + EXPECT_EQ( + convert_result_code(proto::nav_api::RequestResult::kParameterMissing), + ResultCode::ParameterMissing); + EXPECT_EQ( + convert_result_code(proto::nav_api::RequestResult::kInvalidRequest), + ResultCode::InvalidRequest); +} + +TEST(ResultCodeMapping, UnknownDefaultsToUnknownError) { EXPECT_EQ(convert_result_code( static_cast(999)), ResultCode::UnknownError); +} - proto::nav_api::RequestResult result; - result.set_code(proto::nav_api::RequestResult::kInvalidRequest); - result.set_result_detail("bad request"); - const auto converted_result = convert_result(result); - EXPECT_DOUBLE_EQ(converted_result.time.seconds_since_epoch, 0.0); - EXPECT_EQ(converted_result.code, ResultCode::InvalidRequest); - EXPECT_EQ(converted_result.detail, "bad request"); - - EXPECT_EQ(convert_fov_to_proto(FieldOfView::k120d100m), - proto::nav_api::k120d100m); +TEST(FieldOfViewMapping, MapsAllValues) { + EXPECT_EQ(convert_fov(proto::nav_api::k120d100m), FieldOfView::k120d100m); + EXPECT_EQ(convert_fov(proto::nav_api::k120d200m), FieldOfView::k120d200m); + EXPECT_EQ(convert_fov(proto::nav_api::k90d500m), FieldOfView::k90d500m); + EXPECT_EQ(convert_fov(proto::nav_api::k60d1000m), FieldOfView::k60d1000m); + EXPECT_EQ(convert_fov(proto::nav_api::k90d100m), FieldOfView::k90d100m); + EXPECT_EQ(convert_fov(proto::nav_api::k90d200m), FieldOfView::k90d200m); + EXPECT_EQ(convert_fov(proto::nav_api::k90d350m), FieldOfView::k90d350m); + EXPECT_EQ(convert_fov(proto::nav_api::kStandby), FieldOfView::kStandby); } -TEST(SharedConversions, CoversSharedEnumConvertersWithDefaults) { - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS1000), - SystemType::kFS1000); - EXPECT_EQ(convert_system_type( - static_cast( - 999)), - SystemType::kFS500); +TEST(FieldOfViewMapping, RoundTripsThroughProto) { + const FieldOfView values[] = { + FieldOfView::k120d100m, FieldOfView::k120d200m, FieldOfView::k90d500m, + FieldOfView::k60d1000m, FieldOfView::k90d100m, FieldOfView::k90d200m, + FieldOfView::k90d350m, FieldOfView::kStandby}; - EXPECT_EQ(convert_fov(proto::nav_api::k120d200m), FieldOfView::k120d200m); + for (auto value : values) { + EXPECT_EQ(convert_fov(convert_fov_to_proto(value)), value); + } +} + +TEST(FieldOfViewMapping, UnknownDefaultsToNinetyDegreeFiveHundredMeters) { EXPECT_EQ(convert_fov(static_cast(999)), FieldOfView::k90d500m); + EXPECT_EQ(convert_fov_to_proto(static_cast(999)), + proto::nav_api::k90d500m); +} - EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT64), - ArrayDataType::kFloat64); - EXPECT_EQ(convert_array_type(static_cast(999)), - ArrayDataType::kByte); +TEST(SystemTypeMapping, MapsAllValues) { + EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS500), + SystemType::kFS500); + EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS1000), + SystemType::kFS1000); + EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS350), + SystemType::kFS350); +} - EXPECT_EQ(convert_array_order(proto::array::ArrayData::COLUMN_MAJOR), - ArrayDataOrder::kColumnMajor); +TEST(SystemTypeMapping, UnknownDefaultsToFs500) { EXPECT_EQ( - convert_array_order(static_cast(999)), - ArrayDataOrder::kRowMajor); + convert_system_type( + static_cast(999)), + SystemType::kFS500); } -TEST(SharedConversions, CoversTimestampAndStructConverters) { +TEST(TimestampConversion, ConvertsEpochWithMilliseconds) { proto::time::Time time; time.set_year(1970); time.set_month(1); time.set_day(1); time.set_hour(0); time.set_minute(0); - time.set_second(3); + time.set_second(0); time.set_millisecond(500); - EXPECT_DOUBLE_EQ(convert_timestamp(time).seconds_since_epoch, 3.5); + EXPECT_DOUBLE_EQ(convert_timestamp(time).seconds_since_epoch, 0.5); +} - proto::nav_api::Bin bin; - bin.set_hor_index(8); - bin.set_strength(1.25f); - const auto converted_bin = convert_bin(bin); - EXPECT_EQ(converted_bin.hor_index, 8); - EXPECT_FLOAT_EQ(converted_bin.strength, 1.25f); +TEST(TimestampConversion, ConvertsDateTimeToEpoch) { + proto::time::Time time; + time.set_year(2026); + time.set_month(2); + time.set_day(9); + time.set_hour(15); + time.set_minute(42); + time.set_second(38); + time.set_millisecond(188); + EXPECT_NEAR(convert_timestamp(time).seconds_since_epoch, 1770651758.188, + 1e-6); +} + +TEST(RequestResultConversion, CopiesFields) { + proto::nav_api::RequestResult result; + result.set_code(proto::nav_api::RequestResult::kParameterOutOfRange); + result.set_result_detail("range error"); + auto* time = result.mutable_time(); + time->set_year(1970); + time->set_month(1); + time->set_day(1); + time->set_hour(0); + time->set_minute(0); + time->set_second(1); + time->set_millisecond(0); + const auto converted = convert_result(result); + EXPECT_EQ(converted.code, ResultCode::ParameterOutOfRange); + EXPECT_EQ(converted.detail, "range error"); + EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 1.0); +} + +TEST(RequestResultConversion, MissingTimeKeepsDefaultTimestamp) { + proto::nav_api::RequestResult result; + result.set_code(proto::nav_api::RequestResult::kSuccess); + result.set_result_detail("ok"); + const auto converted = convert_result(result); + EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); + EXPECT_EQ(converted.code, ResultCode::Success); + EXPECT_EQ(converted.detail, "ok"); +} +TEST(ProcessorSettingsConversion, CopiesAllFields) { proto::nav_api::ProcessorSettings settings; + settings.set_min_inwater_squelch(1.0f); + settings.set_max_inwater_squelch(2.0f); + settings.set_inwater_squelch(1.5f); + settings.set_squelchless_inwater_detector(true); + settings.set_detect_bottom(false); settings.set_system_type(proto::nav_api::ProcessorSettings::kFS350); settings.set_fov(proto::nav_api::k90d350m); - const auto converted_settings = convert_processor_settings(settings); - EXPECT_EQ(converted_settings.system_type, SystemType::kFS350); - EXPECT_EQ(converted_settings.fov, FieldOfView::k90d350m); - EXPECT_DOUBLE_EQ(converted_settings.time.seconds_since_epoch, 0.0); + auto* time = settings.mutable_time(); + time->set_year(1970); + time->set_month(1); + time->set_day(1); + time->set_hour(0); + time->set_minute(0); + time->set_second(0); + time->set_millisecond(0); + const auto converted = convert_processor_settings(settings); + EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); + EXPECT_FLOAT_EQ(converted.min_inwater_squelch, 1.0f); + EXPECT_FLOAT_EQ(converted.max_inwater_squelch, 2.0f); + EXPECT_FLOAT_EQ(converted.inwater_squelch, 1.5f); + EXPECT_TRUE(converted.squelchless_inwater_detector); + EXPECT_FALSE(converted.detect_bottom); + EXPECT_EQ(converted.system_type, SystemType::kFS350); + EXPECT_EQ(converted.fov, FieldOfView::k90d350m); +} +TEST(ProcessorSettingsConversion, MissingTimeKeepsDefaultTimestamp) { + proto::nav_api::ProcessorSettings settings; + settings.set_min_inwater_squelch(1.0f); + settings.set_max_inwater_squelch(2.0f); + settings.set_inwater_squelch(1.5f); + settings.set_squelchless_inwater_detector(false); + settings.set_detect_bottom(true); + settings.set_system_type(proto::nav_api::ProcessorSettings::kFS500); + settings.set_fov(proto::nav_api::k90d500m); + const auto converted = convert_processor_settings(settings); + EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); +} + +TEST(VesselInfoConversion, CopiesFields) { proto::nav_api::VesselInfo vessel; vessel.set_draft(3.5f); vessel.set_keel_offset(0.25f); - const auto converted_vessel = convert_vessel_info(vessel); - EXPECT_FLOAT_EQ(converted_vessel.draft, 3.5f); - EXPECT_FLOAT_EQ(converted_vessel.keel_offset, 0.25f); -} - -TEST(SharedConversions, CoversSubscriberPayloadConverters) { - proto::nav_api::HydrophoneData hydrophone; - hydrophone.set_serial("h"); - hydrophone.set_transmit_id("tx"); - auto hydrophone_converted = convert_hydrophone_data(hydrophone); - EXPECT_TRUE(hydrophone_converted.dims.empty()); - EXPECT_EQ(hydrophone_converted.type, ArrayDataType::kByte); - - auto* raw_timeseries = hydrophone.mutable_raw_timeseries(); - raw_timeseries->add_dims(2); - raw_timeseries->set_type(proto::array::ArrayData::INT16); - raw_timeseries->set_order(proto::array::ArrayData::COLUMN_MAJOR); - raw_timeseries->set_data(std::string("\x01\x02", 2)); - hydrophone_converted = convert_hydrophone_data(hydrophone); - ASSERT_EQ(hydrophone_converted.dims.size(), 1u); - EXPECT_EQ(hydrophone_converted.type, ArrayDataType::kInt16); - EXPECT_EQ(hydrophone_converted.order, ArrayDataOrder::kColumnMajor); - - proto::nav_api::TargetData target; - target.set_serial("t"); - target.set_max_depth(4.0); - target.set_max_range_index(2); - auto target_converted = convert_target_data(target); - EXPECT_FALSE(target_converted.heading.has_value()); - EXPECT_TRUE(target_converted.bottom.empty()); - EXPECT_FALSE(target_converted.grid_description.has_value()); - - auto* heading = target.mutable_heading(); - heading->set_heading(1.0); - auto* bottom = target.add_bottom(); - bottom->set_hor_index(10); - auto* grid = target.mutable_grid_description(); + const auto converted = convert_vessel_info(vessel); + EXPECT_FLOAT_EQ(converted.draft, 3.5f); + EXPECT_FLOAT_EQ(converted.keel_offset, 0.25f); +} + +TEST(ArrayTypeMapping, MapsAllValues) { + EXPECT_EQ(convert_array_type(proto::array::ArrayData::BYTE), + ArrayDataType::kByte); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT16), + ArrayDataType::kInt16); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT16), + ArrayDataType::kUInt16); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT32), + ArrayDataType::kInt32); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT32), + ArrayDataType::kUInt32); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT64), + ArrayDataType::kInt64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT64), + ArrayDataType::kUInt64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT32), + ArrayDataType::kFloat32); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT64), + ArrayDataType::kFloat64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::COMPLEX64), + ArrayDataType::kComplex64); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::COMPLEX128), + ArrayDataType::kComplex128); + EXPECT_EQ(convert_array_type(proto::array::ArrayData::BOOL), + ArrayDataType::kBool); +} + +TEST(ArrayTypeMapping, UnknownDefaultsToByte) { + EXPECT_EQ( + convert_array_type(static_cast(999)), + ArrayDataType::kByte); +} + +TEST(ArrayOrderMapping, MapsAllValues) { + EXPECT_EQ(convert_array_order(proto::array::ArrayData::ROW_MAJOR), + ArrayDataOrder::kRowMajor); + EXPECT_EQ(convert_array_order(proto::array::ArrayData::COLUMN_MAJOR), + ArrayDataOrder::kColumnMajor); +} + +TEST(ArrayOrderMapping, UnknownDefaultsToRowMajor) { + EXPECT_EQ( + convert_array_order(static_cast(999)), + ArrayDataOrder::kRowMajor); +} + +TEST(BinConversion, CopiesFields) { + proto::nav_api::Bin bin; + bin.set_hor_index(1); + bin.set_ver_index(2); + bin.set_range_index(3); + bin.set_cross_range(1.5f); + bin.set_down_range(2.5f); + bin.set_depth(3.5f); + bin.set_strength(4.5f); + const auto converted = convert_bin(bin); + EXPECT_EQ(converted.hor_index, 1); + EXPECT_EQ(converted.ver_index, 2); + EXPECT_EQ(converted.range_index, 3); + EXPECT_FLOAT_EQ(converted.cross_range, 1.5f); + EXPECT_FLOAT_EQ(converted.down_range, 2.5f); + EXPECT_FLOAT_EQ(converted.depth, 3.5f); + EXPECT_FLOAT_EQ(converted.strength, 4.5f); +} + +TEST(HydrophoneConversion, CopiesArrayDataWhenPresent) { + proto::nav_api::HydrophoneData proto_data; + proto_data.set_serial("serial-1"); + proto_data.set_transmit_id("tx-1"); + proto_data.set_num_hor_phones(3); + proto_data.set_num_ver_phones(4); + auto* time = proto_data.mutable_time(); + time->set_year(1970); + time->set_month(1); + time->set_day(1); + time->set_hour(0); + time->set_minute(0); + time->set_second(0); + time->set_millisecond(0); + auto* array = proto_data.mutable_raw_timeseries(); + array->add_dims(12); + array->add_dims(256); + array->set_type(proto::array::ArrayData::INT16); + array->set_order(proto::array::ArrayData::COLUMN_MAJOR); + array->set_data(std::string("\x01\x02\x03\x04", 4)); + const auto converted = convert_hydrophone_data(proto_data); + EXPECT_EQ(converted.serial, "serial-1"); + EXPECT_EQ(converted.transmit_id, "tx-1"); + EXPECT_EQ(converted.num_hor_phones, 3); + EXPECT_EQ(converted.num_ver_phones, 4); + ASSERT_EQ(converted.dims.size(), 2u); + EXPECT_EQ(converted.dims[0], 12); + EXPECT_EQ(converted.dims[1], 256); + EXPECT_EQ(converted.type, ArrayDataType::kInt16); + EXPECT_EQ(converted.order, ArrayDataOrder::kColumnMajor); + EXPECT_EQ(converted.raw_timeseries, std::string("\x01\x02\x03\x04", 4)); +} + +TEST(HydrophoneConversion, HandlesMissingRawTimeseries) { + proto::nav_api::HydrophoneData proto_data; + proto_data.set_serial("serial-missing-array"); + proto_data.set_transmit_id("tx-missing-array"); + proto_data.set_num_hor_phones(1); + proto_data.set_num_ver_phones(2); + const auto converted = convert_hydrophone_data(proto_data); + EXPECT_EQ(converted.serial, "serial-missing-array"); + EXPECT_EQ(converted.transmit_id, "tx-missing-array"); + EXPECT_TRUE(converted.dims.empty()); + EXPECT_EQ(converted.type, ArrayDataType::kByte); + EXPECT_EQ(converted.order, ArrayDataOrder::kRowMajor); + EXPECT_TRUE(converted.raw_timeseries.empty()); +} + +TEST(TargetDataConversion, CopiesFields) { + proto::nav_api::TargetData proto_data; + proto_data.set_serial("serial-2"); + proto_data.set_max_depth(55.0); + proto_data.set_max_range_index(7); + auto* time = proto_data.mutable_time(); + time->set_year(1970); + time->set_month(1); + time->set_day(1); + time->set_hour(0); + time->set_minute(0); + time->set_second(1); + time->set_millisecond(0); + auto* heading = proto_data.mutable_heading(); + heading->set_heading(12.5); + auto* position = proto_data.mutable_position(); + position->set_lat(42.0); + position->set_lon(-70.0); + auto* bottom = proto_data.add_bottom(); + bottom->set_hor_index(1); + bottom->set_ver_index(2); + bottom->set_range_index(3); + bottom->set_cross_range(1.0f); + bottom->set_down_range(2.0f); + bottom->set_depth(3.0f); + bottom->set_strength(4.0f); + auto* group = proto_data.add_groups(); + auto* group_bin = group->add_bins(); + group_bin->set_hor_index(4); + group_bin->set_ver_index(5); + group_bin->set_range_index(6); + group_bin->set_cross_range(5.0f); + group_bin->set_down_range(6.0f); + group_bin->set_depth(7.0f); + group_bin->set_strength(8.0f); + auto* grid = proto_data.mutable_grid_description(); grid->set_mode(proto::grid_description::GridDescription::kFixed); - target_converted = convert_target_data(target); - EXPECT_TRUE(target_converted.heading.has_value()); - ASSERT_EQ(target_converted.bottom.size(), 1u); - EXPECT_EQ(target_converted.bottom[0].hor_index, 10); - EXPECT_TRUE(target_converted.grid_description.has_value()); + grid->add_hor_angles(1.0); + grid->add_hor_angles(-1.0); + grid->add_ver_angles(2.0); + grid->add_ver_angles(-2.0); + grid->set_max_range(123.0); + const auto converted = convert_target_data(proto_data); + EXPECT_EQ(converted.serial, "serial-2"); + ASSERT_TRUE(converted.heading.has_value()); + EXPECT_DOUBLE_EQ(converted.heading->degrees, 12.5); + ASSERT_TRUE(converted.position.has_value()); + EXPECT_DOUBLE_EQ(converted.position->latitude_degrees, 42.0); + EXPECT_DOUBLE_EQ(converted.position->longitude_degrees, -70.0); + ASSERT_EQ(converted.bottom.size(), 1u); + EXPECT_EQ(converted.bottom[0].hor_index, 1); + ASSERT_EQ(converted.groups.size(), 1u); + ASSERT_EQ(converted.groups[0].bins.size(), 1u); + EXPECT_EQ(converted.groups[0].bins[0].hor_index, 4); + ASSERT_TRUE(converted.grid_description.has_value()); + EXPECT_EQ(converted.grid_description->mode, GridMode::kFixed); + ASSERT_EQ(converted.grid_description->hor_angles.size(), 2u); + EXPECT_DOUBLE_EQ(converted.grid_description->hor_angles[0], 1.0); + EXPECT_DOUBLE_EQ(converted.grid_description->hor_angles[1], -1.0); + ASSERT_EQ(converted.grid_description->ver_angles.size(), 2u); + EXPECT_DOUBLE_EQ(converted.grid_description->ver_angles[0], 2.0); + EXPECT_DOUBLE_EQ(converted.grid_description->ver_angles[1], -2.0); + EXPECT_DOUBLE_EQ(converted.grid_description->max_range, 123.0); + EXPECT_DOUBLE_EQ(converted.max_depth, 55.0); + EXPECT_EQ(converted.max_range_index, 7); +} + +TEST(TargetDataConversion, HandlesMissingOptionalFields) { + proto::nav_api::TargetData proto_data; + proto_data.set_serial("serial-minimal"); + proto_data.set_max_depth(10.0); + proto_data.set_max_range_index(3); + const auto converted = convert_target_data(proto_data); + EXPECT_EQ(converted.serial, "serial-minimal"); + EXPECT_FALSE(converted.heading.has_value()); + EXPECT_FALSE(converted.position.has_value()); + EXPECT_TRUE(converted.bottom.empty()); + EXPECT_TRUE(converted.groups.empty()); + EXPECT_FALSE(converted.grid_description.has_value()); + EXPECT_DOUBLE_EQ(converted.max_depth, 10.0); + EXPECT_EQ(converted.max_range_index, 3); +} + +TEST(BasicSanity, ConversionHelpersRemainUsable) { + // Keep a small broad sanity for grouped helper usage. + proto::nav_api::Bin bin; + bin.set_hor_index(8); + bin.set_strength(1.25f); + const auto converted_bin = convert_bin(bin); + EXPECT_EQ(converted_bin.hor_index, 8); + EXPECT_FLOAT_EQ(converted_bin.strength, 1.25f); + + proto::time::Time time; + time.set_year(1970); + time.set_month(1); + time.set_day(1); + time.set_hour(0); + time.set_minute(0); + time.set_second(3); + time.set_millisecond(500); + EXPECT_DOUBLE_EQ(convert_timestamp(time).seconds_since_epoch, 3.5); } } // namespace diff --git a/tests/requests_conversion_tests.cpp b/tests/requests_conversion_tests.cpp deleted file mode 100644 index 384cf28..0000000 --- a/tests/requests_conversion_tests.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include - -#include "conversions_internal.hpp" - -namespace { - -using farsounder::FieldOfView; -using farsounder::ResultCode; -using farsounder::SystemType; -using farsounder::detail::convert_fov; -using farsounder::detail::convert_fov_to_proto; -using farsounder::detail::convert_processor_settings; -using farsounder::detail::convert_result; -using farsounder::detail::convert_result_code; -using farsounder::detail::convert_system_type; -using farsounder::detail::convert_timestamp; -using farsounder::detail::convert_vessel_info; - -TEST(ResultCodeMapping, MapsAllValues) { - EXPECT_EQ(convert_result_code(proto::nav_api::RequestResult::kSuccess), - ResultCode::Success); - EXPECT_EQ(convert_result_code(proto::nav_api::RequestResult::kUnknownError), - ResultCode::UnknownError); - EXPECT_EQ(convert_result_code( - proto::nav_api::RequestResult::kOperationUnavailable), - ResultCode::OperationUnavailable); - EXPECT_EQ(convert_result_code( - proto::nav_api::RequestResult::kParameterOutOfRange), - ResultCode::ParameterOutOfRange); - EXPECT_EQ( - convert_result_code(proto::nav_api::RequestResult::kParameterMissing), - ResultCode::ParameterMissing); - EXPECT_EQ( - convert_result_code(proto::nav_api::RequestResult::kInvalidRequest), - ResultCode::InvalidRequest); -} - -TEST(ResultCodeMapping, UnknownDefaultsToUnknownError) { - const auto unknown_code = - static_cast(99); - EXPECT_EQ(convert_result_code(unknown_code), ResultCode::UnknownError); -} - -TEST(FieldOfViewMapping, MapsAllValues) { - EXPECT_EQ(convert_fov(proto::nav_api::k120d100m), FieldOfView::k120d100m); - EXPECT_EQ(convert_fov(proto::nav_api::k120d200m), FieldOfView::k120d200m); - EXPECT_EQ(convert_fov(proto::nav_api::k90d500m), FieldOfView::k90d500m); - EXPECT_EQ(convert_fov(proto::nav_api::k60d1000m), FieldOfView::k60d1000m); - EXPECT_EQ(convert_fov(proto::nav_api::k90d100m), FieldOfView::k90d100m); - EXPECT_EQ(convert_fov(proto::nav_api::k90d200m), FieldOfView::k90d200m); - EXPECT_EQ(convert_fov(proto::nav_api::k90d350m), FieldOfView::k90d350m); - EXPECT_EQ(convert_fov(proto::nav_api::kStandby), FieldOfView::kStandby); -} - -TEST(FieldOfViewMapping, RoundTripsThroughProto) { - const FieldOfView values[] = { - FieldOfView::k120d100m, FieldOfView::k120d200m, FieldOfView::k90d500m, - FieldOfView::k60d1000m, FieldOfView::k90d100m, FieldOfView::k90d200m, - FieldOfView::k90d350m, FieldOfView::kStandby}; - - for (auto value : values) { - EXPECT_EQ(convert_fov(convert_fov_to_proto(value)), value); - } -} - -TEST(FieldOfViewMapping, UnknownDefaultsToNinetyDegreeFiveHundredMeters) { - const auto unknown_fov = static_cast(999); - EXPECT_EQ(convert_fov(unknown_fov), FieldOfView::k90d500m); - - const auto unknown_wrapper = static_cast(999); - EXPECT_EQ(convert_fov_to_proto(unknown_wrapper), proto::nav_api::k90d500m); -} - -TEST(SystemTypeMapping, MapsAllValues) { - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS500), - SystemType::kFS500); - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS1000), - SystemType::kFS1000); - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS350), - SystemType::kFS350); -} - -TEST(SystemTypeMapping, UnknownDefaultsToFs500) { - const auto unknown_system = - static_cast(999); - EXPECT_EQ(convert_system_type(unknown_system), SystemType::kFS500); -} - -TEST(TimestampConversion, ConvertsEpochWithMilliseconds) { - proto::time::Time time; - time.set_year(1970); - time.set_month(1); - time.set_day(1); - time.set_hour(0); - time.set_minute(0); - time.set_second(0); - time.set_millisecond(500); - - const auto converted = convert_timestamp(time); - EXPECT_DOUBLE_EQ(converted.seconds_since_epoch, 0.5); -} - -TEST(TimestampConversion, ConvertsDateTimeToEpoch) { - proto::time::Time time; - time.set_year(2026); - time.set_month(2); - time.set_day(9); - time.set_hour(15); - time.set_minute(42); - time.set_second(38); - time.set_millisecond(188); - - const auto converted = convert_timestamp(time); - EXPECT_NEAR(converted.seconds_since_epoch, 1770651758.188, 1e-6); -} - -TEST(RequestResultConversion, CopiesFields) { - proto::nav_api::RequestResult result; - result.set_code(proto::nav_api::RequestResult::kParameterOutOfRange); - result.set_result_detail("range error"); - auto* time = result.mutable_time(); - time->set_year(1970); - time->set_month(1); - time->set_day(1); - time->set_hour(0); - time->set_minute(0); - time->set_second(1); - time->set_millisecond(0); - - const auto converted = convert_result(result); - EXPECT_EQ(converted.code, ResultCode::ParameterOutOfRange); - EXPECT_EQ(converted.detail, "range error"); - EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 1.0); -} - -TEST(RequestResultConversion, MissingTimeKeepsDefaultTimestamp) { - proto::nav_api::RequestResult result; - result.set_code(proto::nav_api::RequestResult::kSuccess); - result.set_result_detail("ok"); - - const auto converted = convert_result(result); - EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); - EXPECT_EQ(converted.code, ResultCode::Success); - EXPECT_EQ(converted.detail, "ok"); -} - -TEST(ProcessorSettingsConversion, CopiesAllFields) { - proto::nav_api::ProcessorSettings settings; - settings.set_min_inwater_squelch(1.0f); - settings.set_max_inwater_squelch(2.0f); - settings.set_inwater_squelch(1.5f); - settings.set_squelchless_inwater_detector(true); - settings.set_detect_bottom(false); - settings.set_system_type(proto::nav_api::ProcessorSettings::kFS350); - settings.set_fov(proto::nav_api::k90d350m); - - auto* time = settings.mutable_time(); - time->set_year(1970); - time->set_month(1); - time->set_day(1); - time->set_hour(0); - time->set_minute(0); - time->set_second(0); - time->set_millisecond(0); - - const auto converted = convert_processor_settings(settings); - EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); - EXPECT_FLOAT_EQ(converted.min_inwater_squelch, 1.0f); - EXPECT_FLOAT_EQ(converted.max_inwater_squelch, 2.0f); - EXPECT_FLOAT_EQ(converted.inwater_squelch, 1.5f); - EXPECT_TRUE(converted.squelchless_inwater_detector); - EXPECT_FALSE(converted.detect_bottom); - EXPECT_EQ(converted.system_type, SystemType::kFS350); - EXPECT_EQ(converted.fov, FieldOfView::k90d350m); -} - -TEST(ProcessorSettingsConversion, MissingTimeKeepsDefaultTimestamp) { - proto::nav_api::ProcessorSettings settings; - settings.set_min_inwater_squelch(1.0f); - settings.set_max_inwater_squelch(2.0f); - settings.set_inwater_squelch(1.5f); - settings.set_squelchless_inwater_detector(false); - settings.set_detect_bottom(true); - settings.set_system_type(proto::nav_api::ProcessorSettings::kFS500); - settings.set_fov(proto::nav_api::k90d500m); - - const auto converted = convert_processor_settings(settings); - EXPECT_DOUBLE_EQ(converted.time.seconds_since_epoch, 0.0); -} - -TEST(VesselInfoConversion, CopiesFields) { - proto::nav_api::VesselInfo vessel_info; - vessel_info.set_draft(2.5f); - vessel_info.set_keel_offset(-0.5f); - - const auto converted = convert_vessel_info(vessel_info); - EXPECT_FLOAT_EQ(converted.draft, 2.5f); - EXPECT_FLOAT_EQ(converted.keel_offset, -0.5f); -} - -} // namespace diff --git a/tests/subscriber_conversion_tests.cpp b/tests/subscriber_conversion_tests.cpp deleted file mode 100644 index 3fa08dc..0000000 --- a/tests/subscriber_conversion_tests.cpp +++ /dev/null @@ -1,303 +0,0 @@ -#include - -#include - -#include "conversions_internal.hpp" - -namespace { - -using farsounder::ArrayDataOrder; -using farsounder::ArrayDataType; -using farsounder::FieldOfView; -using farsounder::GridMode; -using farsounder::SystemType; -using farsounder::detail::convert_array_order; -using farsounder::detail::convert_array_type; -using farsounder::detail::convert_bin; -using farsounder::detail::convert_fov; -using farsounder::detail::convert_hydrophone_data; -using farsounder::detail::convert_processor_settings; -using farsounder::detail::convert_system_type; -using farsounder::detail::convert_target_data; -using farsounder::detail::convert_timestamp; -using farsounder::detail::convert_vessel_info; - -TEST(SubscriberConversions, TimestampConversion) { - proto::time::Time time; - time.set_year(1970); - time.set_month(1); - time.set_day(1); - time.set_hour(0); - time.set_minute(0); - time.set_second(2); - time.set_millisecond(250); - - const auto converted = convert_timestamp(time); - EXPECT_DOUBLE_EQ(converted.seconds_since_epoch, 2.25); -} - -TEST(SubscriberConversions, EnumMappings) { - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS500), - SystemType::kFS500); - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS1000), - SystemType::kFS1000); - EXPECT_EQ(convert_system_type(proto::nav_api::ProcessorSettings::kFS350), - SystemType::kFS350); - - EXPECT_EQ(convert_fov(proto::nav_api::k120d100m), FieldOfView::k120d100m); - EXPECT_EQ(convert_fov(proto::nav_api::kStandby), FieldOfView::kStandby); - - EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT32), - ArrayDataType::kFloat32); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT16), - ArrayDataType::kUInt16); - - EXPECT_EQ(convert_array_order(proto::array::ArrayData::ROW_MAJOR), - ArrayDataOrder::kRowMajor); - EXPECT_EQ(convert_array_order(proto::array::ArrayData::COLUMN_MAJOR), - ArrayDataOrder::kColumnMajor); -} - -TEST(SubscriberConversions, EnumMappingsUseDefaultsForUnknownValues) { - const auto unknown_system = - static_cast(999); - const auto unknown_fov = static_cast(999); - const auto unknown_type = static_cast(999); - const auto unknown_order = static_cast(999); - - EXPECT_EQ(convert_system_type(unknown_system), SystemType::kFS500); - EXPECT_EQ(convert_fov(unknown_fov), FieldOfView::k90d500m); - EXPECT_EQ(convert_array_type(unknown_type), ArrayDataType::kByte); - EXPECT_EQ(convert_array_order(unknown_order), ArrayDataOrder::kRowMajor); -} - -TEST(SubscriberConversions, ArrayTypeMappingsCoverAllValues) { - EXPECT_EQ(convert_array_type(proto::array::ArrayData::BYTE), - ArrayDataType::kByte); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT16), - ArrayDataType::kInt16); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT16), - ArrayDataType::kUInt16); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT32), - ArrayDataType::kInt32); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT32), - ArrayDataType::kUInt32); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::INT64), - ArrayDataType::kInt64); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::UINT64), - ArrayDataType::kUInt64); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT32), - ArrayDataType::kFloat32); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::FLOAT64), - ArrayDataType::kFloat64); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::COMPLEX64), - ArrayDataType::kComplex64); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::COMPLEX128), - ArrayDataType::kComplex128); - EXPECT_EQ(convert_array_type(proto::array::ArrayData::BOOL), - ArrayDataType::kBool); -} - -TEST(SubscriberConversions, BinConversionCopiesFields) { - proto::nav_api::Bin bin; - bin.set_hor_index(1); - bin.set_ver_index(2); - bin.set_range_index(3); - bin.set_cross_range(1.5f); - bin.set_down_range(2.5f); - bin.set_depth(3.5f); - bin.set_strength(4.5f); - - const auto converted = convert_bin(bin); - EXPECT_EQ(converted.hor_index, 1); - EXPECT_EQ(converted.ver_index, 2); - EXPECT_EQ(converted.range_index, 3); - EXPECT_FLOAT_EQ(converted.cross_range, 1.5f); - EXPECT_FLOAT_EQ(converted.down_range, 2.5f); - EXPECT_FLOAT_EQ(converted.depth, 3.5f); - EXPECT_FLOAT_EQ(converted.strength, 4.5f); -} - -TEST(SubscriberConversions, HydrophoneDataConversionCopiesArrayData) { - proto::nav_api::HydrophoneData proto_data; - proto_data.set_serial("serial-1"); - proto_data.set_transmit_id("tx-1"); - proto_data.set_num_hor_phones(3); - proto_data.set_num_ver_phones(4); - - auto* time = proto_data.mutable_time(); - time->set_year(1970); - time->set_month(1); - time->set_day(1); - time->set_hour(0); - time->set_minute(0); - time->set_second(0); - time->set_millisecond(0); - - auto* array = proto_data.mutable_raw_timeseries(); - array->add_dims(12); - array->add_dims(256); - array->set_type(proto::array::ArrayData::INT16); - array->set_order(proto::array::ArrayData::COLUMN_MAJOR); - array->set_data(std::string("\x01\x02\x03\x04", 4)); - - const auto converted = convert_hydrophone_data(proto_data); - EXPECT_EQ(converted.serial, "serial-1"); - EXPECT_EQ(converted.transmit_id, "tx-1"); - EXPECT_EQ(converted.num_hor_phones, 3); - EXPECT_EQ(converted.num_ver_phones, 4); - ASSERT_EQ(converted.dims.size(), 2u); - EXPECT_EQ(converted.dims[0], 12); - EXPECT_EQ(converted.dims[1], 256); - EXPECT_EQ(converted.type, ArrayDataType::kInt16); - EXPECT_EQ(converted.order, ArrayDataOrder::kColumnMajor); - EXPECT_EQ(converted.raw_timeseries, std::string("\x01\x02\x03\x04", 4)); -} - -TEST(SubscriberConversions, HydrophoneDataConversionHandlesMissingRawTimeseries) { - proto::nav_api::HydrophoneData proto_data; - proto_data.set_serial("serial-missing-array"); - proto_data.set_transmit_id("tx-missing-array"); - proto_data.set_num_hor_phones(1); - proto_data.set_num_ver_phones(2); - - const auto converted = convert_hydrophone_data(proto_data); - EXPECT_EQ(converted.serial, "serial-missing-array"); - EXPECT_EQ(converted.transmit_id, "tx-missing-array"); - EXPECT_TRUE(converted.dims.empty()); - EXPECT_EQ(converted.type, ArrayDataType::kByte); - EXPECT_EQ(converted.order, ArrayDataOrder::kRowMajor); - EXPECT_TRUE(converted.raw_timeseries.empty()); -} - -TEST(SubscriberConversions, TargetDataConversionCopiesFields) { - proto::nav_api::TargetData proto_data; - proto_data.set_serial("serial-2"); - proto_data.set_max_depth(55.0); - proto_data.set_max_range_index(7); - - auto* time = proto_data.mutable_time(); - time->set_year(1970); - time->set_month(1); - time->set_day(1); - time->set_hour(0); - time->set_minute(0); - time->set_second(1); - time->set_millisecond(0); - - auto* heading = proto_data.mutable_heading(); - heading->set_heading(12.5); - - auto* position = proto_data.mutable_position(); - position->set_lat(42.0); - position->set_lon(-70.0); - - auto* bottom = proto_data.add_bottom(); - bottom->set_hor_index(1); - bottom->set_ver_index(2); - bottom->set_range_index(3); - bottom->set_cross_range(1.0f); - bottom->set_down_range(2.0f); - bottom->set_depth(3.0f); - bottom->set_strength(4.0f); - - auto* group = proto_data.add_groups(); - auto* group_bin = group->add_bins(); - group_bin->set_hor_index(4); - group_bin->set_ver_index(5); - group_bin->set_range_index(6); - group_bin->set_cross_range(5.0f); - group_bin->set_down_range(6.0f); - group_bin->set_depth(7.0f); - group_bin->set_strength(8.0f); - - auto* grid = proto_data.mutable_grid_description(); - grid->set_mode(proto::grid_description::GridDescription::kFixed); - grid->add_hor_angles(1.0); - grid->add_hor_angles(-1.0); - grid->add_ver_angles(2.0); - grid->add_ver_angles(-2.0); - grid->set_max_range(123.0); - - const auto converted = convert_target_data(proto_data); - EXPECT_EQ(converted.serial, "serial-2"); - ASSERT_TRUE(converted.heading.has_value()); - EXPECT_DOUBLE_EQ(converted.heading->degrees, 12.5); - ASSERT_TRUE(converted.position.has_value()); - EXPECT_DOUBLE_EQ(converted.position->latitude_degrees, 42.0); - EXPECT_DOUBLE_EQ(converted.position->longitude_degrees, -70.0); - ASSERT_EQ(converted.bottom.size(), 1u); - EXPECT_EQ(converted.bottom[0].hor_index, 1); - ASSERT_EQ(converted.groups.size(), 1u); - ASSERT_EQ(converted.groups[0].bins.size(), 1u); - EXPECT_EQ(converted.groups[0].bins[0].hor_index, 4); - ASSERT_TRUE(converted.grid_description.has_value()); - EXPECT_EQ(converted.grid_description->mode, GridMode::kFixed); - ASSERT_EQ(converted.grid_description->hor_angles.size(), 2u); - EXPECT_DOUBLE_EQ(converted.grid_description->hor_angles[0], 1.0); - EXPECT_DOUBLE_EQ(converted.grid_description->hor_angles[1], -1.0); - ASSERT_EQ(converted.grid_description->ver_angles.size(), 2u); - EXPECT_DOUBLE_EQ(converted.grid_description->ver_angles[0], 2.0); - EXPECT_DOUBLE_EQ(converted.grid_description->ver_angles[1], -2.0); - EXPECT_DOUBLE_EQ(converted.grid_description->max_range, 123.0); - EXPECT_DOUBLE_EQ(converted.max_depth, 55.0); - EXPECT_EQ(converted.max_range_index, 7); -} - -TEST(SubscriberConversions, TargetDataConversionHandlesMissingOptionalFields) { - proto::nav_api::TargetData proto_data; - proto_data.set_serial("serial-minimal"); - proto_data.set_max_depth(10.0); - proto_data.set_max_range_index(3); - - const auto converted = convert_target_data(proto_data); - EXPECT_EQ(converted.serial, "serial-minimal"); - EXPECT_FALSE(converted.heading.has_value()); - EXPECT_FALSE(converted.position.has_value()); - EXPECT_TRUE(converted.bottom.empty()); - EXPECT_TRUE(converted.groups.empty()); - EXPECT_FALSE(converted.grid_description.has_value()); - EXPECT_DOUBLE_EQ(converted.max_depth, 10.0); - EXPECT_EQ(converted.max_range_index, 3); -} - -TEST(SubscriberConversions, ProcessorSettingsConversionCopiesFields) { - proto::nav_api::ProcessorSettings proto_settings; - proto_settings.set_min_inwater_squelch(1.0f); - proto_settings.set_max_inwater_squelch(2.0f); - proto_settings.set_inwater_squelch(1.5f); - proto_settings.set_squelchless_inwater_detector(true); - proto_settings.set_detect_bottom(false); - proto_settings.set_system_type(proto::nav_api::ProcessorSettings::kFS500); - proto_settings.set_fov(proto::nav_api::k120d200m); - - auto* time = proto_settings.mutable_time(); - time->set_year(1970); - time->set_month(1); - time->set_day(1); - time->set_hour(0); - time->set_minute(0); - time->set_second(0); - time->set_millisecond(0); - - const auto converted = convert_processor_settings(proto_settings); - EXPECT_FLOAT_EQ(converted.min_inwater_squelch, 1.0f); - EXPECT_FLOAT_EQ(converted.max_inwater_squelch, 2.0f); - EXPECT_FLOAT_EQ(converted.inwater_squelch, 1.5f); - EXPECT_TRUE(converted.squelchless_inwater_detector); - EXPECT_FALSE(converted.detect_bottom); - EXPECT_EQ(converted.system_type, SystemType::kFS500); - EXPECT_EQ(converted.fov, FieldOfView::k120d200m); -} - -TEST(SubscriberConversions, VesselInfoConversionCopiesFields) { - proto::nav_api::VesselInfo proto_info; - proto_info.set_draft(2.5f); - proto_info.set_keel_offset(-0.5f); - - const auto converted = convert_vessel_info(proto_info); - EXPECT_FLOAT_EQ(converted.draft, 2.5f); - EXPECT_FLOAT_EQ(converted.keel_offset, -0.5f); -} - -} // namespace From 58d8ff89b8f728defa0813ec8344d4bac5ebc195 Mon Sep 17 00:00:00 2001 From: Heath Henley Date: Fri, 13 Feb 2026 11:41:31 -0500 Subject: [PATCH 3/3] Lint: too long --- examples/basic_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/basic_client.cpp b/examples/basic_client.cpp index b55ec47..9875108 100644 --- a/examples/basic_client.cpp +++ b/examples/basic_client.cpp @@ -14,7 +14,7 @@ namespace { const std::string kDefaultHost = "127.0.0.1"; const std::string kHelpFlag = "--help"; // notes on running the client in WSL: -// https://learn.microsoft.com/en-us/windows/wsl/networking#identify-ip-address (scenario 2) +// https://learn.microsoft.com/en-us/windows/wsl/networking#identify-ip-address std::atomic g_running{true}; void handle_sigint(int) {