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