testcodeparser.cpp 32.5 KB
Newer Older
Christian Stenger's avatar
Christian Stenger committed
1
2
/****************************************************************************
**
Christian Stenger's avatar
Christian Stenger committed
3
** Copyright (C) 2015 The Qt Company Ltd
Christian Stenger's avatar
Christian Stenger committed
4
** All rights reserved.
Christian Stenger's avatar
Christian Stenger committed
5
6
** For any questions to The Qt Company, please use contact form at
** http://www.qt.io/contact-us
Christian Stenger's avatar
Christian Stenger committed
7
8
9
10
11
12
**
** This file is part of the Qt Creator Enterprise Auto Test Add-on.
**
** Licensees holding valid Qt Enterprise licenses may use this file in
** accordance with the Qt Enterprise License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
Christian Stenger's avatar
Christian Stenger committed
13
** a written agreement between you and The Qt Company.
Christian Stenger's avatar
Christian Stenger committed
14
15
**
** If you have questions regarding the use of this file, please use
Christian Stenger's avatar
Christian Stenger committed
16
** contact form at http://www.qt.io/contact-us
Christian Stenger's avatar
Christian Stenger committed
17
18
19
**
****************************************************************************/

20
#include "autotestconstants.h"
Christian Stenger's avatar
Christian Stenger committed
21
22
23
24
#include "testcodeparser.h"
#include "testinfo.h"
#include "testvisitor.h"

25
#include <coreplugin/editormanager/editormanager.h>
26
#include <coreplugin/progressmanager/futureprogress.h>
27
28
#include <coreplugin/progressmanager/progressmanager.h>

Christian Stenger's avatar
Christian Stenger committed
29
30
31
#include <cplusplus/LookupContext.h>
#include <cplusplus/TypeOfExpression.h>

32
#include <cpptools/cpptoolsconstants.h>
Christian Stenger's avatar
Christian Stenger committed
33
34
35
#include <cpptools/cppmodelmanager.h>
#include <cpptools/cppworkingcopy.h>

36
#include <projectexplorer/project.h>
Christian Stenger's avatar
Christian Stenger committed
37
38
#include <projectexplorer/session.h>

39
40
41
42
#include <qmljs/parser/qmljsast_p.h>
#include <qmljs/qmljsdialect.h>
#include <qmljstools/qmljsmodelmanager.h>

43
#include <utils/multitask.h>
44
#include <utils/qtcassert.h>
45
46
#include <utils/textfileformat.h>

47
#include <QDirIterator>
48
49
#include <QFuture>
#include <QFutureInterface>
50
#include <QTimer>
51

Christian Stenger's avatar
Christian Stenger committed
52
53
54
55
56
57
namespace Autotest {
namespace Internal {

TestCodeParser::TestCodeParser(TestTreeModel *parent)
    : QObject(parent),
      m_model(parent),
58
      m_codeModelParsing(false),
59
60
      m_fullUpdatePostponed(false),
      m_partialUpdatePostponed(false),
61
62
      m_dirty(false),
      m_singleShotScheduled(false),
63
      m_parserState(Disabled)
Christian Stenger's avatar
Christian Stenger committed
64
{
65
    // connect to ProgressManager to postpone test parsing when CppModelManager is parsing
Christian Stenger's avatar
Christian Stenger committed
66
67
68
69
70
    auto progressManager = qobject_cast<Core::ProgressManager *>(Core::ProgressManager::instance());
    connect(progressManager, &Core::ProgressManager::taskStarted,
            this, &TestCodeParser::onTaskStarted);
    connect(progressManager, &Core::ProgressManager::allTasksFinished,
            this, &TestCodeParser::onAllTasksFinished);
71
72
    connect(this, &TestCodeParser::partialParsingFinished,
            this, &TestCodeParser::onPartialParsingFinished);
Christian Stenger's avatar
Christian Stenger committed
73
74
}

75
76
TestCodeParser::~TestCodeParser()
{
77
    clearCache();
78
79
}

80
81
void TestCodeParser::setState(State state)
{
82
83
84
    // avoid triggering parse before code model parsing has finished, but mark as dirty
    if (m_codeModelParsing) {
        m_dirty = true;
85
        return;
86
    }
87
88
89
90
91
92

    if ((state == Disabled || state == Idle)
            && (m_parserState == PartialParse || m_parserState == FullParse))
        return;
    m_parserState = state;

93
    if (m_parserState == Disabled) {
94
95
        m_fullUpdatePostponed = m_partialUpdatePostponed = false;
        m_postponedFiles.clear();
96
97
98
99
100
101
102
    } else if (m_parserState == Idle && ProjectExplorer::SessionManager::startupProject()) {
        if (m_fullUpdatePostponed || m_dirty) {
            emitUpdateTestTree();
        } else if (m_partialUpdatePostponed) {
            m_partialUpdatePostponed = false;
            scanForTests(m_postponedFiles.toList());
        }
103
104
105
    }
}

106
107
void TestCodeParser::emitUpdateTestTree()
{
108
109
110
111
    if (m_singleShotScheduled)
        return;

    m_singleShotScheduled = true;
112
113
114
    QTimer::singleShot(1000, this, SLOT(updateTestTree()));
}

Christian Stenger's avatar
Christian Stenger committed
115
116
void TestCodeParser::updateTestTree()
{
117
    m_singleShotScheduled = false;
118
    if (m_codeModelParsing) {
119
        m_fullUpdatePostponed = true;
120
121
122
        return;
    }

123
    if (!ProjectExplorer::SessionManager::startupProject())
Christian Stenger's avatar
Christian Stenger committed
124
125
        return;

126
    m_fullUpdatePostponed = false;
127

128
    clearCache();
129
    scanForTests();
Christian Stenger's avatar
Christian Stenger committed
130
131
132
133
}

/****** scan for QTest related stuff helpers ******/

134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
static QByteArray getFileContent(QString filePath)
{
    QByteArray fileContent;
    CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
    CppTools::WorkingCopy wc = cppMM->workingCopy();
    if (wc.contains(filePath)) {
        fileContent = wc.source(filePath);
    } else {
        QString error;
        const QTextCodec *codec = Core::EditorManager::defaultTextCodec();
        if (Utils::TextFileFormat::readFileUTF8(filePath, codec, &fileContent, &error)
                != Utils::TextFileFormat::ReadSuccess) {
            qDebug() << "Failed to read file" << filePath << ":" << error;
        }
    }
    return fileContent;
}

Christian Stenger's avatar
Christian Stenger committed
152
153
154
static bool includesQtTest(const CPlusPlus::Document::Ptr &doc,
                           const CppTools::CppModelManager *cppMM)
{
155
156
157
158
159
    static QString expectedHeaderPrefix
            = Utils::HostOsInfo::isMacHost()
            ? QLatin1String("QtTest.framework/Headers")
            : QLatin1String("QtTest");

160
    const QList<CPlusPlus::Document::Include> includes = doc->resolvedIncludes();
Christian Stenger's avatar
Christian Stenger committed
161

162
    foreach (const CPlusPlus::Document::Include &inc, includes) {
Christian Stenger's avatar
Christian Stenger committed
163
164
165
        // TODO this short cut works only for #include <QtTest>
        // bad, as there could be much more different approaches
        if (inc.unresolvedFileName() == QLatin1String("QtTest")
166
167
                && inc.resolvedFileName().endsWith(
                    QString::fromLatin1("%1/QtTest").arg(expectedHeaderPrefix))) {
Christian Stenger's avatar
Christian Stenger committed
168
169
170
171
172
173
            return true;
        }
    }

    if (cppMM) {
        CPlusPlus::Snapshot snapshot = cppMM->snapshot();
174
        const QSet<QString> allIncludes = snapshot.allIncludesForDocument(doc->fileName());
175
        foreach (const QString &include, allIncludes) {
176
177

            if (include.endsWith(QString::fromLatin1("%1/qtest.h").arg(expectedHeaderPrefix))) {
Christian Stenger's avatar
Christian Stenger committed
178
179
180
181
182
183
184
                return true;
            }
        }
    }
    return false;
}

185
186
187
static bool includesQtQuickTest(const CPlusPlus::Document::Ptr &doc,
                                const CppTools::CppModelManager *cppMM)
{
188
189
190
191
192
    static QString expectedHeaderPrefix
            = Utils::HostOsInfo::isMacHost()
            ? QLatin1String("QtQuickTest.framework/Headers")
            : QLatin1String("QtQuickTest");

193
194
195
196
    const QList<CPlusPlus::Document::Include> includes = doc->resolvedIncludes();

    foreach (const CPlusPlus::Document::Include &inc, includes) {
        if (inc.unresolvedFileName() == QLatin1String("QtQuickTest/quicktest.h")
197
198
                && inc.resolvedFileName().endsWith(
                    QString::fromLatin1("%1/quicktest.h").arg(expectedHeaderPrefix))) {
199
200
201
202
203
204
            return true;
        }
    }

    if (cppMM) {
        foreach (const QString &include, cppMM->snapshot().allIncludesForDocument(doc->fileName())) {
205
            if (include.endsWith(QString::fromLatin1("%1/quicktest.h").arg(expectedHeaderPrefix)))
206
207
208
209
210
211
                return true;
        }
    }
    return false;
}

Christian Stenger's avatar
Christian Stenger committed
212
213
214
static bool qtTestLibDefined(const CppTools::CppModelManager *cppMM,
                             const QString &fileName)
{
215
216
217
    const QList<CppTools::ProjectPart::Ptr> parts = cppMM->projectPart(fileName);
    if (parts.size() > 0)
        return parts.at(0)->projectDefines.contains("#define QT_TESTLIB_LIB 1");
Christian Stenger's avatar
Christian Stenger committed
218
219
220
    return false;
}

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
static QString quickTestSrcDir(const CppTools::CppModelManager *cppMM,
                               const QString &fileName)
{
    static const QByteArray qtsd(" QUICK_TEST_SOURCE_DIR ");
    const QList<CppTools::ProjectPart::Ptr> parts = cppMM->projectPart(fileName);
    if (parts.size() > 0) {
        QByteArray projDefines(parts.at(0)->projectDefines);
        foreach (const QByteArray &line, projDefines.split('\n')) {
            if (line.contains(qtsd)) {
                QByteArray result = line.mid(line.indexOf(qtsd) + qtsd.length());
                if (result.startsWith('"'))
                    result.remove(result.length() - 1, 1).remove(0, 1);
                if (result.startsWith("\\\""))
                    result.remove(result.length() - 2, 2).remove(0, 2);
                return QLatin1String(result);
            }
        }
    }
    return QString();
}

Christian Stenger's avatar
Christian Stenger committed
242
243
static QString testClass(const CppTools::CppModelManager *modelManager,
                         CPlusPlus::Document::Ptr &document)
Christian Stenger's avatar
Christian Stenger committed
244
{
245
    static const QByteArray qtTestMacros[] = {"QTEST_MAIN", "QTEST_APPLESS_MAIN", "QTEST_GUILESS_MAIN"};
Christian Stenger's avatar
Christian Stenger committed
246
    const QList<CPlusPlus::Document::MacroUse> macros = document->macroUses();
247
248
249
250
251
252
253

    foreach (const CPlusPlus::Document::MacroUse &macro, macros) {
        if (!macro.isFunctionLike())
            continue;
        const QByteArray name = macro.macro().name();
        if (name == qtTestMacros[0] || name == qtTestMacros[1] || name == qtTestMacros[2]) {
            const CPlusPlus::Document::Block arg = macro.arguments().at(0);
Christian Stenger's avatar
Christian Stenger committed
254
255
            return QLatin1String(getFileContent(document->fileName())
                                 .mid(arg.bytesBegin(), arg.bytesEnd() - arg.bytesBegin()));
256
257
        }
    }
Christian Stenger's avatar
Christian Stenger committed
258
259
260
261
262
263
264
265
266
    // check if one has used a self-defined macro or QTest::qExec() directly
    const CPlusPlus::Snapshot snapshot = modelManager->snapshot();
    const QByteArray fileContent = getFileContent(document->fileName());
    document = snapshot.preprocessedDocument(fileContent, document->fileName());
    document->check();
    CPlusPlus::AST *ast = document->translationUnit()->ast();
    TestAstVisitor astVisitor(document);
    astVisitor.accept(ast);
    return astVisitor.className();
267
268
269
270
271
}

static QString quickTestName(const CPlusPlus::Document::Ptr &doc)
{
    static const QByteArray qtTestMacros[] = {"QUICK_TEST_MAIN", "QUICK_TEST_OPENGL_MAIN"};
272
    const QList<CPlusPlus::Document::MacroUse> macros = doc->macroUses();
Christian Stenger's avatar
Christian Stenger committed
273

274
    foreach (const CPlusPlus::Document::MacroUse &macro, macros) {
Christian Stenger's avatar
Christian Stenger committed
275
276
        if (!macro.isFunctionLike())
            continue;
277
        const QByteArray name = macro.macro().name();
Christian Stenger's avatar
Christian Stenger committed
278
279
        if (name == qtTestMacros[0] || name == qtTestMacros[1] || name == qtTestMacros[2]) {
            CPlusPlus::Document::Block arg = macro.arguments().at(0);
Christian Stenger's avatar
Christian Stenger committed
280
281
            return QLatin1String(getFileContent(doc->fileName())
                                 .mid(arg.bytesBegin(), arg.bytesEnd() - arg.bytesBegin()));
Christian Stenger's avatar
Christian Stenger committed
282
283
        }
    }
284
285
286
287
288
289
290
291
292
293
294
    return QString();
}

static QList<QmlJS::Document::Ptr> scanDirectoryForQuickTestQmlFiles(const QString &srcDir)
{
    QStringList dirs(srcDir);
    QmlJS::ModelManagerInterface *qmlJsMM = QmlJSTools::Internal::ModelManager::instance();
    // make sure even files not listed in pro file are available inside the snapshot
    QFutureInterface<void> future;
    QmlJS::PathsAndLanguages paths;
    paths.maybeInsert(Utils::FileName::fromString(srcDir), QmlJS::Dialect::Qml);
295
296
    const bool emitDocumentChanges = false;
    const bool onlyTheLib = false;
297
    QmlJS::ModelManagerInterface::importScan(future, qmlJsMM->workingCopy(), paths, qmlJsMM,
298
        emitDocumentChanges, onlyTheLib);
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318

    const QmlJS::Snapshot snapshot = QmlJSTools::Internal::ModelManager::instance()->snapshot();
    QDirIterator it(srcDir, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
    while (it.hasNext()) {
        it.next();
        QFileInfo fi(it.fileInfo().canonicalFilePath());
        dirs << fi.filePath();
    }
    QList<QmlJS::Document::Ptr> foundDocs;

    foreach (const QString &path, dirs) {
        const QList<QmlJS::Document::Ptr> docs = snapshot.documentsInDirectory(path);
        foreach (const QmlJS::Document::Ptr &doc, docs) {
            const QString fileName(QFileInfo(doc->fileName()).fileName());
            if (fileName.startsWith(QLatin1String("tst_")) && fileName.endsWith(QLatin1String(".qml")))
                foundDocs << doc;
        }
    }

    return foundDocs;
Christian Stenger's avatar
Christian Stenger committed
319
320
}

Christian Stenger's avatar
Christian Stenger committed
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
static CPlusPlus::Document::Ptr declaringDocument(CPlusPlus::Document::Ptr doc,
                                                  const QString &testCaseName,
                                                  unsigned *line, unsigned *column)
{
    CPlusPlus::Document::Ptr declaringDoc = doc;
    const CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
    CPlusPlus::TypeOfExpression typeOfExpr;
    typeOfExpr.init(doc, cppMM->snapshot());

    auto  lookupItems = typeOfExpr(testCaseName.toUtf8(), doc->globalNamespace());
    if (lookupItems.size()) {
        CPlusPlus::Class *toeClass = lookupItems.first().declaration()->asClass();
        if (toeClass) {
            const QString declFileName = QLatin1String(toeClass->fileId()->chars(),
                                                       toeClass->fileId()->size());
            declaringDoc = cppMM->snapshot().document(declFileName);
            *line = toeClass->line();
            *column = toeClass->column() - 1;
        }
    }
    return declaringDoc;
}

344
345
346
347
348
static TestTreeItem constructTestTreeItem(const QString &fileName,
                                          const QString &mainFile,  // used for Quick Tests only
                                          const QString &testCaseName,
                                          int line, int column,
                                          const QMap<QString, TestCodeLocationAndType> functions)
Christian Stenger's avatar
Christian Stenger committed
349
{
350
351
352
353
    TestTreeItem treeItem(testCaseName, fileName, TestTreeItem::TEST_CLASS);
    treeItem.setMainFile(mainFile); // used for Quick Tests only
    treeItem.setLine(line);
    treeItem.setColumn(column);
Christian Stenger's avatar
Christian Stenger committed
354
355
356
357

    foreach (const QString &functionName, functions.keys()) {
        const TestCodeLocationAndType locationAndType = functions.value(functionName);
        TestTreeItem *treeItemChild = new TestTreeItem(functionName, locationAndType.m_fileName,
358
                                                       locationAndType.m_type, &treeItem);
Christian Stenger's avatar
Christian Stenger committed
359
360
        treeItemChild->setLine(locationAndType.m_line);
        treeItemChild->setColumn(locationAndType.m_column);
361
        treeItem.appendChild(treeItemChild);
Christian Stenger's avatar
Christian Stenger committed
362
363
364
365
    }
    return treeItem;
}

Christian Stenger's avatar
Christian Stenger committed
366
367
/****** end of helpers ******/

368
369
370
371
// used internally to indicate a parse that failed due to having triggered a parse for a file that
// is not (yet) part of the CppModelManager's snapshot
static bool parsingHasFailed;

372
373
374
375
376
377
378
379
380
381
382
383
384
385
void performParse(QFutureInterface<void> &futureInterface, QStringList list,
                  TestCodeParser *testCodeParser)
{
    int progressValue = 0;
    futureInterface.setProgressRange(0, list.size());
    futureInterface.setProgressValue(progressValue);
    CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
    CPlusPlus::Snapshot snapshot = cppMM->snapshot();

    foreach (const QString &file, list) {
        if (snapshot.contains(file)) {
            CPlusPlus::Document::Ptr doc = snapshot.find(file).value();
            futureInterface.setProgressValue(++progressValue);
            testCodeParser->checkDocumentForTestCode(doc);
386
387
388
        } else {
            parsingHasFailed |= (CppTools::ProjectFile::classify(file)
                                 != CppTools::ProjectFile::Unclassified);
389
390
391
392
393
394
        }
    }
    futureInterface.setProgressValue(list.size());
}

/****** threaded parsing stuff *******/
Christian Stenger's avatar
Christian Stenger committed
395
void TestCodeParser::checkDocumentForTestCode(CPlusPlus::Document::Ptr document)
Christian Stenger's avatar
Christian Stenger committed
396
{
Christian Stenger's avatar
Christian Stenger committed
397
398
    const QString fileName = document->fileName();
    const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
Christian Stenger's avatar
Christian Stenger committed
399

Christian Stenger's avatar
Christian Stenger committed
400
    QList<CppTools::ProjectPart::Ptr> projParts = modelManager->projectPart(fileName);
401
402
    if (projParts.size())
        if (!projParts.at(0)->selectedForBuilding) {
Christian Stenger's avatar
Christian Stenger committed
403
            removeTestsIfNecessary(fileName);
404
405
406
            return;
        }

Christian Stenger's avatar
Christian Stenger committed
407
408
    if (includesQtQuickTest(document, modelManager)) {
        handleQtQuickTest(document);
409
410
411
        return;
    }

Christian Stenger's avatar
Christian Stenger committed
412
413
414
415
416
417
418
419
420
421
422
423
424
425
    if (includesQtTest(document, modelManager) && qtTestLibDefined(modelManager, fileName)) {
        QString testCaseName(testClass(modelManager, document));
        if (!testCaseName.isEmpty()) {
            unsigned line = 0;
            unsigned column = 0;
            CPlusPlus::Document::Ptr declaringDoc = declaringDocument(document, testCaseName,
                                                                      &line, &column);
            if (declaringDoc.isNull())
                return;

            TestVisitor visitor(testCaseName);
            visitor.accept(declaringDoc->globalNamespace());
            const QMap<QString, TestCodeLocationAndType> testFunctions = visitor.privateSlots();

426
427
428
            TestTreeItem item = constructTestTreeItem(declaringDoc->fileName(), QString(),
                                                      testCaseName, line, column, testFunctions);
            updateModelAndCppDocMap(document, declaringDoc->fileName(), item);
Christian Stenger's avatar
Christian Stenger committed
429
430
431
432
433
434
435
436
437
438
439
440
441
            return;
        }
    }
    // could not find the class to test, or QTest is not included and QT_TESTLIB_LIB defined
    // maybe file is only a referenced file
    if (m_cppDocMap.contains(fileName)) {
        const TestInfo info = m_cppDocMap[fileName];
        CPlusPlus::Snapshot snapshot = modelManager->snapshot();
        if (snapshot.contains(info.referencingFile())) {
            checkDocumentForTestCode(snapshot.find(info.referencingFile()).value());
        } else { // no referencing file too, so this test case is no more a test case
            m_cppDocMap.remove(fileName);
            emit testItemsRemoved(fileName, TestTreeModel::AutoTest);
442
        }
Christian Stenger's avatar
Christian Stenger committed
443
444
445
    }
}

Christian Stenger's avatar
Christian Stenger committed
446
void TestCodeParser::handleQtQuickTest(CPlusPlus::Document::Ptr document)
447
{
Christian Stenger's avatar
Christian Stenger committed
448
    const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
449

Christian Stenger's avatar
Christian Stenger committed
450
    if (quickTestName(document).isEmpty())
451
452
        return;

453
454
    const QString cppFileName = document->fileName();
    const QString srcDir = quickTestSrcDir(modelManager, cppFileName);
455
456
457
458
    if (srcDir.isEmpty())
        return;

    const QList<QmlJS::Document::Ptr> qmlDocs = scanDirectoryForQuickTestQmlFiles(srcDir);
Christian Stenger's avatar
Christian Stenger committed
459
460
    foreach (const QmlJS::Document::Ptr &qmlJSDoc, qmlDocs) {
        QmlJS::AST::Node *ast = qmlJSDoc->ast();
461
        QTC_ASSERT(ast, continue);
Christian Stenger's avatar
Christian Stenger committed
462
        TestQmlVisitor qmlVisitor(qmlJSDoc);
463
464
        QmlJS::AST::Node::accept(ast, &qmlVisitor);

Christian Stenger's avatar
Christian Stenger committed
465
        const QString testCaseName = qmlVisitor.testCaseName();
466
467
        const TestCodeLocationAndType tcLocationAndType = qmlVisitor.testCaseLocation();
        const QMap<QString, TestCodeLocationAndType> testFunctions = qmlVisitor.testFunctions();
468

Christian Stenger's avatar
Christian Stenger committed
469
        if (testCaseName.isEmpty()) {
470
            updateUnnamedQuickTests(qmlJSDoc->fileName(), cppFileName, testFunctions);
471
472
473
474
            continue;
        } // end of handling test cases without name property

        // construct new/modified TestTreeItem
475
476
        TestTreeItem testTreeItem
                = constructTestTreeItem(tcLocationAndType.m_fileName, cppFileName, testCaseName,
Christian Stenger's avatar
Christian Stenger committed
477
                                        tcLocationAndType.m_line, tcLocationAndType.m_column,
478
                                        testFunctions);
479
480

        // update model and internal map
481
        updateModelAndQuickDocMap(qmlJSDoc, cppFileName, testTreeItem);
482
483
484
    }
}

Christian Stenger's avatar
Christian Stenger committed
485
void TestCodeParser::onCppDocumentUpdated(const CPlusPlus::Document::Ptr &document)
Christian Stenger's avatar
Christian Stenger committed
486
{
487
    if (m_codeModelParsing) {
488
489
490
        if (!m_fullUpdatePostponed) {
            m_partialUpdatePostponed = true;
            m_postponedFiles.insert(document->fileName());
491
492
493
494
        }
        return;
    }

495
    ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
496
    if (!project)
Christian Stenger's avatar
Christian Stenger committed
497
        return;
Christian Stenger's avatar
Christian Stenger committed
498
    const QString fileName = document->fileName();
499
    if (m_cppDocMap.contains(fileName)) {
Christian Stenger's avatar
Christian Stenger committed
500
501
        if (m_cppDocMap[fileName].revision() == document->revision()
                && m_cppDocMap[fileName].editorRevision() == document->editorRevision()) {
Christian Stenger's avatar
Christian Stenger committed
502
503
            return;
        }
504
    } else if (!project->files(ProjectExplorer::Project::AllFiles).contains(fileName)) {
505
        return;
Christian Stenger's avatar
Christian Stenger committed
506
    }
507
    scanForTests(QStringList(fileName));
Christian Stenger's avatar
Christian Stenger committed
508
509
}

Christian Stenger's avatar
Christian Stenger committed
510
void TestCodeParser::onQmlDocumentUpdated(const QmlJS::Document::Ptr &document)
511
{
512
    if (m_codeModelParsing) {
513
514
515
        if (!m_fullUpdatePostponed) {
            m_partialUpdatePostponed = true;
            m_postponedFiles.insert(document->fileName());
516
517
518
519
        }
        return;
    }

520
    ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
521
    if (!project)
522
        return;
Christian Stenger's avatar
Christian Stenger committed
523
    const QString fileName = document->fileName();
524
    if (m_quickDocMap.contains(fileName)) {
Christian Stenger's avatar
Christian Stenger committed
525
        if ((int)m_quickDocMap[fileName].editorRevision() == document->editorRevision()) {
526
527
            return;
        }
528
    } else if (!project->files(ProjectExplorer::Project::AllFiles).contains(fileName)) {
529
530
531
532
533
534
        // what if the file is not listed inside the pro file, but will be used anyway?
        return;
    }
    const CPlusPlus::Snapshot snapshot = CppTools::CppModelManager::instance()->snapshot();
    if (m_quickDocMap.contains(fileName)
            && snapshot.contains(m_quickDocMap[fileName].referencingFile())) {
535
536
            if (!m_quickDocMap[fileName].referencingFile().isEmpty())
                scanForTests(QStringList(m_quickDocMap[fileName].referencingFile()));
537
    }
538
    if (m_unnamedQuickDocList.size() == 0)
539
540
541
        return;

    // special case of having unnamed TestCases
542
    const QString &mainFile = m_model->getMainFileForUnnamedQuickTest(fileName);
543
544
545
    if (!mainFile.isEmpty() && snapshot.contains(mainFile)) {
        scanForTests(QStringList(mainFile));
    }
546
547
}

548
549
550
551
552
553
554
555
556
557
void TestCodeParser::onStartupProjectChanged(ProjectExplorer::Project *)
{
    if (m_parserState == FullParse || m_parserState == PartialParse) {
        Core::ProgressManager::instance()->cancelTasks(Constants::TASK_PARSE);
    } else {
        clearCache();
        emitUpdateTestTree();
    }
}

558
559
void TestCodeParser::onProjectPartsUpdated(ProjectExplorer::Project *project)
{
560
    if (project != ProjectExplorer::SessionManager::startupProject())
561
        return;
562
    if (m_codeModelParsing || m_parserState == Disabled)
Christian Stenger's avatar
Christian Stenger committed
563
564
565
        m_fullUpdatePostponed = true;
    else
        emitUpdateTestTree();
566
567
}

Christian Stenger's avatar
Christian Stenger committed
568
569
void TestCodeParser::removeFiles(const QStringList &files)
{
570
571
    foreach (const QString &file, files)
        removeTestsIfNecessary(file);
Christian Stenger's avatar
Christian Stenger committed
572
573
}

574
575
576
577
578
579
bool TestCodeParser::postponed(const QStringList &fileList)
{
    switch (m_parserState) {
    case Idle:
        return false;
    case PartialParse:
Christian Stenger's avatar
Christian Stenger committed
580
581
    case FullParse:
        // parse is running, postponing a full parse
582
        if (fileList.isEmpty()) {
583
584
585
            m_partialUpdatePostponed = false;
            m_postponedFiles.clear();
            m_fullUpdatePostponed = true;
586
587
        } else {
            // partial parse triggered, but full parse is postponed already, ignoring this
588
            if (m_fullUpdatePostponed)
589
590
591
                return true;
            // partial parse triggered, postpone or add current files to already postponed partial
            foreach (const QString &file, fileList)
592
593
                m_postponedFiles.insert(file);
            m_partialUpdatePostponed = true;
594
595
        }
        return true;
596
    case Disabled:
597
        break;
598
599
600
601
    }
    QTC_ASSERT(false, return false); // should not happen at all
}

602
void TestCodeParser::scanForTests(const QStringList &fileList)
Christian Stenger's avatar
Christian Stenger committed
603
{
604
605
606
    if (m_parserState == Disabled) {
        m_dirty = true;
        if (fileList.isEmpty()) {
607
608
609
            m_fullUpdatePostponed = true;
            m_partialUpdatePostponed = false;
            m_postponedFiles.clear();
610
        } else {
611
612
            if (!m_fullUpdatePostponed) {
                m_partialUpdatePostponed = true;
613
                foreach (const QString &file, fileList)
614
                    m_postponedFiles.insert(file);
615
616
617
618
619
            }
        }
        return;
    }

620
621
622
    if (postponed(fileList))
        return;

623
    m_postponedFiles.clear();
624
625
    bool isFullParse = fileList.isEmpty();
    bool isSmallChange = !isFullParse && fileList.size() < 6;
626
    QStringList list;
627
    if (isFullParse) {
628
        list = ProjectExplorer::SessionManager::startupProject()->files(ProjectExplorer::Project::AllFiles);
629
630
631
        if (list.isEmpty())
            return;
        m_parserState = FullParse;
632
633
    } else {
        list << fileList;
634
        m_parserState = PartialParse;
635
636
    }

637
    parsingHasFailed = false;
638
639
640
641
642
643
644
    if (isSmallChange) { // no need to do this async or should we do this always async?
        CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
        CPlusPlus::Snapshot snapshot = cppMM->snapshot();
        foreach (const QString &file, list) {
            if (snapshot.contains(file)) {
                CPlusPlus::Document::Ptr doc = snapshot.find(file).value();
                checkDocumentForTestCode(doc);
645
646
647
            } else {
                parsingHasFailed |= (CppTools::ProjectFile::classify(file)
                                     != CppTools::ProjectFile::Unclassified);
648
            }
Christian Stenger's avatar
Christian Stenger committed
649
        }
650
        onFinished();
651
        return;
Christian Stenger's avatar
Christian Stenger committed
652
    }
653
654
655
656

    QFuture<void> future = QtConcurrent::run(&performParse, list, this);
    Core::FutureProgress *progress
            = Core::ProgressManager::addTask(future, isFullParse ? tr("Scanning for Tests")
657
                                                                 : tr("Refreshing Tests List"),
658
659
660
                                             Autotest::Constants::TASK_PARSE);
    connect(progress, &Core::FutureProgress::finished,
            this, &TestCodeParser::onFinished);
661
662

    emit parsingStarted();
Christian Stenger's avatar
Christian Stenger committed
663
664
}

665
void TestCodeParser::clearCache()
666
667
{
    m_cppDocMap.clear();
668
    m_quickDocMap.clear();
669
    m_unnamedQuickDocList.clear();
670
    emit cacheCleared();
671
672
}

673
674
675
676
677
void TestCodeParser::removeTestsIfNecessary(const QString &fileName)
{
    // check if this file was listed before and remove if necessary (switched config,...)
    if (m_cppDocMap.contains(fileName)) {
        m_cppDocMap.remove(fileName);
678
        emit testItemsRemoved(fileName, TestTreeModel::AutoTest);
679
680
681
682
683
684
685
686
687
688
689
690
691
    } else { // handle Qt Quick Tests
        QList<QString> toBeRemoved;
        foreach (const QString &file, m_quickDocMap.keys()) {
            if (file == fileName) {
                toBeRemoved.append(file);
                continue;
            }
            const TestInfo info = m_quickDocMap.value(file);
            if (info.referencingFile() == fileName)
                toBeRemoved.append(file);
        }
        foreach (const QString &file, toBeRemoved) {
            m_quickDocMap.remove(file);
692
            emit testItemsRemoved(file, TestTreeModel::QuickTest);
693
694
        }
        // unnamed Quick Tests must be handled separately
695
696
697
698
        if (fileName.endsWith(QLatin1String(".qml"))) {
            removeUnnamedQuickTestsByName(fileName);
            emit unnamedQuickTestsRemoved(fileName);
        } else {
699
            QSet<QString> filePaths;
700
701
702
703
704
            m_model->qmlFilesForMainFile(fileName, &filePaths);
            foreach (const QString &file, filePaths) {
                removeUnnamedQuickTestsByName(file);
                emit unnamedQuickTestsRemoved(file);
            }
705
706
707
708
        }
    }
}

709
710
void TestCodeParser::onTaskStarted(Core::Id type)
{
711
712
    if (type == CppTools::Constants::TASK_INDEX)
        m_codeModelParsing = true;
713
714
715
716
}

void TestCodeParser::onAllTasksFinished(Core::Id type)
{
717
718
    // only CPP parsing is relevant as we trigger Qml parsing internally anyway
    if (type != CppTools::Constants::TASK_INDEX)
719
        return;
720
    m_codeModelParsing = false;
721

722
723
    // avoid illegal parser state if respective widgets became hidden while parsing
    setState(Idle);
724
725
726
727
728
729
730
731
732
733
734
}

void TestCodeParser::onFinished()
{
    switch (m_parserState) {
    case PartialParse:
        m_parserState = Idle;
        emit partialParsingFinished();
        break;
    case FullParse:
        m_parserState = Idle;
735
736
737
738
739
        m_dirty = parsingHasFailed;
        if (m_partialUpdatePostponed || m_fullUpdatePostponed || parsingHasFailed)
            emit partialParsingFinished();
        else
            emit parsingFinished();
740
        m_dirty = false;
741
        break;
742
743
744
    case Disabled: // can happen if all Test related widgets become hidden while parsing
        emit parsingFinished();
        break;
745
    default:
746
        qWarning("I should not be here... State: %d", m_parserState);
747
748
        break;
    }
749
750
}

751
752
void TestCodeParser::onPartialParsingFinished()
{
753
754
755
756
757
    QTC_ASSERT(m_fullUpdatePostponed != m_partialUpdatePostponed
            || ((m_fullUpdatePostponed || m_partialUpdatePostponed) == false),
               m_partialUpdatePostponed = false;m_postponedFiles.clear(););
    if (m_fullUpdatePostponed) {
        m_fullUpdatePostponed = false;
758
        updateTestTree();
759
760
    } else if (m_partialUpdatePostponed) {
        m_partialUpdatePostponed = false;
761
        scanForTests(m_postponedFiles.toList());
762
    } else {
763
764
765
766
767
        m_dirty |= m_codeModelParsing;
        if (m_dirty)
            emit parsingFailed();
        else if (!m_singleShotScheduled)
            emit parsingFinished();
768
769
770
    }
}

771
772
773
774
775
776
777
void TestCodeParser::updateUnnamedQuickTests(const QString &fileName, const QString &mainFile,
                                             const QMap<QString, TestCodeLocationAndType> &functions)
{
    // if this test case was named before remove it
    m_quickDocMap.remove(fileName);
    emit testItemsRemoved(fileName, TestTreeModel::QuickTest);

778
779
780
781
    removeUnnamedQuickTestsByName(fileName);
    foreach (const QString &functionName, functions.keys()) {
        UnnamedQuickTestInfo info(functionName, fileName);
        m_unnamedQuickDocList.append(info);
782
    }
783
784

    emit unnamedQuickTestsUpdated(fileName, mainFile, functions);
785
786
}

Christian Stenger's avatar
Christian Stenger committed
787
void TestCodeParser::updateModelAndCppDocMap(CPlusPlus::Document::Ptr document,
788
                                             const QString &declaringFile, TestTreeItem &testItem)
Christian Stenger's avatar
Christian Stenger committed
789
790
791
{
    const CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
    const QString fileName = document->fileName();
792
    const QString testCaseName = testItem.name();
Christian Stenger's avatar
Christian Stenger committed
793
794
795
796
797
798
799
    QString proFile;
    const QList<CppTools::ProjectPart::Ptr> ppList = cppMM->projectPart(fileName);
    if (ppList.size())
        proFile = ppList.at(0)->projectFile;

    if (m_cppDocMap.contains(fileName)) {
        QStringList files = QStringList() << fileName;
800
801
        if (fileName != declaringFile)
            files << declaringFile;
Christian Stenger's avatar
Christian Stenger committed
802
        foreach (const QString &file, files) {
803
804
805
806
807
808
809
810
            const bool setReferencingFile = (files.size() == 2 && file == declaringFile);
            emit testItemModified(testItem, TestTreeModel::AutoTest, file);
            TestInfo testInfo(testCaseName, testItem.getChildNames(),
                              document->revision(), document->editorRevision());
            testInfo.setProfile(proFile);
            if (setReferencingFile)
                testInfo.setReferencingFile(fileName);
            m_cppDocMap.insert(file, testInfo);
Christian Stenger's avatar
Christian Stenger committed
811
812
        }
    } else {
813
814
        emit testItemCreated(testItem, TestTreeModel::AutoTest);
        TestInfo ti(testCaseName, testItem.getChildNames(),
Christian Stenger's avatar
Christian Stenger committed
815
816
817
                    document->revision(), document->editorRevision());
        ti.setProfile(proFile);
        m_cppDocMap.insert(fileName, ti);
818
        if (declaringFile != fileName) {
Christian Stenger's avatar
Christian Stenger committed
819
            ti.setReferencingFile(fileName);
820
            m_cppDocMap.insert(declaringFile, ti);
Christian Stenger's avatar
Christian Stenger committed
821
822
823
824
        }
    }
}

825
826
827
void TestCodeParser::updateModelAndQuickDocMap(QmlJS::Document::Ptr document,
                                               const QString &referencingFile,
                                               TestTreeItem &testItem)
Christian Stenger's avatar
Christian Stenger committed
828
829
{
    const CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
830
    const QString fileName = document->fileName();
Christian Stenger's avatar
Christian Stenger committed
831
    QString proFile;
832
    QList<CppTools::ProjectPart::Ptr> ppList = cppMM->projectPart(referencingFile);
Christian Stenger's avatar
Christian Stenger committed
833
834
835
836
    if (ppList.size())
        proFile = ppList.at(0)->projectFile;

    if (m_quickDocMap.contains(fileName)) {
837
838
839
840
841
        emit testItemModified(testItem, TestTreeModel::QuickTest, fileName);
        TestInfo testInfo(testItem.name(), testItem.getChildNames(), 0, document->editorRevision());
        testInfo.setReferencingFile(referencingFile);
        testInfo.setProfile(proFile);
        m_quickDocMap.insert(fileName, testInfo);
Christian Stenger's avatar
Christian Stenger committed
842
843
    } else {
        // if it was formerly unnamed remove the respective items
844
        removeUnnamedQuickTestsByName(fileName);
845
        emit unnamedQuickTestsRemoved(fileName);
Christian Stenger's avatar
Christian Stenger committed
846

847
848
849
        emit testItemCreated(testItem, TestTreeModel::QuickTest);
        TestInfo testInfo(testItem.name(), testItem.getChildNames(), 0, document->editorRevision());
        testInfo.setReferencingFile(referencingFile);
Christian Stenger's avatar
Christian Stenger committed
850
        testInfo.setProfile(proFile);
851
        m_quickDocMap.insert(testItem.filePath(), testInfo);
Christian Stenger's avatar
Christian Stenger committed
852
853
854
    }
}

855
856
857
858
859
860
861
862
void TestCodeParser::removeUnnamedQuickTestsByName(const QString &fileName)
{
    for (int i = m_unnamedQuickDocList.size() - 1; i >= 0; --i) {
        if (m_unnamedQuickDocList.at(i).fileName() == fileName)
            m_unnamedQuickDocList.removeAt(i);
    }
}

863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
#ifdef WITH_TESTS
int TestCodeParser::autoTestsCount() const
{
    int count = 0;
    foreach (const QString &file, m_cppDocMap.keys()) {
        if (m_cppDocMap.value(file).referencingFile().isEmpty())
            ++count;
    }
    return count;
}

int TestCodeParser::namedQuickTestsCount() const
{
    return m_quickDocMap.size();
}

int TestCodeParser::unnamedQuickTestsCount() const
{
881
    return m_unnamedQuickDocList.size();
882
883
884
}
#endif

Christian Stenger's avatar
Christian Stenger committed
885
886
} // namespace Internal
} // namespace Autotest