Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
coverage:
range: 85..100
range: "85...100"
round: down
precision: 2
status:
project:
default:
target: auto
target: 85%
threshold: 0%
patch:
default:
Expand Down
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ hardware-bias-test = "NETFT_ALLOW_BIAS=1 bash test/hardware_test.sh"
format = "git ls-files -z '*.cpp' '*.hpp' | xargs -0 clang-format -i"
format-check = "git ls-files -z '*.cpp' '*.hpp' | xargs -0 clang-format --dry-run --Werror"
tidy = { cmd = "cmake -S . -B build/tidy -G Ninja -DBUILD_TESTING=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON && clang-tidy -p build/tidy --config-file=.clang-tidy $(git ls-files 'src/*.cpp' 'app/*.cpp')", depends-on = [] }
coverage = "cmake -S . -B build/coverage -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=ON -DCMAKE_CXX_FLAGS='--coverage -O0 -g' -DCMAKE_EXE_LINKER_FLAGS=--coverage -DCMAKE_SHARED_LINKER_FLAGS=--coverage && cmake --build build/coverage && ctest --test-dir build/coverage --output-on-failure && gcovr --root . --filter 'src/' --filter 'app/' --exclude 'test/' --exclude-unreachable-branches --exclude-throw-branches --xml-pretty --output coverage.xml --fail-under-line 85 build/coverage"
coverage = "cmake -S . -B build/coverage -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=ON -DCMAKE_CXX_FLAGS='--coverage -O0 -g' -DCMAKE_EXE_LINKER_FLAGS=--coverage -DCMAKE_SHARED_LINKER_FLAGS=--coverage && cmake --build build/coverage && ctest --test-dir build/coverage --output-on-failure && gcovr --root . --filter 'src/' --filter 'app/' --exclude 'test/' --exclude-unreachable-branches --exclude-throw-branches --xml-pretty --output coverage.xml --fail-under-line 85 --fail-under-branch 85 build/coverage"

[feature.cmake-316.dependencies]
cmake = "3.16.*"
Expand Down
114 changes: 114 additions & 0 deletions test/test_cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ TEST(CliParser, RejectsMissingUnknownAndInvalidValues) {
EXPECT_THROW(parse({"monitor", "--duration", "0"}), netft::cli::UsageError);
}

TEST(CliParser, RejectsEveryUnsupportedBoundaryWithoutInspectingCopy) {
const std::vector<std::vector<std::string>> cases{
{"monitor", "--host"},
{"monitor", "--duration", "not-a-number"},
{"monitor", "--rdt-port", "not-a-port"},
{"monitor", "--output", ""},
{"monitor", "--force-unit", "unsupported"},
{"monitor", "--torque-unit", "unsupported"},
{"monitor", "--unknown"},
{"info", "--duration", "1"},
{"bias", "--duration", "1"},
{"monitor", "--host", " "},
{"info", "--counts-per-force-unit", "1", "--counts-per-torque-unit", "1", "--force-unit", "N",
"--torque-unit", "N-mm"},
};

for (const auto &arguments : cases) {
EXPECT_THROW(netft::cli::parse_options(arguments), netft::cli::UsageError);
}
}

TEST(CliParser, RequiresCompleteManualCalibration) {
EXPECT_THROW(parse({"monitor", "--counts-per-force-unit", "1000000"}), netft::cli::UsageError);

Expand Down Expand Up @@ -233,6 +254,42 @@ TEST(CliInfo, PreservesValidUtf8WhileEscapingJsonControls) {
std::string::npos);
}

TEST(CliInfo, SerializesEveryJsonControlBranch) {
netft::test::FakeSensor sensor;
std::string product{"controls"};
product.push_back('"');
product.push_back('\\');
product.push_back('\b');
product.push_back('\f');
product.push_back('\n');
product.push_back('\r');
product.push_back('\t');
product.push_back(static_cast<char>(0x01));
sensor.set_xml_configuration("<netft><prodname>" + product +
"</prodname><cfgcpf>1000000</cfgcpf><cfgcpt>1000000</cfgcpt>"
"<scfgfu>N</scfgfu><scfgtu>Nm</scfgtu></netft>");
auto options = options_for(netft::cli::Command::Info, sensor);
std::ostringstream output;
std::ostringstream errors;

ASSERT_EQ(netft::cli::run(options, output, errors), 0);
EXPECT_NE(output.str().find(R"json("product":"controls\"\\\b\f\n\r\t\u0001")json"),
std::string::npos);
EXPECT_TRUE(errors.str().empty());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

TEST(CliInfo, HumanOutputIsNonempty) {
netft::test::FakeSensor sensor;
auto options = options_for(netft::cli::Command::Info, sensor);
options.json = false;
std::ostringstream output;
std::ostringstream errors;

EXPECT_EQ(netft::cli::run(options, output, errors), 0);
EXPECT_FALSE(output.str().empty());
EXPECT_TRUE(errors.str().empty());
}

TEST(CliMonitor, EmitsStableJsonAndUsesDeliveredCountAsSampleCount) {
netft::test::FakeSensor sensor;
auto options = options_for(netft::cli::Command::Monitor, sensor);
Expand Down Expand Up @@ -362,6 +419,21 @@ TEST(CliMonitor, ReturnsTwoWhenNoSampleArrives) {
EXPECT_FALSE(errors.str().empty());
}

TEST(CliMonitor, ReturnsTwoAfterAStoredClientTimeout) {
netft::test::FakeSensor sensor;
sensor.pause();
auto options = options_for(netft::cli::Command::Monitor, sensor);
options.config.receive_timeout = 20ms;
options.config.recovery_policy = netft::RecoveryPolicy::FailStop;
options.duration = 80ms;
std::ostringstream output;
std::ostringstream errors;

EXPECT_EQ(netft::cli::run(options, output, errors), 2);
EXPECT_TRUE(output.str().empty());
EXPECT_FALSE(errors.str().empty());
}

TEST(CliBias, BiasesAfterFirstSampleAndReportsLaterSample) {
netft::test::FakeSensor sensor{10.0};
auto options = options_for(netft::cli::Command::Bias, sensor);
Expand All @@ -382,6 +454,32 @@ TEST(CliBias, BiasesAfterFirstSampleAndReportsLaterSample) {
EXPECT_GE(json_unsigned(output.str(), "delivered_count"), 3U);
}

TEST(CliBias, StopsWhenAlreadyInterruptedBeforeAFirstSample) {
netft::test::FakeSensor sensor;
sensor.pause();
auto options = options_for(netft::cli::Command::Bias, sensor);
volatile std::sig_atomic_t interrupted = 1;
std::ostringstream output;
std::ostringstream errors;

EXPECT_EQ(netft::cli::run(options, output, errors, &interrupted), 130);
EXPECT_TRUE(output.str().empty());
}

TEST(CliBias, ReturnsTwoWhenTheClientFaultsBeforeAFirstSample) {
netft::test::FakeSensor sensor;
sensor.pause();
auto options = options_for(netft::cli::Command::Bias, sensor);
options.config.receive_timeout = 20ms;
options.config.recovery_policy = netft::RecoveryPolicy::FailStop;
std::ostringstream output;
std::ostringstream errors;

EXPECT_EQ(netft::cli::run(options, output, errors), 2);
EXPECT_TRUE(output.str().empty());
EXPECT_FALSE(errors.str().empty());
}

TEST(CliOutput, WritesResultToRequestedFile) {
netft::test::FakeSensor sensor;
auto options = options_for(netft::cli::Command::Info, sensor);
Expand All @@ -400,6 +498,22 @@ TEST(CliOutput, WritesResultToRequestedFile) {
std::filesystem::remove(path);
}

TEST(CliOutput, PreservesAnExistingDirectoryWhenReplacementFails) {
netft::test::FakeSensor sensor;
auto options = options_for(netft::cli::Command::Info, sensor);
const auto path = temporary_path("output-directory");
std::filesystem::remove_all(path);
ASSERT_TRUE(std::filesystem::create_directory(path));
options.output_path = path.string();
std::ostringstream output;
std::ostringstream errors;

EXPECT_EQ(netft::cli::run(options, output, errors), 2);
EXPECT_TRUE(output.str().empty());
EXPECT_TRUE(std::filesystem::is_directory(path));
std::filesystem::remove(path);
}

TEST(CliProcess, SigintStopsClientAndReturns130) {
netft::test::FakeSensor sensor;
const int null_fd = ::open("/dev/null", O_WRONLY);
Expand Down
37 changes: 37 additions & 0 deletions test/test_discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
#include <array>
#include <chrono>
#include <cmath>
#include <functional>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>

#include "detail/xml_config.hpp"
#include "netft/discovery.hpp"
Expand Down Expand Up @@ -70,6 +72,41 @@ netft::DiscoveryOptions options_for(const FakeHttpServer &server) {
return options;
}

TEST(SensorDiscovery, RejectsInvalidPublicOptionsBeforeNetworkIo) {
const std::vector<std::function<void(netft::DiscoveryOptions &)>> cases{
[](auto &options) { options.sensor_host.clear(); },
[](auto &options) { options.sensor_host = " \t\r\n"; },
[](auto &options) { options.http_port = 0; },
[](auto &options) { options.http_port = 65'536; },
[](auto &options) { options.connect_timeout = std::chrono::duration<double>{0.0}; },
[](auto &options) { options.connect_timeout = std::chrono::duration<double>{-1.0}; },
[](auto &options) {
options.connect_timeout =
std::chrono::duration<double>{std::numeric_limits<double>::infinity()};
},
[](auto &options) {
options.connect_timeout =
std::chrono::duration<double>{std::numeric_limits<double>::quiet_NaN()};
},
[](auto &options) { options.total_timeout = std::chrono::duration<double>{0.0}; },
[](auto &options) { options.total_timeout = std::chrono::duration<double>{-1.0}; },
[](auto &options) {
options.total_timeout =
std::chrono::duration<double>{std::numeric_limits<double>::infinity()};
},
[](auto &options) {
options.total_timeout =
std::chrono::duration<double>{std::numeric_limits<double>::quiet_NaN()};
},
};

for (const auto &configure : cases) {
netft::DiscoveryOptions options;
configure(options);
EXPECT_THROW(netft::discover_sensor(options), netft::DiscoveryError);
}
}

TEST(XmlConfiguration, ParsesTheRealSensorFixtureExactly) {
const auto result = netft::detail::parse_sensor_configuration(kValidXml);

Expand Down
68 changes: 53 additions & 15 deletions test/test_types.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include <gtest/gtest.h>

#include <array>
#include <chrono>
#include <functional>
#include <limits>
#include <string>
#include <string_view>
#include <utility>

#include "netft/types.hpp"

Expand Down Expand Up @@ -263,25 +266,60 @@ TEST(Config, RejectsAnAmbiguousManualCalibration) {
}

TEST(Units, ConvertsForceAndTorqueUnitStrings) {
EXPECT_EQ(netft::to_string(netft::ForceUnit::PoundForce), "lbf");
EXPECT_EQ(netft::to_string(netft::ForceUnit::Newton), "N");
EXPECT_EQ(netft::to_string(netft::ForceUnit::KiloPoundForce), "klbf");
EXPECT_EQ(netft::to_string(netft::ForceUnit::KiloNewton), "kN");
EXPECT_EQ(netft::to_string(netft::ForceUnit::KilogramForce), "kgf");
EXPECT_EQ(netft::force_unit_from_string("N"), netft::ForceUnit::Newton);
constexpr std::array force_cases{
std::pair{netft::ForceUnit::Unknown, std::string_view{"unknown"}},
std::pair{netft::ForceUnit::PoundForce, std::string_view{"lbf"}},
std::pair{netft::ForceUnit::Newton, std::string_view{"N"}},
std::pair{netft::ForceUnit::KiloPoundForce, std::string_view{"klbf"}},
std::pair{netft::ForceUnit::KiloNewton, std::string_view{"kN"}},
std::pair{netft::ForceUnit::KilogramForce, std::string_view{"kgf"}},
};
for (const auto &[unit, spelling] : force_cases) {
EXPECT_EQ(netft::to_string(unit), spelling);
EXPECT_EQ(netft::force_unit_from_string(spelling), unit);
}
EXPECT_EQ(netft::force_unit_from_string("invalid"), std::nullopt);

EXPECT_EQ(netft::to_string(netft::TorqueUnit::PoundForceInch), "lbf-in");
EXPECT_EQ(netft::to_string(netft::TorqueUnit::PoundForceFoot), "lbf-ft");
EXPECT_EQ(netft::to_string(netft::TorqueUnit::NewtonMeter), "N-m");
EXPECT_EQ(netft::to_string(netft::TorqueUnit::NewtonMillimeter), "N-mm");
EXPECT_EQ(netft::to_string(netft::TorqueUnit::KilogramForceCentimeter), "kgf-cm");
EXPECT_EQ(netft::to_string(netft::TorqueUnit::KiloNewtonMeter), "kN-m");
EXPECT_EQ(netft::torque_unit_from_string("N-m"), netft::TorqueUnit::NewtonMeter);
constexpr std::array torque_cases{
std::pair{netft::TorqueUnit::Unknown, std::string_view{"unknown"}},
std::pair{netft::TorqueUnit::PoundForceInch, std::string_view{"lbf-in"}},
std::pair{netft::TorqueUnit::PoundForceFoot, std::string_view{"lbf-ft"}},
std::pair{netft::TorqueUnit::NewtonMeter, std::string_view{"N-m"}},
std::pair{netft::TorqueUnit::NewtonMillimeter, std::string_view{"N-mm"}},
std::pair{netft::TorqueUnit::KilogramForceCentimeter, std::string_view{"kgf-cm"}},
std::pair{netft::TorqueUnit::KiloNewtonMeter, std::string_view{"kN-m"}},
};
for (const auto &[unit, spelling] : torque_cases) {
EXPECT_EQ(netft::to_string(unit), spelling);
EXPECT_EQ(netft::torque_unit_from_string(spelling), unit);
}
EXPECT_EQ(netft::torque_unit_from_string("invalid"), std::nullopt);
}

TEST(States, ConvertsStateAndFaultCodeStrings) {
EXPECT_EQ(netft::to_string(netft::ClientState::Streaming), "streaming");
EXPECT_EQ(netft::to_string(netft::FaultCode::SeriousStatus), "serious_status");
constexpr std::array state_cases{
std::pair{netft::ClientState::Stopped, std::string_view{"stopped"}},
std::pair{netft::ClientState::Connecting, std::string_view{"connecting"}},
std::pair{netft::ClientState::Streaming, std::string_view{"streaming"}},
std::pair{netft::ClientState::Backoff, std::string_view{"backoff"}},
std::pair{netft::ClientState::Faulted, std::string_view{"faulted"}},
};
for (const auto &[state, spelling] : state_cases) {
EXPECT_EQ(netft::to_string(state), spelling);
}

constexpr std::array fault_cases{
std::pair{netft::FaultCode::None, std::string_view{"none"}},
std::pair{netft::FaultCode::SensorConfiguration, std::string_view{"sensor_configuration"}},
std::pair{netft::FaultCode::Timeout, std::string_view{"timeout"}},
std::pair{netft::FaultCode::Socket, std::string_view{"socket"}},
std::pair{netft::FaultCode::SeriousStatus, std::string_view{"serious_status"}},
std::pair{netft::FaultCode::FtStall, std::string_view{"ft_stall"}},
std::pair{netft::FaultCode::FtBackward, std::string_view{"ft_backward"}},
std::pair{netft::FaultCode::MalformedStorm, std::string_view{"malformed_storm"}},
std::pair{netft::FaultCode::Callback, std::string_view{"callback"}},
};
for (const auto &[code, spelling] : fault_cases) {
EXPECT_EQ(netft::to_string(code), spelling);
}
}
Loading