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"