File indexing completed on 2024-05-12 04:40:04

0001 /*
0002     SPDX-FileCopyrightText: 1999-2001 Bernd Gehrmann <bernd@kdevelop.org>
0003     SPDX-FileCopyrightText: 1999-2001 the KDevelop Team
0004     SPDX-FileCopyrightText: 2010 Julien Desgats <julien.desgats@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "test_findreplace.h"
0010 
0011 #include <QByteArray>
0012 #include <QDebug>
0013 #include <QString>
0014 #include <QStringList>
0015 #include <QTest>
0016 #include <QRegExp>
0017 
0018 #include <QTemporaryFile>
0019 #include <QTemporaryDir>
0020 
0021 #include <project/projectmodel.h>
0022 #include <tests/testcore.h>
0023 #include <tests/autotestshell.h>
0024 #include <tests/testproject.h>
0025 #include <util/filesystemhelpers.h>
0026 
0027 #include "../grepjob.h"
0028 #include "../grepviewplugin.h"
0029 #include "../grepoutputmodel.h"
0030 
0031 #include <iterator>
0032 #include <vector>
0033 
0034 using namespace KDevelop;
0035 
0036 namespace {
0037 GrepJobSettings verbatimGrepJobSettings()
0038 {
0039     GrepJobSettings settings;
0040     settings.caseSensitive = true;
0041     settings.regexp = false;
0042     const QString verbatimTemplate = "%s";
0043     settings.searchTemplate = verbatimTemplate;
0044     settings.replacementTemplate = verbatimTemplate;
0045     return settings;
0046 }
0047 }
0048 
0049 void FindReplaceTest::initTestCase()
0050 {
0051     KDevelop::AutoTestShell::init({{}}); // do not load plugins at all
0052     const auto core = TestCore::initialize(Core::NoUi);
0053 
0054     delete core->projectController();
0055     m_projectController = new TestProjectController(core);
0056     core->setProjectController(m_projectController);
0057 }
0058 
0059 void FindReplaceTest::cleanupTestCase()
0060 {
0061     KDevelop::TestCore::shutdown();
0062 }
0063 
0064 void FindReplaceTest::init()
0065 {
0066     // Ensure there are no open projects even if the last test run crashed.
0067     m_projectController->closeAllProjects();
0068 }
0069 
0070 void FindReplaceTest::testFind_data()
0071 {
0072     QTest::addColumn<QString>("subject");
0073     QTest::addColumn<QRegExp>("search");
0074     QTest::addColumn<MatchList>("matches");
0075 
0076     QTest::newRow("Basic") << "foobar" << QRegExp("foo")
0077                            << (MatchList() << Match(0, 0, 3));
0078     QTest::newRow("Multiple matches") << "foobar\nbar\nbarfoo" << QRegExp("foo")
0079                            << (MatchList() << Match(0, 0, 3) << Match(2, 3, 6));
0080     QTest::newRow("Multiple on same line") << "foobarbaz" << QRegExp("ba")
0081                            << (MatchList() << Match(0, 3, 5) << Match(0, 6, 8));
0082     QTest::newRow("Multiple sticked together") << "foofoobar" << QRegExp("foo")
0083                            << (MatchList() << Match(0, 0, 3) << Match(0, 3, 6));
0084     QTest::newRow("RegExp (member call)") << "foo->bar ();\nbar();" << QRegExp("\\->\\s*\\b(bar)\\b\\s*\\(")
0085                            << (MatchList() << Match(0, 3, 10));
0086     // the matching must be started after the last previous match
0087     QTest::newRow("RegExp (greedy match)") << "foofooo" << QRegExp("[o]+")
0088                            << (MatchList() << Match(0, 1, 3) << Match(0, 4, 7));
0089     QTest::newRow("Matching EOL") << "foobar\nfoobar" << QRegExp("foo.*")
0090                            << (MatchList() << Match(0, 0, 6) << Match(1, 0, 6));
0091     QTest::newRow("Matching EOL (Windows style)") << "foobar\r\nfoobar" << QRegExp("foo.*")
0092                            << (MatchList() << Match(0, 0, 6) << Match(1, 0, 6));
0093     QTest::newRow("Empty lines handling") << "foo\n\n\n" << QRegExp("bar")
0094                            << (MatchList());
0095     QTest::newRow("Can match empty string (at EOL)") << "foobar\n" << QRegExp(".*")
0096                            << (MatchList() << Match(0, 0, 6));
0097     QTest::newRow("Matching empty string anywhere") << "foobar\n" << QRegExp("")
0098                            << (MatchList());
0099 }
0100 
0101 void FindReplaceTest::testFind()
0102 {
0103     QFETCH(QString,   subject);
0104     QFETCH(QRegExp,   search);
0105     QFETCH(MatchList, matches);
0106 
0107     QTemporaryFile file;
0108     QVERIFY(file.open());
0109     file.write(subject.toUtf8());
0110     file.close();
0111 
0112     GrepOutputItem::List actualMatches = grepFile(file.fileName(), search);
0113 
0114     QCOMPARE(actualMatches.length(), matches.length());
0115 
0116     for(int i=0; i<matches.length(); i++)
0117     {
0118         QCOMPARE(actualMatches[i].change()->m_range.start().line(),   matches[i].line);
0119         QCOMPARE(actualMatches[i].change()->m_range.start().column(), matches[i].start);
0120         QCOMPARE(actualMatches[i].change()->m_range.end().column(),   matches[i].end);
0121     }
0122 
0123     // check that file has not been altered by grepFile
0124     QVERIFY(file.open());
0125     QCOMPARE(QString(file.readAll()), subject);
0126 }
0127 
0128 void FindReplaceTest::testSingleFileAsDirectoryChoice()
0129 {
0130     QTemporaryDir tmpDir;
0131     QVERIFY2(tmpDir.isValid(), qPrintable("couldn't create temporary directory: " + tmpDir.errorString()));
0132 
0133     const QByteArray fileContents = "A";
0134     QString filePath = "testfile.cpp";
0135 
0136     using FilesystemHelpers::makeAbsoluteCreateAndWrite;
0137     QString errorPath = makeAbsoluteCreateAndWrite(tmpDir.path(), filePath, fileContents);
0138     QVERIFY2(errorPath.isEmpty(), qPrintable("couldn't create or write to temporary file or directory " + errorPath));
0139 
0140     const QString siblingDirPath = tmpDir.filePath("dir");
0141     QVERIFY(QDir{}.mkpath(siblingDirPath));
0142 
0143     const QString siblingDirSameStartPath = tmpDir.filePath("test");
0144     QVERIFY(filePath.startsWith(siblingDirSameStartPath));
0145     QVERIFY(QDir{}.mkpath(siblingDirSameStartPath));
0146 
0147     GrepJobSettings settings = verbatimGrepJobSettings();
0148     settings.pattern = fileContents;
0149 
0150     const auto test = [filePath, &settings](const QList<QUrl>& directoryChoice, bool expectMatch) {
0151         GrepJob job;
0152         GrepOutputModel model;
0153         job.setOutputModel(&model);
0154         job.setDirectoryChoice(directoryChoice);
0155         job.setSettings(settings);
0156 
0157         QVERIFY(job.exec());
0158 
0159         const auto index = model.nextItemIndex(QModelIndex{});
0160 
0161         if (!expectMatch) {
0162             QVERIFY(!index.isValid());
0163             return;
0164         }
0165 
0166         QVERIFY(index.isValid());
0167         auto* const item = dynamic_cast<const GrepOutputItem*>(model.itemFromIndex(index));
0168         QVERIFY(item);
0169         QVERIFY(item->isText());
0170         QCOMPARE(item->filename(), filePath);
0171 
0172         const auto nextIndex = model.nextItemIndex(index);
0173         QVERIFY2(!nextIndex.isValid() || nextIndex == index, "unexpected second matched file");
0174     };
0175 
0176     const QString nonexistentFilePath = "/tmp/nonexistent/file/path.kdevelop";
0177     QVERIFY2(!QFileInfo::exists(nonexistentFilePath), "what a strange file path to exist...");
0178     // Test searching nowhere, in a nonexistent file and in empty directories in addition to
0179     // the file set up above. The search locations, where nothing can be found, are
0180     // unrelated to the main test, but do not complicate this test function much either.
0181     const QList<QUrl> directoryChoices[] = {{},
0182                                             {QUrl::fromLocalFile(nonexistentFilePath)},
0183                                             {QUrl::fromLocalFile(siblingDirPath)},
0184                                             {QUrl::fromLocalFile(siblingDirSameStartPath)},
0185                                             {QUrl::fromLocalFile(filePath)}};
0186 
0187     varyProjectFilesOnly(settings, tmpDir.path(), [this, &settings, &directoryChoices, test] {
0188         if (settings.projectFilesOnly) {
0189             for (const int i : {2, 3, 4}) {
0190                 QVERIFY(m_projectController->findProjectForUrl(directoryChoices[i].constFirst()));
0191             }
0192         }
0193 
0194         for (const auto& directoryChoice : directoryChoices) {
0195             qDebug() << "\tsearch locations:" << directoryChoice;
0196             const bool expectMatch = &directoryChoice == &*std::rbegin(directoryChoices);
0197             for (int depth = -1; depth <= 2; ++depth) {
0198                 // Depth makes no difference to matching a file path search location.
0199                 qDebug("\t\tdepth=%d", depth);
0200                 settings.depth = depth;
0201                 for (const auto files : {"*", "*.nonmatching"}) {
0202                     // A file path search location is not matched against the Files filter.
0203                     qDebug("\t\t\tFiles filter: \"%s\"", files);
0204                     settings.files = files;
0205                     for (const auto exclude : {"", "/"}) {
0206                         // A file path search location is not matched against the Exclude filter.
0207                         qDebug("\t\t\t\tExclude filter: \"%s\"", exclude);
0208                         settings.exclude = exclude;
0209 
0210                         test(directoryChoice, expectMatch);
0211                     }
0212                 }
0213             }
0214         }
0215     });
0216 }
0217 
0218 void FindReplaceTest::testIncludeExcludeFilters_data()
0219 {
0220     struct Row{
0221         const char* dataTag;
0222         int depth;
0223         const char* files;
0224         const char* exclude;
0225         std::vector<const char*> unmatchedPaths;
0226         std::vector<const char*> matchedPaths;
0227     };
0228 
0229     const std::vector<Row> dataRows{
0230         Row{"depth=0",
0231             0,
0232             "*",
0233             "",
0234             {"a/b", "x/y.cpp", "my/long/path/n.txt"},
0235             {"no", "b", "t.cpp", "n.txt", "A very long file name"}},
0236         Row{"depth=1",
0237             1,
0238             "*",
0239             "",
0240             {"a/c/d", "p/y/z.cpp", "my/long/path/n.txt"},
0241             {"y", "t.cpp", "a/b", "a/nt", "x/y.cpp"}},
0242         Row{"Files filter",
0243             -1,
0244             "*.cpp,*.cc,*.h,INSTALL",
0245             "",
0246             {"A", "cpp", ".cp", "a.c", "INSTAL", "oINSTALL", "d./h", "d.h/c", "u/INSTALL/v", "a.cpp/b.cp", "INSTALL/h",
0247              "a.h.c"},
0248             {"x/INSTALL", "x/.cpp", ".cc", "x.h", "t/s/r/.h/.cc", "INSTALL.cpp", "x/y/z/a/b/c.h", "y/b.cc", "a.hh.cc",
0249              "t.h.cc"}},
0250         Row{"Exclude filter",
0251             4,
0252             "*",
0253             "/build/,/.git/,~",
0254             {"build/C", ".git/config", "~", "a/b/c/build/t/n", "build/me", "a/~/x", "a~b/c", "temp~", "a/p/test~",
0255              "a/build/b/c", "d/c/.git/t"},
0256             {"d/build", "a/b/.git", "x", "a.h", "a.git", "a/.gitignore/b", ".gitignore", "buildme/now",
0257              "to build/.git"}},
0258         Row{"Files and Exclude filters",
0259             -1,
0260             "*.a,*-b,*se",
0261             "/release/,/.*/,bak",
0262             {"release/x.a", ".git/q-b", "a-b.c", "bak.a", "abakse", "a/bak-b", "a/x.bak", "u/v/wbakxyz", "a/.g/se",
0263              "-/b"},
0264             {"a.a", "b-b", "a/release", ".a", "git/q-b", "se", "a/.se", "a/b.c/d-b", "Bse", "u/v/.a-b", "a/b/.ignorse",
0265              "ba.k/.a"}},
0266         Row{"depth=1, Files and Exclude filters",
0267             1,
0268             "*.a,*-b,*se",
0269             "/release/,/.*/,bak",
0270             {"release/x.a", ".git/q-b", "a-b.c", "bak.a", "abakse", "a/bak-b", "a/x.bak", "u/v/wbakxyz", "a/.g/se",
0271              "-/b", "a/b.c/d-b", "u/v/.a-b", "a/b/.ignorse"},
0272             {"a.a", "b-b", "a/release", ".a", "git/q-b", "se", "a/.se", "Bse", "ba.k/.a"}},
0273         Row{"Matching case-insensitive",
0274             9,
0275             "A*b,*.Cd,*.AUX",
0276             "GiT,garble,B.CD",
0277             {"acbGArblE.cD", "git/a.b", ".git/x.cd", "b.Cd", "u/v/q", "u/v/bcd", "u/v/b.Cd", "garble.AUX"},
0278             {"Ab", "a.b", "u/v/ADB", "gi.cd", ".CD", "az.cd", "u/v/w/agb", "p.AuX", "u/q.aux"}}};
0279 
0280     QTest::addColumn<int>("depth");
0281     QTest::addColumn<QString>("files");
0282     QTest::addColumn<QString>("exclude");
0283     QTest::addColumn<QStringList>("unmatchedPaths");
0284     QTest::addColumn<QStringList>("matchedPaths");
0285 
0286     for (const Row& row : dataRows) {
0287         const QStringList unmatchedPaths(row.unmatchedPaths.cbegin(), row.unmatchedPaths.cend());
0288         const QStringList matchedPaths(row.matchedPaths.cbegin(), row.matchedPaths.cend());
0289         QTest::newRow(row.dataTag) << row.depth << QString{row.files} << QString{row.exclude} << unmatchedPaths
0290                                    << matchedPaths;
0291     }
0292 }
0293 
0294 void FindReplaceTest::testIncludeExcludeFilters()
0295 {
0296     QFETCH(const int, depth);
0297     QFETCH(QString, files);
0298     QFETCH(QString, exclude);
0299     QFETCH(QStringList, unmatchedPaths);
0300     QFETCH(QStringList, matchedPaths);
0301 
0302     QTemporaryDir tmpDir;
0303     QVERIFY2(tmpDir.isValid(), qPrintable("couldn't create temporary directory: " + tmpDir.errorString()));
0304 
0305     const QByteArray commonFileContents = "x";
0306 
0307     using FilesystemHelpers::makeAbsoluteCreateAndWrite;
0308     QString errorPath = makeAbsoluteCreateAndWrite(tmpDir.path(), unmatchedPaths, commonFileContents);
0309     if (errorPath.isEmpty()) {
0310         errorPath = makeAbsoluteCreateAndWrite(tmpDir.path(), matchedPaths, commonFileContents);
0311     }
0312     QVERIFY2(errorPath.isEmpty(), qPrintable("couldn't create or write to temporary file or directory " + errorPath));
0313 
0314     GrepJobSettings settings = verbatimGrepJobSettings();
0315     settings.depth = depth;
0316     settings.pattern = commonFileContents;
0317     settings.files = files;
0318     settings.exclude = exclude;
0319 
0320     // Modify a copy of the matchedPaths list - pathsToMatch - in order
0321     // to keep matchedPaths intact for each invocation of this lambda.
0322     const auto test = [&tmpDir, &settings](QStringList pathsToMatch) {
0323         GrepJob job;
0324         GrepOutputModel model;
0325         job.setOutputModel(&model);
0326         job.setDirectoryChoice({QUrl::fromLocalFile(tmpDir.path())});
0327         job.setSettings(settings);
0328 
0329         QVERIFY(job.exec());
0330 
0331         QModelIndex index;
0332         const GrepOutputItem* previousItem = nullptr;
0333         while (true) {
0334             index = model.nextItemIndex(index);
0335             if (!index.isValid()) {
0336                 break;
0337             }
0338             auto* const item = dynamic_cast<const GrepOutputItem*>(model.itemFromIndex(index));
0339             QVERIFY(item);
0340             QVERIFY(item->isText());
0341             if (item == previousItem) {
0342                 break; // This must be the last match.
0343             }
0344             previousItem = item;
0345 
0346             const QString filename = item->filename();
0347             QVERIFY2(pathsToMatch.contains(filename), qPrintable("unexpected matched file " + filename));
0348             QVERIFY2(pathsToMatch.removeOne(filename),
0349                      qPrintable("there must be exactly one text match for " + filename));
0350         }
0351         QVERIFY2(pathsToMatch.empty(),
0352                  qPrintable("these files should have been matched, but weren't: " + pathsToMatch.join("; ")));
0353     };
0354 
0355     varyProjectFilesOnly(settings, tmpDir.path(), [&matchedPaths, test] {
0356         test(matchedPaths);
0357     });
0358 }
0359 
0360 void FindReplaceTest::testReplace_data()
0361 {
0362     QTest::addColumn<FileList>("subject");
0363     QTest::addColumn<QString>("searchPattern");
0364     QTest::addColumn<QString>("searchTemplate");
0365     QTest::addColumn<QString>("replace");
0366     QTest::addColumn<QString>("replaceTemplate");
0367     QTest::addColumn<FileList>("result");
0368 
0369     QTest::newRow("Raw replace")
0370         << (FileList() << File(QStringLiteral("myfile.txt"), QStringLiteral("some text\nreplacement\nsome other test\n"))
0371                        << File(QStringLiteral("otherfile.txt"), QStringLiteral("some replacement text\n\n")))
0372         << "replacement" << "%s"
0373         << "dummy"       << "%s"
0374         << (FileList() << File(QStringLiteral("myfile.txt"), QStringLiteral("some text\ndummy\nsome other test\n"))
0375                        << File(QStringLiteral("otherfile.txt"), QStringLiteral("some dummy text\n\n")));
0376 
0377     // see bug: https://bugs.kde.org/show_bug.cgi?id=301362
0378     QTest::newRow("LF character replace")
0379         << (FileList() << File(QStringLiteral("somefile.txt"), QStringLiteral("hello world\\n")))
0380         << "\\\\n" << "%s"
0381         << "\\n\\n" << "%s"
0382         << (FileList() << File(QStringLiteral("somefile.txt"), QStringLiteral("hello world\\n\\n")));
0383 
0384     QTest::newRow("Template replace")
0385         << (FileList() << File(QStringLiteral("somefile.h"),   QStringLiteral("struct Foo {\n  void setFoo(int foo);\n};"))
0386                        << File(QStringLiteral("somefile.cpp"), QStringLiteral("instance->setFoo(0);\n setFoo(0); /*not replaced*/")))
0387         << "setFoo" << "\\->\\s*\\b%s\\b\\s*\\("
0388         << "setBar" << "->%s("
0389         << (FileList() << File(QStringLiteral("somefile.h"),   QStringLiteral("struct Foo {\n  void setFoo(int foo);\n};"))
0390                        << File(QStringLiteral("somefile.cpp"), QStringLiteral("instance->setBar(0);\n setFoo(0); /*not replaced*/")));
0391 
0392     QTest::newRow("Template with captures")
0393         << (FileList() << File(QStringLiteral("somefile.cpp"), QStringLiteral("inst::func(1, 2)\n otherInst :: func (\"foo\")\n func()")))
0394         << "func" << "([a-z0-9_$]+)\\s*::\\s*\\b%s\\b\\s*\\("
0395         << "REPL" << "\\1::%s("
0396         << (FileList() << File(QStringLiteral("somefile.cpp"), QStringLiteral("inst::REPL(1, 2)\n otherInst::REPL(\"foo\")\n func()")));
0397 
0398     QTest::newRow("Regexp pattern")
0399         << (FileList() << File(QStringLiteral("somefile.txt"), QStringLiteral("foobar\n foooobar\n fake")))
0400         << "f\\w*o" << "%s"
0401         << "FOO" << "%s"
0402         << (FileList() << File(QStringLiteral("somefile.txt"), QStringLiteral("FOObar\n FOObar\n fake")));
0403 }
0404 
0405 
0406 void FindReplaceTest::testReplace()
0407 {
0408     QFETCH(FileList, subject);
0409     QFETCH(QString,  searchPattern);
0410     QFETCH(QString,  searchTemplate);
0411     QFETCH(QString,  replace);
0412     QFETCH(QString,  replaceTemplate);
0413     QFETCH(FileList, result);
0414 
0415     QTemporaryDir tempDir;
0416     QDir     dir(tempDir.path());  // we need some convenience functions that are not in QTemporaryDir
0417 
0418     for (const File& fileData : qAsConst(subject)) {
0419         QFile file(dir.filePath(fileData.first));
0420         QVERIFY(file.open(QIODevice::WriteOnly));
0421         QVERIFY(file.write(fileData.second.toUtf8()) != -1);
0422         file.close();
0423     }
0424 
0425     auto *job = new GrepJob(this);
0426     auto *model = new GrepOutputModel(job);
0427     GrepJobSettings settings;
0428 
0429     job->setOutputModel(model);
0430     job->setDirectoryChoice(QList<QUrl>() << QUrl::fromLocalFile(dir.path()));
0431 
0432     settings.projectFilesOnly = false;
0433     settings.caseSensitive = true;
0434     settings.regexp = true;
0435     settings.depth = -1; // fully recursive
0436     settings.pattern = searchPattern;
0437     settings.searchTemplate = searchTemplate;
0438     settings.replacementTemplate = replaceTemplate;
0439     settings.files = QStringLiteral("*");
0440     settings.exclude = QString();
0441 
0442     job->setSettings(settings);
0443 
0444     QVERIFY(job->exec());
0445 
0446     QVERIFY(model->hasResults());
0447     model->setReplacement(replace);
0448     model->makeItemsCheckable(true);
0449     model->doReplacements();
0450 
0451     for (const File& fileData : qAsConst(result)) {
0452         QFile file(dir.filePath(fileData.first));
0453         QVERIFY(file.open(QIODevice::ReadOnly));
0454         QCOMPARE(QString(file.readAll()), fileData.second);
0455         file.close();
0456     }
0457     tempDir.remove();
0458 }
0459 
0460 void FindReplaceTest::addTestProjectFromFileSystem(const QString& path)
0461 {
0462     auto* const project = new TestProject(Path{path});
0463     TestProjectUtils::addChildrenFromFileSystem(project->projectItem());
0464     m_projectController->addProject(project);
0465 }
0466 
0467 template<typename Test>
0468 void FindReplaceTest::varyProjectFilesOnly(GrepJobSettings& settings, const QString& projectPath, Test testToRun)
0469 {
0470     for (const bool projectFilesOnly : {false, true}) {
0471         qDebug() << "limit to project files:" << projectFilesOnly;
0472         settings.projectFilesOnly = projectFilesOnly;
0473 
0474         if (projectFilesOnly) {
0475             addTestProjectFromFileSystem(projectPath);
0476         }
0477 
0478         testToRun();
0479 
0480         m_projectController->closeAllProjects();
0481     }
0482 }
0483 
0484 QTEST_MAIN(FindReplaceTest)
0485 
0486 #include "moc_test_findreplace.cpp"