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 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,
and a Qt model adapter are left to the consuming application.
used by the regression tests; the optional adapter requires 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,
[tests/README.md](tests/README.md) for test instructions, and
@@ -55,7 +55,7 @@ DbcTreeBuilder
↓
TreeNode hierarchy
↓
future Qt UI
DbcTreeModel → Qt views
```
### 2. Runtime decode path
@@ -114,7 +114,8 @@ This avoids forcing UI-oriented structures into a decode role they were not mean
```text
core/ C++17 parser, statement handlers, metadata, tree, and decoder
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
dbc.pro Root qmake project: core + tests
Doxyfile API documentation configuration
@@ -235,7 +236,7 @@ has no metadata payload. The payload types are:
- `FrameInfo`
- `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.
@@ -884,7 +885,7 @@ nonnegative `uint64_t`; when constructing mappings manually, normalize keys
with `NormalizeValueKey()` before insertion.
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:
- parent pointer in `TreeNode`
- Qt model adapter
- better display strings for UI
- selective decoding of only chosen signals
### Stage 3
@@ -1009,6 +1007,99 @@ make -j
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
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 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
factory, with complete syntax validation, exact signed/unsigned 64-bit keys,
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) {
if (child)
if (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 {
@@ -61,3 +64,11 @@ const FrameInfo *TreeNode::GetFrame() const {
const SignalInfo *TreeNode::GetSignal() const {
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 &operator=(const TreeNode &) = delete;
TreeNode(TreeNode &&) = default;
TreeNode &operator=(TreeNode &&) = default;
TreeNode(TreeNode &&) = delete;
TreeNode &operator=(TreeNode &&) = delete;
/**
* @brief Add child node.
@@ -65,6 +65,12 @@ public:
*/
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.
* @return Number of children.
@@ -110,6 +116,8 @@ public:
const SignalInfo *GetSignal() const;
private:
TreeNode *m_parent = nullptr;
std::size_t m_row = 0U;
NodeType m_type;
std::string m_name;
std::vector<std::unique_ptr<TreeNode>> m_children;
+7
View File
@@ -3,3 +3,10 @@ SUBDIRS += core tests
core.subdir = core
tests.subdir = tests
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,
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());
}