1 /*
2 SPDX-FileCopyrightText: 2008 Manuel Breugelmans <mbr.nxi@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "test_projectcontroller.h"
8
9 #include <QFile>
10 #include <QSignalSpy>
11 #include <QTest>
12
13 #include <KAboutData>
14
15 #include <tests/testhelpers.h>
16 #include <tests/autotestshell.h>
17 #include <tests/testcore.h>
18
19 #include <interfaces/iplugin.h>
20 #include <project/interfaces/iprojectfilemanager.h>
21 #include <project/projectmodel.h>
22 #include <shell/core.h>
23 #include <shell/projectcontroller.h>
24 #include <shell/plugincontroller.h>
25 #include <shell/project.h>
26
27 using namespace KDevelop;
28
29 namespace {
30
31 class DialogProviderFake : public IProjectDialogProvider
32 {
33 Q_OBJECT
34 public:
DialogProviderFake()35 DialogProviderFake()
36 {}
~DialogProviderFake()37 ~DialogProviderFake() override {}
38 bool m_reopen = true;
39
40 public Q_SLOTS:
askProjectConfigLocation(bool,const QUrl &,const QUrl &,IPlugin *)41 QUrl askProjectConfigLocation(bool /*fetch*/, const QUrl& /*startUrl*/,
42 const QUrl& /*repoUrl*/, IPlugin* /*plugin*/) override
43 { return QUrl(); }
userWantsReopen()44 bool userWantsReopen() override { return m_reopen; }
45 };
46
47 }
48
49 /*! A Filemanager plugin that allows you to setup a file & directory structure */
50 class FakeFileManager : public IPlugin, public IProjectFileManager
51 {
52 Q_OBJECT
53 Q_INTERFACES(KDevelop::IProjectFileManager)
54 public:
FakeFileManager(QObject *,const QVariantList &)55 FakeFileManager(QObject*, const QVariantList&)
56 : IPlugin(QStringLiteral("FakeFileManager"), Core::self())
57 {
58 }
59
FakeFileManager()60 FakeFileManager()
61 : IPlugin(QStringLiteral("FakeFileManager"), Core::self())
62 {
63 }
64
~FakeFileManager()65 ~FakeFileManager() override {}
66
features() const67 Features features() const override
68 {
69 return IProjectFileManager::Files | IProjectFileManager::Folders;
70 }
71
72 QMap<Path, Path::List> m_filesInFolder; // initialize
73 QMap<Path, Path::List> m_subFoldersInFolder;
74
75 /*! Setup this manager such that @p folder contains @p file */
addFileToFolder(const Path & folder,const Path & file)76 void addFileToFolder(const Path& folder, const Path& file)
77 {
78 if (!m_filesInFolder.contains(folder)) {
79 m_filesInFolder[folder] = Path::List();
80 }
81 m_filesInFolder[folder] << file;
82 }
83
84 /*! Setup this manager such that @p folder has @p subFolder */
addSubFolderTo(const Path & folder,const Path & subFolder)85 void addSubFolderTo(const Path& folder, const Path& subFolder)
86 {
87 if (!m_subFoldersInFolder.contains(folder)) {
88 m_subFoldersInFolder[folder] = Path::List();
89 }
90 m_subFoldersInFolder[folder] << subFolder;
91 }
92
parse(ProjectFolderItem * dom)93 QList<ProjectFolderItem*> parse(ProjectFolderItem *dom) override
94 {
95 const Path::List files = m_filesInFolder[dom->path()];
96 for (const Path& file : files) {
97 new ProjectFileItem(dom->project(), file, dom);
98 }
99 const Path::List folderPaths = m_subFoldersInFolder[dom->path()];
100 QList<ProjectFolderItem*> folders;
101 for (const Path& folderPath : folderPaths) {
102 folders << new ProjectFolderItem(dom->project(), folderPath, dom);
103 }
104 return folders;
105 }
106
import(IProject * project)107 ProjectFolderItem *import(IProject *project) override
108 {
109 auto* it = new ProjectFolderItem(project, project->path());
110 return it;
111 }
112
addFolder(const Path &,ProjectFolderItem *)113 ProjectFolderItem* addFolder(const Path& /*folder*/, ProjectFolderItem* /*parent*/) override { return nullptr; }
addFile(const Path &,ProjectFolderItem *)114 ProjectFileItem* addFile(const Path& /*file*/, ProjectFolderItem* /*parent*/) override { return nullptr; }
removeFilesAndFolders(const QList<ProjectBaseItem * > &)115 bool removeFilesAndFolders(const QList<ProjectBaseItem*> &/*items*/) override { return false; }
moveFilesAndFolders(const QList<KDevelop::ProjectBaseItem * > &,KDevelop::ProjectFolderItem *)116 bool moveFilesAndFolders(const QList< KDevelop::ProjectBaseItem* > &/*items*/, KDevelop::ProjectFolderItem* /*newParent*/) override { return false; }
copyFilesAndFolders(const Path::List &,KDevelop::ProjectFolderItem *)117 bool copyFilesAndFolders(const Path::List &/*items*/, KDevelop::ProjectFolderItem* /*newParent*/) override { return false; }
renameFile(ProjectFileItem *,const Path &)118 bool renameFile(ProjectFileItem* /*file*/, const Path& /*newPath*/) override { return false; }
renameFolder(ProjectFolderItem *,const Path &)119 bool renameFolder(ProjectFolderItem* /*oldFolder*/, const Path& /*newPath*/ ) override { return false; }
reload(ProjectFolderItem *)120 bool reload(ProjectFolderItem* /*item*/) override { return false; }
121 };
122
123 class FakePluginController : public PluginController
124 {
125 Q_OBJECT
126 public:
127 using PluginController::PluginController;
128
pluginForExtension(const QString & extension,const QString & pluginName={},const QVariantMap & constraints=QVariantMap ())129 IPlugin* pluginForExtension(const QString& extension, const QString& pluginName = {}, const QVariantMap& constraints = QVariantMap()) override
130 {
131 if (extension == qobject_interface_iid<IProjectFileManager*>()) {
132 if (!m_fakeFileManager) {
133 // Can't initialize in the constructor, because the pluginController must be setup
134 // before constructing a plugin, and this _is_ the pluginController.
135 m_fakeFileManager = new FakeFileManager;
136 }
137 return m_fakeFileManager;
138 }
139 return PluginController::pluginForExtension(extension, pluginName, constraints);
140 }
141
142 private:
143 FakeFileManager* m_fakeFileManager = nullptr;
144 };
145
146 ////////////////////// Fixture ///////////////////////////////////////////////
147
initTestCase()148 void TestProjectController::initTestCase()
149 {
150 AutoTestShell::init({{}});
151 auto* testCore = new TestCore;
152 testCore->setPluginController( new FakePluginController(testCore) );
153 testCore->initialize();
154 qRegisterMetaType<KDevelop::IProject*>();
155 m_core = Core::self();
156 m_scratchDir = QDir(QDir::tempPath());
157 m_scratchDir.mkdir(QStringLiteral("prjctrltest"));
158 m_scratchDir.cd(QStringLiteral("prjctrltest"));
159
160 QSignalSpy projectControllerInitializedSpy(m_core->projectControllerInternal(),
161 &ProjectController::initialized);
162 QVERIFY(projectControllerInitializedSpy.wait(100));
163 }
164
cleanupTestCase()165 void TestProjectController::cleanupTestCase()
166 {
167 TestCore::shutdown();
168 }
169
init()170 void TestProjectController::init()
171 {
172 m_projName = QStringLiteral("foo");
173 m_projFilePath = writeProjectConfig(m_projName);
174 m_projCtrl = m_core->projectControllerInternal();
175 m_tmpConfigs << m_projFilePath;
176 m_projFolder = Path(m_scratchDir.absolutePath() + '/');
177 }
178
cleanup()179 void TestProjectController::cleanup()
180 {
181 // also close any opened projects as we do not get a clean fixture,
182 // following tests should start off clean.
183 const auto projects = m_projCtrl->projects();
184 for (IProject* p : projects) {
185 m_projCtrl->closeProject(p);
186 }
187 for (const Path& cfg : qAsConst(m_tmpConfigs)) {
188 QFile::remove(cfg.pathOrUrl());
189 }
190 qDeleteAll(m_fileManagerGarbage);
191 m_fileManagerGarbage.clear();
192 }
193
194 ////////////////////// Commands //////////////////////////////////////////////
195
196 #define WAIT_FOR_OPEN_SIGNAL \
197 {\
198 QSignalSpy signal(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*)));\
199 QVERIFY2(signal.wait(30000), "Timeout while waiting for opened signal");\
200 } void(0)
201
openProject()202 void TestProjectController::openProject()
203 {
204 auto spy = createOpenedSpy();
205 QVERIFY(!m_projCtrl->isProjectNameUsed(m_projName));
206 m_projCtrl->openProject(m_projFilePath.toUrl());
207 WAIT_FOR_OPEN_SIGNAL;
208 QCOMPARE(m_projCtrl->projectCount(), 1);
209 auto* proj = assertProjectOpened(m_projName);
210 assertSpyCaughtProject(spy.get(), proj);
211 QCOMPARE(proj->projectFile(), m_projFilePath);
212 QCOMPARE(proj->path(), Path(m_scratchDir.absolutePath()+'/'));
213 QVERIFY(m_projCtrl->isProjectNameUsed(m_projName));
214 }
215
closeProject()216 void TestProjectController::closeProject()
217 {
218 m_projCtrl->openProject(m_projFilePath.toUrl());
219 WAIT_FOR_OPEN_SIGNAL;
220 IProject* proj = m_projCtrl->findProjectByName(m_projName);
221 Q_ASSERT(proj);
222
223 auto spy1 = createClosedSpy();
224 auto spy2 = createClosingSpy();
225 m_projCtrl->closeProject(proj);
226
227 QVERIFY(!m_projCtrl->isProjectNameUsed(m_projName));
228 QCOMPARE(m_projCtrl->projectCount(), 0);
229 assertProjectClosed(proj);
230 assertSpyCaughtProject(spy1.get(), proj);
231 assertSpyCaughtProject(spy2.get(), proj);
232 }
233
openCloseOpen()234 void TestProjectController::openCloseOpen()
235 {
236 m_projCtrl->openProject(m_projFilePath.toUrl());
237 WAIT_FOR_OPEN_SIGNAL;
238 auto* proj = assertProjectOpened(m_projName);
239 m_projCtrl->closeProject(proj);
240 auto spy = createOpenedSpy();
241 m_projCtrl->openProject(m_projFilePath.toUrl());
242 WAIT_FOR_OPEN_SIGNAL;
243 QVERIFY(m_projCtrl->isProjectNameUsed(m_projName));
244 QCOMPARE(m_projCtrl->projectCount(), 1);
245 proj = assertProjectOpened(m_projName);
246 assertSpyCaughtProject(spy.get(), proj);
247 }
248
reopen()249 void TestProjectController::reopen()
250 {
251 m_projCtrl->setDialogProvider(new DialogProviderFake);
252 m_projCtrl->openProject(m_projFilePath.toUrl());
253 WAIT_FOR_OPEN_SIGNAL;
254 auto spy = createOpenedSpy();
255 m_projCtrl->openProject(m_projFilePath.toUrl());
256 WAIT_FOR_OPEN_SIGNAL;
257 QCOMPARE(m_projCtrl->projectCount(), 1);
258 QVERIFY(m_projCtrl->isProjectNameUsed(m_projName));
259 auto* proj = assertProjectOpened(m_projName);
260 assertSpyCaughtProject(spy.get(), proj);
261 }
262
reopenWhileLoading()263 void TestProjectController::reopenWhileLoading()
264 {
265 // Open the same project again while the first is still
266 // loading. The second open request should be blocked.
267 m_projCtrl->setDialogProvider(new DialogProviderFake);
268 auto spy = createOpenedSpy();
269 m_projCtrl->openProject(m_projFilePath.toUrl());
270 //m_projCtrl->openProject(m_projFilePath.toUrl());
271 WAIT_FOR_OPEN_SIGNAL;
272 // wait a bit for a second signal, this should timeout
273 QSignalSpy signal(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*)));
274 QVERIFY2(!signal.wait(100), "Received 2 projectOpened signals.");
275 QCOMPARE(m_projCtrl->projectCount(), 1);
276 auto* proj = assertProjectOpened(m_projName);
277 assertSpyCaughtProject(spy.get(), proj);
278 }
279
openMultiple()280 void TestProjectController::openMultiple()
281 {
282 QString secondProj(QStringLiteral("bar"));
283 Path secondCfgUrl = writeProjectConfig(secondProj);
284 auto spy = createOpenedSpy();
285 m_projCtrl->openProject(m_projFilePath.toUrl());
286 WAIT_FOR_OPEN_SIGNAL;
287 m_projCtrl->openProject(secondCfgUrl.toUrl());
288 WAIT_FOR_OPEN_SIGNAL;
289
290 QCOMPARE(m_projCtrl->projectCount(), 2);
291 auto* proj1 = assertProjectOpened(m_projName);
292 auto* proj2 = assertProjectOpened(secondProj);
293
294 QVERIFY(m_projCtrl->isProjectNameUsed(m_projName));
295 QVERIFY(m_projCtrl->isProjectNameUsed(QStringLiteral("bar")));
296
297 QCOMPARE(spy->size(), 2);
298 auto* emittedProj1 = (*spy)[0][0].value<IProject*>();
299 auto* emittedProj2 = (*spy)[1][0].value<IProject*>();
300 QCOMPARE(emittedProj1, proj1);
301 QCOMPARE(emittedProj2, proj2);
302
303 m_tmpConfigs << secondCfgUrl;
304 }
305
306 /*! Verify that the projectmodel contains a single project. Put this project's
307 * ProjectFolderItem in the output parameter @p RootItem */
308 #define ASSERT_SINGLE_PROJECT_IN_MODEL(rootItem) \
309 {\
310 QCOMPARE(m_projCtrl->projectModel()->rowCount(), 1); \
311 QModelIndex projIndex = m_projCtrl->projectModel()->index(0,0); \
312 QVERIFY(projIndex.isValid()); \
313 ProjectBaseItem* i = m_projCtrl->projectModel()->itemFromIndex( projIndex ); \
314 QVERIFY(i); \
315 QVERIFY(i->folder()); \
316 rootItem = i->folder();\
317 } void(0)
318
319 /*! Verify that the projectitem @p item has a single child item
320 * named @p name with url @p url. @p subFolder is an output parameter
321 * that contains the sub-folder projectitem. */
322 #define ASSERT_SINGLE_SUBFOLDER_IN(item, name, path__, subFolder) \
323 {\
324 QCOMPARE(item->rowCount(), 1);\
325 QCOMPARE(item->folderList().size(), 1);\
326 ProjectFolderItem* fo = item->folderList().at(0);\
327 QVERIFY(fo);\
328 QCOMPARE(fo->path(), path__);\
329 QCOMPARE(fo->folderName(), QStringLiteral(name));\
330 subFolder = fo;\
331 } void(0)
332
333 #define ASSERT_SINGLE_FILE_IN(rootFolder, name, path__, fileItem)\
334 {\
335 QCOMPARE(rootFolder->rowCount(), 1);\
336 QCOMPARE(rootFolder->fileList().size(), 1);\
337 fileItem = rootFolder->fileList().at(0);\
338 QVERIFY(fileItem);\
339 QCOMPARE(fileItem->path(), path__);\
340 QCOMPARE(fileItem->fileName(), QStringLiteral(name));\
341 } void(0)
342
343 // command
emptyProject()344 void TestProjectController::emptyProject()
345 {
346 // verify that the project model contains a single top-level folder after loading
347 // an empty project
348
349 assertEmptyProjectModel();
350
351 m_projCtrl->openProject(m_projFilePath.toUrl());
352 WAIT_FOR_OPEN_SIGNAL;
353 auto* proj = assertProjectOpened(m_projName);
354
355 FakeFileManager* fileMng = createFileManager();
356 Q_ASSERT(fileMng);
357
358 proj->setManagerPlugin(fileMng);
359 proj->reloadModel();
360 QTest::qWait(100);
361
362 ProjectFolderItem* rootFolder;
363 ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder);
364
365 // check that the project is empty
366 QCOMPARE(rootFolder->rowCount(), 0);
367 QCOMPARE(rootFolder->project()->name(), m_projName);
368 QCOMPARE(rootFolder->path(), m_projFolder);
369 }
370
371 // command
singleFile()372 void TestProjectController::singleFile()
373 {
374 // verify that the project model contains a single file in the
375 // top folder. First setup a FakeFileManager with this file
376
377 m_projCtrl->openProject(m_projFilePath.toUrl());
378 WAIT_FOR_OPEN_SIGNAL;
379 auto* proj = assertProjectOpened(m_projName);
380
381 FakeFileManager* fileMng = createFileManager();
382 proj->setManagerPlugin(fileMng);
383
384 Path filePath = Path(m_projFolder, QStringLiteral("foobar"));
385 fileMng->addFileToFolder(m_projFolder, filePath);
386
387 proj->reloadModel();
388 QTest::qWait(100); // NO signals for reload ...
389
390 ProjectFolderItem* rootFolder;
391 ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder);
392 ProjectFileItem* fi;
393 ASSERT_SINGLE_FILE_IN(rootFolder, "foobar", filePath, fi);
394 QCOMPARE(fi->rowCount(), 0);
395
396 ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder);
397 ASSERT_SINGLE_FILE_IN(rootFolder, "foobar", filePath, fi);
398 }
399
400 // command
singleDirectory()401 void TestProjectController::singleDirectory()
402 {
403 // verify that the project model contains a single folder in the
404 // top folder. First setup a FakeFileManager with this folder
405
406 m_projCtrl->openProject(m_projFilePath.toUrl());
407 WAIT_FOR_OPEN_SIGNAL;
408 auto* proj = assertProjectOpened(m_projName);
409
410 Path folderPath = Path(m_projFolder, QStringLiteral("foobar/"));
411 FakeFileManager* fileMng = createFileManager();
412 fileMng->addSubFolderTo(m_projFolder, folderPath);
413
414 proj->setManagerPlugin(fileMng);
415 proj->reloadModel();
416 QTest::qWait(100);
417
418 ProjectFolderItem* rootFolder;
419 ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder);
420
421 // check that the project contains a single subfolder
422 ProjectFolderItem* sub;
423 ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub);
424 QCOMPARE(sub->rowCount(), 0);
425 }
426
427 // command
fileInSubdirectory()428 void TestProjectController::fileInSubdirectory()
429 {
430 // verify that the project model contains a single file in a subfolder
431 // First setup a FakeFileManager with this folder + file
432
433 m_projCtrl->openProject(m_projFilePath.toUrl());
434 WAIT_FOR_OPEN_SIGNAL;
435 auto* proj = assertProjectOpened(m_projName);
436
437 Path folderPath = Path(m_projFolder, QStringLiteral("foobar/"));
438 FakeFileManager* fileMng = createFileManager();
439 fileMng->addSubFolderTo(m_projFolder, folderPath);
440 Path filePath = Path(folderPath, QStringLiteral("zoo"));
441 fileMng->addFileToFolder(folderPath, filePath);
442
443 proj->setManagerPlugin(fileMng);
444 ProjectFolderItem* rootFolder = nullptr;
445 ProjectFolderItem* sub = nullptr;
446 ProjectFileItem* file = nullptr;
447
448 proj->reloadModel();
449 QTest::qWait(100);
450
451 ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder);
452 ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub);
453 ASSERT_SINGLE_FILE_IN(sub,"zoo",filePath,file);
454
455 ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder);
456 ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub);
457 ASSERT_SINGLE_FILE_IN(sub,"zoo",filePath,file);
458 }
459
prettyFileName_data()460 void TestProjectController::prettyFileName_data()
461 {
462 QTest::addColumn<QString>("relativeFilePath");
463
464 QTest::newRow("basic")
465 << "foobar.txt";
466 QTest::newRow("subfolder")
467 << "sub/foobar.txt";
468 }
469
prettyFileName()470 void TestProjectController::prettyFileName()
471 {
472 QFETCH(QString, relativeFilePath);
473
474 m_projCtrl->openProject(m_projFilePath.toUrl());
475 WAIT_FOR_OPEN_SIGNAL;
476 auto* proj = assertProjectOpened(m_projName);
477
478 FakeFileManager* fileMng = createFileManager();
479 proj->setManagerPlugin(fileMng);
480
481 Path filePath = Path(m_projFolder, relativeFilePath);
482 fileMng->addFileToFolder(m_projFolder, filePath);
483
484 QCOMPARE(m_projCtrl->prettyFileName(filePath.toUrl(), ProjectController::FormattingOptions::FormatPlain), QString(m_projName + ':' + relativeFilePath));
485 }
486
487 ////////////////////// Helpers ///////////////////////////////////////////////
488
writeProjectConfig(const QString & name)489 Path TestProjectController::writeProjectConfig(const QString& name)
490 {
491 Path configPath = Path(m_scratchDir.absolutePath() + '/' + name + ".kdev4");
492 QFile f(configPath.pathOrUrl());
493 f.open(QIODevice::WriteOnly);
494 QTextStream str(&f);
495 str << "[Project]\n"
496 << "Name=" << name << "\n";
497 f.close();
498 return configPath;
499 }
500
501 ////////////////// Custom assertions /////////////////////////////////////////
502
assertProjectOpened(const QString & name)503 KDevelop::Project* TestProjectController::assertProjectOpened(const QString& name)
504 {
505 auto* projRaw = m_projCtrl->findProjectByName(name);
506 QVERIFY_RETURN(projRaw, nullptr);
507 QVERIFY_RETURN(m_projCtrl->projects().contains(projRaw), nullptr);
508
509 auto proj = dynamic_cast<KDevelop::Project*>(projRaw);
510 QVERIFY_RETURN(projRaw, nullptr);
511 return proj;
512 }
513
assertSpyCaughtProject(QSignalSpy * spy,IProject * proj)514 void TestProjectController::assertSpyCaughtProject(QSignalSpy* spy, IProject* proj)
515 {
516 QCOMPARE(spy->size(), 1);
517 auto* emittedProj = (*spy)[0][0].value<IProject*>();
518 QCOMPARE(proj, emittedProj);
519 }
520
assertProjectClosed(IProject * proj)521 void TestProjectController::assertProjectClosed(IProject* proj)
522 {
523 IProject* p = m_projCtrl->findProjectByName(proj->name());
524 QVERIFY(p == nullptr);
525 QVERIFY(!m_projCtrl->projects().contains(proj));
526 }
527
assertEmptyProjectModel()528 void TestProjectController::assertEmptyProjectModel()
529 {
530 ProjectModel* m = m_projCtrl->projectModel();
531 Q_ASSERT(m);
532 QCOMPARE(m->rowCount(), 0);
533 }
534
535 ///////////////////// Creation stuff /////////////////////////////////////////
536
createOpenedSpy()537 std::unique_ptr<QSignalSpy> TestProjectController::createOpenedSpy()
538 {
539 return std::make_unique<QSignalSpy>(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*)));
540 }
541
createClosedSpy()542 std::unique_ptr<QSignalSpy> TestProjectController::createClosedSpy()
543 {
544 return std::make_unique<QSignalSpy>(m_projCtrl, SIGNAL(projectClosed(KDevelop::IProject*)));
545 }
546
createClosingSpy()547 std::unique_ptr<QSignalSpy> TestProjectController::createClosingSpy()
548 {
549 return std::make_unique<QSignalSpy>(m_projCtrl, SIGNAL(projectClosing(KDevelop::IProject*)));
550 }
551
createFileManager()552 FakeFileManager* TestProjectController::createFileManager()
553 {
554 auto* fileMng = new FakeFileManager;
555 m_fileManagerGarbage << fileMng;
556 return fileMng;
557 }
558
559 QTEST_MAIN(TestProjectController)
560 #include "moc_test_projectcontroller.cpp"
561 #include "test_projectcontroller.moc"
562