Add basic support for Qt Quick Test

This commit is contained in:
Christian Stenger
2014-11-06 16:01:06 +01:00
committed by Christian Stenger
parent 85efa2c3c9
commit 0357b0e98b
13 changed files with 681 additions and 90 deletions

View File

@@ -30,6 +30,10 @@
#include <projectexplorer/session.h>
#include <qmljs/parser/qmljsast_p.h>
#include <qmljs/qmljsdialect.h>
#include <qmljstools/qmljsmodelmanager.h>
#include <utils/textfileformat.h>
namespace Autotest {
@@ -53,6 +57,7 @@ void TestCodeParser::updateTestTree()
clearMaps();
m_model->removeAllAutoTests();
m_model->removeAllQuickTests();
const ProjectExplorer::SessionManager *session = ProjectExplorer::SessionManager::instance();
if (!session || !session->hasProjects())
return;
@@ -88,7 +93,7 @@ static bool includesQtTest(const CPlusPlus::Document::Ptr &doc,
{
const QList<CPlusPlus::Document::Include> includes = doc->resolvedIncludes();
foreach (const CPlusPlus::Document::Include inc, includes) {
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")
@@ -100,7 +105,7 @@ static bool includesQtTest(const CPlusPlus::Document::Ptr &doc,
if (cppMM) {
CPlusPlus::Snapshot snapshot = cppMM->snapshot();
const QSet<QString> allIncludes = snapshot.allIncludesForDocument(doc->fileName());
foreach (const QString include, allIncludes) {
foreach (const QString &include, allIncludes) {
if (include.endsWith(QLatin1String("QtTest/qtest.h"))) {
return true;
}
@@ -109,6 +114,27 @@ static bool includesQtTest(const CPlusPlus::Document::Ptr &doc,
return false;
}
static bool includesQtQuickTest(const CPlusPlus::Document::Ptr &doc,
const CppTools::CppModelManager *cppMM)
{
const QList<CPlusPlus::Document::Include> includes = doc->resolvedIncludes();
foreach (const CPlusPlus::Document::Include &inc, includes) {
if (inc.unresolvedFileName() == QLatin1String("QtQuickTest/quicktest.h")
&& inc.resolvedFileName().endsWith(QLatin1String("QtQuickTest/quicktest.h"))) {
return true;
}
}
if (cppMM) {
foreach (const QString &include, cppMM->snapshot().allIncludesForDocument(doc->fileName())) {
if (include.endsWith(QLatin1String("QtQuickTest/quicktest.h")))
return true;
}
}
return false;
}
static bool qtTestLibDefined(const CppTools::CppModelManager *cppMM,
const QString &fileName)
{
@@ -118,24 +144,91 @@ static bool qtTestLibDefined(const CppTools::CppModelManager *cppMM,
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 CPlusPlus::Document::Ptr &doc)
{
static QByteArray qtTestMacros[] = {"QTEST_MAIN", "QTEST_APPLESS_MAIN", "QTEST_GUILESS_MAIN"};
QString tC;
static const QByteArray qtTestMacros[] = {"QTEST_MAIN", "QTEST_APPLESS_MAIN", "QTEST_GUILESS_MAIN"};
const QList<CPlusPlus::Document::MacroUse> macros = doc->macroUses();
foreach (const CPlusPlus::Document::MacroUse macro, macros) {
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);
return QLatin1String(getFileContent(doc->fileName()).mid(arg.bytesBegin(), arg.bytesEnd() - arg.bytesBegin()));
}
}
return QString();
}
static QString quickTestName(const CPlusPlus::Document::Ptr &doc)
{
static const QByteArray qtTestMacros[] = {"QUICK_TEST_MAIN", "QUICK_TEST_OPENGL_MAIN"};
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 (name == qtTestMacros[0] || name == qtTestMacros[1] || name == qtTestMacros[2]) {
CPlusPlus::Document::Block arg = macro.arguments().at(0);
tC = QLatin1String(getFileContent(doc->fileName()).mid(arg.bytesBegin(),
arg.bytesEnd() - arg.bytesBegin()));
break;
return QLatin1String(getFileContent(doc->fileName()).mid(arg.bytesBegin(), arg.bytesEnd() - arg.bytesBegin()));
}
}
return tC;
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);
QmlJS::ModelManagerInterface::importScan(future, qmlJsMM->workingCopy(),
paths, qmlJsMM, false, false);
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;
}
/****** end of helpers ******/
@@ -145,6 +238,11 @@ void TestCodeParser::checkDocumentForTestCode(CPlusPlus::Document::Ptr doc)
const QString file = doc->fileName();
const CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
if (includesQtQuickTest(doc, cppMM)) {
handleQtQuickTest(doc);
return;
}
if (includesQtTest(doc, cppMM) && qtTestLibDefined(cppMM, file)) {
QString tc(testClass(doc));
if (tc.isEmpty()) {
@@ -187,7 +285,7 @@ void TestCodeParser::checkDocumentForTestCode(CPlusPlus::Document::Ptr doc)
TestVisitor myVisitor(tc);
myVisitor.accept(declaringDoc->globalNamespace());
const QMap<QString, TestCodeLocation> privSlots = myVisitor.privateSlots();
foreach (const QString privS, privSlots.keys()) {
foreach (const QString &privS, privSlots.keys()) {
const TestCodeLocation location = privSlots.value(privS);
TestTreeItem *ttSub = new TestTreeItem(privS, location.m_fileName,
TestTreeItem::TEST_FUNCTION, ttItem);
@@ -253,7 +351,143 @@ void TestCodeParser::checkDocumentForTestCode(CPlusPlus::Document::Ptr doc)
}
}
void TestCodeParser::onDocumentUpdated(CPlusPlus::Document::Ptr doc)
void TestCodeParser::handleQtQuickTest(CPlusPlus::Document::Ptr doc)
{
const CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
if (quickTestName(doc).isEmpty())
return;
const QString srcDir = quickTestSrcDir(cppMM, doc->fileName());
if (srcDir.isEmpty())
return;
const QList<QmlJS::Document::Ptr> qmlDocs = scanDirectoryForQuickTestQmlFiles(srcDir);
foreach (const QmlJS::Document::Ptr &d, qmlDocs) {
QmlJS::AST::Node *ast = d->ast();
if (!ast) {
qDebug() << "ast is zero pointer" << d->fileName(); // should not happen
continue;
}
TestQmlVisitor qmlVisitor(d);
QmlJS::AST::Node::accept(ast, &qmlVisitor);
const QString tcName = qmlVisitor.testCaseName();
const TestCodeLocation tcLocation = qmlVisitor.testCaseLocation();
const QMap<QString, TestCodeLocation> testFunctions = qmlVisitor.testFunctions();
const QModelIndex quickTestRootIndex = m_model->index(1, 0);
TestTreeItem *quickTestRootItem = static_cast<TestTreeItem *>(quickTestRootIndex.internalPointer());
if (tcName.isEmpty()) {
// if this test case was named before remove it
if (m_quickDocMap.contains(d->fileName())) {
m_model->removeQuickTestSubtreeByFilePath(d->fileName());
m_quickDocMap.remove(d->fileName());
}
bool hadUnnamedTestsBefore;
TestTreeItem *ttItem = m_model->unnamedQuickTests();
if (!ttItem) {
hadUnnamedTestsBefore = false;
ttItem = new TestTreeItem(QString(), QString(), TestTreeItem::TEST_CLASS,
quickTestRootItem);
foreach (const QString &func, testFunctions.keys()) {
const TestCodeLocation location = testFunctions.value(func);
TestTreeItem *ttSub = new TestTreeItem(func, location.m_fileName,
TestTreeItem::TEST_FUNCTION, ttItem);
ttSub->setLine(location.m_line);
ttSub->setColumn(location.m_column);
ttSub->setMainFile(doc->fileName());
ttItem->appendChild(ttSub);
}
} else {
hadUnnamedTestsBefore = true;
// remove unnamed quick tests that are already found for this qml file
m_model->removeUnnamedQuickTest(d->fileName());
foreach (const QString &func, testFunctions.keys()) {
const TestCodeLocation location = testFunctions.value(func);
TestTreeItem *ttSub = new TestTreeItem(func, location.m_fileName,
TestTreeItem::TEST_FUNCTION, ttItem);
ttSub->setLine(location.m_line);
ttSub->setColumn(location.m_column);
ttSub->setMainFile(doc->fileName());
ttItem->appendChild(ttSub);
}
}
TestInfo info = m_quickDocMap.contains(QLatin1String("<unnamed>"))
? m_quickDocMap[QLatin1String("<unnamed>")]
: TestInfo(QString(), QStringList(), 666);
QStringList originalFunctions(info.testFunctions());
foreach (const QString &func, testFunctions.keys()) {
if (!originalFunctions.contains(func))
originalFunctions.append(func);
}
info.setTestFunctions(originalFunctions);
if (hadUnnamedTestsBefore)
m_model->modifyQuickTestSubtree(ttItem->row(), ttItem);
else
m_model->addQuickTest(ttItem);
m_quickDocMap.insert(QLatin1String("<unnamed>"), info);
continue;
} // end of handling test cases without name property
// construct new/modified TestTreeItem
TestTreeItem *ttItem = new TestTreeItem(tcName, tcLocation.m_fileName,
TestTreeItem::TEST_CLASS, quickTestRootItem);
ttItem->setLine(tcLocation.m_line);
ttItem->setColumn(tcLocation.m_column);
ttItem->setMainFile(doc->fileName());
foreach (const QString &func, testFunctions.keys()) {
const TestCodeLocation location = testFunctions.value(func);
TestTreeItem *ttSub = new TestTreeItem(func, location.m_fileName,
TestTreeItem::TEST_FUNCTION, ttItem);
ttSub->setLine(location.m_line);
ttSub->setColumn(location.m_column);
ttItem->appendChild(ttSub);
}
// update model and internal map
const QString fileName(tcLocation.m_fileName);
const QmlJS::Document::Ptr qmlDoc =
QmlJSTools::Internal::ModelManager::instance()->snapshot().document(fileName);
if (m_quickDocMap.contains(fileName)) {
for (int i = 0; i < quickTestRootItem->childCount(); ++i) {
if (quickTestRootItem->child(i)->filePath() == fileName) {
m_model->modifyQuickTestSubtree(i, ttItem);
TestInfo ti(tcName, testFunctions.keys(), 0, qmlDoc->editorRevision());
ti.setReferencingFile(doc->fileName());
m_quickDocMap.insert(fileName, ti);
break;
}
}
delete ttItem;
} else {
// if it was formerly unnamed remove the respective items
if (m_quickDocMap.contains(QLatin1String("<unnamed>"))) {
m_model->removeUnnamedQuickTest(d->fileName());
TestInfo unnamedInfo = m_quickDocMap[QLatin1String("<unnamed>")];
QStringList functions = unnamedInfo.testFunctions();
foreach (const QString &func, testFunctions.keys())
if (functions.contains(func))
functions.removeOne(func);
unnamedInfo.setTestFunctions(functions);
m_quickDocMap.insert(QLatin1String("<unnamed>"), unnamedInfo);
}
m_model->addQuickTest(ttItem);
TestInfo ti(tcName, testFunctions.keys(), 0, qmlDoc->editorRevision());
ti.setReferencingFile(doc->fileName());
m_quickDocMap.insert(tcLocation.m_fileName, ti);
}
}
}
void TestCodeParser::onCppDocumentUpdated(const CPlusPlus::Document::Ptr &doc)
{
if (!m_currentProject)
return;
@@ -270,13 +504,51 @@ void TestCodeParser::onDocumentUpdated(CPlusPlus::Document::Ptr doc)
checkDocumentForTestCode(doc);
}
void TestCodeParser::onQmlDocumentUpdated(const QmlJS::Document::Ptr &doc)
{
if (!m_currentProject)
return;
const QString fileName = doc->fileName();
if (m_quickDocMap.contains(fileName)) {
if ((int)m_quickDocMap[fileName].editorRevision() == doc->editorRevision()) {
qDebug("Skipped due revision equality (QML)"); // added to verify this ever happens....
return;
}
} else if (!m_currentProject->files(ProjectExplorer::Project::AllFiles).contains(fileName)) {
// 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())) {
checkDocumentForTestCode(snapshot.document(m_quickDocMap[fileName].referencingFile()));
}
if (!m_quickDocMap.contains(QLatin1String("<unnamed>")))
return;
// special case of having unnamed TestCases
TestTreeItem *unnamed = m_model->unnamedQuickTests();
for (int row = 0, count = unnamed->childCount(); row < count; ++row) {
const TestTreeItem *child = unnamed->child(row);
if (fileName == child->filePath()) {
if (snapshot.contains(child->mainFile()))
checkDocumentForTestCode(snapshot.document(child->mainFile()));
break;
}
}
}
void TestCodeParser::removeFiles(const QStringList &files)
{
foreach (const QString file, files) {
foreach (const QString &file, files) {
if (m_cppDocMap.contains(file)) {
m_cppDocMap.remove(file);
m_model->removeAutoTestSubtreeByFilePath(file);
}
if (m_quickDocMap.contains(file)) {
m_quickDocMap.remove(file);
m_model->removeQuickTestSubtreeByFilePath(file);
}
}
}
@@ -286,7 +558,7 @@ void TestCodeParser::scanForTests()
CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
CPlusPlus::Snapshot snapshot = cppMM->snapshot();
foreach (const QString file, list) {
foreach (const QString &file, list) {
if (snapshot.contains(file)) {
CPlusPlus::Document::Ptr doc = snapshot.find(file).value();
checkDocumentForTestCode(doc);
@@ -297,6 +569,7 @@ void TestCodeParser::scanForTests()
void TestCodeParser::clearMaps()
{
m_cppDocMap.clear();
m_quickDocMap.clear();
}
} // namespace Internal