From d60f2d758bc991021f283f48f567c4afd590ee2c Mon Sep 17 00:00:00 2001 From: Wang Zichong Date: Wed, 16 Jul 2025 16:07:30 +0800 Subject: [PATCH] fix: many issues with RoleCombineModel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正 RoleCombineModel 的若干问题,包括索引映射错误,minor 模型变化处理不完整,QAbstractItemModelTester运行失败等. Log: --- panels/dock/taskmanager/rolecombinemodel.cpp | 228 ++++++++++++++-- panels/dock/taskmanager/rolecombinemodel.h | 1 + .../taskmanager/rolecombinemodeltests.cpp | 250 ++++++++++++++++++ 3 files changed, 458 insertions(+), 21 deletions(-) diff --git a/panels/dock/taskmanager/rolecombinemodel.cpp b/panels/dock/taskmanager/rolecombinemodel.cpp index 8bad47c2b..e93ba6348 100644 --- a/panels/dock/taskmanager/rolecombinemodel.cpp +++ b/panels/dock/taskmanager/rolecombinemodel.cpp @@ -24,42 +24,133 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel } connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, [this, majorRoles, func](const QModelIndex &parent, int first, int last) { - beginInsertRows(index(parent.row(), parent.column()), first, last); + // 对于QAbstractListModel,parent通常是无效的,我们直接使用QModelIndex() + beginInsertRows(QModelIndex(), first, last); + + // 先调整现有映射中后续行的索引 + QMap, QPair> newIndexMap; + for (auto it = m_indexMap.constBegin(); it != m_indexMap.constEnd(); ++it) { + int row = it.key().first; + int col = it.key().second; + + if (row >= first) { + // 将后续行的索引向后移动 + int newRow = row + (last - first + 1); + newIndexMap[qMakePair(newRow, col)] = it.value(); + } else { + // 保持前面行的映射不变 + newIndexMap[it.key()] = it.value(); + } + } + m_indexMap = newIndexMap; + + // 为新插入的行创建映射 + int columnCount = sourceModel()->columnCount(); for (int i = first; i <= last; i++) { - QModelIndex majorIndex = sourceModel()->index(i, 0); - QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); - if (majorIndex.isValid() && minorIndex.isValid()) - m_indexMap[qMakePair(i, 0)] = qMakePair(minorIndex.row(), minorIndex.column()); + for (int j = 0; j < columnCount; j++) { + QModelIndex majorIndex = sourceModel()->index(i, j); + QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); + if (majorIndex.isValid() && minorIndex.isValid()) + m_indexMap[qMakePair(i, j)] = qMakePair(minorIndex.row(), minorIndex.column()); + } } endInsertRows(); }); + connect(sourceModel(), &QAbstractItemModel::columnsInserted, this, [this, majorRoles, func](const QModelIndex &parent, int first, int last) { - beginInsertColumns(index(parent.row(), parent.column()), first, last); + beginInsertColumns(QModelIndex(), first, last); + + // 先调整现有映射中后续列的索引 + QMap, QPair> newIndexMap; + for (auto it = m_indexMap.constBegin(); it != m_indexMap.constEnd(); ++it) { + int row = it.key().first; + int col = it.key().second; + + if (col >= first) { + // 将后续列的索引向后移动 + int newCol = col + (last - first + 1); + newIndexMap[qMakePair(row, newCol)] = it.value(); + } else { + // 保持前面列的映射不变 + newIndexMap[it.key()] = it.value(); + } + } + m_indexMap = newIndexMap; + + // 为新插入的列创建映射 + int rowCount = sourceModel()->rowCount(); for (int j = first; j <= last; j++) { - QModelIndex majorIndex = sourceModel()->index(0, j); - QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); - if (majorIndex.isValid() && minorIndex.isValid()) - m_indexMap[qMakePair(0, j)] = qMakePair(minorIndex.row(), minorIndex.column()); + for (int i = 0; i < rowCount; i++) { + QModelIndex majorIndex = sourceModel()->index(i, j); + QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); + if (majorIndex.isValid() && minorIndex.isValid()) + m_indexMap[qMakePair(i, j)] = qMakePair(minorIndex.row(), minorIndex.column()); + } } endInsertColumns(); }); connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int first, int last) { - beginRemoveRows(index(parent.row(), parent.column()), first, last); + beginRemoveRows(QModelIndex(), first, last); + + // 删除被移除行的映射 + int columnCount = sourceModel()->columnCount(); for (int i = first; i <= last; i++) { - if (m_indexMap.contains(qMakePair(i, 0))) { - m_indexMap.remove(qMakePair(i, 0)); + for (int j = 0; j < columnCount; j++) { + m_indexMap.remove(qMakePair(i, j)); + } + } + + // 调整后续行的索引映射 + QMap, QPair> newIndexMap; + for (auto it = m_indexMap.constBegin(); it != m_indexMap.constEnd(); ++it) { + int row = it.key().first; + int col = it.key().second; + + if (row > last) { + // 将后续行的索引向前移动 + int newRow = row - (last - first + 1); + newIndexMap[qMakePair(newRow, col)] = it.value(); + } else if (row < first) { + // 保持前面行的映射不变 + newIndexMap[it.key()] = it.value(); } + // 被删除行的映射已经在上面移除了 } + m_indexMap = newIndexMap; + endRemoveRows(); }); + connect(sourceModel(), &QAbstractItemModel::columnsRemoved, this, [this](const QModelIndex &parent, int first, int last) { - beginRemoveColumns(index(parent.row(), parent.column()), first, last); + beginRemoveColumns(QModelIndex(), first, last); + + // 删除被移除列的映射 + int rowCount = sourceModel()->rowCount(); for (int j = first; j <= last; j++) { - if (m_indexMap.contains(qMakePair(0, j))) { - m_indexMap.remove(qMakePair(0, j)); + for (int i = 0; i < rowCount; i++) { + m_indexMap.remove(qMakePair(i, j)); + } + } + + // 调整后续列的索引映射 + QMap, QPair> newIndexMap; + for (auto it = m_indexMap.constBegin(); it != m_indexMap.constEnd(); ++it) { + int row = it.key().first; + int col = it.key().second; + + if (col > last) { + // 将后续列的索引向前移动 + int newCol = col - (last - first + 1); + newIndexMap[qMakePair(row, newCol)] = it.value(); + } else if (col < first) { + // 保持前面列的映射不变 + newIndexMap[it.key()] = it.value(); } + // 被删除列的映射已经在上面移除了 } + m_indexMap = newIndexMap; + endRemoveColumns(); }); @@ -104,13 +195,73 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel } }); + // 添加对minor模型删除操作的处理 + connect(m_minor, &QAbstractItemModel::rowsRemoved, this, [this, majorRoles, func](const QModelIndex &parent, int first, int last) { + // 当minor模型删除行时,需要更新映射并可能触发数据变化信号 + QList affectedMajorIndexes; + + // 找到受影响的major索引 + for (auto it = m_indexMap.begin(); it != m_indexMap.end();) { + int minorRow = it.value().first; + int minorCol = it.value().second; + + if (minorRow >= first && minorRow <= last) { + // 这个映射指向的minor行被删除了,需要重新建立映射 + int majorRow = it.key().first; + int majorCol = it.key().second; + auto majorIndex = sourceModel()->index(majorRow, majorCol); + + if (majorIndex.isValid()) { + affectedMajorIndexes.append(majorIndex); + // 尝试重新建立映射 + QModelIndex newMinorIndex = func(majorIndex.data(majorRoles), m_minor); + if (newMinorIndex.isValid()) { + it.value() = qMakePair(newMinorIndex.row(), newMinorIndex.column()); + ++it; + } else { + // 无法建立新映射,删除这个映射 + it = m_indexMap.erase(it); + } + } else { + it = m_indexMap.erase(it); + } + } else if (minorRow > last) { + // 调整后续行的索引 + int newMinorRow = minorRow - (last - first + 1); + it.value().first = newMinorRow; + ++it; + } else { + ++it; + } + } + + // 对受影响的major索引发送数据变化信号 + for (const auto &majorIndex : affectedMajorIndexes) { + Q_EMIT dataChanged(majorIndex, majorIndex, m_minorRolesMap.values()); + } + }); + + connect(m_minor, &QAbstractItemModel::columnsRemoved, this, [this, majorRoles, func](const QModelIndex &parent, int first, int last) { + // 当minor模型删除列时,需要更新映射 + for (auto it = m_indexMap.begin(); it != m_indexMap.end(); ++it) { + int minorRow = it.value().first; + int minorCol = it.value().second; + + if (minorCol > last) { + // 调整后续列的索引 + int newMinorCol = minorCol - (last - first + 1); + it.value().second = newMinorCol; + } + } + }); + connect(m_minor, &QAbstractItemModel::rowsInserted, this, [this, majorRoles, func](const QModelIndex &parent, int first, int last){ auto rowCount = sourceModel()->rowCount(); auto columnCount = sourceModel()->columnCount(); for (int i = 0; i < rowCount; i++) { for (int j = 0; j < columnCount; j++) { - // alreay bind, pass this + // already bind, pass this if (m_indexMap.contains(qMakePair(i ,j))) continue; @@ -127,8 +278,25 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel // create minor role map auto minorRolenames = m_minor->roleNames(); m_roleNames = createRoleNames(); - std::for_each(minorRolenames.constBegin(), minorRolenames.constEnd(), [&minorRolenames, this](auto &roleName) { - m_minorRolesMap.insert(m_roleNames.key(roleName), minorRolenames.key(roleName)); + + // 修复角色映射逻辑:应该映射到新创建的minor角色,而不是major角色 + auto majorRoleNames = sourceModel()->roleNames(); + + std::for_each(minorRolenames.constBegin(), minorRolenames.constEnd(), [&minorRolenames, &majorRoleNames, this](auto &roleName) { + int minorRoleKey = minorRolenames.key(roleName); + + // 在组合角色中找到对应的key,但排除major模型已有的key + int combinedRoleKey = -1; + for (auto it = m_roleNames.constBegin(); it != m_roleNames.constEnd(); ++it) { + if (it.value() == roleName && !majorRoleNames.contains(it.key())) { + combinedRoleKey = it.key(); + break; + } + } + + if (combinedRoleKey != -1) { + m_minorRolesMap.insert(combinedRoleKey, minorRoleKey); + } }); } @@ -152,19 +320,31 @@ QHash RoleCombineModel::roleNames() const int RoleCombineModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) + return 0; // 平坦列表模型:有效的parent表示某个项目,项目没有子项 return sourceModel()->rowCount(); } int RoleCombineModel::columnCount(const QModelIndex &parent) const { + if (parent.isValid()) + return 0; // 平坦列表模型:有效的parent表示某个项目,项目没有子项 return sourceModel()->columnCount(); } QVariant RoleCombineModel::data(const QModelIndex &index, int role) const { if (m_minorRolesMap.contains(role)) { - int row, column; - std::tie(row, column) = m_indexMap.value(qMakePair(index.row(), index.column()), qMakePair(-1, -1)); + auto majorKey = qMakePair(index.row(), index.column()); + auto mapping = m_indexMap.value(majorKey, qMakePair(-1, -1)); + int row = mapping.first; + int column = mapping.second; + + // 检查映射是否有效 + if (row == -1 || column == -1) { + return QVariant(); // 返回空值而不是用无效索引访问 + } + return m_minor->data(m_minor->index(row, column), m_minorRolesMap[role]); } else { return sourceModel()->data(sourceModel()->index(index.row(), index.column()), role); @@ -176,6 +356,12 @@ bool RoleCombineModel::hasIndex(int row, int column, const QModelIndex &parent) return sourceModel()->hasIndex(row, column, parent); } +bool RoleCombineModel::hasChildren(const QModelIndex &parent) const +{ + // 平坦列表模型:只有根节点有子项,其他项目都没有子项 + return !parent.isValid() && rowCount(parent) > 0; +} + QModelIndex RoleCombineModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) diff --git a/panels/dock/taskmanager/rolecombinemodel.h b/panels/dock/taskmanager/rolecombinemodel.h index 3aa34fdb4..3d7fb8004 100644 --- a/panels/dock/taskmanager/rolecombinemodel.h +++ b/panels/dock/taskmanager/rolecombinemodel.h @@ -24,6 +24,7 @@ class RoleCombineModel : public QAbstractProxyModel QHash roleNames() const override; bool hasIndex(int row, int column, const QModelIndex &parent = QModelIndex()) const; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; Q_INVOKABLE virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; Q_INVOKABLE virtual QModelIndex parent(const QModelIndex &child) const override; diff --git a/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp b/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp index a45235701..493b70711 100644 --- a/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp +++ b/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +#include #include #include @@ -47,6 +48,23 @@ TEST(RoleCombineModel, RowCountTest) { EXPECT_NE(model.rowCount(), modelB.rowCount()); } +TEST(RoleGroupModel, ModelTest) +{ + TestModelA modelA; + TestModelB modelB; + modelA.addData(new DataA(0, &modelA)); + modelA.addData(new DataA(1, &modelA)); + + modelB.addData(new DataB(0, &modelB)); + modelB.addData(new DataB(1, &modelB)); + modelB.addData(new DataB(2, &modelB)); + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + return QModelIndex(); + }); + + auto tester = new QAbstractItemModelTester(&model, QAbstractItemModelTester::FailureReportingMode::Fatal); +} + TEST(RoleCombineModel, dataTest) { TestModelA modelA; TestModelB modelB; @@ -99,3 +117,235 @@ TEST(RoleCombineModel, dataTest) { modelB.setData(modelB.index(1), "dataB22"); EXPECT_EQ(model.index(1, 0).data(names2Role.value(roleNamesB.value(TestModelB::dataRole))), modelB.index(1, 0).data(TestModelB::dataRole)); } + +// 新增测试用例:检测行删除后索引映射错误的问题 +TEST(RoleCombineModel, IndexMappingAfterRowRemovalBug) +{ + TestModelA modelA; + TestModelB modelB; + + // 创建combine model,通过ID匹配 + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + auto matches = model->match(model->index(0, 0), TestModelB::idRole, data); + return matches.isEmpty() ? QModelIndex() : matches.first(); + }); + + // 添加测试数据 - modelA有id为0,1,2,3的数据 + DataA *dataA0 = new DataA(0, "dataA0", &modelA); + DataA *dataA1 = new DataA(1, "dataA1", &modelA); + DataA *dataA2 = new DataA(2, "dataA2", &modelA); + DataA *dataA3 = new DataA(3, "dataA3", &modelA); + + modelA.addData(dataA0); + modelA.addData(dataA1); + modelA.addData(dataA2); + modelA.addData(dataA3); + + // modelB有对应的id数据 + modelB.addData(new DataB(0, "dataB0", &modelB)); + modelB.addData(new DataB(1, "dataB1", &modelB)); + modelB.addData(new DataB(2, "dataB2", &modelB)); + modelB.addData(new DataB(3, "dataB3", &modelB)); + + auto roleNames = model.roleNames(); + auto roleNamesB = modelB.roleNames(); + QHash names2Role; + for (auto roleName : roleNames.keys()) { + names2Role.insert(roleNames.value(roleName), roleName); + } + + int bDataRole = names2Role.value(roleNamesB.value(TestModelB::dataRole)); + + // 删除前验证数据正确性 + EXPECT_EQ(model.index(0, 0).data(bDataRole).toString(), "dataB0"); + EXPECT_EQ(model.index(1, 0).data(bDataRole).toString(), "dataB1"); + EXPECT_EQ(model.index(2, 0).data(bDataRole).toString(), "dataB2"); + EXPECT_EQ(model.index(3, 0).data(bDataRole).toString(), "dataB3"); + + // 删除第1行 (索引1,包含id=1的数据) + modelA.removeData(dataA1); + + // 验证行数正确 + EXPECT_EQ(model.rowCount(), 3); + + // 这里会暴露问题:删除第1行后,原来的第2、3行索引映射没有正确更新 + // 原来第2行(id=2)现在应该在第1行位置,但由于映射没更新,可能访问到错误数据 + EXPECT_EQ(model.index(0, 0).data(bDataRole).toString(), "dataB0"); // 第0行应该正常 + + // 这个测试会失败,暴露了索引映射更新的问题 + // 原来第2行(id=2)现在在第1行位置,应该返回"dataB2" + EXPECT_EQ(model.index(1, 0).data(bDataRole).toString(), "dataB2"); + + // 原来第3行(id=3)现在在第2行位置,应该返回"dataB3" + EXPECT_EQ(model.index(2, 0).data(bDataRole).toString(), "dataB3"); +} + +// 新增测试用例:检测parent参数处理问题 +TEST(RoleCombineModel, InvalidParentHandlingBug) +{ + TestModelA modelA; + TestModelB modelB; + + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + return QModelIndex(); + }); + + modelA.addData(new DataA(0, "dataA0", &modelA)); + + // 测试无效parent的情况,这可能导致index(parent.row(), parent.column())访问无效索引 + // 在实际使用中,parent可能是无效的QModelIndex() + QModelIndex invalidParent; // 无效的parent + + // 当parent无效时调用parent.row()和parent.column()会返回-1 + // 这时index(-1, -1)可能导致问题 + // 这个测试主要验证代码不会崩溃 + EXPECT_NO_THROW({ + // 触发rowsInserted信号来测试parent处理 + modelA.addData(new DataA(1, "dataA1", &modelA)); + }); +} + +// 新增测试用例:检测minor模型变化处理不完整的问题 +TEST(RoleCombineModel, MinorModelChangesHandlingBug) +{ + TestModelA modelA; + TestModelB modelB; + + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + auto matches = model->match(model->index(0, 0), TestModelB::idRole, data); + return matches.isEmpty() ? QModelIndex() : matches.first(); + }); + + modelA.addData(new DataA(0, "dataA0", &modelA)); + modelA.addData(new DataA(1, "dataA1", &modelA)); + + DataB *dataB0 = new DataB(0, "dataB0", &modelB); + DataB *dataB1 = new DataB(1, "dataB1", &modelB); + DataB *dataB2 = new DataB(2, "dataB2", &modelB); + + modelB.addData(dataB0); + modelB.addData(dataB1); + modelB.addData(dataB2); + + auto roleNames = model.roleNames(); + auto roleNamesB = modelB.roleNames(); + QHash names2Role; + for (auto roleName : roleNames.keys()) { + names2Role.insert(roleNames.value(roleName), roleName); + } + int bDataRole = names2Role.value(roleNamesB.value(TestModelB::dataRole)); + + // 验证初始映射正确 + EXPECT_EQ(model.index(0, 0).data(bDataRole).toString(), "dataB0"); + EXPECT_EQ(model.index(1, 0).data(bDataRole).toString(), "dataB1"); + + // 删除minor模型中的数据 - 这里会暴露问题: + // RoleCombineModel没有处理minor模型的rowsRemoved信号 + modelB.removeData(dataB0); + + // 删除后,映射应该更新,但由于没有处理rowsRemoved信号,映射可能变得无效 + // 这可能导致访问已删除的数据或返回错误结果 + // 注意:由于当前实现没有处理minor模型的删除,这个测试主要验证不会崩溃 + EXPECT_NO_THROW({ + auto result = model.index(0, 0).data(bDataRole); + // 结果可能是无效的,但至少不应该崩溃 + }); +} + +// 新增测试用例:验证角色映射修复 +TEST(RoleCombineModel, RoleMappingFix) +{ + TestModelA modelA; + TestModelB modelB; + + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + auto matches = model->match(model->index(0, 0), TestModelB::idRole, data); + return matches.isEmpty() ? QModelIndex() : matches.first(); + }); + + modelA.addData(new DataA(0, "dataA0", &modelA)); + modelB.addData(new DataB(0, "dataB0", &modelB)); + + // 验证角色映射正确性:Minor角色应该映射到新创建的组合角色,而不是Major角色 + auto combinedRoleNames = model.roleNames(); + auto majorRoleNames = modelA.roleNames(); + auto minorRoleNames = modelB.roleNames(); + + // 验证组合模型包含所有角色 + EXPECT_GT(combinedRoleNames.size(), majorRoleNames.size()); + EXPECT_GT(combinedRoleNames.size(), minorRoleNames.size()); + + // 验证Minor数据可以通过组合模型正确访问 + // 找到Minor的"data"角色在组合模型中的key + int minorDataRoleInCombined = -1; + for (auto it = combinedRoleNames.constBegin(); it != combinedRoleNames.constEnd(); ++it) { + if (it.value() == "bData" && !majorRoleNames.contains(it.key())) { + minorDataRoleInCombined = it.key(); + break; + } + } + + EXPECT_NE(minorDataRoleInCombined, -1) << "Minor data role should be found in combined model"; + + // 验证可以通过组合模型获取Minor数据 + auto index = model.index(0, 0); + auto minorData = index.data(minorDataRoleInCombined); + EXPECT_EQ(minorData.toString(), "dataB0") << "Should be able to access minor data through combined model"; +} + +// 新增测试用例:验证数据访问安全性修复 +TEST(RoleCombineModel, DataAccessSafetyFix) +{ + TestModelA modelA; + TestModelB modelB; + + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + auto matches = model->match(model->index(0, 0), TestModelB::idRole, data); + return matches.isEmpty() ? QModelIndex() : matches.first(); + }); + + // 添加没有对应Minor数据的Major数据 + modelA.addData(new DataA(999, "orphanData", &modelA)); + + // 验证访问无映射的数据时不会崩溃,应该返回空值 + auto combinedRoleNames = model.roleNames(); + int minorDataRole = -1; + for (auto it = combinedRoleNames.constBegin(); it != combinedRoleNames.constEnd(); ++it) { + if (it.value() == "bData") { + minorDataRole = it.key(); + break; + } + } + + ASSERT_NE(minorDataRole, -1); + + // 访问没有Minor映射的数据应该返回空值而不是崩溃 + auto index = model.index(0, 0); + EXPECT_NO_THROW({ + auto result = index.data(minorDataRole); + EXPECT_FALSE(result.isValid()) << "Should return invalid QVariant for unmapped minor data"; + }); +} + +// 新增测试用例:验证parent参数处理修复 +TEST(RoleCombineModel, ParentParameterHandlingFix) +{ + TestModelA modelA; + TestModelB modelB; + + // 这个测试主要验证不会因为parent参数处理问题导致Qt模型警告或崩溃 + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + auto matches = model->match(model->index(0, 0), TestModelB::idRole, data); + return matches.isEmpty() ? QModelIndex() : matches.first(); + }); + + // 添加数据时会触发rowsInserted信号,测试parent参数处理 + EXPECT_NO_THROW({ + modelA.addData(new DataA(0, "testData", &modelA)); + modelB.addData(new DataB(0, "testDetail", &modelB)); + }); + + // 验证数据正确添加 + EXPECT_EQ(model.rowCount(), 1); + EXPECT_TRUE(model.index(0, 0).isValid()); +}