File indexing completed on 2024-05-12 04:37:36
0001 /* 0002 * SPDX-FileCopyrightText: 2023 JATothrim <jarmo.tiitto@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "test_breakpointmodel.h" 0008 0009 #include "testfilepaths.h" 0010 0011 #include <debugger/breakpoint/breakpoint.h> 0012 #include <debugger/breakpoint/breakpointmodel.h> 0013 #include <interfaces/idebugcontroller.h> 0014 #include <interfaces/idocument.h> 0015 #include <interfaces/idocumentcontroller.h> 0016 #include <interfaces/ilanguagecontroller.h> 0017 #include <interfaces/isession.h> 0018 #include <language/backgroundparser/backgroundparser.h> 0019 #include <shell/documentcontroller.h> 0020 #include <tests/autotestshell.h> 0021 #include <tests/testcore.h> 0022 #include <tests/testhelpers.h> 0023 0024 #include <KConfigGroup> 0025 #include <KSharedConfig> 0026 #include <KTextEditor/Cursor> 0027 #include <KTextEditor/Document> 0028 0029 #include <QFile> 0030 #include <QFileInfo> 0031 #include <QString> 0032 #include <QTest> 0033 #include <QUrl> 0034 0035 #include <algorithm> 0036 #include <memory> 0037 #include <vector> 0038 0039 QTEST_MAIN(TestBreakpointModel) 0040 0041 using namespace KDevelop; 0042 0043 /// Expected test helper failure handling. 0044 /// Use QEXPECT_FAIL_ABORT() to mark the next check as an expected failure, 0045 /// and call RETURN_IF_TEST_ABORTED() right after RETURN_IF_TEST_FAILED() 0046 /// to check if the test helper aborted. 0047 0048 static constexpr const char* testAbortPropertyName = "kdevelop.test.abort"; 0049 0050 #define QEXPECT_FAIL_ABORT(comment) \ 0051 do { \ 0052 QTest::testObject()->setProperty(testAbortPropertyName, true); \ 0053 QEXPECT_FAIL("", comment, Abort); \ 0054 } while (false) 0055 0056 #define RETURN_IF_TEST_ABORTED(...) \ 0057 do { \ 0058 if (QTest::testObject()->property(testAbortPropertyName).isValid()) { \ 0059 qCritical("ABORTED AT: %s:%d", __FILE__, __LINE__); \ 0060 return __VA_ARGS__; \ 0061 } \ 0062 } while (false) 0063 0064 namespace { 0065 0066 /// Primary test file used as document in the temporary directory. 0067 /// This file is always restored for a test to have same content. 0068 constexpr const char* primaryTestFileName = "primary_test.cpp"; 0069 0070 /// Get breakpoint config group. 0071 /// NOTE: this config is written into 0072 /// "~/.qttest/share/test_breakpointmodel/sessions/{session-UUID}/sessionrc" 0073 KConfigGroup breakpointConfig() 0074 { 0075 return ICore::self()->activeSession()->config()->group("Breakpoints"); 0076 } 0077 0078 /// Qt 5.15 does not have C++11 emplace() like functionality. 0079 /// Make use of QList< std::shared_ptr< Breakpoint > > instead. 0080 using BreakpointPtr = std::shared_ptr<Breakpoint>; 0081 0082 /// Read BreakpointModel config data, like BreakpointModel::load() does. 0083 /// The Breakpoints are not attached to the model, so they stay as standalone instances. 0084 QList<BreakpointPtr> readBreakpointsFromConfig() 0085 { 0086 QList<BreakpointPtr> data; 0087 const KConfigGroup config = breakpointConfig(); 0088 const int count = config.readEntry("number", 0); 0089 0090 for (int i = 0; i < count; ++i) { 0091 const auto group = config.group(QString::number(i)); 0092 QVERIFY_RETURN(!group.readEntry("kind", "").isEmpty(), {}); 0093 data.push_back(std::make_shared<Breakpoint>(nullptr, group)); 0094 } 0095 return data; 0096 } 0097 0098 IDocumentController* documentController() 0099 { 0100 return ICore::self()->documentController(); 0101 } 0102 0103 BreakpointModel* breakpointModel() 0104 { 0105 return ICore::self()->debugController()->breakpointModel(); 0106 } 0107 0108 } // unnamed namespace 0109 0110 /// Gather breakpoint marks in the document. 0111 /// @return line numbers as int keys and BreakpointModel::MarkType (possibly a bitwise-or combination of) as mapped values. 0112 TestBreakpointModel::DocumentMarks TestBreakpointModel::documentMarks(const IDocument* doc) 0113 { 0114 DocumentMarks ret; 0115 // if it is not possible to get marks, fail. 0116 QVERIFY_RETURN(doc, ret); 0117 auto* const imark = qobject_cast<KTextEditor::MarkInterface*>(doc->textDocument()); 0118 QVERIFY_RETURN(imark, ret); 0119 0120 const auto marks = imark->marks(); 0121 for (const auto* mark : marks) { 0122 // mask to remove non-breakpoint mark type bits. 0123 const auto type = mark->type & BreakpointModel::MarkType::AllBreakpointMarks; 0124 if (type) { 0125 ret.insert(mark->line, static_cast<BreakpointModel::MarkType>(type)); 0126 } 0127 } 0128 return ret; 0129 } 0130 0131 /// Print sections of example code. 0132 /// Used for checking what edited text looks like when writing tests. 0133 void TestBreakpointModel::printLines(int from, int count, const IDocument* doc) 0134 { 0135 // gather a set of breakpoint line numbers in doc. 0136 std::vector<int> breakpointLines; 0137 const auto breakpoints = breakpointModel()->breakpoints(); 0138 for (const auto* b : breakpoints) { 0139 if (b->url() == doc->url()) { 0140 breakpointLines.push_back(b->line()); 0141 } 0142 } 0143 std::sort(breakpointLines.begin(), breakpointLines.end()); 0144 0145 // gather a set of breakpoint marks in doc. 0146 const auto markLines = documentMarks(doc); 0147 0148 // Breakpoint prefixes: 0149 // "[bm]" ok: a line has both mark and breakpoint instance 0150 // "[b-]" a line has breakpoint instance but no mark in UI 0151 // "[-m]" a line has mark in UI but no breakpoint instance 0152 // TODO: print upper-case letters for marks or breakpoints that are in an enabled state. 0153 for (int i = from; i < from + count && i < doc->textDocument()->lines(); ++i) { 0154 const bool hasBreakpoint = std::binary_search(breakpointLines.cbegin(), breakpointLines.cend(), i); 0155 QString prefix = (hasBreakpoint ? "b" : "-"); 0156 prefix += (markLines.contains(i) ? "m" : "-"); 0157 0158 qDebug("%d\t[%s]: %s", i, qUtf8Printable(prefix), qUtf8Printable(doc->textDocument()->line(i))); 0159 } 0160 } 0161 0162 /// Verify that the breakpoint is set correctly at the expected line number. 0163 /// Check success with RETURN_IF_TEST_FAILED() and RETURN_IF_TEST_ABORTED(). 0164 void TestBreakpointModel::verifyBreakpoint(Breakpoint* breakpoint, int expectedLine, const DocumentMarks& marks) 0165 { 0166 QEXPECT_FAIL_ABORT("CodeBreakpoint tracked line number is not updated"); 0167 QCOMPARE(breakpoint->line(), expectedLine); 0168 0169 // To be noted, there is no way to detect if an editor mark is actually 0170 // associated with a breakpoint instance. Because of this, the tests should 0171 // alternate using an enabled or disabled breakpoint to detect conflicts. 0172 const auto mark = marks.constFind(expectedLine); 0173 QEXPECT_FAIL_ABORT("CodeBreakpoint mark does not follow the tracking"); 0174 QVERIFY(mark != marks.cend()); 0175 0176 const auto breakpointType = breakpointModel()->breakpointType(breakpoint); 0177 QCOMPARE(static_cast<uint>(mark.value()), breakpointType); 0178 } 0179 0180 /// Convenience macro for verifyBreakpoint(). 0181 /// The fourth argument is an optional return value on failure. 0182 #define VERIFY_BREAKPOINT(breakpoint, expectedLine, marks, ...) \ 0183 do { \ 0184 verifyBreakpoint(breakpoint, expectedLine, marks); \ 0185 RETURN_IF_TEST_FAILED(__VA_ARGS__); \ 0186 RETURN_IF_TEST_ABORTED(__VA_ARGS__); \ 0187 } while (false) 0188 0189 TestBreakpointModel::TestBreakpointModel(QObject* parent) 0190 : QObject(parent) 0191 { 0192 } 0193 0194 /// Get URL to existing test file under the temporary dir. 0195 QUrl TestBreakpointModel::testFileUrl(const QString& fileName) const 0196 { 0197 const QFileInfo info(m_tempDir->path(), fileName); 0198 QVERIFY_RETURN(info.isFile(), QUrl{}); 0199 auto url = QUrl::fromLocalFile(info.canonicalFilePath()); 0200 QVERIFY_RETURN(url.isValid(), QUrl{}); 0201 return url; 0202 } 0203 0204 /// Prologue for tests that use the primary test file and the breakpoint created in init(). 0205 /// Check success with RETURN_IF_TEST_FAILED(). 0206 TestBreakpointModel::DocumentAndBreakpoint TestBreakpointModel::setupPrimaryDocumentAndBreakpoint() 0207 { 0208 // setup. 0209 breakpointModel()->load(); 0210 QCOMPARE_RETURN(breakpointModel()->rowCount(), 1, {}); 0211 0212 const auto url = testFileUrl(primaryTestFileName); 0213 RETURN_IF_TEST_FAILED({}); 0214 auto* const doc = documentController()->openDocument(url); 0215 QVERIFY_RETURN(doc, {}); 0216 QCOMPARE_RETURN(doc->url(), url, {}); 0217 0218 // pre-conditions. 0219 auto* const b1 = breakpointModel()->breakpoint(0); 0220 QVERIFY_RETURN(b1, {}); 0221 QCOMPARE_RETURN(b1->url(), url, {}); 0222 QCOMPARE_RETURN(b1->line(), 21, {}); 0223 QVERIFY_RETURN(b1->condition().isEmpty(), {}); 0224 QCOMPARE_RETURN(b1->ignoreHits(), 0, {}); 0225 QVERIFY_RETURN(b1->expression().isEmpty(), {}); 0226 QVERIFY_RETURN(b1->movingCursor(), {}); 0227 0228 return {url, doc, b1}; 0229 } 0230 0231 /// Prologue for tests that inserts two text lines and sets up two breakpoints in the primary document. 0232 /// The second breakpoint is added below the first one on a moved text line. 0233 /// Check success with RETURN_IF_TEST_FAILED() and RETURN_IF_TEST_ABORTED(). 0234 TestBreakpointModel::DocumentAndTwoBreakpoints TestBreakpointModel::setupEditAndCheckPrimaryDocumentAndBreakpoints() 0235 { 0236 const auto [url, doc, b1] = setupPrimaryDocumentAndBreakpoint(); 0237 RETURN_IF_TEST_FAILED({}); 0238 0239 // set b1 as disabled, so its mark differs from b2 in the editor. 0240 b1->setData(Breakpoint::EnableColumn, Qt::Unchecked); 0241 0242 // test. 0243 doc->textDocument()->insertLine(21, "// Comment A"); 0244 // FIXME: adding b2 breaks b1 text line tracking. 0245 auto* const b2 = breakpointModel()->addCodeBreakpoint(url, 23); 0246 doc->textDocument()->insertLine(21, "// Comment B"); 0247 printLines(21, 4, doc); 0248 0249 const auto marks = documentMarks(doc); 0250 QCOMPARE_RETURN(marks.size(), 2, {}); 0251 0252 VERIFY_BREAKPOINT(b1, 23, marks, {}); 0253 VERIFY_BREAKPOINT(b2, 24, marks, {}); 0254 0255 return {url, doc, b1, b2}; 0256 } 0257 0258 void TestBreakpointModel::initTestCase() 0259 { 0260 AutoTestShell::init({{}}); 0261 TestCore::initialize(); 0262 ICore::self()->languageController()->backgroundParser()->disableProcessing(); 0263 0264 // NOTE: DebugController is already initialized at this point, 0265 // and BreakpointModel::load() has run. 0266 // The model may have loaded some breakpoints from last run, 0267 // that must be cleared by init(). 0268 } 0269 0270 void TestBreakpointModel::init() 0271 { 0272 // Reset the test abort property before each test function. 0273 QTest::testObject()->setProperty(testAbortPropertyName, QVariant{}); 0274 0275 Core::self()->documentControllerInternal()->initialize(); 0276 0277 // Restore the primary test file under empty temporary working dir. 0278 m_tempDir = std::make_unique<QTemporaryDir>(); 0279 QVERIFY(QFile::copy(TEST_FILES_DIR "/primary_test.cpp", m_tempDir->filePath(primaryTestFileName))); 0280 0281 // pre-conditions 0282 QVERIFY(documentController()); 0283 QVERIFY(breakpointModel()); 0284 QVERIFY(documentController()->openDocuments().empty()); 0285 0286 // init() must put breakpointModel() and breakpointConfig() 0287 // into state where "breakpointModel()->load()" can work: 0288 // - breakpointConfig() has single breakpoint. 0289 // - No breakpoints are registered/exist in the model. 0290 0291 // Remove all breakpoints, overwrites breakpointConfig(). 0292 // This must be done *before* writing the hard-coded config below. 0293 auto* const model = breakpointModel(); 0294 model->removeRows(0, model->rowCount()); 0295 0296 // Setup hard-coded breakpoint model config with single breakpoint. 0297 // Tests can use this by doing "breakpointModel()->load()" before modifying the BreakpointModel. 0298 KConfigGroup config = breakpointConfig(); 0299 config.writeEntry("number", 1); 0300 // (ab)use Breakpoint to write the config data for it. 0301 Breakpoint entry(nullptr, Breakpoint::CodeBreakpoint); 0302 // set source line location at "foo(5);" 0303 const auto url = testFileUrl(primaryTestFileName); 0304 RETURN_IF_TEST_FAILED(); 0305 entry.setLocation(url, 21); 0306 { 0307 KConfigGroup group = config.group(QString::number(0)); 0308 entry.save(group); 0309 } 0310 0311 QCOMPARE(breakpointModel()->rowCount(), 0); 0312 } 0313 0314 void TestBreakpointModel::cleanup() 0315 { 0316 Core::self()->documentControllerInternal()->cleanup(); 0317 } 0318 0319 void TestBreakpointModel::cleanupTestCase() 0320 { 0321 TestCore::shutdown(); 0322 } 0323 0324 void TestBreakpointModel::testDocumentSave() 0325 { 0326 const auto [url, doc, b1] = setupPrimaryDocumentAndBreakpoint(); 0327 RETURN_IF_TEST_FAILED(); 0328 0329 // test. 0330 b1->setLocation(url, 22); 0331 auto* const b2 = breakpointModel()->addCodeBreakpoint(url, 24); 0332 b2->setCondition("*i > 0"); 0333 b2->setIgnoreHits(1); 0334 printLines(21, 4, doc); 0335 0336 QVERIFY(b1->movingCursor()); 0337 0338 // FIXME: all addCodeBreakpoint(url, line) should gain moving cursor. 0339 QEXPECT_FAIL("", "Added CodeBreakpoint b2 does not have a moving cursor", Continue); 0340 QVERIFY(b2->movingCursor()); 0341 0342 const auto marks = documentMarks(doc); 0343 QCOMPARE(marks.size(), 2); 0344 QVERIFY(marks.contains(b1->line())); 0345 QVERIFY(marks.contains(b2->line())); 0346 0347 QVERIFY(doc->save()); 0348 QVERIFY(doc->close()); 0349 QCOMPARE(doc->state(), IDocument::Clean); 0350 0351 // Wait needed for BreakpointModel::save() to complete. 0352 QTest::qWait(1); 0353 0354 // verify. 0355 QVERIFY(!b1->movingCursor()); 0356 QVERIFY(!b2->movingCursor()); 0357 QCOMPARE(b1->line(), 22); 0358 QCOMPARE(b2->line(), 24); 0359 const auto savedBreakpoints = readBreakpointsFromConfig(); 0360 QCOMPARE(savedBreakpoints.size(), 2); 0361 QCOMPARE(savedBreakpoints.at(0)->line(), 22); 0362 QCOMPARE(savedBreakpoints.at(1)->line(), 24); 0363 QCOMPARE(savedBreakpoints.at(1)->condition(), "*i > 0"); 0364 QCOMPARE(savedBreakpoints.at(1)->ignoreHits(), 1); 0365 } 0366 0367 void TestBreakpointModel::testDocumentEditAndSave() 0368 { 0369 const auto [url, doc, b1, b2] = setupEditAndCheckPrimaryDocumentAndBreakpoints(); 0370 RETURN_IF_TEST_FAILED(); 0371 RETURN_IF_TEST_ABORTED(); 0372 0373 // save() shall make the breakpoint tracked line numbers persistent. 0374 // After saving, close() should have no effect on the line numbers 0375 // other than detaching all moving cursors from the document. 0376 QVERIFY(doc->save()); 0377 QVERIFY(doc->close()); 0378 QCOMPARE(doc->state(), IDocument::Clean); 0379 0380 // Wait needed for BreakpointModel::save() to complete. 0381 QTest::qWait(1); 0382 0383 // verify. 0384 QVERIFY(!b1->movingCursor()); 0385 QVERIFY(!b2->movingCursor()); 0386 QCOMPARE(b1->line(), 23); 0387 QEXPECT_FAIL("", "CodeBreakpoint b2 tracked line number is not retained", Continue); 0388 QCOMPARE(b2->line(), 24); 0389 const auto savedBreakpoints = readBreakpointsFromConfig(); 0390 QCOMPARE(savedBreakpoints.size(), 2); 0391 QCOMPARE(savedBreakpoints.at(0)->line(), 23); 0392 QEXPECT_FAIL("", "CodeBreakpoint b2 tracked line number is not persistent", Continue); 0393 QCOMPARE(savedBreakpoints.at(1)->line(), 24); 0394 } 0395 0396 void TestBreakpointModel::testDocumentEditAndDiscard() 0397 { 0398 const auto [url, doc, b1, b2] = setupEditAndCheckPrimaryDocumentAndBreakpoints(); 0399 RETURN_IF_TEST_FAILED(); 0400 RETURN_IF_TEST_ABORTED(); 0401 0402 // After discarding text changes b2->line() reverts to the line number 23. 0403 // This is where the text line tracking was started by addCodeBreakpoint() for b2. 0404 // Same applies to b1->line(), tracking which was started at the line 21 by openDocument(). 0405 // Ideally b2->line() should be 22 after the discard, but this would be hard to implement. 0406 QVERIFY(doc->close(IDocument::DocumentSaveMode::Discard)); 0407 0408 // Wait needed for BreakpointModel::save() to complete. 0409 QTest::qWait(1); 0410 0411 // verify. 0412 QVERIFY(!b1->movingCursor()); 0413 QVERIFY(!b2->movingCursor()); 0414 QCOMPARE(b1->line(), 21); 0415 QCOMPARE(b2->line(), 23); 0416 const auto savedBreakpoints = readBreakpointsFromConfig(); 0417 QCOMPARE(savedBreakpoints.size(), 2); 0418 QCOMPARE(savedBreakpoints.at(0)->line(), 21); 0419 QCOMPARE(savedBreakpoints.at(1)->line(), 23); 0420 } 0421 0422 #include "moc_test_breakpointmodel.cpp"