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"