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
- 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<std::int64_t>(&decodedSignal.rawValue)) {
if (const std::int64_t *value = std::get_if<std::int64_t>(&decodedSignal.rawValue)) {
std::cout << *value;
} else {
std::cout << std::get<std::uint64_t>(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<MyCustomHandler>("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).
+25
View File
@@ -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
+1
View File
@@ -1,3 +1,4 @@
include($$PWD/qt_version.pri)
include($$PWD/../core/core.pri)
QT += core
INCLUDEPATH += $$PWD
+1
View File
@@ -1,4 +1,5 @@
TEMPLATE = lib
include(qt_version.pri)
TARGET = dbc_qt
CONFIG += staticlib c++17
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
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.
+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.
#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 <QtTest>
#include <QTemporaryFile>
#include <QtTest>
#include <array>
#include <limits>
#include <stdexcept>
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<qint64>(text.size())) != static_cast<qint64>(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<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;
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<qint64>(text.size())) != static_cast<qint64>(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<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; }
};
}
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 {
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 {
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<unsigned>("length");
QTest::addColumn<qlonglong>("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<std::int64_t, 4> 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<std::uint8_t>(((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<std::int64_t>(result.signals[0].rawValue));
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 (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<std::vector<std::uint8_t>> patterns = {
std::vector<std::uint8_t>(10U, 0U), std::vector<std::uint8_t>(10U, 255U),
{0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}};
for (const auto &data : patterns) {
std::vector<std::uint8_t>(10U, 0U),
std::vector<std::uint8_t>(10U, 255U),
{0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}};
for (const std::vector<std::uint8_t> &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<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");
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<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;
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<QString>("before"); QTest::addColumn<QString>("after");
QTest::addColumn<QString>("frameComment"); QTest::addColumn<QString>("signalComment");
QTest::addColumn<QString>("before");
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("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<QString>("text"); QTest::addColumn<unsigned>("frames");
QTest::addColumn<QString>("text");
QTest::addColumn<unsigned>("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<TreeNode> 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<TreeNode> 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<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("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<std::uint64_t>(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<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));
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<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 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<void>(factory.create("missing", std::make_unique<int>(1))));
factory.registerType<FactoryFirst>("value");
QVERIFY(factory.contains("value"));
auto argument = std::make_unique<int>(42);
const auto first = factory.create("value", std::move(argument));
std::unique_ptr<int> argument = std::make_unique<int>(42);
const std::shared_ptr<FactoryBase> first = factory.create("value", std::move(argument));
QVERIFY(argument == nullptr);
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);
factory.registerType<FactorySecond>("value");
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_model.h"
#include <QAbstractItemModelTester>
#include <QGuiApplication>
#include <QSignalSpy>
#include <QTemporaryFile>
#include <QStringList>
#include <QTemporaryFile>
#include <QtTest>
#include <limits>
@@ -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"
+1 -2
View File
@@ -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)
+1
View File
@@ -1,4 +1,5 @@
TEMPLATE = app
include(../qt/qt_version.pri)
TARGET = dbc_tests
QT += core testlib
QT -= gui