diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 71f9766..3f96f40 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,6 +1,6 @@ name: Libdbc Pipeline -on: [push, workflow_call] +on: [push, pull_request, workflow_call, workflow_dispatch] env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) @@ -9,18 +9,18 @@ env: jobs: linux-builds: name: linux ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: cxx: - - g++-13 + - g++-11 - clang++-16 build_type: [Debug, Release] std: [11] include: - - cxx: g++-13 - cc: gcc-13 + - cxx: g++-11 + cc: gcc-11 - cxx: clang++-16 cc: clang-16 llvm_version: 16 @@ -48,6 +48,9 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" + - name: Print gcc version + run: gcc --version + - name: Configure build env: CC: ${{matrix.cc}} @@ -70,13 +73,17 @@ jobs: CTEST_OUTPUT_ON_FAILURE: 1 run: ctest --output-on-failure --test-dir build -j `nproc` + - name: Run install + run: cmake --install build --prefix . + + windows-build: name: ${{matrix.os}}, ${{matrix.std}}, ${{matrix.build_type}}, ${{matrix.platform}} runs-on: ${{matrix.os}} strategy: fail-fast: false matrix: - os: [windows-2019, windows-2022] + os: [windows-2025, windows-2022] platform: [Win32, x64] build_type: [Debug, Release] std: [11] @@ -113,11 +120,12 @@ jobs: shell: cmd macos-builds: - name: macos ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} - runs-on: macos-latest + name: ${{matrix.os}}, ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} + runs-on: ${{matrix.os}} strategy: fail-fast: false matrix: + os: [macos-15-large] cxx: - g++ - clang++ @@ -218,6 +226,6 @@ jobs: - name: Make build directory run: cmake -Bbuild -H$GITHUB_WORKSPACE - - name: Run clang-tidy check + - name: Run build run: cmake --build build diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d4fb58..551af92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) # Keep this on one line for release checking -project(dbc VERSION 0.3.0 DESCRIPTION "C++ DBC Parser") +project(dbc VERSION 0.5.0 DESCRIPTION "C++ DBC Parser") # -- PROJECT OPTIONS -- # option(DBC_ENABLE_TESTS "Enable Unittests" ON) diff --git a/include/libdbc/dbc.hpp b/include/libdbc/dbc.hpp index 68e9789..5e0e9e4 100644 --- a/include/libdbc/dbc.hpp +++ b/include/libdbc/dbc.hpp @@ -15,6 +15,7 @@ class Parser { virtual ~Parser() = default; virtual void parse_file(const std::string& file) = 0; + virtual void parse_file(std::istream& file) = 0; protected: }; @@ -23,7 +24,8 @@ class DbcParser : public Parser { public: DbcParser(); - void parse_file(const std::string& file) override; + void parse_file(const std::string& file_name) override; + void parse_file(std::istream& stream) override; std::string get_version() const; std::vector get_nodes() const; @@ -31,6 +33,8 @@ class DbcParser : public Parser { Message::ParseSignalsStatus parse_message(uint32_t message_id, const std::vector& data, std::vector& out_values); + std::vector unused_lines() const; + private: std::string version; std::vector nodes; @@ -44,9 +48,13 @@ class DbcParser : public Parser { std::regex value_re; std::regex signal_re; + std::vector missed_lines; + void parse_dbc_header(std::istream& file_stream); void parse_dbc_nodes(std::istream& file_stream); void parse_dbc_messages(const std::vector& lines); + + static std::string get_extension(const std::string& file_name); }; } diff --git a/include/libdbc/exceptions/error.hpp b/include/libdbc/exceptions/error.hpp index 27e2fa4..03fd5a0 100644 --- a/include/libdbc/exceptions/error.hpp +++ b/include/libdbc/exceptions/error.hpp @@ -2,6 +2,7 @@ #define ERROR_HPP #include +#include namespace Libdbc { @@ -19,6 +20,49 @@ class ValidityError : public Exception { } }; +class NonDbcFileFormatError : public ValidityError { +public: + NonDbcFileFormatError(const std::string& path, const std::string& extension) { + error_msg = {"File is not of DBC format. Expected a .dbc extension. Cannot read this type of file (" + path + "). Found the extension (" + extension + + ")."}; + } + + const char* what() const throw() override { + return error_msg.c_str(); + } + +private: + std::string error_msg; +}; + +class DbcFileIsMissingVersion : public ValidityError { +public: + DbcFileIsMissingVersion(const std::string& line) { + error_msg = {"Invalid dbc file. Missing the required version header. Attempting to read line: (" + line + ")."}; + } + + const char* what() const throw() override { + return error_msg.c_str(); + } + +private: + std::string error_msg; +}; + +class DbcFileIsMissingBitTiming : public ValidityError { +public: + DbcFileIsMissingBitTiming(const std::string& line) { + error_msg = {"Invalid dbc file. Missing required bit timing in the header. Attempting to read line: (" + line + ")."}; + } + + const char* what() const throw() override { + return error_msg.c_str(); + } + +private: + std::string error_msg; +}; + } // libdbc #endif // ERROR_HPP diff --git a/src/dbc.cpp b/src/dbc.cpp index 360f05c..89e8c24 100644 --- a/src/dbc.cpp +++ b/src/dbc.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -59,7 +60,7 @@ DbcParser::DbcParser() : version_re("^(VERSION)\\s\"(.*)\"") , bit_timing_re("^(BS_:)") , name_space_re("^(NS_)\\s\\:") - , node_re("^(BU_:)\\s((?:[\\w]+?\\s?)*)") + , node_re("^(BU_:)\\s?((?:[\\w]+?\\s?)*)?") , message_re("^(BO_)\\s(\\d+)\\s(\\w+)\\:\\s(\\d+)\\s(\\w+|Vector__XXX)") , value_re("^(VAL_)\\s(\\d+)\\s(\\w+)((?:\\s(\\d+)\\s\"([^\"]*)\")+)\\s;$") , @@ -69,15 +70,13 @@ DbcParser::DbcParser() + whiteSpace + receiverPattern) { } -void DbcParser::parse_file(const std::string& file) { - std::ifstream stream(file.c_str()); +void DbcParser::parse_file(std::istream& stream) { std::string line; std::vector lines; messages.clear(); parse_dbc_header(stream); - parse_dbc_nodes(stream); while (!stream.eof()) { @@ -88,6 +87,26 @@ void DbcParser::parse_file(const std::string& file) { parse_dbc_messages(lines); } +void DbcParser::parse_file(const std::string& file_name) { + auto extension = get_extension(file_name); + if (extension != ".dbc") { + throw NonDbcFileFormatError(file_name, extension); + } + + std::ifstream stream(file_name.c_str()); + + parse_file(stream); +} + +std::string DbcParser::get_extension(const std::string& file_name) { + std::size_t dot = file_name.find_last_of("."); + if (dot != std::string::npos) { + return file_name.substr(dot, file_name.size() - dot); + } + + return ""; +} + std::string DbcParser::get_version() const { return version; } @@ -116,7 +135,7 @@ void DbcParser::parse_dbc_header(std::istream& file_stream) { Utils::StreamHandler::get_line(file_stream, line); if (!std::regex_search(line, match, version_re)) { - throw ValidityError(); + throw DbcFileIsMissingVersion(line); } version = match.str(2); @@ -126,7 +145,7 @@ void DbcParser::parse_dbc_header(std::istream& file_stream) { Utils::StreamHandler::get_next_non_blank_line(file_stream, line); if (!std::regex_search(line, match, bit_timing_re)) { - throw ValidityError(); + throw DbcFileIsMissingBitTiming(line); } } @@ -136,9 +155,7 @@ void DbcParser::parse_dbc_nodes(std::istream& file_stream) { Utils::StreamHandler::get_next_non_blank_line(file_stream, line); - if (!std::regex_search(line, match, node_re)) { - throw ValidityError(); - } + std::regex_search(line, match, node_re); if (match.length() > 2) { std::string node = match.str(2); @@ -164,7 +181,7 @@ void DbcParser::parse_dbc_messages(const std::vector& lines) { continue; } - if (std::regex_search(line, match, signal_re)) { + if (std::regex_search(line, match, signal_re) && !messages.empty()) { std::string name = match.str(SIGNAL_NAME_GROUP); bool is_multiplexed = false; // No support yet uint32_t start_bit = static_cast(std::stoul(match.str(SIGNAL_START_BIT_GROUP))); @@ -187,7 +204,7 @@ void DbcParser::parse_dbc_messages(const std::vector& lines) { continue; } - if (std::regex_search(line, match, value_re)) { + if (std::regex_search(line, match, value_re) && !messages.empty()) { uint32_t message_id = static_cast(std::stoul(match.str(2))); std::string signal_name = match.str(3); @@ -213,6 +230,10 @@ void DbcParser::parse_dbc_messages(const std::vector& lines) { signal_value.push_back(val); continue; } + + if (line.length() > 0) { + missed_lines.push_back(line); + } } for (const auto& signal : signal_value) { @@ -225,4 +246,8 @@ void DbcParser::parse_dbc_messages(const std::vector& lines) { } } +std::vector DbcParser::unused_lines() const { + return missed_lines; +} + } diff --git a/test/dbcs/MissingVersion.dbc b/test/dbcs/MissingVersion.dbc index 0db087a..40d5967 100644 --- a/test/dbcs/MissingVersion.dbc +++ b/test/dbcs/MissingVersion.dbc @@ -1,36 +1,36 @@ -NS_ : - BA_ - BA_DEF_ - BA_DEF_DEF_ - BA_DEF_DEF_REL_ - BA_DEF_REL_ - BA_DEF_SGTYPE_ - BA_REL_ - BA_SGTYPE_ - BO_TX_BU_ - BU_BO_REL_ - BU_EV_REL_ - BU_SG_REL_ - CAT_ - CAT_DEF_ - CM_ - ENVVAR_DATA_ - EV_DATA_ - FILTER - NS_DESC_ - SGTYPE_ - SGTYPE_VAL_ - SG_MUL_VAL_ - SIGTYPE_VALTYPE_ - SIG_GROUP_ - SIG_TYPE_REF_ - SIG_VALTYPE_ - VAL_ - VAL_TABLE_ - -BS_: - -BU_: DBG DRIVER IO MOTOR SENSOR - -BO_ 500 IO_DEBUG: 4 IO - SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG \ No newline at end of file +NS_ : + BA_ + BA_DEF_ + BA_DEF_DEF_ + BA_DEF_DEF_REL_ + BA_DEF_REL_ + BA_DEF_SGTYPE_ + BA_REL_ + BA_SGTYPE_ + BO_TX_BU_ + BU_BO_REL_ + BU_EV_REL_ + BU_SG_REL_ + CAT_ + CAT_DEF_ + CM_ + ENVVAR_DATA_ + EV_DATA_ + FILTER + NS_DESC_ + SGTYPE_ + SGTYPE_VAL_ + SG_MUL_VAL_ + SIGTYPE_VALTYPE_ + SIG_GROUP_ + SIG_TYPE_REF_ + SIG_VALTYPE_ + VAL_ + VAL_TABLE_ + +BS_: + +BU_: DBG DRIVER IO MOTOR SENSOR + +BO_ 500 IO_DEBUG: 4 IO + SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG diff --git a/test/single_header_testing/test_single_header.cpp b/test/single_header_testing/test_single_header.cpp index e4abc6c..c512d35 100644 --- a/test/single_header_testing/test_single_header.cpp +++ b/test/single_header_testing/test_single_header.cpp @@ -54,3 +54,27 @@ TEST_CASE("Testing big endian, little endian") { REQUIRE(signal.is_bigendian == false); } } + +TEST_CASE("Testing file stream mirrors the filename interface") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 234 MSG1: 8 Vector__XXX + SG_ Sig1 : 55|16@0- (0.1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Sig2 : 39|16@1- (0.1,0) [-3276.8|-3276.7] "C" Vector__XXX)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + std::ifstream file(filename.c_str()); + + auto parser = Libdbc::DbcParser(); + parser.parse_file(file); + + REQUIRE(parser.get_messages().size() == 1); + REQUIRE(parser.get_messages().at(0).name() == "MSG1"); + REQUIRE(parser.get_messages().at(0).size() == 8); + REQUIRE(parser.get_messages().at(0).get_signals().size() == 2); + { + const auto signal = parser.get_messages().at(0).get_signals().at(0); + REQUIRE(signal.is_bigendian == true); + } + { + const auto signal = parser.get_messages().at(0).get_signals().at(1); + REQUIRE(signal.is_bigendian == false); + } +} diff --git a/test/test_dbc.cpp b/test/test_dbc.cpp index 5fd2515..8ae1b54 100644 --- a/test/test_dbc.cpp +++ b/test/test_dbc.cpp @@ -3,29 +3,35 @@ #include #include #include +#include #include #include #include +using Catch::Matchers::ContainsSubstring; + TEST_CASE("Testing dbc file loading error issues", "[fileio][error]") { auto parser = std::unique_ptr(new Libdbc::DbcParser()); SECTION("Loading a non dbc file should throw an error", "[error]") { - REQUIRE_THROWS_AS(parser->parse_file(TEXT_FILE), Libdbc::ValidityError); + REQUIRE_THROWS_AS(parser->parse_file(TEXT_FILE), Libdbc::NonDbcFileFormatError); + REQUIRE_THROWS_WITH(parser->parse_file(TEXT_FILE), ContainsSubstring("TextFile.txt")); } - SECTION("Loading a dbc with bad headers throws an error", "[error]") { - REQUIRE_THROWS_AS(parser->parse_file(MISSING_VERSION_DBC_FILE), Libdbc::ValidityError); + SECTION("Loading a dbc with missing version header throws an error (VERSION)", "[error]") { + REQUIRE_THROWS_AS(parser->parse_file(MISSING_VERSION_DBC_FILE), Libdbc::DbcFileIsMissingVersion); + REQUIRE_THROWS_WITH(parser->parse_file(MISSING_VERSION_DBC_FILE), ContainsSubstring("line: (NS_ :)")); } SECTION("Loading a dbc without the required bit timing section (BS_:)", "[error]") { - REQUIRE_THROWS_AS(parser->parse_file(MISSING_BIT_TIMING_DBC_FILE), Libdbc::ValidityError); + REQUIRE_THROWS_AS(parser->parse_file(MISSING_BIT_TIMING_DBC_FILE), Libdbc::DbcFileIsMissingBitTiming); + REQUIRE_THROWS_WITH(parser->parse_file(MISSING_BIT_TIMING_DBC_FILE), ContainsSubstring("BU_: DBG DRIVER IO MOTOR SENSOR")); } SECTION("Loading a dbc with some missing namespace section tags (NS_ :)", "[error]") { // Confusion about this type of error. it appears that the header isn't // very well standardized for now we ignore this type of error. - CHECK_NOTHROW(parser->parse_file(MISSING_NEW_SYMBOLS_DBC_FILE)); + REQUIRE_NOTHROW(parser->parse_file(MISSING_NEW_SYMBOLS_DBC_FILE)); } SECTION("Verify that what() method is accessible for all exceptions", "[error]") { @@ -239,3 +245,67 @@ VAL_ 123 State1 123 "Description 3" 0 "Description 4" ;)"; REQUIRE(signal2.value_descriptions.at(1).value == 0); REQUIRE(signal2.value_descriptions.at(1).description == "Description 4"); } + +TEST_CASE("Should parse DBC with empty BU_", "[error][optional]") { + std::string contents = R"(VERSION "" + + +NS_ : + +BS_: + +BU_: + + +BO_ 293 Msg1: 2 Vector__XXX + SG_ Wert7 : 0|16@1- (1,0) [0|0] "" Vector__XXX + +BO_ 292 Msg2: 1 Vector__XXX + SG_ Wert8 : 56|8@1- (1,0) [0|0] "" Vector__XXX +)"; + const auto filename = create_temporary_dbc_with(contents.c_str()); + + auto parser = Libdbc::DbcParser(); + REQUIRE_NOTHROW(parser.parse_file(filename.c_str())); + + REQUIRE(parser.get_messages().size() == 2); + REQUIRE(parser.get_messages().at(0).name() == "Msg1"); + REQUIRE(parser.get_messages().at(1).name() == "Msg2"); +} + +TEST_CASE("Should report unused lines since we don't have tracing.", "[parsing]") { + std::string contents = R"(VERSION "" + +NS_ : + +BS_: + +BU_: + + +BO_ 293 Msg1: 2 Vector__XXX + SG_ Whitespace: | 0|16@1- (1,0) [0|0] "" Vector__XXX + SG_ Wert7 : 0|16@1- (1,0) [0|0] "" Vector__XXX + SG_ Wert8 : 0|16@1- (1,0) [0|0] "" Vector__XXX + +BO_ 292 Msg2: 1 Vector__XXX + SG_ Wert8 : 56|8@1- (1,0) [0|0] "" Vector__XXX + SB_ not a correct line + +BO_ have a issue here: +)"; + + const auto filename = create_temporary_dbc_with(contents.c_str()); + + auto parser = Libdbc::DbcParser(); + REQUIRE_NOTHROW(parser.parse_file(filename.c_str())); + + REQUIRE(parser.get_messages().size() == 2); + REQUIRE(parser.get_messages()[0].size() == 2); + REQUIRE(parser.get_messages()[1].size() == 1); + + auto unused = parser.unused_lines(); + + // We could match them all here but i think just a check that the size is sufficent. + REQUIRE(unused.size() == 3); +} diff --git a/test/test_parse_message.cpp b/test/test_parse_message.cpp index aad3b1c..08d68f3 100644 --- a/test/test_parse_message.cpp +++ b/test/test_parse_message.cpp @@ -173,3 +173,39 @@ TEST_CASE("Parse Message data length < 8 unsigned") { REQUIRE(Catch::Approx(result_values.at(0)) == 0x1); REQUIRE(Catch::Approx(result_values.at(1)) == 0x2); } + +TEST_CASE("Parse message with BO_ on single line should fail.") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ +234 MSG1: 8 Vector__XXX + SG_ State1 : 0|8@1+ (1,0) [0|200] "Km/h" DEVICE1,DEVICE2,DEVICE3 + SG_ State2 : 0|8@1+ (1,0) [0|204] "" DEVICE1,DEVICE2,DEVICE3 +VAL_ 234 State1 123 "Description 1" 0 "Description 2" 90903489 "Big value and special characters &$§())!")"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + Libdbc::DbcParser p; + p.parse_file(filename); + + std::vector data{0x1, 0x2}; + std::vector result_values; + REQUIRE(p.get_messages().size() == 0); + REQUIRE(p.parse_message(234, data, result_values) == Libdbc::Message::ParseSignalsStatus::ErrorUnknownID); +} + +TEST_CASE("Parse signal with SG_ on single line should fail.") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 234 MSG1: 8 Vector__XXX + SG_ +State1 : 0|8@1+ (1,0) [0|200] "Km/h" DEVICE1,DEVICE2,DEVICE3 + SG_ State2 : 0|8@1+ (1,0) [0|204] "" DEVICE1,DEVICE2,DEVICE3 +VAL_ 234 State1 123 "Description 1" 0 "Description 2" 90903489 "Big value and special characters &$§())!")"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + Libdbc::DbcParser p; + p.parse_file(filename); + + std::vector data{0x1, 0x2}; + std::vector result_values; + REQUIRE(p.get_messages().size() == 1); + REQUIRE(p.parse_message(234, data, result_values) == Libdbc::Message::ParseSignalsStatus::Success); + REQUIRE(result_values.size() == 1); + REQUIRE(Catch::Approx(result_values.at(0)) == 0x1); +} diff --git a/test/testing_utils/common.cpp b/test/testing_utils/common.cpp index e058054..4c3ef54 100644 --- a/test/testing_utils/common.cpp +++ b/test/testing_utils/common.cpp @@ -23,7 +23,7 @@ std::string generate_unique_filename() { int random_num = dis(gen); // Concatenate time and random number to create a unique filename - return "temp_file_" + std::to_string(milliseconds) + "_" + std::to_string(random_num) + ".txt"; + return "temp_file_" + std::to_string(milliseconds) + "_" + std::to_string(random_num) + ".dbc"; } std::string create_temporary_dbc_with(const char* contents) {