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"