Closes #9
This commit is contained in:
1 parent
e5d818d9b2
commit
a533d9aa00
13 files changed
+527
-12
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user