Files
qt-creator/src/plugins/autotest/testcodeparser.cpp

498 lines
18 KiB
C++
Raw Normal View History

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
2014-10-07 12:30:54 +02:00
#include "testcodeparser.h"
#include "autotestconstants.h"
#include "autotesttr.h"
#include "testtreemodel.h"
2014-10-07 12:30:54 +02:00
#include <coreplugin/progressmanager/progressmanager.h>
#include <coreplugin/progressmanager/taskprogress.h>
#include <cppeditor/cppeditorconstants.h>
#include <cppeditor/cppmodelmanager.h>
#include <projectexplorer/buildsystem.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
2014-11-06 16:01:06 +01:00
#include <utils/algorithm.h>
#include <utils/asynctask.h>
#include <utils/qtcassert.h>
#include <QLoggingCategory>
using namespace Core;
using namespace Utils;
2014-10-07 12:30:54 +02:00
namespace Autotest {
namespace Internal {
Q_LOGGING_CATEGORY(LOG, "qtc.autotest.testcodeparser", QtWarningMsg)
using namespace ProjectExplorer;
static bool isProjectParsing()
{
const BuildSystem *bs = ProjectManager::startupBuildSystem();
return bs && bs->isParsing();
}
TestCodeParser::TestCodeParser()
: m_threadPool(new QThreadPool(this))
2014-10-07 12:30:54 +02:00
{
// connect to ProgressManager to postpone test parsing when CppModelManager is parsing
ProgressManager *progressManager = ProgressManager::instance();
connect(progressManager, &ProgressManager::taskStarted,
this, &TestCodeParser::onTaskStarted);
connect(progressManager, &ProgressManager::allTasksFinished,
this, &TestCodeParser::onAllTasksFinished);
connect(this, &TestCodeParser::parsingFinished, this, &TestCodeParser::releaseParserInternals);
m_reparseTimer.setSingleShot(true);
connect(&m_reparseTimer, &QTimer::timeout, this, &TestCodeParser::parsePostponedFiles);
m_threadPool->setMaxThreadCount(std::max(QThread::idealThreadCount()/4, 1));
m_threadPool->setThreadPriority(QThread::LowestPriority);
m_futureSynchronizer.setCancelOnWait(true);
2014-10-07 12:30:54 +02:00
}
TestCodeParser::~TestCodeParser() = default;
void TestCodeParser::setState(State state)
{
if (m_parserState == Shutdown)
return;
qCDebug(LOG) << "setState(" << state << "), currentState:" << m_parserState;
// avoid triggering parse before code model parsing has finished, but mark as dirty
if (isProjectParsing() || m_codeModelParsing) {
m_dirty = true;
qCDebug(LOG) << "Not setting new state - code model parsing is running, just marking dirty";
return;
}
if ((state == Idle) && (m_parserState == PartialParse || m_parserState == FullParse)) {
qCDebug(LOG) << "Not setting state, parse is running";
return;
}
m_parserState = state;
if (m_parserState == Idle && ProjectManager::startupProject()) {
if (m_postponedUpdateType == UpdateType::FullUpdate || m_dirty) {
emitUpdateTestTree();
} else if (m_postponedUpdateType == UpdateType::PartialUpdate) {
m_postponedUpdateType = UpdateType::NoUpdate;
qCDebug(LOG) << "calling scanForTests with postponed files (setState)";
if (!m_reparseTimer.isActive())
scanForTests(Utils::toList(m_postponedFiles));
}
}
}
void TestCodeParser::syncTestFrameworks(const QList<ITestParser *> &parsers)
{
if (m_parserState != Idle) {
// there's a running parse
m_postponedUpdateType = UpdateType::NoUpdate;
m_postponedFiles.clear();
ProgressManager::cancelTasks(Constants::TASK_PARSE);
}
qCDebug(LOG) << "Setting" << parsers << "as current parsers";
m_testCodeParsers = parsers;
}
void TestCodeParser::emitUpdateTestTree(ITestParser *parser)
{
if (m_testCodeParsers.isEmpty())
return;
if (parser)
m_updateParsers.insert(parser);
else
m_updateParsers.clear();
if (m_singleShotScheduled) {
qCDebug(LOG) << "not scheduling another updateTestTree";
return;
}
qCDebug(LOG) << "adding singleShot";
m_singleShotScheduled = true;
QTimer::singleShot(1000, this, [this] { updateTestTree(m_updateParsers); });
}
void TestCodeParser::updateTestTree(const QSet<ITestParser *> &parsers)
2014-10-07 12:30:54 +02:00
{
m_singleShotScheduled = false;
if (isProjectParsing() || m_codeModelParsing) {
m_postponedUpdateType = UpdateType::FullUpdate;
m_postponedFiles.clear();
if (parsers.isEmpty()) {
m_updateParsers.clear();
} else {
for (ITestParser *parser : parsers)
m_updateParsers.insert(parser);
}
return;
}
if (!ProjectManager::startupProject())
2014-10-07 12:30:54 +02:00
return;
m_postponedUpdateType = UpdateType::NoUpdate;
qCDebug(LOG) << "calling scanForTests (updateTestTree)";
const QList<ITestParser *> sortedParsers = Utils::sorted(Utils::toList(parsers),
[](const ITestParser *lhs, const ITestParser *rhs) {
return lhs->framework()->priority() < rhs->framework()->priority();
});
scanForTests({}, sortedParsers);
2014-10-07 12:30:54 +02:00
}
/****** threaded parsing stuff *******/
void TestCodeParser::onDocumentUpdated(const FilePath &fileName, bool isQmlFile)
2014-10-07 12:30:54 +02:00
{
if (isProjectParsing() || m_codeModelParsing || m_postponedUpdateType == UpdateType::FullUpdate)
return;
Project *project = ProjectManager::startupProject();
if (!project)
2014-10-07 12:30:54 +02:00
return;
// Quick tests: qml files aren't necessarily listed inside project files
if (!isQmlFile && !project->isKnownFile(fileName))
return;
scanForTests({fileName});
2014-11-06 16:01:06 +01:00
}
void TestCodeParser::onCppDocumentUpdated(const CPlusPlus::Document::Ptr &document)
{
onDocumentUpdated(document->filePath());
}
void TestCodeParser::onQmlDocumentUpdated(const QmlJS::Document::Ptr &document)
{
static const QStringList ignoredSuffixes{ "qbs", "ui.qml" };
const FilePath fileName = document->fileName();
if (!ignoredSuffixes.contains(fileName.suffix()))
onDocumentUpdated(fileName, true);
}
void TestCodeParser::onStartupProjectChanged(Project *project)
{
if (m_parserState == FullParse || m_parserState == PartialParse) {
qCDebug(LOG) << "Canceling scanForTest (startup project changed)";
ProgressManager::cancelTasks(Constants::TASK_PARSE);
}
emit aboutToPerformFullParse();
if (project)
emitUpdateTestTree();
}
void TestCodeParser::onProjectPartsUpdated(Project *project)
{
if (project != ProjectManager::startupProject())
return;
if (isProjectParsing() || m_codeModelParsing)
m_postponedUpdateType = UpdateType::FullUpdate;
else
emitUpdateTestTree();
}
void TestCodeParser::aboutToShutdown()
{
qCDebug(LOG) << "Disabling (immediately) - shutting down";
m_parserState = Shutdown;
m_taskTree.reset();
m_futureSynchronizer.waitForFinished();
}
bool TestCodeParser::postponed(const FilePaths &fileList)
{
switch (m_parserState) {
case Idle:
if (fileList.size() == 1) {
if (m_reparseTimerTimedOut)
return false;
switch (m_postponedFiles.size()) {
case 0:
m_postponedFiles.insert(fileList.first());
m_reparseTimer.setInterval(1000);
m_reparseTimer.start();
return true;
case 1:
if (m_postponedFiles.contains(fileList.first())) {
m_reparseTimer.start();
return true;
}
Q_FALLTHROUGH();
default:
m_postponedFiles.insert(fileList.first());
m_reparseTimer.stop();
m_reparseTimer.setInterval(0);
m_reparseTimerTimedOut = false;
m_reparseTimer.start();
return true;
}
}
return false;
case PartialParse:
case FullParse:
// parse is running, postponing a full parse
if (fileList.isEmpty()) {
m_postponedFiles.clear();
m_postponedUpdateType = UpdateType::FullUpdate;
qCDebug(LOG) << "Canceling scanForTest (full parse triggered while running a scan)";
ProgressManager::cancelTasks(Constants::TASK_PARSE);
} else {
// partial parse triggered, but full parse is postponed already, ignoring this
if (m_postponedUpdateType == UpdateType::FullUpdate)
return true;
// partial parse triggered, postpone or add current files to already postponed partial
for (const FilePath &file : fileList)
m_postponedFiles.insert(file);
m_postponedUpdateType = UpdateType::PartialUpdate;
}
return true;
case Shutdown:
break;
}
QTC_ASSERT(false, return false); // should not happen at all
}
static void parseFileForTests(QPromise<TestParseResultPtr> &promise,
const QList<ITestParser *> &parsers, const FilePath &fileName)
{
for (ITestParser *parser : parsers) {
if (promise.isCanceled())
return;
if (parser->processDocument(promise, fileName))
break;
}
}
void TestCodeParser::scanForTests(const FilePaths &fileList, const QList<ITestParser *> &parsers)
2014-10-07 12:30:54 +02:00
{
if (m_parserState == Shutdown || m_testCodeParsers.isEmpty())
return;
if (postponed(fileList))
return;
m_reparseTimer.stop();
m_reparseTimerTimedOut = false;
m_postponedFiles.clear();
bool isFullParse = fileList.isEmpty();
Project *project = ProjectManager::startupProject();
if (!project)
return;
FilePaths list;
if (isFullParse) {
list = project->files(Project::SourceFiles);
if (list.isEmpty()) {
// at least project file should be there, but might happen if parsing current project
// takes too long, especially when opening sessions holding multiple projects
qCDebug(LOG) << "File list empty (FullParse) - trying again in a sec";
emitUpdateTestTree();
return;
} else if (list.size() == 1 && list.first() == project->projectFilePath()) {
qCDebug(LOG) << "File list contains only the project file.";
return;
}
qCDebug(LOG) << "setting state to FullParse (scanForTests)";
m_parserState = FullParse;
} else {
list << fileList;
qCDebug(LOG) << "setting state to PartialParse (scanForTests)";
m_parserState = PartialParse;
}
m_parsingHasFailed = false;
TestTreeModel::instance()->updateCheckStateCache();
if (isFullParse) {
// remove qml files as they will be found automatically by the referencing cpp file
list = Utils::filtered(list, [](const FilePath &fn) {
return !fn.endsWith(".qml");
});
if (!parsers.isEmpty()) {
for (ITestParser *parser : parsers) {
parser->framework()->rootNode()->markForRemovalRecursively(true);
}
} else {
emit requestRemoveAllFrameworkItems();
}
} else if (!parsers.isEmpty()) {
for (ITestParser *parser: parsers) {
for (const FilePath &filePath : std::as_const(list))
parser->framework()->rootNode()->markForRemovalRecursively(filePath);
}
} else {
for (const FilePath &filePath : std::as_const(list))
emit requestRemoval(filePath);
2014-10-07 12:30:54 +02:00
}
QTC_ASSERT(!(isFullParse && list.isEmpty()), onFinished(true); return);
// use only a single parser or all current active?
const QList<ITestParser *> codeParsers = parsers.isEmpty() ? m_testCodeParsers : parsers;
qCDebug(LOG) << QDateTime::currentDateTime().toString("hh:mm:ss.zzz") << "StartParsing";
QSet<QString> extensions;
const auto cppSnapshot = CppEditor::CppModelManager::instance()->snapshot();
for (ITestParser *parser : codeParsers) {
parser->init(list, isFullParse);
for (const QString &ext : parser->supportedExtensions())
extensions.insert(ext);
}
// We are only interested in files that have been either parsed by the c++ parser,
// or have an extension that one of the parsers is specifically interested in.
const FilePaths filteredList
= Utils::filtered(list, [&extensions, &cppSnapshot](const FilePath &fn) {
const bool isSupportedExtension = Utils::anyOf(extensions, [&fn](const QString &ext) {
return fn.suffix() == ext;
});
if (isSupportedExtension)
return true;
return cppSnapshot.contains(fn);
});
qCDebug(LOG) << "Starting scan of" << filteredList.size() << "(" << list.size() << ")"
<< "files with" << codeParsers.size() << "parsers";
using namespace Tasking;
QList<TaskItem> tasks{parallel}; // TODO: use ParallelLimit(N) and add to settings?
for (const FilePath &file : filteredList) {
const auto setup = [this, codeParsers, file](AsyncTask<TestParseResultPtr> &async) {
async.setConcurrentCallData(parseFileForTests, codeParsers, file);
async.setThreadPool(m_threadPool);
async.setFutureSynchronizer(&m_futureSynchronizer);
};
const auto onDone = [this](const AsyncTask<TestParseResultPtr> &async) {
const QList<TestParseResultPtr> results = async.results();
for (const TestParseResultPtr &result : results)
emit testParseResultReady(result);
};
tasks.append(Async<TestParseResultPtr>(setup, onDone));
}
m_taskTree.reset(new TaskTree{tasks});
const auto onDone = [this] { m_taskTree.release()->deleteLater(); onFinished(true); };
const auto onError = [this] { m_taskTree.release()->deleteLater(); onFinished(false); };
connect(m_taskTree.get(), &TaskTree::started, this, &TestCodeParser::parsingStarted);
connect(m_taskTree.get(), &TaskTree::done, this, onDone);
connect(m_taskTree.get(), &TaskTree::errorOccurred, this, onError);
if (filteredList.size() > 5) {
auto progress = new TaskProgress(m_taskTree.get());
progress->setDisplayName(Tr::tr("Scanning for Tests"));
progress->setId(Constants::TASK_PARSE);
}
m_taskTree->start();
2014-10-07 12:30:54 +02:00
}
void TestCodeParser::onTaskStarted(Id type)
{
if (type == CppEditor::Constants::TASK_INDEX) {
m_codeModelParsing = true;
if (m_parserState == FullParse || m_parserState == PartialParse) {
m_postponedUpdateType = m_parserState == FullParse
? UpdateType::FullUpdate : UpdateType::PartialUpdate;
qCDebug(LOG) << "Canceling scan for test (CppModelParsing started)";
m_parsingHasFailed = true;
ProgressManager::cancelTasks(Constants::TASK_PARSE);
}
}
}
void TestCodeParser::onAllTasksFinished(Id type)
{
// if we cancel parsing ensure that progress animation is canceled as well
if (type == Constants::TASK_PARSE && m_parsingHasFailed)
emit parsingFailed();
// only CPP parsing is relevant as we trigger Qml parsing internally anyway
if (type != CppEditor::Constants::TASK_INDEX)
return;
m_codeModelParsing = false;
// avoid illegal parser state if respective widgets became hidden while parsing
setState(Idle);
}
void TestCodeParser::onFinished(bool success)
{
m_parsingHasFailed = !success;
switch (m_parserState) {
case PartialParse:
qCDebug(LOG) << "setting state to Idle (onFinished, PartialParse)";
m_parserState = Idle;
onPartialParsingFinished();
qCDebug(LOG) << QDateTime::currentDateTime().toString("hh:mm:ss.zzz") << "PartParsingFin";
break;
case FullParse:
qCDebug(LOG) << "setting state to Idle (onFinished, FullParse)";
m_parserState = Idle;
m_dirty = m_parsingHasFailed;
if (m_postponedUpdateType != UpdateType::NoUpdate || m_parsingHasFailed) {
onPartialParsingFinished();
} else {
qCDebug(LOG) << "emitting parsingFinished"
<< "(onFinished, FullParse, nothing postponed, parsing succeeded)";
m_updateParsers.clear();
emit parsingFinished();
qCDebug(LOG) << QDateTime::currentDateTime().toString("hh:mm:ss.zzz") << "ParsingFin";
}
m_dirty = false;
break;
case Shutdown:
qCDebug(LOG) << "Shutdown complete - not emitting parsingFinished (onFinished)";
break;
default:
qWarning("I should not be here... State: %d", m_parserState);
break;
}
}
void TestCodeParser::onPartialParsingFinished()
{
const UpdateType oldType = m_postponedUpdateType;
m_postponedUpdateType = UpdateType::NoUpdate;
switch (oldType) {
case UpdateType::FullUpdate:
qCDebug(LOG) << "calling updateTestTree (onPartialParsingFinished)";
updateTestTree(m_updateParsers);
break;
case UpdateType::PartialUpdate:
qCDebug(LOG) << "calling scanForTests with postponed files (onPartialParsingFinished)";
if (!m_reparseTimer.isActive())
scanForTests(Utils::toList(m_postponedFiles));
break;
case UpdateType::NoUpdate:
m_dirty |= m_codeModelParsing;
if (m_dirty) {
emit parsingFailed();
qCDebug(LOG) << QDateTime::currentDateTime().toString("hh:mm:ss.zzz") << "ParsingFail";
} else if (!m_singleShotScheduled) {
qCDebug(LOG) << "emitting parsingFinished"
<< "(onPartialParsingFinished, nothing postponed, not dirty)";
m_updateParsers.clear();
emit parsingFinished();
qCDebug(LOG) << QDateTime::currentDateTime().toString("hh:mm:ss.zzz") << "ParsingFin";
} else {
qCDebug(LOG) << "not emitting parsingFinished"
<< "(on PartialParsingFinished, singleshot scheduled)";
}
break;
}
}
void TestCodeParser::parsePostponedFiles()
{
m_reparseTimerTimedOut = true;
scanForTests(Utils::toList(m_postponedFiles));
}
void TestCodeParser::releaseParserInternals()
{
for (ITestParser *parser : std::as_const(m_testCodeParsers))
parser->release();
}
2014-10-07 12:30:54 +02:00
} // namespace Internal
} // namespace Autotest