diff --git a/README.md b/README.md index 25acad5..edda697 100644 --- a/README.md +++ b/README.md @@ -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 + +// 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: diff --git a/changelog.md b/changelog.md index 3752a00..5f8cada 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/core/tree_node.cpp b/core/tree_node.cpp index e8af979..7bb19ee 100644 --- a/core/tree_node.cpp +++ b/core/tree_node.cpp @@ -24,8 +24,11 @@ TreeNode::TreeNode(const SignalInfo &signal) } void TreeNode::AddChild(std::unique_ptr 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; +} diff --git a/core/tree_node.h b/core/tree_node.h index a047e20..f7ff791 100644 --- a/core/tree_node.h +++ b/core/tree_node.h @@ -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 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> m_children; diff --git a/dbc.pro b/dbc.pro index fde5fbc..0f91bb4 100644 --- a/dbc.pro +++ b/dbc.pro @@ -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 +} diff --git a/qt/dbc_tree_model.cpp b/qt/dbc_tree_model.cpp new file mode 100644 index 0000000..ab08e1d --- /dev/null +++ b/qt/dbc_tree_model.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +#include "dbc_tree_model.h" +#include +#include +#include +#include + +DbcTreeModel::DbcTreeModel(QObject *parent) : QAbstractItemModel(parent) {} + +void DbcTreeModel::SetTree(std::unique_ptr 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(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(row)); + return child ? createIndex(row, column, const_cast(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(owner->GetRow()), 0, const_cast(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(qMin(owner->GetChildCount(), static_cast(std::numeric_limits::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(node->GetType()); + if (role == ValueDescriptionsRole && signal) { + QVariantList descriptions; + for (const auto &entry: signal->valueDescriptions) { + QVariantMap pair; + if (const auto *number = std::get_if(&entry.first)) + pair.insert("value", QVariant::fromValue(static_cast(*number))); + else + pair.insert("value", QVariant::fromValue(static_cast(std::get(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(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(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; +} diff --git a/qt/dbc_tree_model.h b/qt/dbc_tree_model.h new file mode 100644 index 0000000..c31d481 --- /dev/null +++ b/qt/dbc_tree_model.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +#ifndef DBC_TREE_MODEL_H +#define DBC_TREE_MODEL_H + +#include "tree_node.h" +#include + +/** 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 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 m_root; +}; +#endif diff --git a/qt/qt.pri b/qt/qt.pri new file mode 100644 index 0000000..5fef620 --- /dev/null +++ b/qt/qt.pri @@ -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 diff --git a/qt/qt.pro b/qt/qt.pro new file mode 100644 index 0000000..7accaaa --- /dev/null +++ b/qt/qt.pro @@ -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 diff --git a/tests/README.md b/tests/README.md index 218c24b..2397728 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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. diff --git a/tests/dbc_tree_model_test.cpp b/tests/dbc_tree_model_test.cpp new file mode 100644 index 0000000..068f515 --- /dev/null +++ b/tests/dbc_tree_model_test.cpp @@ -0,0 +1,136 @@ +#include "dbc_parser.h" +#include "dbc_tree_builder.h" +#include "dbc_tree_model.h" +#include +#include +#include +#include +#include + +static std::unique_ptr 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(NodeType::Frame)); + QCOMPARE(model.data(signal, DbcTreeModel::NodeTypeRole).toInt(), static_cast(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::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(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" diff --git a/tests/model_tests.pro b/tests/model_tests.pro new file mode 100644 index 0000000..b1413f0 --- /dev/null +++ b/tests/model_tests.pro @@ -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 diff --git a/tests/tree_node_test.cpp b/tests/tree_node_test.cpp new file mode 100644 index 0000000..75c0979 --- /dev/null +++ b/tests/tree_node_test.cpp @@ -0,0 +1,33 @@ +// Standalone C++17 test: no Qt required. +#include "dbc_tree_builder.h" +#include +#include + +static void Check(bool condition) { + if (!condition) throw std::runtime_error("Tree navigation test failed"); +} + +int main() { + static_assert(!std::is_move_constructible::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()); +}