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