From a6016248f80242e2f411aba9a5bb65bd1cd8e17d Mon Sep 17 00:00:00 2001 From: deeaitch Date: Wed, 7 Oct 2026 20:18:37 -0400 Subject: [PATCH] Tests --- README.md | 216 ++++++++++++----- changelog.md | 25 ++ qt/qt.pri | 1 + qt/qt.pro | 1 + qt/qt_version.pri | 2 + tests/README.md | 41 +++- tests/coverage_report.sh | 44 ++++ tests/dbc_coverage_test.cpp | 429 ++++++++++++++++++++-------------- tests/dbc_tree_model_test.cpp | 18 +- tests/model_tests.pro | 3 +- tests/tests.pro | 1 + 11 files changed, 529 insertions(+), 252 deletions(-) create mode 100644 qt/qt_version.pri create mode 100644 tests/coverage_report.sh diff --git a/README.md b/README.md index 0de39e2..13b1a6b 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,10 @@ The implementation follows a simple and practical design: - Qt is not required at the parser or decoder level - the same decode engine can be reused for live traffic and trace replay -The core library uses C++17 and has no Qt dependency. Qt Core and Qt Test are -used by the regression tests; the optional adapter requires Qt Core. Live CAN acquisition, trace-file readers, +The core library uses C++17 and has no Qt dependency. Qt integration targets the +installed Qt 6.8.2, which is the only supported Qt version. Qt Core and Qt Test are +used by the regression tests; model tests also link QtGui for graphical metatypes. +The optional adapter requires only Qt Core. Live CAN acquisition, trace-file readers, are left to the consuming application. An optional Qt Core model adapter is provided in `qt/`. See [changelog.md](changelog.md) for the release and development history, @@ -396,7 +398,7 @@ alternative. Raw values remain exact across their full 64-bit ranges. Check Use `std::get` when the numeric type is known, or `std::get_if` to handle both: ```cpp -if (const auto *value = std::get_if(&decodedSignal.rawValue)) { +if (const std::int64_t *value = std::get_if(&decodedSignal.rawValue)) { std::cout << *value; } else { std::cout << std::get(decodedSignal.rawValue); @@ -854,6 +856,7 @@ lines: - `SG_` → `DbcSignalHandler` - `CM_` → `DbcCommentHandler` - `VAL_` → `DbcValueDescriptionHandler` +- `SG_MUL_VAL_` → `DbcUnsupportedMultiplexingHandler` Grammar implementations live in `core/dbc_statement_handler.cpp`; their interface, context, and dispatcher live in `core/dbc_statement_handler.h`. `core/typefactory.h` @@ -875,8 +878,8 @@ RegisterType("CUSTOM_"); // after implementing MyCustomHandler Registration creates one handler; replacing a registration replaces that instance. Register during initialization before dispatching. A line is routed using its full first whitespace-delimited keyword, so `BO_TX_BU_` cannot match `BO_`. -Unregistered keywords remain ignored. `SG_` accepts spaces and tabs after its keyword; `CM_` retains its existing -policy requiring a literal space. +Unregistered keywords remain ignored. `SG_` and `CM_` accept spaces and tabs +after their keywords. This factory handles statements inside DBC files. A separate format-level factory can be introduced when another file format is implemented. The decoder @@ -960,8 +963,9 @@ Additional behavior to account for: - unregistered DBC keywords are ignored - an `SG_` statement before the first frame is rejected -- duplicate frame and signal definitions are not rejected globally; ambiguous - `VAL_` references to them are rejected +- duplicate frame keys (normalized CAN ID and frame type) and repeated signal + names within a frame are rejected at their definition lines; manually created + databases are also checked for ambiguous `VAL_` references during resolution - payload lengths are checked against storage bounds (`0..255`), rather than CAN or CAN FD protocol limits - comment handling is limited to the supported forms and already parsed targets @@ -1025,12 +1029,15 @@ build also requires Qt Core and Qt Test development packages. Other build systems can compile the sources listed in `core/core_files.pri`; no CMake configuration is currently provided. +Qt-dependent qmake targets share `qt/qt_version.pri`, which rejects Qt versions +other than 6.8.2. The core library remains independent of Qt libraries. + Open `dbc.pro` in Qt Creator to see two subprojects: `core` and `tests`. `core/core.pro` builds the C++17 static library `dbc_core` without a Qt dependency. `tests/tests.pro` builds the QtTest executable `dbc_tests` and links that library. The root project declares `tests.depends = core` for parallel builds. -Build out of source (Qt 6; use `qmake` for Qt 5): +Build out of source with Qt 6.8.2: ```sh mkdir build @@ -1061,8 +1068,15 @@ qmake is still the build tool; this target does not link Qt libraries. ### Optional Qt model adapter -Requires Qt 5.11 or newer (including Qt 6) for `QAbstractItemModelTester`. -The adapter itself links only Qt Core. Enable its library and focused QtTest suite: +The adapter and its `QAbstractItemModelTester` suite use Qt 6.8.2. +The adapter itself links only Qt Core. + +The model test executable additionally links QtGui and uses QGuiApplication so +QAbstractItemModelTester can resolve graphical metatypes such as QPixmap. It +defaults to the `offscreen` platform when `QT_QPA_PLATFORM` is unset or empty; +no display server is required. An explicit platform setting is preserved. + +Enable its library and focused QtTest suite: ```sh mkdir -p build-qt @@ -1086,7 +1100,7 @@ directories when building separately. A widget application also needs `QT += wid // Run after constructing QApplication; view owns the model through QObject. QTreeView view; -auto *model = new DbcTreeModel(&view); +DbcTreeModel *model = new DbcTreeModel(&view); model->SetTree(DbcTreeBuilder().Build(DbcParser().ParseFile("example.dbc"))); view.setModel(model); view.expandAll(); @@ -1207,7 +1221,7 @@ sudo apt install qtcreator GCC supplies `gcov`; the `lcov` package supplies `lcov` and `genhtml`. Use the `gcov` version matching the GCC used by the selected Qt Creator kit. These -instructions use GCC and Qt 6. +instructions use GCC and Qt 6.8.2. From the repository root, create a fresh coverage build. `--coverage` must be passed to both compilation and linking; Debug avoids optimization that can make @@ -1271,12 +1285,16 @@ QtTest executables and the standalone tree test, yielded 97.8% line coverage (724/740) and 70.7% branch coverage (833/1178). These are a snapshot, not fixed expected values for later changes or other toolchains. +After adding direct comment-handler and ambiguous `VAL_` reference tests, the +same configuration measured 98.4% line coverage (728/740) and 71.4% branch +coverage (841/1178). + See [GCC instrumentation options](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html) and the [LCOV project documentation](https://github.com/linux-test-project/lcov). ### Configure the GCC coverage build in Qt Creator -1. Open `dbc.pro` and select a GCC kit with Qt 6. +1. Open `dbc.pro` and select a GCC kit with Qt 6.8.2. 2. In **Projects > Build Settings**, clone **Debug** as **Coverage**. 3. Choose a separate build directory, such as `build-coverage`. 4. Expand the **qmake** build step and add these **Additional arguments**: @@ -1292,64 +1310,140 @@ and the [LCOV project documentation](https://github.com/linux-test-project/lcov) repository path if using a new terminal. `make check` runs both QtTest executables; the standalone tree executable must be run separately. 7. View `html/index.html` in a browser. This workflow produces HTML reports; - the Coco plugin described below uses a different instrumentation toolchain. + it does not require a licensed coverage toolchain. -### Coco integration in Qt Creator +### Configure all build variants in Qt Creator -Coco is a separate licensed toolchain. The Qt Creator plugin alone does not -install CoverageScanner, CoverageBrowser, or activate a Coco license. Obtain a -Coco license or evaluation license and download the Linux installer from -[Qt Account downloads](https://account.qt.io/s/downloads). +The local `dbc.pro.user` stores machine-specific kit IDs and build/run settings. +It is ignored by Git (`*.user`) and should not be committed or copied between +machines. Recreate the configurations below through the UI when setting up a +new checkout. The `Qt4ProjectManager` identifiers inside `.user` are historical +Qt Creator setting names; the project still requires Qt 6.8.2. -For this project's Debian/Ubuntu build, install the compiler, Qt development -tools, and IDE before installing Coco: +The local configuration set uses these independent build directories: -```sh -sudo apt install build-essential qt6-base-dev qt6-base-dev-tools qmake6 qtcreator -# Coco's installer uses locate to discover compilers on Unix: -sudo apt install plocate -sudo updatedb -``` +| Configuration | Build directory example | Purpose | +| --- | --- | --- | +| Debug | `/tmp/build/dbc/Desktop-Debug` | Core library and parser/decoder/coverage regression tests | +| Release | `/tmp/build/dbc/Desktop-Release` | The same targets with release settings | +| Profile | `/tmp/build/dbc/Desktop-Profile` | Existing Qt Creator profiling configuration; not code coverage | +| Debug Qt Models | `/tmp/build/dbc/Desktop-Debug-Qt-Models` | Debug targets plus optional adapter and model tests | +| Release Qt Models | `/tmp/build/dbc/Desktop-Release-Qt-Models` | Release targets plus optional adapter and model tests | +| Debug Qt Models Strict | `/tmp/build/dbc/Desktop-Debug-Qt-Models-Strict` | Model build with `QT_FATAL_WARNINGS=1` for builds and runs | +| Coverage | `/tmp/build/dbc/Desktop-Coverage` | Instrumented core/model build, test execution, and LCOV HTML report | +| Core Only | `/tmp/build/dbc/Desktop-Core-Only` | The Qt-free core static library | +| Tree Tests | `/tmp/build/dbc/Desktop-Tree-Tests` | Standalone C++17 tree test without Qt | +| Demo | `/tmp/build/dbc/Desktop-Demo` | Core library and command-line demo | -Install Coco using the actual downloaded filename, for example: +The temporary paths are examples; choose permanent build directories if desired. +Do not share a build directory between these configurations. -```sh -bash /path/to/SquishCocoSetup_VERSION_Linux_x86_64.run -``` +To add the ordinary qmake configurations: -The exact filename and supported Linux package depend on the downloaded release. -Coco's own tools come from this installer; `lcov` is not required for Coco. -Installing or configuring a new compiler later requires rerunning the installer -or using `cocosetup` to register the compiler. +1. Open `dbc.pro` with a GCC/Qt 6.8.2 kit. +2. Go to **Projects > Build Settings**. Keep the existing Debug, Release, and + Profile configurations. Remove or disable a Coco build step if one remains. +3. Select Debug, press **Clone**, and name the clone **Debug Qt Models**. Set + its build directory from the table and add `CONFIG+=dbc_qt` to the qmake + step's **Additional arguments**. +4. Clone Release as **Release Qt Models** with the same qmake argument and its + own build directory. +5. Clone Debug Qt Models as **Debug Qt Models Strict**. In **Build Environment**, + add `QT_FATAL_WARNINGS=1`. Run configurations should use the build environment. +6. Clone Debug Qt Models as **Coverage**. In qmake **Additional arguments**, use: -For Coco 7.4 or later, set up and check the Qt License Service using the installed -`cocolic` executable (use its full path if it is not on `PATH`): + ```text + CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release "QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path" "QMAKE_LFLAGS+=--coverage" + ``` -```sh -cocolic --setup-qls -cocolic --check -``` +7. For Coverage, select **Add Build Step > Custom Process Step** after qmake + and Make, then set: -Then configure Qt Creator: + | Field | Value | + | --- | --- | + | Command | `/bin/sh` | + | Arguments | `"%{sourceDir}/tests/coverage_report.sh" "%{sourceDir}" "%{buildDir}"` | + | Working directory | `%{buildDir}` | -1. Enable **Coco** in the plugin manager (**Help > About Plugins** on Linux) and - restart Qt Creator if requested. The Debian Qt Creator 16.0.1 package used for - this project's coverage check already includes `libCoco.so`. -2. In **Preferences > Coco**, set the Coco installation directory. -3. Clone **Debug** as **DebugCoco**, with its own build directory. Use an ordinary - Debug configuration as the starting point, without GCC `--coverage` flags. -4. In **Projects > Project Settings > Coco Code Coverage**, enable coverage for - this configuration and save/reconfigure. Depending on the Qt Creator version, - the **Coco code coverage** build step also offers **Enable Coverage**. -5. Exclude test sources and Qt/STL headers as appropriate for a library-only - report. Enable `CONFIG+=dbc_qt` to build the optional model tests. -6. Build using Coco's compiler wrappers, then run both test executables. Use the - Coco plugin's report view or CoverageBrowser to inspect the collected results. + This script compiles the standalone tree test, resets counters, runs both + QtTest executables and the tree test, and generates `%{buildDir}/html/index.html`. + It stops if any test fails. Therefore building Coverage also executes tests. + `lcov` and `genhtml` must be installed. Use this configuration with the GCC kit; + `gcov` and the compiler used for the standalone test must match that kit. +8. Run qmake and build each new configuration once. The default root builds + expose `dbc_tests`; model configurations also expose `dbc_model_tests`. -For qmake, the plugin generates `cocoplugin.prf` in the source directory. Coco -must instrument the library subprojects as well as the test executables. -The GCC/LCOV commands above do not collect Coco data. +Qt Creator expands `%{sourceDir}` and `%{buildDir}` in custom steps to the source +and active build directories. Use the field's variable chooser if needed. -See the official [Coco installation and license setup](https://doc.qt.io/coco/installation.html), -[Coco license requirements](https://doc.qt.io/coco/coco-license-utilization.html), -and [Qt Creator Coco configuration](https://doc.qt.io/qtcreator/creator-coco.html). +For the three independent builds, clone Debug, assign the name and build +directory from the table, and replace its qmake/Make build steps with the custom +process steps below. These steps are local IDE settings and do not change +`dbc.pro`. Give each step a descriptive name and retain the listed order. + +For **Core Only**: + +| Command | Arguments | Working directory | +| --- | --- | --- | +| `/usr/bin/mkdir` | `-p "%{buildDir}"` | `%{sourceDir}` | +| `/usr/bin/qmake6` | `"%{sourceDir}/core/core.pro" CONFIG+=debug CONFIG-=release` | `%{buildDir}` | +| `/usr/bin/make` | `-j4` | `%{buildDir}` | + +Keep a clean step with `make clean` in `%{buildDir}`. Core Only creates a static +library, so there is no executable to run. + +For **Tree Tests**: + +1. Add a custom step with command `/usr/bin/mkdir`, arguments `-p "%{buildDir}"`, + and working directory `%{sourceDir}`. +2. Add a custom step with command `/usr/bin/c++`, working directory `%{buildDir}`, + and these arguments: + + ```text + -std=c++17 -O0 -g -I"%{sourceDir}/core" "%{sourceDir}/tests/tree_node_test.cpp" "%{sourceDir}/core/tree_node.cpp" "%{sourceDir}/core/dbc_tree_builder.cpp" -o "%{buildDir}/tree_node_tests" + ``` + +3. Remove the default Make clean step: this direct compiler build creates no + Makefile. Run the resulting executable as described below. + +For **Demo**: + +| Command | Arguments | Working directory | +| --- | --- | --- | +| `/usr/bin/mkdir` | `-p "%{buildDir}/core" "%{buildDir}/examples"` | `%{sourceDir}` | +| `/usr/bin/qmake6` | `"%{sourceDir}/core/core.pro" CONFIG+=debug CONFIG-=release` | `%{buildDir}/core` | +| `/usr/bin/make` | `-j4` | `%{buildDir}/core` | +| `/usr/bin/qmake6` | `"%{sourceDir}/examples/demo.pro" "DBC_CORE_BUILD_DIR=%{buildDir}/core" CONFIG+=debug CONFIG-=release` | `%{buildDir}/examples` | +| `/usr/bin/make` | `-j4` | `%{buildDir}/examples` | + +Replace the default clean step with two custom steps: `/usr/bin/make` with +arguments `clean`, first in `%{buildDir}/examples`, then in `%{buildDir}/core`. + +To configure execution and switch variants: + +1. Go to **Projects > Run Settings** and select the appropriate qmake target: + `dbc_tests` for ordinary builds or `dbc_model_tests` for model builds. +2. To run all QtTest suites together, add a **Custom Executable** named + **All QtTest suites**, with executable `/usr/bin/make`, arguments `check`, + and working directory `%{buildDir}`. Use it with an ordinary or model qmake + configuration. Coverage already runs every test in its final build step. +3. For Tree Tests, add a **Custom Executable** named **Standalone tree test**, + executable `%{buildDir}/tree_node_tests`, working directory `%{buildDir}`, + and no arguments. +4. For Demo, add a **Custom Executable** named **Demo**, executable + `%{buildDir}/examples/dbc_demo`, working directory `%{buildDir}/examples`. + Set **Command line arguments** to the quoted absolute path of your DBC file. +5. For Coverage on Linux, optionally add a **Custom Executable** named + **Coverage HTML report**, executable `/usr/bin/xdg-open`, arguments + `"%{buildDir}/html/index.html"`, working directory `%{buildDir}`. Running it + opens the generated report in the default browser. +6. Use the kit/build/run selector above the Run button to choose the build + configuration and its corresponding executable. Build with **Ctrl+B**, + then run with **Ctrl+R**. The Build Settings configuration list can also + switch the active build. +7. If `.user` was edited externally, close and reopen the project so Qt Creator + reads the new settings. Do not edit it while the project is open, because + Qt Creator can overwrite external changes when saving its session. + +See Qt Creator's [build configuration guide](https://doc.qt.io/qtcreator/creator-build-settings.html) +and [run configuration guide](https://doc.qt.io/qtcreator/creator-run-settings.html). diff --git a/changelog.md b/changelog.md index 4b7f339..316908a 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,18 @@ existing Git tag; subsequent changes are listed as unreleased. #### Added +- Added regression coverage for signed bit-length boundaries, deterministic + Intel/Motorola extraction, lookup failures, runtime validation, PGN, comments, + file formatting, metadata copying/lifetime, and TypeFactory behavior. +- Added direct-handler and resolver tests for unsupported comments and ambiguous + `VAL_` references in manually constructed databases. +- Documented GCC/gcov/LCOV instrumentation, Qt Creator setup, and HTML reports. + Coverage after the added tests: 98.4% of lines and 71.4% of branches with + GCC 14.2 and Qt 6.8.2, including the standalone tree test. +- Added a reusable post-build coverage report script and step-by-step Qt Creator + configurations for model tests, strict warning checks, coverage, core-only, + standalone tree tests, and the demo. Machine-specific `.user` settings stay + outside version control. - Basic `M` / `mN` multiplexing metadata in parsed, tree, and runtime definitions. - Deferred selector and overlap validation with source locations: one unsigned selector, representable branch values, and overlaps only between exclusive branches. @@ -22,10 +34,23 @@ existing Git tag; subsequent changes are listed as unreleased. #### Changed +- Qt-dependent targets now require the installed Qt 6.8.2 through a shared qmake + version check; removed Qt 5 compatibility instructions. Core remains Qt-free. +- Used explicit local types in the additional coverage and model tests. Functions + retain explicit return types, and runtime handler selection uses TypeFactory + without lambdas or extra factory layers for concrete objects. +- Coverage documentation now describes the selected open GCC/LCOV toolchain. - Overlapping unconditional signals are now rejected during parsing. #### Fixed +- Linked QtGui only in the optional model tests and used QGuiApplication with an + offscreen default, resolving the QAbstractItemModelTester invalid QPixmap + metatype warning without adding QtGui to the adapter library. +- Accepted tabs after `CM_` by using the comment parser's keyword extraction + instead of requiring a literal-space prefix. +- Removed test compiler warnings by using `QVERIFY_THROWS_EXCEPTION` and + explicitly discarding `nodiscard` return values in exception checks. - Reject duplicate frames by normalized CAN ID and frame type, including equivalent extended ID encodings, and duplicate signal names within a frame. Errors report the file path and line of the repeated definition. Standard and diff --git a/qt/qt.pri b/qt/qt.pri index 5fef620..0d10bd6 100644 --- a/qt/qt.pri +++ b/qt/qt.pri @@ -1,3 +1,4 @@ +include($$PWD/qt_version.pri) include($$PWD/../core/core.pri) QT += core INCLUDEPATH += $$PWD diff --git a/qt/qt.pro b/qt/qt.pro index 7accaaa..4ee51b4 100644 --- a/qt/qt.pro +++ b/qt/qt.pro @@ -1,4 +1,5 @@ TEMPLATE = lib +include(qt_version.pri) TARGET = dbc_qt CONFIG += staticlib c++17 QT += core diff --git a/qt/qt_version.pri b/qt/qt_version.pri new file mode 100644 index 0000000..a82af8a --- /dev/null +++ b/qt/qt_version.pri @@ -0,0 +1,2 @@ +# Qt-dependent targets use the project's installed and tested Qt version. +!equals(QT_VERSION, 6.8.2): error("This project requires Qt 6.8.2; found $$QT_VERSION") diff --git a/tests/README.md b/tests/README.md index e02ddb8..a706300 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,14 +1,20 @@ # QtTest regression tests -For instrumented builds, LCOV HTML reports, Qt Creator coverage configuration, -and Coco installation, see [Measuring code coverage](../README.md#measuring-code-coverage). +For instrumented builds, LCOV HTML reports, and Qt Creator coverage configuration, +see [Measuring code coverage](../README.md#measuring-code-coverage). +For the complete IDE build/run configuration set, see +[Configure all build variants in Qt Creator](../README.md#configure-all-build-variants-in-qt-creator). +The Coverage configuration uses `coverage_report.sh` after its instrumented +build; the script takes the source and build directories as its two arguments. Open the repository's root `dbc.pro` in Qt Creator. It contains `core` (a static C++17 library) and `tests` (the `dbc_tests` QtTest executable). Core is built first; tests link its archive rather than compiling its implementation again. Qt Core and Qt Test development packages are required for tests only. +The optional model tests also link QtGui, which is provided by Qt base development +packages. The adapter library itself still links only QtCore. -Build from the repository root with Qt 6 (use `qmake` for Qt 5): +Build from the repository root with the installed Qt 6.8.2: ```sh mkdir build @@ -18,8 +24,8 @@ make -j make check ``` -Or run `./tests/dbc_tests` directly. Both parser and decoder suites run, and the -executable returns nonzero when either suite fails. Decoder cases appear as +Or run `./tests/dbc_tests` directly. Parser, decoder, and additional coverage +suites run, and the executable returns nonzero when any suite fails. Decoder cases appear as individual QtTest data rows; parser checks are grouped into named test slots. Checks also run in release builds. @@ -34,7 +40,8 @@ make make check ``` -`parser_tests.pri` and `decoder_tests.pri` include the test sources. +`parser_tests.pri` and `decoder_tests.pri` include the original test sources; +`tests.pro` also includes `dbc_coverage_test.cpp`. `../core/core.pri` supplies the library include path and linkage. ## Decoder coverage @@ -73,14 +80,32 @@ README for its compiler command. It checks parents, sibling positions, null children, bounds, and unique_ptr transfer. Enable `CONFIG+=dbc_qt` in the root qmake invocation to build `dbc_qt` and -`dbc_model_tests` alongside existing tests. Qt 5.11+ or Qt 6 is required. +`dbc_model_tests` alongside existing tests. Qt 6.8.2 is required. `make check` runs all suites. Model tests parse a temporary DBC fixture and use QAbstractItemModelTester, verify navigation across columns, invalid/foreign indexes, metadata and exact VAL_ integers, and persistent-index invalidation -when reloading or clearing the tree. No GUI/display server is needed. +when reloading or clearing the tree. Their QGuiApplication registers graphical +metatypes used by QAbstractItemModelTester, avoiding the invalid QPixmap type +warning in Qt 6.8.2. The test executable defaults to `QT_QPA_PLATFORM=offscreen` +when the variable is unset or empty, so no display server is needed. An explicit +platform setting is preserved. Basic multiplexing coverage includes selector declarations after branches, Intel/Motorola branch overlap, signed active values and VAL_ lookup, unconditional signals, inactive branches, missing selector/branch payloads, exact UINT64_MAX selector matching, malformed runtime selectors, source locations, and rejection of invalid markers, selector references/ranges, overlaps, and SG_MUL_VAL_. + +## Additional regression coverage + +`dbc_coverage_test.cpp` runs as part of `dbc_tests`. It covers signed boundaries +below 64 bits, deterministic extraction comparisons for both byte orders across +lengths 1–64, frame lookup failures, exact decode statuses, malformed runtime +multiplexing, PGN boundaries, comment handling, file formatting, metadata copying +and source lifetime, physical conversion, and TypeFactory registration/creation. +Direct resolver tests construct ambiguous databases through the public metadata +API, verify source locations, and check that ambiguous targets receive no values. + +All QtTest targets use Qt 6.8.2 and `QVERIFY_THROWS_EXCEPTION`. Exception tests +explicitly discard `nodiscard` results when only the exception is being checked. +The standalone tree test requires only a C++17 compiler. diff --git a/tests/coverage_report.sh b/tests/coverage_report.sh new file mode 100644 index 0000000..1f259db --- /dev/null +++ b/tests/coverage_report.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Run after building dbc.pro with GCC coverage and CONFIG+=dbc_qt. +set -eu + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 SOURCE_DIRECTORY BUILD_DIRECTORY" >&2 + exit 2 +fi + +dbc_source_dir=$(cd "$1" && pwd) +dbc_build_dir=$(cd "$2" && pwd) +cd "$dbc_build_dir" + +if [ ! -f core/dbc_decoder.gcno ] || [ ! -f qt/dbc_tree_model.gcno ]; then + echo "Build with --coverage and CONFIG+=dbc_qt before generating the report." >&2 + exit 1 +fi + +mkdir -p standalone +"${CXX:-c++}" -std=c++17 -O0 -g --coverage -fprofile-abs-path \ + -I"$dbc_source_dir/core" \ + "$dbc_source_dir/tests/tree_node_test.cpp" \ + "$dbc_source_dir/core/tree_node.cpp" \ + "$dbc_source_dir/core/dbc_tree_builder.cpp" \ + -o standalone/tree_node_tests + +lcov --capture --initial --directory . --branch-coverage \ + --include "$dbc_source_dir/core/*" --include "$dbc_source_dir/qt/*" \ + --output-file baseline.info --quiet +lcov --zerocounters --directory . --quiet + +./tests/dbc_tests +./tests/dbc_model_tests +./standalone/tree_node_tests + +lcov --capture --directory . --branch-coverage \ + --include "$dbc_source_dir/core/*" --include "$dbc_source_dir/qt/*" \ + --output-file executed.info --quiet +lcov --add-tracefile baseline.info --add-tracefile executed.info \ + --branch-coverage --output-file coverage.info --quiet +genhtml coverage.info --branch-coverage --demangle-cpp \ + --output-directory html --quiet +lcov --summary coverage.info --branch-coverage +echo "HTML report: $dbc_build_dir/html/index.html" diff --git a/tests/dbc_coverage_test.cpp b/tests/dbc_coverage_test.cpp index 2aebc53..276a259 100644 --- a/tests/dbc_coverage_test.cpp +++ b/tests/dbc_coverage_test.cpp @@ -1,64 +1,65 @@ // Additional regression coverage. Kept separate for review before the first run. -#include "dbc_parser.h" #include "dbc_decode_builder.h" #include "dbc_decoder.h" -#include "dbc_tree_builder.h" +#include "dbc_parser.h" #include "dbc_statement_handler.h" +#include "dbc_tree_builder.h" #include "typefactory.h" -#include #include +#include #include #include #include namespace { -DbcDatabase ParseText(const std::string &text) { - QTemporaryFile file; - if (!file.open()) throw std::runtime_error("Cannot create DBC fixture"); - if (file.write(text.data(), static_cast(text.size())) != static_cast(text.size()) || !file.flush()) - throw std::runtime_error("Cannot write DBC fixture"); - return DbcParser().ParseFile(file.fileName().toStdString()); -} - -DecodeDatabase SingleSignal(const DecodeSignal &signal) { - DecodeDatabase database; - DecodeFrame frame; - frame.canId = 17U; - frame.signals.push_back(signal); - database.frames.push_back(frame); - database.frameIndexByKey.emplace(FrameKey(17U, false), 0U); - return database; -} - -// Independent reference: collect the visited bits first, then weight them. -// Motorola uses a linear MSB-first coordinate instead of sawtooth traversal. -std::uint64_t ReferenceExtract(const std::vector &data, - ByteOrder order, unsigned start, unsigned length) { - std::uint64_t expected = 0; - for (unsigned i = 0; i < length; ++i) { - const unsigned coordinate = order == ByteOrder::Intel - ? start + i : (start / 8U) * 8U + (7U - start % 8U) + i; - const unsigned bit = order == ByteOrder::Intel ? coordinate % 8U : 7U - coordinate % 8U; - const unsigned weight = order == ByteOrder::Intel ? i : length - 1U - i; - expected |= std::uint64_t((data.at(coordinate / 8U) >> bit) & 1U) << weight; + DbcDatabase ParseText(const std::string &text) { + QTemporaryFile file; + if (!file.open()) throw std::runtime_error("Cannot create DBC fixture"); + if (file.write(text.data(), static_cast(text.size())) != static_cast(text.size()) || !file.flush()) + throw std::runtime_error("Cannot write DBC fixture"); + return DbcParser().ParseFile(file.fileName().toStdString()); } - return expected; -} -struct FactoryBase { - virtual ~FactoryBase() = default; - virtual int Value() const = 0; -}; -struct FactoryFirst : FactoryBase { - explicit FactoryFirst(std::unique_ptr value) : value(std::move(value)) {} - int Value() const override { return *value; } - std::unique_ptr value; -}; -struct FactorySecond : FactoryFirst { - using FactoryFirst::FactoryFirst; - int Value() const override { return *value + 1; } -}; -} + DecodeDatabase SingleSignal(const DecodeSignal &signal) { + DecodeDatabase database; + DecodeFrame frame; + frame.canId = 17U; + frame.signals.push_back(signal); + database.frames.push_back(frame); + database.frameIndexByKey.emplace(FrameKey(17U, false), 0U); + return database; + } + + // Independent reference: collect the visited bits first, then weight them. + // Motorola uses a linear MSB-first coordinate instead of sawtooth traversal. + std::uint64_t ReferenceExtract(const std::vector &data, + ByteOrder order, unsigned start, unsigned length) { + std::uint64_t expected = 0; + for (unsigned i = 0; i < length; ++i) { + const unsigned coordinate = order == ByteOrder::Intel + ? start + i + : (start / 8U) * 8U + (7U - start % 8U) + i; + const unsigned bit = order == ByteOrder::Intel ? coordinate % 8U : 7U - coordinate % 8U; + const unsigned weight = order == ByteOrder::Intel ? i : length - 1U - i; + expected |= std::uint64_t((data.at(coordinate / 8U) >> bit) & 1U) << weight; + } + return expected; + } + + struct FactoryBase { + virtual ~FactoryBase() = default; + virtual int Value() const = 0; + }; + struct FactoryFirst : FactoryBase { + explicit FactoryFirst(std::unique_ptr value) : value(std::move(value)) {} + int Value() const override { return *value; } + std::unique_ptr value; + }; + struct FactorySecond : FactoryFirst { + using FactoryFirst::FactoryFirst; + int Value() const override { return *value + 1; } + }; +}// namespace class CoverageTest : public QObject { Q_OBJECT @@ -100,12 +101,12 @@ void CoverageTest::frameLookup() { raw.canId = 17U; DecodeDatabase empty; QVERIFY(decoder.FindFrame(empty, 17U, false) == nullptr); - const auto absent = decoder.Decode(empty, raw); + const DecodedFrameValue absent = decoder.Decode(empty, raw); QVERIFY(!absent.valid); QVERIFY(absent.definition == nullptr); QVERIFY(absent.signals.empty()); DecodeSignal signal; - auto database = SingleSignal(signal); + DecodeDatabase database = SingleSignal(signal); QVERIFY(decoder.FindFrame(database, 18U, false) == nullptr); QVERIFY(decoder.FindFrame(database, 17U, true) == nullptr); raw.isExtended = true; @@ -119,7 +120,7 @@ void CoverageTest::frameLookup() { QVERIFY(!decoder.Decode(database, raw).valid); database.frameIndexByKey.at(FrameKey(17U, false)) = 0U; database.frames[0].signals.clear(); - const auto matched = decoder.Decode(database, raw); + const DecodedFrameValue matched = decoder.Decode(database, raw); QVERIFY(matched.valid); QCOMPARE(matched.definition, &database.frames[0]); QVERIFY(matched.signals.empty()); @@ -130,11 +131,11 @@ void CoverageTest::signedBoundaries_data() { QTest::addColumn("length"); QTest::addColumn("expected"); for (int order = 0; order < 2; ++order) { - for (unsigned length : {1U, 2U, 7U, 8U, 31U, 32U, 63U}) { + for (unsigned length: {1U, 2U, 7U, 8U, 31U, 32U, 63U}) { const std::int64_t half = std::int64_t(1) << (length - 1U); const std::array values{{0, -1, -half, half - 1}}; for (unsigned index = 0; index < values.size(); ++index) { - const auto name = QByteArray::number(order) + "-" + QByteArray::number(length) + "-" + QByteArray::number(index); + const QByteArray name = QByteArray::number(order) + "-" + QByteArray::number(length) + "-" + QByteArray::number(index); QTest::newRow(name.constData()) << order << length << qlonglong(values[index]); } } @@ -159,8 +160,8 @@ void CoverageTest::signedBoundaries() { const unsigned position = order == 0 ? i % 8U : 7U - i % 8U; raw.data[i / 8U] |= static_cast(((bits >> weight) & 1U) << position); } - const auto database = SingleSignal(signal); - const auto result = DbcDecoder().Decode(database, raw); + const DecodeDatabase database = SingleSignal(signal); + const DecodedFrameValue result = DbcDecoder().Decode(database, raw); QVERIFY(result.signals[0].valid); QVERIFY(std::holds_alternative(result.signals[0].rawValue)); QCOMPARE(std::get(result.signals[0].rawValue), static_cast(expected)); @@ -173,7 +174,7 @@ void CoverageTest::extraction_data() { for (int order = 0; order < 2; ++order) for (unsigned start = 0; start < 16U; ++start) for (unsigned length = 1; length <= 64U; ++length) { - const auto name = QByteArray::number(order) + "-" + QByteArray::number(start) + "-" + QByteArray::number(length); + const QByteArray name = QByteArray::number(order) + "-" + QByteArray::number(start) + "-" + QByteArray::number(length); QTest::newRow(name.constData()) << order << start << length; } } @@ -186,17 +187,18 @@ void CoverageTest::extraction() { signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; signal.startBit = start; signal.length = length; - const auto database = SingleSignal(signal); + const DecodeDatabase database = SingleSignal(signal); RawCanFrame raw; raw.canId = 17U; const std::vector> patterns = { - std::vector(10U, 0U), std::vector(10U, 255U), - {0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}}; - for (const auto &data : patterns) { + std::vector(10U, 0U), + std::vector(10U, 255U), + {0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}}; + for (const std::vector &data: patterns) { raw.data = data; const unsigned firstCoordinate = order == 0 ? start : (start / 8U) * 8U + 7U - start % 8U; raw.data.resize((firstCoordinate + length + 7U) / 8U); - const auto decoded = DbcDecoder().Decode(database, raw); + const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw); QVERIFY(decoded.signals[0].valid); QCOMPARE(std::get(decoded.signals[0].rawValue), ReferenceExtract(data, signal.byteOrder, start, length)); } @@ -210,7 +212,7 @@ void CoverageTest::invalidSignalStatus_data() { QTest::addColumn("status"); for (int order = 0; order < 2; ++order) { const unsigned start = order == 0 ? 0U : 7U; - const auto prefix = QByteArray::number(order); + const QByteArray prefix = QByteArray::number(order); QTest::newRow((prefix + "-zero").constData()) << order << start << 0U << 1U << int(SignalDecodeStatus::InvalidDefinition); QTest::newRow((prefix + "-65").constData()) << order << start << 65U << 9U << int(SignalDecodeStatus::InvalidDefinition); QTest::newRow((prefix + "-empty").constData()) << order << start << 8U << 0U << int(SignalDecodeStatus::InsufficientPayload); @@ -220,14 +222,20 @@ void CoverageTest::invalidSignalStatus_data() { } void CoverageTest::invalidSignalStatus() { - QFETCH(int, order); QFETCH(unsigned, start); QFETCH(unsigned, length); - QFETCH(unsigned, bytes); QFETCH(int, status); + QFETCH(int, order); + QFETCH(unsigned, start); + QFETCH(unsigned, length); + QFETCH(unsigned, bytes); + QFETCH(int, status); DecodeSignal signal; - signal.startBit = start; signal.length = length; + signal.startBit = start; + signal.length = length; signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; - const auto database = SingleSignal(signal); - RawCanFrame raw; raw.canId = 17U; raw.data.assign(bytes, 255U); - const auto decoded = DbcDecoder().Decode(database, raw); + const DecodeDatabase database = SingleSignal(signal); + RawCanFrame raw; + raw.canId = 17U; + raw.data.assign(bytes, 255U); + const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw); QVERIFY(decoded.valid); QVERIFY(!decoded.signals[0].valid); QCOMPARE(int(decoded.signals[0].status), status); @@ -244,19 +252,28 @@ void CoverageTest::malformedRuntimeMultiplexing_data() { void CoverageTest::malformedRuntimeMultiplexing() { QFETCH(int, kind); - DecodeSignal selector; selector.length = 1U; selector.multiplexing.role = MultiplexRole::Selector; - auto database = SingleSignal(selector); - DecodeSignal branch; branch.startBit = 8U; branch.length = 8U; - branch.multiplexing.role = MultiplexRole::Branch; branch.multiplexing.value = 1U; + DecodeSignal selector; + selector.length = 1U; + selector.multiplexing.role = MultiplexRole::Selector; + DecodeDatabase database = SingleSignal(selector); + DecodeSignal branch; + branch.startBit = 8U; + branch.length = 8U; + branch.multiplexing.role = MultiplexRole::Branch; + branch.multiplexing.value = 1U; database.frames[0].signals.push_back(branch); - DecodeSignal ordinary; ordinary.startBit = 16U; ordinary.length = 8U; + DecodeSignal ordinary; + ordinary.startBit = 16U; + ordinary.length = 8U; database.frames[0].signals.push_back(ordinary); if (kind == 0) database.frames[0].signals.push_back(selector); if (kind == 1) database.frames[0].signals[0].valueType = ValueType::Signed; if (kind == 2) database.frames[0].signals[0].length = 65U; if (kind == 3) database.frames[0].signals[1].multiplexing.value = 2U; - RawCanFrame raw; raw.canId = 17U; raw.data = {1U, 42U, 99U}; - const auto result = DbcDecoder().Decode(database, raw); + RawCanFrame raw; + raw.canId = 17U; + raw.data = {1U, 42U, 99U}; + const DecodedFrameValue result = DbcDecoder().Decode(database, raw); QCOMPARE(int(result.signals[1].status), int(SignalDecodeStatus::InvalidDefinition)); QVERIFY(!result.signals[1].valid); QVERIFY(result.signals[2].valid); @@ -268,28 +285,34 @@ void CoverageTest::malformedRuntimeMultiplexing() { } void CoverageTest::pgn_data() { - QTest::addColumn("id"); QTest::addColumn("expected"); QTest::addColumn("hasPgn"); + QTest::addColumn("id"); + QTest::addColumn("expected"); + QTest::addColumn("hasPgn"); QTest::newRow("standard") << 0x7FFU << 0U << false; - for (unsigned dp : {0U, 1U}) - for (unsigned pf : {239U, 240U}) - for (unsigned ps : {0U, 0xABU}) { + for (unsigned dp: {0U, 1U}) + for (unsigned pf: {239U, 240U}) + for (unsigned ps: {0U, 0xABU}) { const unsigned id = 0x80000000U | (dp << 24U) | (pf << 16U) | (ps << 8U) | 0x42U; const unsigned expected = (dp << 16U) | (pf == 239U ? 0xEF00U : 0xF000U | ps); - const auto name = QByteArray::number(dp) + "-" + QByteArray::number(pf) + "-" + QByteArray::number(ps); + const QByteArray name = QByteArray::number(dp) + "-" + QByteArray::number(pf) + "-" + QByteArray::number(ps); QTest::newRow(name.constData()) << id << expected << true; } } void CoverageTest::pgn() { - QFETCH(unsigned, id); QFETCH(unsigned, expected); QFETCH(bool, hasPgn); - const auto parsed = ParseText("BO_ " + std::to_string(id) + " Frame: 8 ECU\n"); + QFETCH(unsigned, id); + QFETCH(unsigned, expected); + QFETCH(bool, hasPgn); + const DbcDatabase parsed = ParseText("BO_ " + std::to_string(id) + " Frame: 8 ECU\n"); QCOMPARE(parsed.frames[0].hasPgn, hasPgn); QCOMPARE(parsed.frames[0].pgn, expected); } void CoverageTest::comments_data() { - QTest::addColumn("before"); QTest::addColumn("after"); - QTest::addColumn("frameComment"); QTest::addColumn("signalComment"); + QTest::addColumn("before"); + QTest::addColumn("after"); + QTest::addColumn("frameComment"); + QTest::addColumn("signalComment"); QTest::newRow("tabs") << QString::fromUtf8("") << QString::fromUtf8("CM_\tBO_ 1 \"Frame\";\nCM_\tSG_ 1 State \"Signal\";\n") << QString::fromUtf8("Frame") << QString::fromUtf8("Signal"); QTest::newRow("empty-overwrite") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 1 \"Old\";\nCM_ BO_ 1 \"\";\nCM_ SG_ 1 State \"Old\";\nCM_ SG_ 1 State \"\";\n") << QString::fromUtf8("") << QString::fromUtf8(""); QTest::newRow("last-wins") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 1 \"First\";\nCM_ BO_ 1 \"Last\";\nCM_ SG_ 1 State \"First\";\nCM_ SG_ 1 State \"Last\";\n") << QString::fromUtf8("Last") << QString::fromUtf8("Last"); @@ -300,8 +323,11 @@ void CoverageTest::comments_data() { } void CoverageTest::comments() { - QFETCH(QString, before); QFETCH(QString, after); QFETCH(QString, frameComment); QFETCH(QString, signalComment); - const auto parsed = ParseText(before.toStdString() + "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" + after.toStdString()); + QFETCH(QString, before); + QFETCH(QString, after); + QFETCH(QString, frameComment); + QFETCH(QString, signalComment); + const DbcDatabase parsed = ParseText(before.toStdString() + "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" + after.toStdString()); QCOMPARE(parsed.frames[0].comment, frameComment.toStdString()); QCOMPARE(parsed.frames[0].signals[0].comment, signalComment.toStdString()); } @@ -384,12 +410,12 @@ void CoverageTest::ambiguousValueReferences() { QFAIL("The resolver must reject an ambiguous target"); } catch (const std::runtime_error &error) { const std::string message = duplicateFrames ? "Ambiguous VAL_ frame reference." - : "Ambiguous VAL_ signal reference: State"; + : "Ambiguous VAL_ signal reference: State"; QCOMPARE(std::string(error.what()), path + ":37: " + message); } // Ambiguity must be detected before applying any descriptions. - for (const auto &candidate : database.frames) - for (const auto &candidateSignal : candidate.signals) + for (const FrameInfo &candidate: database.frames) + for (const SignalInfo &candidateSignal: candidate.signals) QVERIFY(candidateSignal.valueDescriptions.empty()); } @@ -415,8 +441,8 @@ void CoverageTest::valueReferenceScope() { handler.Handle("VAL_ 2147483649 State 0 \"Extended\";", context); context.ResolveValueDescriptions("manual database.dbc"); - const auto *standard = FindValueDescription(database.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); - const auto *extended = FindValueDescription(database.frames[1].signals[0].valueDescriptions, std::uint64_t(0)); + const std::string *standard = FindValueDescription(database.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); + const std::string *extended = FindValueDescription(database.frames[1].signals[0].valueDescriptions, std::uint64_t(0)); QVERIFY(standard != nullptr); QVERIFY(extended != nullptr); QCOMPARE(*standard, std::string("Standard")); @@ -425,7 +451,8 @@ void CoverageTest::valueReferenceScope() { } void CoverageTest::fileFormatting_data() { - QTest::addColumn("text"); QTest::addColumn("frames"); + QTest::addColumn("text"); + QTest::addColumn("frames"); QTest::newRow("empty") << QString::fromUtf8("") << 0U; QTest::newRow("whitespace") << QString::fromUtf8(" \t\r\n\n\t") << 0U; QTest::newRow("unknown-only") << QString::fromUtf8("VERSION \"test\"\nBU_: ECU\nUNKNOWN_ data\n") << 0U; @@ -434,14 +461,15 @@ void CoverageTest::fileFormatting_data() { } void CoverageTest::fileFormatting() { - QFETCH(QString, text); QFETCH(unsigned, frames); - const auto parsed = ParseText(text.toStdString()); + QFETCH(QString, text); + QFETCH(unsigned, frames); + const DbcDatabase parsed = ParseText(text.toStdString()); QCOMPARE(parsed.frames.size(), std::size_t(frames)); if (frames) { QCOMPARE(parsed.frames[0].signals.size(), std::size_t(1)); if (text.contains("\r\nBO_")) { QCOMPARE(parsed.frames[0].comment, std::string("Comment")); - const auto *description = FindValueDescription(parsed.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); + const std::string *description = FindValueDescription(parsed.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); QVERIFY(description != nullptr); QCOMPARE(*description, std::string("Off")); } @@ -451,31 +479,33 @@ void CoverageTest::fileFormatting() { void CoverageTest::missingFile() { QTemporaryFile file; QVERIFY(file.open()); - const auto path = file.fileName(); + const QString path = file.fileName(); QVERIFY(file.remove()); - try { DbcParser().ParseFile(path.toStdString()); QFAIL("Missing file must throw"); } - catch (const std::runtime_error &error) { + try { + DbcParser().ParseFile(path.toStdString()); + QFAIL("Missing file must throw"); + } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), "Failed to open DBC file: " + path.toStdString()); } } void CoverageTest::builders() { - const auto emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase()); + const DecodeDatabase emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase()); QVERIFY(emptyRuntime.frames.empty()); QVERIFY(emptyRuntime.frameIndexByKey.empty()); - const auto emptyTree = DbcTreeBuilder().Build(DbcDatabase()); + const std::unique_ptr emptyTree = DbcTreeBuilder().Build(DbcDatabase()); QCOMPARE(emptyTree->GetType(), NodeType::Root); QCOMPARE(emptyTree->GetName(), std::string("dbc")); QCOMPARE(emptyTree->GetChildCount(), std::size_t(0)); QVERIFY(emptyTree->GetFrame() == nullptr && emptyTree->GetSignal() == nullptr); - const auto expected = ParseText( - "BO_ 256 Standard: 3 ECU1\n" - "SG_ Mode M : 0|8@1+ (2,3) [0|513] \"mode\" ECU1,ECU2\n" - "SG_ Branch m1 : 15|8@0- (-2,5) [-249|261] \"rpm\" ECU2,ECU3\n" - "SG_ Always : 16|8@1+ (0.25,-1) [-1|62.75] \"V\" ECU3\n" - "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Branch \"Branch comment\";\n" - "VAL_ 256 Branch -1 \"Negative\" 1 \"Positive\";\n" - "BO_ 2163212866 Extended: 8 ECU4\n"); + const DbcDatabase expected = ParseText( + "BO_ 256 Standard: 3 ECU1\n" + "SG_ Mode M : 0|8@1+ (2,3) [0|513] \"mode\" ECU1,ECU2\n" + "SG_ Branch m1 : 15|8@0- (-2,5) [-249|261] \"rpm\" ECU2,ECU3\n" + "SG_ Always : 16|8@1+ (0.25,-1) [-1|62.75] \"V\" ECU3\n" + "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Branch \"Branch comment\";\n" + "VAL_ 256 Branch -1 \"Negative\" 1 \"Positive\";\n" + "BO_ 2163212866 Extended: 8 ECU4\n"); DecodeDatabase runtime; std::unique_ptr tree; { @@ -500,44 +530,62 @@ void CoverageTest::builders() { QVERIFY(node->GetSignal() == nullptr); QCOMPARE(runtime.frameIndexByKey.at(FrameKey(frame.canId, frame.isExtended)), i); const FrameInfo &copied = *node->GetFrame(); - QCOMPARE(copied.name, frame.name); QCOMPARE(decodedFrame.name, frame.name); - QCOMPARE(copied.canId, frame.canId); QCOMPARE(decodedFrame.canId, frame.canId); - QCOMPARE(copied.isExtended, frame.isExtended); QCOMPARE(decodedFrame.isExtended, frame.isExtended); - QCOMPARE(copied.dlc, frame.dlc); QCOMPARE(decodedFrame.dlc, frame.dlc); - QCOMPARE(copied.pgn, frame.pgn); QCOMPARE(decodedFrame.pgn, frame.pgn); - QCOMPARE(copied.hasPgn, frame.hasPgn); QCOMPARE(decodedFrame.hasPgn, frame.hasPgn); - QCOMPARE(copied.transmitter, frame.transmitter); QCOMPARE(decodedFrame.transmitter, frame.transmitter); - QCOMPARE(copied.comment, frame.comment); QCOMPARE(decodedFrame.comment, frame.comment); + QCOMPARE(copied.name, frame.name); + QCOMPARE(decodedFrame.name, frame.name); + QCOMPARE(copied.canId, frame.canId); + QCOMPARE(decodedFrame.canId, frame.canId); + QCOMPARE(copied.isExtended, frame.isExtended); + QCOMPARE(decodedFrame.isExtended, frame.isExtended); + QCOMPARE(copied.dlc, frame.dlc); + QCOMPARE(decodedFrame.dlc, frame.dlc); + QCOMPARE(copied.pgn, frame.pgn); + QCOMPARE(decodedFrame.pgn, frame.pgn); + QCOMPARE(copied.hasPgn, frame.hasPgn); + QCOMPARE(decodedFrame.hasPgn, frame.hasPgn); + QCOMPARE(copied.transmitter, frame.transmitter); + QCOMPARE(decodedFrame.transmitter, frame.transmitter); + QCOMPARE(copied.comment, frame.comment); + QCOMPARE(decodedFrame.comment, frame.comment); QCOMPARE(node->GetChildCount(), frame.signals.size()); QCOMPARE(copied.signals.size(), frame.signals.size()); QCOMPARE(decodedFrame.signals.size(), frame.signals.size()); for (std::size_t j = 0; j < frame.signals.size(); ++j) { - const auto &original = frame.signals[j]; - const auto &target = decodedFrame.signals[j]; - const auto *leaf = node->GetChild(j); + const SignalInfo &original = frame.signals[j]; + const DecodeSignal &target = decodedFrame.signals[j]; + const TreeNode *leaf = node->GetChild(j); QVERIFY(leaf && leaf->GetSignal()); QCOMPARE(leaf->GetType(), NodeType::Signal); QCOMPARE(leaf->GetName(), original.name); QVERIFY(leaf->GetFrame() == nullptr); // Check both the frame payload copy and each child payload copy. - for (const SignalInfo *copy : {&copied.signals[j], leaf->GetSignal()}) { - QCOMPARE(copy->name, original.name); QCOMPARE(copy->startBit, original.startBit); - QCOMPARE(copy->length, original.length); QCOMPARE(copy->isLittleEndian, original.isLittleEndian); - QCOMPARE(copy->isSigned, original.isSigned); QCOMPARE(copy->factor, original.factor); - QCOMPARE(copy->offset, original.offset); QCOMPARE(copy->minimum, original.minimum); - QCOMPARE(copy->maximum, original.maximum); QCOMPARE(copy->unit, original.unit); - QCOMPARE(copy->comment, original.comment); QVERIFY(copy->receivers == original.receivers); + for (const SignalInfo *copy: {&copied.signals[j], leaf->GetSignal()}) { + QCOMPARE(copy->name, original.name); + QCOMPARE(copy->startBit, original.startBit); + QCOMPARE(copy->length, original.length); + QCOMPARE(copy->isLittleEndian, original.isLittleEndian); + QCOMPARE(copy->isSigned, original.isSigned); + QCOMPARE(copy->factor, original.factor); + QCOMPARE(copy->offset, original.offset); + QCOMPARE(copy->minimum, original.minimum); + QCOMPARE(copy->maximum, original.maximum); + QCOMPARE(copy->unit, original.unit); + QCOMPARE(copy->comment, original.comment); + QVERIFY(copy->receivers == original.receivers); QVERIFY(copy->valueDescriptions == original.valueDescriptions); QCOMPARE(copy->multiplexing.role, original.multiplexing.role); QCOMPARE(copy->multiplexing.value, original.multiplexing.value); } - QCOMPARE(target.name, original.name); QCOMPARE(target.startBit, original.startBit); + QCOMPARE(target.name, original.name); + QCOMPARE(target.startBit, original.startBit); QCOMPARE(target.length, original.length); QCOMPARE(target.byteOrder, original.isLittleEndian ? ByteOrder::Intel : ByteOrder::Motorola); QCOMPARE(target.valueType, original.isSigned ? ValueType::Signed : ValueType::Unsigned); - QCOMPARE(target.factor, original.factor); QCOMPARE(target.offset, original.offset); - QCOMPARE(target.minimum, original.minimum); QCOMPARE(target.maximum, original.maximum); - QCOMPARE(target.unit, original.unit); QCOMPARE(target.comment, original.comment); + QCOMPARE(target.factor, original.factor); + QCOMPARE(target.offset, original.offset); + QCOMPARE(target.minimum, original.minimum); + QCOMPARE(target.maximum, original.maximum); + QCOMPARE(target.unit, original.unit); + QCOMPARE(target.comment, original.comment); QVERIFY(target.receivers == original.receivers); QVERIFY(target.valueDescriptions == original.valueDescriptions); QCOMPARE(target.multiplexing.role, original.multiplexing.role); @@ -547,93 +595,122 @@ void CoverageTest::builders() { } void CoverageTest::physicalConversion_data() { - QTest::addColumn("factor"); QTest::addColumn("offset"); QTest::addColumn("expected"); + QTest::addColumn("factor"); + QTest::addColumn("offset"); + QTest::addColumn("expected"); QTest::newRow("zero-factor") << 0.0 << 7.0 << 7.0; QTest::newRow("negative-factor") << -2.0 << 5.0 << -15.0; QTest::newRow("above-declared-range") << 2.0 << 3.0 << 23.0; } void CoverageTest::physicalConversion() { - QFETCH(double, factor); QFETCH(double, offset); QFETCH(double, expected); - DecodeSignal signal; signal.length = 8U; signal.factor = factor; signal.offset = offset; - signal.minimum = 0.0; signal.maximum = 1.0; - const auto database = SingleSignal(signal); - RawCanFrame raw; raw.canId = 17U; raw.data = {10U}; - const auto decoded = DbcDecoder().Decode(database, raw); + QFETCH(double, factor); + QFETCH(double, offset); + QFETCH(double, expected); + DecodeSignal signal; + signal.length = 8U; + signal.factor = factor; + signal.offset = offset; + signal.minimum = 0.0; + signal.maximum = 1.0; + const DecodeDatabase database = SingleSignal(signal); + RawCanFrame raw; + raw.canId = 17U; + raw.data = {10U}; + const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw); QVERIFY(decoded.signals[0].valid); QCOMPARE(std::get(decoded.signals[0].rawValue), std::uint64_t(10)); QCOMPARE(decoded.signals[0].physicalValue, expected); } void CoverageTest::multiplexingScenarios() { - const auto selectorOnly = DbcDecodeBuilder().Build(ParseText( - "BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n")); - RawCanFrame raw; raw.canId = 1U; raw.data = {1U}; - const auto only = DbcDecoder().Decode(selectorOnly, raw); + const DecodeDatabase selectorOnly = DbcDecodeBuilder().Build(ParseText( + "BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n")); + RawCanFrame raw; + raw.canId = 1U; + raw.data = {1U}; + const DecodedFrameValue only = DbcDecoder().Decode(selectorOnly, raw); QCOMPARE(only.signals.size(), std::size_t(1)); QVERIFY(only.signals[0].valid); - const auto database = DbcDecodeBuilder().Build(ParseText( - "BO_ 1 First: 3 ECU\n" - "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" - "SG_ A m0 : 8|8@1+ (1,0) [0|255] \"\" ECU\n" - "SG_ B m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n" - "BO_ 2 Second: 2 ECU\n" - "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" - "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n")); + const DecodeDatabase database = DbcDecodeBuilder().Build(ParseText( + "BO_ 1 First: 3 ECU\n" + "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" + "SG_ A m0 : 8|8@1+ (1,0) [0|255] \"\" ECU\n" + "SG_ B m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n" + "BO_ 2 Second: 2 ECU\n" + "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" + "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n")); raw.data = {0U, 42U, 99U}; - const auto first = DbcDecoder().Decode(database, raw); + const DecodedFrameValue first = DbcDecoder().Decode(database, raw); QVERIFY(first.signals[1].valid && first.signals[2].valid); QCOMPARE(std::get(first.signals[1].rawValue), std::uint64_t(42)); QCOMPARE(std::get(first.signals[2].rawValue), std::uint64_t(99)); raw.canId = 2U; - const auto secondInactive = DbcDecoder().Decode(database, raw); + const DecodedFrameValue secondInactive = DbcDecoder().Decode(database, raw); QCOMPARE(secondInactive.signals[1].status, SignalDecodeStatus::Inactive); raw.data[0] = 1U; QVERIFY(DbcDecoder().Decode(database, raw).signals[1].valid); raw.canId = 1U; - const auto firstInactive = DbcDecoder().Decode(database, raw); + const DecodedFrameValue firstInactive = DbcDecoder().Decode(database, raw); QCOMPARE(firstInactive.signals[1].status, SignalDecodeStatus::Inactive); QCOMPARE(firstInactive.signals[2].status, SignalDecodeStatus::Inactive); } void CoverageTest::multiplexingErrorLocations_data() { - QTest::addColumn("text"); QTest::addColumn("line"); QTest::addColumn("message"); + QTest::addColumn("text"); + QTest::addColumn("line"); + QTest::addColumn("message"); const QString header = "BO_ 1 Frame: 3 ECU\n\n"; const QString selector = "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"; const QString branch = "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"; QTest::newRow("missing-selector") << header + branch << 3U << QString::fromUtf8("Multiplexed branch has no selector."); - QTest::newRow("second-selector") << header + selector + "\nSG_ Other M : 16|1@1+ (1,0) [0|1] \"\" ECU\n" << 5U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame."); - QTest::newRow("signed-selector") << header + "SG_ Mode M : 0|1@1- (1,0) [-1|0] \"\" ECU\n" << 3U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame."); + QTest::newRow("second-selector") << header + selector + "\nSG_ Other M : 16|1@1+ (1,0) [0|1] \"\" ECU\n" + << 5U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame."); + QTest::newRow("signed-selector") << header + "SG_ Mode M : 0|1@1- (1,0) [-1|0] \"\" ECU\n" + << 3U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame."); QTest::newRow("branch-range") << header + "SG_ A m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n" + selector << 3U << QString::fromUtf8("Branch value exceeds selector raw range."); - QTest::newRow("same-branch-overlap") << header + selector + branch + "SG_ B m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n" << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); - QTest::newRow("selector-overlap") << header + selector + "SG_ A m1 : 0|1@1+ (1,0) [0|1] \"\" ECU\n" << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); - QTest::newRow("unconditional-overlap") << header + selector + branch + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n" << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); - QTest::newRow("ordinary-overlap") << header + "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\nSG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n" << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); - QTest::newRow("extended-multiplexing") << header + "SG_MUL_VAL_ 1 A Mode 1-2;\n" << 3U << QString::fromUtf8("Extended and nested multiplexing are unsupported."); - QTest::newRow("nested-marker") << header + "SG_ A m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n" << 3U << QString::fromUtf8("Invalid or unsupported multiplexing marker."); + QTest::newRow("same-branch-overlap") << header + selector + branch + "SG_ B m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n" + << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); + QTest::newRow("selector-overlap") << header + selector + "SG_ A m1 : 0|1@1+ (1,0) [0|1] \"\" ECU\n" + << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); + QTest::newRow("unconditional-overlap") << header + selector + branch + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n" + << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); + QTest::newRow("ordinary-overlap") << header + "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\nSG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n" + << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); + QTest::newRow("extended-multiplexing") << header + "SG_MUL_VAL_ 1 A Mode 1-2;\n" + << 3U << QString::fromUtf8("Extended and nested multiplexing are unsupported."); + QTest::newRow("nested-marker") << header + "SG_ A m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n" + << 3U << QString::fromUtf8("Invalid or unsupported multiplexing marker."); } void CoverageTest::multiplexingErrorLocations() { - QFETCH(QString, text); QFETCH(unsigned, line); QFETCH(QString, message); - QTemporaryFile file; QVERIFY(file.open()); + QFETCH(QString, text); + QFETCH(unsigned, line); + QFETCH(QString, message); + QTemporaryFile file; + QVERIFY(file.open()); const QByteArray content = text.toUtf8(); - QCOMPARE(file.write(content), qint64(content.size())); QVERIFY(file.flush()); - try { DbcParser().ParseFile(file.fileName().toStdString()); QFAIL("Expected multiplexing rejection"); } - catch (const std::runtime_error &error) { + QCOMPARE(file.write(content), qint64(content.size())); + QVERIFY(file.flush()); + try { + DbcParser().ParseFile(file.fileName().toStdString()); + QFAIL("Expected multiplexing rejection"); + } catch (const std::runtime_error &error) { QCOMPARE(QString::fromStdString(error.what()), file.fileName() + ":" + QString::number(line) + ": " + message); } } void CoverageTest::escapedValueDescriptions() { - const auto parsed = ParseText( - "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" - R"dbc(VAL_ 1 State -0 "Ноль" 1 "C:\\trace" 2 "\\\"quoted\"\\";)dbc" "\n"); - const auto &values = parsed.frames[0].signals[0].valueDescriptions; + const DbcDatabase parsed = ParseText( + "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" + R"dbc(VAL_ 1 State -0 "Ноль" 1 "C:\\trace" 2 "\\\"quoted\"\\";)dbc" + "\n"); + const ValueDescriptions &values = parsed.frames[0].signals[0].valueDescriptions; QCOMPARE(values.size(), std::size_t(3)); QVERIFY(values.find(RawSignalValue(std::int64_t(0))) == values.end()); - const auto *zero = FindValueDescription(values, std::int64_t(0)); - const auto *path = FindValueDescription(values, std::uint64_t(1)); - const auto *quoted = FindValueDescription(values, std::uint64_t(2)); + const std::string *zero = FindValueDescription(values, std::int64_t(0)); + const std::string *path = FindValueDescription(values, std::uint64_t(1)); + const std::string *quoted = FindValueDescription(values, std::uint64_t(2)); QVERIFY(zero && path && quoted); QCOMPARE(*zero, std::string("Ноль")); QCOMPARE(*path, std::string("C:\\trace")); @@ -649,11 +726,11 @@ void CoverageTest::factory() { QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast(factory.create("missing", std::make_unique(1)))); factory.registerType("value"); QVERIFY(factory.contains("value")); - auto argument = std::make_unique(42); - const auto first = factory.create("value", std::move(argument)); + std::unique_ptr argument = std::make_unique(42); + const std::shared_ptr first = factory.create("value", std::move(argument)); QVERIFY(argument == nullptr); QCOMPARE(first->Value(), 42); - const auto creator = factory.creator("value"); + const TypeFactory>::CreatorFn creator = factory.creator("value"); QCOMPARE(creator(std::make_unique(7))->Value(), 7); factory.registerType("value"); QCOMPARE(factory.create("value", std::make_unique(42))->Value(), 43); diff --git a/tests/dbc_tree_model_test.cpp b/tests/dbc_tree_model_test.cpp index 37794b8..e33c5a1 100644 --- a/tests/dbc_tree_model_test.cpp +++ b/tests/dbc_tree_model_test.cpp @@ -2,9 +2,10 @@ #include "dbc_tree_builder.h" #include "dbc_tree_model.h" #include +#include #include -#include #include +#include #include #include @@ -50,8 +51,8 @@ private Q_SLOTS: const QStringList headers = {"Name", "CAN ID", "Payload (bytes)", "Start bit", "Length (bits)", "Byte order", "Signedness", "Unit", "Comment"}; for (int column = 0; column < model.columnCount(); ++column) { - const auto frameIndex = model.index(0, column); - const auto signalIndex = model.index(0, column, parent); + const QModelIndex frameIndex = model.index(0, column); + const QModelIndex signalIndex = model.index(0, column, parent); QCOMPARE(model.headerData(column, Qt::Horizontal).toString(), headers.at(column)); QVERIFY(!model.headerData(column, Qt::Horizontal, Qt::ToolTipRole).isValid()); QCOMPARE(model.data(frameIndex, Qt::ToolTipRole).toString(), QString("Frame tooltip")); @@ -66,7 +67,7 @@ private Q_SLOTS: QCOMPARE(model.data(signalIndex, DbcTreeModel::LittleEndianRole).toBool(), false); QCOMPARE(model.data(signalIndex, DbcTreeModel::SignedRole).toBool(), true); QCOMPARE(model.data(signalIndex, DbcTreeModel::UnitRole).toString(), QString("rpm")); - const auto descriptions = model.data(signalIndex, DbcTreeModel::ValueDescriptionsRole); + const QVariant descriptions = model.data(signalIndex, DbcTreeModel::ValueDescriptionsRole); QVERIFY(descriptions.isValid()); QVERIFY(descriptions.toList().isEmpty()); QVERIFY(!model.data(frameIndex, Qt::UserRole + 1000).isValid()); @@ -203,5 +204,12 @@ private Q_SLOTS: QCOMPARE(resets.count(), 4); } }; -QTEST_GUILESS_MAIN(ModelTest) +int main(int argc, char **argv) { + // Model tests need QtGui's metatypes, but do not need a display server. + if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) + qputenv("QT_QPA_PLATFORM", "offscreen"); + QGuiApplication application(argc, argv); + ModelTest tests; + return QTest::qExec(&tests, argc, argv); +} #include "dbc_tree_model_test.moc" diff --git a/tests/model_tests.pro b/tests/model_tests.pro index b1413f0..7eb0b44 100644 --- a/tests/model_tests.pro +++ b/tests/model_tests.pro @@ -1,7 +1,6 @@ TEMPLATE = app TARGET = dbc_model_tests -QT += core testlib -QT -= gui +QT += core gui testlib CONFIG += console testcase c++17 no_keywords CONFIG -= app_bundle debug_and_release debug_and_release_target include(../qt/qt.pri) diff --git a/tests/tests.pro b/tests/tests.pro index 13abad9..e58e53e 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,4 +1,5 @@ TEMPLATE = app +include(../qt/qt_version.pri) TARGET = dbc_tests QT += core testlib QT -= gui