1 /*
2 SPDX-FileCopyrightText: 2012 Sven Brauch <svenbrauch@googlemail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "pycompletiontest.h"
8
9 #include <language/backgroundparser/backgroundparser.h>
10 #include <language/codecompletion/codecompletiontesthelper.h>
11 #include <language/duchain/declaration.h>
12 #include <language/codegen/coderepresentation.h>
13 #include <language/duchain/duchain.h>
14 #include <interfaces/ilanguagecontroller.h>
15
16 #include <tests/testcore.h>
17 #include <tests/autotestshell.h>
18
19 #include <ktexteditor_version.h>
20 #include <KTextEditor/Editor>
21 #include <KService>
22
23 #include "codecompletion/context.h"
24 #include "codecompletion/helpers.h"
25 #include "codecompletiondebug.h"
26
27 #include <QDebug>
28 #include <QStandardPaths>
29 #include <QTest>
30
31 using namespace KDevelop;
32
33 QTEST_MAIN(Python::PyCompletionTest)
34
35 Q_DECLARE_METATYPE(QList<Python::RangeInString>)
36
37 static int testId = 0;
38 static QString basepath = "/tmp/__kdevpythoncompletiontest.dir/";
39
40 namespace Python {
41
fakeModel()42 QStandardItemModel& fakeModel() {
43 static QStandardItemModel model;
44 model.setColumnCount(10);
45 model.setRowCount(10);
46 return model;
47 }
48
filenameForTestId(const int id)49 QString filenameForTestId(const int id) {
50 return basepath + "test_" + QString::number(id) + ".py";
51 }
52
nextFilename()53 QString nextFilename() {
54 testId += 1;
55 return filenameForTestId(testId);
56 }
57
PyCompletionTest(QObject * parent)58 PyCompletionTest::PyCompletionTest(QObject* parent) : QObject(parent)
59 {
60 initShell();
61 }
62
makefile(QString filename,QString contents)63 void makefile(QString filename, QString contents) {
64 QFile fileptr;
65 fileptr.setFileName(basepath + filename);
66 fileptr.open(QIODevice::WriteOnly);
67 fileptr.write(contents.toUtf8());
68 fileptr.close();
69 auto url = QUrl::fromLocalFile(QDir::cleanPath(basepath + filename));
70 qCDebug(KDEV_PYTHON_CODECOMPLETION) << "updating duchain for " << url.url() << basepath;
71 const IndexedString urlstring(url);
72 DUChain::self()->updateContextForUrl(urlstring, KDevelop::TopDUContext::ForceUpdate);
73 ICore::self()->languageController()->backgroundParser()->parseDocuments();
74 DUChain::self()->waitForUpdate(urlstring, KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
75 }
76
initShell()77 void PyCompletionTest::initShell()
78 {
79 AutoTestShell::init();
80 TestCore* core = new TestCore();
81 core->initialize(KDevelop::Core::NoUi);
82 QDir d;
83 d.mkpath(basepath);
84
85 auto doc_url = QDir::cleanPath(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
86 "kdevpythonsupport/documentation_files/builtindocumentation.py"));
87
88 DUChain::self()->updateContextForUrl(IndexedString(doc_url), KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
89 ICore::self()->languageController()->backgroundParser()->parseDocuments();
90 DUChain::self()->waitForUpdate(IndexedString(doc_url), KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
91
92 DUChain::self()->disablePersistentStorage();
93 KDevelop::CodeRepresentation::setDiskChangesForbidden(true);
94
95 // now, create a nice little completion hierarchy
96 d.mkpath(basepath + "submoduledir");
97 d.mkpath(basepath + "submoduledir/anothersubdir");
98 makefile("toplevelmodule.py", "some_var = 3\ndef some_function(): pass\nclass some_class():\n def method(): pass");
99 makefile("submoduledir/__init__.py", "var_in_sub_init = 5");
100 makefile("submoduledir/subfile.py", "var_in_subfile = 5\nclass some_subfile_class():\n def method2(): pass");
101 makefile("submoduledir/anothersubdir/__init__.py", "var_in_subsub_init = 5");
102 makefile("submoduledir/anothersubdir/subsubfile.py", "var_in_subsubfile = 5\nclass another_subfile_class():"
103 "\n def method3(): pass");
104 }
105
testIdentifierMatching()106 void PyCompletionTest::testIdentifierMatching()
107 {
108 QCOMPARE(camelCaseToUnderscore("FooBarBaz").toUtf8().data(), "foo_bar_baz");
109 QCOMPARE(camelCaseToUnderscore("fooBarbaz").toUtf8().data(), "foo_barbaz");
110
111 QCOMPARE(identifierMatchQuality("foobar", "foobar"), 3);
112 QCOMPARE(identifierMatchQuality("foobar", "bar"), 2);
113 QCOMPARE(identifierMatchQuality("bar", "foobar"), 2);
114 QCOMPARE(identifierMatchQuality("foobarbaz", "bar"), 2);
115 QCOMPARE(identifierMatchQuality("bar", "foobarbaz"), 2);
116 QCOMPARE(identifierMatchQuality("FoobarBaz", "FoobarBang"), 1);
117 QCOMPARE(identifierMatchQuality("Foobar_Baz", "Foobar_Bang"), 1);
118 QCOMPARE(identifierMatchQuality("xydsf", "qkigfb"), 0);
119 QCOMPARE(identifierMatchQuality("ac_ac", "ac_ae"), 0);
120 QCOMPARE(identifierMatchQuality("AcAb", "AbDe"), 0);
121 }
122
testExpressionParserMisc()123 void PyCompletionTest::testExpressionParserMisc()
124 {
125 // in completion, strings are filtered out and never contain " or ' chars.
126 ExpressionParser p("foobar(3, \"some_string\", func(), funcfunc(3, 5), \t");
127 bool ok;
128 int expressionsSkipped = 0;
129 p.skipUntilStatus(ExpressionParser::EventualCallFound, &ok, &expressionsSkipped);
130 QVERIFY(ok);
131 QCOMPARE(expressionsSkipped, 4); // number of params
132 QCOMPARE(p.getRemainingCode(), QString("foobar"));
133 ExpressionParser::Status s;
134 QString calledFunction = p.popExpression(&s);
135 QVERIFY(s == ExpressionParser::ExpressionFound);
136 QCOMPARE(calledFunction, QString("foobar"));
137
138 ExpressionParser q("hello(world, foo.bar[3].foobar(3, \"some_string\", func(), funcfunc(3, 5), \t");
139 q.skipUntilStatus(ExpressionParser::EventualCallFound, &ok, &expressionsSkipped);
140 QVERIFY(ok);
141 QCOMPARE(expressionsSkipped, 4);
142 QCOMPARE(q.getRemainingCode(), QString("hello(world, foo.bar[3].foobar"));
143 calledFunction = q.popExpression(&s);
144 QCOMPARE(s, ExpressionParser::ExpressionFound);
145 QCOMPARE(calledFunction, QString("foo.bar[3].foobar"));
146 }
147
testExpressionParser()148 void PyCompletionTest::testExpressionParser()
149 {
150 QFETCH(QString, data);
151 QFETCH(int, expectedStatus);
152 QFETCH(QString, expectedExpression);
153
154 ExpressionParser p(data);
155 ExpressionParser::Status status;
156 QString result = p.popExpression(&status);
157 QCOMPARE((int) status, expectedStatus);
158 QCOMPARE(result, expectedExpression);
159 }
160
testExpressionParser_data()161 void PyCompletionTest::testExpressionParser_data()
162 {
163 QTest::addColumn<QString>("data");
164 QTest::addColumn<int>("expectedStatus");
165 QTest::addColumn<QString>("expectedExpression");
166
167 QTest::newRow("attrExpression") << "foo.bar.baz" << (int) ExpressionParser::ExpressionFound << "foo.bar.baz";
168 QTest::newRow("attrExpressionAccess") << "foo.bar.baz." << (int) ExpressionParser::MemberAccessFound << "";
169 QTest::newRow("attrExpressionCall") << "foo.bar(3, 5, 7, hell0(3)).baz" << (int) ExpressionParser::ExpressionFound << "foo.bar(3, 5, 7, hell0(3)).baz";
170 QTest::newRow("nextArg") << "foo(3, 5, \t" << (int) ExpressionParser::CommaFound << "";
171 QTest::newRow("call") << "fo0barR( \t " << (int) ExpressionParser::EventualCallFound << "";
172 QTest::newRow("initializer") << "my_list = [" << (int) ExpressionParser::InitializerFound << "";
173 QTest::newRow("fancy_initializer") << "my_list = [1, 2, 3, 4, []" << (int) ExpressionParser::ExpressionFound << "[]";
174 QTest::newRow("def") << "def " << (int) ExpressionParser::DefFound << "";
175 }
176
invokeCompletionOn(const QString & initCode,const QString & invokeCode)177 const QList<CompletionTreeItem*> PyCompletionTest::invokeCompletionOn(const QString& initCode, const QString& invokeCode)
178 {
179 CompletionParameters data = prepareCompletion(initCode, invokeCode);
180 return runCompletion(data);
181 }
182
prepareCompletion(const QString & initCode,const QString & invokeCode)183 const CompletionParameters PyCompletionTest::prepareCompletion(const QString& initCode, const QString& invokeCode)
184 {
185 CompletionParameters completion_data;
186
187 QString filename = nextFilename();
188 QFile fileptr(filename);
189 fileptr.open(QIODevice::WriteOnly);
190 fileptr.write(initCode.toUtf8().replace("%INVOKE", ""));
191 fileptr.close();
192
193 DUChain::self()->updateContextForUrl(IndexedString(filename), KDevelop::TopDUContext::ForceUpdate);
194 ICore::self()->languageController()->backgroundParser()->parseDocuments();
195 ReferencedTopDUContext topContext = DUChain::self()->waitForUpdate(IndexedString(filename),
196 KDevelop::TopDUContext::AllDeclarationsAndContexts);
197
198 Q_ASSERT(topContext);
199
200 Q_ASSERT(initCode.indexOf("%INVOKE") != -1);
201 QString copy = initCode;
202 QString allCode = copy.replace("%INVOKE", invokeCode);
203
204 QStringList lines = allCode.split('\n');
205 completion_data.cursorAt = CursorInRevision::invalid();
206 for ( int i = 0; i < lines.length(); i++ ) {
207 int j = lines.at(i).indexOf("%CURSOR");
208 if ( j != -1 ) {
209 completion_data.cursorAt = CursorInRevision(i, j);
210 break;
211 }
212 }
213 Q_ASSERT(completion_data.cursorAt.isValid());
214 // codeCompletionContext only gets passed the text until the place where completion is invoked
215 completion_data.snip = allCode.mid(0, allCode.indexOf("%CURSOR"));
216 completion_data.remaining = allCode.mid(allCode.indexOf("%CURSOR") + 7);
217
218 DUChainReadLocker lock;
219 completion_data.contextAtCursor = DUContextPointer(topContext->findContextAt(completion_data.cursorAt, true));
220 Q_ASSERT(completion_data.contextAtCursor);
221
222 return completion_data;
223 }
224
runCompletion(const CompletionParameters parameters)225 const QList<CompletionTreeItem*> PyCompletionTest::runCompletion(const CompletionParameters parameters)
226 {
227 PythonCodeCompletionContext* context = new PythonCodeCompletionContext(parameters.contextAtCursor, parameters.snip, parameters.remaining, parameters.cursorAt, 0, nullptr);
228 bool abort = false;
229 QList<CompletionTreeItem*> items;
230 foreach ( CompletionTreeItemPointer ptr, context->completionItems(abort, true) ) {
231 items << ptr.data();
232 // those are leaked, but it's only a few kb while the tests are running. who cares.
233 m_ptrs << ptr;
234 }
235 return items;
236 }
237
containsItemForDeclarationNamed(const QList<CompletionTreeItem * > items,QString itemName)238 bool PyCompletionTest::containsItemForDeclarationNamed(const QList<CompletionTreeItem*> items, QString itemName)
239 {
240 foreach ( const CompletionTreeItem* ptr, items ) {
241 if ( ptr->declaration() ) {
242 if ( ptr->declaration()->identifier().toString() == itemName ) {
243 return true;
244 }
245 }
246 }
247 return false;
248 }
249
containsItemStartingWith(const QList<CompletionTreeItem * > items,const QString & itemName)250 bool PyCompletionTest::containsItemStartingWith(const QList<CompletionTreeItem*> items, const QString& itemName)
251 {
252 QModelIndex idx = fakeModel().index(0, KDevelop::CodeCompletionModel::Name);
253 foreach ( const CompletionTreeItem* ptr, items ) {
254 if ( ptr->data(idx, Qt::DisplayRole, nullptr).toString().startsWith(itemName) ) {
255 return true;
256 }
257 }
258 return false;
259 }
260
itemInCompletionList(const QString & initCode,const QString & invokeCode,QString itemName)261 bool PyCompletionTest::itemInCompletionList(const QString& initCode, const QString& invokeCode, QString itemName)
262 {
263 QList< CompletionTreeItem* > items = invokeCompletionOn(initCode, invokeCode);
264 return containsItemStartingWith(items, itemName);
265 }
266
declarationInCompletionList(const QString & initCode,const QString & invokeCode,QString itemName)267 bool PyCompletionTest::declarationInCompletionList(const QString& initCode, const QString& invokeCode, QString itemName)
268 {
269 QList< CompletionTreeItem* > items = invokeCompletionOn(initCode, invokeCode);
270 return containsItemForDeclarationNamed(items, itemName);
271 }
272
completionListIsEmpty(const QString & initCode,const QString & invokeCode)273 bool PyCompletionTest::completionListIsEmpty(const QString& initCode, const QString& invokeCode)
274 {
275 return invokeCompletionOn(initCode, invokeCode).isEmpty();
276 }
277
testImportCompletion()278 void PyCompletionTest::testImportCompletion()
279 {
280 QFETCH(QString, invokeCode);
281 QFETCH(QString, completionCode);
282 QFETCH(QString, expectedItem);
283
284 if ( expectedItem == "EMPTY" ) {
285 QVERIFY(completionListIsEmpty(invokeCode, completionCode));
286 }
287 else {
288 QVERIFY(itemInCompletionList(invokeCode, completionCode, expectedItem));
289 }
290 }
291
testImportCompletion_data()292 void PyCompletionTest::testImportCompletion_data()
293 {
294 QTest::addColumn<QString>("invokeCode");
295 QTest::addColumn<QString>("completionCode");
296 QTest::addColumn<QString>("expectedItem");
297
298 QTest::newRow("same_directory") << "%INVOKE" << "import %CURSOR" << "toplevelmodule";
299 // QTest::newRow("same_directory_beginText") << "%INVOKE" << "import toplevelmo%CURSOR" << "toplevelmodule";
300 QTest::newRow("nocompletion") << "%INVOKE" << "from toplevelmodule %CURSOR" << "EMPTY";
301 QTest::newRow("subdirectory_full") << "%INVOKE" << "import %CURSOR" << "submoduledir";
302 QTest::newRow("subdirectory_file") << "%INVOKE" << "import submoduledir.%CURSOR" << "subfile";
303 QTest::newRow("subsubdirectory_file") << "%INVOKE" << "import submoduledir.anothersubdir.%CURSOR" << "subsubfile";
304 QTest::newRow("subdirectory_from") << "%INVOKE" << "from submoduledir import %CURSOR" << "subfile";
305 QTest::newRow("subdirectory_declfromfile") << "%INVOKE" << "from submoduledir.subfile import %CURSOR" << "var_in_subfile";
306 QTest::newRow("declaration_from_init_subdir") << "%INVOKE" << "from submoduledir import %CURSOR" << "var_in_sub_init";
307 QTest::newRow("class_from_file") << "%INVOKE" << "from toplevelmodule import %CURSOR" << "some_class";
308 // TODO implement this or not? It breaks the possibility to easily document modules like PyQT.
309 // maybe enable this behaviour only for doc files?
310 // QTest::newRow("class_property_not") << "%INVOKE" << "import toplevelmodule.some_class.%CURSOR" << "EMPTY";
311 QTest::newRow("class_from_file_in_subdir") << "%INVOKE" << "from submoduledir.subfile import %CURSOR" << "some_subfile_class";
312 }
313
testCompletionAfterQuotes()314 void PyCompletionTest::testCompletionAfterQuotes()
315 {
316 QFETCH(QString, invokeCode);
317 QFETCH(QString, completionCode);
318 invokeCode = "testvar = 3\n" + invokeCode;
319 QVERIFY( ! completionListIsEmpty(invokeCode, completionCode) );
320 }
321
322
testCompletionAfterQuotes_data()323 void PyCompletionTest::testCompletionAfterQuotes_data()
324 {
325 QTest::addColumn<QString>("invokeCode");
326 QTest::addColumn<QString>("completionCode");
327
328 QTest::newRow("nothing") << "\n%INVOKE" << "%CURSOR";
329 QTest::newRow("sq_in_string") << "\"foo'bar\"\n%INVOKE" << "%CURSOR";
330 QTest::newRow("sq_in_sl_comment") << "#foo'bar\n%INVOKE" << "%CURSOR";
331 QTest::newRow("sq_in_ml_string") << "\"\"\"foo'bar\n\n' \n'\"\"\"\n%INVOKE" << "%CURSOR";
332 QTest::newRow("dq_in_string") << "'foo\"bar'\n%INVOKE" << "%CURSOR";
333 QTest::newRow("dq_in_comment") << "# \" foo\n%INVOKE" << "%CURSOR";
334 QTest::newRow("dq_in_ml_string") << "\'\'\'foo \n\"\n\n \'\'\'\n%INVOKE" << "%CURSOR";
335 }
336
testNoImplicitMagicFunctions()337 void PyCompletionTest::testNoImplicitMagicFunctions()
338 {
339 QVERIFY(! itemInCompletionList("class my(): pass\nd = my()\n%INVOKE", "d.%CURSOR", "__get__") );
340 QEXPECT_FAIL("", "Sorting needs to be fixed first before magic function completion can be re-enabled", Continue);
341 QVERIFY(itemInCompletionList("class my():\n def __get__(self): pass\nd = my()\n%INVOKE", "d.%CURSOR", "__get__") );
342 }
343
testIntegralTypesImmediate()344 void PyCompletionTest::testIntegralTypesImmediate()
345 {
346 QFETCH(QString, invokeCode);
347 QFETCH(QString, completionCode);
348 QFETCH(QString, expectedDeclaration);
349
350 QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedDeclaration));
351 }
352
testIntegralTypesImmediate_data()353 void PyCompletionTest::testIntegralTypesImmediate_data()
354 {
355 QTest::addColumn<QString>("invokeCode");
356 QTest::addColumn<QString>("completionCode");
357 QTest::addColumn<QString>("expectedDeclaration");
358
359 QTest::newRow("list_syntax") << "[]%INVOKE" << ".%CURSOR" << "append";
360 QTest::newRow("dict_syntax") << "{}%INVOKE" << ".%CURSOR" << "items";
361 QTest::newRow("string_syntax") << "\"\"%INVOKE" << ".%CURSOR" << "capitalize";
362 QTest::newRow("list_class") << "list()%INVOKE" << ".%CURSOR" << "append";
363 QTest::newRow("dict_class") << "dict()%INVOKE" << ".%CURSOR" << "items";
364 QTest::newRow("string_class") << "str()%INVOKE" << ".%CURSOR" << "capitalize";
365 }
366
testIntegralExpressionsDifferentContexts()367 void PyCompletionTest::testIntegralExpressionsDifferentContexts()
368 {
369 QFETCH(QString, invokeCode);
370 QFETCH(QString, completionCode);
371 QFETCH(QString, expectedDeclaration);
372
373 QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedDeclaration));
374 }
375
testIntegralExpressionsDifferentContexts_data()376 void PyCompletionTest::testIntegralExpressionsDifferentContexts_data()
377 {
378 QTest::addColumn<QString>("invokeCode");
379 QTest::addColumn<QString>("completionCode");
380 QTest::addColumn<QString>("expectedDeclaration");
381
382 QTest::newRow("function_call") << "foo([]%INVOKE)" << ".%CURSOR" << "append";
383 QTest::newRow("function_call_multi") << "foo(bar(baz(bang([]%INVOKE))))" << ".%CURSOR" << "append";
384 QTest::newRow("empty_list") << "[[]%INVOKE]" << ".%CURSOR" << "append";
385 QTest::newRow("list") << "[1, 2, 3, 4, 5, []%INVOKE]" << ".%CURSOR" << "append";
386 QTest::newRow("list_with_fancy_string") << "[\"FooFObar\\\", 3)\", []%INVOKE]" << ".%CURSOR" << "append";
387 QTest::newRow("empty_dict") << "{[]%INVOKE}" << ".%CURSOR" << "append";
388 QTest::newRow("print_stmt") << "%INVOKE" << "print([].%CURSOR" << "append";
389 }
390
testIgnoreCommentSignsInStringLiterals()391 void PyCompletionTest::testIgnoreCommentSignsInStringLiterals()
392 {
393 QVERIFY( ! completionListIsEmpty("'#'%INVOKE", ".%CURSOR") );
394 QVERIFY( ! completionListIsEmpty("def addEntry(self,array):\n"
395 " \"\"\"\"some comment\"\"\"\n %INVOKE", "%CURSOR") );
396 }
397
testNoCompletionInCommentsOrStrings()398 void PyCompletionTest::testNoCompletionInCommentsOrStrings()
399 {
400 QFETCH(QString, invokeCode);
401 QFETCH(QString, completionCode);
402
403 QVERIFY(! declarationInCompletionList(invokeCode, completionCode, "append"));
404 }
405
testNoCompletionInCommentsOrStrings_data()406 void PyCompletionTest::testNoCompletionInCommentsOrStrings_data()
407 {
408 QTest::addColumn<QString>("invokeCode");
409 QTest::addColumn<QString>("completionCode");
410
411 QTest::newRow("single_comment") << "# []%INVOKE" << ".%CURSOR";
412 QTest::newRow("single_comment_local") << "local=3\n# %INVOKE" << "%CURSOR";
413 QTest::newRow("stringDQ") << "\"[]%INVOKE\"" << ".%CURSOR";
414 QTest::newRow("stringSQ") << "\'[]%INVOKE\'" << ".%CURSOR";
415 QTest::newRow("multilineDQ") << "\"\"\"[]%INVOKE\"\"\"" << ".%CURSOR";
416 QTest::newRow("multilineSQ") << "\'\'\'[]%INVOKE\'\'\'" << ".%CURSOR";
417 QTest::newRow("multilineDQ_newlines") << "\"\"\"\n\n[]%INVOKE\n\n\n\"\"\"" << ".%CURSOR";
418 QTest::newRow("multilineSQ_newlines") << "\'\'\'\n\n[]%INVOKE\n\n\n\'\'\'" << ".%CURSOR";
419 }
420
testImplementMethodCompletion()421 void PyCompletionTest::testImplementMethodCompletion()
422 {
423 QFETCH(QString, invokeCode);
424 QFETCH(QString, completionCode);
425 QVERIFY(itemInCompletionList(invokeCode, completionCode, "__init__"));
426 }
427
testImplementMethodCompletion_data()428 void PyCompletionTest::testImplementMethodCompletion_data()
429 {
430 QTest::addColumn<QString>("invokeCode");
431 QTest::addColumn<QString>("completionCode");
432
433 QTest::newRow("simple_begin") << "class myclass():\n %INVOKE\n pass" << "def %CURSOR";
434 QTest::newRow("another_method_before") << "class myclass():\n def some_method(param):pass\n %INVOKE" << "def %CURSOR";
435 QTest::newRow("another_method_before_multiline") << "class myclass():\n def some_method(param):\n pass\n pass \n pass"
436 "\n %INVOKE" << "def %CURSOR";
437 QTest::newRow("contextskip") << "class myclass():\n def some_method(param):\n pass\n \n \n \n %INVOKE" << "def %CURSOR";
438 QTest::newRow("contextskip2") << "class myclass():\n def some_method(param): pass\n"
439 " def some_method2(param):\n pass\n pass\n %INVOKE" << "\n \n \n def %CURSOR";
440 }
441
testAutoBrackets()442 void PyCompletionTest::testAutoBrackets()
443 {
444 QList< CompletionTreeItem* > items = invokeCompletionOn("class Foo:\n @property\n def myprop(self): pass\n"
445 "a=Foo()\n%INVOKE", "a.%CURSOR");
446 QVERIFY(containsItemForDeclarationNamed(items, "myprop"));
447 CompletionTreeItem* item = nullptr;
448 foreach ( CompletionTreeItem* ptr, items ) {
449 if ( ptr->declaration() ) {
450 if ( ptr->declaration()->identifier().toString() == "myprop" ) {
451 item = ptr;
452 break;
453 }
454 }
455 }
456 QVERIFY(item);
457 KService::Ptr documentService = KService::serviceByDesktopPath("katepart.desktop");
458 QVERIFY(documentService);
459 KTextEditor::Document* document = documentService->createInstance<KTextEditor::Document>(this);
460 auto view = document->createView(nullptr);
461 QVERIFY(document);
462 item->execute(view, KTextEditor::Range(0, 0, 0, 0));
463 QCOMPARE(document->text(), QLatin1String("myprop"));
464 }
465
testExceptionCompletion()466 void PyCompletionTest::testExceptionCompletion()
467 {
468 QList< CompletionTreeItem* > items = invokeCompletionOn("localvar = 3\nraise %INVOKE", "%CURSOR");
469 QVERIFY(containsItemForDeclarationNamed(items, "Exception"));
470 QVERIFY(! containsItemForDeclarationNamed(items, "localvar"));
471
472 items = invokeCompletionOn("localvar = 3\n%INVOKE", "try: pass\nexcept %CURSOR");
473 QVERIFY(containsItemForDeclarationNamed(items, "Exception"));
474 QVERIFY(! containsItemForDeclarationNamed(items, "localvar"));
475 }
476
testGeneratorCompletion()477 void PyCompletionTest::testGeneratorCompletion()
478 {
479 QVERIFY(itemInCompletionList("%INVOKE", "foobar = [item for %CURSOR", "item in"));
480 QVERIFY(itemInCompletionList("%INVOKE", "foobar = [key, value for %CURSOR", "key, value in"));
481 QVERIFY(itemInCompletionList("%INVOKE", "foobar = [str(key + value) for %CURSOR", "key, value in"));
482 QVERIFY(itemInCompletionList("%INVOKE\ndec_l8r=3", "foobar = [dec_l8r for %CURSOR", "dec_l8r in"));
483 }
484
testInheritanceCompletion()485 void PyCompletionTest::testInheritanceCompletion()
486 {
487 QList< CompletionTreeItem* > items = invokeCompletionOn("class parentClass: pass\n%INVOKE", "class childClass(%CURSOR");
488 QVERIFY(containsItemForDeclarationNamed(items, "parentClass"));
489 items = invokeCompletionOn("class parentClass: pass\nclass childClass(%INVOKE): pass", "%CURSOR");
490 QVERIFY(containsItemForDeclarationNamed(items, "parentClass"));
491 items = invokeCompletionOn("class parentClass:\n class blubb: pass\nclass childClass(%INVOKE): pass", "parentClass.%CURSOR");
492 QVERIFY(! containsItemForDeclarationNamed(items, "parentClass"));
493 QVERIFY(containsItemForDeclarationNamed(items, "blubb"));
494 }
495
testAddImportCompletion()496 void PyCompletionTest::testAddImportCompletion()
497 {
498 QFETCH(QString, completionCode);
499 QFETCH(QString, invokeCode);
500 QFETCH(int, expectedItems);
501
502 QCOMPARE(invokeCompletionOn(completionCode, invokeCode).size(), expectedItems);
503 }
504
testAddImportCompletion_data()505 void PyCompletionTest::testAddImportCompletion_data()
506 {
507 QTest::addColumn<QString>("completionCode");
508 QTest::addColumn<QString>("invokeCode");
509 QTest::addColumn<int>("expectedItems");
510
511 QTest::newRow("has_entry_when_necessary") << "toplevelmodule%INVOKE" << ".%CURSOR" << 1;
512 QTest::newRow("has_no_when_not_necessary") << "toplevelmodule = 3;\ntoplevelmodule%INVOKE" << ".%CURSOR" << 0;
513 }
514
testFunctionDeclarationCompletion()515 void PyCompletionTest::testFunctionDeclarationCompletion()
516 {
517 QFETCH(QString, completionCode);
518 QFETCH(QString, invokeCode);
519 QFETCH(KTextEditor::Range, executeRange);
520 QFETCH(QString, expectedReplacement);
521
522 QString documentCode = completionCode;
523 documentCode.replace("%INVOKE", invokeCode).replace("%CURSOR", "");
524
525 QString expectedCode = completionCode;
526 expectedCode.replace("%INVOKE", expectedReplacement);
527
528 const QList<CompletionTreeItem *> completionItems = invokeCompletionOn(completionCode, invokeCode);
529
530 QVERIFY( ! completionItems.isEmpty() );
531
532 KService::Ptr documentService = KService::serviceByDesktopPath("katepart.desktop");
533 QVERIFY(documentService);
534 KTextEditor::Document* document = documentService->createInstance<KTextEditor::Document>(this);
535 QVERIFY(document);
536 document->setText(documentCode);
537
538 auto view = document->createView(nullptr);
539
540 completionItems.first()->execute(view, executeRange);
541 QCOMPARE(document->text(), expectedCode);
542 }
543
testFunctionDeclarationCompletion_data()544 void PyCompletionTest::testFunctionDeclarationCompletion_data()
545 {
546 QTest::addColumn<QString>("completionCode");
547 QTest::addColumn<QString>("invokeCode");
548 QTest::addColumn<KTextEditor::Range>("executeRange");
549 QTest::addColumn<QString>("expectedReplacement");
550
551 QTest::newRow("func_decl_no_parens") << "def foo():\n pass\n%INVOKE" << "foo%CURSOR" << KTextEditor::Range(2, 0, 2, 3)
552 << "foo()";
553
554 QTest::newRow("func_decl_existing_parens") << "def foo():\n return 0\nbar = %INVOKE" << "foo%CURSOR()" << KTextEditor::Range(2, 6, 2, 9)
555 << "foo()";
556
557 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)
558 << "@mydecorator";
559
560 QTest::newRow("class_name_no_constructor_parens") << "class Foo:\n pass\nbar = %INVOKE" << "Foo%CURSOR" << KTextEditor::Range(2, 6, 2, 9)
561 << "Foo()";
562
563 QTest::newRow("class_name_explicit_constructor_parens") << "class Foo:\n def __init__(self):\n pass\nbar = %INVOKE" << "Fo%CURSOR"
564 << KTextEditor::Range(3, 6, 3, 9)
565 << "Foo()";
566 }
567
testCompletionScopes()568 void PyCompletionTest::testCompletionScopes()
569 {
570 QFETCH(QString, invokeCode);
571 QFETCH(QString, completionCode);
572 QFETCH(QString, expectedDeclaration);
573 QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedDeclaration));
574 }
575
testCompletionScopes_data()576 void PyCompletionTest::testCompletionScopes_data()
577 {
578 QTest::addColumn<QString>("invokeCode");
579 QTest::addColumn<QString>("completionCode");
580 QTest::addColumn<QString>("expectedDeclaration");
581 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";
582 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";
583 }
584
testStringFormattingCompletion()585 void PyCompletionTest::testStringFormattingCompletion()
586 {
587 QFETCH(QString, completionCode);
588 QFETCH(QString, invokeCode);
589 QFETCH(QString, expectedItem);
590 QFETCH(bool, expectedPresent);
591
592 QCOMPARE(itemInCompletionList(completionCode, invokeCode, expectedItem), expectedPresent);
593 }
594
testStringFormattingCompletion_data()595 void PyCompletionTest::testStringFormattingCompletion_data()
596 {
597 QTest::addColumn<QString>("completionCode");
598 QTest::addColumn<QString>("invokeCode");
599 QTest::addColumn<QString>("expectedItem");
600 QTest::addColumn<bool>("expectedPresent");
601
602 QTest::newRow("sq_string") << "'foo %INVOKE'" << "%CURSOR" << "{0}" << true;
603 QTest::newRow("dq_string") << "\"foo %INVOKE bar\"" << "%CURSOR" << "{0}" << true;
604 QTest::newRow("sq_ml_string") << "'''foo\n\n%INVOKE\nbar'''" << "%CURSOR" << "{0}" << true;
605 QTest::newRow("dq_ml_string") << "\"\"\"foo\nbar %INVOKE baz\"\"\"" << "%CURSOR" << "{0}" << true;
606 QTest::newRow("auto_id") << "\"foo {0} bar {1} baz %INVOKE\"" << "%CURSOR" << "{2}" << true;
607
608 QTest::newRow("format_suggestions_conversion") << "\"foo {0} bar %INVOKE\"" << "{1}%CURSOR" << "{1!r}" << true;
609 QTest::newRow("format_suggestions_spec") << "\"foo {0} bar {1}%INVOKE baz\"" << "{2}%CURSOR" << "{2:%}" << true;
610
611 QTest::newRow("incompatible_suggestions_conversion") << "\"foo %INVOKE bar\"" << "{0:%}%CURSOR" << "{0!s:%}" << false;
612 QTest::newRow("incompatible_suggestions_spec") << "\"foo %INVOKE bar\"" << "{0!s}%CURSOR" << "{0!s:%}" << false;
613
614 QTest::newRow("alignment_for_strings") << "\"foo %INVOKE bar\"" << "{0!s}%CURSOR" << "{0!s:^${width}}" << true;
615 QTest::newRow("conversion_for_aliged_strings") << "\"foo %INVOKE bar\"" << "{0!s}%CURSOR" << "{0!s:^${width}}" << true;
616
617 }
618
testStringFormatter()619 void PyCompletionTest::testStringFormatter()
620 {
621 QFETCH(QString, string);
622 QFETCH(int, expectedId);
623 QFETCH(QList<RangeInString>, expectedVariablePositions);
624
625 StringFormatter f(string);
626
627 int id = f.nextIdentifierId();
628 QCOMPARE(id, expectedId);
629
630 for (int i = 0; i < string.size(); i++) {
631 bool expectedInsideVariable = false;
632 foreach (RangeInString range, expectedVariablePositions) {
633 if (i >= range.beginIndex && i <= range.endIndex) {
634 expectedInsideVariable = true;
635 break;
636 }
637 }
638 QCOMPARE(f.isInsideReplacementVariable(i), expectedInsideVariable);
639 }
640 }
641
testStringFormatter_data()642 void PyCompletionTest::testStringFormatter_data()
643 {
644 QTest::addColumn<QString>("string");
645 QTest::addColumn<int>("expectedId");
646 QTest::addColumn<QList<RangeInString> >("expectedVariablePositions");
647
648 QTest::newRow("sl_string") << "\"foo {0} bar {1}\"" << 2
649 << (QList<RangeInString>() << RangeInString(5, 8) << RangeInString(13, 16));
650
651 QTest::newRow("ml_string") << "\"\"\"foo {0} \n\nbar {1} \n{foo} {2}\nbaz\"\"\"" << 3
652 << (QList<RangeInString>() << RangeInString(7, 10) << RangeInString(17, 20)
653 << RangeInString(22, 27) << RangeInString(28, 31));
654
655 QTest::newRow("containing_quotes") << "'''foo {0}\nbar\n{1}{0} \\' \\\" \\\" {2} {foo} \n\\'\n {3}baz'''" << 4
656 << (QList<RangeInString>() << RangeInString(7, 10) << RangeInString(15, 18)
657 << RangeInString(18, 21) << RangeInString(31, 34) << RangeInString(35, 40)
658 << RangeInString(46, 49));
659 }
660
repeat_distinct(const QString & code,int count)661 QString repeat_distinct(const QString& code, int count) {
662 QString result;
663 QString line;
664 for ( int i = 0; i < count; i++ ) {
665 line = code;
666 result.append(line.replace(QString("%X"), QString::number(i)));
667 }
668 return result;
669 }
670
completionBenchTest()671 void PyCompletionTest::completionBenchTest()
672 {
673 QFETCH(QString, completionCode);
674 QFETCH(QString, invokeCode);
675
676 CompletionParameters data = prepareCompletion(completionCode, invokeCode);
677 QBENCHMARK {
678 runCompletion(data);
679 }
680 }
681
completionBenchTest_data()682 void PyCompletionTest::completionBenchTest_data()
683 {
684 QTest::addColumn<QString>("completionCode");
685 QTest::addColumn<QString>("invokeCode");
686
687 QTest::newRow("variable_completion") << "a0 = 2\n%INVOKE" << "b = a%CURSOR";
688 QTest::newRow("no_items") << "%INVOKE" << "def func(%CURSOR";
689 QTest::newRow("function") << "%INVOKE" << "foo(%CURSOR";
690 QTest::newRow("deep_function") << "foo(bar(baz(bang([]%INVOKE))))" << ".%CURSOR";
691 QTest::newRow("class_completion") << "class my(): pass\nd = my()\n%INVOKE" << "d.%CURSOR";
692
693 QString many_globals = repeat_distinct("a%X=%X\n", 1000);
694
695 QTest::newRow("function_many_globals") << many_globals + "%INVOKE" << "foo(%CURSOR";
696 QTest::newRow("variable_completion_many_globals") << many_globals + "%INVOKE" << "b = a%CURSOR";
697 }
698
699 }
700