diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e5d309b --- /dev/null +++ b/.clang-format @@ -0,0 +1,106 @@ +--- +# SPDX-FileCopyrightText: 2019 Christoph Cullmann +# SPDX-FileCopyrightText: 2019 Gernot Gebhard +# +# SPDX-License-Identifier: MIT + +# This file got automatically created by ECM, do not edit +# See https://clang.llvm.org/docs/ClangFormatStyleOptions.html for the config options +# and https://community.kde.org/Policies/Frameworks_Coding_Style#Clang-format_automatic_code_formatting +# for clang-format tips & tricks +--- +Language: JavaScript +DisableFormat: true +--- + +# Style for C++ +Language: Cpp + +# base is WebKit coding style: https://webkit.org/code-style-guidelines/ +# below are only things set that diverge from this style! +BasedOnStyle: WebKit + +# enforce C++11 (e.g. for std::vector> +Standard: Cpp11 + +# 4 spaces indent +TabWidth: 4 +IndentWidth: 4 +UseTab: Always + +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: 1 + +# 2 * 80 wide lines +ColumnLimit: 160 + +# sort includes inside line separated groups +SortIncludes: true + +# break before braces on function, namespace and class definitions. +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: false + AfterCaseLabel: false + +# CrlInstruction *a; +DerivePointerAlignment: false +PointerAlignment: Left +ReferenceAlignment: Left + +# only clang format 14 +# RemoveBracesLLVM: true + +#QualifierAlignment: Left + +# horizontally aligns arguments after an open bracket. +AlignAfterOpenBracket: Align + +# don't move all parameters to new line +AllowAllParametersOfDeclarationOnNextLine: false + +# no single line functions +AllowShortFunctionsOnASingleLine: None + +# always break before you encounter multi line strings +AlwaysBreakBeforeMultilineStrings: true + +# don't move arguments to own lines if they are not all on the same +BinPackArguments: false + +# don't move parameters to own lines if they are not all on the same +BinPackParameters: false + +# In case we have an if statement with multiple lines the operator should be at the beginning of the line +# but we do not want to break assignments +BreakBeforeBinaryOperators: NonAssignment + +# format C++11 braced lists like function calls +Cpp11BracedListStyle: true + +# do not put a space before C++11 braced lists +SpaceBeforeCpp11BracedList: false + +# remove empty lines +KeepEmptyLinesAtTheStartOfBlocks: false + +# no namespace indentation to keep indent level low +NamespaceIndentation: None + +# we use template< without space. +SpaceAfterTemplateKeyword: false + +# Always break after template declaration +AlwaysBreakTemplateDeclarations: true + +# macros for which the opening brace stays attached. +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE , wl_resource_for_each, wl_resource_for_each_safe ] + +# keep lambda formatting multi-line if not empty +AllowShortLambdasOnASingleLine: Empty + +# We do not want clang-format to put all arguments on a new line +AllowAllArgumentsOnNextLine: false + +InsertNewlineAtEOF: true diff --git a/.github/data/release_body.md b/.github/data/release_body.md new file mode 100644 index 0000000..df96e6d --- /dev/null +++ b/.github/data/release_body.md @@ -0,0 +1,19 @@ +# Libdbc release + +### Developer Notes + +**TODO: Update this!** + +## Breaking Changes + +* + +## Features + +* + +## Bugs + +* + + diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..b630847 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,169 @@ +name: Libdbc Pipeline + +on: [push, workflow_call] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + linux-builds: + name: linux ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cxx: + - g++-13 + - clang++-16 + build_type: [Debug, Release] + std: [11] + include: + - cxx: g++-13 + cc: gcc-13 + - cxx: clang++-16 + cc: clang-16 + llvm_version: 16 + + steps: + - uses: actions/checkout@v4 + + - name: Install clang + if: ${{ matrix.llvm_version }} + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh ${{ matrix.llvm_version }} + + - name: Prepare environment + run: | + sudo apt-get update + sudo apt-get install -y locales + + sudo locale-gen de_DE.UTF-8 + + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" + + - name: Configure build + env: + CC: ${{matrix.cc}} + CXX: ${{matrix.cxx}} + run: | + cmake -Bbuild -H$GITHUB_WORKSPACE \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_CXX_STANDARD=${{matrix.std}} \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DCMAKE_CXX_EXTENSIONS=ON \ + -DDBC_TEST_LOCALE_INDEPENDENCE=ON \ + -DDBC_GENERATE_SINGLE_HEADER=ON + + - name: Build tests + lib + run: cmake --build build --parallel `nproc` + + - name: Run tests + env: + CTEST_OUTPUT_ON_FAILURE: 1 + run: ctest --output-on-failure --test-dir build -j `nproc` + + 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] + platform: [Win32, x64] + build_type: [Debug, Release] + std: [11] + + steps: + - uses: actions/checkout@v4 + + - name: Configure build + working-directory: ${{runner.workspace}} + run: | + cmake -S $Env:GITHUB_WORKSPACE ` + -B ${{runner.workspace}}/build ` + -A ${{matrix.platform}} ` + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} ` + -DCMAKE_CXX_STANDARD=${{matrix.std}} ` + -DCMAKE_CXX_STANDARD_REQUIRED=ON ` + -DCMAKE_CXX_EXTENSIONS=ON ` + -DDBC_TEST_LOCALE_INDEPENDENCE=ON + + - name: Build tests + lib + working-directory: ${{runner.workspace}} + run: cmake --build build --config ${{matrix.build_type}} --parallel %NUMBER_OF_PROCESSORS% + shell: cmd + + - name: Run tests + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}} + run: ctest --output-on-failure --test-dir build -j %NUMBER_OF_PROCESSORS% + shell: cmd + + macos-builds: + name: macos ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + cxx: + - g++ + - clang++ + build_type: [Debug, Release] + std: [11] + include: + - cxx: g++ + cc: gcc + - cxx: clang++ + cc: clang + + steps: + - uses: actions/checkout@v4 + + - name: Configure build + working-directory: ${{runner.workspace}} + env: + CC: ${{matrix.cc}} + CXX: ${{matrix.cxx}} + run: | + cmake -Bbuild -H$GITHUB_WORKSPACE \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_CXX_STANDARD=${{matrix.std}} \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DCMAKE_CXX_EXTENSIONS=ON \ + -DDBC_TEST_LOCALE_INDEPENDENCE=ON + + - name: Build tests + lib + working-directory: ${{runner.workspace}} + run: cmake --build build --parallel `sysctl -n hw.ncpu` + + - name: Run tests + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}} + run: ctest --output-on-failure --test-dir build -j `sysctl -n hw.ncpu` + + format-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install clang-format version + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 16 + + sudo apt update && sudo apt install -y clang-format-16 + sudo ln -sf $(which clang-format-16) $(which clang-format) + + test "$(clang-format --version)" == "$(clang-format-16 --version)" + + - name: Test format with clang format + run: ./scripts/fmt.sh + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0e45a26 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Libdbc Release +run-name: Libdbc release v${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} as release type ${{ inputs.release_type }} + +on: + workflow_dispatch: + inputs: + major: + required: true + description: "The major version" + type: number + minor: + required: true + description: "The minor version" + type: number + patch: + required: true + description: "The patch version" + type: number + + release_type: + type: choice + description: "The type of release you are making. Controls branch naming / creation" + options: + - patch + - minor + - major + +jobs: + check_version: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: "Validate version in cmake before continuing" + run: ./scripts/check_version.py --version "v${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }}" + + pipeline: + needs: [check_version] + uses: ./.github/workflows/pipeline.yml + + create_release: + runs-on: ubuntu-latest + needs: [pipeline] + + env: + header_file_path: build/single_header/libdbc/libdbc.hpp + + steps: + - uses: actions/checkout@v4 + + - name: "Setup Cargo" + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" + + - name: Configure build + run: cmake -Bbuild -H$GITHUB_WORKSPACE -DDBC_GENERATE_SINGLE_HEADER=ON + + - name: Generate the header file + run: cmake --build build --parallel `nproc` --target single_header + + - uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: header-only + path: ${{ env.header_file_path }} + + - name: "Create a branch if we are making a major / minor release" + uses: peterjgrainger/action-create-branch@v2.2.0 + if: ${{ inputs.release_type }} == "minor" || ${{ inputs.release_type }} == "major" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + branch: 'release/v${{ inputs.major }}.${{ inputs.minor }}.X' + sha: '${{ github.sha }}' + + - uses: ncipollo/release-action@v1 + with: + artifacts: "${{ env.header_file_path }}" + draft: true + bodyFile: ".github/data/release_body.md" + tag: v${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} + commit: release/v${{ inputs.major }}.${{ inputs.minor }}.X + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 781c60c..fcb11cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,20 @@ # Build folders bin/ build/ +Testing/ # -- Git -- # -*.bak \ No newline at end of file +*.bak + +# -- Python -- # +venv*/ +__pycache__/ + +# -- IDEs / Editors -- # +.idea/ +.vscode/ +*.sublime-* + +# -- Static Analyzers -- # +.cache/ + diff --git a/.gitmodules b/.gitmodules index 61932e2..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "third_party/Catch2"] - path = third_party/Catch2 - url = https://github.com/catchorg/Catch2.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8992cfc..e3afc99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,42 +1,113 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.16) -project(dbc) +# Keep this on one line for release checking +project(dbc VERSION 0.2.0 DESCRIPTION "C++ DBC Parser") -option(DEBUG "use debug flag" NO) +# -- PROJECT OPTIONS -- # +option(DBC_ENABLE_TESTS "Enable Unittests" ON) +option(DBC_TEST_LOCALE_INDEPENDENCE "Used to deterime if the libary is locale agnostic when it comes to converting floats. You need `de_DE.UTF-8` locale installed for this testing." OFF) +option(DBC_GENERATE_DOCS "Use doxygen if installed to generated documentation files" OFF) +option(DBC_GENERATE_SINGLE_HEADER "This will run the generator for the single header file version. Default is OFF since we make a static build. Requires cargo installed." OFF) +# ---------------------- # + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# package +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE) +set(CPACK_RESOURCE_FILE_README ${CMAKE_CURRENT_SOURCE_DIR}/README.md) +include(CPack) # specify the C++ standard set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) -set(GCC_COMPILE_FLAGS "-Wextra -Wall -Wfloat-equal -Wundef -Wshadow \ --Wpointer-arith -Wcast-align -Wstrict-prototypes -Wwrite-strings \ --Waggregate-return -Wcast-qual -Wswitch-default -Wswitch-enum -Wconversion \ --Wunreachable-code -Wformat=2 -Werror -Wuninitialized -Winit-self") +find_package(FastFloat QUIET) +if (NOT ${FastFloat_FOUND}) + include(FetchContent) + FetchContent_Declare( + FastFloat + GIT_REPOSITORY https://github.com/fastfloat/fast_float.git + GIT_TAG 1ea4f27b2aeee2859a1354a3c24cff52a116cad1 + ) + FetchContent_MakeAvailable(FastFloat) +endif() + +# add where to find the source files +list(APPEND SOURCE_FILES + ${PROJECT_SOURCE_DIR}/src/utils.cpp + ${PROJECT_SOURCE_DIR}/src/message.cpp + ${PROJECT_SOURCE_DIR}/src/signal.cpp + ${PROJECT_SOURCE_DIR}/src/dbc.cpp +) + +list(APPEND HEADER_FILES + ${PROJECT_SOURCE_DIR}/include/libdbc/dbc.hpp + ${PROJECT_SOURCE_DIR}/include/libdbc/message.hpp + ${PROJECT_SOURCE_DIR}/include/libdbc/signal.hpp + ${PROJECT_SOURCE_DIR}/include/libdbc/utils/utils.hpp + ${PROJECT_SOURCE_DIR}/include/libdbc/exceptions/error.hpp +) + +if(DBC_ENABLE_TESTS) + include(CTest) + add_subdirectory(test) +endif() + +if(DBC_GENERATE_DOCS) + add_subdirectory(doc) +endif() + +list(APPEND GCC_CLANG_COMPILE_FLAGS + -Wall -Wextra -Wpedantic + -Wconversion -Wint-in-bool-context + -Wmissing-declarations -Wmissing-field-initializers + -Werror +) -if(DEBUG) - set(GCC_COMPILE_FLAGS ${GCC_COMPILE_FLAGS}" -g") + +if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + add_compile_options(/W4 /WX) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # Clang shadow warnings aren't as sensitive as gcc + add_compile_options(${GCC_CLANG_COMPILE_FLAGS} -Wshadow) else() - set(GCC_COMPILE_FLAGS ${GCC_COMPILE_FLAGS}" -O2") + add_compile_options(${GCC_CLANG_COMPILE_FLAGS}) endif() -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GCC_COVERAGE_COMPILE_FLAGS}") +add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES}) +target_link_libraries(${PROJECT_NAME} FastFloat::fast_float) +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $ +) +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_11) -# add where to find the source files -# file(GLOB_RECURSE SOURCE ${PROJECT_SOURCE_DIR}/src/ *.cpp) -list(APPEND SOURCE ${PROJECT_SOURCE_DIR}/src/utils.cpp - ${PROJECT_SOURCE_DIR}/src/message.cpp - ${PROJECT_SOURCE_DIR}/src/signal.cpp - ${PROJECT_SOURCE_DIR}/src/dbc.cpp) +target_sources(${PROJECT_NAME} INTERFACE FILE_SET HEADERS + TYPE HEADERS + BASE_DIRS ${PROJECT_SOURCE_DIR}/include/libdbc + FILES ${HEADER_FILES} +) -include_directories(src) -include_directories(include) +if(DBC_GENERATE_SINGLE_HEADER) + add_custom_target(single_header ALL + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${CMAKE_SOURCE_DIR}/scripts/create_single_header.sh + ) +endif() -add_subdirectory(test) -add_subdirectory(doc) +## Installation +# install lib +install(TARGETS ${PROJECT_NAME} + DESTINATION ${CMAKE_INSTALL_LIBDIR}) -add_library(${PROJECT_NAME} STATIC ${SOURCE}) +# install headers +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/libdbc DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -add_custom_target(release - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND ${CMAKE_SOURCE_DIR}/scripts/create_single_header.sh - DEPENDS ${PROJECT_NAME}) +# Generate pkg-config file +configure_file(${PROJECT_NAME}.pc.in ${PROJECT_NAME}.pc @ONLY) +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) diff --git a/README.md b/README.md index 438ef36..8702453 100644 --- a/README.md +++ b/README.md @@ -4,32 +4,65 @@ This is to provide a library header only file to read in DBC files. I was lookin find a simple library that didn't have dependencies. So here we are making one. I got some inspiration from the python dbc library here: https://pypi.org/project/cantools/ -## Testing - -I am trying to always make sure that this is very well tested code. I am using Catch2 to do this -testing and if you aren't familiar here is the documentation: https://github.com/catchorg/Catch2/blob/master/docs/Readme.md#top - -To run the tests locally you can use the following: -```bash -mkdir build -cd build -cmake .. -make test -j -``` - ## Building I am using Cmake to be able to build the tests and the lib. I plan on doing more with it but this is what it -is for now. I am doing developement on the WSL Ubuntu 18.04 kernel. This doesn't mean that IDEs aren't +is for now. This doesn't mean that IDEs aren't welcome but the build process might not be suited for this. You will need to modify it for your needs. Feel free to submit changes so the building process will be more robust. Here are the steps to get started: ```bash -mkdir build +# Release Build +cmake -DCMAKE_BUILD_TYPE=Release -Bbuild -H. + +# Debug Build +cmake -DCMAKE_BUILD_TYPE=Debug -Bbuild -H. + +# Run the build +cmake --build build +``` + +### Listing Build Options + +You can check the latest build options with cmake. After you configure cmake you can run this. +```shell cd build -cmake .. -make + +# List this projects options +cmake -LH .. | grep -B1 "DBC_" + +# To see all the included project cache variables and options +cmake -LAH .. +``` + +### Creating a Single Header File + +It requires you have `cargo` installed from rust. See these instructions if you don't have that https://www.rust-lang.org/tools/install. +It uses the https://github.com/Felerius/cpp-amalgamate crate to do the single header file creation. + +The output will be generated in the `build/single_header/libdbc/` folder. You can run a cmake command to build this as well as other targets. + +To just build the single header you can simply run the target: +```shell +cmake -Bbuild -H. -DDBC_GENERATE_SINGLE_HEADER=ON + +cmake --build build --parallel `nproc` --target single_header +``` + + +## Testing + +I am trying to always make sure that this is very well tested code. I am using Catch2 to do this +testing and if you aren't familiar here is the documentation: https://github.com/catchorg/Catch2/blob/master/docs/Readme.md#top + +There is one option you will want for testing: `DBC_TEST_LOCALE_INDEPENDENCE`. This requires the `de_DE.UTF-8` locale installed to test. It is for checking we don't rely on locale to convert floats. i.e. 1.23 vs 1,23 + +You will need to configure the project to enable this: `cmake -DCMAKE_BUILD_TYPE=Release -DDBC_TEST_LOCALE_INDEPENDENCE=ON -Bbuild -H.`. You will get a warning if it isn't enabled because it isn't enabled by default. + +To run the tests locally you can use the following. Assuming you have built the project you should get a test executable. +```bash +ctest --output-on-failure --test-dir build ``` ## Scripts diff --git a/dbc.pc.in b/dbc.pc.in new file mode 100644 index 0000000..b3cb36f --- /dev/null +++ b/dbc.pc.in @@ -0,0 +1,10 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ + +Name: @PROJECT_NAME@ +Description: @CPACK_PACKAGE_DESCRIPTION_SUMMARY@ +Version: @CPACK_PACKAGE_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@PROJECT_NAME@ diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 666f5b7..1827d5d 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -13,6 +13,7 @@ if(BUILD_DOCUMENTATION) configure_file(${doxyfile_in} ${doxyfile} @ONLY) add_custom_target(doc + ALL COMMAND ${DOXYGEN_EXECUTABLE} ${doxyfile} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating API documentation with Doxygen" diff --git a/include/libdbc/dbc.hpp b/include/libdbc/dbc.hpp index 8b3c0cf..bff9c4e 100644 --- a/include/libdbc/dbc.hpp +++ b/include/libdbc/dbc.hpp @@ -1,56 +1,54 @@ - #ifndef __DBC_HPP__ #define __DBC_HPP__ #include -#include -#include #include +#include +#include #include namespace libdbc { - class Parser { - public: - virtual ~Parser() = default; - - virtual void parse_file(const std::string& file) = 0; - - protected: +class Parser { +public: + virtual ~Parser() = default; + virtual void parse_file(const std::string& file) = 0; - }; +protected: +}; - class DbcParser : public Parser { - public: - DbcParser(); +class DbcParser : public Parser { +public: + DbcParser(); - virtual ~DbcParser() = default; + virtual ~DbcParser() = default; - virtual void parse_file(const std::string& file) final override; + virtual void parse_file(const std::string& file) final override; - std::string get_version() const; - std::vector get_nodes() const; - std::vector get_messages() const; + std::string get_version() const; + std::vector get_nodes() const; + std::vector get_messages() const; - private: - std::string version; - std::vector nodes; - std::vector messages; + Message::ParseSignalsStatus parseMessage(const uint32_t id, const std::vector& data, std::vector& out_values); - const std::regex version_re; - const std::regex bit_timing_re; - const std::regex name_space_re; - const std::regex node_re; - const std::regex message_re; - const std::regex signal_re; +private: + std::string version; + std::vector nodes; + std::vector messages; - void parse_dbc_header(std::istream& file_stream); - void parse_dbc_nodes(std::istream& file_stream); - void parse_dbc_messages(const std::vector& lines); + const std::regex version_re; + const std::regex bit_timing_re; + const std::regex name_space_re; + const std::regex node_re; + const std::regex message_re; + const std::regex signal_re; - }; + void parse_dbc_header(std::istream& file_stream); + void parse_dbc_nodes(std::istream& file_stream); + void parse_dbc_messages(const std::vector& lines); +}; } diff --git a/include/libdbc/exceptions/error.hpp b/include/libdbc/exceptions/error.hpp index abbc837..175cac7 100644 --- a/include/libdbc/exceptions/error.hpp +++ b/include/libdbc/exceptions/error.hpp @@ -5,35 +5,20 @@ namespace libdbc { - class exception : public std::exception { - const char * what() const throw() { - return "libdbc exception occurred"; - } - }; - - class validity_error : public exception { - const char * what() const throw() { - return "Invalid DBC file..."; - } - }; - - class header_error : public validity_error { - header_error(const char* line, const char* expected, const char * file) : - line(line), expected(expected), file(file) {} - - const char * what() const throw() { - std::ostringstream os; - os << "Issue with the header line ( " << line << " ) in file ( "; - os << file << " ). Expected to find: " << expected; - return os.str().c_str(); - } - - private: - const char * line; - const char * expected; - const char * file; - }; +class exception : public std::exception { +public: + const char* what() const throw() { + return "libdbc exception occurred"; + } +}; + +class validity_error : public exception { +public: + const char* what() const throw() { + return "Invalid DBC file"; + } +}; } // libdbc -#endif // __ERROR_HPP__ \ No newline at end of file +#endif // __ERROR_HPP__ diff --git a/include/libdbc/message.hpp b/include/libdbc/message.hpp index 37d021b..e903fed 100644 --- a/include/libdbc/message.hpp +++ b/include/libdbc/message.hpp @@ -1,26 +1,54 @@ #ifndef __MESSAGE_HPP__ #define __MESSAGE_HPP__ -#include -#include +#include #include #include +#include +#include namespace libdbc { - struct Message { - uint32_t id; - std::string name; - uint8_t size; - std::string node; - std::vector signals; - - Message() = delete; - explicit Message(uint32_t id, const std::string& name, uint8_t size, const std::string& node); +struct Message { + Message() = delete; + virtual ~Message() = default; + explicit Message(uint32_t id, const std::string& name, uint8_t size, const std::string& node); - virtual bool operator==(const Message& rhs) const; + enum class ParseSignalsStatus { + Success, + ErrorMessageToLong, + ErrorBigEndian, + ErrorUnknownID, + ErrorInvalidConversion, }; - std::ostream& operator<< (std::ostream &out, const Message& msg); + /*! + * \brief parseSignals + * \param data + * \param values + * \return + */ + ParseSignalsStatus parseSignals(const std::vector& data, std::vector& values) const; + + void appendSignal(const Signal& signal); + const std::vector getSignals() const; + uint32_t id() const; + uint8_t size() const; + const std::string& name() const; + void addValueDescription(const std::string& signal_name, const std::vector&); + + virtual bool operator==(const Message& rhs) const; + +private: + uint32_t m_id; + std::string m_name; + uint8_t m_size; + std::string m_node; + std::vector m_signals; + + friend std::ostream& operator<<(std::ostream& os, const Message& dt); +}; + +std::ostream& operator<<(std::ostream& out, const Message& msg); } diff --git a/include/libdbc/signal.hpp b/include/libdbc/signal.hpp index 085994e..c7311cb 100644 --- a/include/libdbc/signal.hpp +++ b/include/libdbc/signal.hpp @@ -2,32 +2,52 @@ #ifndef __SIGNAL_HPP__ #define __SIGNAL_HPP__ +#include +#include #include #include -#include namespace libdbc { - struct Signal { - std::string name; - bool is_multiplexed; - uint32_t start_bit; - uint32_t size; - bool is_bigendian; - bool is_signed; - double factor; - double offset; - double min; - double max; - std::string unit; - std::vector receivers; - - Signal() = delete; - explicit Signal(std::string name, bool is_multiplexed, uint32_t start_bit, uint32_t size, bool is_bigendian, bool is_signed, double factor, double offset, double min, double max, std::string unit, std::vector recievers); - - virtual bool operator==(const Signal& rhs) const; +struct Signal { + struct SignalValueDescriptions { + uint32_t value; + std::string description; }; - std::ostream& operator<< (std::ostream &out, const Signal& sig); + std::string name; + bool is_multiplexed; + uint32_t start_bit; + uint32_t size; + bool is_bigendian; + bool is_signed; + double factor; + double offset; + double min; + double max; + std::string unit; + std::vector receivers; + std::vector svDescriptions; + + Signal() = delete; + virtual ~Signal() = default; + explicit Signal(std::string name, + bool is_multiplexed, + uint32_t start_bit, + uint32_t size, + bool is_bigendian, + bool is_signed, + double factor, + double offset, + double min, + double max, + std::string unit, + std::vector recievers); + + virtual bool operator==(const Signal& rhs) const; + bool operator<(const Signal& rhs) const; +}; + +std::ostream& operator<<(std::ostream& out, const Signal& sig); } diff --git a/include/libdbc/utils/utils.hpp b/include/libdbc/utils/utils.hpp index aadfd8c..21ff540 100644 --- a/include/libdbc/utils/utils.hpp +++ b/include/libdbc/utils/utils.hpp @@ -2,52 +2,49 @@ #ifndef __UTILS_HPP__ #define __UTILS_HPP__ -#include +#include #include #include -#include -#include #include +#include +#include namespace utils { - class StreamHandler { - public: - /** - * This is a safe non line ending specific get_ine function. This is to help with files - * carried over from different systems. i.e Unix file comes to Windows with LF endings - * instead of CRLF. - * - * @param stream [description] - * @param line [description] - * @return [description] - */ - static std::istream & get_line( std::istream & stream, std::string & line ); - - - static std::istream & get_next_non_blank_line( std::istream & stream, std::string & line ); - - static std::istream & skip_to_next_blank_line( std::istream & stream, std::string & line ); - - }; - - class String { - public: - - static std::string trim(const std::string& line); - - template - static void split(const std::string& str, Container& cont, char delim = ' ') { - std::stringstream ss(str); - std::string token; - - while (std::getline(ss, token, delim)) { - cont.push_back(token); - } +class StreamHandler { +public: + /** + * This is a safe non line ending specific get_ine function. This is to help with files + * carried over from different systems. i.e Unix file comes to Windows with LF endings + * instead of CRLF. + * + * @param stream [description] + * @param line [description] + * @return [description] + */ + static std::istream& get_line(std::istream& stream, std::string& line); + + static std::istream& get_next_non_blank_line(std::istream& stream, std::string& line); + + static std::istream& skip_to_next_blank_line(std::istream& stream, std::string& line); +}; + +class String { +public: + static std::string trim(const std::string& line); + + template + static void split(const std::string& str, Container& cont, char delim = ' ') { + std::stringstream ss(str); + std::string token; + + while (std::getline(ss, token, delim)) { + cont.push_back(token); } + } - - }; + static double convert_to_double(const std::string& value, double default_value = 0); +}; } diff --git a/script-requirements.txt b/script-requirements.txt deleted file mode 100644 index 86aed3c..0000000 --- a/script-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Quom==1.2.0 \ No newline at end of file diff --git a/scripts/check_version.py b/scripts/check_version.py new file mode 100755 index 0000000..3881db4 --- /dev/null +++ b/scripts/check_version.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# Used to check the input verion against the input +import re +import argparse + + +def get_cmake_version(cmake_file): + with open(cmake_file, 'r') as f: + contents = f.read() + match = re.search(r'project\(.*VERSION (\d+)\.(\d+)\.(\d+)', contents) + if match: + major, minor, patch = map(int, match.groups()) + return major, minor, patch + return None + + +def validate_semver(version): + pattern = r'^v(\d+)\.(\d+)\.(\d+)$' + match = re.match(pattern, version) + if match: + return tuple(map(int, match.groups())) + else: + return None + + +def compare_versions(input_version, cmake_version): + if input_version > cmake_version: + print("Input version is greater than CMake version.") + exit(1) + elif input_version < cmake_version: + print("Input version is smaller than CMake version.") + exit(1) + else: + print("Input version is equal to CMake version.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check input version against CMake project version") + parser.add_argument("--version", type=str, help="Input version with a 'v' prefix", required=True) + args = parser.parse_args() + + cmake_version = get_cmake_version("CMakeLists.txt") + if cmake_version is None: + print("Failed to retrieve version from CMakeLists.txt.") + exit(1) + else: + input_version = validate_semver(args.version) + if input_version is None: + print("Invalid input version format. Please provide a version in the format 'vX.Y.Z'") + exit(1) + else: + compare_versions(input_version, cmake_version) + + + diff --git a/scripts/create_single_header.sh b/scripts/create_single_header.sh old mode 100644 new mode 100755 index 71ee4fa..a740133 --- a/scripts/create_single_header.sh +++ b/scripts/create_single_header.sh @@ -1 +1,13 @@ -quom -I include -I src src/dbc.cpp build/libdbc.hpp \ No newline at end of file +#!/bin/sh + +set -e + +cargo install cpp-amalgamate + +rm -rf build/single_header/ +mkdir -p build/single_header/libdbc + +source_files=$(find src -name "*.cpp") +include_files=$(find include -name "*.hpp") + +cpp-amalgamate -d include -d build/_deps/fastfloat-src/include -o build/single_header/libdbc/libdbc.hpp ${source_files} ${include_files} diff --git a/scripts/fmt.sh b/scripts/fmt.sh new file mode 100755 index 0000000..9e15b4e --- /dev/null +++ b/scripts/fmt.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Used to format files to the given format using clang format. +# example runs: +# ./scripts/fmt.sh +# ./scripts/fmt.sh format # This will write changes to file +APPLY_FORMAT=${1:-""} + + +# Variable that will hold the name of the clang-format command +FMT="" + +# Some distros just call it clang-format. Others (e.g. Ubuntu) are insistent +# that the version number be part of the command. We prefer clang-format if +# that's present, otherwise we check clang-format-16 +for clangfmt in clang-format{,-16}; do + if which "$clangfmt" &>/dev/null; then + FMT="$clangfmt" + break + fi +done + +# Check if we found a working clang-format +if [ -z "$FMT" ]; then + echo "failed to find clang-format. Please install clang-format version 16 or above" + exit 1 +fi + +files=$(find . \( -path ./build -prune -o -path ./.git -prune -o -path ./third_party -prune \) -o -type f -name "*[h|c]pp" -print) + +if [[ ${APPLY_FORMAT} = "format" ]]; then + ${FMT} -i ${files} +else + ${FMT} --dry-run --Werror ${files} +fi + diff --git a/src/dbc.cpp b/src/dbc.cpp index ee3b173..8b3dac6 100644 --- a/src/dbc.cpp +++ b/src/dbc.cpp @@ -1,128 +1,292 @@ +#include +#include #include #include -#include #include namespace libdbc { - DbcParser::DbcParser() : version(""), nodes(), - version_re("^(VERSION)\\s\"(.*)\""), bit_timing_re("^(BS_:)"), - name_space_re("^(NS_)\\s\\:"), node_re("^(BU_:)\\s((?:[\\w]+?\\s?)*)"), - message_re("^(BO_)\\s(\\d+)\\s(\\w+)\\:\\s(\\d+)\\s(\\w+|Vector__XXX)"), - // NOTE: No multiplex support yet - signal_re("\\s(SG_)\\s(\\w+)\\s\\:\\s(\\d+)\\|(\\d+)\\@(\\d+)(\\+|\\-)\\s\\((\\d+\\.?(\\d+)?)\\,(\\d+\\.?(\\d+)?)\\)\\s\\[(\\d+\\.?(\\d+)?)\\|(\\d+\\.?(\\d+)?)\\]\\s\"(\\w*)\"\\s([\\w\\,]+|Vector__XXX)*") { - +const auto floatPattern = "(-?\\d+\\.?(\\d+)?)"; // Can be negative + +const auto signalIdentifierPattern = "(SG_)"; +const auto namePattern = "(\\w+)"; +const auto bitStartPattern = "(\\d+)"; // Cannot be negative +const auto lengthPattern = "(\\d+)"; // Cannot be negative +const auto byteOrderPattern = "([0-1])"; +const auto signPattern = "(\\+|\\-)"; +const auto scalePattern = "(\\d+\\.?(\\d+)?)"; // Non negative float +const auto offsetPattern = floatPattern; +const auto offsetScalePattern = std::string("\\(") + scalePattern + "\\," + offsetPattern + "\\)"; +const auto minPattern = floatPattern; +const auto maxPattern = floatPattern; +const auto minMaxPattern = std::string("\\[") + minPattern + "\\|" + maxPattern + "\\]"; +const auto unitPattern = "\"(.*)\""; // Random string +const auto receiverPattern = "([\\w\\,]+|Vector__XXX)*"; +const auto whiteSpace = "\\s"; + +enum VALToken { Identifier = 0, CANId, SignalName, Value, Description }; + +struct VALObject { + uint32_t can_id; + std::string signal_name; + std::vector vd; +}; + +static bool parseVal(const std::string& str, VALObject& obj); +bool parseVal(const std::string& str, VALObject& obj) { + obj.signal_name = ""; + obj.vd.clear(); + auto state = Identifier; + const char* a = str.data(); + Signal::SignalValueDescriptions vd; + for (;;) { + switch (state) { + case Identifier: { + if (*a != 'V') + return false; + a++; + if (*a != 'A') + return false; + a++; + if (*a != 'L') + return false; + a++; + if (*a != '_') + return false; + a++; + if (*a != ' ') + return false; + a++; // skip whitespace + state = CANId; + break; + } + case CANId: { + std::string can_id_str; + while (*a >= '0' && *a <= '9') { + can_id_str += *a; + a++; + } + if (can_id_str.empty()) + return false; + obj.can_id = static_cast(std::stoul(can_id_str)); + if (*a != ' ') + return false; + a++; // skip whitespace + state = SignalName; + break; + } + case SignalName: { + if ((*a >= 'a' && *a <= 'z') || (*a >= 'A' && *a <= 'Z') || *a == '_') + obj.signal_name += *a; + else + return false; + a++; + while ((*a >= 'a' && *a <= 'z') || (*a >= 'A' && *a <= 'Z') || *a == '_' || (*a >= '0' && *a <= '9')) { + obj.signal_name += *a; + a++; + } + if (*a != ' ') + return false; + a++; // skip whitespace + state = Value; + break; + } + case Value: { + std::string value_str; + while (*a >= '0' && *a <= '9') { + value_str += *a; + a++; + } + if (*a == ';') { + if (value_str.empty()) + return true; + return false; + } + if (value_str.empty()) + return false; + + if (*a != ' ') + return false; + a++; // skip whitespace + vd.value = (uint32_t)std::stoul(value_str); + state = Description; + break; + } + case Description: { + std::string desc; + if (*a != '"') + return false; + a++; + while (*a != '"' && *a != 0) { + desc += *a; + a++; + } + if (*a == 0) + return false; + a++; + if (*a != ' ') + return false; + a++; // skip whitespace + + vd.description = desc; + obj.vd.push_back(vd); + + state = Value; + break; + } + } } + return false; +} - void DbcParser::parse_file(const std::string& file) { - std::ifstream s(file.c_str()); - std::string line; - std::vector lines; +DbcParser::DbcParser() + : version("") + , nodes() + , version_re("^(VERSION)\\s\"(.*)\"") + , bit_timing_re("^(BS_:)") + , name_space_re("^(NS_)\\s\\:") + , node_re("^(BU_:)\\s((?:[\\w]+?\\s?)*)") + , message_re("^(BO_)\\s(\\d+)\\s(\\w+)\\:\\s(\\d+)\\s(\\w+|Vector__XXX)") + , + // NOTE: No multiplex support yet + signal_re(std::string("^") + whiteSpace + signalIdentifierPattern + whiteSpace + namePattern + whiteSpace + "\\:" + whiteSpace + bitStartPattern + "\\|" + + lengthPattern + "\\@" + byteOrderPattern + signPattern + whiteSpace + offsetScalePattern + whiteSpace + minMaxPattern + whiteSpace + unitPattern + + whiteSpace + receiverPattern) { +} - parse_dbc_header(s); +void DbcParser::parse_file(const std::string& file) { + std::ifstream s(file.c_str()); + std::string line; + std::vector lines; - parse_dbc_nodes(s); + messages.clear(); - while(!s.eof()) { - utils::StreamHandler::get_next_non_blank_line( s, line ); - lines.push_back(line); - } + parse_dbc_header(s); - parse_dbc_messages(lines); + parse_dbc_nodes(s); + while (!s.eof()) { + utils::StreamHandler::get_next_non_blank_line(s, line); + lines.push_back(line); } - std::string DbcParser::get_version() const { - return version; - } + parse_dbc_messages(lines); +} - std::vector DbcParser::get_nodes() const { - return nodes; - } +std::string DbcParser::get_version() const { + return version; +} - std::vector DbcParser::get_messages() const { - return messages; +std::vector DbcParser::get_nodes() const { + return nodes; +} + +std::vector DbcParser::get_messages() const { + return messages; +} + +Message::ParseSignalsStatus DbcParser::parseMessage(const uint32_t id, const std::vector& data, std::vector& out_values) { + for (const auto& message : messages) { + if (message.id() == id) + return message.parseSignals(data, out_values); } + return Message::ParseSignalsStatus::ErrorUnknownID; +} +void DbcParser::parse_dbc_header(std::istream& file_stream) { + std::string line; + std::smatch match; - void DbcParser::parse_dbc_header(std::istream& file_stream) { - std::string line; - std::smatch match; + utils::StreamHandler::get_line(file_stream, line); - utils::StreamHandler::get_line(file_stream, line); + if (!std::regex_search(line, match, version_re)) { + throw validity_error(); + } - if(!std::regex_search(line, match, version_re)) { - throw validity_error(); - } + version = match.str(2); + + utils::StreamHandler::get_next_non_blank_line(file_stream, line); - version = match.str(2); + utils::StreamHandler::skip_to_next_blank_line(file_stream, line); - utils::StreamHandler::get_next_non_blank_line( file_stream, line ); + utils::StreamHandler::get_next_non_blank_line(file_stream, line); - utils::StreamHandler::skip_to_next_blank_line( file_stream, line ); + if (!std::regex_search(line, match, bit_timing_re)) + throw validity_error(); +} - utils::StreamHandler::get_next_non_blank_line( file_stream, line ); +void DbcParser::parse_dbc_nodes(std::istream& file_stream) { + std::string line; + std::smatch match; - if(!std::regex_search(line, match, bit_timing_re)) - throw validity_error(); + utils::StreamHandler::get_next_non_blank_line(file_stream, line); + if (!std::regex_search(line, match, node_re)) + throw validity_error(); + + if (match.length() > 2) { + std::string n = match.str(2); + utils::String::split(n, nodes); } +} - void DbcParser::parse_dbc_nodes(std::istream& file_stream) { - std::string line; - std::smatch match; +void DbcParser::parse_dbc_messages(const std::vector& lines) { + std::smatch match; - utils::StreamHandler::get_next_non_blank_line( file_stream, line ); + std::vector sv; - if(!std::regex_search(line, match, node_re)) - throw validity_error(); + VALObject obj{}; + for (const auto& line : lines) { + if (std::regex_search(line, match, message_re)) { + uint32_t id = static_cast(std::stoul(match.str(2))); + std::string name = match.str(3); + uint8_t size = static_cast(std::stoul(match.str(4))); + std::string node = match.str(5); - if(match.length() > 2) { - std::string n = match.str(2); - utils::String::split(n, nodes); + Message msg(id, name, size, node); + + messages.push_back(msg); + continue; } - } + if (std::regex_search(line, match, signal_re)) { + std::string name = match.str(2); + bool is_multiplexed = false; // No support yet + uint32_t start_bit = static_cast(std::stoul(match.str(3))); + uint32_t size = static_cast(std::stoul(match.str(4))); + bool is_bigendian = (std::stoul(match.str(5)) == 0); + bool is_signed = (match.str(6) == "-"); - void DbcParser::parse_dbc_messages(const std::vector& lines) { - std::smatch match; + double factor = utils::String::convert_to_double(match.str(7).data()); + double offset = utils::String::convert_to_double(match.str(9).data()); + double min = utils::String::convert_to_double(match.str(11).data()); + double max = utils::String::convert_to_double(match.str(13).data()); - for(const auto &line : lines) { - if(std::regex_search(line, match, message_re)) { - uint32_t id = std::stoul(match.str(2)); - std::string name = match.str(3); - uint8_t size = std::stoul(match.str(4)); - std::string node = match.str(5); + std::string unit = match.str(15); - Message msg(id, name, size, node); + std::vector receivers; + utils::String::split(match.str(16), receivers, ','); - messages.push_back(msg); - } - - if(std::regex_search(line, match, signal_re)) { - std::string name = match.str(2); - bool is_multiplexed = false; // No support yet - uint32_t start_bit = std::stoul(match.str(3)); - uint32_t size = std::stoul(match.str(4)); - bool is_bigendian = (std::stoul(match.str(5)) == 1); - bool is_signed = (match.str(6) == "-"); - // Alternate groups because a group is for the decimal portion - double factor = std::stod(match.str(7)); - double offset = std::stod(match.str(9)); - double min = std::stod(match.str(11)); - double max = std::stod(match.str(13)); - std::string unit = match.str(15); - - std::vector receivers; - utils::String::split(match.str(16), receivers, ','); - - Signal sig(name, is_multiplexed, start_bit, size, is_bigendian, is_signed, factor, offset, min, max, unit, receivers); - messages.back().signals.push_back(sig); - } + Signal sig(name, is_multiplexed, start_bit, size, is_bigendian, is_signed, factor, offset, min, max, unit, receivers); + messages.back().appendSignal(sig); + continue; } + if (parseVal(line, obj)) { + sv.push_back(obj); + continue; + } } + for (const auto& signal : sv) { + for (auto& msg : messages) { + if (msg.id() == signal.can_id) { + msg.addValueDescription(signal.signal_name, signal.vd); + break; + } + } + } +} } diff --git a/src/message.cpp b/src/message.cpp index 35956f6..443526a 100644 --- a/src/message.cpp +++ b/src/message.cpp @@ -1,19 +1,112 @@ +#include +#include #include namespace libdbc { - Message::Message(uint32_t id, const std::string& name, uint8_t size, const std::string& node) : - id(id), name(name), size(size), node(node) {} +Message::Message(uint32_t id, const std::string& name, uint8_t size, const std::string& node) + : m_id(id) + , m_name(name) + , m_size(size) + , m_node(node) { +} - bool Message::operator==(const Message& rhs) const { - return (this->id == rhs.id) && (this->name == rhs.name) && - (this->size == rhs.size) && (this->node == rhs.node); +bool Message::operator==(const Message& rhs) const { + return (m_id == rhs.id()) && (m_name == rhs.m_name) && (m_size == rhs.m_size) && (m_node == rhs.m_node); +} + +Message::ParseSignalsStatus Message::parseSignals(const std::vector& data, std::vector& values) const { + auto size = data.size(); + if (size > 8) + return ParseSignalsStatus::ErrorMessageToLong; // not supported yet + + uint64_t data_little_endian = 0; + uint64_t data_big_endian = 0; + for (size_t i = 0; i < size; i++) { + data_little_endian |= ((uint64_t)data[i]) << i * 8; + data_big_endian = (data_big_endian << 8) | (uint64_t)data[i]; + } + + // TODO: does this also work on a big endian machine? + + const auto len = size * 8; + uint64_t v = 0; + for (const auto& signal : m_signals) { + if (signal.is_bigendian) { + uint32_t start_bit = 8 * (signal.start_bit / 8) + (7 - (signal.start_bit % 8)); // Calculation taken from python CAN + v = data_big_endian << start_bit; + v = v >> (len - signal.size); + } else + v = data_little_endian >> signal.start_bit; + + if (signal.is_signed && signal.size > 1) { + switch (signal.size) { + case 8: + values.push_back(static_cast(v) * signal.factor + signal.offset); + break; + case 16: + values.push_back(static_cast(v) * signal.factor + signal.offset); + break; + case 32: + values.push_back(static_cast(v) * signal.factor + signal.offset); + break; + case 64: + values.push_back(static_cast(v) * signal.factor + signal.offset); + break; + default: { + // 2 complement -> decimal + const int negative = (v & (1ull << (signal.size - 1))) != 0; + int64_t nativeInt; + if (negative) + nativeInt = static_cast(v | ~((1ull << signal.size) - 1)); // invert all bits above signal.size + else + nativeInt = static_cast(v & ((1ull << signal.size) - 1)); // masking + values.push_back(static_cast(nativeInt) * signal.factor + signal.offset); + break; + } + } + } else { + // use only the relevant bits + v = v & ((1 << signal.size) - 1); // masking + values.push_back(static_cast(v) * signal.factor + signal.offset); + } } + return ParseSignalsStatus::Success; +} - std::ostream& operator<< (std::ostream &out, const Message& msg) { - out << "Message: {id: " << msg.id << ", "; - out << "name: " << msg.name << ", "; - out << "size: " << msg.size << ", "; - out << "node: " << msg.node << "}"; - return out; +void Message::appendSignal(const Signal& signal) { + m_signals.push_back(signal); +} + +const std::vector Message::getSignals() const { + return m_signals; +} + +uint32_t Message::id() const { + return m_id; +} + +uint8_t Message::size() const { + return m_size; +} + +const std::string& Message::name() const { + return m_name; +} + +void Message::addValueDescription(const std::string& signal_name, const std::vector& vd) { + for (auto& s : m_signals) { + if (s.name.compare(signal_name) == 0) { + s.svDescriptions = vd; + return; + } } -} \ No newline at end of file +} + +std::ostream& operator<<(std::ostream& out, const Message& msg) { + out << "Message: {id: " << msg.id() << ", "; + out << "name: " << msg.m_name << ", "; + out << "size: " << msg.m_size << ", "; + out << "node: " << msg.m_node << "}"; + return out; +} +} diff --git a/src/signal.cpp b/src/signal.cpp index 971cfd0..a2ad53d 100644 --- a/src/signal.cpp +++ b/src/signal.cpp @@ -1,30 +1,54 @@ #include namespace libdbc { - Signal::Signal(std::string name, bool is_multiplexed, uint32_t start_bit, uint32_t size, bool is_bigendian, bool is_signed, double factor, double offset, double min, double max, std::string unit, std::vector receivers) : - name(name), is_multiplexed(is_multiplexed), start_bit(start_bit), size(size), is_bigendian(is_bigendian), is_signed(is_signed), offset(offset), min(min), max(max), unit(unit), receivers(receivers) {} +Signal::Signal(std::string name, + bool is_multiplexed, + uint32_t start_bit, + uint32_t size, + bool is_bigendian, + bool is_signed, + double factor, + double offset, + double min, + double max, + std::string unit, + std::vector receivers) + : name(name) + , is_multiplexed(is_multiplexed) + , start_bit(start_bit) + , size(size) + , is_bigendian(is_bigendian) + , is_signed(is_signed) + , factor(factor) + , offset(offset) + , min(min) + , max(max) + , unit(unit) + , receivers(receivers) { +} - bool Signal::operator==(const Signal& rhs) const { - return (this->name == rhs.name) && (this->is_multiplexed == rhs.is_multiplexed) && - (this->start_bit == rhs.start_bit) && (this->size == rhs.size) && - (this->is_bigendian == rhs.is_bigendian) && (this->is_signed == rhs.is_signed) && - (this->offset == rhs.offset) && (this->min == rhs.min) && (this->max == rhs.max) && - (this->unit == rhs.unit) && (this->receivers == rhs.receivers); - } +bool Signal::operator==(const Signal& rhs) const { + return (this->name == rhs.name) && (this->is_multiplexed == rhs.is_multiplexed) && (this->start_bit == rhs.start_bit) && (this->size == rhs.size) + && (this->is_bigendian == rhs.is_bigendian) && (this->is_signed == rhs.is_signed) && (this->offset == rhs.offset) && (this->min == rhs.min) + && (this->max == rhs.max) && (this->unit == rhs.unit) && (this->receivers == rhs.receivers); +} +bool Signal::operator<(const Signal& rhs) const { + return start_bit < rhs.start_bit; +} - std::ostream& operator<< (std::ostream &out, const Signal& sig) { - out << "Signal {name: " << sig.name << ", "; - out << "Multiplexed: " << (sig.is_multiplexed ? "True" : "False") << ", "; - out << "Start bit: " << sig.start_bit << ", "; - out << "Size: " << sig.size << ", "; - out << "Endianness: " << (sig.is_bigendian ? "Big endian" : "Little endian") << ", "; - out << "Value Type: " << (sig.is_signed ? "Signed" : "Unsigned") << ", "; - out << "Min: " << sig.min << ", Max: " << sig.max << ", "; - out << "Unit: (" << sig.unit << "), "; - out << "receivers: "; - for(const auto &r : sig.receivers) - out << r; - return out << "}"; - } -} \ No newline at end of file +std::ostream& operator<<(std::ostream& out, const Signal& sig) { + out << "Signal {name: " << sig.name << ", "; + out << "Multiplexed: " << (sig.is_multiplexed ? "True" : "False") << ", "; + out << "Start bit: " << sig.start_bit << ", "; + out << "Size: " << sig.size << ", "; + out << "Endianness: " << (sig.is_bigendian ? "Big endian" : "Little endian") << ", "; + out << "Value Type: " << (sig.is_signed ? "Signed" : "Unsigned") << ", "; + out << "Min: " << sig.min << ", Max: " << sig.max << ", "; + out << "Unit: (" << sig.unit << "), "; + out << "receivers: "; + for (const auto& r : sig.receivers) + out << r; + return out << "}"; +} +} diff --git a/src/utils.cpp b/src/utils.cpp index 773b802..35cff0c 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,75 +1,82 @@ +#include +#include +#include #include #include namespace utils { - std::istream & StreamHandler::get_line( std::istream & stream, std::string & line ) { - std::string newline; +std::istream& StreamHandler::get_line(std::istream& stream, std::string& line) { + std::string newline; - std::getline( stream, newline ); + std::getline(stream, newline); - // Windows CRLF (\r\n) - if ( newline.size() && newline[newline.size()-1] == '\r' ) { - line = newline.substr( 0, newline.size() - 1 ); + // Windows CRLF (\r\n) + if (newline.size() && newline[newline.size() - 1] == '\r') { + line = newline.substr(0, newline.size() - 1); // MacOS LF (\r) - } else if (newline.size() && newline[newline.size()] == '\r') { - line = newline.replace(newline.size(), 1, "\n"); - } else { - line = newline; - } - - return stream; + } else if (newline.size() && newline[newline.size()] == '\r') { + line = newline.replace(newline.size(), 1, "\n"); + } else { + line = newline; } + return stream; +} - std::istream & StreamHandler::get_next_non_blank_line( std::istream & stream, std::string & line ) { - bool is_blank = true; +std::istream& StreamHandler::get_next_non_blank_line(std::istream& stream, std::string& line) { + bool is_blank = true; - const std::regex whitespace_re("\\s*(.*)"); - std::smatch match; + const std::regex whitespace_re("\\s*(.*)"); + std::smatch match; - while(is_blank) { - utils::StreamHandler::get_line(stream, line); + while (is_blank) { + utils::StreamHandler::get_line(stream, line); - std::regex_search(line, match, whitespace_re); + std::regex_search(line, match, whitespace_re); - if((!line.empty() && !match.empty()) || (stream.eof())){ - if((match.length(1) > 0) || (stream.eof())){ - is_blank = false; - } + if ((!line.empty() && !match.empty()) || (stream.eof())) { + if ((match.length(1) > 0) || (stream.eof())) { + is_blank = false; } } - - return stream; } - std::istream & StreamHandler::skip_to_next_blank_line( std::istream & stream, std::string & line ) { - bool line_is_empty = false; + return stream; +} - const std::regex whitespace_re("\\s*(.*)"); - std::smatch match; +std::istream& StreamHandler::skip_to_next_blank_line(std::istream& stream, std::string& line) { + bool line_is_empty = false; - while(!line_is_empty) { - utils::StreamHandler::get_line(stream, line); + const std::regex whitespace_re("\\s*(.*)"); + std::smatch match; - std::regex_search(line, match, whitespace_re); + while (!line_is_empty) { + utils::StreamHandler::get_line(stream, line); - if((match.length(1) == 0) || (stream.eof())){ - line_is_empty = true; - } - } + std::regex_search(line, match, whitespace_re); - return stream; + if ((match.length(1) == 0) || (stream.eof())) { + line_is_empty = true; + } } + return stream; +} +std::string String::trim(const std::string& line) { + const char* WhiteSpace = " \t\v\r\n"; + std::size_t start = line.find_first_not_of(WhiteSpace); + std::size_t end = line.find_last_not_of(WhiteSpace); + return start == end ? std::string() : line.substr(start, end - start + 1); +} - std::string String::trim(const std::string& line) { - const char* WhiteSpace = " \t\v\r\n"; - std::size_t start = line.find_first_not_of(WhiteSpace); - std::size_t end = line.find_last_not_of(WhiteSpace); - return start == end ? std::string() : line.substr(start, end - start + 1); - } +double String::convert_to_double(const std::string& value, double default_value) { + double converted_value = default_value; + fast_float::from_chars(value.data(), value.data() + value.size(), converted_value); + // converted_value = std::stod(value); + return converted_value; +} -} // Namespace Utils \ No newline at end of file +} // Namespace Utils diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3d2cbe2..fc8cf97 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,14 +1,73 @@ -project(tests VERSION 0.1.0) +enable_testing() -list(APPEND TEST_SOURCES main.cpp - test_dbc.cpp - test_utils.cpp) +# Download and build Catch2 test framework +Include(FetchContent) +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.5.2 +) +FetchContent_MakeAvailable(Catch2) +include(Catch) -include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/Catch2/single_include) +# Need filesystem for testing +set(CMAKE_CXX_STANDARD 17) -add_executable(tests ${TEST_SOURCES} ${SOURCE}) +if (MSVC) + add_compile_options(/W4 /WX) +else() + add_compile_options(-Wall -Wextra -Wpedantic -Werror) +endif() -add_custom_target(test - COMMAND ${PROJECT_NAME} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - DEPENDS ${PROJECT_NAME}) \ No newline at end of file +# Code coverage compiler specific +if (GCC) + add_compile_options(--coverage) +endif() + + +add_executable(dbcParserTests + test_dbc.cpp + test_utils.cpp + test_parse_message.cpp + testing_utils/common.cpp +) + +target_compile_definitions(dbcParserTests PRIVATE TESTDBCFILES_PATH="${CMAKE_CURRENT_SOURCE_DIR}/dbcs") +target_link_libraries(dbcParserTests PRIVATE dbc Catch2::Catch2WithMain) +target_include_directories(dbcParserTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +catch_discover_tests(dbcParserTests) + +# We want a seperate binary for this test. We setup global locals which mess with all of the testing. +# Opting for a sperate test running so we don't conflict +if(DBC_TEST_LOCALE_INDEPENDENCE) + add_executable(dbcLocaleTests + locale_testing/test_locale_main.cpp + testing_utils/common.cpp + ) + + target_compile_definitions(dbcLocaleTests PRIVATE TESTDBCFILES_PATH="${CMAKE_CURRENT_SOURCE_DIR}/dbcs") + target_link_libraries(dbcLocaleTests PRIVATE dbc Catch2::Catch2WithMain) + target_include_directories(dbcLocaleTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +catch_discover_tests(dbcLocaleTests) +else() + message(WARNING "Locale independent testing is turned off!") +endif() + +# Again another test binary to ensure we aren't including our other headers. +# It should compile and run on one include +if(DBC_GENERATE_SINGLE_HEADER) + add_executable(dbcSingleHeaderTest + single_header_testing/test_single_header.cpp + testing_utils/common.cpp + ) + + target_compile_definitions(dbcSingleHeaderTest PRIVATE TESTDBCFILES_PATH="${CMAKE_CURRENT_SOURCE_DIR}/dbcs") + target_link_libraries(dbcSingleHeaderTest PRIVATE Catch2::Catch2WithMain) + target_include_directories(dbcSingleHeaderTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/single_header/) + + catch_discover_tests(dbcSingleHeaderTest) + + add_dependencies(dbcSingleHeaderTest single_header) +endif() diff --git a/test/defines.hpp b/test/defines.hpp deleted file mode 100644 index 7095570..0000000 --- a/test/defines.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#include - -// Correctly formated files -static const std::string COMPLEX_DBC_FILE = "./dbcs/Complex.dbc"; -static const std::string SIMPLE_DBC_FILE = "./dbcs/Simple.dbc"; - -// Files with Errors -static const std::string MISSING_NEW_SYMBOLS_DBC_FILE = "./dbcs/MissingNewSymbols.dbc"; -static const std::string MISSING_VERSION_DBC_FILE = "./dbcs/MissingVersion.dbc"; -static const std::string MISSING_BIT_TIMING_DBC_FILE = "./dbcs/MissingBitTiming.dbc"; -static const std::string TEXT_FILE = "./dbcs/TextFile.txt"; diff --git a/test/locale_testing/test_locale_main.cpp b/test/locale_testing/test_locale_main.cpp new file mode 100644 index 0000000..7336545 --- /dev/null +++ b/test/locale_testing/test_locale_main.cpp @@ -0,0 +1,80 @@ +#include "testing_utils/common.hpp" +#include "testing_utils/defines.hpp" +#include +#include + +#include +#include +#include +#include + +class testRunListener : public Catch::EventListenerBase { +public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override { + // Mac OS uses global and c++ standard uses the std. Using this to remove ambiguity between the two. + prev_loc = ::setlocale(LC_ALL, nullptr); + // Set the locale to something that has , instead of . for floats + std::locale::global(std::locale("de_DE.UTF-8")); + } + + void testCaseEnded(Catch::TestCaseStats const&) override { + // Restore the old locale + std::locale::global(std::locale(prev_loc)); + } + +private: + std::string prev_loc; +}; + +CATCH_REGISTER_LISTENER(testRunListener) + +TEST_CASE("Should parse doubld string locale independently") { + REQUIRE(Catch::Approx(utils::String::convert_to_double("6.82")) == 6.82); +} + +TEST_CASE("Should process message with floats locale indpendently") { + 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@0- (0.1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Sig3 : 23|16@0- (10,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Sig4 : 7|16@0- (1,-10) [0|32767] "" Vector__XXX)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename); + + REQUIRE(parser.get_messages().size() == 1); + REQUIRE(parser.get_messages().at(0).name() == "MSG1"); + REQUIRE(parser.get_messages().at(0).getSignals().size() == 4); + + SECTION("Evaluating first message") { + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.factor == 0.1); + REQUIRE(signal.offset == 0); + REQUIRE(signal.min == -3276.8); + REQUIRE(signal.max == -3276.7); + } + SECTION("Evaluating second message") { + const auto signal = parser.get_messages().at(0).getSignals().at(1); + REQUIRE(signal.factor == 0.1); + REQUIRE(signal.offset == 0); + REQUIRE(signal.min == -3276.8); + REQUIRE(signal.max == -3276.7); + } + SECTION("Evaluating third message") { + const auto signal = parser.get_messages().at(0).getSignals().at(2); + REQUIRE(signal.factor == 10); + REQUIRE(signal.offset == 0); + REQUIRE(signal.min == -3276.8); + REQUIRE(signal.max == -3276.7); + } + SECTION("Evaluating fourth message") { + const auto signal = parser.get_messages().at(0).getSignals().at(3); + REQUIRE(signal.factor == 1); + REQUIRE(signal.offset == -10); + REQUIRE(signal.min == 0); + REQUIRE(signal.max == 32767); + } +} diff --git a/test/main.cpp b/test/main.cpp deleted file mode 100644 index afeca98..0000000 --- a/test/main.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file -#include diff --git a/test/single_header_testing/test_single_header.cpp b/test/single_header_testing/test_single_header.cpp new file mode 100644 index 0000000..d61dac6 --- /dev/null +++ b/test/single_header_testing/test_single_header.cpp @@ -0,0 +1,56 @@ +#include "testing_utils/common.hpp" +#include "testing_utils/defines.hpp" +#include + +#include +#include +#include + +TEST_CASE("Testing dbc file loading", "[fileio]") { + auto parser = std::unique_ptr(new libdbc::DbcParser()); + + SECTION("Loading a single simple dbc file", "[dbc]") { + std::vector nodes = {"DBG", "DRIVER", "IO", "MOTOR", "SENSOR"}; + + libdbc::Message msg(500, "IO_DEBUG", 4, "IO"); + + std::vector receivers{"DBG"}; + libdbc::Signal sig("IO_DEBUG_test_unsigned", false, 0, 8, false, false, 1, 0, 0, 0, "", receivers); + msg.appendSignal(sig); + + std::vector msgs = {msg}; + + parser->parse_file(SIMPLE_DBC_FILE); + + REQUIRE(parser->get_version() == "1.0.0"); + + REQUIRE(parser->get_nodes() == nodes); + + REQUIRE(parser->get_messages() == msgs); + + REQUIRE(parser->get_messages().front().getSignals() == msg.getSignals()); + } +} + +TEST_CASE("Testing big endian, little endian") { + 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()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename.c_str()); + + 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).getSignals().size() == 2); + { + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.is_bigendian == true); + } + { + const auto signal = parser.get_messages().at(0).getSignals().at(1); + REQUIRE(signal.is_bigendian == false); + } +} diff --git a/test/test_dbc.cpp b/test/test_dbc.cpp index 55c4a14..508df5c 100644 --- a/test/test_dbc.cpp +++ b/test/test_dbc.cpp @@ -1,6 +1,10 @@ -#include -#include "defines.hpp" +#include "testing_utils/common.hpp" +#include "testing_utils/defines.hpp" +#include +#include +#include #include +#include TEST_CASE("Testing dbc file loading error issues", "[fileio][error]") { auto parser = std::unique_ptr(new libdbc::DbcParser()); @@ -22,6 +26,14 @@ TEST_CASE("Testing dbc file loading error issues", "[fileio][error]") { // very well standardized for now we ignore this type of error. CHECK_NOTHROW(parser->parse_file(MISSING_NEW_SYMBOLS_DBC_FILE)); } + + SECTION("Verify that what() method is accessible for all exceptions", "[error]") { + auto generic_error = libdbc::exception(); + REQUIRE(std::string{generic_error.what()} == "libdbc exception occurred"); + + auto validity_check = libdbc::validity_error(); + REQUIRE(std::string{validity_check.what()} == "Invalid DBC file"); + } } TEST_CASE("Testing dbc file loading", "[fileio]") { @@ -33,8 +45,8 @@ TEST_CASE("Testing dbc file loading", "[fileio]") { libdbc::Message msg(500, "IO_DEBUG", 4, "IO"); std::vector receivers{"DBG"}; - libdbc::Signal sig("IO_DEBUG_test_unsigned", false, 0, 8, true, false, 1, 0, 0, 0, "", receivers); - msg.signals.push_back(sig); + libdbc::Signal sig("IO_DEBUG_test_unsigned", false, 0, 8, false, false, 1, 0, 0, 0, "", receivers); + msg.appendSignal(sig); std::vector msgs = {msg}; @@ -46,7 +58,183 @@ TEST_CASE("Testing dbc file loading", "[fileio]") { REQUIRE(parser->get_messages() == msgs); - REQUIRE(parser->get_messages().front().signals == msg.signals); + REQUIRE(parser->get_messages().front().getSignals() == msg.getSignals()); + } +} + +TEST_CASE("Testing big endian, little endian") { + 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()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename.c_str()); + + 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).getSignals().size() == 2); + { + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.is_bigendian == true); + } + { + const auto signal = parser.get_messages().at(0).getSignals().at(1); + REQUIRE(signal.is_bigendian == false); + } +} + +TEST_CASE("Testing negative values") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 234 MSG1: 58 Vector__XXX + SG_ Sig1 : 55|16@0- (0.1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Sig2 : 39|16@0- (0.1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Sig3 : 23|16@0- (10,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Sig4 : 7|16@0- (1,-10) [0|32767] "" Vector__XXX)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename.c_str()); + + REQUIRE(parser.get_messages().size() == 1); + REQUIRE(parser.get_messages().at(0).name() == "MSG1"); + REQUIRE(parser.get_messages().at(0).size() == 58); + REQUIRE(parser.get_messages().at(0).getSignals().size() == 4); + + SECTION("Evaluating first message") { + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.factor == 0.1); + REQUIRE(signal.offset == 0); + REQUIRE(signal.min == -3276.8); + REQUIRE(signal.max == -3276.7); + } + SECTION("Evaluating second message") { + const auto signal = parser.get_messages().at(0).getSignals().at(1); + REQUIRE(signal.factor == 0.1); + REQUIRE(signal.offset == 0); + REQUIRE(signal.min == -3276.8); + REQUIRE(signal.max == -3276.7); + } + SECTION("Evaluating third message") { + const auto signal = parser.get_messages().at(0).getSignals().at(2); + REQUIRE(signal.factor == 10); + REQUIRE(signal.offset == 0); + REQUIRE(signal.min == -3276.8); + REQUIRE(signal.max == -3276.7); + } + SECTION("Evaluating fourth message") { + const auto signal = parser.get_messages().at(0).getSignals().at(3); + REQUIRE(signal.factor == 1); + REQUIRE(signal.offset == -10); + REQUIRE(signal.min == 0); + REQUIRE(signal.max == 32767); + } +} + +TEST_CASE("Special characters in unit") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 234 MSG1: 255 Vector__XXX + SG_ Speed : 0|8@1+ (1,0) [0|204] "Km/h" DEVICE1,DEVICE2,DEVICE3)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename.c_str()); + + REQUIRE(parser.get_messages().size() == 1); + REQUIRE(parser.get_messages().at(0).name() == "MSG1"); + REQUIRE(parser.get_messages().at(0).size() == 255); + REQUIRE(parser.get_messages().at(0).getSignals().size() == 1); + SECTION("Checking that signal with special characters as unit is parsed correctly") { + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.unit.compare("Km/h") == 0); } +} -} \ No newline at end of file +TEST_CASE("Signal Value Description") { + 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()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename.c_str()); + + REQUIRE(parser.get_messages().size() == 1); + REQUIRE(parser.get_messages().at(0).name() == "MSG1"); + REQUIRE(parser.get_messages().at(0).getSignals().size() == 2); + + REQUIRE(parser.get_messages().at(0).getSignals().at(0).svDescriptions.size() == 3); + REQUIRE(parser.get_messages().at(0).getSignals().at(1).svDescriptions.size() == 0); + + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.svDescriptions.at(0).value == 123); + REQUIRE(signal.svDescriptions.at(0).description == "Description 1"); + REQUIRE(signal.svDescriptions.at(1).value == 0); + REQUIRE(signal.svDescriptions.at(1).description == "Description 2"); + REQUIRE(signal.svDescriptions.at(2).value == 90903489); + REQUIRE(signal.svDescriptions.at(2).description == "Big value and special characters &$§())!"); +} + +TEST_CASE("Signal Value Description Extended CAN id") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 3221225472 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_ 3221225472 State1 123 "Description 1" 0 "Description 2" 4000000000 "Big value and special characters &$§())!" ;)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + auto parser = libdbc::DbcParser(); + parser.parse_file(filename.c_str()); + + REQUIRE(parser.get_messages().size() == 1); + REQUIRE(parser.get_messages().at(0).name() == "MSG1"); + REQUIRE(parser.get_messages().at(0).getSignals().size() == 2); + + REQUIRE(parser.get_messages().at(0).getSignals().at(0).svDescriptions.size() == 3); + REQUIRE(parser.get_messages().at(0).getSignals().at(1).svDescriptions.size() == 0); + + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.svDescriptions.at(0).value == 123); + REQUIRE(signal.svDescriptions.at(0).description == "Description 1"); + REQUIRE(signal.svDescriptions.at(1).value == 0); + REQUIRE(signal.svDescriptions.at(1).description == "Description 2"); + REQUIRE(signal.svDescriptions.at(2).value == 4000000000); + REQUIRE(signal.svDescriptions.at(2).description == "Big value and special characters &$§())!"); +} + +TEST_CASE("Signal Value Multiple VAL_") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 3221225472 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" +BO_ 123 MSG2: 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_ 3221225472 State1 123 "Description 1" 0 "Description 2" ; +VAL_ 123 State1 123 "Description 3" 0 "Description 4" ;)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + auto parser = libdbc::DbcParser(); + 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"); + + REQUIRE(parser.get_messages().at(0).getSignals().size() == 2); + + REQUIRE(parser.get_messages().at(0).getSignals().at(0).svDescriptions.size() == 2); + REQUIRE(parser.get_messages().at(0).getSignals().at(1).svDescriptions.size() == 0); + REQUIRE(parser.get_messages().at(1).getSignals().at(0).svDescriptions.size() == 2); + REQUIRE(parser.get_messages().at(1).getSignals().at(1).svDescriptions.size() == 0); + + const auto signal = parser.get_messages().at(0).getSignals().at(0); + REQUIRE(signal.svDescriptions.at(0).value == 123); + REQUIRE(signal.svDescriptions.at(0).description == "Description 1"); + REQUIRE(signal.svDescriptions.at(1).value == 0); + REQUIRE(signal.svDescriptions.at(1).description == "Description 2"); + + const auto signal2 = parser.get_messages().at(1).getSignals().at(0); + REQUIRE(signal2.svDescriptions.at(0).value == 123); + REQUIRE(signal2.svDescriptions.at(0).description == "Description 3"); + REQUIRE(signal2.svDescriptions.at(1).value == 0); + REQUIRE(signal2.svDescriptions.at(1).description == "Description 4"); +} diff --git a/test/test_parse_message.cpp b/test/test_parse_message.cpp new file mode 100644 index 0000000..3c3c301 --- /dev/null +++ b/test/test_parse_message.cpp @@ -0,0 +1,175 @@ +#include +#include +#include + +#include + +#include "testing_utils/common.hpp" +#include "testing_utils/defines.hpp" + +// Testing of parsing messages + +TEST_CASE("Parse Message Unknown ID") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 234 MSG1: 8 Vector__XXX + SG_ Msg1Sig1 : 0|8@0+ (1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ MsgSig2 : 8|8@0+ (1,0) [-3276.8|-3276.7] "C" Vector__XXX +BO_ 123 MSG2: 8 Vector__XXX + SG_ Msg2Sig1 : 0|8@0+ (1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Msg2Sig1 : 8|8@0+ (1,0) [-3276.8|-3276.7] "C" Vector__XXX +)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + libdbc::DbcParser parser; + parser.parse_file(filename.c_str()); + + SECTION("Evaluating unknown` message id") { + std::vector out_values; + CHECK(parser.parseMessage(578, std::vector({0xFF, 0xA2}), out_values) == libdbc::Message::ParseSignalsStatus::ErrorUnknownID); + } +} + +TEST_CASE("Parse Message Big Number not aligned little endian") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 337 STATUS: 8 Vector__XXX + SG_ Value6 : 27|3@1+ (1,0) [0|7] "" Vector__XXX + SG_ Value5 : 16|11@1+ (0.1,-102) [-102|102] "%" Vector__XXX + SG_ Value2 : 8|2@1+ (1,0) [0|2] "" Vector__XXX + SG_ Value3 : 10|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ Value7 : 30|2@1+ (1,0) [0|3] "" Vector__XXX + SG_ Value4 : 11|4@1+ (1,0) [0|3] "" Vector__XXX + SG_ Value1 : 0|8@1+ (1,0) [0|204] "Km/h" Vector__XXX +)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + libdbc::DbcParser parser; + parser.parse_file(filename); + + SECTION("Evaluating first message") { + std::vector out_values; + CHECK(parser.parseMessage(337, std::vector({0, 4, 252, 19, 0, 0, 0, 0}), out_values) == libdbc::Message::ParseSignalsStatus::Success); + std::vector refData{2, 0, 0, 1, 0, 0, 0}; + CHECK(refData.size() == 7); + CHECK(out_values.size() == refData.size()); + for (size_t i = 0; i < refData.size(); i++) { + CHECK(out_values.at(i) == refData.at(i)); + } + } + + SECTION("Evaluating second message") { + std::vector out_values; + CHECK(parser.parseMessage(337, std::vector({47, 4, 60, 29, 0, 0, 0, 0}), out_values) == libdbc::Message::ParseSignalsStatus::Success); + std::vector refData{3, 32, 0, 1, 0, 0, 47}; + CHECK(refData.size() == 7); + CHECK(out_values.size() == refData.size()); + for (size_t i = 0; i < refData.size(); i++) { + CHECK(out_values.at(i) == refData.at(i)); + } + } + + SECTION("Evaluating third message") { + std::vector out_values; + CHECK(parser.parseMessage(337, std::vector({57, 4, 250, 29, 0, 0, 0, 0}), out_values) == libdbc::Message::ParseSignalsStatus::Success); + std::vector refData{3, 51, 0, 1, 0, 0, 57}; + CHECK(refData.size() == 7); + CHECK(out_values.size() == refData.size()); + for (size_t i = 0; i < refData.size(); i++) { + CHECK(out_values.at(i) == refData.at(i)); + } + } +} + +TEST_CASE("Parse Message little endian") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 541 STATUS: 8 DEVICE1 + SG_ Temperature : 48|16@1+ (0.01,-40) [-40|125] "C" DEVICE1 + SG_ SOH : 0|16@1+ (0.01,0) [0|100] "%" DEVICE1 + SG_ SOE : 32|16@1+ (0.01,0) [0|100] "%" DEVICE1 + SG_ SOC : 16|16@1+ (0.01,0) [0|100] "%" DEVICE1)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + libdbc::DbcParser parser; + parser.parse_file(filename); + + std::vector data{0x08, 0x27, 0xa3, 0x22, 0xe5, 0x1f, 0x45, 0x14}; // little endian + std::vector result_values; + REQUIRE(parser.parseMessage(0x21d, data, result_values) == libdbc::Message::ParseSignalsStatus::Success); + REQUIRE(result_values.size() == 4); + + REQUIRE(Catch::Approx(result_values.at(0)) == 11.89); + REQUIRE(Catch::Approx(result_values.at(1)) == 99.92); + REQUIRE(Catch::Approx(result_values.at(2)) == 81.65); + REQUIRE(Catch::Approx(result_values.at(3)) == 88.67); +} + +TEST_CASE("Parse Message big endian signed values") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 545 MSG: 8 BMS2 + SG_ Sig1 : 62|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig2 : 49|2@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig3 : 39|16@0- (0.1,0) [0|0] "A" Vector__XXX + SG_ Sig4 : 60|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig5 : 55|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig6 : 58|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig7 : 59|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig8 : 57|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig9 : 56|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig10 : 61|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ Sig11 : 7|16@0+ (0.001,0) [0|65.535] "V" Vector__XXX + SG_ Sig12 : 23|16@0+ (0.1,0) [0|6553.5] "A" Vector__XXX)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + libdbc::DbcParser p; + p.parse_file(filename.c_str()); + + std::vector data{13, 177, 0, 216, 251, 180, 0, 31}; // big endian + std::vector result_values; + REQUIRE(p.parseMessage(545, data, result_values) == libdbc::Message::ParseSignalsStatus::Success); + REQUIRE(result_values.size() == 12); + REQUIRE(Catch::Approx(result_values.at(0)) == 0); + REQUIRE(Catch::Approx(result_values.at(1)) == 0); + REQUIRE(Catch::Approx(result_values.at(2)) == -110); + REQUIRE(Catch::Approx(result_values.at(3)) == 1); + REQUIRE(Catch::Approx(result_values.at(4)) == 0); + REQUIRE(Catch::Approx(result_values.at(5)) == 1); + REQUIRE(Catch::Approx(result_values.at(6)) == 1); + REQUIRE(Catch::Approx(result_values.at(7)) == 1); + REQUIRE(Catch::Approx(result_values.at(8)) == 1); + REQUIRE(Catch::Approx(result_values.at(9)) == 0); + REQUIRE(Catch::Approx(result_values.at(10)) == 3.5050); + REQUIRE(Catch::Approx(result_values.at(11)) == 21.6); +} + +TEST_CASE("Parse Message with non byte aligned values") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 403 INFORMATION: 8 Vector__XXX + SG_ Voltage : 30|9@1+ (0.2,0) [0|102.2] "V" Vector__XXX + SG_ Phase_Current : 20|10@1- (1,0) [-512|512] "A" Vector__XXX + SG_ Iq_Current : 10|10@1- (1,0) [-512|512] "A" Vector__XXX + SG_ Id_Current : 0|10@1- (1,0) [-512|512] "A" Vector__XXX)"; + const auto filename = create_temporary_dbc_with(dbc_contents.c_str()); + + libdbc::DbcParser p; + p.parse_file(filename); + + std::vector data{131, 51, 33, 9, 33, 0, 0, 0}; + std::vector result_values; + REQUIRE(p.parseMessage(403, data, result_values) == libdbc::Message::ParseSignalsStatus::Success); + REQUIRE(result_values.size() == 4); + REQUIRE(Catch::Approx(result_values.at(0)) == 26.4); + REQUIRE(Catch::Approx(result_values.at(1)) == 146); + REQUIRE(Catch::Approx(result_values.at(2)) == 76); + REQUIRE(Catch::Approx(result_values.at(3)) == -125); +} + +TEST_CASE("Parse Message data length < 8 unsigned") { + std::string dbc_contents = PRIMITIVE_DBC + R"(BO_ 234 MSG1: 8 Vector__XXX + SG_ Msg1Sig1 : 7|8@0+ (1,0) [-3276.8|-3276.7] "C" Vector__XXX + SG_ Msg1Sig2 : 15|8@0+ (1,0) [-3276.8|-3276.7] "km/h" Vector__XXX)"; + 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.parseMessage(234, data, result_values) == libdbc::Message::ParseSignalsStatus::Success); + REQUIRE(result_values.size() == 2); + REQUIRE(Catch::Approx(result_values.at(0)) == 0x1); + REQUIRE(Catch::Approx(result_values.at(1)) == 0x2); +} diff --git a/test/test_utils.cpp b/test/test_utils.cpp index 39f623e..ea3d594 100644 --- a/test/test_utils.cpp +++ b/test/test_utils.cpp @@ -1,5 +1,5 @@ -#include -#include "defines.hpp" +#include "testing_utils/defines.hpp" +#include #include #include @@ -14,7 +14,7 @@ TEST_CASE("Basic file input with safe get_line that is non line ending specific" TextFile.open(TEXT_FILE, std::ios::in); CHECK(TextFile.is_open()); - if(TextFile.is_open()) { + if (TextFile.is_open()) { StreamHandler::get_line(TextFile, test); REQUIRE(test == "This is a non dbc formatted file."); StreamHandler::get_line(TextFile, test); @@ -31,8 +31,8 @@ TEST_CASE("Basic file input with safe get_line that is non line ending specific" TEST_CASE("Test line finding utility functions", "") { std::string line; - std::string test_string = \ -"hello\n\ + std::string test_string = + "hello\n\ \n\ \n\ \n\ @@ -91,4 +91,3 @@ TEST_CASE("Test string split feature", "[string]") { REQUIRE(v == vs); } - diff --git a/test/testing_utils/common.cpp b/test/testing_utils/common.cpp new file mode 100644 index 0000000..e058054 --- /dev/null +++ b/test/testing_utils/common.cpp @@ -0,0 +1,45 @@ +#include "testing_utils/common.hpp" +#include "testing_utils/defines.hpp" +#include +#include +#include +#include +#include +#include + +// Don't want to use tmpnam due to warnings. So here is an alternative using time and random numbers. +// This should be platform agnostic as well. +static std::string generate_unique_filename(); +std::string generate_unique_filename() { + // Get current time since epoch + auto now = std::chrono::system_clock::now(); + auto duration = now.time_since_epoch(); + auto milliseconds = std::chrono::duration_cast(duration).count(); + + // Generate a random number + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, 9999); + 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"; +} + +std::string create_temporary_dbc_with(const char* contents) { + std::filesystem::path temp_dir = std::filesystem::temp_directory_path(); + + // Generate a unique temporary file name + std::string filename = generate_unique_filename(); + std::filesystem::path temp_file = temp_dir / filename; + + std::ofstream file(temp_file); + if (!file.is_open()) { + throw std::runtime_error("Failed to create temporary file."); + } + + file << contents << std::endl; + file.close(); + + return temp_file.string(); +} diff --git a/test/testing_utils/common.hpp b/test/testing_utils/common.hpp new file mode 100644 index 0000000..49ee838 --- /dev/null +++ b/test/testing_utils/common.hpp @@ -0,0 +1,8 @@ +#ifndef COMMON_H +#define COMMON_H + +#include + +std::string create_temporary_dbc_with(const char* contents); + +#endif // COMMON_H diff --git a/test/testing_utils/defines.hpp b/test/testing_utils/defines.hpp new file mode 100644 index 0000000..42d7ee8 --- /dev/null +++ b/test/testing_utils/defines.hpp @@ -0,0 +1,22 @@ +#include + +// Correctly formated files +static const std::string COMPLEX_DBC_FILE = std::string(TESTDBCFILES_PATH) + "/Complex.dbc"; +static const std::string SIMPLE_DBC_FILE = std::string(TESTDBCFILES_PATH) + "/Simple.dbc"; + +// Files with Errors +static const std::string MISSING_NEW_SYMBOLS_DBC_FILE = std::string(TESTDBCFILES_PATH) + "/MissingNewSymbols.dbc"; +static const std::string MISSING_VERSION_DBC_FILE = std::string(TESTDBCFILES_PATH) + "/MissingVersion.dbc"; +static const std::string MISSING_BIT_TIMING_DBC_FILE = std::string(TESTDBCFILES_PATH) + "/MissingBitTiming.dbc"; +static const std::string TEXT_FILE = std::string(TESTDBCFILES_PATH) + "/TextFile.txt"; + +static const std::string PRIMITIVE_DBC = + R"(VERSION "1.0.0" + +NS_ : + +BS_: + +BU_: DBG DRIVER IO MOTOR SENSOR + +)"; diff --git a/third_party/Catch2 b/third_party/Catch2 deleted file mode 160000 index de6fe18..0000000 --- a/third_party/Catch2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit de6fe184a9ac1a06895cdd1c9b437f0a0bdf14ad