Files
qt-creator/src/plugins/autotest/testcodeparser.cpp
Christian Stenger bad4bac21c AutoTest: Do not use special members in base class
Change-Id: I9e110039ea886f6b0c3e6ddc7361bc9fac387abe
Reviewed-by: David Schulz <david.schulz@theqtcompany.com>
2016-05-27 06:01:57 +00:00

915 lines
35 KiB
C++

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "autotestconstants.h"
#include "autotest_utils.h"
#include "testcodeparser.h"
#include "testvisitor.h"
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/progressmanager/futureprogress.h>
#include <coreplugin/progressmanager/progressmanager.h>
#include <cplusplus/LookupContext.h>
#include <cplusplus/TypeOfExpression.h>
#include <cpptools/cpptoolsconstants.h>
#include <cpptools/cppmodelmanager.h>
#include <cpptools/cppworkingcopy.h>
#include <projectexplorer/project.h>
#include <projectexplorer/session.h>
#include <qmljs/parser/qmljsast_p.h>
#include <qmljs/qmljsdialect.h>
#include <qmljstools/qmljsmodelmanager.h>
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <utils/runextensions.h>
#include <utils/textfileformat.h>
#include <QDirIterator>
#include <QFuture>
#include <QFutureInterface>
#include <QLoggingCategory>
#include <QTimer>
static Q_LOGGING_CATEGORY(LOG, "qtc.autotest.testcodeparser")
namespace Autotest {
namespace Internal {
TestCodeParser::TestCodeParser(TestTreeModel *parent)
: QObject(parent),
m_model(parent),
m_codeModelParsing(false),
m_fullUpdatePostponed(false),
m_partialUpdatePostponed(false),
m_dirty(false),
m_singleShotScheduled(false),
m_parserState(Disabled)
{
// connect to ProgressManager to postpone test parsing when CppModelManager is parsing
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);
connect(&m_futureWatcher, &QFutureWatcher<TestParseResultPtr>::started,
this, &TestCodeParser::parsingStarted);
connect(&m_futureWatcher, &QFutureWatcher<TestParseResultPtr>::finished,
this, &TestCodeParser::onFinished);
connect(&m_futureWatcher, &QFutureWatcher<TestParseResultPtr>::resultReadyAt,
this, [this] (int index) {
emit testParseResultReady(m_futureWatcher.resultAt(index));
});
}
TestCodeParser::~TestCodeParser()
{
}
void TestCodeParser::setState(State state)
{
qCDebug(LOG) << "setState(" << state << "), currentState:" << m_parserState;
// avoid triggering parse before code model parsing has finished, but mark as dirty
if (m_codeModelParsing) {
m_dirty = true;
qCDebug(LOG) << "Not setting new state - code model parsing is running, just marking dirty";
return;
}
if ((state == Disabled || state == Idle)
&& (m_parserState == PartialParse || m_parserState == FullParse)) {
qCDebug(LOG) << "Not setting state, parse is running";
return;
}
m_parserState = state;
if (m_parserState == Disabled) {
m_fullUpdatePostponed = m_partialUpdatePostponed = false;
m_postponedFiles.clear();
} else if (m_parserState == Idle && ProjectExplorer::SessionManager::startupProject()) {
if (m_fullUpdatePostponed || m_dirty) {
emitUpdateTestTree();
} else if (m_partialUpdatePostponed) {
m_partialUpdatePostponed = false;
qCDebug(LOG) << "calling scanForTests with postponed files (setState)";
scanForTests(m_postponedFiles.toList());
}
}
}
void TestCodeParser::emitUpdateTestTree()
{
if (m_singleShotScheduled) {
qCDebug(LOG) << "not scheduling another updateTestTree";
return;
}
qCDebug(LOG) << "adding singleShot";
m_singleShotScheduled = true;
QTimer::singleShot(1000, this, SLOT(updateTestTree()));
}
void TestCodeParser::updateTestTree()
{
m_singleShotScheduled = false;
if (m_codeModelParsing) {
m_fullUpdatePostponed = true;
m_partialUpdatePostponed = false;
m_postponedFiles.clear();
return;
}
if (!ProjectExplorer::SessionManager::startupProject())
return;
m_fullUpdatePostponed = false;
emit aboutToPerformFullParse();
qCDebug(LOG) << "calling scanForTests (updateTestTree)";
scanForTests();
}
/****** scan for QTest related stuff helpers ******/
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;
}
static bool includesQtTest(const CPlusPlus::Document::Ptr &doc,
const CppTools::CppModelManager *cppMM)
{
static QStringList expectedHeaderPrefixes
= Utils::HostOsInfo::isMacHost()
? QStringList({ QLatin1String("QtTest.framework/Headers"), QLatin1String("QtTest") })
: QStringList({ QLatin1String("QtTest") });
const QList<CPlusPlus::Document::Include> includes = doc->resolvedIncludes();
foreach (const CPlusPlus::Document::Include &inc, includes) {
// TODO this short cut works only for #include <QtTest>
// bad, as there could be much more different approaches
if (inc.unresolvedFileName() == QLatin1String("QtTest")) {
foreach (const QString &prefix, expectedHeaderPrefixes) {
if (inc.resolvedFileName().endsWith(QString::fromLatin1("%1/QtTest").arg(prefix)))
return true;
}
}
}
if (cppMM) {
CPlusPlus::Snapshot snapshot = cppMM->snapshot();
const QSet<QString> allIncludes = snapshot.allIncludesForDocument(doc->fileName());
foreach (const QString &include, allIncludes) {
foreach (const QString &prefix, expectedHeaderPrefixes) {
if (include.endsWith(QString::fromLatin1("%1/qtest.h").arg(prefix)))
return true;
}
}
}
return false;
}
static bool includesQtQuickTest(const CPlusPlus::Document::Ptr &doc,
const CppTools::CppModelManager *cppMM)
{
static QStringList expectedHeaderPrefixes
= Utils::HostOsInfo::isMacHost()
? QStringList({ QLatin1String("QtQuickTest.framework/Headers"),
QLatin1String("QtQuickTest") })
: QStringList({ QLatin1String("QtQuickTest") });
const QList<CPlusPlus::Document::Include> includes = doc->resolvedIncludes();
foreach (const CPlusPlus::Document::Include &inc, includes) {
if (inc.unresolvedFileName() == QLatin1String("QtQuickTest/quicktest.h")) {
foreach (const QString &prefix, expectedHeaderPrefixes) {
if (inc.resolvedFileName().endsWith(
QString::fromLatin1("%1/quicktest.h").arg(prefix))) {
return true;
}
}
}
}
if (cppMM) {
foreach (const QString &include, cppMM->snapshot().allIncludesForDocument(doc->fileName())) {
foreach (const QString &prefix, expectedHeaderPrefixes) {
if (include.endsWith(QString::fromLatin1("%1/quicktest.h").arg(prefix)))
return true;
}
}
}
return false;
}
static bool includesGTest(const CPlusPlus::Document::Ptr &doc,
const CppTools::CppModelManager *cppMM)
{
const QString gtestH = QLatin1String("gtest/gtest.h");
foreach (const CPlusPlus::Document::Include &inc, doc->resolvedIncludes()) {
if (inc.resolvedFileName().endsWith(gtestH))
return true;
}
if (cppMM) {
const CPlusPlus::Snapshot snapshot = cppMM->snapshot();
foreach (const QString &include, snapshot.allIncludesForDocument(doc->fileName())) {
if (include.endsWith(gtestH))
return true;
}
}
return false;
}
static bool qtTestLibDefined(const CppTools::CppModelManager *cppMM,
const QString &fileName)
{
const QList<CppTools::ProjectPart::Ptr> parts = cppMM->projectPart(fileName);
if (parts.size() > 0)
return parts.at(0)->projectDefines.contains("#define QT_TESTLIB_LIB");
return false;
}
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();
}
static QString testClass(const CppTools::CppModelManager *modelManager, const QString &fileName)
{
const QByteArray &fileContent = getFileContent(fileName);
CPlusPlus::Document::Ptr document = modelManager->document(fileName);
if (document.isNull())
return QString();
const QList<CPlusPlus::Document::MacroUse> macros = document->macroUses();
foreach (const CPlusPlus::Document::MacroUse &macro, macros) {
if (!macro.isFunctionLike())
continue;
const QByteArray name = macro.macro().name();
if (TestUtils::isQTestMacro(name)) {
const CPlusPlus::Document::Block arg = macro.arguments().at(0);
return QLatin1String(fileContent.mid(arg.bytesBegin(),
arg.bytesEnd() - arg.bytesBegin()));
}
}
// check if one has used a self-defined macro or QTest::qExec() directly
const CPlusPlus::Snapshot snapshot = modelManager->snapshot();
document = snapshot.preprocessedDocument(fileContent, fileName);
document->check();
CPlusPlus::AST *ast = document->translationUnit()->ast();
TestAstVisitor astVisitor(document);
astVisitor.accept(ast);
return astVisitor.className();
}
static QString quickTestName(const CPlusPlus::Document::Ptr &doc)
{
const QList<CPlusPlus::Document::MacroUse> macros = doc->macroUses();
foreach (const CPlusPlus::Document::MacroUse &macro, macros) {
if (!macro.isFunctionLike())
continue;
const QByteArray name = macro.macro().name();
if (TestUtils::isQuickTestMacro(name)) {
CPlusPlus::Document::Block arg = macro.arguments().at(0);
return QLatin1String(getFileContent(doc->fileName())
.mid(arg.bytesBegin(), arg.bytesEnd() - arg.bytesBegin()));
}
}
return QString();
}
static bool hasGTestNames(CPlusPlus::Document::Ptr &document)
{
foreach (const CPlusPlus::Document::MacroUse &macro, document->macroUses()) {
if (!macro.isFunctionLike())
continue;
if (TestUtils::isGTestMacro(QLatin1String(macro.macro().name()))) {
const QVector<CPlusPlus::Document::Block> args = macro.arguments();
if (args.size() != 2)
continue;
return true;
}
}
return false;
}
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);
const bool emitDocumentChanges = false;
const bool onlyTheLib = false;
QmlJS::ModelManagerInterface::importScan(future, qmlJsMM->workingCopy(), paths, qmlJsMM,
emitDocumentChanges, onlyTheLib);
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;
}
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());
QList<CPlusPlus::LookupItem> lookupItems = typeOfExpr(testCaseName.toUtf8(),
doc->globalNamespace());
if (lookupItems.size()) {
if (CPlusPlus::Symbol *symbol = lookupItems.first().declaration()) {
if (CPlusPlus::Class *toeClass = symbol->asClass()) {
const QString declFileName = QLatin1String(toeClass->fileId()->chars(),
toeClass->fileId()->size());
declaringDoc = cppMM->snapshot().document(declFileName);
*line = toeClass->line();
*column = toeClass->column() - 1;
}
}
}
return declaringDoc;
}
static QSet<QString> filesWithDataFunctionDefinitions(
const QMap<QString, TestCodeLocationAndType> &testFunctions)
{
QSet<QString> result;
QMap<QString, TestCodeLocationAndType>::ConstIterator it = testFunctions.begin();
const QMap<QString, TestCodeLocationAndType>::ConstIterator end = testFunctions.end();
for ( ; it != end; ++it) {
const QString &key = it.key();
if (key.endsWith(QLatin1String("_data")) && testFunctions.contains(key.left(key.size() - 5)))
result.insert(it.value().m_name);
}
return result;
}
static QMap<QString, TestCodeLocationList> checkForDataTags(const QString &fileName)
{
const CPlusPlus::Snapshot snapshot = CPlusPlus::CppModelManagerBase::instance()->snapshot();
const QByteArray fileContent = getFileContent(fileName);
CPlusPlus::Document::Ptr document = snapshot.preprocessedDocument(fileContent, fileName);
document->check();
CPlusPlus::AST *ast = document->translationUnit()->ast();
TestDataFunctionVisitor visitor(document);
visitor.accept(ast);
return visitor.dataTags();
}
/****** end of helpers ******/
static bool checkQmlDocumentForTestCode(QFutureInterface<TestParseResultPtr> futureInterface,
const QmlJS::Document::Ptr &qmlJSDoc,
const QString &proFile = QString())
{
if (qmlJSDoc.isNull())
return false;
QmlJS::AST::Node *ast = qmlJSDoc->ast();
QTC_ASSERT(ast, return false);
TestQmlVisitor qmlVisitor(qmlJSDoc);
QmlJS::AST::Node::accept(ast, &qmlVisitor);
const QString testCaseName = qmlVisitor.testCaseName();
const TestCodeLocationAndType tcLocationAndType = qmlVisitor.testCaseLocation();
const QMap<QString, TestCodeLocationAndType> &testFunctions = qmlVisitor.testFunctions();
QuickTestParseResult *parseResult = new QuickTestParseResult;
parseResult->proFile = proFile;
parseResult->itemType = TestTreeItem::TestCase;
QMap<QString, TestCodeLocationAndType>::ConstIterator it = testFunctions.begin();
const QMap<QString, TestCodeLocationAndType>::ConstIterator end = testFunctions.end();
for ( ; it != end; ++it) {
const TestCodeLocationAndType &loc = it.value();
QuickTestParseResult *funcResult = new QuickTestParseResult;
funcResult->name = it.key();
funcResult->displayName = it.key();
funcResult->itemType = loc.m_type;
funcResult->fileName = loc.m_name;
funcResult->line = loc.m_line;
funcResult->column = loc.m_column;
funcResult->proFile = proFile;
parseResult->children.append(funcResult);
}
if (!testCaseName.isEmpty()) {
parseResult->fileName = tcLocationAndType.m_name;
parseResult->name = testCaseName;
parseResult->line = tcLocationAndType.m_line;
parseResult->column = tcLocationAndType.m_column;
}
futureInterface.reportResult(TestParseResultPtr(parseResult));
return true;
}
static bool handleQtTest(QFutureInterface<TestParseResultPtr> futureInterface,
CPlusPlus::Document::Ptr document, const QString &oldTestCaseName)
{
const QString &fileName = document->fileName();
const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
QString testCaseName(testClass(modelManager, fileName));
// we might be in a reparse without the original entry point with the QTest::qExec()
if (testCaseName.isEmpty())
testCaseName = oldTestCaseName;
if (!testCaseName.isEmpty()) {
unsigned line = 0;
unsigned column = 0;
CPlusPlus::Document::Ptr declaringDoc = declaringDocument(document, testCaseName,
&line, &column);
if (declaringDoc.isNull())
return false;
TestVisitor visitor(testCaseName);
visitor.accept(declaringDoc->globalNamespace());
if (!visitor.resultValid())
return false;
const QMap<QString, TestCodeLocationAndType> &testFunctions = visitor.privateSlots();
const QSet<QString> &files = filesWithDataFunctionDefinitions(testFunctions);
// TODO: change to QHash<>
QMap<QString, TestCodeLocationList> dataTags;
foreach (const QString &file, files)
dataTags.unite(checkForDataTags(file));
QtTestParseResult *parseResult = new QtTestParseResult;
parseResult->itemType = TestTreeItem::TestCase;
parseResult->fileName = declaringDoc->fileName();
parseResult->name = testCaseName;
parseResult->displayName = testCaseName;
parseResult->line = line;
parseResult->column = column;
parseResult->proFile = modelManager->projectPart(fileName).first()->projectFile;
QMap<QString, TestCodeLocationAndType>::ConstIterator it = testFunctions.begin();
const QMap<QString, TestCodeLocationAndType>::ConstIterator end = testFunctions.end();
for ( ; it != end; ++it) {
const TestCodeLocationAndType &location = it.value();
QtTestParseResult *func = new QtTestParseResult;
func->itemType = location.m_type;
func->name = testCaseName + QLatin1String("::") + it.key();
func->displayName = it.key();
func->fileName = location.m_name;
func->line = location.m_line;
func->column = location.m_column;
foreach (const TestCodeLocationAndType &tag, dataTags.value(func->name)) {
QtTestParseResult *dataTag = new QtTestParseResult;
dataTag->itemType = tag.m_type;
dataTag->name = tag.m_name;
dataTag->displayName = tag.m_name;
dataTag->fileName = testFunctions.value(it.key() + QLatin1String("_data")).m_name;
dataTag->line = tag.m_line;
dataTag->column = tag.m_column;
func->children.append(dataTag);
}
parseResult->children.append(func);
}
futureInterface.reportResult(TestParseResultPtr(parseResult));
return true;
}
return false;
}
static bool handleQtQuickTest(QFutureInterface<TestParseResultPtr> futureInterface,
CPlusPlus::Document::Ptr document)
{
const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
if (quickTestName(document).isEmpty())
return false;
const QString cppFileName = document->fileName();
QList<CppTools::ProjectPart::Ptr> ppList = modelManager->projectPart(cppFileName);
QTC_ASSERT(!ppList.isEmpty(), return false);
const QString &proFile = ppList.at(0)->projectFile;
const QString srcDir = quickTestSrcDir(modelManager, cppFileName);
if (srcDir.isEmpty())
return false;
const QList<QmlJS::Document::Ptr> qmlDocs = scanDirectoryForQuickTestQmlFiles(srcDir);
bool result = false;
foreach (const QmlJS::Document::Ptr &qmlJSDoc, qmlDocs)
result |= checkQmlDocumentForTestCode(futureInterface, qmlJSDoc, proFile);
return result;
}
static bool handleGTest(QFutureInterface<TestParseResultPtr> futureInterface, const QString &filePath)
{
const QByteArray &fileContent = getFileContent(filePath);
const CPlusPlus::Snapshot snapshot = CPlusPlus::CppModelManagerBase::instance()->snapshot();
CPlusPlus::Document::Ptr document = snapshot.preprocessedDocument(fileContent, filePath);
document->check();
CPlusPlus::AST *ast = document->translationUnit()->ast();
GTestVisitor visitor(document);
visitor.accept(ast);
QMap<GTestCaseSpec, GTestCodeLocationList> result = visitor.gtestFunctions();
QString proFile;
const CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
QList<CppTools::ProjectPart::Ptr> ppList = cppMM->projectPart(filePath);
if (ppList.size())
proFile = ppList.first()->projectFile;
foreach (const GTestCaseSpec &testSpec, result.keys()) {
GoogleTestParseResult *parseResult = new GoogleTestParseResult;
parseResult->itemType = TestTreeItem::TestCase;
parseResult->fileName = filePath;
parseResult->name = testSpec.testCaseName;
parseResult->parameterized = testSpec.parameterized;
parseResult->typed = testSpec.typed;
parseResult->disabled = testSpec.disabled;
parseResult->proFile = proFile;
foreach (const GTestCodeLocationAndType &location, result.value(testSpec)) {
GoogleTestParseResult *testSet = new GoogleTestParseResult;
testSet->name = location.m_name;
testSet->fileName = filePath;
testSet->line = location.m_line;
testSet->column = location.m_column;
testSet->disabled = location.m_state & GoogleTestTreeItem::Disabled;
testSet->itemType = location.m_type;
testSet->proFile = proFile;
parseResult->children.append(testSet);
}
futureInterface.reportResult(TestParseResultPtr(parseResult));
}
return !result.keys().isEmpty();
}
static void checkDocumentForTestCode(QFutureInterface<TestParseResultPtr> futureInterface,
CPlusPlus::Document::Ptr document,
QHash<QString, QString> testCaseNames)
{
const QString fileName = document->fileName();
const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
QList<CppTools::ProjectPart::Ptr> projParts = modelManager->projectPart(fileName);
if (projParts.size()) {
if (!projParts.at(0)->selectedForBuilding)
return;
}
const QString &oldTestCaseName = testCaseNames.value(fileName);
if (includesQtQuickTest(document, modelManager)) {
if (handleQtQuickTest(futureInterface, document))
return;
}
if (!oldTestCaseName.isEmpty() // if we do a reparse
|| (includesQtTest(document, modelManager)
&& qtTestLibDefined(modelManager, fileName))) {
if (handleQtTest(futureInterface, document, oldTestCaseName))
return;
}
if (includesGTest(document, modelManager) && hasGTestNames(document)) {
if (handleGTest(futureInterface, document->fileName()))
return;
}
}
// 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;
static void performParse(QFutureInterface<TestParseResultPtr> &futureInterface,
const QStringList &list, const QHash<QString, QString> testCaseNames)
{
int progressValue = 0;
futureInterface.setProgressRange(0, list.size());
futureInterface.setProgressValue(progressValue);
CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
CPlusPlus::Snapshot snapshot = cppMM->snapshot();
QmlJS::Snapshot qmlSnapshot = QmlJSTools::Internal::ModelManager::instance()->snapshot();
foreach (const QString &file, list) {
if (futureInterface.isCanceled())
return;
if (file.endsWith(QLatin1String(".qml"))) {
checkQmlDocumentForTestCode(futureInterface, qmlSnapshot.document(file));
} else if (snapshot.contains(file)) {
CPlusPlus::Document::Ptr doc = snapshot.find(file).value();
futureInterface.setProgressValue(++progressValue);
checkDocumentForTestCode(futureInterface, doc, testCaseNames);
} else {
parsingHasFailed |= (CppTools::ProjectFile::classify(file)
!= CppTools::ProjectFile::Unclassified);
}
}
futureInterface.setProgressValue(list.size());
}
/****** threaded parsing stuff *******/
void TestCodeParser::onCppDocumentUpdated(const CPlusPlus::Document::Ptr &document)
{
if (m_codeModelParsing) {
if (!m_fullUpdatePostponed) {
m_partialUpdatePostponed = true;
m_postponedFiles.insert(document->fileName());
}
return;
}
ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
if (!project)
return;
const QString fileName = document->fileName();
if (!project->files(ProjectExplorer::Project::AllFiles).contains(fileName))
return;
qCDebug(LOG) << "calling scanForTests (onCppDocumentUpdated)";
scanForTests(QStringList(fileName));
}
void TestCodeParser::onQmlDocumentUpdated(const QmlJS::Document::Ptr &document)
{
const QString &fileName = document->fileName();
if (m_codeModelParsing) {
if (!m_fullUpdatePostponed) {
m_partialUpdatePostponed = true;
m_postponedFiles.insert(fileName);
}
return;
}
ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
if (!project)
return;
if (!project->files(ProjectExplorer::Project::AllFiles).contains(fileName)) {
// what if the file is not listed inside the pro file, but will be used anyway?
return;
}
scanForTests(QStringList(fileName));
}
void TestCodeParser::onStartupProjectChanged(ProjectExplorer::Project *project)
{
if (m_parserState == FullParse || m_parserState == PartialParse)
Core::ProgressManager::instance()->cancelTasks(Constants::TASK_PARSE);
else if (project)
emitUpdateTestTree();
}
void TestCodeParser::onProjectPartsUpdated(ProjectExplorer::Project *project)
{
if (project != ProjectExplorer::SessionManager::startupProject())
return;
if (m_codeModelParsing || m_parserState == Disabled)
m_fullUpdatePostponed = true;
else
emitUpdateTestTree();
}
bool TestCodeParser::postponed(const QStringList &fileList)
{
switch (m_parserState) {
case Idle:
return false;
case PartialParse:
case FullParse:
// parse is running, postponing a full parse
if (fileList.isEmpty()) {
m_partialUpdatePostponed = false;
m_postponedFiles.clear();
m_fullUpdatePostponed = true;
Core::ProgressManager::instance()->cancelTasks(Constants::TASK_PARSE);
} else {
// partial parse triggered, but full parse is postponed already, ignoring this
if (m_fullUpdatePostponed)
return true;
// partial parse triggered, postpone or add current files to already postponed partial
foreach (const QString &file, fileList)
m_postponedFiles.insert(file);
m_partialUpdatePostponed = true;
}
return true;
case Disabled:
break;
}
QTC_ASSERT(false, return false); // should not happen at all
}
void TestCodeParser::scanForTests(const QStringList &fileList)
{
if (m_parserState == Disabled) {
m_dirty = true;
if (fileList.isEmpty()) {
m_fullUpdatePostponed = true;
m_partialUpdatePostponed = false;
m_postponedFiles.clear();
} else if (!m_fullUpdatePostponed) {
m_partialUpdatePostponed = true;
foreach (const QString &file, fileList)
m_postponedFiles.insert(file);
}
return;
}
if (postponed(fileList))
return;
m_postponedFiles.clear();
bool isFullParse = fileList.isEmpty();
QStringList list;
if (isFullParse) {
list = ProjectExplorer::SessionManager::startupProject()->files(ProjectExplorer::Project::AllFiles);
if (list.isEmpty())
return;
qCDebug(LOG) << "setting state to FullParse (scanForTests)";
m_parserState = FullParse;
} else {
list << fileList;
qCDebug(LOG) << "setting state to PartialParse (scanForTests)";
m_parserState = PartialParse;
}
parsingHasFailed = false;
QHash<QString, QString> testCaseNames;
if (isFullParse) {
// remove qml files as they will be found automatically by the referencing cpp file
list = Utils::filtered(list, [] (const QString &fn) {
return !fn.endsWith(QLatin1String(".qml"));
});
m_model->markAllForRemoval();
} else {
testCaseNames = m_model->testCaseNamesForFiles(list);
foreach (const QString &filePath, list)
m_model->markForRemoval(filePath);
}
QFuture<TestParseResultPtr> future = Utils::runAsync(&performParse, list, testCaseNames);
m_futureWatcher.setFuture(future);
if (list.size() > 5) {
Core::ProgressManager::addTask(future, tr("Scanning for Tests"),
Autotest::Constants::TASK_PARSE);
}
}
void TestCodeParser::onTaskStarted(Core::Id type)
{
if (type == CppTools::Constants::TASK_INDEX)
m_codeModelParsing = true;
}
void TestCodeParser::onAllTasksFinished(Core::Id type)
{
// only CPP parsing is relevant as we trigger Qml parsing internally anyway
if (type != CppTools::Constants::TASK_INDEX)
return;
m_codeModelParsing = false;
// avoid illegal parser state if respective widgets became hidden while parsing
setState(Idle);
}
void TestCodeParser::onFinished()
{
switch (m_parserState) {
case PartialParse:
qCDebug(LOG) << "setting state to Idle (onFinished, PartialParse)";
m_parserState = Idle;
onPartialParsingFinished();
break;
case FullParse:
qCDebug(LOG) << "setting state to Idle (onFinished, FullParse)";
m_parserState = Idle;
m_dirty = parsingHasFailed;
if (m_partialUpdatePostponed || m_fullUpdatePostponed || parsingHasFailed) {
onPartialParsingFinished();
} else {
qCDebug(LOG) << "emitting parsingFinished"
<< "(onFinished, FullParse, nothing postponed, parsing succeeded)";
emit parsingFinished();
}
m_dirty = false;
break;
case Disabled: // can happen if all Test related widgets become hidden while parsing
qCDebug(LOG) << "emitting parsingFinished (onFinished, Disabled)";
emit parsingFinished();
break;
default:
qWarning("I should not be here... State: %d", m_parserState);
break;
}
}
void TestCodeParser::onPartialParsingFinished()
{
QTC_ASSERT(m_fullUpdatePostponed != m_partialUpdatePostponed
|| ((m_fullUpdatePostponed || m_partialUpdatePostponed) == false),
m_partialUpdatePostponed = false;m_postponedFiles.clear(););
if (m_fullUpdatePostponed) {
m_fullUpdatePostponed = false;
qCDebug(LOG) << "calling updateTestTree (onPartialParsingFinished)";
updateTestTree();
} else if (m_partialUpdatePostponed) {
m_partialUpdatePostponed = false;
qCDebug(LOG) << "calling scanForTests with postponed files (onPartialParsingFinished)";
scanForTests(m_postponedFiles.toList());
} else {
m_dirty |= m_codeModelParsing;
if (m_dirty) {
emit parsingFailed();
} else if (!m_singleShotScheduled) {
qCDebug(LOG) << "emitting parsingFinished"
<< "(onPartialParsingFinished, nothing postponed, not dirty)";
emit parsingFinished();
} else {
qCDebug(LOG) << "not emitting parsingFinished"
<< "(on PartialParsingFinished, singleshot scheduled)";
}
}
}
} // namespace Internal
} // namespace Autotest