diff --git a/debuggers/CMakeLists.txt b/debuggers/CMakeLists.txt --- a/debuggers/CMakeLists.txt +++ b/debuggers/CMakeLists.txt @@ -1,3 +1,21 @@ +function(add_debuggable_executable target) + cmake_parse_arguments(add_debuggable_executable "" "" "SRCS" ${ARGN}) + add_executable(${target} ${add_debuggable_executable_SRCS}) + # force debug symbols for our debuggees, disable optimizations + if (WIN32) + set(_flags "/0d") + else() + set(_flags "-O0") + endif() + set_target_properties(${target} PROPERTIES COMPILE_FLAGS "${CMAKE_CXX_FLAGS_DEBUG} ${_flags}") +endfunction() + +if (CMAKE_VERSION VERSION_GREATER "2.9" OR NOT CMAKE_GENERATOR MATCHES "Ninja") + set(HAVE_PATH_WITH_SPACES_TEST TRUE) +else() + message(WARNING "Disabling 'path with spaces' test, this CMake version would create a faulty build.ninja file. Upgrade to at least CMake v3.0") +endif() + include_directories(common) if(NOT WIN32) diff --git a/debuggers/common/CMakeLists.txt b/debuggers/common/CMakeLists.txt --- a/debuggers/common/CMakeLists.txt +++ b/debuggers/common/CMakeLists.txt @@ -50,3 +50,17 @@ endif() kde_target_enable_exceptions(kdevdebuggercommon PUBLIC) + + +# Unit test helpers +add_library(kdevdebugger_unittest STATIC + unittest/unittest.cpp +) +target_link_libraries(kdevdebugger_unittest + kdevdebuggercommon + Qt5::Core + Qt5::Test +) + +# debugees for unit tests +add_subdirectory(unittest/debugees) diff --git a/debuggers/gdb/unittests/CMakeLists.txt b/debuggers/common/unittest/debugees/CMakeLists.txt rename from debuggers/gdb/unittests/CMakeLists.txt rename to debuggers/common/unittest/debugees/CMakeLists.txt diff --git a/debuggers/gdb/unittests/debugee space.cpp b/debuggers/common/unittest/debugees/debugee space.cpp rename from debuggers/gdb/unittests/debugee space.cpp rename to debuggers/common/unittest/debugees/debugee space.cpp diff --git a/debuggers/gdb/unittests/debugee.cpp b/debuggers/common/unittest/debugees/debugee.cpp rename from debuggers/gdb/unittests/debugee.cpp rename to debuggers/common/unittest/debugees/debugee.cpp diff --git a/debuggers/gdb/unittests/debugeecrash.cpp b/debuggers/common/unittest/debugees/debugeecrash.cpp rename from debuggers/gdb/unittests/debugeecrash.cpp rename to debuggers/common/unittest/debugees/debugeecrash.cpp diff --git a/debuggers/gdb/unittests/debugeeexception.cpp b/debuggers/common/unittest/debugees/debugeeexception.cpp rename from debuggers/gdb/unittests/debugeeexception.cpp rename to debuggers/common/unittest/debugees/debugeeexception.cpp diff --git a/debuggers/gdb/unittests/debugeemultilocbreakpoint.cpp b/debuggers/common/unittest/debugees/debugeemultilocbreakpoint.cpp rename from debuggers/gdb/unittests/debugeemultilocbreakpoint.cpp rename to debuggers/common/unittest/debugees/debugeemultilocbreakpoint.cpp diff --git a/debuggers/gdb/unittests/debugeemultiplebreakpoint.cpp b/debuggers/common/unittest/debugees/debugeemultiplebreakpoint.cpp rename from debuggers/gdb/unittests/debugeemultiplebreakpoint.cpp rename to debuggers/common/unittest/debugees/debugeemultiplebreakpoint.cpp diff --git a/debuggers/gdb/unittests/debugeeqt.cpp b/debuggers/common/unittest/debugees/debugeeqt.cpp rename from debuggers/gdb/unittests/debugeeqt.cpp rename to debuggers/common/unittest/debugees/debugeeqt.cpp diff --git a/debuggers/gdb/unittests/debugeerecursion.cpp b/debuggers/common/unittest/debugees/debugeerecursion.cpp rename from debuggers/gdb/unittests/debugeerecursion.cpp rename to debuggers/common/unittest/debugees/debugeerecursion.cpp diff --git a/debuggers/gdb/unittests/debugeeslow.cpp b/debuggers/common/unittest/debugees/debugeeslow.cpp rename from debuggers/gdb/unittests/debugeeslow.cpp rename to debuggers/common/unittest/debugees/debugeeslow.cpp diff --git a/debuggers/gdb/unittests/debugeethreads.cpp b/debuggers/common/unittest/debugees/debugeethreads.cpp rename from debuggers/gdb/unittests/debugeethreads.cpp rename to debuggers/common/unittest/debugees/debugeethreads.cpp diff --git a/debuggers/gdb/unittests/path with space/CMakeLists.txt b/debuggers/common/unittest/debugees/path with space/CMakeLists.txt rename from debuggers/gdb/unittests/path with space/CMakeLists.txt rename to debuggers/common/unittest/debugees/path with space/CMakeLists.txt diff --git a/debuggers/gdb/unittests/path with space/spacedebugee.cpp b/debuggers/common/unittest/debugees/path with space/spacedebugee.cpp rename from debuggers/gdb/unittests/path with space/spacedebugee.cpp rename to debuggers/common/unittest/debugees/path with space/spacedebugee.cpp diff --git a/debuggers/common/unittest/unittest.h b/debuggers/common/unittest/unittest.h new file mode 100644 --- /dev/null +++ b/debuggers/common/unittest/unittest.h @@ -0,0 +1,82 @@ +/* + * Common helpers for MI debugger unit tests + * Copyright 2016 Aetf + * + * 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) 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 14 of version 3 of the license. + * + * 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, see . + * + */ + +#ifndef KDEVMI_UNITTEST_H +#define KDEVMI_UNITTEST_H + +#include + +#include +#include +#include +#include + +namespace KDevMI { + +class MIDebugSession; + +namespace UnitTest { + +QUrl findExecutable(const QString& name); +QString findSourceFile(const char *file, const QString& name); +bool isAttachForbidden(const char *file, int line); + +void compareData(QModelIndex index, QString expected, const char *file, int line); + +bool waitForState(MIDebugSession *session, KDevelop::IDebugSession::DebuggerState state, + const char *file, int line, bool waitForIdle = false); + +class TestWaiter +{ +public: + TestWaiter(MIDebugSession * session_, const char * condition_, const char * file_, int line_); + + bool waitUnless(bool ok); + +private: + QTime stopWatch; + QPointer session; + const char * condition; + const char * file; + int line; +}; + + +} // end of namespace UnitTest +} // end of namespace KDevMI + +#define WAIT_FOR_STATE(session, state) \ + do { if (!KDevMI::UnitTest::waitForState((session), (state), __FILE__, __LINE__)) return; } while (0) + +#define WAIT_FOR_STATE_AND_IDLE(session, state) \ + do { if (!KDevMI::UnitTest::waitForState((session), (state), __FILE__, __LINE__, true)) return; } while (0) + +#define WAIT_FOR(session, condition) \ + do { \ + KDevMI::UnitTest::TestWaiter w((session), #condition, __FILE__, __LINE__); \ + while (w.waitUnless((condition))) /* nothing */ ; \ + } while(0) + +#define COMPARE_DATA(index, expected) \ + KDevMI::UnitTest::compareData((index), (expected), __FILE__, __LINE__) + +#endif // KDEVMI_UNITTEST_H diff --git a/debuggers/common/unittest/unittest.cpp b/debuggers/common/unittest/unittest.cpp new file mode 100644 --- /dev/null +++ b/debuggers/common/unittest/unittest.cpp @@ -0,0 +1,155 @@ +/* + * Common helpers for MI debugger unit tests + * Copyright 2016 Aetf + * + * 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) 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 14 of version 3 of the license. + * + * 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, see . + * + */ + +#include "unittest.h" + +#include "midebugsession.h" + +#include +#include +#include +#include +#include + +namespace KDevMI { namespace UnitTest { + +QUrl findExecutable(const QString& name) +{ + QFileInfo info(qApp->applicationDirPath() + "/../common/unittest/debugees/" + name); + Q_ASSERT(info.exists()); + Q_ASSERT(info.isExecutable()); + return QUrl::fromLocalFile(info.canonicalFilePath()); +} + +// First try to find file in the same folder as `file`, +// if not found, find it at common/unittest/debugees +QString findSourceFile(const char *file, const QString& name) +{ + QDir baseDir = QFileInfo(file).dir(); + QFileInfo info(baseDir.absoluteFilePath(name)); + if (info.exists()) { + return info.canonicalFilePath(); + } + + baseDir = QFileInfo(__FILE__).dir(); + baseDir.cd("debugees"); + info = baseDir.absoluteFilePath(name); + Q_ASSERT(info.exists()); + return info.canonicalFilePath(); +} + +bool isAttachForbidden(const char *file, int line) +{ + // if on linux, ensure we can actually attach + QFile canRun("/proc/sys/kernel/yama/ptrace_scope"); + if (canRun.exists()) { + if (!canRun.open(QIODevice::ReadOnly)) { + QTest::qFail("Something is wrong: /proc/sys/kernel/yama/ptrace_scope exists but cannot be read", file, line); + return true; + } + if (canRun.read(1).toInt() != 0) { + QTest::qSkip("ptrace attaching not allowed, skipping test. To enable it, set /proc/sys/kernel/yama/ptrace_scope to 0.", file, line); + return true; + } + } + + return false; +} + +void compareData(QModelIndex index, QString expected, const char *file, int line) +{ + QString s = index.model()->data(index, Qt::DisplayRole).toString(); + if (s != expected) { + QFAIL(qPrintable(QString("'%0' didn't match expected '%1' in %2:%3").arg(s).arg(expected).arg(file).arg(line))); + } +} + +bool waitForState(MIDebugSession *session, KDevelop::IDebugSession::DebuggerState state, + const char *file, int line, bool waitForIdle) +{ + QPointer s(session); //session can get deleted in DebugController + QTime stopWatch; + stopWatch.start(); + + // legacy behavior for tests that implicitly may require waiting for idle, + // but which were written before waitForIdle was added + waitForIdle = waitForIdle || state != MIDebugSession::EndedState; + + while (s && (s->state() != state + || (waitForIdle && s->debuggerStateIsOn(s_dbgBusy)))) { + if (stopWatch.elapsed() > 5000) { + qWarning() << "current state" << s->state() << "waiting for" << state; + QTest::qFail(qPrintable(QString("Timeout before reaching state %0").arg(state)), + file, line); + return false; + } + QTest::qWait(20); + } + + // NOTE: don't wait anymore after leaving the loop. Waiting reenters event loop and + // may change session state. + + if (!s && state != MIDebugSession::EndedState) { + QTest::qFail(qPrintable(QString("Session ended before reaching state %0").arg(state)), + file, line); + return false; + } + + qDebug() << "Reached state " << state << " in " << file << ':' << line; + return true; +} + +TestWaiter::TestWaiter(MIDebugSession * session_, const char * condition_, const char * file_, int line_) + : session(session_) + , condition(condition_) + , file(file_) + , line(line_) +{ + stopWatch.start(); +} + +bool TestWaiter::waitUnless(bool ok) +{ + if (ok) { + qDebug() << "Condition " << condition << " reached in " << file << ':' << line; + return false; + } + + if (stopWatch.elapsed() > 5000) { + QTest::qFail(qPrintable(QString("Timeout before reaching condition %0").arg(condition)), + file, line); + return false; + } + + QTest::qWait(100); + + if (!session) { + QTest::qFail(qPrintable(QString("Session ended without reaching condition %0").arg(condition)), + file, line); + return false; + } + + return true; +} + +} // end of namespace UnitTest +} // end of namespace KDevMI diff --git a/debuggers/gdb/CMakeLists.txt b/debuggers/gdb/CMakeLists.txt --- a/debuggers/gdb/CMakeLists.txt +++ b/debuggers/gdb/CMakeLists.txt @@ -1,25 +1,6 @@ project(gdb) add_definitions(-DTRANSLATION_DOMAIN=\"kdevgdb\") -function(add_debuggable_executable target) - cmake_parse_arguments(add_debuggable_executable "" "" "SRCS" ${ARGN}) - add_executable(${target} ${add_debuggable_executable_SRCS}) - # force debug symbols for our debuggees, disable optimizations - if (WIN32) - set(_flags "/0d") - else() - set(_flags "-O0") - endif() - set_target_properties(${target} PROPERTIES COMPILE_FLAGS "${CMAKE_CXX_FLAGS_DEBUG} ${_flags}") -endfunction() - -if (CMAKE_VERSION VERSION_GREATER "2.9" OR NOT CMAKE_GENERATOR MATCHES "Ninja") - set(HAVE_PATH_WITH_SPACES_TEST TRUE) -else() - message(WARNING "Disabling 'path with spaces' test, this CMake version would create a faulty build.ninja file. Upgrade to at least CMake v3.0") -endif() - -add_subdirectory(unittests) add_subdirectory(printers) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) @@ -93,6 +74,7 @@ KF5::TextEditor KF5::Parts kdevdebuggercommon + kdevdebugger_unittest ) if (HAVE_PATH_WITH_SPACES_TEST) diff --git a/debuggers/gdb/unittests/test_gdb.h b/debuggers/gdb/unittests/test_gdb.h --- a/debuggers/gdb/unittests/test_gdb.h +++ b/debuggers/gdb/unittests/test_gdb.h @@ -98,10 +98,6 @@ void testPathWithSpace(); private: - bool waitForState(DebugSession *session, - KDevelop::IDebugSession::DebuggerState state, - const char *file, int line, - bool waitForIdle = false); IExecutePlugin* m_iface; }; diff --git a/debuggers/gdb/unittests/test_gdb.cpp b/debuggers/gdb/unittests/test_gdb.cpp --- a/debuggers/gdb/unittests/test_gdb.cpp +++ b/debuggers/gdb/unittests/test_gdb.cpp @@ -24,6 +24,7 @@ #include "mi/micommand.h" #include "mi/milexer.h" #include "mi/miparser.h" +#include "unittest/unittest.h" #include #include @@ -53,48 +54,19 @@ #include using KDevelop::AutoTestShell; - -namespace KDevMI { namespace GDB { - -QUrl findExecutable(const QString& name) -{ - QFileInfo info(qApp->applicationDirPath() + "/unittests/" + name); - Q_ASSERT(info.exists()); - Q_ASSERT(info.isExecutable()); - return QUrl::fromLocalFile(info.canonicalFilePath()); -} - -QString findSourceFile(const QString& name) -{ - QFileInfo info(QFileInfo(__FILE__).dir().absoluteFilePath(name)); - Q_ASSERT(info.exists()); - return info.canonicalFilePath(); -} - -static bool isAttachForbidden(const char * file, int line) -{ - // if on linux, ensure we can actually attach - QFile canRun("/proc/sys/kernel/yama/ptrace_scope"); - if (canRun.exists()) { - if (!canRun.open(QIODevice::ReadOnly)) { - QTest::qFail("Something is wrong: /proc/sys/kernel/yama/ptrace_scope exists but cannot be read", file, line); - return true; - } - if (canRun.read(1).toInt() != 0) { - QTest::qSkip("ptrace attaching not allowed, skipping test. To enable it, set /proc/sys/kernel/yama/ptrace_scope to 0.", file, line); - return true; - } - } - - return false; -} +using namespace KDevMI::UnitTest; #define SKIP_IF_ATTACH_FORBIDDEN() \ do { \ if (isAttachForbidden(__FILE__, __LINE__)) \ return; \ } while(0) +#define findSourceFile(name) \ + findSourceFile(__FILE__, (name)) + +namespace KDevMI { namespace GDB { + void GdbTest::initTestCase() { AutoTestShell::init(); @@ -241,29 +213,6 @@ int line; }; -#define WAIT_FOR_STATE(session, state) \ - do { if (!waitForState((session), (state), __FILE__, __LINE__)) return; } while (0) - -#define WAIT_FOR_STATE_AND_IDLE(session, state) \ - do { if (!waitForState((session), (state), __FILE__, __LINE__, true)) return; } while (0) - -#define WAIT_FOR(session, condition) \ - do { \ - TestWaiter w((session), #condition, __FILE__, __LINE__); \ - while (w.waitUnless((condition))) /* nothing */ ; \ - } while(0) - -#define COMPARE_DATA(index, expected) \ - compareData((index), (expected), __FILE__, __LINE__) - -void compareData(QModelIndex index, QString expected, const char *file, int line) -{ - QString s = index.model()->data(index, Qt::DisplayRole).toString(); - if (s != expected) { - QFAIL(qPrintable(QString("'%0' didn't match expected '%1' in %2:%3").arg(s).arg(expected).arg(file).arg(line))); - } -} - static const QString debugeeFileName = findSourceFile("debugee.cpp"); KDevelop::BreakpointModel* breakpoints() @@ -1994,37 +1943,6 @@ #endif } -bool GdbTest::waitForState(DebugSession *session, DebugSession::DebuggerState state, - const char *file, int line, bool waitForIdle) -{ - QPointer s(session); //session can get deleted in DebugController - QTime stopWatch; - stopWatch.start(); - while (s.data()->state() != state || (waitForIdle && s->debuggerStateIsOn(s_dbgBusy))) { - if (stopWatch.elapsed() > 5000) { - qWarning() << "current state" << s.data()->state() << "waiting for" << state; - QTest::qFail(qPrintable(QString("Timeout before reaching state %0").arg(state)), - file, line); - return false; - } - QTest::qWait(20); - if (!s) { - if (state == DebugSession::EndedState) - break; - QTest::qFail(qPrintable(QString("Session ended before reaching state %0").arg(state)), - file, line); - return false; - } - } - if (!waitForIdle && state != DebugSession::EndedState) { - // legacy behavior for tests that implicitly may require waiting for idle, - // but which were written before waitForIdle was added - QTest::qWait(100); - } - - qDebug() << "Reached state " << state << " in " << file << ':' << line; - return true; -} } // end of namespace GDB } // end of namespace KDevMI