This commit is contained in:
deeaitch committed 2026-10-07 20:18:37 -04:00
1 parent ef8e21db2e
commit a6016248f8
11 files changed
+529 -252

No files matched your search

+155 -61
View File
@@ -20,8 +20,10 @@ The implementation follows a simple and practical design:
- Qt is not required at the parser or decoder level - Qt is not required at the parser or decoder level
- the same decode engine can be reused for live traffic and trace replay - 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 The core library uses C++17 and has no Qt dependency. Qt integration targets the
used by the regression tests; the optional adapter requires Qt Core. Live CAN acquisition, trace-file readers, 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/`. 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, 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: Use `std::get` when the numeric type is known, or `std::get_if` to handle both:
```cpp ```cpp
if (const auto *value = std::get_if<std::int64_t>(&decodedSignal.rawValue)) { if (const std::int64_t *value = std::get_if<std::int64_t>(&decodedSignal.rawValue)) {
std::cout << *value; std::cout << *value;
} else { } else {
std::cout << std::get<std::uint64_t>(decodedSignal.rawValue); std::cout << std::get<std::uint64_t>(decodedSignal.rawValue);
@@ -854,6 +856,7 @@ lines:
- `SG_` → `DbcSignalHandler` - `SG_` → `DbcSignalHandler`
- `CM_` → `DbcCommentHandler` - `CM_` → `DbcCommentHandler`
- `VAL_` → `DbcValueDescriptionHandler` - `VAL_` → `DbcValueDescriptionHandler`
- `SG_MUL_VAL_` → `DbcUnsupportedMultiplexingHandler`
Grammar implementations live in `core/dbc_statement_handler.cpp`; their interface, Grammar implementations live in `core/dbc_statement_handler.cpp`; their interface,
context, and dispatcher live in `core/dbc_statement_handler.h`. `core/typefactory.h` context, and dispatcher live in `core/dbc_statement_handler.h`. `core/typefactory.h`
@@ -875,8 +878,8 @@ RegisterType<MyCustomHandler>("CUSTOM_"); // after implementing MyCustomHandler
Registration creates one handler; replacing a registration replaces that instance. Registration creates one handler; replacing a registration replaces that instance.
Register during initialization before dispatching. A line is routed using its Register during initialization before dispatching. A line is routed using its
full first whitespace-delimited keyword, so `BO_TX_BU_` cannot match `BO_`. 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 Unregistered keywords remain ignored. `SG_` and `CM_` accept spaces and tabs
policy requiring a literal space. after their keywords.
This factory handles statements inside DBC files. A separate format-level This factory handles statements inside DBC files. A separate format-level
factory can be introduced when another file format is implemented. The decoder 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 - unregistered DBC keywords are ignored
- an `SG_` statement before the first frame is rejected - an `SG_` statement before the first frame is rejected
- duplicate frame and signal definitions are not rejected globally; ambiguous - duplicate frame keys (normalized CAN ID and frame type) and repeated signal
`VAL_` references to them are rejected 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 - payload lengths are checked against storage bounds (`0..255`), rather than
CAN or CAN FD protocol limits CAN or CAN FD protocol limits
- comment handling is limited to the supported forms and already parsed targets - 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 systems can compile the sources listed in `core/core_files.pri`; no CMake
configuration is currently provided. 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`. 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. `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. `tests/tests.pro` builds the QtTest executable `dbc_tests` and links that library.
The root project declares `tests.depends = core` for parallel builds. 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 ```sh
mkdir build mkdir build
@@ -1061,8 +1068,15 @@ qmake is still the build tool; this target does not link Qt libraries.
### Optional Qt model adapter ### Optional Qt model adapter
Requires Qt 5.11 or newer (including Qt 6) for `QAbstractItemModelTester`. The adapter and its `QAbstractItemModelTester` suite use Qt 6.8.2.
The adapter itself links only Qt Core. Enable its library and focused QtTest suite: 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 ```sh
mkdir -p build-qt 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. // Run after constructing QApplication; view owns the model through QObject.
QTreeView view; QTreeView view;
auto *model = new DbcTreeModel(&view); DbcTreeModel *model = new DbcTreeModel(&view);
model->SetTree(DbcTreeBuilder().Build(DbcParser().ParseFile("example.dbc"))); model->SetTree(DbcTreeBuilder().Build(DbcParser().ParseFile("example.dbc")));
view.setModel(model); view.setModel(model);
view.expandAll(); view.expandAll();
@@ -1207,7 +1221,7 @@ sudo apt install qtcreator
GCC supplies `gcov`; the `lcov` package supplies `lcov` and `genhtml`. Use the 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 `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 From the repository root, create a fresh coverage build. `--coverage` must be
passed to both compilation and linking; Debug avoids optimization that can make 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 (724/740) and 70.7% branch coverage (833/1178). These are a snapshot, not fixed
expected values for later changes or other toolchains. 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) 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). and the [LCOV project documentation](https://github.com/linux-test-project/lcov).
### Configure the GCC coverage build in Qt Creator ### 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**. 2. In **Projects > Build Settings**, clone **Debug** as **Coverage**.
3. Choose a separate build directory, such as `build-coverage`. 3. Choose a separate build directory, such as `build-coverage`.
4. Expand the **qmake** build step and add these **Additional arguments**: 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 repository path if using a new terminal. `make check` runs both QtTest
executables; the standalone tree executable must be run separately. executables; the standalone tree executable must be run separately.
7. View `html/index.html` in a browser. This workflow produces HTML reports; 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 The local `dbc.pro.user` stores machine-specific kit IDs and build/run settings.
install CoverageScanner, CoverageBrowser, or activate a Coco license. Obtain a It is ignored by Git (`*.user`) and should not be committed or copied between
Coco license or evaluation license and download the Linux installer from machines. Recreate the configurations below through the UI when setting up a
[Qt Account downloads](https://account.qt.io/s/downloads). 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 The local configuration set uses these independent build directories:
tools, and IDE before installing Coco:
```sh | Configuration | Build directory example | Purpose |
sudo apt install build-essential qt6-base-dev qt6-base-dev-tools qmake6 qtcreator | --- | --- | --- |
# Coco's installer uses locate to discover compilers on Unix: | Debug | `/tmp/build/dbc/Desktop-Debug` | Core library and parser/decoder/coverage regression tests |
sudo apt install plocate | Release | `/tmp/build/dbc/Desktop-Release` | The same targets with release settings |
sudo updatedb | 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 To add the ordinary qmake configurations:
bash /path/to/SquishCocoSetup_VERSION_Linux_x86_64.run
```
The exact filename and supported Linux package depend on the downloaded release. 1. Open `dbc.pro` with a GCC/Qt 6.8.2 kit.
Coco's own tools come from this installer; `lcov` is not required for Coco. 2. Go to **Projects > Build Settings**. Keep the existing Debug, Release, and
Installing or configuring a new compiler later requires rerunning the installer Profile configurations. Remove or disable a Coco build step if one remains.
or using `cocosetup` to register the compiler. 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 ```text
`cocolic` executable (use its full path if it is not on `PATH`): CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release "QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path" "QMAKE_LFLAGS+=--coverage"
```
```sh 7. For Coverage, select **Add Build Step > Custom Process Step** after qmake
cocolic --setup-qls and Make, then set:
cocolic --check
```
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 This script compiles the standalone tree test, resets counters, runs both
restart Qt Creator if requested. The Debian Qt Creator 16.0.1 package used for QtTest executables and the tree test, and generates `%{buildDir}/html/index.html`.
this project's coverage check already includes `libCoco.so`. It stops if any test fails. Therefore building Coverage also executes tests.
2. In **Preferences > Coco**, set the Coco installation directory. `lcov` and `genhtml` must be installed. Use this configuration with the GCC kit;
3. Clone **Debug** as **DebugCoco**, with its own build directory. Use an ordinary `gcov` and the compiler used for the standalone test must match that kit.
Debug configuration as the starting point, without GCC `--coverage` flags. 8. Run qmake and build each new configuration once. The default root builds
4. In **Projects > Project Settings > Coco Code Coverage**, enable coverage for expose `dbc_tests`; model configurations also expose `dbc_model_tests`.
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.
For qmake, the plugin generates `cocoplugin.prf` in the source directory. Coco Qt Creator expands `%{sourceDir}` and `%{buildDir}` in custom steps to the source
must instrument the library subprojects as well as the test executables. and active build directories. Use the field's variable chooser if needed.
The GCC/LCOV commands above do not collect Coco data.
See the official [Coco installation and license setup](https://doc.qt.io/coco/installation.html), For the three independent builds, clone Debug, assign the name and build
[Coco license requirements](https://doc.qt.io/coco/coco-license-utilization.html), directory from the table, and replace its qmake/Make build steps with the custom
and [Qt Creator Coco configuration](https://doc.qt.io/qtcreator/creator-coco.html). 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).
+25
View File
@@ -10,6 +10,18 @@ existing Git tag; subsequent changes are listed as unreleased.
#### Added #### 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. - Basic `M` / `mN` multiplexing metadata in parsed, tree, and runtime definitions.
- Deferred selector and overlap validation with source locations: one unsigned - Deferred selector and overlap validation with source locations: one unsigned
selector, representable branch values, and overlaps only between exclusive branches. selector, representable branch values, and overlaps only between exclusive branches.
@@ -22,10 +34,23 @@ existing Git tag; subsequent changes are listed as unreleased.
#### Changed #### 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. - Overlapping unconditional signals are now rejected during parsing.
#### Fixed #### 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 - Reject duplicate frames by normalized CAN ID and frame type, including
equivalent extended ID encodings, and duplicate signal names within a frame. equivalent extended ID encodings, and duplicate signal names within a frame.
Errors report the file path and line of the repeated definition. Standard and Errors report the file path and line of the repeated definition. Standard and
+1
View File
@@ -1,3 +1,4 @@
include($$PWD/qt_version.pri)
include($$PWD/../core/core.pri) include($$PWD/../core/core.pri)
QT += core QT += core
INCLUDEPATH += $$PWD INCLUDEPATH += $$PWD
+1
View File
@@ -1,4 +1,5 @@
TEMPLATE = lib TEMPLATE = lib
include(qt_version.pri)
TARGET = dbc_qt TARGET = dbc_qt
CONFIG += staticlib c++17 CONFIG += staticlib c++17
QT += core QT += core
+2
View File
@@ -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")
+33 -8
View File
@@ -1,14 +1,20 @@
# QtTest regression tests # QtTest regression tests
For instrumented builds, LCOV HTML reports, Qt Creator coverage configuration, For instrumented builds, LCOV HTML reports, and Qt Creator coverage configuration,
and Coco installation, see [Measuring code coverage](../README.md#measuring-code-coverage). 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 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; C++17 library) and `tests` (the `dbc_tests` QtTest executable). Core is built first;
tests link its archive rather than compiling its implementation again. tests link its archive rather than compiling its implementation again.
Qt Core and Qt Test development packages are required for tests only. 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 ```sh
mkdir build mkdir build
@@ -18,8 +24,8 @@ make -j
make check make check
``` ```
Or run `./tests/dbc_tests` directly. Both parser and decoder suites run, and the Or run `./tests/dbc_tests` directly. Parser, decoder, and additional coverage
executable returns nonzero when either suite fails. Decoder cases appear as 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. individual QtTest data rows; parser checks are grouped into named test slots.
Checks also run in release builds. Checks also run in release builds.
@@ -34,7 +40,8 @@ make
make check 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. `../core/core.pri` supplies the library include path and linkage.
## Decoder coverage ## Decoder coverage
@@ -73,14 +80,32 @@ README for its compiler command. It checks parents, sibling positions, null
children, bounds, and unique_ptr transfer. children, bounds, and unique_ptr transfer.
Enable `CONFIG+=dbc_qt` in the root qmake invocation to build `dbc_qt` and 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 `make check` runs all suites. Model tests parse a temporary DBC fixture and use
QAbstractItemModelTester, verify navigation across columns, invalid/foreign QAbstractItemModelTester, verify navigation across columns, invalid/foreign
indexes, metadata and exact VAL_ integers, and persistent-index invalidation 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, Basic multiplexing coverage includes selector declarations after branches,
Intel/Motorola branch overlap, signed active values and VAL_ lookup, unconditional Intel/Motorola branch overlap, signed active values and VAL_ lookup, unconditional
signals, inactive branches, missing selector/branch payloads, exact UINT64_MAX signals, inactive branches, missing selector/branch payloads, exact UINT64_MAX
selector matching, malformed runtime selectors, source locations, and rejection selector matching, malformed runtime selectors, source locations, and rejection
of invalid markers, selector references/ranges, overlaps, and SG_MUL_VAL_. 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.
+44
View File
@@ -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"
+253 -176
View File
@@ -1,64 +1,65 @@
// Additional regression coverage. Kept separate for review before the first run. // Additional regression coverage. Kept separate for review before the first run.
#include "dbc_parser.h"
#include "dbc_decode_builder.h" #include "dbc_decode_builder.h"
#include "dbc_decoder.h" #include "dbc_decoder.h"
#include "dbc_tree_builder.h" #include "dbc_parser.h"
#include "dbc_statement_handler.h" #include "dbc_statement_handler.h"
#include "dbc_tree_builder.h"
#include "typefactory.h" #include "typefactory.h"
#include <QtTest>
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QtTest>
#include <array> #include <array>
#include <limits> #include <limits>
#include <stdexcept> #include <stdexcept>
namespace { namespace {
DbcDatabase ParseText(const std::string &text) { DbcDatabase ParseText(const std::string &text) {
QTemporaryFile file; QTemporaryFile file;
if (!file.open()) throw std::runtime_error("Cannot create DBC fixture"); if (!file.open()) throw std::runtime_error("Cannot create DBC fixture");
if (file.write(text.data(), static_cast<qint64>(text.size())) != static_cast<qint64>(text.size()) || !file.flush()) if (file.write(text.data(), static_cast<qint64>(text.size())) != static_cast<qint64>(text.size()) || !file.flush())
throw std::runtime_error("Cannot write DBC fixture"); throw std::runtime_error("Cannot write DBC fixture");
return DbcParser().ParseFile(file.fileName().toStdString()); 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<std::uint8_t> &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 { DecodeDatabase SingleSignal(const DecodeSignal &signal) {
virtual ~FactoryBase() = default; DecodeDatabase database;
virtual int Value() const = 0; DecodeFrame frame;
}; frame.canId = 17U;
struct FactoryFirst : FactoryBase { frame.signals.push_back(signal);
explicit FactoryFirst(std::unique_ptr<int> value) : value(std::move(value)) {} database.frames.push_back(frame);
int Value() const override { return *value; } database.frameIndexByKey.emplace(FrameKey(17U, false), 0U);
std::unique_ptr<int> value; return database;
}; }
struct FactorySecond : FactoryFirst {
using FactoryFirst::FactoryFirst; // Independent reference: collect the visited bits first, then weight them.
int Value() const override { return *value + 1; } // Motorola uses a linear MSB-first coordinate instead of sawtooth traversal.
}; std::uint64_t ReferenceExtract(const std::vector<std::uint8_t> &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<int> value) : value(std::move(value)) {}
int Value() const override { return *value; }
std::unique_ptr<int> value;
};
struct FactorySecond : FactoryFirst {
using FactoryFirst::FactoryFirst;
int Value() const override { return *value + 1; }
};
}// namespace
class CoverageTest : public QObject { class CoverageTest : public QObject {
Q_OBJECT Q_OBJECT
@@ -100,12 +101,12 @@ void CoverageTest::frameLookup() {
raw.canId = 17U; raw.canId = 17U;
DecodeDatabase empty; DecodeDatabase empty;
QVERIFY(decoder.FindFrame(empty, 17U, false) == nullptr); 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.valid);
QVERIFY(absent.definition == nullptr); QVERIFY(absent.definition == nullptr);
QVERIFY(absent.signals.empty()); QVERIFY(absent.signals.empty());
DecodeSignal signal; DecodeSignal signal;
auto database = SingleSignal(signal); DecodeDatabase database = SingleSignal(signal);
QVERIFY(decoder.FindFrame(database, 18U, false) == nullptr); QVERIFY(decoder.FindFrame(database, 18U, false) == nullptr);
QVERIFY(decoder.FindFrame(database, 17U, true) == nullptr); QVERIFY(decoder.FindFrame(database, 17U, true) == nullptr);
raw.isExtended = true; raw.isExtended = true;
@@ -119,7 +120,7 @@ void CoverageTest::frameLookup() {
QVERIFY(!decoder.Decode(database, raw).valid); QVERIFY(!decoder.Decode(database, raw).valid);
database.frameIndexByKey.at(FrameKey(17U, false)) = 0U; database.frameIndexByKey.at(FrameKey(17U, false)) = 0U;
database.frames[0].signals.clear(); database.frames[0].signals.clear();
const auto matched = decoder.Decode(database, raw); const DecodedFrameValue matched = decoder.Decode(database, raw);
QVERIFY(matched.valid); QVERIFY(matched.valid);
QCOMPARE(matched.definition, &database.frames[0]); QCOMPARE(matched.definition, &database.frames[0]);
QVERIFY(matched.signals.empty()); QVERIFY(matched.signals.empty());
@@ -130,11 +131,11 @@ void CoverageTest::signedBoundaries_data() {
QTest::addColumn<unsigned>("length"); QTest::addColumn<unsigned>("length");
QTest::addColumn<qlonglong>("expected"); QTest::addColumn<qlonglong>("expected");
for (int order = 0; order < 2; ++order) { 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::int64_t half = std::int64_t(1) << (length - 1U);
const std::array<std::int64_t, 4> values{{0, -1, -half, half - 1}}; const std::array<std::int64_t, 4> values{{0, -1, -half, half - 1}};
for (unsigned index = 0; index < values.size(); ++index) { 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]); 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; const unsigned position = order == 0 ? i % 8U : 7U - i % 8U;
raw.data[i / 8U] |= static_cast<std::uint8_t>(((bits >> weight) & 1U) << position); raw.data[i / 8U] |= static_cast<std::uint8_t>(((bits >> weight) & 1U) << position);
} }
const auto database = SingleSignal(signal); const DecodeDatabase database = SingleSignal(signal);
const auto result = DbcDecoder().Decode(database, raw); const DecodedFrameValue result = DbcDecoder().Decode(database, raw);
QVERIFY(result.signals[0].valid); QVERIFY(result.signals[0].valid);
QVERIFY(std::holds_alternative<std::int64_t>(result.signals[0].rawValue)); QVERIFY(std::holds_alternative<std::int64_t>(result.signals[0].rawValue));
QCOMPARE(std::get<std::int64_t>(result.signals[0].rawValue), static_cast<std::int64_t>(expected)); QCOMPARE(std::get<std::int64_t>(result.signals[0].rawValue), static_cast<std::int64_t>(expected));
@@ -173,7 +174,7 @@ void CoverageTest::extraction_data() {
for (int order = 0; order < 2; ++order) for (int order = 0; order < 2; ++order)
for (unsigned start = 0; start < 16U; ++start) for (unsigned start = 0; start < 16U; ++start)
for (unsigned length = 1; length <= 64U; ++length) { 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; QTest::newRow(name.constData()) << order << start << length;
} }
} }
@@ -186,17 +187,18 @@ void CoverageTest::extraction() {
signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola;
signal.startBit = start; signal.startBit = start;
signal.length = length; signal.length = length;
const auto database = SingleSignal(signal); const DecodeDatabase database = SingleSignal(signal);
RawCanFrame raw; RawCanFrame raw;
raw.canId = 17U; raw.canId = 17U;
const std::vector<std::vector<std::uint8_t>> patterns = { const std::vector<std::vector<std::uint8_t>> patterns = {
std::vector<std::uint8_t>(10U, 0U), std::vector<std::uint8_t>(10U, 255U), std::vector<std::uint8_t>(10U, 0U),
{0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}}; std::vector<std::uint8_t>(10U, 255U),
for (const auto &data : patterns) { {0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}};
for (const std::vector<std::uint8_t> &data: patterns) {
raw.data = data; raw.data = data;
const unsigned firstCoordinate = order == 0 ? start : (start / 8U) * 8U + 7U - start % 8U; const unsigned firstCoordinate = order == 0 ? start : (start / 8U) * 8U + 7U - start % 8U;
raw.data.resize((firstCoordinate + length + 7U) / 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); QVERIFY(decoded.signals[0].valid);
QCOMPARE(std::get<std::uint64_t>(decoded.signals[0].rawValue), ReferenceExtract(data, signal.byteOrder, start, length)); QCOMPARE(std::get<std::uint64_t>(decoded.signals[0].rawValue), ReferenceExtract(data, signal.byteOrder, start, length));
} }
@@ -210,7 +212,7 @@ void CoverageTest::invalidSignalStatus_data() {
QTest::addColumn<int>("status"); QTest::addColumn<int>("status");
for (int order = 0; order < 2; ++order) { for (int order = 0; order < 2; ++order) {
const unsigned start = order == 0 ? 0U : 7U; 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 + "-zero").constData()) << order << start << 0U << 1U << int(SignalDecodeStatus::InvalidDefinition);
QTest::newRow((prefix + "-65").constData()) << order << start << 65U << 9U << 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); QTest::newRow((prefix + "-empty").constData()) << order << start << 8U << 0U << int(SignalDecodeStatus::InsufficientPayload);
@@ -220,14 +222,20 @@ void CoverageTest::invalidSignalStatus_data() {
} }
void CoverageTest::invalidSignalStatus() { void CoverageTest::invalidSignalStatus() {
QFETCH(int, order); QFETCH(unsigned, start); QFETCH(unsigned, length); QFETCH(int, order);
QFETCH(unsigned, bytes); QFETCH(int, status); QFETCH(unsigned, start);
QFETCH(unsigned, length);
QFETCH(unsigned, bytes);
QFETCH(int, status);
DecodeSignal signal; DecodeSignal signal;
signal.startBit = start; signal.length = length; signal.startBit = start;
signal.length = length;
signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola;
const auto database = SingleSignal(signal); const DecodeDatabase database = SingleSignal(signal);
RawCanFrame raw; raw.canId = 17U; raw.data.assign(bytes, 255U); RawCanFrame raw;
const auto decoded = DbcDecoder().Decode(database, raw); raw.canId = 17U;
raw.data.assign(bytes, 255U);
const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw);
QVERIFY(decoded.valid); QVERIFY(decoded.valid);
QVERIFY(!decoded.signals[0].valid); QVERIFY(!decoded.signals[0].valid);
QCOMPARE(int(decoded.signals[0].status), status); QCOMPARE(int(decoded.signals[0].status), status);
@@ -244,19 +252,28 @@ void CoverageTest::malformedRuntimeMultiplexing_data() {
void CoverageTest::malformedRuntimeMultiplexing() { void CoverageTest::malformedRuntimeMultiplexing() {
QFETCH(int, kind); QFETCH(int, kind);
DecodeSignal selector; selector.length = 1U; selector.multiplexing.role = MultiplexRole::Selector; DecodeSignal selector;
auto database = SingleSignal(selector); selector.length = 1U;
DecodeSignal branch; branch.startBit = 8U; branch.length = 8U; selector.multiplexing.role = MultiplexRole::Selector;
branch.multiplexing.role = MultiplexRole::Branch; branch.multiplexing.value = 1U; 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); 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); database.frames[0].signals.push_back(ordinary);
if (kind == 0) database.frames[0].signals.push_back(selector); if (kind == 0) database.frames[0].signals.push_back(selector);
if (kind == 1) database.frames[0].signals[0].valueType = ValueType::Signed; if (kind == 1) database.frames[0].signals[0].valueType = ValueType::Signed;
if (kind == 2) database.frames[0].signals[0].length = 65U; if (kind == 2) database.frames[0].signals[0].length = 65U;
if (kind == 3) database.frames[0].signals[1].multiplexing.value = 2U; if (kind == 3) database.frames[0].signals[1].multiplexing.value = 2U;
RawCanFrame raw; raw.canId = 17U; raw.data = {1U, 42U, 99U}; RawCanFrame raw;
const auto result = DbcDecoder().Decode(database, 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)); QCOMPARE(int(result.signals[1].status), int(SignalDecodeStatus::InvalidDefinition));
QVERIFY(!result.signals[1].valid); QVERIFY(!result.signals[1].valid);
QVERIFY(result.signals[2].valid); QVERIFY(result.signals[2].valid);
@@ -268,28 +285,34 @@ void CoverageTest::malformedRuntimeMultiplexing() {
} }
void CoverageTest::pgn_data() { void CoverageTest::pgn_data() {
QTest::addColumn<unsigned>("id"); QTest::addColumn<unsigned>("expected"); QTest::addColumn<bool>("hasPgn"); QTest::addColumn<unsigned>("id");
QTest::addColumn<unsigned>("expected");
QTest::addColumn<bool>("hasPgn");
QTest::newRow("standard") << 0x7FFU << 0U << false; QTest::newRow("standard") << 0x7FFU << 0U << false;
for (unsigned dp : {0U, 1U}) for (unsigned dp: {0U, 1U})
for (unsigned pf : {239U, 240U}) for (unsigned pf: {239U, 240U})
for (unsigned ps : {0U, 0xABU}) { for (unsigned ps: {0U, 0xABU}) {
const unsigned id = 0x80000000U | (dp << 24U) | (pf << 16U) | (ps << 8U) | 0x42U; const unsigned id = 0x80000000U | (dp << 24U) | (pf << 16U) | (ps << 8U) | 0x42U;
const unsigned expected = (dp << 16U) | (pf == 239U ? 0xEF00U : 0xF000U | ps); 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; QTest::newRow(name.constData()) << id << expected << true;
} }
} }
void CoverageTest::pgn() { void CoverageTest::pgn() {
QFETCH(unsigned, id); QFETCH(unsigned, expected); QFETCH(bool, hasPgn); QFETCH(unsigned, id);
const auto parsed = ParseText("BO_ " + std::to_string(id) + " Frame: 8 ECU\n"); 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].hasPgn, hasPgn);
QCOMPARE(parsed.frames[0].pgn, expected); QCOMPARE(parsed.frames[0].pgn, expected);
} }
void CoverageTest::comments_data() { void CoverageTest::comments_data() {
QTest::addColumn<QString>("before"); QTest::addColumn<QString>("after"); QTest::addColumn<QString>("before");
QTest::addColumn<QString>("frameComment"); QTest::addColumn<QString>("signalComment"); QTest::addColumn<QString>("after");
QTest::addColumn<QString>("frameComment");
QTest::addColumn<QString>("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("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("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"); 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() { void CoverageTest::comments() {
QFETCH(QString, before); QFETCH(QString, after); QFETCH(QString, frameComment); QFETCH(QString, signalComment); QFETCH(QString, before);
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, 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].comment, frameComment.toStdString());
QCOMPARE(parsed.frames[0].signals[0].comment, signalComment.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"); QFAIL("The resolver must reject an ambiguous target");
} catch (const std::runtime_error &error) { } catch (const std::runtime_error &error) {
const std::string message = duplicateFrames ? "Ambiguous VAL_ frame reference." 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); QCOMPARE(std::string(error.what()), path + ":37: " + message);
} }
// Ambiguity must be detected before applying any descriptions. // Ambiguity must be detected before applying any descriptions.
for (const auto &candidate : database.frames) for (const FrameInfo &candidate: database.frames)
for (const auto &candidateSignal : candidate.signals) for (const SignalInfo &candidateSignal: candidate.signals)
QVERIFY(candidateSignal.valueDescriptions.empty()); QVERIFY(candidateSignal.valueDescriptions.empty());
} }
@@ -415,8 +441,8 @@ void CoverageTest::valueReferenceScope() {
handler.Handle("VAL_ 2147483649 State 0 \"Extended\";", context); handler.Handle("VAL_ 2147483649 State 0 \"Extended\";", context);
context.ResolveValueDescriptions("manual database.dbc"); context.ResolveValueDescriptions("manual database.dbc");
const auto *standard = FindValueDescription(database.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); const std::string *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 *extended = FindValueDescription(database.frames[1].signals[0].valueDescriptions, std::uint64_t(0));
QVERIFY(standard != nullptr); QVERIFY(standard != nullptr);
QVERIFY(extended != nullptr); QVERIFY(extended != nullptr);
QCOMPARE(*standard, std::string("Standard")); QCOMPARE(*standard, std::string("Standard"));
@@ -425,7 +451,8 @@ void CoverageTest::valueReferenceScope() {
} }
void CoverageTest::fileFormatting_data() { void CoverageTest::fileFormatting_data() {
QTest::addColumn<QString>("text"); QTest::addColumn<unsigned>("frames"); QTest::addColumn<QString>("text");
QTest::addColumn<unsigned>("frames");
QTest::newRow("empty") << QString::fromUtf8("") << 0U; QTest::newRow("empty") << QString::fromUtf8("") << 0U;
QTest::newRow("whitespace") << QString::fromUtf8(" \t\r\n\n\t") << 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; 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() { void CoverageTest::fileFormatting() {
QFETCH(QString, text); QFETCH(unsigned, frames); QFETCH(QString, text);
const auto parsed = ParseText(text.toStdString()); QFETCH(unsigned, frames);
const DbcDatabase parsed = ParseText(text.toStdString());
QCOMPARE(parsed.frames.size(), std::size_t(frames)); QCOMPARE(parsed.frames.size(), std::size_t(frames));
if (frames) { if (frames) {
QCOMPARE(parsed.frames[0].signals.size(), std::size_t(1)); QCOMPARE(parsed.frames[0].signals.size(), std::size_t(1));
if (text.contains("\r\nBO_")) { if (text.contains("\r\nBO_")) {
QCOMPARE(parsed.frames[0].comment, std::string("Comment")); 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); QVERIFY(description != nullptr);
QCOMPARE(*description, std::string("Off")); QCOMPARE(*description, std::string("Off"));
} }
@@ -451,31 +479,33 @@ void CoverageTest::fileFormatting() {
void CoverageTest::missingFile() { void CoverageTest::missingFile() {
QTemporaryFile file; QTemporaryFile file;
QVERIFY(file.open()); QVERIFY(file.open());
const auto path = file.fileName(); const QString path = file.fileName();
QVERIFY(file.remove()); QVERIFY(file.remove());
try { DbcParser().ParseFile(path.toStdString()); QFAIL("Missing file must throw"); } try {
catch (const std::runtime_error &error) { 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()); QCOMPARE(std::string(error.what()), "Failed to open DBC file: " + path.toStdString());
} }
} }
void CoverageTest::builders() { void CoverageTest::builders() {
const auto emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase()); const DecodeDatabase emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase());
QVERIFY(emptyRuntime.frames.empty()); QVERIFY(emptyRuntime.frames.empty());
QVERIFY(emptyRuntime.frameIndexByKey.empty()); QVERIFY(emptyRuntime.frameIndexByKey.empty());
const auto emptyTree = DbcTreeBuilder().Build(DbcDatabase()); const std::unique_ptr<TreeNode> emptyTree = DbcTreeBuilder().Build(DbcDatabase());
QCOMPARE(emptyTree->GetType(), NodeType::Root); QCOMPARE(emptyTree->GetType(), NodeType::Root);
QCOMPARE(emptyTree->GetName(), std::string("dbc")); QCOMPARE(emptyTree->GetName(), std::string("dbc"));
QCOMPARE(emptyTree->GetChildCount(), std::size_t(0)); QCOMPARE(emptyTree->GetChildCount(), std::size_t(0));
QVERIFY(emptyTree->GetFrame() == nullptr && emptyTree->GetSignal() == nullptr); QVERIFY(emptyTree->GetFrame() == nullptr && emptyTree->GetSignal() == nullptr);
const auto expected = ParseText( const DbcDatabase expected = ParseText(
"BO_ 256 Standard: 3 ECU1\n" "BO_ 256 Standard: 3 ECU1\n"
"SG_ Mode M : 0|8@1+ (2,3) [0|513] \"mode\" ECU1,ECU2\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_ 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" "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" "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Branch \"Branch comment\";\n"
"VAL_ 256 Branch -1 \"Negative\" 1 \"Positive\";\n" "VAL_ 256 Branch -1 \"Negative\" 1 \"Positive\";\n"
"BO_ 2163212866 Extended: 8 ECU4\n"); "BO_ 2163212866 Extended: 8 ECU4\n");
DecodeDatabase runtime; DecodeDatabase runtime;
std::unique_ptr<TreeNode> tree; std::unique_ptr<TreeNode> tree;
{ {
@@ -500,44 +530,62 @@ void CoverageTest::builders() {
QVERIFY(node->GetSignal() == nullptr); QVERIFY(node->GetSignal() == nullptr);
QCOMPARE(runtime.frameIndexByKey.at(FrameKey(frame.canId, frame.isExtended)), i); QCOMPARE(runtime.frameIndexByKey.at(FrameKey(frame.canId, frame.isExtended)), i);
const FrameInfo &copied = *node->GetFrame(); const FrameInfo &copied = *node->GetFrame();
QCOMPARE(copied.name, frame.name); QCOMPARE(decodedFrame.name, frame.name); QCOMPARE(copied.name, frame.name);
QCOMPARE(copied.canId, frame.canId); QCOMPARE(decodedFrame.canId, frame.canId); QCOMPARE(decodedFrame.name, frame.name);
QCOMPARE(copied.isExtended, frame.isExtended); QCOMPARE(decodedFrame.isExtended, frame.isExtended); QCOMPARE(copied.canId, frame.canId);
QCOMPARE(copied.dlc, frame.dlc); QCOMPARE(decodedFrame.dlc, frame.dlc); QCOMPARE(decodedFrame.canId, frame.canId);
QCOMPARE(copied.pgn, frame.pgn); QCOMPARE(decodedFrame.pgn, frame.pgn); QCOMPARE(copied.isExtended, frame.isExtended);
QCOMPARE(copied.hasPgn, frame.hasPgn); QCOMPARE(decodedFrame.hasPgn, frame.hasPgn); QCOMPARE(decodedFrame.isExtended, frame.isExtended);
QCOMPARE(copied.transmitter, frame.transmitter); QCOMPARE(decodedFrame.transmitter, frame.transmitter); QCOMPARE(copied.dlc, frame.dlc);
QCOMPARE(copied.comment, frame.comment); QCOMPARE(decodedFrame.comment, frame.comment); 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(node->GetChildCount(), frame.signals.size());
QCOMPARE(copied.signals.size(), frame.signals.size()); QCOMPARE(copied.signals.size(), frame.signals.size());
QCOMPARE(decodedFrame.signals.size(), frame.signals.size()); QCOMPARE(decodedFrame.signals.size(), frame.signals.size());
for (std::size_t j = 0; j < frame.signals.size(); ++j) { for (std::size_t j = 0; j < frame.signals.size(); ++j) {
const auto &original = frame.signals[j]; const SignalInfo &original = frame.signals[j];
const auto &target = decodedFrame.signals[j]; const DecodeSignal &target = decodedFrame.signals[j];
const auto *leaf = node->GetChild(j); const TreeNode *leaf = node->GetChild(j);
QVERIFY(leaf && leaf->GetSignal()); QVERIFY(leaf && leaf->GetSignal());
QCOMPARE(leaf->GetType(), NodeType::Signal); QCOMPARE(leaf->GetType(), NodeType::Signal);
QCOMPARE(leaf->GetName(), original.name); QCOMPARE(leaf->GetName(), original.name);
QVERIFY(leaf->GetFrame() == nullptr); QVERIFY(leaf->GetFrame() == nullptr);
// Check both the frame payload copy and each child payload copy. // Check both the frame payload copy and each child payload copy.
for (const SignalInfo *copy : {&copied.signals[j], leaf->GetSignal()}) { for (const SignalInfo *copy: {&copied.signals[j], leaf->GetSignal()}) {
QCOMPARE(copy->name, original.name); QCOMPARE(copy->startBit, original.startBit); QCOMPARE(copy->name, original.name);
QCOMPARE(copy->length, original.length); QCOMPARE(copy->isLittleEndian, original.isLittleEndian); QCOMPARE(copy->startBit, original.startBit);
QCOMPARE(copy->isSigned, original.isSigned); QCOMPARE(copy->factor, original.factor); QCOMPARE(copy->length, original.length);
QCOMPARE(copy->offset, original.offset); QCOMPARE(copy->minimum, original.minimum); QCOMPARE(copy->isLittleEndian, original.isLittleEndian);
QCOMPARE(copy->maximum, original.maximum); QCOMPARE(copy->unit, original.unit); QCOMPARE(copy->isSigned, original.isSigned);
QCOMPARE(copy->comment, original.comment); QVERIFY(copy->receivers == original.receivers); 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); QVERIFY(copy->valueDescriptions == original.valueDescriptions);
QCOMPARE(copy->multiplexing.role, original.multiplexing.role); QCOMPARE(copy->multiplexing.role, original.multiplexing.role);
QCOMPARE(copy->multiplexing.value, original.multiplexing.value); 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.length, original.length);
QCOMPARE(target.byteOrder, original.isLittleEndian ? ByteOrder::Intel : ByteOrder::Motorola); QCOMPARE(target.byteOrder, original.isLittleEndian ? ByteOrder::Intel : ByteOrder::Motorola);
QCOMPARE(target.valueType, original.isSigned ? ValueType::Signed : ValueType::Unsigned); QCOMPARE(target.valueType, original.isSigned ? ValueType::Signed : ValueType::Unsigned);
QCOMPARE(target.factor, original.factor); QCOMPARE(target.offset, original.offset); QCOMPARE(target.factor, original.factor);
QCOMPARE(target.minimum, original.minimum); QCOMPARE(target.maximum, original.maximum); QCOMPARE(target.offset, original.offset);
QCOMPARE(target.unit, original.unit); QCOMPARE(target.comment, original.comment); 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.receivers == original.receivers);
QVERIFY(target.valueDescriptions == original.valueDescriptions); QVERIFY(target.valueDescriptions == original.valueDescriptions);
QCOMPARE(target.multiplexing.role, original.multiplexing.role); QCOMPARE(target.multiplexing.role, original.multiplexing.role);
@@ -547,93 +595,122 @@ void CoverageTest::builders() {
} }
void CoverageTest::physicalConversion_data() { void CoverageTest::physicalConversion_data() {
QTest::addColumn<double>("factor"); QTest::addColumn<double>("offset"); QTest::addColumn<double>("expected"); QTest::addColumn<double>("factor");
QTest::addColumn<double>("offset");
QTest::addColumn<double>("expected");
QTest::newRow("zero-factor") << 0.0 << 7.0 << 7.0; QTest::newRow("zero-factor") << 0.0 << 7.0 << 7.0;
QTest::newRow("negative-factor") << -2.0 << 5.0 << -15.0; QTest::newRow("negative-factor") << -2.0 << 5.0 << -15.0;
QTest::newRow("above-declared-range") << 2.0 << 3.0 << 23.0; QTest::newRow("above-declared-range") << 2.0 << 3.0 << 23.0;
} }
void CoverageTest::physicalConversion() { void CoverageTest::physicalConversion() {
QFETCH(double, factor); QFETCH(double, offset); QFETCH(double, expected); QFETCH(double, factor);
DecodeSignal signal; signal.length = 8U; signal.factor = factor; signal.offset = offset; QFETCH(double, offset);
signal.minimum = 0.0; signal.maximum = 1.0; QFETCH(double, expected);
const auto database = SingleSignal(signal); DecodeSignal signal;
RawCanFrame raw; raw.canId = 17U; raw.data = {10U}; signal.length = 8U;
const auto decoded = DbcDecoder().Decode(database, raw); 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); QVERIFY(decoded.signals[0].valid);
QCOMPARE(std::get<std::uint64_t>(decoded.signals[0].rawValue), std::uint64_t(10)); QCOMPARE(std::get<std::uint64_t>(decoded.signals[0].rawValue), std::uint64_t(10));
QCOMPARE(decoded.signals[0].physicalValue, expected); QCOMPARE(decoded.signals[0].physicalValue, expected);
} }
void CoverageTest::multiplexingScenarios() { void CoverageTest::multiplexingScenarios() {
const auto selectorOnly = DbcDecodeBuilder().Build(ParseText( const DecodeDatabase selectorOnly = DbcDecodeBuilder().Build(ParseText(
"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n")); "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}; RawCanFrame raw;
const auto only = DbcDecoder().Decode(selectorOnly, raw); raw.canId = 1U;
raw.data = {1U};
const DecodedFrameValue only = DbcDecoder().Decode(selectorOnly, raw);
QCOMPARE(only.signals.size(), std::size_t(1)); QCOMPARE(only.signals.size(), std::size_t(1));
QVERIFY(only.signals[0].valid); QVERIFY(only.signals[0].valid);
const auto database = DbcDecodeBuilder().Build(ParseText( const DecodeDatabase database = DbcDecodeBuilder().Build(ParseText(
"BO_ 1 First: 3 ECU\n" "BO_ 1 First: 3 ECU\n"
"SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" 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_ A m0 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
"SG_ B m0 : 16|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" "BO_ 2 Second: 2 ECU\n"
"SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" 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")); "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"));
raw.data = {0U, 42U, 99U}; 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); QVERIFY(first.signals[1].valid && first.signals[2].valid);
QCOMPARE(std::get<std::uint64_t>(first.signals[1].rawValue), std::uint64_t(42)); QCOMPARE(std::get<std::uint64_t>(first.signals[1].rawValue), std::uint64_t(42));
QCOMPARE(std::get<std::uint64_t>(first.signals[2].rawValue), std::uint64_t(99)); QCOMPARE(std::get<std::uint64_t>(first.signals[2].rawValue), std::uint64_t(99));
raw.canId = 2U; raw.canId = 2U;
const auto secondInactive = DbcDecoder().Decode(database, raw); const DecodedFrameValue secondInactive = DbcDecoder().Decode(database, raw);
QCOMPARE(secondInactive.signals[1].status, SignalDecodeStatus::Inactive); QCOMPARE(secondInactive.signals[1].status, SignalDecodeStatus::Inactive);
raw.data[0] = 1U; raw.data[0] = 1U;
QVERIFY(DbcDecoder().Decode(database, raw).signals[1].valid); QVERIFY(DbcDecoder().Decode(database, raw).signals[1].valid);
raw.canId = 1U; 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[1].status, SignalDecodeStatus::Inactive);
QCOMPARE(firstInactive.signals[2].status, SignalDecodeStatus::Inactive); QCOMPARE(firstInactive.signals[2].status, SignalDecodeStatus::Inactive);
} }
void CoverageTest::multiplexingErrorLocations_data() { void CoverageTest::multiplexingErrorLocations_data() {
QTest::addColumn<QString>("text"); QTest::addColumn<unsigned>("line"); QTest::addColumn<QString>("message"); QTest::addColumn<QString>("text");
QTest::addColumn<unsigned>("line");
QTest::addColumn<QString>("message");
const QString header = "BO_ 1 Frame: 3 ECU\n\n"; 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 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"; 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("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("second-selector") << header + selector + "\nSG_ Other M : 16|1@1+ (1,0) [0|1] \"\" ECU\n"
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."); << 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("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("same-branch-overlap") << header + selector + branch + "SG_ B m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n"
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."); << 5U << 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("selector-overlap") << header + selector + "SG_ A m1 : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
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."); << 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("unconditional-overlap") << header + selector + branch + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
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."); << 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() { void CoverageTest::multiplexingErrorLocations() {
QFETCH(QString, text); QFETCH(unsigned, line); QFETCH(QString, message); QFETCH(QString, text);
QTemporaryFile file; QVERIFY(file.open()); QFETCH(unsigned, line);
QFETCH(QString, message);
QTemporaryFile file;
QVERIFY(file.open());
const QByteArray content = text.toUtf8(); const QByteArray content = text.toUtf8();
QCOMPARE(file.write(content), qint64(content.size())); QVERIFY(file.flush()); QCOMPARE(file.write(content), qint64(content.size()));
try { DbcParser().ParseFile(file.fileName().toStdString()); QFAIL("Expected multiplexing rejection"); } QVERIFY(file.flush());
catch (const std::runtime_error &error) { 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); QCOMPARE(QString::fromStdString(error.what()), file.fileName() + ":" + QString::number(line) + ": " + message);
} }
} }
void CoverageTest::escapedValueDescriptions() { void CoverageTest::escapedValueDescriptions() {
const auto parsed = ParseText( const DbcDatabase parsed = ParseText(
"BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "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"); R"dbc(VAL_ 1 State -0 "Ноль" 1 "C:\\trace" 2 "\\\"quoted\"\\";)dbc"
const auto &values = parsed.frames[0].signals[0].valueDescriptions; "\n");
const ValueDescriptions &values = parsed.frames[0].signals[0].valueDescriptions;
QCOMPARE(values.size(), std::size_t(3)); QCOMPARE(values.size(), std::size_t(3));
QVERIFY(values.find(RawSignalValue(std::int64_t(0))) == values.end()); QVERIFY(values.find(RawSignalValue(std::int64_t(0))) == values.end());
const auto *zero = FindValueDescription(values, std::int64_t(0)); const std::string *zero = FindValueDescription(values, std::int64_t(0));
const auto *path = FindValueDescription(values, std::uint64_t(1)); const std::string *path = FindValueDescription(values, std::uint64_t(1));
const auto *quoted = FindValueDescription(values, std::uint64_t(2)); const std::string *quoted = FindValueDescription(values, std::uint64_t(2));
QVERIFY(zero && path && quoted); QVERIFY(zero && path && quoted);
QCOMPARE(*zero, std::string("Ноль")); QCOMPARE(*zero, std::string("Ноль"));
QCOMPARE(*path, std::string("C:\\trace")); QCOMPARE(*path, std::string("C:\\trace"));
@@ -649,11 +726,11 @@ void CoverageTest::factory() {
QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast<void>(factory.create("missing", std::make_unique<int>(1)))); QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast<void>(factory.create("missing", std::make_unique<int>(1))));
factory.registerType<FactoryFirst>("value"); factory.registerType<FactoryFirst>("value");
QVERIFY(factory.contains("value")); QVERIFY(factory.contains("value"));
auto argument = std::make_unique<int>(42); std::unique_ptr<int> argument = std::make_unique<int>(42);
const auto first = factory.create("value", std::move(argument)); const std::shared_ptr<FactoryBase> first = factory.create("value", std::move(argument));
QVERIFY(argument == nullptr); QVERIFY(argument == nullptr);
QCOMPARE(first->Value(), 42); QCOMPARE(first->Value(), 42);
const auto creator = factory.creator("value"); const TypeFactory<std::string, FactoryBase, std::unique_ptr<int>>::CreatorFn creator = factory.creator("value");
QCOMPARE(creator(std::make_unique<int>(7))->Value(), 7); QCOMPARE(creator(std::make_unique<int>(7))->Value(), 7);
factory.registerType<FactorySecond>("value"); factory.registerType<FactorySecond>("value");
QCOMPARE(factory.create("value", std::make_unique<int>(42))->Value(), 43); QCOMPARE(factory.create("value", std::make_unique<int>(42))->Value(), 43);
+13 -5
View File
@@ -2,9 +2,10 @@
#include "dbc_tree_builder.h" #include "dbc_tree_builder.h"
#include "dbc_tree_model.h" #include "dbc_tree_model.h"
#include <QAbstractItemModelTester> #include <QAbstractItemModelTester>
#include <QGuiApplication>
#include <QSignalSpy> #include <QSignalSpy>
#include <QTemporaryFile>
#include <QStringList> #include <QStringList>
#include <QTemporaryFile>
#include <QtTest> #include <QtTest>
#include <limits> #include <limits>
@@ -50,8 +51,8 @@ private Q_SLOTS:
const QStringList headers = {"Name", "CAN ID", "Payload (bytes)", "Start bit", const QStringList headers = {"Name", "CAN ID", "Payload (bytes)", "Start bit",
"Length (bits)", "Byte order", "Signedness", "Unit", "Comment"}; "Length (bits)", "Byte order", "Signedness", "Unit", "Comment"};
for (int column = 0; column < model.columnCount(); ++column) { for (int column = 0; column < model.columnCount(); ++column) {
const auto frameIndex = model.index(0, column); const QModelIndex frameIndex = model.index(0, column);
const auto signalIndex = model.index(0, column, parent); const QModelIndex signalIndex = model.index(0, column, parent);
QCOMPARE(model.headerData(column, Qt::Horizontal).toString(), headers.at(column)); QCOMPARE(model.headerData(column, Qt::Horizontal).toString(), headers.at(column));
QVERIFY(!model.headerData(column, Qt::Horizontal, Qt::ToolTipRole).isValid()); QVERIFY(!model.headerData(column, Qt::Horizontal, Qt::ToolTipRole).isValid());
QCOMPARE(model.data(frameIndex, Qt::ToolTipRole).toString(), QString("Frame tooltip")); 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::LittleEndianRole).toBool(), false);
QCOMPARE(model.data(signalIndex, DbcTreeModel::SignedRole).toBool(), true); QCOMPARE(model.data(signalIndex, DbcTreeModel::SignedRole).toBool(), true);
QCOMPARE(model.data(signalIndex, DbcTreeModel::UnitRole).toString(), QString("rpm")); 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.isValid());
QVERIFY(descriptions.toList().isEmpty()); QVERIFY(descriptions.toList().isEmpty());
QVERIFY(!model.data(frameIndex, Qt::UserRole + 1000).isValid()); QVERIFY(!model.data(frameIndex, Qt::UserRole + 1000).isValid());
@@ -203,5 +204,12 @@ private Q_SLOTS:
QCOMPARE(resets.count(), 4); 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" #include "dbc_tree_model_test.moc"
+1 -2
View File
@@ -1,7 +1,6 @@
TEMPLATE = app TEMPLATE = app
TARGET = dbc_model_tests TARGET = dbc_model_tests
QT += core testlib QT += core gui testlib
QT -= gui
CONFIG += console testcase c++17 no_keywords CONFIG += console testcase c++17 no_keywords
CONFIG -= app_bundle debug_and_release debug_and_release_target CONFIG -= app_bundle debug_and_release debug_and_release_target
include(../qt/qt.pri) include(../qt/qt.pri)
+1
View File
@@ -1,4 +1,5 @@
TEMPLATE = app TEMPLATE = app
include(../qt/qt_version.pri)
TARGET = dbc_tests TARGET = dbc_tests
QT += core testlib QT += core testlib
QT -= gui QT -= gui