diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c9b082..e2a3ad5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: name: Format and static analysis runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Check formatting @@ -49,8 +49,8 @@ jobs: - library: static build_shared: "OFF" steps: - - uses: actions/checkout@v7 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure @@ -73,10 +73,10 @@ jobs: name: CMake 3.16 compatibility runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Verify pinned CMake version @@ -88,10 +88,10 @@ jobs: name: GCC 10 compatibility runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 - name: Build and test with GCC 10 run: bash test/gcc10_compatibility_test.sh @@ -99,8 +99,8 @@ jobs: name: ASan and UBSan runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure @@ -123,8 +123,8 @@ jobs: name: ARM64 Release shared runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@v7 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure @@ -144,7 +144,7 @@ jobs: name: macOS Release static runs-on: macos-15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install dependencies run: brew install curl googletest ninja - name: Configure @@ -172,7 +172,7 @@ jobs: name: Windows Release static runs-on: windows-2025 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install dependencies shell: pwsh run: >- diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e4b753e..7ca0bae 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,12 +19,12 @@ jobs: analyze: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 with: languages: c-cpp build-mode: manual @@ -38,4 +38,4 @@ jobs: - name: Build run: pixi run cmake --build build/codeql - name: Analyze - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ae80687..1003201 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,14 +17,14 @@ jobs: coverage: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Generate coverage run: pixi run coverage - name: Upload coverage - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: files: coverage.xml disable_search: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae3b331..46b6ff5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,10 +16,10 @@ jobs: release: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Verify release version diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1462c..c0af5b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project are documented in this file. +## Unreleased + +- Reject durations and deadlines outside the steady-clock/HTTP timer range before conversion; document synchronous resolver shutdown limits. + +### Fixed + +- Reject calibration divisors that cannot represent the full signed raw-count range, + consistently for HTTP discovery and manual overrides. Never deliver non-finite axes + as successful samples; numeric conversion failures use the sensor-configuration fault. + ## 0.3.3 - 2026-08-01 ### Fixed diff --git a/README.md b/README.md index 7708520..a8d1c00 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,9 @@ security issues through [SECURITY.md](SECURITY.md). ## License This project is licensed under the [Apache License 2.0](LICENSE). + +### Timing limits (unreleased candidate) + +Configuration durations must be finite, positive and representable by the steady clock; HTTP timers must also fit libcurl milliseconds. `wait_for_first_sample` accepts a zero-duration poll and rejects negative, non-finite or nonrepresentable waits. Deadlines are checked before integer conversion or addition. + +UDP hostname resolution uses synchronous system `getaddrinfo`. Its DNS/NSS latency cannot be cancelled by `stop()`, and libcurl discovery cancellation also depends on the resolver backend. Receive/configuration timeouts do not provide a hard bound for every hostname resolution or callback. Use a numeric device address on the isolated sensor network when a predictable shutdown bound is required; keep callbacks bounded. The SDK does not create detached resolver threads or promise a hard realtime stop. diff --git a/src/detail/client_impl.cpp b/src/detail/client_impl.cpp index 5ebc20c..c8f7167 100644 --- a/src/detail/client_impl.cpp +++ b/src/detail/client_impl.cpp @@ -1,7 +1,9 @@ #include "detail/client_impl.hpp" +#include "detail/time.hpp" #include #include +#include #include #include #include @@ -301,12 +303,13 @@ void Client::Impl::bias() { } bool Client::Impl::wait_for_first_sample(const std::chrono::duration timeout) { + const auto deadline = detail::checked_deadline(std::chrono::steady_clock::now(), timeout); std::unique_lock data_lock(data_mutex_); const auto captured_generation = generation_; if (captured_generation == 0) { return false; } - first_sample_cv_.wait_for(data_lock, timeout, [this, captured_generation] { + first_sample_cv_.wait_until(data_lock, deadline, [this, captured_generation] { return generation_ != captured_generation || delivered_generation_ == captured_generation || stopping_ || faulted(); }); @@ -406,7 +409,9 @@ void Client::Impl::run() noexcept { } std::unique_lock data_lock(data_mutex_); - first_sample_cv_.wait_for(data_lock, backoff, [this] { return stopping_.load(); }); + first_sample_cv_.wait_until(data_lock, + detail::checked_deadline(detail::SteadyClock::now(), backoff), + [this] { return stopping_.load(); }); data_lock.unlock(); backoff = std::min(backoff * 2.0, config_.reconnect_max_delay); } @@ -461,7 +466,7 @@ Client::Impl::SessionOutcome Client::Impl::receive_session() { } const auto timeout = config_.receive_timeout; - auto deadline = std::chrono::steady_clock::now() + timeout; + auto deadline = detail::checked_deadline(std::chrono::steady_clock::now(), timeout); std::array buffer{}; while (!stopping_) { const auto now = std::chrono::steady_clock::now(); @@ -511,7 +516,7 @@ Client::Impl::SessionOutcome Client::Impl::receive_session() { consecutive_malformed_ = 0; } received_valid_record = true; - deadline = received_at + timeout; + deadline = detail::checked_deadline(received_at, timeout); if (const auto outcome = handle_record(record, received_at)) { return {*outcome, {}, received_valid_record}; } @@ -631,6 +636,11 @@ Client::Impl::handle_record(const detail::RawRecord &record, sample.torque = {record.tx / calibration.counts_per_torque_unit, record.ty / calibration.counts_per_torque_unit, record.tz / calibration.counts_per_torque_unit}; + const auto finite = [](double value) { return std::isfinite(value); }; + if (!std::all_of(sample.force.begin(), sample.force.end(), finite) || + !std::all_of(sample.torque.begin(), sample.torque.end(), finite)) { + return SessionResult::SensorConfiguration; + } sample.force_unit = calibration.force_unit; sample.torque_unit = calibration.torque_unit; sample.configuration_revision = configuration.revision; diff --git a/src/detail/time.hpp b/src/detail/time.hpp new file mode 100644 index 0000000..b5c1aae --- /dev/null +++ b/src/detail/time.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +namespace netft::detail { +using SteadyClock = std::chrono::steady_clock; + +inline SteadyClock::duration checked_duration(std::chrono::duration duration) { + const auto ticks = static_cast(duration.count()) * SteadyClock::period::den / + SteadyClock::period::num; + if (!std::isfinite(ticks) || ticks < 0 || + ticks >= static_cast(SteadyClock::duration::max().count())) { + throw std::invalid_argument("duration is outside the supported steady-clock range"); + } + return SteadyClock::duration{static_cast(ticks)}; +} + +inline SteadyClock::time_point checked_deadline(SteadyClock::time_point origin, + std::chrono::duration duration) { + const auto ticks = checked_duration(duration); + if (origin > SteadyClock::time_point::max() - ticks) { + throw std::invalid_argument("deadline is outside the supported steady-clock range"); + } + return origin + ticks; +} +} // namespace netft::detail diff --git a/src/detail/udp_transport_posix.cpp b/src/detail/udp_transport_posix.cpp index 5af2b7f..923f7f8 100644 --- a/src/detail/udp_transport_posix.cpp +++ b/src/detail/udp_transport_posix.cpp @@ -1,3 +1,4 @@ +#include "detail/time.hpp" #include "detail/udp_transport.hpp" #include @@ -106,7 +107,7 @@ std::size_t UdpTransport::receive(std::uint8_t *data, const std::size_t capacity } pollfd descriptor{socket, POLLIN, 0}; - const auto deadline = std::chrono::steady_clock::now() + timeout; + const auto deadline = checked_deadline(std::chrono::steady_clock::now(), timeout); auto remaining = timeout; int poll_result{}; if (wait_started_hook != nullptr) { diff --git a/src/detail/udp_transport_windows.cpp b/src/detail/udp_transport_windows.cpp index 1330f54..7209db2 100644 --- a/src/detail/udp_transport_windows.cpp +++ b/src/detail/udp_transport_windows.cpp @@ -1,3 +1,4 @@ +#include "detail/time.hpp" #include "detail/udp_transport.hpp" #ifndef NOMINMAX @@ -145,8 +146,9 @@ std::size_t UdpTransport::receive(std::uint8_t *data, const std::size_t capacity if (wait_started_hook != nullptr) { wait_started_hook(wait_started_context); } - const auto bounded_timeout = std::max(timeout, std::chrono::duration::zero()); - const auto deadline = std::chrono::steady_clock::now() + bounded_timeout; + static_cast(checked_duration(timeout)); + const auto bounded_timeout = timeout; + const auto deadline = checked_deadline(std::chrono::steady_clock::now(), bounded_timeout); constexpr int kShutdownCheckIntervalMilliseconds = 50; constexpr auto kShutdownCheckInterval = std::chrono::milliseconds{kShutdownCheckIntervalMilliseconds}; diff --git a/src/detail/xml_config.cpp b/src/detail/xml_config.cpp index 3e6bb03..dd40fef 100644 --- a/src/detail/xml_config.cpp +++ b/src/detail/xml_config.cpp @@ -278,6 +278,11 @@ SensorConfiguration parse_sensor_configuration(std::string_view xml) { configuration.calibration.counts_per_torque_unit = parse_positive_count(fields[2], "cfgcpt"); configuration.calibration.force_unit = parse_force_unit(fields[3]); configuration.calibration.torque_unit = parse_torque_unit(fields[4]); + try { + validate(configuration.calibration); + } catch (const std::invalid_argument &error) { + throw DiscoveryError(error.what()); + } configuration.source = CalibrationSource::Sensor; configuration.revision = 1; return configuration; diff --git a/src/types.cpp b/src/types.cpp index 3f2824b..5698a53 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -1,8 +1,10 @@ #include "netft/types.hpp" +#include "detail/time.hpp" #include #include #include +#include #include namespace netft { @@ -20,17 +22,43 @@ void require_positive_finite(std::string_view name, double value) { } } +void require_safe_timeout(std::string_view name, double value, bool http = false) { + require_positive_finite(name, value); + if (detail::checked_duration(std::chrono::duration{value}) <= + detail::SteadyClock::duration::zero()) { + throw std::invalid_argument(std::string{name} + " is below the steady-clock resolution"); + } + static_cast( + detail::checked_deadline(detail::SteadyClock::now(), std::chrono::duration{value})); + if (http && std::ceil(static_cast(value) * 1000.0L) >= + static_cast(std::numeric_limits::max())) { + throw std::invalid_argument(std::string{name} + " is outside the HTTP timer range"); + } +} + void require_port(std::string_view name, int value) { if (value < 1 || value > 65535) { throw std::invalid_argument(std::string{name} + " must be in the range 1..65535"); } } +void require_safe_count(std::string_view name, double value) { + require_positive_finite(name, value); + // INT32_MIN has the largest magnitude. Round the minimum divisor up so + // every raw count remains representable, including under directed rounding. + constexpr double magnitude = -static_cast(std::numeric_limits::min()); + constexpr double minimum = (magnitude / std::numeric_limits::max()) * + (1.0 + std::numeric_limits::epsilon()); + if (value < minimum) { + throw std::invalid_argument(std::string{name} + " cannot represent the full raw count range"); + } +} + } // namespace void validate(const Calibration &calibration) { - require_positive_finite("counts_per_force_unit", calibration.counts_per_force_unit); - require_positive_finite("counts_per_torque_unit", calibration.counts_per_torque_unit); + require_safe_count("counts_per_force_unit", calibration.counts_per_force_unit); + require_safe_count("counts_per_torque_unit", calibration.counts_per_torque_unit); if (calibration.force_unit == ForceUnit::Unknown || calibration.torque_unit == TorqueUnit::Unknown) { throw std::invalid_argument("calibration units must be known"); @@ -43,12 +71,12 @@ void validate(const Config &config) { } require_port("rdt_port", config.rdt_port); require_port("http_port", config.http_port); - require_positive_finite("receive_timeout", config.receive_timeout.count()); - require_positive_finite("configuration_connect_timeout", - config.configuration_connect_timeout.count()); - require_positive_finite("configuration_timeout", config.configuration_timeout.count()); - require_positive_finite("reconnect_initial_delay", config.reconnect_initial_delay.count()); - require_positive_finite("reconnect_max_delay", config.reconnect_max_delay.count()); + require_safe_timeout("receive_timeout", config.receive_timeout.count()); + require_safe_timeout("configuration_connect_timeout", + config.configuration_connect_timeout.count(), true); + require_safe_timeout("configuration_timeout", config.configuration_timeout.count(), true); + require_safe_timeout("reconnect_initial_delay", config.reconnect_initial_delay.count()); + require_safe_timeout("reconnect_max_delay", config.reconnect_max_delay.count()); if (config.reconnect_max_delay < config.reconnect_initial_delay) { throw std::invalid_argument("reconnect_max_delay must not be below reconnect_initial_delay"); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d19098b..ca11cc6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -86,6 +86,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") ${CMAKE_CURRENT_SOURCE_DIR}/../src ) target_link_options(netft_posix_transport PRIVATE + "-Wl,--wrap=getaddrinfo" "-Wl,--wrap=poll" "-Wl,--wrap=recv" "-Wl,--wrap=send" diff --git a/test/test_client_lifecycle.cpp b/test/test_client_lifecycle.cpp index 208b643..513993e 100644 --- a/test/test_client_lifecycle.cpp +++ b/test/test_client_lifecycle.cpp @@ -755,3 +755,12 @@ TEST(ClientLifecycle, DestructionFromCallbackIsDeferredUntilWorkerExit) { } } // namespace + +TEST(ClientLifecycle, RejectsNonRepresentableFirstSampleWaitBeforeStarting) { + netft::Client client{netft::Config{}}; + EXPECT_THROW(client.wait_for_first_sample(std::chrono::duration{1e300}), + std::invalid_argument); + EXPECT_THROW(client.wait_for_first_sample(std::chrono::duration{-1}), + std::invalid_argument); + EXPECT_FALSE(client.wait_for_first_sample(std::chrono::duration{0})); +} diff --git a/test/test_client_recovery.cpp b/test/test_client_recovery.cpp index 6e3ef93..5ee0f2e 100644 --- a/test/test_client_recovery.cpp +++ b/test/test_client_recovery.cpp @@ -519,6 +519,8 @@ TEST(ClientRecovery, InvalidReplacementConfigurationCannotStartNewRdtSession) { "", R"xml(Fake1000000 1000Nmystery)xml", + R"xml(Fake1e-300 +1000NNm)xml", }) { netft::test::FakeSensor sensor{200.0}; auto config = config_for(sensor); diff --git a/test/test_client_stream.cpp b/test/test_client_stream.cpp index e7d7de7..ab146ef 100644 --- a/test/test_client_stream.cpp +++ b/test/test_client_stream.cpp @@ -4,8 +4,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -160,6 +162,31 @@ TEST(ClientStream, CompleteCalibrationOverrideSkipsHttpDiscovery) { client.stop(); } +TEST(ClientStream, DeliversFiniteAxesForExtremeRawCounts) { + netft::test::FakeSensor sensor; + sensor.pause(); + const auto low = std::numeric_limits::min(); + const auto high = std::numeric_limits::max(); + const std::array axes{low, high, low, high, low, high}; + sensor.queue_record(1, 0, 100, axes); + auto config = config_for(sensor); + config.calibration_override = + netft::Calibration{1e-298, 1e-298, netft::ForceUnit::Newton, netft::TorqueUnit::NewtonMeter}; + netft::Client client{config}; + client.start([&](const netft::Sample &) { sensor.pause(); }); + ASSERT_TRUE(sensor.wait_for_command(netft::detail::Command::StartRealtime)); + sensor.resume(); + ASSERT_TRUE(client.wait_for_first_sample(500ms)); + const auto sample = client.latest_sample(); + ASSERT_TRUE(sample); + EXPECT_EQ(sample->raw_wrench, axes); + for (const auto value : sample->force) + EXPECT_TRUE(std::isfinite(value)); + for (const auto value : sample->torque) + EXPECT_TRUE(std::isfinite(value)); + client.stop(); +} + TEST(ClientStream, StopIsIdempotentAndSendsOneEffectiveCommand) { netft::test::FakeSensor sensor; netft::Client client{config_for(sensor)}; diff --git a/test/test_discovery.cpp b/test/test_discovery.cpp index 2b96655..16d5799 100644 --- a/test/test_discovery.cpp +++ b/test/test_discovery.cpp @@ -236,7 +236,8 @@ INSTANTIATE_TEST_SUITE_P( InvalidCountCase{"cfgcpt", "not-a-number"}, InvalidCountCase{"cfgcpt", "10remaining"}, InvalidCountCase{"cfgcpt", "0"}, InvalidCountCase{"cfgcpt", "-1"}, InvalidCountCase{"cfgcpt", "NaN"}, - InvalidCountCase{"cfgcpt", "infinity"})); + InvalidCountCase{"cfgcpt", "infinity"}, InvalidCountCase{"cfgcpf", "1e-300"}, + InvalidCountCase{"cfgcpt", "1e-300"})); TEST(XmlConfiguration, ParsesCountsIndependentlyOfTheGlobalLocale) { const GlobalLocaleGuard locale_guard{std::locale{std::locale::classic(), new CommaDecimalPoint}}; diff --git a/test/test_posix_transport.cpp b/test/test_posix_transport.cpp index 5f47b18..50cff7a 100644 --- a/test/test_posix_transport.cpp +++ b/test/test_posix_transport.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +35,8 @@ enum class ReceiveBehavior { Payload, Interrupted, Error }; constexpr int kInterruptCount = 3; std::vector poll_timeouts; int poll_calls{}; +std::promise *resolver_entered{}; +std::shared_future *resolver_release{}; PollBehavior poll_behavior{PollBehavior::RepeatedEintrThenTimeout}; SendBehavior send_behavior{SendBehavior::Complete}; ReceiveBehavior receive_behavior{ReceiveBehavior::Payload}; @@ -47,6 +51,16 @@ void reset_behaviors() { } // namespace +extern "C" int __real_getaddrinfo(const char *, const char *, const addrinfo *, addrinfo **); +extern "C" int __wrap_getaddrinfo(const char *host, const char *service, const addrinfo *hints, + addrinfo **result) { + if (resolver_entered && resolver_release) { + resolver_entered->set_value(); + resolver_release->wait(); + } + return __real_getaddrinfo(host, service, hints, result); +} + extern "C" int __wrap_poll(pollfd *descriptors, const nfds_t count, const int timeout) { poll_timeouts.push_back(timeout); switch (poll_behavior) { @@ -233,3 +247,23 @@ TEST(PosixTransportTest, LargeTimeoutSaturatesPollMilliseconds) { ASSERT_EQ(poll_timeouts.size(), 1U); EXPECT_EQ(poll_timeouts.front(), std::numeric_limits::max()); } + +TEST(PosixTransportTest, ShutdownCannotCancelSystemResolution) { + netft::detail::UdpTransport transport; + std::promise entered, release; + auto entered_future = entered.get_future(); + auto release_future = release.get_future().share(); + resolver_entered = &entered; + resolver_release = &release_future; + auto connection = std::async(std::launch::async, [&] { transport.connect("127.0.0.1", 49152); }); + const auto entered_status = entered_future.wait_for(1s); + EXPECT_EQ(entered_status, std::future_status::ready); + if (entered_status == std::future_status::ready) { + transport.shutdown(); + EXPECT_EQ(connection.wait_for(10ms), std::future_status::timeout); + } + release.set_value(); + EXPECT_NO_THROW(connection.get()); + resolver_entered = nullptr; + resolver_release = nullptr; +} diff --git a/test/test_types.cpp b/test/test_types.cpp index c195fcd..234a739 100644 --- a/test/test_types.cpp +++ b/test/test_types.cpp @@ -10,6 +10,18 @@ #include "netft/types.hpp" +TEST(Calibration, RejectsScalesThatOverflowRawRange) { + netft::Calibration calibration{1.0, 1.0, netft::ForceUnit::Newton, + netft::TorqueUnit::NewtonMeter}; + calibration.counts_per_force_unit = 1e-300; + EXPECT_THROW(netft::validate(calibration), std::invalid_argument); + calibration.counts_per_force_unit = 1.0; + calibration.counts_per_torque_unit = 1e-300; + EXPECT_THROW(netft::validate(calibration), std::invalid_argument); + calibration.counts_per_torque_unit = 1e-298; + EXPECT_NO_THROW(netft::validate(calibration)); +} + TEST(Config, DefaultsToAutomaticSensorDiscovery) { const netft::Config config; EXPECT_EQ(config.sensor_host, "192.168.1.1"); @@ -323,3 +335,29 @@ TEST(States, ConvertsStateAndFaultCodeStrings) { EXPECT_EQ(netft::to_string(code), spelling); } } + +TEST(Config, RejectsFiniteDurationsOutsideClockRange) { + for (int field = 0; field < 5; ++field) { + netft::Config config; + const std::chrono::duration extreme{1e300}; + switch (field) { + case 0: + config.receive_timeout = extreme; + break; + case 1: + config.configuration_connect_timeout = extreme; + break; + case 2: + config.configuration_timeout = extreme; + break; + case 3: + config.reconnect_initial_delay = extreme; + config.reconnect_max_delay = extreme; + break; + case 4: + config.reconnect_max_delay = extreme; + break; + } + EXPECT_THROW(netft::validate(config), std::invalid_argument); + } +}