File indexing completed on 2024-05-19 04:41:08

0001 /*
0002     SPDX-FileCopyrightText: 2010 Niko Sams <niko.sams@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "test_projectload.h"
0008 
0009 #include <QTest>
0010 #include <QSignalSpy>
0011 #include <QProcess>
0012 #include <QTemporaryDir>
0013 #include <QDebug>
0014 #include <QRandomGenerator>
0015 
0016 #include <tests/autotestshell.h>
0017 #include <tests/testcore.h>
0018 
0019 #include <interfaces/icore.h>
0020 #include <interfaces/iprojectcontroller.h>
0021 #include <interfaces/iproject.h>
0022 #include <interfaces/ilanguagecontroller.h>
0023 
0024 #include <project/interfaces/iprojectfilemanager.h>
0025 #include <project/projectmodel.h>
0026 #include <language/backgroundparser/backgroundparser.h>
0027 
0028 #include <KIO/Global>
0029 
0030 QTEST_MAIN(TestProjectLoad)
0031 
0032 using namespace KDevelop;
0033 
0034 namespace {
0035 
0036 struct TestProject
0037 {
0038     // temp directory of project
0039     QTemporaryDir* dir;
0040     // name of the project (random)
0041     QString name;
0042     // project file (*.kdev4)
0043     QUrl file;
0044     ~TestProject() {
0045         IProject* p = ICore::self()->projectController()->findProjectByName(name);
0046         if (p) {
0047             ICore::self()->projectController()->closeProject(p);
0048         }
0049         delete dir;
0050     }
0051 };
0052 
0053 TestProject makeProject()
0054 {
0055     TestProject ret;
0056     ret.dir = new QTemporaryDir();
0057     QFileInfo dir(ret.dir->path());
0058     Q_ASSERT(dir.exists());
0059     ret.name = dir.fileName();
0060 
0061     QStringList projectFileContents;
0062     projectFileContents
0063     << QStringLiteral("[Project]")
0064     << QStringLiteral("Name=") + ret.name
0065     << QStringLiteral("Manager=KDevGenericManager");
0066 
0067     QUrl projecturl = QUrl::fromLocalFile( dir.absoluteFilePath() + "/simpleproject.kdev4" );
0068     QFile projectFile(projecturl.toLocalFile());
0069     projectFile.open(QIODevice::WriteOnly);
0070     projectFile.write(projectFileContents.join(QLatin1Char('\n')).toLatin1());
0071     projectFile.close();
0072     ret.file = projecturl;
0073 
0074     Q_ASSERT(ret.dir->isValid());
0075     Q_ASSERT(projecturl.adjusted(QUrl::RemoveFilename).toLocalFile() == ret.dir->path() + '/');
0076 
0077     return ret;
0078 }
0079 
0080 bool createFile(const QString& path)
0081 {
0082     QFile f(path);
0083     if (!f.open(QIODevice::WriteOnly)) {
0084         qWarning() << f.errorString() << path;
0085         return false;
0086     }
0087 
0088     auto* randomGenerator = QRandomGenerator::global();
0089     for (int i = 0; i < 4; ++i) {
0090         f.write(QByteArray::number(randomGenerator->generate()));
0091     }
0092 
0093     if (!f.flush()) {
0094         qWarning() << f.errorString() << path;
0095         return false;
0096     }
0097 
0098     f.close();
0099     return true;
0100 }
0101 
0102 bool writeRandomStructure(QString path, int files)
0103 {
0104     QDir p(path);
0105     const QString name = QString::number(QRandomGenerator::global()->generate());
0106     if (QRandomGenerator::global()->bounded(5) < 1) {
0107         if (!p.mkdir(name)) {
0108             return false;
0109         }
0110 
0111         //qDebug() << "wrote path" << path;
0112         path += '/' + name;
0113     } else {
0114         if (!createFile(path+'/'+name)) {
0115             return false;
0116         }
0117 
0118         //qDebug() << "wrote file" << path+"/"+name;
0119     }
0120     files--;
0121     if (files > 0) {
0122         if (!writeRandomStructure(path, files)) {
0123             return false;
0124         }
0125     }
0126     return true;
0127 }
0128 
0129 bool fillProject(int filesPerDir, int dirs, const TestProject& project, bool wait)
0130 {
0131     for(int i=0; i < dirs; ++i) {
0132         const QString name = "foox" + QString::number(i);
0133         if (!QDir(project.dir->path()).mkdir(name)) {
0134             return false;
0135         }
0136 
0137         if (!writeRandomStructure(project.dir->path() + "/" + name, filesPerDir)) {
0138             return false;
0139         }
0140 
0141         if (wait) {
0142             QTest::qWait(100);
0143         }
0144     }
0145     return true;
0146 }
0147 }
0148 
0149 void TestProjectLoad::initTestCase()
0150 {
0151     AutoTestShell::init({QStringLiteral("KDevGenericManager")});
0152     TestCore::initialize();
0153     ICore::self()->languageController()->backgroundParser()->disableProcessing();
0154 
0155     qRegisterMetaType<IProject*>();
0156 
0157     const auto projects = ICore::self()->projectController()->projects();
0158     for (IProject* p : projects) {
0159         ICore::self()->projectController()->closeProject(p);
0160     }
0161 }
0162 
0163 void TestProjectLoad::cleanupTestCase()
0164 {
0165     TestCore::shutdown();
0166 }
0167 
0168 void TestProjectLoad::init()
0169 {
0170     const auto projects = ICore::self()->projectController()->projects();
0171     for (IProject* p : projects) {
0172         ICore::self()->projectController()->closeProject(p);
0173     }
0174     QCOMPARE(ICore::self()->projectController()->projects().size(), 0);
0175 }
0176 
0177 void TestProjectLoad::addRemoveFiles()
0178 {
0179     const TestProject p = makeProject();
0180 
0181     createFile(p.dir->path()+"/sdf");
0182 
0183     ICore::self()->projectController()->openProject(p.file);
0184     QTRY_COMPARE(ICore::self()->projectController()->projects().size(), 1);
0185     IProject* project = ICore::self()->projectController()->projects().first();
0186     QCOMPARE(project->projectFile().toUrl(), p.file);
0187 
0188     //KDirWatch adds/removes the file automatically
0189     for (int i=0; i<100; ++i) {
0190         createFile(p.dir->path()+"/blub"+QString::number(i));
0191     }
0192     for (int i=0; i<50; ++i) {
0193         QFile::remove(p.dir->path()+"/blub"+QString::number(i));
0194     }
0195     QTRY_COMPARE(project->projectItem()->fileList().count(), 51);
0196 
0197     QUrl url = QUrl::fromLocalFile(p.dir->path()+"/blub"+QString::number(50)).adjusted(QUrl::NormalizePathSegments);
0198     QCOMPARE(project->filesForPath(IndexedString(url)).count(), 1);
0199     ProjectFileItem* file = project->filesForPath(IndexedString(url)).at(0);
0200     project->projectFileManager()->removeFilesAndFolders(QList<ProjectBaseItem*>() << file ); //message box has to be accepted manually :(
0201     QTRY_COMPARE(project->projectItem()->fileList().count(), 50);
0202 
0203     for (int i=51; i<100; ++i) {
0204         QFile::remove(p.dir->path()+"/blub"+QString::number(i));
0205     }
0206 
0207     QTRY_COMPARE(project->projectItem()->fileList().count(), 1);
0208 }
0209 
0210 void TestProjectLoad::removeDirRecursive()
0211 {
0212     const TestProject p = makeProject();
0213 
0214     createFile(p.dir->path()+"/sdf");
0215     {
0216         QDir d(p.dir->path());
0217         QVERIFY(d.mkdir(QStringLiteral("blub")));
0218         QVERIFY(d.cd(QStringLiteral("blub")));
0219         for (int i=0; i<10; ++i) {
0220             createFile(d.filePath("file"+QString::number(i)));
0221         }
0222     }
0223 
0224     QVERIFY(ICore::self()->projectController()->projects().isEmpty());
0225 
0226     ICore::self()->projectController()->openProject(p.file);
0227     QTRY_COMPARE(ICore::self()->projectController()->projects().size(), 1);
0228     IProject* project = ICore::self()->projectController()->projects().first();
0229     QCOMPARE(project->projectFile().toUrl(), p.file);
0230 
0231     for (int i=0; i<1; ++i) {
0232         QUrl url = QUrl::fromLocalFile(p.dir->path()+"/blub").adjusted(QUrl::NormalizePathSegments);
0233         QCOMPARE(project->foldersForPath(IndexedString(url)).count(), 1);
0234 
0235         ProjectFolderItem* file = project->foldersForPath(IndexedString(url)).at(0);
0236         project->projectFileManager()->removeFilesAndFolders(QList<ProjectBaseItem*>() << file );
0237     }
0238 
0239     QTRY_COMPARE(project->projectItem()->fileList().count(), 1);
0240 }
0241 
0242 void TestProjectLoad::addLotsOfFiles()
0243 {
0244     TestProject p = makeProject();
0245 
0246     ICore::self()->projectController()->openProject(p.file);
0247     QTRY_COMPARE(ICore::self()->projectController()->projects().size(), 1);
0248     IProject* project = ICore::self()->projectController()->projects().first();
0249     QCOMPARE(project->projectFile().toUrl(), p.file);
0250 
0251     QVERIFY(fillProject(50, 25, p, true));
0252 
0253     QTest::qWait(2000);
0254 }
0255 
0256 void TestProjectLoad::addMultipleJobs()
0257 {
0258     const TestProject p1 = makeProject();
0259     QVERIFY(fillProject(10, 25, p1, false));
0260     const TestProject p2 = makeProject();
0261     QVERIFY(fillProject(10, 25, p2, false));
0262 
0263     ICore::self()->projectController()->openProject(p1.file);
0264     ICore::self()->projectController()->openProject(p2.file);
0265 
0266     QTRY_COMPARE(ICore::self()->projectController()->projects().size(), 2);
0267 }
0268 
0269 void TestProjectLoad::raceJob()
0270 {
0271     // our goal here is to try to reproduce https://bugs.kde.org/show_bug.cgi?id=260741
0272     // my idea is that this can be triggered by the following:
0273     // - list dir foo/bar containing lots of files
0274     // - remove dir foo while listjob is still running
0275     TestProject p = makeProject();
0276     QDir dir(p.dir->path());
0277     QVERIFY(dir.mkpath(QStringLiteral("test/zzzzz")));
0278     for(int i = 0; i < 1000; ++i) {
0279         createFile(QString(p.dir->path() + "/test/zzzzz/%1").arg(i));
0280         createFile(QString(p.dir->path() + "/test/%1").arg(i));
0281     }
0282 
0283     ICore::self()->projectController()->openProject(p.file);
0284     QTRY_COMPARE(ICore::self()->projectController()->projectCount(), 1);
0285     IProject *project = ICore::self()->projectController()->projectAt(0);
0286     QCOMPARE(project->projectFile().toUrl(), p.file);
0287     ProjectFolderItem* root = project->projectItem();
0288     QCOMPARE(root->project(), project);
0289     QVERIFY(root->model());
0290     QCOMPARE(root->rowCount(), 1);
0291     ProjectBaseItem* testItem = root->child(0);
0292     QVERIFY(testItem->folder());
0293     QCOMPARE(testItem->baseName(), QStringLiteral("test"));
0294     QCOMPARE(testItem->rowCount(), 1001);
0295     int last = testItem->children().size() - 1;
0296     ProjectBaseItem* asdfItem = testItem->children().at(last);
0297     QVERIFY(asdfItem->folder());
0298 
0299     // reload to trigger new list job
0300     project->projectFileManager()->reload(testItem->folder());
0301     // move dir
0302     QVERIFY(dir.rename(QStringLiteral("test"), QStringLiteral("test2")));
0303     // move sub dir
0304     QVERIFY(dir.rename(QStringLiteral("test2/zzzzz"), QStringLiteral("test2/bla")));
0305 
0306     QTRY_COMPARE(root->rowCount() == 1 ? root->child(0)->baseName() : QString(), QStringLiteral("test2"));
0307 
0308     // reload full model and then move dir
0309     project->reloadModel();
0310     QVERIFY(dir.rename(QStringLiteral("test2"), QStringLiteral("test3")));
0311 
0312     // note: this actually invalidates the root, so query that again
0313     root = project->projectItem();
0314     QVERIFY(root);
0315 
0316     QTRY_COMPARE(root->rowCount() == 1 ? root->child(0)->baseName() : QString(), QStringLiteral("test3"));
0317 }
0318 
0319 void TestProjectLoad::addDuringImport()
0320 {
0321     // our goal here is to try to reproduce an issue in the optimized filesForPath implementation
0322     // which requires the project to be associated to the model to function properly
0323     // to trigger this we create a big project, import it and then call filesForPath during
0324     // the import action
0325     TestProject p = makeProject();
0326     QDir dir(p.dir->path());
0327     QVERIFY(dir.mkpath(QStringLiteral("test/zzzzz")));
0328     for(int i = 0; i < 1000; ++i) {
0329         createFile(QString(p.dir->path() + "/test/zzzzz/%1").arg(i));
0330         createFile(QString(p.dir->path() + "/test/%1").arg(i));
0331     }
0332 
0333     QSignalSpy spy(ICore::self()->projectController(),
0334                    SIGNAL(projectAboutToBeOpened(KDevelop::IProject*)));
0335     ICore::self()->projectController()->openProject(p.file);
0336     // not yet ready
0337     QCOMPARE(ICore::self()->projectController()->projectCount(), 0);
0338     // but about to be opened
0339     QCOMPARE(spy.count(), 1);
0340     auto* project = spy.value(0).at(0).value<IProject*>();
0341     QVERIFY(project);
0342     QCOMPARE(project->path(), Path(KIO::upUrl(p.file)));
0343     QUrl file = p.file.resolved(QUrl(QStringLiteral("test/zzzzz/999")));
0344     QVERIFY(QFile::exists(file.toLocalFile()));
0345     // this most probably is not yet loaded
0346     // and this should not crash
0347     QCOMPARE(project->itemsForPath(IndexedString(file)).size(), 0);
0348     // now delete that file and don't crash
0349     QFile::remove(file.toLocalFile());
0350     // now create another file
0351     QUrl file2 = file.adjusted(QUrl::RemoveFilename);
0352     file2.setPath(file2.path() + "999v2");
0353     createFile(file2.toLocalFile());
0354     QVERIFY(!project->isReady());
0355     // now wait for finish
0356     QTRY_VERIFY(project->isReady());
0357     // make sure our file removal + addition was properly tracked
0358     QCOMPARE(project->filesForPath(IndexedString(file)).size(), 0);
0359     QCOMPARE(project->filesForPath(IndexedString(file2)).size(), 1);
0360 
0361     //NOTE: this test is probably incomplete, I bet there are some race conditions left,
0362     //      esp. when adding a file at a point where the parent folder was already imported
0363     //      or removing a file that was already imported
0364 }
0365 
0366 #include "moc_test_projectload.cpp"