diff --git a/src/parser/backtraceparsergdb.cpp b/src/parser/backtraceparsergdb.cpp index 82f660ce..1f57ac10 100644 --- a/src/parser/backtraceparsergdb.cpp +++ b/src/parser/backtraceparsergdb.cpp @@ -1,294 +1,291 @@ /* Copyright (C) 2009-2010 George Kiagiadakis This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "backtraceparsergdb.h" #include "backtraceparser_p.h" #include "drkonqi_parser_debug.h" #include +#include //BEGIN BacktraceLineGdb -class BacktraceLineGdb : public BacktraceLine -{ -public: - BacktraceLineGdb(const QString & line); - -private: - void parse(); - void rate(); -}; - BacktraceLineGdb::BacktraceLineGdb(const QString & lineStr) : BacktraceLine() { d->m_line = lineStr; d->m_functionName = QLatin1String("??"); parse(); if (d->m_type == StackFrame) { rate(); } } void BacktraceLineGdb::parse() { QRegExp regExp; if (d->m_line == QLatin1Char('\n')) { d->m_type = EmptyLine; return; } else if (d->m_line == QLatin1String("[KCrash Handler]\n")) { d->m_type = KCrash; return; } else if (d->m_line.contains(QLatin1String(""))) { d->m_type = SignalHandlerStart; return; } regExp.setPattern(QStringLiteral("^#([0-9]+)" //matches the stack frame number, ex. "#0" "[\\s]+(0x[0-9a-f]+[\\s]+in[\\s]+)?" // matches " 0x0000dead in " (optionally) - "((\\(anonymous namespace\\)::)?[^\\(]+)" //matches the function name + "((\\(anonymous namespace\\)::)?[^\\(]+)?" //matches the function name //(anything except left parenthesis, which is the start of the arguments section) //and optionally the prefix "(anonymous namespace)::" "(\\(.*\\))?" //matches the function arguments //(when the app doesn't have debugging symbols) "[\\s]+(const[\\s]+)?" //matches a traling const, if it exists "\\(.*\\)" //matches the arguments of the function with their values //(when the app has debugging symbols) "([\\s]+" //beginning of optional file information "(from|at)[\\s]+" //matches "from " or "at " "(.+)" //matches the filename (source file or shared library file) ")?\n$")); //matches trailing newline. //the )? at the end closes the parenthesis before [\\s]+(from|at) and //notes that the whole expression from there is optional. if (regExp.exactMatch(d->m_line)) { d->m_type = StackFrame; d->m_stackFrameNumber = regExp.cap(1).toInt(); d->m_functionName = regExp.cap(3).trimmed(); if (!regExp.cap(7).isEmpty()) { //we have file information (stuff after from|at) - if (regExp.cap(8) == QLatin1String("at")) { //'at' means we have a source file + bool file = regExp.cap(8) == QLatin1String("at"); //'at' means we have a source file (likely) + // Gdb isn't entirely consistent here, when it uses 'from' it always refers to a library, but + // sometimes the stack can resolve to a library even when it uses the 'at' key word. + // This specifically seems to happen when a frame has no function name. + const QString path = regExp.cap(9); + file = file && !QFileInfo(path).completeSuffix().contains(QLatin1String(".so")); + if (file) { d->m_file = regExp.cap(9); } else { //'from' means we have a library d->m_library = regExp.cap(9); } } qCDebug(DRKONQI_PARSER_LOG) << d->m_stackFrameNumber << d->m_functionName << d->m_file << d->m_library; return; } regExp.setPattern(QStringLiteral(".*\\(no debugging symbols found\\).*|" ".*\\[Thread debugging using libthread_db enabled\\].*|" ".*\\[New .*|" "0x[0-9a-f]+.*|" "Current language:.*")); if (regExp.exactMatch(d->m_line)) { qCDebug(DRKONQI_PARSER_LOG) << "garbage detected:" << d->m_line; d->m_type = Crap; return; } regExp.setPattern(QStringLiteral("Thread [0-9]+\\s+\\(Thread [0-9a-fx]+\\s+\\(.*\\)\\):\n")); if (regExp.exactMatch(d->m_line)) { qCDebug(DRKONQI_PARSER_LOG) << "thread start detected:" << d->m_line; d->m_type = ThreadStart; return; } regExp.setPattern(QStringLiteral("\\[Current thread is [0-9]+ \\(.*\\)\\]\n")); if (regExp.exactMatch(d->m_line)) { qCDebug(DRKONQI_PARSER_LOG) << "thread indicator detected:" << d->m_line; d->m_type = ThreadIndicator; return; } qCDebug(DRKONQI_PARSER_LOG) << "line" << d->m_line << "did not match"; } void BacktraceLineGdb::rate() { LineRating r; //for explanations, see the LineRating enum definition if (!fileName().isEmpty()) { r = Good; } else if (!libraryName().isEmpty()) { if (functionName() == QLatin1String("??")) { r = MissingFunction; } else { r = MissingSourceFile; } } else { if (functionName() == QLatin1String("??")) { r = MissingEverything; } else { r = MissingLibrary; } } d->m_rating = r; } //END BacktraceLineGdb //BEGIN BacktraceParserGdb class BacktraceParserGdbPrivate : public BacktraceParserPrivate { public: BacktraceParserGdbPrivate() : BacktraceParserPrivate(), m_possibleKCrashStart(0), m_threadsCount(0), m_isBelowSignalHandler(false), m_frameZeroAppeared(false) {} QString m_lineInputBuffer; int m_possibleKCrashStart; int m_threadsCount; bool m_isBelowSignalHandler; bool m_frameZeroAppeared; }; BacktraceParserGdb::BacktraceParserGdb(QObject *parent) : BacktraceParser(parent) { } BacktraceParserPrivate* BacktraceParserGdb::constructPrivate() const { return new BacktraceParserGdbPrivate; } void BacktraceParserGdb::newLine(const QString & lineStr) { Q_D(BacktraceParserGdb); //when the line is too long, gdb splits it into two lines. //This breaks parsing and results in two Unknown lines instead of a StackFrame one. //Here we workaround this by joining the two lines when such a scenario is detected. if (d->m_lineInputBuffer.isEmpty()) { d->m_lineInputBuffer = lineStr; } else if (lineStr.startsWith(QLatin1Char(' ')) || lineStr.startsWith(QLatin1Char('\t'))) { //gdb always adds some whitespace at the beginning of the second line d->m_lineInputBuffer.append(lineStr); } else { parseLine(d->m_lineInputBuffer); d->m_lineInputBuffer = lineStr; } } void BacktraceParserGdb::parseLine(const QString & lineStr) { Q_D(BacktraceParserGdb); BacktraceLineGdb line(lineStr); switch (line.type()) { case BacktraceLine::Crap: break; //we don't want crap in the backtrace ;) case BacktraceLine::ThreadStart: d->m_linesList.append(line); d->m_possibleKCrashStart = d->m_linesList.size(); d->m_threadsCount++; //reset the state of the flags that need to be per-thread d->m_isBelowSignalHandler = false; d->m_frameZeroAppeared = false; // gdb bug workaround flag, see below break; case BacktraceLine::SignalHandlerStart: if (!d->m_isBelowSignalHandler) { //replace the stack frames of KCrash with a nice message d->m_linesList.erase(d->m_linesList.begin() + d->m_possibleKCrashStart, d->m_linesList.end()); d->m_linesList.insert(d->m_possibleKCrashStart, BacktraceLineGdb(QStringLiteral("[KCrash Handler]\n"))); d->m_isBelowSignalHandler = true; //next line is the first below the signal handler } else { //this is not the first time we see a crash handler frame on the same thread, //so we just add it to the list d->m_linesList.append(line); } break; case BacktraceLine::StackFrame: // gdb workaround - (v6.8 at least) - 'thread apply all bt' writes // the #0 stack frame again at the end. // Here we ignore this frame by using a flag that tells us whether // this is the first or the second time that the #0 frame appears in this thread. // The flag is cleared on each thread start. if (line.frameNumber() == 0) { if (d->m_frameZeroAppeared) { break; //break from the switch so that the frame is not added to the list. } else { d->m_frameZeroAppeared = true; } } //rate the stack frame if we are below the signal handler if (d->m_isBelowSignalHandler) { d->m_linesToRate.append(line); } Q_FALLTHROUGH(); //fall through and append the line to the list default: d->m_linesList.append(line); break; } } QString BacktraceParserGdb::parsedBacktrace() const { Q_D(const BacktraceParserGdb); QString result; if (d) { QList::const_iterator i; for (i = d->m_linesList.constBegin(); i != d->m_linesList.constEnd(); ++i) { //if there is only one thread, we can omit the thread indicator, //the thread header and all the empty lines. if (d->m_threadsCount == 1 && ((*i).type() == BacktraceLine::ThreadIndicator || (*i).type() == BacktraceLine::ThreadStart || (*i).type() == BacktraceLine::EmptyLine)) { continue; } result += i->toString(); } } return result; } QList BacktraceParserGdb::parsedBacktraceLines() const { Q_D(const BacktraceParserGdb); QList result; if (d) { QList::const_iterator i; for (i = d->m_linesList.constBegin(); i != d->m_linesList.constEnd(); ++i) { //if there is only one thread, we can omit the thread indicator, //the thread header and all the empty lines. if (d->m_threadsCount == 1 && ((*i).type() == BacktraceLine::ThreadIndicator || (*i).type() == BacktraceLine::ThreadStart || (*i).type() == BacktraceLine::EmptyLine)) { continue; } result.append(*i); } } return result; } //END BacktraceParserGdb diff --git a/src/parser/backtraceparsergdb.h b/src/parser/backtraceparsergdb.h index 0ce78b27..199b0f5f 100644 --- a/src/parser/backtraceparsergdb.h +++ b/src/parser/backtraceparsergdb.h @@ -1,44 +1,54 @@ /* Copyright (C) 2009-2010 George Kiagiadakis This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef BACKTRACEPARSERGDB_H #define BACKTRACEPARSERGDB_H #include "backtraceparser.h" class BacktraceParserGdbPrivate; +class BacktraceLineGdb : public BacktraceLine +{ +public: + BacktraceLineGdb(const QString & line); + +private: + void parse(); + void rate(); +}; + class BacktraceParserGdb : public BacktraceParser { Q_OBJECT Q_DECLARE_PRIVATE(BacktraceParserGdb) public: explicit BacktraceParserGdb(QObject *parent = nullptr); QString parsedBacktrace() const override; QList parsedBacktraceLines() const override; protected: BacktraceParserPrivate *constructPrivate() const override; protected Q_SLOTS: void newLine(const QString & lineStr) override; private: void parseLine(const QString & lineStr); }; #endif // BACKTRACEPARSERGDB_H diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index c8848957..cb21d548 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,68 +1,76 @@ if(NOT BUILD_TESTING) # Skip everything. Particularly trying to look for integration test deps. return() endif() remove_definitions(-DQT_NO_CAST_FROM_ASCII) find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG REQUIRED) # Test helper for systeminformationtest if(HAVE_UNAME) add_executable(lsb_release lsb_release_double.c) ecm_add_tests( systeminformationtest LINK_LIBRARIES Qt5::Core Qt5::Test DrKonqiInternal ) endif() add_subdirectory(crashtest) add_subdirectory(backtraceparsertest) add_subdirectory(bugzillalibtest) +ecm_add_tests( + gdbbacktracelinetest + LINK_LIBRARIES + Qt5::Core + Qt5::Test + drkonqi_backtrace_parser +) + if(NOT APPLE) if(NOT RUBY_EXECTUABLE) find_program(RUBY_EXECUTABLE ruby) endif() if(RUBY_EXECUTABLE) execute_process(COMMAND ${RUBY_EXECUTABLE} -e "require 'atspi'" RESULT_VARIABLE RUBY_ATSPI) endif() if(NOT GDB_EXECUTABLE) # Needed so drkonqi can actually trace something. find_program(GDB_EXECUTABLE gdb) endif() if(NOT XVFB_RUN_EXECTUABLE) find_program(XVFB_RUN_EXECTUABLE xvfb-run) endif() set(ATSPI_PATHS /usr/lib/at-spi2-core/ # debians /usr/lib/at-spi2/ # suses ) if(NOT ATSPI_BUS_LAUNCHER_EXECUTABLE) find_program(ATSPI_BUS_LAUNCHER_EXECUTABLE NAMES at-spi-bus-launcher PATHS ${ATSPI_PATHS} DOC "AT-SPI accessibility dbus launcher") endif() if(NOT ATSPI_REGISTRYD_EXECUTABLE) find_program(ATSPI_REGISTRYD_EXECUTABLE NAMES at-spi2-registryd PATHS ${ATSPI_PATHS} DOC "AT-SPI accessibility registry daemon") endif() if(RUBY_EXECUTABLE AND XVFB_RUN_EXECTUABLE AND ATSPI_BUS_LAUNCHER_EXECUTABLE AND ATSPI_REGISTRYD_EXECUTABLE AND GDB_EXECUTABLE AND RUBY_ATSPI EQUAL 0) set(WITH_DRKONI_INTEGRATION_TESTING TRUE) add_subdirectory(integration) endif() add_feature_info(DrKonqiIntegrationTesting WITH_DRKONI_INTEGRATION_TESTING "Needs Ruby, functional atspi gem, gdb, as well as xvfb-run.") endif() diff --git a/src/tests/gdbbacktracelinetest.cpp b/src/tests/gdbbacktracelinetest.cpp new file mode 100644 index 00000000..4fa11f1d --- /dev/null +++ b/src/tests/gdbbacktracelinetest.cpp @@ -0,0 +1,98 @@ +/* + Copyright 2020 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include + +#include "../parser/backtraceparsergdb.h" + +class GdbBacktraceLineTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + // rating() is often times somewhat misleading because it is an exclusive state + // but in practise a frame may have multiple issues. for purposes of rating all + // issues are considered equal. it's not ideal though, a frame that is missing + // multiple elements is objectively worse than a frame that is just missing one. + + void testComplete() + { + BacktraceLineGdb line( + "#7 0x00007f468b177bfa in KMime::DateFormatterPrivate::localized (t=t@entry=1579263464, shortFormat=shortFormat@entry=true, lang=...) at /usr/src/debug/kmime-19.12.1-lp151.150.1.x86_64/src/kmime_dateformatter.cpp:310\n" + ); + QCOMPARE(line.type(), BacktraceLine::StackFrame); + QCOMPARE(line.frameNumber(), 7); + QCOMPARE(line.functionName(), "KMime::DateFormatterPrivate::localized"); + QCOMPARE(line.fileName(), "/usr/src/debug/kmime-19.12.1-lp151.150.1.x86_64/src/kmime_dateformatter.cpp:310"); + QCOMPARE(line.libraryName(), ""); + QCOMPARE(line.rating(), BacktraceLine::Good); + } + + void testPoorFile() + { + BacktraceLineGdb line("#41 0x00007f4684ae4e87 in g_main_context_dispatch () from /usr/lib64/libglib-2.0.so.0\n"); + QCOMPARE(line.type(), BacktraceLine::StackFrame); + QCOMPARE(line.frameNumber(), 41); + QCOMPARE(line.functionName(), "g_main_context_dispatch"); + QCOMPARE(line.fileName(), ""); + QCOMPARE(line.libraryName(), "/usr/lib64/libglib-2.0.so.0"); + QCOMPARE(line.rating(), BacktraceLine::MissingSourceFile); + } + + void testNoFunctionPoorFile() + { + // Also uses 'at' keyword while referring to a library, this used to trip up + // the parser and make it think there's a source file, when in reality there + // is not. + // Lacking a function name further tripped up the parsing because originally + // couldn't deal with the function name being missing entirely. + // As a result this line used to rate as 'Good' -.- + // https://bugs.kde.org/show_bug.cgi?id=416923 + BacktraceLineGdb line("#13 0x00007fe6059971b1 in () at /usr/lib/libglib-2.0.so.0\n"); + QCOMPARE(line.type(), BacktraceLine::StackFrame); + QCOMPARE(line.frameNumber(), 13); + QCOMPARE(line.functionName(), ""); + QCOMPARE(line.fileName(), ""); + QCOMPARE(line.libraryName(), "/usr/lib/libglib-2.0.so.0"); + QCOMPARE(line.rating(), BacktraceLine::MissingSourceFile); + } + + void testOnlyFunctionNofile() + { + BacktraceLineGdb line("#20 0x0000557e978c1b7e in _start ()\n"); + QCOMPARE(line.type(), BacktraceLine::StackFrame); + QCOMPARE(line.frameNumber(), 20); + QCOMPARE(line.functionName(), "_start"); + QCOMPARE(line.fileName(), ""); + QCOMPARE(line.libraryName(), ""); + QCOMPARE(line.rating(), BacktraceLine::MissingLibrary); + } + + void testInferiorMarker() + { + BacktraceLineGdb line("[Inferior 1 (process 72692) detached]\n"); + QCOMPARE(line.type(), BacktraceLine::Unknown); + QCOMPARE(line.rating(), BacktraceLine::InvalidRating); + } +}; + +QTEST_GUILESS_MAIN(GdbBacktraceLineTest) + +#include "gdbbacktracelinetest.moc"