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
1 change: 1 addition & 0 deletions cmake/gtest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ FetchContent_MakeAvailable(googletest)
# Add test files
set(TEST_SOURCES
test/cpp/CanDevice_test.cpp
test/cpp/CanDeviceConfiguration_test.cpp
test/cpp/CanFrame_test.cpp
test/cpp/LogIt_test.cpp
test/cpp/CanVersion_test.cpp
Expand Down
13 changes: 13 additions & 0 deletions src/include/CanDeviceConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <cstdint>
#include <iostream>
#include <map>
#include <optional>
#include <string>

Expand All @@ -15,6 +16,18 @@
* depending on the type of CAN device being used.
*/
struct CanDeviceConfiguration {
/**
* @brief Builds a configuration from a map of string parameters, where
* each key must be the name of one of the parameters below.
*
* @param parameters The configuration parameters, indexed by name.
* @return The parsed configuration.
* @throws std::invalid_argument if a key is not a configuration parameter or
* if a value cannot be converted to the type of its parameter.
*/
static CanDeviceConfiguration from_map(
const std::map<std::string, std::string>& parameters);

/**
* @brief The name of the CAN bus.
*
Expand Down
98 changes: 98 additions & 0 deletions src/main/CanDeviceConfiguration.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,108 @@
#include "CanDeviceConfiguration.h"

#include <algorithm>
#include <cctype>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>

namespace {

/**
* @brief Converts a string to an unsigned 32 bits integer.
*
* @param key The name of the parameter being converted, used in the error.
* @param value The value to convert, a decimal number without sign.
* @return The converted value.
* @throws std::invalid_argument if the value is not an unsigned 32 bits
* integer.
*/
uint32_t to_uint32(const std::string& key, const std::string& value) {
const bool is_decimal =
!value.empty() &&
std::all_of(value.begin(), value.end(), [](unsigned char character) {
return std::isdigit(character) != 0;
});

if (is_decimal) {
try {
const uint64_t parsed = std::stoull(value);

if (parsed <= std::numeric_limits<uint32_t>::max()) {
return static_cast<uint32_t>(parsed);
}
} catch (const std::out_of_range&) {
// Reported below, together with the values that are not decimal.
}
}

throw std::invalid_argument("Invalid value '" + value +
"' for CAN device "
"configuration parameter '" +
key + "': expected an unsigned 32 bits integer");
}

/**
* @brief Converts a string to a boolean.
*
* @param key The name of the parameter being converted, used in the error.
* @param value The value to convert, either "true" or "false".
* @return The converted value.
* @throws std::invalid_argument if the value is not a boolean.
*/
bool to_bool(const std::string& key, const std::string& value) {
if (value == "true") {
return true;
}

if (value == "false") {
return false;
}

throw std::invalid_argument("Invalid value '" + value +
"' for CAN device "
"configuration parameter '" +
key + "': expected 'true' or 'false'");
}

} // namespace

CanDeviceConfiguration CanDeviceConfiguration::from_map(
const std::map<std::string, std::string>& parameters) {
CanDeviceConfiguration config;

for (const auto& [key, value] : parameters) {
if (key == "bus_name") {
Comment thread
TiagoLourinho marked this conversation as resolved.
config.bus_name = value;
} else if (key == "bus_number") {
config.bus_number = to_uint32(key, value);
} else if (key == "host") {
config.host = value;
} else if (key == "bitrate") {
config.bitrate = to_uint32(key, value);
} else if (key == "enable_termination") {
config.enable_termination = to_bool(key, value);
} else if (key == "high_speed") {
config.high_speed = to_bool(key, value);
} else if (key == "timeout") {
config.timeout = to_uint32(key, value);
} else if (key == "vcan") {
config.vcan = to_bool(key, value);
} else if (key == "sent_acknowledgement") {
config.sent_acknowledgement = to_uint32(key, value);
} else {
throw std::invalid_argument(
"Unknown CAN device configuration parameter '" + key + "'");
}
}

return config;
}

/**
* @brief Converts the CanDeviceConfiguration object to a string representation.
*
Expand Down
8 changes: 6 additions & 2 deletions src/main/CanVendorAnagate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ void anagate_receive(AnaInt32 nIdentifier, const char* pcBuffer,
*/
CanVendorAnagate::CanVendorAnagate(const CanDeviceArguments& args)
: CanDevice("anagate", args) {
if (!args.config.bus_number.has_value() || !args.config.host.has_value()) {
throw std::invalid_argument("Missing required configuration parameters");
if (!args.config.host.has_value()) {
throw std::invalid_argument("Missing required host");
}

if (!args.config.bus_number.has_value()) {
throw std::invalid_argument("Missing required bus number");
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/CanVendorSocketCan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ constexpr auto LIBSOCKETCAN_SUCCESS = 0;
CanVendorSocketCan::CanVendorSocketCan(const CanDeviceArguments& args)
: CanDevice("socketcan", args) {
if (!args.config.bus_name.has_value()) {
throw std::invalid_argument("Missing required configuration parameters");
throw std::invalid_argument("Missing required bus name");
}
}
/**
Expand Down
4 changes: 4 additions & 0 deletions src/python/CanModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

#include <map>
#include <string>
#include <vector>

#include "CanDevice.h"
Expand Down Expand Up @@ -77,6 +79,8 @@ PYBIND11_MODULE(canmodule, m) {

py::class_<CanDeviceConfiguration>(m, "CanDeviceConfiguration")
.def(py::init<>())
.def_static("from_map", &CanDeviceConfiguration::from_map,
py::arg("parameters"))
.def_readwrite("bus_name", &CanDeviceConfiguration::bus_name)
.def_readwrite("bus_number", &CanDeviceConfiguration::bus_number)
.def_readwrite("host", &CanDeviceConfiguration::host)
Expand Down
127 changes: 127 additions & 0 deletions test/cpp/CanDeviceConfiguration_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "CanDeviceConfiguration.h"

#include <gtest/gtest.h>

#include <map>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

class CanDeviceConfigurationTest : public ::testing::Test {};

TEST_F(CanDeviceConfigurationTest, DefaultConstructorLeavesEverythingUnset) {
const CanDeviceConfiguration config;

ASSERT_FALSE(config.bus_name.has_value());
ASSERT_FALSE(config.bus_number.has_value());
ASSERT_FALSE(config.host.has_value());
ASSERT_FALSE(config.bitrate.has_value());
ASSERT_FALSE(config.enable_termination.has_value());
ASSERT_FALSE(config.high_speed.has_value());
ASSERT_FALSE(config.timeout.has_value());
ASSERT_FALSE(config.vcan.has_value());
ASSERT_FALSE(config.sent_acknowledgement.has_value());
}

TEST_F(CanDeviceConfigurationTest, AssignsEveryParameterOfTheMap) {
const CanDeviceConfiguration config = CanDeviceConfiguration::from_map({
{"bus_name", "can0"},
{"bus_number", "2"},
{"host", "127.0.0.1"},
{"bitrate", "125000"},
{"enable_termination", "true"},
{"high_speed", "false"},
{"timeout", "6000"},
{"vcan", "true"},
{"sent_acknowledgement", "1"},
});

ASSERT_EQ(config.bus_name.value(), "can0");
ASSERT_EQ(config.bus_number.value(), 2);
ASSERT_EQ(config.host.value(), "127.0.0.1");
ASSERT_EQ(config.bitrate.value(), 125000);
ASSERT_TRUE(config.enable_termination.value());
ASSERT_FALSE(config.high_speed.value());
ASSERT_EQ(config.timeout.value(), 6000);
ASSERT_TRUE(config.vcan.value());
ASSERT_EQ(config.sent_acknowledgement.value(), 1);
}

TEST_F(CanDeviceConfigurationTest, LeavesTheAbsentParametersUnset) {
const CanDeviceConfiguration config =
CanDeviceConfiguration::from_map({{"bus_name", "can0"}});

ASSERT_EQ(config.bus_name.value(), "can0");
ASSERT_FALSE(config.bus_number.has_value());
ASSERT_FALSE(config.host.has_value());
ASSERT_FALSE(config.bitrate.has_value());
ASSERT_FALSE(config.enable_termination.has_value());
ASSERT_FALSE(config.high_speed.has_value());
ASSERT_FALSE(config.timeout.has_value());
ASSERT_FALSE(config.vcan.has_value());
ASSERT_FALSE(config.sent_acknowledgement.has_value());
}

TEST_F(CanDeviceConfigurationTest, AssignsEveryParameterPositionally) {
const CanDeviceConfiguration config{"can0", 2, "127.0.0.1", 125000, true,
false, 6000, true, 1};

ASSERT_EQ(config.bus_name.value(), "can0");
ASSERT_EQ(config.bus_number.value(), 2);
ASSERT_EQ(config.host.value(), "127.0.0.1");
ASSERT_EQ(config.bitrate.value(), 125000);
ASSERT_TRUE(config.enable_termination.value());
ASSERT_FALSE(config.high_speed.value());
ASSERT_EQ(config.timeout.value(), 6000);
ASSERT_TRUE(config.vcan.value());
ASSERT_EQ(config.sent_acknowledgement.value(), 1);
}

TEST_F(CanDeviceConfigurationTest, LeavesTrailingPositionalParametersUnset) {
const CanDeviceConfiguration config{"can0", 2};

ASSERT_EQ(config.bus_name.value(), "can0");
ASSERT_EQ(config.bus_number.value(), 2);
ASSERT_FALSE(config.host.has_value());
ASSERT_FALSE(config.bitrate.has_value());
ASSERT_FALSE(config.enable_termination.has_value());
ASSERT_FALSE(config.high_speed.has_value());
ASSERT_FALSE(config.timeout.has_value());
ASSERT_FALSE(config.vcan.has_value());
ASSERT_FALSE(config.sent_acknowledgement.has_value());
}

TEST_F(CanDeviceConfigurationTest, RejectsUnknownParameters) {
ASSERT_THROW(CanDeviceConfiguration::from_map({{"buss_name", "can0"}}),
std::invalid_argument);
ASSERT_THROW(CanDeviceConfiguration::from_map({{"vendor", "socketcan"}}),
std::invalid_argument);
ASSERT_THROW(CanDeviceConfiguration::from_map({{"", ""}}),
std::invalid_argument);
}

TEST_F(CanDeviceConfigurationTest, RejectsValuesOfTheWrongType) {
const std::vector<std::pair<std::string, std::string>> invalid_values = {
{"bus_number", ""},
{"bus_number", "-1"},
{"bus_number", "+1"},
{"bus_number", "1.5"},
{"bus_number", "12a"},
{"bus_number", "0x2"},
{"bus_number", " 2 "},
{"bitrate", "4294967296"},
{"bitrate", "99999999999999999999"},
{"enable_termination", ""},
{"enable_termination", "yes"},
{"high_speed", "TRUE"},
{"vcan", "1"},
{"sent_acknowledgement", "abc"},
};

for (const auto& [key, value] : invalid_values) {
ASSERT_THROW(CanDeviceConfiguration::from_map({{key, value}}),
std::invalid_argument)
<< "Expected '" << value << "' to be invalid for '" << key << "'";
}
}
18 changes: 18 additions & 0 deletions test/python/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ def test_loopback_multiple_messages():
assert received_frames[1].message() == ["W", "o", "r", "l", "d"]


def test_configuration_from_parameters():
config = CanDeviceConfiguration.from_map(
{"bus_name": "can0", "bitrate": "500000", "vcan": "true"}
)
assert config.bus_name == "can0"
assert config.bitrate == 500000
assert config.vcan is True
assert config.host is None


def test_configuration_from_parameters_unknown_key():
with pytest.raises(ValueError) as e:
CanDeviceConfiguration.from_map({"not_a_parameter": "value"})
assert (
str(e.value) == "Unknown CAN device configuration parameter 'not_a_parameter'"
)


def test_loopback_construction_empty_callback():
myDevice = CanDevice.create(
"loopback", CanDeviceArguments(CanDeviceConfiguration())
Expand Down