This commit is contained in:
deeaitch committed 2026-10-06 17:14:50 -04:00
1 parent e5d818d9b2
commit a533d9aa00
13 files changed
+527 -12

No files matched your search

+100 -9
View File
@@ -21,8 +21,8 @@ The implementation follows a simple and practical design:
- 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 Core and Qt Test are
required only for the regression tests. Live CAN acquisition, trace-file readers, used by the regression tests; the optional adapter requires Qt Core. Live CAN acquisition, trace-file readers,
and a Qt model adapter are left to the consuming application. 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,
[tests/README.md](tests/README.md) for test instructions, and [tests/README.md](tests/README.md) for test instructions, and
@@ -55,7 +55,7 @@ DbcTreeBuilder
↓ ↓
TreeNode hierarchy TreeNode hierarchy
↓ ↓
future Qt UI DbcTreeModel → Qt views
``` ```
### 2. Runtime decode path ### 2. Runtime decode path
@@ -114,7 +114,8 @@ This avoids forcing UI-oriented structures into a decode role they were not mean
```text ```text
core/ C++17 parser, statement handlers, metadata, tree, and decoder core/ C++17 parser, statement handlers, metadata, tree, and decoder
examples/ Command-line demo (built separately) examples/ Command-line demo (built separately)
tests/ QtTest parser and decoder regression suites qt/ Optional read-only Qt Core model adapter
tests/ Parser, decoder, tree, and optional model tests
docs/ Generated Doxygen documentation docs/ Generated Doxygen documentation
dbc.pro Root qmake project: core + tests dbc.pro Root qmake project: core + tests
Doxyfile API documentation configuration Doxyfile API documentation configuration
@@ -235,7 +236,7 @@ has no metadata payload. The payload types are:
- `FrameInfo` - `FrameInfo`
- `SignalInfo` - `SignalInfo`
The tree is intended for browsing and later Qt model integration. The tree supports browsing through the optional `DbcTreeModel` adapter.
It is **not** the primary runtime decode structure. It is **not** the primary runtime decode structure.
@@ -884,7 +885,7 @@ nonnegative `uint64_t`; when constructing mappings manually, normalize keys
with `NormalizeValueKey()` before insertion. with `NormalizeValueKey()` before insertion.
Named `VAL_TABLE_` tables, environment-variable mappings, multiline statements, Named `VAL_TABLE_` tables, environment-variable mappings, multiline statements,
and Qt presentation are outside this implementation. are outside this implementation; the optional Qt adapter exposes signal mappings.
--- ---
@@ -942,9 +943,6 @@ A practical development order would be:
Recommended additions: Recommended additions:
- parent pointer in `TreeNode`
- Qt model adapter
- better display strings for UI
- selective decoding of only chosen signals - selective decoding of only chosen signals
### Stage 3 ### Stage 3
@@ -1009,6 +1007,99 @@ make -j
qmake is still the build tool; this target does not link Qt libraries. 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:
```sh
mkdir -p build-qt
cd build-qt
qmake6 ../dbc.pro CONFIG+=dbc_qt
make -j
make check
```
Consumers include `qt/qt.pri` instead of `core/core.pri`, and set
`DBC_CORE_BUILD_DIR` and `DBC_QT_BUILD_DIR` to the corresponding library build
directories when building separately. A widget application also needs `QT += widgets`. The adapter integration enables
`no_keywords`, because core metadata has a `signals` member; use `Q_SIGNALS`,
`Q_SLOTS`, and `Q_EMIT` in consuming Qt classes.
```cpp
#include "dbc_parser.h"
#include "dbc_tree_builder.h"
#include "dbc_tree_model.h"
#include <QTreeView>
// Run after constructing QApplication; view owns the model through QObject.
QTreeView view;
auto *model = new DbcTreeModel(&view);
model->SetTree(DbcTreeBuilder().Build(DbcParser().ParseFile("example.dbc")));
view.setModel(model);
view.expandAll();
view.show();
// On reload, pass a newly built tree to SetTree(); to clear:
// model->SetTree(nullptr);
```
The root is invisible: frames are top-level rows, signals are their children.
Only column zero has children. `parent()` always returns column zero.
The model takes exclusive ownership of the tree; do not retain mutable aliases.
Metadata is copied by the tree builder, so the parsed database may be destroyed.
`TreeNode::GetParent()` is non-owning and `GetRow()` gives the zero-based sibling
position (zero for a detached node/root). Nodes cannot be copied or moved;
transferring their `unique_ptr` ownership preserves addresses and navigation.
Call `SetTree()` on the model's thread. It accepts a detached Root or nullptr,
uses `beginResetModel()`/`endResetModel()`, and destroys the previous tree during
reset. All ordinary QModelIndex values expire on reset and must not be reused;
QPersistentModelIndex values are invalidated by Qt. Destroying the model also
invalidates indexes. Invalid and foreign indexes return no data/flags/children.
Valid items are enabled and selectable, with no editing support.
| Column (zero-based enum) | Frame display | Signal display |
| --- | --- | --- |
| 0 `Name` | Name | Name |
| 1 `CanId` | Normalized hexadecimal ID | Empty |
| 2 `PayloadLength` | Bytes | Empty |
| 3 `StartBit` | Empty | DBC start bit |
| 4 `BitLength` | Empty | Bit count |
| 5 `ByteOrder` | Empty | Intel / Motorola |
| 6 `Signed` | Empty | Signed / Unsigned |
| 7 `Unit` | Empty | Unit |
| 8 `Comment` | Comment | Comment |
Horizontal DisplayRole headers describe these columns; other header requests
return invalid QVariant. ToolTipRole returns the comment on every column.
Custom roles also apply on every column; inapplicable roles return invalid QVariant:
| Role | Node types | QVariant content |
| --- | --- | --- |
| `NodeTypeRole` | Frame, signal | int corresponding to NodeType |
| `CanIdRole`, `PayloadLengthRole`, `ExtendedRole` | Frame | uint, int, bool |
| `StartBitRole`, `BitLengthRole` | Signal | uint |
| `LittleEndianRole`, `SignedRole` | Signal | bool |
| `UnitRole` | Signal | QString |
| `CommentRole` | Frame, signal | QString |
| `ValueDescriptionsRole` | Signal | QVariantList of QVariantMap entries |
Each value-description entry contains `value` (qlonglong for negative keys,
qulonglong for nonnegative keys) and `description` (QString). This preserves the
full signed/unsigned 64-bit range without converting through double. An unmapped
signal returns an empty list. Data comes from `SignalInfo::valueDescriptions`.
Unsupported roles return invalid QVariant.
### Standalone tree tests without Qt
From the repository root, use only a C++17 compiler:
```sh
c++ -std=c++17 -Icore tests/tree_node_test.cpp core/tree_node.cpp \
core/dbc_tree_builder.cpp -o /tmp/dbc_tree_node_test
/tmp/dbc_tree_node_test
```
### Build and run the demo ### Build and run the demo
After building core, run these commands from the repository root: After building core, run these commands from the repository root:
+8
View File
@@ -10,6 +10,14 @@ existing Git tag; subsequent changes are listed as unreleased.
#### Added #### Added
- Added non-owning TreeNode parent navigation and stable sibling positions;
disabled node moves to preserve links while retaining unique_ptr ownership.
- Added optional read-only Qt Core DbcTreeModel with documented columns and
metadata roles, exact VAL_ entries, exclusive tree ownership, and reset-based
reload/clear support. Core remains independent of Qt.
- Added QAbstractItemModelTester coverage, navigation/metadata/reset tests,
a standalone Qt-free tree test, and adapter build and usage documentation.
- Added `VAL_` signal value descriptions through the statement dispatcher and - Added `VAL_` signal value descriptions through the statement dispatcher and
factory, with complete syntax validation, exact signed/unsigned 64-bit keys, factory, with complete syntax validation, exact signed/unsigned 64-bit keys,
deferred reference resolution, duplicate detection, and source error locations. deferred reference resolution, duplicate detection, and source error locations.
+12 -1
View File
@@ -24,8 +24,11 @@ TreeNode::TreeNode(const SignalInfo &signal)
} }
void TreeNode::AddChild(std::unique_ptr<TreeNode> child) { void TreeNode::AddChild(std::unique_ptr<TreeNode> child) {
if (child) if (child) {
m_children.push_back(std::move(child)); m_children.push_back(std::move(child));
m_children.back()->m_parent = this;
m_children.back()->m_row = m_children.size() - 1U;
}
} }
std::size_t TreeNode::GetChildCount() const { std::size_t TreeNode::GetChildCount() const {
@@ -61,3 +64,11 @@ const FrameInfo *TreeNode::GetFrame() const {
const SignalInfo *TreeNode::GetSignal() const { const SignalInfo *TreeNode::GetSignal() const {
return m_signal.get(); return m_signal.get();
} }
const TreeNode *TreeNode::GetParent() const {
return m_parent;
}
std::size_t TreeNode::GetRow() const {
return m_row;
}
+10 -2
View File
@@ -56,8 +56,8 @@ public:
TreeNode(const TreeNode &) = delete; TreeNode(const TreeNode &) = delete;
TreeNode &operator=(const TreeNode &) = delete; TreeNode &operator=(const TreeNode &) = delete;
TreeNode(TreeNode &&) = default; TreeNode(TreeNode &&) = delete;
TreeNode &operator=(TreeNode &&) = default; TreeNode &operator=(TreeNode &&) = delete;
/** /**
* @brief Add child node. * @brief Add child node.
@@ -65,6 +65,12 @@ public:
*/ */
void AddChild(std::unique_ptr<TreeNode> child); void AddChild(std::unique_ptr<TreeNode> child);
/** Non-owning parent; nullptr for a detached node or the root. */
const TreeNode *GetParent() const;
/** Zero-based sibling position; zero for a detached node or the root. */
std::size_t GetRow() const;
/** /**
* @brief Get child count. * @brief Get child count.
* @return Number of children. * @return Number of children.
@@ -110,6 +116,8 @@ public:
const SignalInfo *GetSignal() const; const SignalInfo *GetSignal() const;
private: private:
TreeNode *m_parent = nullptr;
std::size_t m_row = 0U;
NodeType m_type; NodeType m_type;
std::string m_name; std::string m_name;
std::vector<std::unique_ptr<TreeNode>> m_children; std::vector<std::unique_ptr<TreeNode>> m_children;
+7
View File
@@ -3,3 +3,10 @@ SUBDIRS += core tests
core.subdir = core core.subdir = core
tests.subdir = tests tests.subdir = tests
tests.depends = core tests.depends = core
contains(CONFIG, dbc_qt) {
SUBDIRS += qt model_tests
qt.subdir = qt
qt.depends = core
model_tests.file = tests/model_tests.pro
model_tests.depends = qt
}
+128
View File
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT
#include "dbc_tree_model.h"
#include <QVariantList>
#include <QVariantMap>
#include <limits>
#include <stdexcept>
DbcTreeModel::DbcTreeModel(QObject *parent) : QAbstractItemModel(parent) {}
void DbcTreeModel::SetTree(std::unique_ptr<TreeNode> root) {
if (root && (root->GetType() != NodeType::Root || root->GetParent()))
throw std::invalid_argument("Model requires a detached root node");
beginResetModel();
m_root = std::move(root);
endResetModel();
}
const TreeNode *DbcTreeModel::Node(const QModelIndex &index) const {
if (!index.isValid() || index.model() != this || index.column() < 0 ||
index.column() >= ColumnCount || index.row() < 0)
return nullptr;
return static_cast<const TreeNode *>(index.internalPointer());
}
QModelIndex DbcTreeModel::index(int row, int column, const QModelIndex &parentIndex) const {
if (row < 0 || column < 0 || column >= ColumnCount ||
(parentIndex.isValid() && parentIndex.column() != 0))
return {};
const TreeNode *owner = parentIndex.isValid() ? Node(parentIndex) : m_root.get();
if (!owner)
return {};
const TreeNode *child = owner->GetChild(static_cast<std::size_t>(row));
return child ? createIndex(row, column, const_cast<TreeNode *>(child)) : QModelIndex();
}
QModelIndex DbcTreeModel::parent(const QModelIndex &childIndex) const {
const TreeNode *child = Node(childIndex);
const TreeNode *owner = child ? child->GetParent() : nullptr;
if (!owner || owner == m_root.get())
return {};
return createIndex(static_cast<int>(owner->GetRow()), 0, const_cast<TreeNode *>(owner));
}
int DbcTreeModel::rowCount(const QModelIndex &parentIndex) const {
if (parentIndex.isValid() && parentIndex.column() != 0)
return 0;
const TreeNode *owner = parentIndex.isValid() ? Node(parentIndex) : m_root.get();
if (!owner)
return 0;
return static_cast<int>(qMin(owner->GetChildCount(), static_cast<std::size_t>(std::numeric_limits<int>::max())));
}
int DbcTreeModel::columnCount(const QModelIndex &parentIndex) const {
return parentIndex.isValid() && !Node(parentIndex) ? 0 : ColumnCount;
}
QVariant DbcTreeModel::data(const QModelIndex &item, int role) const {
const TreeNode *node = Node(item);
if (!node)
return {};
const FrameInfo *frame = node->GetFrame();
const SignalInfo *signal = node->GetSignal();
const QString comment = QString::fromStdString(frame ? frame->comment : signal ? signal->comment
: "");
if (role == Qt::ToolTipRole || role == CommentRole)
return comment;
if (role == NodeTypeRole)
return static_cast<int>(node->GetType());
if (role == ValueDescriptionsRole && signal) {
QVariantList descriptions;
for (const auto &entry: signal->valueDescriptions) {
QVariantMap pair;
if (const auto *number = std::get_if<std::int64_t>(&entry.first))
pair.insert("value", QVariant::fromValue(static_cast<qlonglong>(*number)));
else
pair.insert("value", QVariant::fromValue(static_cast<qulonglong>(std::get<std::uint64_t>(entry.first))));
pair.insert("description", QString::fromStdString(entry.second));
descriptions.append(pair);
}
return descriptions;
}
if (frame) {
if (role == CanIdRole) return frame->canId;
if (role == PayloadLengthRole) return static_cast<int>(frame->dlc);
if (role == ExtendedRole) return frame->isExtended;
}
if (signal) {
if (role == StartBitRole) return signal->startBit;
if (role == BitLengthRole) return signal->length;
if (role == LittleEndianRole) return signal->isLittleEndian;
if (role == SignedRole) return signal->isSigned;
if (role == UnitRole) return QString::fromStdString(signal->unit);
}
if (role != Qt::DisplayRole) return {};
switch (item.column()) {
case Name:
return QString::fromStdString(node->GetName());
case CanId:
return frame ? QVariant(QString("0x%1").arg(frame->canId, 0, 16).toUpper()) : QVariant();
case PayloadLength:
return frame ? QVariant(static_cast<int>(frame->dlc)) : QVariant();
case StartBit:
return signal ? QVariant(signal->startBit) : QVariant();
case BitLength:
return signal ? QVariant(signal->length) : QVariant();
case ByteOrder:
return signal ? QVariant(signal->isLittleEndian ? "Intel" : "Motorola") : QVariant();
case Signed:
return signal ? QVariant(signal->isSigned ? "Signed" : "Unsigned") : QVariant();
case Unit:
return signal ? QVariant(QString::fromStdString(signal->unit)) : QVariant();
case Comment:
return comment;
default:
return {};
}
}
QVariant DbcTreeModel::headerData(int section, Qt::Orientation orientation, int role) const {
static const char *const headers[] = {"Name", "CAN ID", "Payload (bytes)", "Start bit", "Length (bits)", "Byte order", "Signedness", "Unit", "Comment"};
if (orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= ColumnCount)
return {};
return QString::fromLatin1(headers[section]);
}
Qt::ItemFlags DbcTreeModel::flags(const QModelIndex &item) const {
return Node(item) ? Qt::ItemIsEnabled | Qt::ItemIsSelectable : Qt::NoItemFlags;
}
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
#ifndef DBC_TREE_MODEL_H
#define DBC_TREE_MODEL_H
#include "tree_node.h"
#include <QAbstractItemModel>
/** Read-only model. Owns an immutable tree; all indexes expire on SetTree().
* Use QPersistentModelIndex to observe reset invalidation. Call on the model thread.
*/
class DbcTreeModel : public QAbstractItemModel {
public:
enum Column { Name,
CanId,
PayloadLength,
StartBit,
BitLength,
ByteOrder,
Signed,
Unit,
Comment,
ColumnCount };
// Metadata roles apply on every column; inapplicable roles return invalid QVariant.
enum Role { NodeTypeRole = Qt::UserRole + 1,
CanIdRole,
PayloadLengthRole,
StartBitRole,
BitLengthRole,
LittleEndianRole,
SignedRole,
UnitRole,
CommentRole,
ValueDescriptionsRole,
ExtendedRole };
explicit DbcTreeModel(QObject *parent = nullptr);
void SetTree(std::unique_ptr<TreeNode> root);
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
const TreeNode *Node(const QModelIndex &index) const;
std::unique_ptr<TreeNode> m_root;
};
#endif
+11
View File
@@ -0,0 +1,11 @@
include($$PWD/../core/core.pri)
QT += core
INCLUDEPATH += $$PWD
isEmpty(DBC_QT_BUILD_DIR): DBC_QT_BUILD_DIR = $$clean_path($$OUT_PWD/../qt)
# Put the adapter before core for static linker dependency ordering.
LIBS = -L$$DBC_QT_BUILD_DIR -ldbc_qt $$LIBS
win32-msvc*: DBC_QT_LIBRARY = $$DBC_QT_BUILD_DIR/dbc_qt.lib
else: DBC_QT_LIBRARY = $$DBC_QT_BUILD_DIR/libdbc_qt.a
PRE_TARGETDEPS += $$DBC_QT_LIBRARY
CONFIG += no_keywords
+11
View File
@@ -0,0 +1,11 @@
TEMPLATE = lib
TARGET = dbc_qt
CONFIG += staticlib c++17
QT += core
QT -= gui
include(../core/core.pri)
HEADERS += dbc_tree_model.h
SOURCES += dbc_tree_model.cpp
CONFIG -= debug_and_release debug_and_release_target
CONFIG += no_keywords
+13
View File
@@ -59,3 +59,16 @@ automatically.
Dispatch tests also verify exact keyword matching, handler construction and reuse, Dispatch tests also verify exact keyword matching, handler construction and reuse,
unknown statements, and independent parsing sessions, including recovery after errors. unknown statements, and independent parsing sessions, including recovery after errors.
## Tree and optional model tests
`tree_node_test.cpp` is a standalone C++17 executable without Qt; see the root
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.
`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.
+136
View File
@@ -0,0 +1,136 @@
#include "dbc_parser.h"
#include "dbc_tree_builder.h"
#include "dbc_tree_model.h"
#include <QAbstractItemModelTester>
#include <QSignalSpy>
#include <QTemporaryFile>
#include <QtTest>
#include <limits>
static std::unique_ptr<TreeNode> ParsedTree() {
QTemporaryFile fixture;
if (!fixture.open()) throw std::runtime_error("Fixture creation failed");
fixture.write("BO_ 256 Status: 8 ECU\n SG_ State : 0|8@1+ (1,0) [0|255] \"rpm\" ECU\n"
" SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
"BO_ 257 Second: 8 ECU\n SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
"CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 State \"Signal comment\";\n"
"VAL_ 256 State -1 \"Negative\" 0 \"Off\" 18446744073709551615 \"Maximum\";\n");
fixture.flush();
return DbcTreeBuilder().Build(DbcParser().ParseFile(fixture.fileName().toStdString()));
}
class ModelTest : public QObject {
Q_OBJECT
private Q_SLOTS:
void navigation() {
DbcTreeModel model;
QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest);
QCOMPARE(model.rowCount(), 0);
model.SetTree(ParsedTree());
QCOMPARE(model.rowCount(), 2);
for (int row = 0; row < model.rowCount(); ++row) {
const QModelIndex frame = model.index(row, 0);
for (int column = 0; column < model.columnCount(); ++column) {
const QModelIndex frameColumn = model.index(row, column);
QVERIFY(!model.parent(frameColumn).isValid());
QCOMPARE(model.rowCount(frameColumn), column == 0 ? (row == 0 ? 2 : 1) : 0);
for (int signalRow = 0; signalRow < model.rowCount(frame); ++signalRow) {
const QModelIndex signal = model.index(signalRow, column, frame);
QCOMPARE(model.parent(signal), frame);
QCOMPARE(model.rowCount(signal), 0);
QVERIFY(!model.index(0, 0, signal).isValid());
QCOMPARE(model.flags(signal), Qt::ItemIsEnabled | Qt::ItemIsSelectable);
}
}
}
QVERIFY(!model.index(-1, 0).isValid());
QVERIFY(!model.index(2, 0).isValid());
QVERIFY(!model.index(0, -1).isValid());
QVERIFY(!model.index(0, model.columnCount()).isValid());
QVERIFY(!model.index(0, 0, model.index(0, 1)).isValid());
QVERIFY(!model.parent({}).isValid());
QVERIFY(!model.data({}).isValid());
QCOMPARE(model.flags({}), Qt::NoItemFlags);
DbcTreeModel other;
other.SetTree(ParsedTree());
const QModelIndex foreign = other.index(0, 0);
QVERIFY(!model.index(0, 0, foreign).isValid());
QVERIFY(!model.parent(foreign).isValid());
QCOMPARE(model.rowCount(foreign), 0);
QCOMPARE(model.columnCount(foreign), 0);
QVERIFY(!model.data(foreign).isValid());
QCOMPARE(model.flags(foreign), Qt::NoItemFlags);
}
void metadata() {
DbcTreeModel model;
model.SetTree(ParsedTree());
const QModelIndex frame = model.index(0, 0);
const QModelIndex signal = model.index(0, 0, frame);
QCOMPARE(model.data(frame).toString(), QString("Status"));
QCOMPARE(model.data(model.index(0, DbcTreeModel::CanId)).toString(), QString("0X100"));
QCOMPARE(model.data(frame, DbcTreeModel::CanIdRole).toUInt(), 256U);
QCOMPARE(model.data(frame, DbcTreeModel::PayloadLengthRole).toInt(), 8);
QCOMPARE(model.data(signal, DbcTreeModel::UnitRole).toString(), QString("rpm"));
const QVariantList expectedSignal = {"State", QVariant(), QVariant(), 0U, 8U,
"Intel", "Unsigned", "rpm", "Signal comment"};
const QVariantList expectedFrame = {"Status", "0X100", 8, QVariant(), QVariant(),
QVariant(), QVariant(), QVariant(), "Frame comment"};
for (int column = 0; column < model.columnCount(); ++column) {
QCOMPARE(model.data(model.index(0, column, frame)), expectedSignal.at(column));
QCOMPARE(model.data(model.index(0, column)), expectedFrame.at(column));
}
QCOMPARE(model.data(frame, DbcTreeModel::NodeTypeRole).toInt(), static_cast<int>(NodeType::Frame));
QCOMPARE(model.data(signal, DbcTreeModel::NodeTypeRole).toInt(), static_cast<int>(NodeType::Signal));
QCOMPARE(model.data(frame, DbcTreeModel::ExtendedRole).toBool(), false);
QVERIFY(!model.data(frame, DbcTreeModel::ValueDescriptionsRole).isValid());
QVERIFY(model.data(model.index(1, 0, frame), DbcTreeModel::ValueDescriptionsRole).toList().isEmpty());
QCOMPARE(model.data(signal, Qt::ToolTipRole).toString(), QString("Signal comment"));
QCOMPARE(model.data(signal, DbcTreeModel::StartBitRole).toUInt(), 0U);
QCOMPARE(model.data(signal, DbcTreeModel::BitLengthRole).toUInt(), 8U);
QCOMPARE(model.data(signal, DbcTreeModel::LittleEndianRole).toBool(), true);
QCOMPARE(model.data(signal, DbcTreeModel::SignedRole).toBool(), false);
QVERIFY(!model.data(signal, DbcTreeModel::CanIdRole).isValid());
QVERIFY(!model.data(frame, DbcTreeModel::UnitRole).isValid());
QVERIFY(!model.data(frame, Qt::EditRole).isValid());
const QVariantList values = model.data(signal, DbcTreeModel::ValueDescriptionsRole).toList();
QCOMPARE(values.size(), 3);
QCOMPARE(values.at(0).toMap().value("value").toLongLong(), -1LL);
QCOMPARE(values.at(2).toMap().value("value").toULongLong(), std::numeric_limits<qulonglong>::max());
QCOMPARE(values.at(2).toMap().value("description").toString(), QString("Maximum"));
for (int column = 0; column < model.columnCount(); ++column) {
QVERIFY(model.headerData(column, Qt::Horizontal).isValid());
QCOMPARE(model.data(model.index(0, column, frame), DbcTreeModel::ValueDescriptionsRole), QVariant(values));
}
QVERIFY(!model.headerData(-1, Qt::Horizontal).isValid());
QVERIFY(!model.headerData(model.columnCount(), Qt::Horizontal).isValid());
QVERIFY(!model.headerData(0, Qt::Vertical).isValid());
}
void replacement() {
DbcTreeModel model;
QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest);
QSignalSpy resets(&model, &QAbstractItemModel::modelReset);
model.SetTree(ParsedTree());
QPersistentModelIndex frame(model.index(0, 0));
QPersistentModelIndex signal(model.index(0, 4, frame));
model.SetTree(ParsedTree());
QVERIFY(!frame.isValid());
QVERIFY(!signal.isValid());
QCOMPARE(model.rowCount(), 2);
QPersistentModelIndex replacement(model.index(1, 0));
model.SetTree(nullptr);
QVERIFY(!replacement.isValid());
QCOMPARE(model.rowCount(), 0);
QVERIFY(!model.index(0, 0).isValid());
model.SetTree(nullptr);
QCOMPARE(resets.count(), 4);
try {
model.SetTree(std::unique_ptr<TreeNode>(new TreeNode(FrameInfo())));
QFAIL("A frame cannot be the invisible model root");
} catch (const std::invalid_argument &) {
}
QCOMPARE(resets.count(), 4);
}
};
QTEST_GUILESS_MAIN(ModelTest)
#include "dbc_tree_model_test.moc"
+8
View File
@@ -0,0 +1,8 @@
TEMPLATE = app
TARGET = dbc_model_tests
QT += core testlib
QT -= gui
CONFIG += console testcase c++17 no_keywords
CONFIG -= app_bundle debug_and_release debug_and_release_target
include(../qt/qt.pri)
SOURCES += dbc_tree_model_test.cpp
+33
View File
@@ -0,0 +1,33 @@
// Standalone C++17 test: no Qt required.
#include "dbc_tree_builder.h"
#include <stdexcept>
#include <type_traits>
static void Check(bool condition) {
if (!condition) throw std::runtime_error("Tree navigation test failed");
}
int main() {
static_assert(!std::is_move_constructible<TreeNode>::value, "Node addresses must be stable");
DbcDatabase database;
FrameInfo frame;
SignalInfo signal;
frame.signals.push_back(signal);
frame.signals.push_back(signal);
database.frames.push_back(frame);
database.frames.push_back(frame);
auto root = DbcTreeBuilder().Build(database);
Check(root->GetParent() == nullptr && root->GetRow() == 0);
root->AddChild(nullptr);
Check(root->GetChildCount() == 2 && !root->GetChild(2));
for (std::size_t row = 0; row < root->GetChildCount(); ++row) {
const TreeNode *child = root->GetChild(row);
Check(child->GetParent() == root.get() && child->GetRow() == row);
for (std::size_t position = 0; position < child->GetChildCount(); ++position) {
const TreeNode *leaf = child->GetChild(position);
Check(leaf->GetParent() == child && leaf->GetRow() == position);
}
}
auto transferred = std::move(root);
Check(transferred->GetChild(0)->GetParent() == transferred.get());
}