1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "testtreemodel.h"
27
28 #include "autotestconstants.h"
29 #include "autotestplugin.h"
30 #include "testcodeparser.h"
31 #include "testframeworkmanager.h"
32 #include "testprojectsettings.h"
33 #include "testsettings.h"
34
35 #include <cpptools/cppmodelmanager.h>
36 #include <projectexplorer/buildsystem.h>
37 #include <projectexplorer/project.h>
38 #include <projectexplorer/session.h>
39 #include <projectexplorer/target.h>
40 #include <qmljs/qmljsmodelmanagerinterface.h>
41 #include <texteditor/texteditor.h>
42 #include <utils/algorithm.h>
43 #include <utils/qtcassert.h>
44
45 using namespace ProjectExplorer;
46 using namespace Autotest::Internal;
47
48 namespace Autotest {
49
50 static Q_LOGGING_CATEGORY(LOG, "qtc.autotest.frameworkmanager", QtWarningMsg)
51
52 static TestTreeModel *s_instance = nullptr;
53
TestTreeModel(TestCodeParser * parser)54 TestTreeModel::TestTreeModel(TestCodeParser *parser) :
55 m_parser(parser)
56 {
57 s_instance = this;
58
59 connect(m_parser, &TestCodeParser::aboutToPerformFullParse, this,
60 &TestTreeModel::removeAllTestItems, Qt::QueuedConnection);
61 connect(m_parser, &TestCodeParser::testParseResultReady,
62 this, &TestTreeModel::onParseResultReady, Qt::QueuedConnection);
63 connect(m_parser, &TestCodeParser::parsingFinished,
64 this, &TestTreeModel::sweep, Qt::QueuedConnection);
65 connect(m_parser, &TestCodeParser::parsingFailed,
66 this, &TestTreeModel::sweep, Qt::QueuedConnection);
67 connect(m_parser, &TestCodeParser::requestRemoveAllFrameworkItems,
68 this, &TestTreeModel::markAllFrameworkItemsForRemoval);
69 connect(m_parser, &TestCodeParser::requestRemoval,
70 this, &TestTreeModel::markForRemoval);
71 connect(this, &QAbstractItemModel::dataChanged,
72 this, &TestTreeModel::onDataChanged);
73 setupParsingConnections();
74 }
75
instance()76 TestTreeModel *TestTreeModel::instance()
77 {
78 return s_instance;
79 }
80
~TestTreeModel()81 TestTreeModel::~TestTreeModel()
82 {
83 s_instance = nullptr;
84 }
85
setupParsingConnections()86 void TestTreeModel::setupParsingConnections()
87 {
88 static bool connectionsInitialized = false;
89 if (connectionsInitialized)
90 return;
91 m_parser->setDirty();
92 m_parser->setState(TestCodeParser::Idle);
93
94 SessionManager *sm = SessionManager::instance();
95 connect(sm, &SessionManager::startupProjectChanged, [this, sm](Project *project) {
96 synchronizeTestFrameworks(); // we might have project settings
97 m_parser->onStartupProjectChanged(project);
98 removeAllTestToolItems();
99 synchronizeTestTools();
100 m_checkStateCache = project ? AutotestPlugin::projectSettings(project)->checkStateCache()
101 : nullptr;
102 onBuildSystemTestsUpdated(); // we may have old results if project was open before switching
103 m_failedStateCache.clear();
104 if (project) {
105 if (sm->startupBuildSystem()) {
106 connect(sm->startupBuildSystem(), &BuildSystem::testInformationUpdated,
107 this, &TestTreeModel::onBuildSystemTestsUpdated, Qt::UniqueConnection);
108 } else {
109 connect(project, &Project::activeTargetChanged,
110 this, &TestTreeModel::onTargetChanged);
111 }
112 }
113 });
114
115 CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
116 connect(cppMM, &CppTools::CppModelManager::documentUpdated,
117 m_parser, &TestCodeParser::onCppDocumentUpdated, Qt::QueuedConnection);
118 connect(cppMM, &CppTools::CppModelManager::aboutToRemoveFiles,
119 this, [this](const QStringList &files) {
120 const Utils::FilePaths filesToRemove
121 = Utils::transform(files, &Utils::FilePath::fromString);
122 removeFiles(filesToRemove);
123 }, Qt::QueuedConnection);
124 connect(cppMM, &CppTools::CppModelManager::projectPartsUpdated,
125 m_parser, &TestCodeParser::onProjectPartsUpdated);
126
127 QmlJS::ModelManagerInterface *qmlJsMM = QmlJS::ModelManagerInterface::instance();
128 connect(qmlJsMM, &QmlJS::ModelManagerInterface::documentUpdated,
129 m_parser, &TestCodeParser::onQmlDocumentUpdated, Qt::QueuedConnection);
130 connect(qmlJsMM, &QmlJS::ModelManagerInterface::aboutToRemoveFiles,
131 this, [this](const QStringList files) {
132 const Utils::FilePaths filesToRemove
133 = Utils::transform(files, &Utils::FilePath::fromString);
134 removeFiles(filesToRemove);
135 }, Qt::QueuedConnection);
136 connectionsInitialized = true;
137 }
138
setData(const QModelIndex & index,const QVariant & value,int role)139 bool TestTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
140 {
141 if (!index.isValid())
142 return false;
143
144 ITestTreeItem *item = static_cast<ITestTreeItem *>(index.internalPointer());
145 if (item && item->setData(index.column(), value, role)) {
146 emit dataChanged(index, index, {role});
147 if (role == Qt::CheckStateRole) {
148 Qt::CheckState checked = item->checked();
149 if (item->hasChildren() && checked != Qt::PartiallyChecked) {
150 // handle the new checkstate for children as well...
151 for (Utils::TreeItem *child : *item) {
152 const QModelIndex &idx = indexForItem(child);
153 setData(idx, checked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
154 }
155 }
156 if (item->parent() != rootItem()) {
157 auto parent = static_cast<ITestTreeItem *>(item->parent());
158 if (parent->checked() != checked)
159 revalidateCheckState(parent); // handle parent too
160 }
161 return true;
162 } else if (role == FailedRole) {
163 if (item->testBase()->type() == ITestBase::Framework)
164 m_failedStateCache.insert(static_cast<TestTreeItem *>(item), true);
165 }
166 }
167 return false;
168 }
169
flags(const QModelIndex & index) const170 Qt::ItemFlags TestTreeModel::flags(const QModelIndex &index) const
171 {
172 if (!index.isValid())
173 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
174
175 ITestTreeItem *item = static_cast<ITestTreeItem *>(itemForIndex(index));
176 return item->flags(index.column());
177 }
178
hasTests() const179 bool TestTreeModel::hasTests() const
180 {
181 for (Utils::TreeItem *frameworkRoot : *rootItem()) {
182 if (frameworkRoot->hasChildren())
183 return true;
184 }
185 return false;
186 }
187
getAllTestCases() const188 QList<ITestConfiguration *> TestTreeModel::getAllTestCases() const
189 {
190 QList<ITestConfiguration *> result;
191 forItemsAtLevel<1>([&result](ITestTreeItem *testRoot) {
192 result.append(testRoot->getAllTestConfigurations());
193 });
194 return result;
195 }
196
getSelectedTests() const197 QList<ITestConfiguration *> TestTreeModel::getSelectedTests() const
198 {
199 QList<ITestConfiguration *> result;
200 forItemsAtLevel<1>([&result](ITestTreeItem *testRoot) {
201 result.append(testRoot->getSelectedTestConfigurations());
202 });
203 return result;
204 }
205
getFailedTests() const206 QList<ITestConfiguration *> TestTreeModel::getFailedTests() const
207 {
208 QList<ITestConfiguration *> result;
209 forItemsAtLevel<1>([&result](ITestTreeItem *testRoot) {
210 result.append(testRoot->getFailedTestConfigurations());
211 });
212 return result;
213 }
214
getTestsForFile(const Utils::FilePath & fileName) const215 QList<ITestConfiguration *> TestTreeModel::getTestsForFile(const Utils::FilePath &fileName) const
216 {
217 QList<ITestConfiguration *> result;
218 forItemsAtLevel<1>([&result, &fileName](ITestTreeItem *testRoot) {
219 if (testRoot->testBase()->type() == ITestBase::Framework)
220 result.append(static_cast<TestTreeItem *>(testRoot)->getTestConfigurationsForFile(fileName));
221 });
222 return result;
223 }
224
testItemsByName(TestTreeItem * root,const QString & testName)225 static QList<ITestTreeItem *> testItemsByName(TestTreeItem *root, const QString &testName)
226 {
227 QList<ITestTreeItem *> result;
228
229 root->forFirstLevelChildItems([&testName, &result](TestTreeItem *node) {
230 if (node->type() == TestTreeItem::TestSuite || node->type() == TestTreeItem::TestCase) {
231 if (node->name() == testName) {
232 result << node;
233 return; // prioritize test suites and cases over test functions
234 }
235 TestTreeItem *testCase = node->findFirstLevelChildItem([&testName](TestTreeItem *it) {
236 QTC_ASSERT(it, return false);
237 return (it->type() == TestTreeItem::TestCase
238 || it->type() == TestTreeItem::TestFunction) && it->name() == testName;
239 }); // collect only actual tests, not special functions like init, cleanup etc.
240 if (testCase)
241 result << testCase;
242 } else {
243 result << testItemsByName(node, testName);
244 }
245 });
246 return result;
247 }
248
onTargetChanged(Target * target)249 void TestTreeModel::onTargetChanged(Target *target)
250 {
251 if (target && target->buildSystem()) {
252 const Target *topLevelTarget = SessionManager::startupProject()->targets().first();
253 connect(topLevelTarget->buildSystem(), &BuildSystem::testInformationUpdated,
254 this, &TestTreeModel::onBuildSystemTestsUpdated, Qt::UniqueConnection);
255 disconnect(target->project(), &Project::activeTargetChanged,
256 this, &TestTreeModel::onTargetChanged);
257 }
258 }
259
onBuildSystemTestsUpdated()260 void TestTreeModel::onBuildSystemTestsUpdated()
261 {
262 const BuildSystem *bs = SessionManager::startupBuildSystem();
263 if (!bs || !bs->project())
264 return;
265
266 QTC_ASSERT(m_checkStateCache, return);
267 m_checkStateCache->evolve(ITestBase::Tool);
268
269 ITestTool *testTool = TestFrameworkManager::testToolForBuildSystemId(bs->project()->id());
270 if (!testTool)
271 return;
272 // FIXME
273 const TestProjectSettings *projectSettings = AutotestPlugin::projectSettings(bs->project());
274 if ((projectSettings->useGlobalSettings() && !testTool->active())
275 || !projectSettings->activeTestTools().contains(testTool)) {
276 return;
277 }
278
279 ITestTreeItem *rootNode = testTool->rootNode();
280 QTC_ASSERT(rootNode, return);
281 rootNode->removeChildren();
282 for (const auto &tci : bs->testcasesInfo()) {
283 ITestTreeItem *item = testTool->createItemFromTestCaseInfo(tci);
284 QTC_ASSERT(item, continue);
285 if (Utils::optional<Qt::CheckState> cached = m_checkStateCache->get(item))
286 item->setData(0, cached.value(), Qt::CheckStateRole);
287 m_checkStateCache->insert(item, item->checked());
288 rootNode->appendChild(item);
289 }
290 revalidateCheckState(rootNode);
291 emit testTreeModelChanged();
292 }
293
frameworkRootNodes() const294 const QList<TestTreeItem *> TestTreeModel::frameworkRootNodes() const
295 {
296 QList<TestTreeItem *> result;
297 forItemsAtLevel<1>([&result](ITestTreeItem *rootNode) {
298 if (auto framework = rootNode->testBase()->asFramework())
299 result.append(framework->rootNode());
300 });
301 return result;
302 }
303
testToolRootNodes() const304 const QList<ITestTreeItem *> TestTreeModel::testToolRootNodes() const
305 {
306 QList<ITestTreeItem *> result;
307 forItemsAtLevel<1>([&result](ITestTreeItem *rootNode) {
308 if (auto testTool = rootNode->testBase()->asTestTool())
309 result.append(testTool->rootNode());
310 });
311 return result;
312 }
313
testItemsByName(const QString & testName)314 QList<ITestTreeItem *> TestTreeModel::testItemsByName(const QString &testName)
315 {
316 QList<ITestTreeItem *> result;
317 for (TestTreeItem *frameworkRoot : frameworkRootNodes())
318 result << Autotest::testItemsByName(frameworkRoot, testName);
319
320 return result;
321 }
322
synchronizeTestFrameworks()323 void TestTreeModel::synchronizeTestFrameworks()
324 {
325 ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
326 TestFrameworks sorted;
327 if (!project || AutotestPlugin::projectSettings(project)->useGlobalSettings()) {
328 sorted = Utils::filtered(TestFrameworkManager::registeredFrameworks(),
329 &ITestFramework::active);
330 qCDebug(LOG) << "Active frameworks sorted by priority" << sorted;
331 } else { // we've got custom project settings
332 const TestProjectSettings *settings = AutotestPlugin::projectSettings(project);
333 const QHash<ITestFramework *, bool> active = settings->activeFrameworks();
334 sorted = Utils::filtered(TestFrameworkManager::registeredFrameworks(),
335 [active](ITestFramework *framework) {
336 return active.value(framework, false);
337 });
338 }
339
340 const auto sortedParsers = Utils::transform(sorted, &ITestFramework::testParser);
341 // pre-check to avoid further processing when frameworks are unchanged
342 Utils::TreeItem *invisibleRoot = rootItem();
343 QSet<ITestParser *> newlyAdded;
344 QList<ITestTreeItem *> oldFrameworkRoots;
345 for (Utils::TreeItem *oldFrameworkRoot : *invisibleRoot)
346 oldFrameworkRoots.append(static_cast<ITestTreeItem *>(oldFrameworkRoot));
347
348 for (ITestTreeItem *oldFrameworkRoot : oldFrameworkRoots)
349 takeItem(oldFrameworkRoot); // do NOT delete the ptr is still held by TestFrameworkManager
350
351 for (ITestParser *parser : sortedParsers) {
352 TestTreeItem *frameworkRootNode = parser->framework()->rootNode();
353 invisibleRoot->appendChild(frameworkRootNode);
354 if (!oldFrameworkRoots.removeOne(frameworkRootNode))
355 newlyAdded.insert(parser);
356 }
357 for (ITestTreeItem *oldFrameworkRoot : oldFrameworkRoots) {
358 if (oldFrameworkRoot->testBase()->type() == ITestBase::Framework)
359 oldFrameworkRoot->removeChildren();
360 else // re-add the test tools - they are handled separately
361 invisibleRoot->appendChild(oldFrameworkRoot);
362 }
363
364 m_parser->syncTestFrameworks(sortedParsers);
365 if (!newlyAdded.isEmpty())
366 m_parser->updateTestTree(newlyAdded);
367 emit updatedActiveFrameworks(invisibleRoot->childCount());
368 }
369
synchronizeTestTools()370 void TestTreeModel::synchronizeTestTools()
371 {
372 ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
373 TestTools tools;
374 if (!project || AutotestPlugin::projectSettings(project)->useGlobalSettings()) {
375 tools = Utils::filtered(TestFrameworkManager::registeredTestTools(),
376 &ITestFramework::active);
377 qCDebug(LOG) << "Active test tools" << tools; // FIXME tools aren't sorted
378 } else { // we've got custom project settings
379 const TestProjectSettings *settings = AutotestPlugin::projectSettings(project);
380 const QHash<ITestTool *, bool> active = settings->activeTestTools();
381 tools = Utils::filtered(TestFrameworkManager::registeredTestTools(),
382 [active](ITestTool *testTool) {
383 return active.value(testTool, false);
384 });
385 }
386
387 // pre-check to avoid further processing when test tools are unchanged
388 Utils::TreeItem *invisibleRoot = rootItem();
389 QSet<ITestTool *> newlyAdded;
390 QList<ITestTreeItem *> oldFrameworkRoots;
391 for (Utils::TreeItem *oldFrameworkRoot : *invisibleRoot) {
392 auto item = static_cast<ITestTreeItem *>(oldFrameworkRoot);
393 if (item->testBase()->type() == ITestBase::Tool)
394 oldFrameworkRoots.append(item);
395 }
396
397 for (ITestTreeItem *oldFrameworkRoot : oldFrameworkRoots)
398 takeItem(oldFrameworkRoot); // do NOT delete the ptr is still held by TestFrameworkManager
399
400 for (ITestTool *testTool : qAsConst(tools)) {
401 ITestTreeItem *testToolRootNode = testTool->rootNode();
402 invisibleRoot->appendChild(testToolRootNode);
403 if (!oldFrameworkRoots.removeOne(testToolRootNode))
404 newlyAdded.insert(testTool);
405 }
406
407 if (project) {
408 const QList<Target *> &allTargets = project->targets();
409 auto target = allTargets.empty() ? nullptr : allTargets.first();
410 if (target) {
411 auto bs = target->buildSystem();
412 for (ITestTool *testTool : newlyAdded) {
413 ITestTreeItem *rootNode = testTool->rootNode();
414 QTC_ASSERT(rootNode, return);
415 rootNode->removeChildren();
416 for (const auto &tci : bs->testcasesInfo()) {
417 ITestTreeItem *item = testTool->createItemFromTestCaseInfo(tci);
418 QTC_ASSERT(item, continue);
419 if (Utils::optional<Qt::CheckState> cached = m_checkStateCache->get(item))
420 item->setData(0, cached.value(), Qt::CheckStateRole);
421 m_checkStateCache->insert(item, item->checked());
422 rootNode->appendChild(item);
423 }
424 revalidateCheckState(rootNode);
425 }
426 }
427 }
428 emit updatedActiveFrameworks(invisibleRoot->childCount());
429 }
430
filterAndInsert(TestTreeItem * item,TestTreeItem * root,bool groupingEnabled)431 void TestTreeModel::filterAndInsert(TestTreeItem *item, TestTreeItem *root, bool groupingEnabled)
432 {
433 TestTreeItem *filtered = item->applyFilters();
434 if (item->shouldBeAddedAfterFiltering())
435 insertItemInParent(item, root, groupingEnabled);
436 else // might be that all children have been filtered out
437 delete item;
438 if (filtered)
439 insertItemInParent(filtered, root, groupingEnabled);
440 }
441
rebuild(const QList<Utils::Id> & frameworkIds)442 void TestTreeModel::rebuild(const QList<Utils::Id> &frameworkIds)
443 {
444 for (const Utils::Id &id : frameworkIds) {
445 ITestFramework *framework = TestFrameworkManager::frameworkForId(id);
446 TestTreeItem *frameworkRoot = framework->rootNode();
447 const bool groupingEnabled = framework->grouping();
448 for (int row = frameworkRoot->childCount() - 1; row >= 0; --row) {
449 auto testItem = frameworkRoot->childItem(row);
450 if (testItem->type() == TestTreeItem::GroupNode) {
451 // process children of group node and delete it afterwards if necessary
452 for (int childRow = testItem->childCount() - 1; childRow >= 0; --childRow) {
453 // FIXME should this be done recursively until we have a non-GroupNode?
454 TestTreeItem *childTestItem = testItem->childItem(childRow);
455 takeItem(childTestItem);
456 filterAndInsert(childTestItem, frameworkRoot, groupingEnabled);
457 }
458 if (!groupingEnabled || testItem->childCount() == 0)
459 delete takeItem(testItem);
460 } else {
461 takeItem(testItem);
462 filterAndInsert(testItem, frameworkRoot, groupingEnabled);
463 }
464 }
465 revalidateCheckState(frameworkRoot);
466 }
467 }
468
updateCheckStateCache()469 void TestTreeModel::updateCheckStateCache()
470 {
471 m_checkStateCache->evolve(ITestBase::Framework);
472
473 for (TestTreeItem *rootNode : frameworkRootNodes()) {
474 rootNode->forAllChildItems([this](TestTreeItem *childItem) {
475 m_checkStateCache->insert(childItem, childItem->checked());
476 });
477 }
478 }
479
hasFailedTests() const480 bool TestTreeModel::hasFailedTests() const
481 {
482 auto failedItem = rootItem()->findAnyChild([](Utils::TreeItem *it) {
483 return it->data(0, FailedRole).toBool();
484 });
485 return failedItem != nullptr;
486 }
487
clearFailedMarks()488 void TestTreeModel::clearFailedMarks()
489 {
490 for (Utils::TreeItem *rootNode : *rootItem()) {
491 rootNode->forAllChildren([](Utils::TreeItem *child) {
492 child->setData(0, false, FailedRole);
493 });
494 }
495 m_failedStateCache.clear();
496 }
497
removeFiles(const Utils::FilePaths & files)498 void TestTreeModel::removeFiles(const Utils::FilePaths &files)
499 {
500 for (const Utils::FilePath &file : files)
501 markForRemoval(file);
502 sweep();
503 }
504
markAllFrameworkItemsForRemoval()505 void TestTreeModel::markAllFrameworkItemsForRemoval()
506 {
507 for (TestTreeItem *frameworkRoot : frameworkRootNodes()) {
508 frameworkRoot->forFirstLevelChildItems([](TestTreeItem *child) {
509 child->markForRemovalRecursively(true);
510 });
511 }
512 }
513
markForRemoval(const Utils::FilePath & filePath)514 void TestTreeModel::markForRemoval(const Utils::FilePath &filePath)
515 {
516 if (filePath.isEmpty())
517 return;
518
519 for (TestTreeItem *frameworkRoot : frameworkRootNodes()) {
520 for (int childRow = frameworkRoot->childCount() - 1; childRow >= 0; --childRow) {
521 TestTreeItem *child = frameworkRoot->childItem(childRow);
522 child->markForRemovalRecursively(filePath);
523 }
524 }
525 }
526
sweep()527 void TestTreeModel::sweep()
528 {
529 for (TestTreeItem *frameworkRoot : frameworkRootNodes()) {
530 sweepChildren(frameworkRoot);
531 revalidateCheckState(frameworkRoot);
532 }
533 // even if nothing has changed by the sweeping we might had parse which added or modified items
534 emit testTreeModelChanged();
535 #ifdef WITH_TESTS
536 if (m_parser->state() == TestCodeParser::Idle && !m_parser->furtherParsingExpected())
537 emit sweepingDone();
538 #endif
539 }
540
541 /**
542 * @note after calling this function emit testTreeModelChanged() if it returns true
543 */
sweepChildren(TestTreeItem * item)544 bool TestTreeModel::sweepChildren(TestTreeItem *item)
545 {
546 bool hasChanged = false;
547 for (int row = item->childCount() - 1; row >= 0; --row) {
548 TestTreeItem *child = item->childItem(row);
549
550 if (child->type() != TestTreeItem::Root && child->markedForRemoval()) {
551 destroyItem(child);
552 revalidateCheckState(item);
553 hasChanged = true;
554 } else if (child->hasChildren()) {
555 hasChanged |= sweepChildren(child);
556 if (!child->hasChildren() && child->removeOnSweepIfEmpty()) {
557 destroyItem(child);
558 revalidateCheckState(item);
559 }
560 } else {
561 hasChanged |= child->newlyAdded();
562 }
563 }
564 return hasChanged;
565 }
566
fullCopyOf(TestTreeItem * other)567 static TestTreeItem *fullCopyOf(TestTreeItem *other)
568 {
569 QTC_ASSERT(other, return nullptr);
570 TestTreeItem *result = other->copyWithoutChildren();
571 for (int row = 0, count = other->childCount(); row < count; ++row)
572 result->appendChild(fullCopyOf(other->childItem(row)));
573 return result;
574 }
575
applyParentCheckState(ITestTreeItem * parent,ITestTreeItem * newItem)576 static void applyParentCheckState(ITestTreeItem *parent, ITestTreeItem *newItem)
577 {
578 QTC_ASSERT(parent && newItem, return);
579
580 if (parent->checked() != newItem->checked()) {
581 const Qt::CheckState checkState = parent->checked() == Qt::Unchecked ? Qt::Unchecked
582 : Qt::Checked;
583 newItem->setData(0, checkState, Qt::CheckStateRole);
584 newItem->forAllChildren([checkState](Utils::TreeItem *it) {
585 it->setData(0, checkState, Qt::CheckStateRole);
586 });
587 }
588 }
589
insertItemInParent(TestTreeItem * item,TestTreeItem * root,bool groupingEnabled)590 void TestTreeModel::insertItemInParent(TestTreeItem *item, TestTreeItem *root, bool groupingEnabled)
591 {
592 TestTreeItem *parentNode = root;
593 if (groupingEnabled && item->isGroupable()) {
594 parentNode = root->findFirstLevelChildItem([item] (const TestTreeItem *it) {
595 return it->isGroupNodeFor(item);
596 });
597 if (!parentNode) {
598 parentNode = item->createParentGroupNode();
599 if (!QTC_GUARD(parentNode)) // we might not get a group node at all
600 parentNode = root;
601 else
602 root->appendChild(parentNode);
603 }
604 }
605 // check if a similar item is already present (can happen for rebuild())
606 if (auto otherItem = parentNode->findChild(item)) {
607 // only handle item's children and add them to the already present one
608 for (int row = 0, count = item->childCount(); row < count; ++row) {
609 TestTreeItem *child = fullCopyOf(item->childItem(row));
610 // use check state of the original
611 child->setData(0, item->childAt(row)->checked(), Qt::CheckStateRole);
612 otherItem->appendChild(child);
613 revalidateCheckState(child);
614 }
615 delete item;
616 } else {
617 // restore former check state if available
618 Utils::optional<Qt::CheckState> cached = m_checkStateCache->get(item);
619 if (cached.has_value())
620 item->setData(0, cached.value(), Qt::CheckStateRole);
621 else
622 applyParentCheckState(parentNode, item);
623 // ..and the failed state if available
624 Utils::optional<bool> failed = m_failedStateCache.get(item);
625 if (failed.has_value())
626 item->setData(0, *failed, FailedRole);
627 parentNode->appendChild(item);
628 revalidateCheckState(parentNode);
629 }
630 }
631
computeCheckStateByChildren(ITestTreeItem * item)632 static Qt::CheckState computeCheckStateByChildren(ITestTreeItem *item)
633 {
634 Qt::CheckState newState = Qt::Checked;
635 bool foundChecked = false;
636 bool foundUnchecked = false;
637 bool foundPartiallyChecked = false;
638
639 item->forFirstLevelChildren([&](ITestTreeItem *child) {
640 switch (child->type()) {
641 case TestTreeItem::TestDataFunction:
642 case TestTreeItem::TestSpecialFunction:
643 return;
644 default:
645 break;
646 }
647
648 foundChecked |= (child->checked() == Qt::Checked);
649 foundUnchecked |= (child->checked() == Qt::Unchecked);
650 foundPartiallyChecked |= (child->checked() == Qt::PartiallyChecked);
651
652 if (foundPartiallyChecked || (foundChecked && foundUnchecked)) {
653 newState = Qt::PartiallyChecked;
654 return;
655 }
656 });
657
658 if (newState != Qt::PartiallyChecked)
659 newState = foundUnchecked ? Qt::Unchecked : Qt::Checked;
660 return newState;
661 }
662
revalidateCheckState(ITestTreeItem * item)663 void TestTreeModel::revalidateCheckState(ITestTreeItem *item)
664 {
665 QTC_ASSERT(item, return);
666
667 const ITestTreeItem::Type type = item->type();
668 if (type == ITestTreeItem::TestSpecialFunction || type == ITestTreeItem::TestDataFunction
669 || type == ITestTreeItem::TestDataTag) {
670 return;
671 }
672 const Qt::CheckState oldState = Qt::CheckState(item->data(0, Qt::CheckStateRole).toInt());
673 Qt::CheckState newState = computeCheckStateByChildren(item);
674 if (oldState != newState) {
675 item->setData(0, newState, Qt::CheckStateRole);
676 emit dataChanged(item->index(), item->index(), {Qt::CheckStateRole});
677 if (item->parent() != rootItem()) {
678 auto parent = static_cast<ITestTreeItem *>(item->parent());
679 if (parent->checked() != newState)
680 revalidateCheckState(parent);
681 }
682 }
683 }
684
onParseResultReady(const TestParseResultPtr result)685 void TestTreeModel::onParseResultReady(const TestParseResultPtr result)
686 {
687 ITestFramework *framework = result->framework;
688 QTC_ASSERT(framework, return);
689 TestTreeItem *rootNode = framework->rootNode();
690 QTC_ASSERT(rootNode, return);
691 handleParseResult(result.data(), rootNode);
692 }
693
onDataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)694 void Autotest::TestTreeModel::onDataChanged(const QModelIndex &topLeft,
695 const QModelIndex &bottomRight,
696 const QVector<int> &roles)
697 {
698 const QModelIndex parent = topLeft.parent();
699 QTC_ASSERT(parent == bottomRight.parent(), return);
700 if (!roles.isEmpty() && !roles.contains(Qt::CheckStateRole))
701 return;
702
703 if (!m_checkStateCache) // dataChanged() may be triggered by closing a project
704 return;
705 for (int row = topLeft.row(), endRow = bottomRight.row(); row <= endRow; ++row) {
706 if (auto item = static_cast<ITestTreeItem *>(itemForIndex(index(row, 0, parent))))
707 m_checkStateCache->insert(item, item->checked());
708 }
709 }
710
handleParseResult(const TestParseResult * result,TestTreeItem * parentNode)711 void TestTreeModel::handleParseResult(const TestParseResult *result, TestTreeItem *parentNode)
712 {
713 const bool groupingEnabled = result->framework->grouping();
714 // lookup existing items
715 if (TestTreeItem *toBeModified = parentNode->find(result)) {
716 // found existing item... Do not remove
717 toBeModified->markForRemoval(false);
718 // if it's a reparse we need to mark the group node as well to avoid purging it in sweep()
719 if (groupingEnabled) {
720 if (auto directParent = toBeModified->parentItem()) {
721 if (directParent->type() == TestTreeItem::GroupNode)
722 directParent->markForRemoval(false);
723 }
724 }
725 // modify and when content has changed inform ui
726 if (toBeModified->modify(result)) {
727 const QModelIndex &idx = indexForItem(toBeModified);
728 emit dataChanged(idx, idx);
729 }
730 // recursively handle children of this item
731 for (const TestParseResult *child : result->children)
732 handleParseResult(child, toBeModified);
733 return;
734 }
735 // if there's no matching item, add the new one
736 TestTreeItem *newItem = result->createTestTreeItem();
737 QTC_ASSERT(newItem, return);
738
739 // restore former check state and fail state if available
740 newItem->forAllChildItems([this](TestTreeItem *childItem) {
741 if (!m_checkStateCache) // parse results may arrive after session switch / project close
742 return;
743 Utils::optional<Qt::CheckState> cached = m_checkStateCache->get(childItem);
744 if (cached.has_value())
745 childItem->setData(0, cached.value(), Qt::CheckStateRole);
746 Utils::optional<bool> failed = m_failedStateCache.get(childItem);
747 if (failed.has_value())
748 childItem->setData(0, *failed, FailedRole);
749 });
750 // it might be necessary to "split" created item
751 filterAndInsert(newItem, parentNode, groupingEnabled);
752 }
753
removeAllTestItems()754 void TestTreeModel::removeAllTestItems()
755 {
756 for (TestTreeItem *item : frameworkRootNodes()) {
757 item->removeChildren();
758 if (item->checked() == Qt::PartiallyChecked)
759 item->setData(0, Qt::Checked, Qt::CheckStateRole);
760 }
761 emit testTreeModelChanged();
762 }
763
removeAllTestToolItems()764 void TestTreeModel::removeAllTestToolItems()
765 {
766 for (ITestTreeItem *item : testToolRootNodes()) {
767 item->removeChildren();
768 if (item->checked() == Qt::PartiallyChecked)
769 item->setData(0, Qt::Checked, Qt::CheckStateRole);
770 }
771 emit testTreeModelChanged();
772 }
773
774 #ifdef WITH_TESTS
775 // we're inside tests - so use some internal knowledge to make testing easier
qtRootNode()776 static TestTreeItem *qtRootNode()
777 {
778 auto id = Utils::Id(Constants::FRAMEWORK_PREFIX).withSuffix("QtTest");
779 return TestFrameworkManager::frameworkForId(id)->rootNode();
780 }
781
quickRootNode()782 static TestTreeItem *quickRootNode()
783 {
784 auto id = Utils::Id(Constants::FRAMEWORK_PREFIX).withSuffix("QtQuickTest");
785 return TestFrameworkManager::frameworkForId(id)->rootNode();
786 }
787
gtestRootNode()788 static TestTreeItem *gtestRootNode()
789 {
790 auto id = Utils::Id(Constants::FRAMEWORK_PREFIX).withSuffix("GTest");
791 return TestFrameworkManager::frameworkForId(id)->rootNode();
792 }
793
boostTestRootNode()794 static TestTreeItem *boostTestRootNode()
795 {
796 auto id = Utils::Id(Constants::FRAMEWORK_PREFIX).withSuffix("Boost");
797 return TestFrameworkManager::frameworkForId(id)->rootNode();
798 }
799
autoTestsCount() const800 int TestTreeModel::autoTestsCount() const
801 {
802 TestTreeItem *rootNode = qtRootNode();
803 return rootNode ? rootNode->childCount() : 0;
804 }
805
hasUnnamedQuickTests(const ITestTreeItem * rootNode) const806 bool TestTreeModel::hasUnnamedQuickTests(const ITestTreeItem* rootNode) const
807 {
808 for (int row = 0, count = rootNode->childCount(); row < count; ++row) {
809 if (rootNode->childAt(row)->name().isEmpty())
810 return true;
811 }
812 return false;
813 }
814
unnamedQuickTests() const815 ITestTreeItem *TestTreeModel::unnamedQuickTests() const
816 {
817 TestTreeItem *rootNode = quickRootNode();
818 if (!rootNode)
819 return nullptr;
820
821 return rootNode->findFirstLevelChildItem([](TestTreeItem *it) { return it->name().isEmpty(); });
822 }
823
namedQuickTestsCount() const824 int TestTreeModel::namedQuickTestsCount() const
825 {
826 TestTreeItem *rootNode = quickRootNode();
827 return rootNode
828 ? rootNode->childCount() - (hasUnnamedQuickTests(rootNode) ? 1 : 0)
829 : 0;
830 }
831
unnamedQuickTestsCount() const832 int TestTreeModel::unnamedQuickTestsCount() const
833 {
834 if (ITestTreeItem *unnamed = unnamedQuickTests())
835 return unnamed->childCount();
836 return 0;
837 }
838
dataTagsCount() const839 int TestTreeModel::dataTagsCount() const
840 {
841 TestTreeItem *rootNode = qtRootNode();
842 if (!rootNode)
843 return 0;
844
845 int dataTagCount = 0;
846 rootNode->forFirstLevelChildren([&dataTagCount](ITestTreeItem *classItem) {
847 classItem->forFirstLevelChildren([&dataTagCount](ITestTreeItem *functionItem) {
848 dataTagCount += functionItem->childCount();
849 });
850 });
851 return dataTagCount;
852 }
853
gtestNamesCount() const854 int TestTreeModel::gtestNamesCount() const
855 {
856 TestTreeItem *rootNode = gtestRootNode();
857 return rootNode ? rootNode->childCount() : 0;
858 }
859
gtestNamesAndSets() const860 QMultiMap<QString, int> TestTreeModel::gtestNamesAndSets() const
861 {
862 QMultiMap<QString, int> result;
863
864 if (TestTreeItem *rootNode = gtestRootNode()) {
865 rootNode->forFirstLevelChildren([&result](ITestTreeItem *child) {
866 result.insert(child->name(), child->childCount());
867 });
868 }
869 return result;
870 }
871
boostTestNamesCount() const872 int TestTreeModel::boostTestNamesCount() const
873 {
874 TestTreeItem *rootNode = boostTestRootNode();
875 return rootNode ? rootNode->childCount() : 0;
876 }
877
boostTestSuitesAndTests() const878 QMap<QString, int> TestTreeModel::boostTestSuitesAndTests() const
879 {
880 QMap<QString, int> result;
881
882 if (TestTreeItem *rootNode = boostTestRootNode()) {
883 rootNode->forFirstLevelChildItems([&result](TestTreeItem *child) {
884 result.insert(child->name() + '|' + child->proFile().toString(), child->childCount());
885 });
886 }
887 return result;
888 }
889
890 #endif
891
892 /***************************** Sort/Filter Model **********************************/
893
TestTreeSortFilterModel(TestTreeModel * sourceModel,QObject * parent)894 TestTreeSortFilterModel::TestTreeSortFilterModel(TestTreeModel *sourceModel, QObject *parent)
895 : QSortFilterProxyModel(parent)
896 {
897 setSourceModel(sourceModel);
898 }
899
setSortMode(ITestTreeItem::SortMode sortMode)900 void TestTreeSortFilterModel::setSortMode(ITestTreeItem::SortMode sortMode)
901 {
902 m_sortMode = sortMode;
903 invalidate();
904 }
905
setFilterMode(FilterMode filterMode)906 void TestTreeSortFilterModel::setFilterMode(FilterMode filterMode)
907 {
908 m_filterMode = filterMode;
909 invalidateFilter();
910 }
911
toggleFilter(FilterMode filterMode)912 void TestTreeSortFilterModel::toggleFilter(FilterMode filterMode)
913 {
914 m_filterMode = toFilterMode(m_filterMode ^ filterMode);
915 invalidateFilter();
916 }
917
toFilterMode(int f)918 TestTreeSortFilterModel::FilterMode TestTreeSortFilterModel::toFilterMode(int f)
919 {
920 switch (f) {
921 case TestTreeSortFilterModel::ShowInitAndCleanup:
922 return TestTreeSortFilterModel::ShowInitAndCleanup;
923 case TestTreeSortFilterModel::ShowTestData:
924 return TestTreeSortFilterModel::ShowTestData;
925 case TestTreeSortFilterModel::ShowAll:
926 return TestTreeSortFilterModel::ShowAll;
927 default:
928 return TestTreeSortFilterModel::Basic;
929 }
930 }
931
lessThan(const QModelIndex & left,const QModelIndex & right) const932 bool TestTreeSortFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
933 {
934 // root items keep the intended order
935 const ITestTreeItem *leftItem = static_cast<ITestTreeItem *>(left.internalPointer());
936 if (leftItem->type() == ITestTreeItem::Root)
937 return left.row() > right.row();
938
939 const ITestTreeItem *rightItem = static_cast<ITestTreeItem *>(right.internalPointer());
940 return leftItem->lessThan(rightItem, m_sortMode);
941 }
942
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const943 bool TestTreeSortFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
944 {
945 const QModelIndex index = sourceModel()->index(sourceRow, 0,sourceParent);
946 if (!index.isValid())
947 return false;
948
949 const ITestTreeItem *item = static_cast<ITestTreeItem *>(index.internalPointer());
950
951 switch (item->type()) {
952 case ITestTreeItem::TestDataFunction:
953 return m_filterMode & ShowTestData;
954 case ITestTreeItem::TestSpecialFunction:
955 return m_filterMode & ShowInitAndCleanup;
956 default:
957 return true;
958 }
959 }
960
961 } // namespace Autotest
962