File indexing completed on 2024-05-19 15:41:38

0001 /*
0002     SPDX-FileCopyrightText: 2012 Sven Brauch <svenbrauch@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "pycompletiontest.h"
0008 
0009 #include <language/backgroundparser/backgroundparser.h>
0010 #include <language/codecompletion/codecompletiontesthelper.h>
0011 #include <language/duchain/declaration.h>
0012 #include <language/codegen/coderepresentation.h>
0013 #include <language/duchain/duchain.h>
0014 #include <interfaces/ilanguagecontroller.h>
0015 
0016 #include <tests/testcore.h>
0017 #include <tests/autotestshell.h>
0018 
0019 #include <ktexteditor_version.h>
0020 #include <KTextEditor/Editor>
0021 #include <KService>
0022 
0023 #include "codecompletion/context.h"
0024 #include "codecompletion/helpers.h"
0025 #include "codecompletiondebug.h"
0026 
0027 #include <QDebug>
0028 #include <QStandardPaths>
0029 #include <QTest>
0030 
0031 using namespace KDevelop;
0032 
0033 QTEST_MAIN(Python::PyCompletionTest)
0034 
0035 Q_DECLARE_METATYPE(QList<Python::RangeInString>)
0036 
0037 static int testId = 0;
0038 static QString basepath = "/tmp/__kdevpythoncompletiontest.dir/";
0039 
0040 namespace Python {
0041 
0042 QStandardItemModel& fakeModel() {
0043   static QStandardItemModel model;
0044   model.setColumnCount(10);
0045   model.setRowCount(10);
0046   return model;
0047 }
0048 
0049 QString filenameForTestId(const int id) {
0050     return basepath + "test_" + QString::number(id) + ".py";
0051 }
0052 
0053 QString nextFilename() {
0054     testId += 1;
0055     return filenameForTestId(testId);
0056 }
0057 
0058 PyCompletionTest::PyCompletionTest(QObject* parent) : QObject(parent)
0059 {
0060     initShell();
0061 }
0062 
0063 void makefile(QString filename, QString contents) {
0064     QFile fileptr;
0065     fileptr.setFileName(basepath + filename);
0066     fileptr.open(QIODevice::WriteOnly);
0067     fileptr.write(contents.toUtf8());
0068     fileptr.close();
0069     auto url = QUrl::fromLocalFile(QDir::cleanPath(basepath + filename));
0070     qCDebug(KDEV_PYTHON_CODECOMPLETION) <<  "updating duchain for " << url.url() << basepath;
0071     const IndexedString urlstring(url);
0072     DUChain::self()->updateContextForUrl(urlstring, KDevelop::TopDUContext::ForceUpdate);
0073     ICore::self()->languageController()->backgroundParser()->parseDocuments();
0074     DUChain::self()->waitForUpdate(urlstring, KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
0075 }
0076 
0077 void PyCompletionTest::initShell()
0078 {
0079     AutoTestShell::init();
0080     TestCore* core = new TestCore();
0081     core->initialize(KDevelop::Core::NoUi);
0082     QDir d;
0083     d.mkpath(basepath);
0084 
0085     auto doc_url = QDir::cleanPath(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0086                                                           "kdevpythonsupport/documentation_files/builtindocumentation.py"));
0087 
0088     DUChain::self()->updateContextForUrl(IndexedString(doc_url), KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
0089     ICore::self()->languageController()->backgroundParser()->parseDocuments();
0090     DUChain::self()->waitForUpdate(IndexedString(doc_url), KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
0091 
0092     DUChain::self()->disablePersistentStorage();
0093     KDevelop::CodeRepresentation::setDiskChangesForbidden(true);
0094 
0095     // now, create a nice little completion hierarchy
0096     d.mkpath(basepath + "submoduledir");
0097     d.mkpath(basepath + "submoduledir/anothersubdir");
0098     makefile("toplevelmodule.py", "some_var = 3\ndef some_function(): pass\nclass some_class():\n def method(): pass");
0099     makefile("submoduledir/__init__.py", "var_in_sub_init = 5");
0100     makefile("submoduledir/subfile.py", "var_in_subfile = 5\nclass some_subfile_class():\n def method2(): pass");
0101     makefile("submoduledir/anothersubdir/__init__.py", "var_in_subsub_init = 5");
0102     makefile("submoduledir/anothersubdir/subsubfile.py", "var_in_subsubfile = 5\nclass another_subfile_class():"
0103                                                       "\n def method3(): pass");
0104 }
0105 
0106 void PyCompletionTest::testIdentifierMatching()
0107 {
0108     QCOMPARE(camelCaseToUnderscore("FooBarBaz").toUtf8().data(), "foo_bar_baz");
0109     QCOMPARE(camelCaseToUnderscore("fooBarbaz").toUtf8().data(),  "foo_barbaz");
0110 
0111     QCOMPARE(identifierMatchQuality("foobar", "foobar"),  3);
0112     QCOMPARE(identifierMatchQuality("foobar", "bar"),  2);
0113     QCOMPARE(identifierMatchQuality("bar", "foobar"),  2);
0114     QCOMPARE(identifierMatchQuality("foobarbaz", "bar"),  2);
0115     QCOMPARE(identifierMatchQuality("bar", "foobarbaz"),  2);
0116     QCOMPARE(identifierMatchQuality("FoobarBaz", "FoobarBang"), 1);
0117     QCOMPARE(identifierMatchQuality("Foobar_Baz", "Foobar_Bang"), 1);
0118     QCOMPARE(identifierMatchQuality("xydsf", "qkigfb"), 0);
0119     QCOMPARE(identifierMatchQuality("ac_ac", "ac_ae"), 0);
0120     QCOMPARE(identifierMatchQuality("AcAb", "AbDe"), 0);
0121 }
0122 
0123 void PyCompletionTest::testExpressionParserMisc()
0124 {
0125     // in completion, strings are filtered out and never contain " or ' chars.
0126     ExpressionParser p("foobar(3, \"some_string\", func(), funcfunc(3, 5), \t");
0127     bool ok;
0128     int expressionsSkipped = 0;
0129     p.skipUntilStatus(ExpressionParser::EventualCallFound, &ok, &expressionsSkipped);
0130     QVERIFY(ok);
0131     QCOMPARE(expressionsSkipped, 4); // number of params
0132     QCOMPARE(p.getRemainingCode(), QString("foobar"));
0133     ExpressionParser::Status s;
0134     QString calledFunction = p.popExpression(&s);
0135     QVERIFY(s == ExpressionParser::ExpressionFound);
0136     QCOMPARE(calledFunction, QString("foobar"));
0137 
0138     ExpressionParser q("hello(world, foo.bar[3].foobar(3, \"some_string\", func(), funcfunc(3, 5), \t");
0139     q.skipUntilStatus(ExpressionParser::EventualCallFound, &ok, &expressionsSkipped);
0140     QVERIFY(ok);
0141     QCOMPARE(expressionsSkipped, 4);
0142     QCOMPARE(q.getRemainingCode(), QString("hello(world, foo.bar[3].foobar"));
0143     calledFunction = q.popExpression(&s);
0144     QCOMPARE(s, ExpressionParser::ExpressionFound);
0145     QCOMPARE(calledFunction, QString("foo.bar[3].foobar"));
0146 }
0147 
0148 void PyCompletionTest::testExpressionParser()
0149 {
0150     QFETCH(QString, data);
0151     QFETCH(int, expectedStatus);
0152     QFETCH(QString, expectedExpression);
0153 
0154     ExpressionParser p(data);
0155     ExpressionParser::Status status;
0156     QString result = p.popExpression(&status);
0157     QCOMPARE((int) status, expectedStatus);
0158     QCOMPARE(result, expectedExpression);
0159 }
0160 
0161 void PyCompletionTest::testExpressionParser_data()
0162 {
0163     QTest::addColumn<QString>("data");
0164     QTest::addColumn<int>("expectedStatus");
0165     QTest::addColumn<QString>("expectedExpression");
0166 
0167     QTest::newRow("attrExpression") << "foo.bar.baz" << (int) ExpressionParser::ExpressionFound << "foo.bar.baz";
0168     QTest::newRow("attrExpressionAccess") << "foo.bar.baz." << (int) ExpressionParser::MemberAccessFound << "";
0169     QTest::newRow("attrExpressionCall") << "foo.bar(3, 5, 7, hell0(3)).baz" << (int) ExpressionParser::ExpressionFound << "foo.bar(3, 5, 7, hell0(3)).baz";
0170     QTest::newRow("nextArg") << "foo(3, 5, \t" << (int) ExpressionParser::CommaFound << "";
0171     QTest::newRow("call") << "fo0barR( \t  " << (int) ExpressionParser::EventualCallFound << "";
0172     QTest::newRow("initializer") << "my_list = [" << (int) ExpressionParser::InitializerFound << "";
0173     QTest::newRow("fancy_initializer") << "my_list = [1, 2, 3, 4, []" << (int) ExpressionParser::ExpressionFound << "[]";
0174     QTest::newRow("def") << "def " << (int) ExpressionParser::DefFound << "";
0175 }
0176 
0177 const QList<CompletionTreeItem*> PyCompletionTest::invokeCompletionOn(const QString& initCode, const QString& invokeCode)
0178 {
0179     CompletionParameters data = prepareCompletion(initCode, invokeCode);
0180     return runCompletion(data);
0181 }
0182 
0183 const CompletionParameters PyCompletionTest::prepareCompletion(const QString& initCode, const QString& invokeCode)
0184 {
0185     CompletionParameters completion_data;
0186 
0187     QString filename = nextFilename();
0188     QFile fileptr(filename);
0189     fileptr.open(QIODevice::WriteOnly);
0190     fileptr.write(initCode.toUtf8().replace("%INVOKE", ""));
0191     fileptr.close();
0192 
0193     DUChain::self()->updateContextForUrl(IndexedString(filename), KDevelop::TopDUContext::ForceUpdate);
0194     ICore::self()->languageController()->backgroundParser()->parseDocuments();
0195     ReferencedTopDUContext topContext = DUChain::self()->waitForUpdate(IndexedString(filename),
0196                                                                        KDevelop::TopDUContext::AllDeclarationsAndContexts);
0197 
0198     Q_ASSERT(topContext);
0199 
0200     Q_ASSERT(initCode.indexOf("%INVOKE") != -1);
0201     QString copy = initCode;
0202     QString allCode = copy.replace("%INVOKE", invokeCode);
0203 
0204     QStringList lines = allCode.split('\n');
0205     completion_data.cursorAt = CursorInRevision::invalid();
0206     for ( int i = 0; i < lines.length(); i++ ) {
0207         int j = lines.at(i).indexOf("%CURSOR");
0208         if ( j != -1 ) {
0209             completion_data.cursorAt = CursorInRevision(i, j);
0210             break;
0211         }
0212     }
0213     Q_ASSERT(completion_data.cursorAt.isValid());
0214     // codeCompletionContext only gets passed the text until the place where completion is invoked
0215     completion_data.snip = allCode.mid(0, allCode.indexOf("%CURSOR"));
0216     completion_data.remaining = allCode.mid(allCode.indexOf("%CURSOR") + 7);
0217 
0218     DUChainReadLocker lock;
0219     completion_data.contextAtCursor = DUContextPointer(topContext->findContextAt(completion_data.cursorAt, true));
0220     Q_ASSERT(completion_data.contextAtCursor);
0221 
0222     return completion_data;
0223 }
0224 
0225 const QList<CompletionTreeItem*> PyCompletionTest::runCompletion(const CompletionParameters parameters)
0226 {
0227     PythonCodeCompletionContext* context = new PythonCodeCompletionContext(parameters.contextAtCursor, parameters.snip, parameters.remaining, parameters.cursorAt, 0, nullptr);
0228     bool abort = false;
0229     QList<CompletionTreeItem*> items;
0230     foreach ( CompletionTreeItemPointer ptr, context->completionItems(abort, true) ) {
0231         items << ptr.data();
0232         // those are leaked, but it's only a few kb while the tests are running. who cares.
0233         m_ptrs << ptr;
0234     }
0235     return items;
0236 }
0237 
0238 bool PyCompletionTest::containsItemForDeclarationNamed(const QList<CompletionTreeItem*> items, QString itemName)
0239 {
0240     foreach ( const CompletionTreeItem* ptr, items ) {
0241         if ( ptr->declaration() ) {
0242             if ( ptr->declaration()->identifier().toString() == itemName ) {
0243                 return true;
0244             }
0245         }
0246     }
0247     return false;
0248 }
0249 
0250 bool PyCompletionTest::containsItemStartingWith(const QList<CompletionTreeItem*> items, const QString& itemName)
0251 {
0252     QModelIndex idx = fakeModel().index(0, KDevelop::CodeCompletionModel::Name);
0253     foreach ( const CompletionTreeItem* ptr, items ) {
0254         if ( ptr->data(idx, Qt::DisplayRole, nullptr).toString().startsWith(itemName) ) {
0255             return true;
0256         }
0257     }
0258     return false;
0259 }
0260 
0261 bool PyCompletionTest::itemInCompletionList(const QString& initCode, const QString& invokeCode, QString itemName)
0262 {
0263     QList< CompletionTreeItem* > items = invokeCompletionOn(initCode, invokeCode);
0264     return containsItemStartingWith(items, itemName);
0265 }
0266 
0267 bool PyCompletionTest::declarationInCompletionList(const QString& initCode, const QString& invokeCode, QString itemName)
0268 {
0269     QList< CompletionTreeItem* > items = invokeCompletionOn(initCode, invokeCode);
0270     return containsItemForDeclarationNamed(items, itemName);
0271 }
0272 
0273 bool PyCompletionTest::completionListIsEmpty(const QString& initCode, const QString& invokeCode)
0274 {
0275     return invokeCompletionOn(initCode, invokeCode).isEmpty();
0276 }
0277 
0278 void PyCompletionTest::testImportCompletion()
0279 {
0280     QFETCH(QString, invokeCode);
0281     QFETCH(QString, completionCode);
0282     QFETCH(QString, expectedItem);
0283 
0284     if ( expectedItem == "EMPTY" ) {
0285         QVERIFY(completionListIsEmpty(invokeCode, completionCode));
0286     }
0287     else {
0288         QVERIFY(itemInCompletionList(invokeCode, completionCode, expectedItem));
0289     }
0290 }
0291 
0292 void PyCompletionTest::testImportCompletion_data()
0293 {
0294     QTest::addColumn<QString>("invokeCode");
0295     QTest::addColumn<QString>("completionCode");
0296     QTest::addColumn<QString>("expectedItem");
0297 
0298     QTest::newRow("same_directory") << "%INVOKE" << "import %CURSOR" << "toplevelmodule";
0299 //     QTest::newRow("same_directory_beginText") << "%INVOKE" << "import toplevelmo%CURSOR" << "toplevelmodule";
0300     QTest::newRow("nocompletion") << "%INVOKE" << "from toplevelmodule %CURSOR" << "EMPTY";
0301     QTest::newRow("subdirectory_full") << "%INVOKE" << "import %CURSOR" << "submoduledir";
0302     QTest::newRow("subdirectory_file") << "%INVOKE" << "import submoduledir.%CURSOR" << "subfile";
0303     QTest::newRow("subsubdirectory_file") << "%INVOKE" << "import submoduledir.anothersubdir.%CURSOR" << "subsubfile";
0304     QTest::newRow("subdirectory_from") << "%INVOKE" << "from submoduledir import %CURSOR" << "subfile";
0305     QTest::newRow("subdirectory_declfromfile") << "%INVOKE" << "from submoduledir.subfile import %CURSOR" << "var_in_subfile";
0306     QTest::newRow("declaration_from_init_subdir") << "%INVOKE" << "from submoduledir import %CURSOR" << "var_in_sub_init";
0307     QTest::newRow("class_from_file") << "%INVOKE" << "from toplevelmodule import %CURSOR" << "some_class";
0308     // TODO implement this or not? It breaks the possibility to easily document modules like PyQT.
0309     // maybe enable this behaviour only for doc files?
0310 //     QTest::newRow("class_property_not") << "%INVOKE" << "import toplevelmodule.some_class.%CURSOR" << "EMPTY";
0311     QTest::newRow("class_from_file_in_subdir") << "%INVOKE" << "from submoduledir.subfile import %CURSOR" << "some_subfile_class";
0312 }
0313 
0314 void PyCompletionTest::testCompletionAfterQuotes()
0315 {
0316     QFETCH(QString, invokeCode);
0317     QFETCH(QString, completionCode);
0318     invokeCode = "testvar = 3\n" + invokeCode;
0319     QVERIFY( ! completionListIsEmpty(invokeCode, completionCode) );
0320 }
0321 
0322 
0323 void PyCompletionTest::testCompletionAfterQuotes_data()
0324 {
0325     QTest::addColumn<QString>("invokeCode");
0326     QTest::addColumn<QString>("completionCode");
0327 
0328     QTest::newRow("nothing") << "\n%INVOKE" << "%CURSOR";
0329     QTest::newRow("sq_in_string") << "\"foo'bar\"\n%INVOKE" << "%CURSOR";
0330     QTest::newRow("sq_in_sl_comment") << "#foo'bar\n%INVOKE" << "%CURSOR";
0331     QTest::newRow("sq_in_ml_string") << "\"\"\"foo'bar\n\n' \n'\"\"\"\n%INVOKE" << "%CURSOR";
0332     QTest::newRow("dq_in_string") << "'foo\"bar'\n%INVOKE" << "%CURSOR";
0333     QTest::newRow("dq_in_comment") << "# \" foo\n%INVOKE" << "%CURSOR";
0334     QTest::newRow("dq_in_ml_string") << "\'\'\'foo \n\"\n\n \'\'\'\n%INVOKE" << "%CURSOR";
0335 }
0336 
0337 void PyCompletionTest::testNoImplicitMagicFunctions()
0338 {
0339     QVERIFY(! itemInCompletionList("class my(): pass\nd = my()\n%INVOKE", "d.%CURSOR", "__get__") );
0340     QEXPECT_FAIL("", "Sorting needs to be fixed first before magic function completion can be re-enabled", Continue);
0341     QVERIFY(itemInCompletionList("class my():\n def __get__(self): pass\nd = my()\n%INVOKE", "d.%CURSOR", "__get__") );
0342 }
0343 
0344 void PyCompletionTest::testIntegralTypesImmediate()
0345 {
0346     QFETCH(QString, invokeCode);
0347     QFETCH(QString, completionCode);
0348     QFETCH(QString, expectedDeclaration);
0349 
0350     QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedDeclaration));
0351 }
0352 
0353 void PyCompletionTest::testIntegralTypesImmediate_data()
0354 {
0355     QTest::addColumn<QString>("invokeCode");
0356     QTest::addColumn<QString>("completionCode");
0357     QTest::addColumn<QString>("expectedDeclaration");
0358 
0359     QTest::newRow("list_syntax") << "[]%INVOKE" << ".%CURSOR" << "append";
0360     QTest::newRow("dict_syntax") << "{}%INVOKE" << ".%CURSOR" << "items";
0361     QTest::newRow("string_syntax") << "\"\"%INVOKE" << ".%CURSOR" << "capitalize";
0362     QTest::newRow("list_class") << "list()%INVOKE" << ".%CURSOR" << "append";
0363     QTest::newRow("dict_class") << "dict()%INVOKE" << ".%CURSOR" << "items";
0364     QTest::newRow("string_class") << "str()%INVOKE" << ".%CURSOR" << "capitalize";
0365 }
0366 
0367 void PyCompletionTest::testIntegralExpressionsDifferentContexts()
0368 {
0369     QFETCH(QString, invokeCode);
0370     QFETCH(QString, completionCode);
0371     QFETCH(QString, expectedDeclaration);
0372 
0373     QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedDeclaration));
0374 }
0375 
0376 void PyCompletionTest::testIntegralExpressionsDifferentContexts_data()
0377 {
0378     QTest::addColumn<QString>("invokeCode");
0379     QTest::addColumn<QString>("completionCode");
0380     QTest::addColumn<QString>("expectedDeclaration");
0381 
0382     QTest::newRow("function_call") << "foo([]%INVOKE)" << ".%CURSOR" << "append";
0383     QTest::newRow("function_call_multi") << "foo(bar(baz(bang([]%INVOKE))))" << ".%CURSOR" << "append";
0384     QTest::newRow("empty_list") << "[[]%INVOKE]" << ".%CURSOR" << "append";
0385     QTest::newRow("list") << "[1, 2, 3, 4, 5, []%INVOKE]" << ".%CURSOR" << "append";
0386     QTest::newRow("list_with_fancy_string") << "[\"FooFObar\\\", 3)\", []%INVOKE]" << ".%CURSOR" << "append";
0387     QTest::newRow("empty_dict") << "{[]%INVOKE}" << ".%CURSOR" << "append";
0388     QTest::newRow("print_stmt") << "%INVOKE" << "print([].%CURSOR" << "append";
0389 }
0390 
0391 void PyCompletionTest::testIgnoreCommentSignsInStringLiterals()
0392 {
0393     QVERIFY( ! completionListIsEmpty("'#'%INVOKE", ".%CURSOR") );
0394     QVERIFY( ! completionListIsEmpty("def addEntry(self,array):\n"
0395                                      "  \"\"\"\"some comment\"\"\"\n  %INVOKE", "%CURSOR") );
0396 }
0397 
0398 void PyCompletionTest::testNoCompletionInCommentsOrStrings()
0399 {
0400     QFETCH(QString, invokeCode);
0401     QFETCH(QString, completionCode);
0402 
0403     QVERIFY(! declarationInCompletionList(invokeCode, completionCode, "append"));
0404 }
0405 
0406 void PyCompletionTest::testNoCompletionInCommentsOrStrings_data()
0407 {
0408     QTest::addColumn<QString>("invokeCode");
0409     QTest::addColumn<QString>("completionCode");
0410 
0411     QTest::newRow("single_comment") << "# []%INVOKE" << ".%CURSOR";
0412     QTest::newRow("single_comment_local") << "local=3\n# %INVOKE" << "%CURSOR";
0413     QTest::newRow("stringDQ") << "\"[]%INVOKE\"" << ".%CURSOR";
0414     QTest::newRow("stringSQ") << "\'[]%INVOKE\'" << ".%CURSOR";
0415     QTest::newRow("multilineDQ") << "\"\"\"[]%INVOKE\"\"\"" << ".%CURSOR";
0416     QTest::newRow("multilineSQ") << "\'\'\'[]%INVOKE\'\'\'" << ".%CURSOR";
0417     QTest::newRow("multilineDQ_newlines") << "\"\"\"\n\n[]%INVOKE\n\n\n\"\"\"" << ".%CURSOR";
0418     QTest::newRow("multilineSQ_newlines") << "\'\'\'\n\n[]%INVOKE\n\n\n\'\'\'" << ".%CURSOR";
0419 }
0420 
0421 void PyCompletionTest::testImplementMethodCompletion()
0422 {
0423     QFETCH(QString, invokeCode);
0424     QFETCH(QString, completionCode);
0425     QVERIFY(itemInCompletionList(invokeCode, completionCode, "__init__"));
0426 }
0427 
0428 void PyCompletionTest::testImplementMethodCompletion_data()
0429 {
0430     QTest::addColumn<QString>("invokeCode");
0431     QTest::addColumn<QString>("completionCode");
0432 
0433     QTest::newRow("simple_begin") << "class myclass():\n %INVOKE\n pass" << "def %CURSOR";
0434     QTest::newRow("another_method_before") << "class myclass():\n def some_method(param):pass\n %INVOKE" << "def %CURSOR";
0435     QTest::newRow("another_method_before_multiline") << "class myclass():\n def some_method(param):\n  pass\n  pass \n  pass"
0436                                                         "\n %INVOKE" << "def %CURSOR";
0437     QTest::newRow("contextskip") << "class myclass():\n def some_method(param):\n  pass\n \n \n \n %INVOKE" << "def %CURSOR";
0438     QTest::newRow("contextskip2") << "class myclass():\n def some_method(param): pass\n"
0439                                      " def some_method2(param):\n  pass\n  pass\n %INVOKE" << "\n \n \n def %CURSOR";
0440 }
0441 
0442 void PyCompletionTest::testAutoBrackets()
0443 {
0444     QList< CompletionTreeItem* > items = invokeCompletionOn("class Foo:\n @property\n def myprop(self): pass\n"
0445                                                             "a=Foo()\n%INVOKE", "a.%CURSOR");
0446     QVERIFY(containsItemForDeclarationNamed(items, "myprop"));
0447     CompletionTreeItem* item = nullptr;
0448     foreach ( CompletionTreeItem* ptr, items ) {
0449         if ( ptr->declaration() ) {
0450             if ( ptr->declaration()->identifier().toString() == "myprop" ) {
0451                 item = ptr;
0452                 break;
0453             }
0454         }
0455     }
0456     QVERIFY(item);
0457     KService::Ptr documentService = KService::serviceByDesktopPath("katepart.desktop");
0458     QVERIFY(documentService);
0459     KTextEditor::Document* document = documentService->createInstance<KTextEditor::Document>(this);
0460     auto view = document->createView(nullptr);
0461     QVERIFY(document);
0462     item->execute(view, KTextEditor::Range(0, 0, 0, 0));
0463     QCOMPARE(document->text(), QLatin1String("myprop"));
0464 }
0465 
0466 void PyCompletionTest::testExceptionCompletion()
0467 {
0468     QList< CompletionTreeItem* > items = invokeCompletionOn("localvar = 3\nraise %INVOKE", "%CURSOR");
0469     QVERIFY(containsItemForDeclarationNamed(items, "Exception"));
0470     QVERIFY(! containsItemForDeclarationNamed(items, "localvar"));
0471 
0472     items = invokeCompletionOn("localvar = 3\n%INVOKE", "try: pass\nexcept %CURSOR");
0473     QVERIFY(containsItemForDeclarationNamed(items, "Exception"));
0474     QVERIFY(! containsItemForDeclarationNamed(items, "localvar"));
0475 }
0476 
0477 void PyCompletionTest::testGeneratorCompletion()
0478 {
0479     QVERIFY(itemInCompletionList("%INVOKE", "foobar = [item for %CURSOR", "item in"));
0480     QVERIFY(itemInCompletionList("%INVOKE", "foobar = [key, value for %CURSOR", "key, value in"));
0481     QVERIFY(itemInCompletionList("%INVOKE", "foobar = [str(key + value) for %CURSOR", "key, value in"));
0482     QVERIFY(itemInCompletionList("%INVOKE\ndec_l8r=3", "foobar = [dec_l8r for %CURSOR", "dec_l8r in"));
0483 }
0484 
0485 void PyCompletionTest::testInheritanceCompletion()
0486 {
0487     QList< CompletionTreeItem* > items = invokeCompletionOn("class parentClass: pass\n%INVOKE", "class childClass(%CURSOR");
0488     QVERIFY(containsItemForDeclarationNamed(items, "parentClass"));
0489     items = invokeCompletionOn("class parentClass: pass\nclass childClass(%INVOKE): pass", "%CURSOR");
0490     QVERIFY(containsItemForDeclarationNamed(items, "parentClass"));
0491     items = invokeCompletionOn("class parentClass:\n class blubb: pass\nclass childClass(%INVOKE): pass", "parentClass.%CURSOR");
0492     QVERIFY(! containsItemForDeclarationNamed(items, "parentClass"));
0493     QVERIFY(containsItemForDeclarationNamed(items, "blubb"));
0494 }
0495 
0496 void PyCompletionTest::testAddImportCompletion()
0497 {
0498     QFETCH(QString, completionCode);
0499     QFETCH(QString, invokeCode);
0500     QFETCH(int, expectedItems);
0501 
0502     QCOMPARE(invokeCompletionOn(completionCode, invokeCode).size(), expectedItems);
0503 }
0504 
0505 void PyCompletionTest::testAddImportCompletion_data()
0506 {
0507     QTest::addColumn<QString>("completionCode");
0508     QTest::addColumn<QString>("invokeCode");
0509     QTest::addColumn<int>("expectedItems");
0510 
0511     QTest::newRow("has_entry_when_necessary") << "toplevelmodule%INVOKE" << ".%CURSOR" << 1;
0512     QTest::newRow("has_no_when_not_necessary") << "toplevelmodule = 3;\ntoplevelmodule%INVOKE" << ".%CURSOR" << 0;
0513 }
0514 
0515 void PyCompletionTest::testFunctionDeclarationCompletion()
0516 {
0517     QFETCH(QString, completionCode);
0518     QFETCH(QString, invokeCode);
0519     QFETCH(KTextEditor::Range, executeRange);
0520     QFETCH(QString, expectedReplacement);
0521 
0522     QString documentCode = completionCode;
0523     documentCode.replace("%INVOKE", invokeCode).replace("%CURSOR", "");
0524 
0525     QString expectedCode = completionCode;
0526     expectedCode.replace("%INVOKE", expectedReplacement);
0527 
0528     const QList<CompletionTreeItem *> completionItems = invokeCompletionOn(completionCode, invokeCode);
0529 
0530     QVERIFY( ! completionItems.isEmpty() );
0531 
0532     KService::Ptr documentService = KService::serviceByDesktopPath("katepart.desktop");
0533     QVERIFY(documentService);
0534     KTextEditor::Document* document = documentService->createInstance<KTextEditor::Document>(this);
0535     QVERIFY(document);
0536     document->setText(documentCode);
0537 
0538     auto view = document->createView(nullptr);
0539 
0540     completionItems.first()->execute(view, executeRange);
0541     QCOMPARE(document->text(), expectedCode);
0542 }
0543 
0544 void PyCompletionTest::testFunctionDeclarationCompletion_data()
0545 {
0546     QTest::addColumn<QString>("completionCode");
0547     QTest::addColumn<QString>("invokeCode");
0548     QTest::addColumn<KTextEditor::Range>("executeRange");
0549     QTest::addColumn<QString>("expectedReplacement");
0550 
0551     QTest::newRow("func_decl_no_parens") << "def foo():\n  pass\n%INVOKE" << "foo%CURSOR" << KTextEditor::Range(2, 0, 2, 3)
0552                                          << "foo()";
0553 
0554     QTest::newRow("func_decl_existing_parens") << "def foo():\n  return 0\nbar = %INVOKE" << "foo%CURSOR()" << KTextEditor::Range(2, 6, 2, 9)
0555                                            << "foo()";
0556 
0557     QTest::newRow("decorator_no_parens") << "def mydecorator():\n  pass\nclass Foo:\n  %INVOKE\n  def bar():\n    pass" << "@mydecorator%CURSOR" << KTextEditor::Range(3, 3, 3, 15)
0558                                          << "@mydecorator";
0559 
0560     QTest::newRow("class_name_no_constructor_parens") << "class Foo:\n  pass\nbar = %INVOKE" << "Foo%CURSOR" << KTextEditor::Range(2, 6, 2, 9)
0561                                                       << "Foo()";
0562 
0563     QTest::newRow("class_name_explicit_constructor_parens") << "class Foo:\n  def __init__(self):\n    pass\nbar = %INVOKE" << "Fo%CURSOR"
0564                                                         << KTextEditor::Range(3, 6, 3, 9)
0565                                                         << "Foo()";
0566 }
0567 
0568 void PyCompletionTest::testCompletionScopes()
0569 {
0570     QFETCH(QString, invokeCode);
0571     QFETCH(QString, completionCode);
0572     QFETCH(QString, expectedDeclaration);
0573     QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedDeclaration));
0574 }
0575 
0576 void PyCompletionTest::testCompletionScopes_data()
0577 {
0578     QTest::addColumn<QString>("invokeCode");
0579     QTest::addColumn<QString>("completionCode");
0580     QTest::addColumn<QString>("expectedDeclaration");
0581     QTest::newRow("class_scope_end_inside") << "class A:\n test1=1\nclass B:\n test2=1\nf = A()\nclass T:\n f = B()\n f%INVOKE" << ".%CURSOR" << "test2";
0582     QTest::newRow("class_scope_end_outside") << "class A:\n test1=1\nclass B:\n test2=1\nf = A()\nclass T:\n f = B()\nf%INVOKE" << ".%CURSOR" << "test1";
0583 }
0584 
0585 void PyCompletionTest::testStringFormattingCompletion()
0586 {
0587     QFETCH(QString, completionCode);
0588     QFETCH(QString, invokeCode);
0589     QFETCH(QString, expectedItem);
0590     QFETCH(bool, expectedPresent);
0591 
0592     QCOMPARE(itemInCompletionList(completionCode, invokeCode, expectedItem), expectedPresent);
0593 }
0594 
0595 void PyCompletionTest::testStringFormattingCompletion_data()
0596 {
0597     QTest::addColumn<QString>("completionCode");
0598     QTest::addColumn<QString>("invokeCode");
0599     QTest::addColumn<QString>("expectedItem");
0600     QTest::addColumn<bool>("expectedPresent");
0601 
0602     QTest::newRow("sq_string") << "'foo %INVOKE'" << "%CURSOR" << "{0}" << true;
0603     QTest::newRow("dq_string") << "\"foo %INVOKE bar\"" << "%CURSOR" << "{0}" << true;
0604     QTest::newRow("sq_ml_string") << "'''foo\n\n%INVOKE\nbar'''" << "%CURSOR" << "{0}" << true;
0605     QTest::newRow("dq_ml_string") << "\"\"\"foo\nbar %INVOKE baz\"\"\"" << "%CURSOR" << "{0}" << true;
0606     QTest::newRow("auto_id") << "\"foo {0} bar {1} baz %INVOKE\"" << "%CURSOR" << "{2}" << true;
0607 
0608     QTest::newRow("format_suggestions_conversion") << "\"foo {0} bar %INVOKE\"" << "{1}%CURSOR" << "{1!r}" << true;
0609     QTest::newRow("format_suggestions_spec") << "\"foo {0} bar {1}%INVOKE baz\"" << "{2}%CURSOR" << "{2:%}" << true;
0610 
0611     QTest::newRow("incompatible_suggestions_conversion") << "\"foo %INVOKE bar\"" << "{0:%}%CURSOR" << "{0!s:%}" << false;
0612     QTest::newRow("incompatible_suggestions_spec") << "\"foo %INVOKE bar\"" << "{0!s}%CURSOR" << "{0!s:%}" << false;
0613 
0614     QTest::newRow("alignment_for_strings") << "\"foo %INVOKE bar\"" << "{0!s}%CURSOR" << "{0!s:^${width}}" << true;
0615     QTest::newRow("conversion_for_aliged_strings") << "\"foo %INVOKE bar\"" << "{0!s}%CURSOR" << "{0!s:^${width}}" << true;
0616 
0617 }
0618 
0619 void PyCompletionTest::testStringFormatter()
0620 {
0621     QFETCH(QString, string);
0622     QFETCH(int, expectedId);
0623     QFETCH(QList<RangeInString>, expectedVariablePositions);
0624 
0625     StringFormatter f(string);
0626 
0627     int id = f.nextIdentifierId();
0628     QCOMPARE(id, expectedId);
0629 
0630     for (int i = 0; i < string.size(); i++) {
0631         bool expectedInsideVariable = false;
0632         foreach (RangeInString range, expectedVariablePositions) {
0633             if (i >= range.beginIndex && i <= range.endIndex) {
0634                 expectedInsideVariable = true;
0635                 break;
0636             }
0637         }
0638         QCOMPARE(f.isInsideReplacementVariable(i), expectedInsideVariable);
0639     }
0640 }
0641 
0642 void PyCompletionTest::testStringFormatter_data()
0643 {
0644     QTest::addColumn<QString>("string");
0645     QTest::addColumn<int>("expectedId");
0646     QTest::addColumn<QList<RangeInString> >("expectedVariablePositions");
0647 
0648     QTest::newRow("sl_string") << "\"foo {0} bar {1}\"" << 2
0649                                << (QList<RangeInString>() << RangeInString(5, 8) << RangeInString(13, 16));
0650 
0651     QTest::newRow("ml_string") << "\"\"\"foo {0} \n\nbar {1} \n{foo} {2}\nbaz\"\"\"" << 3
0652                                << (QList<RangeInString>() << RangeInString(7, 10) << RangeInString(17, 20)
0653                                    << RangeInString(22, 27) << RangeInString(28, 31));
0654 
0655     QTest::newRow("containing_quotes") << "'''foo {0}\nbar\n{1}{0} \\' \\\" \\\" {2} {foo} \n\\'\n {3}baz'''" << 4
0656                                        << (QList<RangeInString>() << RangeInString(7, 10) << RangeInString(15, 18)
0657                                            << RangeInString(18, 21) << RangeInString(31, 34) << RangeInString(35, 40)
0658                                            << RangeInString(46, 49));
0659 }
0660 
0661 QString repeat_distinct(const QString& code, int count) {
0662     QString result;
0663     QString line;
0664     for ( int i = 0; i < count; i++ ) {
0665         line = code;
0666         result.append(line.replace(QString("%X"), QString::number(i)));
0667     }
0668     return result;
0669 }
0670 
0671 void PyCompletionTest::completionBenchTest()
0672 {
0673     QFETCH(QString, completionCode);
0674     QFETCH(QString, invokeCode);
0675 
0676     CompletionParameters data = prepareCompletion(completionCode, invokeCode);
0677     QBENCHMARK {
0678         runCompletion(data);
0679     }
0680 }
0681 
0682 void PyCompletionTest::completionBenchTest_data()
0683 {
0684     QTest::addColumn<QString>("completionCode");
0685     QTest::addColumn<QString>("invokeCode");
0686 
0687     QTest::newRow("variable_completion") << "a0 = 2\n%INVOKE" << "b = a%CURSOR";
0688     QTest::newRow("no_items") << "%INVOKE" << "def func(%CURSOR";
0689     QTest::newRow("function") << "%INVOKE" << "foo(%CURSOR";
0690     QTest::newRow("deep_function") << "foo(bar(baz(bang([]%INVOKE))))" << ".%CURSOR";
0691     QTest::newRow("class_completion") << "class my(): pass\nd = my()\n%INVOKE" << "d.%CURSOR";
0692 
0693     QString many_globals = repeat_distinct("a%X=%X\n", 1000);
0694 
0695     QTest::newRow("function_many_globals") << many_globals + "%INVOKE" << "foo(%CURSOR";
0696     QTest::newRow("variable_completion_many_globals") << many_globals + "%INVOKE" << "b = a%CURSOR";
0697 }
0698 
0699 }
0700 
0701 #include "moc_pycompletiontest.cpp"