Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "debugutil_p.h"
#include "qqmldebugprocess_p.h"
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <private/qqmlenginedebugclient_p.h>
#include <private/qv4debugclient_p.h>
#include <private/qqmldebugconnection_p.h>
#include <private/qpacket_p.h>
#include <QtTest/qtest.h>
#include <QtTest/qtestsystem.h>
#include <QtCore/qprocess.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qdir.h>
#include <QtCore/qmutex.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qjsonobject.h>
#include <QtCore/qjsonarray.h>
const char *BLOCKMODE = "-qmljsdebugger=port:3771,3800,block";
const char *NORMALMODE = "-qmljsdebugger=port:3771,3800";
const char *BLOCKRESTRICTEDMODE = "-qmljsdebugger=port:3771,3800,block,services:V8Debugger";
const char *NORMALRESTRICTEDMODE = "-qmljsdebugger=port:3771,3800,services:V8Debugger";
const char *TEST_QMLFILE = "test.qml";
const char *TEST_JSFILE = "test.js";
const char *TIMER_QMLFILE = "timer.qml";
const char *LOADJSFILE_QMLFILE = "loadjsfile.qml";
const char *EXCEPTION_QMLFILE = "exception.qml";
const char *ONCOMPLETED_QMLFILE = "oncompleted.qml";
const char *CREATECOMPONENT_QMLFILE = "createComponent.qml";
const char *CONDITION_QMLFILE = "condition.qml";
const char *QUIT_QMLFILE = "quit.qml";
const char *QUITINJS_QMLFILE = "quitInJS.qml";
const char *QUIT_JSFILE = "quit.js";
const char *CHANGEBREAKPOINT_QMLFILE = "changeBreakpoint.qml";
const char *STEPACTION_QMLFILE = "stepAction.qml";
const char *BREAKPOINTRELOCATION_QMLFILE = "breakpointRelocation.qml";
const char *BREAKPOINTONDECLARATION_QMLFILE = "breakpointOnDeclaration.qml";
const char *ENCODEQMLSCOPE_QMLFILE = "encodeQmlScope.qml";
const char *BREAKONANCHOR_QMLFILE = "breakOnAnchor.qml";
const char *BREAKPOINTIDS_QMLFILE = "breakPointIds.qml";
const char *LETCONSTLOCALS_QMLFILE = "letConstLocals.qml";
#undef QVERIFY
#define QVERIFY(statement) \
do {\
if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) {\
if (QTest::currentTestFailed()) \
qDebug().nospace() << "\nDEBUGGEE OUTPUT:\n" << m_process->output();\
return;\
}\
} while (0)
class tst_QQmlDebugJS : public QQmlDebugTest
{
Q_OBJECT
public:
tst_QQmlDebugJS();
private slots:
void initTestCase() override;
void connect_data();
void connect();
void interrupt_data() { targetData(); }
void interrupt();
void getVersion_data() { targetData(); }
void getVersion();
void getVersionWhenAttaching_data() { targetData(); }
void getVersionWhenAttaching();
void disconnect_data() { targetData(); }
void disconnect();
void setBreakpointInScriptOnCompleted_data() { targetData(); }
void setBreakpointInScriptOnCompleted();
void setBreakpointInScriptOnComponentCreated_data() { targetData(); }
void setBreakpointInScriptOnComponentCreated();
void setBreakpointInScriptOnTimerCallback_data() { targetData(); }
void setBreakpointInScriptOnTimerCallback();
void setBreakpointInScriptInDifferentFile_data() { targetData(); }
void setBreakpointInScriptInDifferentFile();
void setBreakpointInScriptOnComment_data() { targetData(); }
void setBreakpointInScriptOnComment();
void setBreakpointInScriptOnEmptyLine_data() { targetData(); }
void setBreakpointInScriptOnEmptyLine();
void setBreakpointInScriptOnDeclaration_data() { targetData(); }
void setBreakpointInScriptOnDeclaration();
void setBreakpointInScriptOnOptimizedBinding_data() { targetData(); }
void setBreakpointInScriptOnOptimizedBinding();
void setBreakpointInScriptWithCondition_data() { targetData(); }
void setBreakpointInScriptWithCondition();
void setBreakpointInScriptThatQuits_data() { targetData(); };
void setBreakpointInScriptThatQuits();
void setBreakpointInJavaScript_data();
void setBreakpointInJavaScript();
void setBreakpointWhenAttaching();
void clearBreakpoint_data() { targetData(); }
void clearBreakpoint();
void changeBreakpoint_data() { targetData(); }
void changeBreakpoint();
void setExceptionBreak_data() { targetData(); }
void setExceptionBreak();
void stepNext_data() { targetData(); }
void stepNext();
void stepIn_data() { targetData(); }
void stepIn();
void stepOut_data() { targetData(); }
void stepOut();
void continueDebugging_data() { targetData(); }
void continueDebugging();
void backtrace_data() { targetData(); }
void backtrace();
void getFrameDetails_data() { targetData(); }
void getFrameDetails();
void getScopeDetails_data() { targetData(); }
void getScopeDetails();
void evaluateInGlobalScope();
void evaluateInLocalScope_data() { targetData(); }
void evaluateInLocalScope();
void evaluateInContext();
void getScripts_data() { targetData(); }
void getScripts();
void encodeQmlScope();
void breakOnAnchor();
void breakPointIds();
void letConstLocals();
private:
ConnectResult runAndConnect(bool qmlscene, const QString &qmlFile = QString(TEST_QMLFILE),
bool blockMode = true, bool restrictServices = false);
QList<QQmlDebugClient *> createClients() override;
QPointer<QV4DebugClient> m_client;
void targetData();
bool waitForClientSignal(const char *signal, int timeout = 30000);
void checkVersionParameters();
int setBreakPoint(const QString &file, int sourceLine, bool enabled);
void clearBreakPoint(int id);
};
tst_QQmlDebugJS::tst_QQmlDebugJS()
: QQmlDebugTest(QT_QMLTEST_DATADIR)
{
}
void tst_QQmlDebugJS::initTestCase()
{
QQmlDebugTest::initTestCase();
}
QQmlDebugTest::ConnectResult tst_QQmlDebugJS::runAndConnect(bool qmlscene, const QString &qmlFile,
bool blockMode, bool restrictServices)
{
const QString executable = qmlscene
? QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/qmlscene"
: debugJsServerPath("qqmldebugjs");
return QQmlDebugTest::connectTo(
executable, restrictServices ? QStringLiteral("V8Debugger") : QString(),
testFile(qmlFile), blockMode);
}
void tst_QQmlDebugJS::connect_data()
{
QTest::addColumn<bool>("blockMode");
QTest::addColumn<bool>("restrictMode");
QTest::addColumn<bool>("qmlscene");
QTest::newRow("normal / unrestricted / custom") << false << false << false;
QTest::newRow("block / unrestricted / custom") << true << false << false;
QTest::newRow("normal / restricted / custom") << false << true << false;
QTest::newRow("block / restricted / custom") << true << true << false;
QTest::newRow("normal / unrestricted / qmlscene") << false << false << true;
QTest::newRow("block / unrestricted / qmlscene") << true << false << true;
QTest::newRow("normal / restricted / qmlscene") << false << true << true;
QTest::newRow("block / restricted / qmlscene") << true << true << true;
}
void tst_QQmlDebugJS::connect()
{
QFETCH(bool, blockMode);
QFETCH(bool, restrictMode);
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene, QString(TEST_QMLFILE), blockMode, restrictMode), ConnectSuccess);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(connected())));
}
void tst_QQmlDebugJS::interrupt()
{
//void connect()
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene), ConnectSuccess);
m_client->connect();
m_client->interrupt();
QVERIFY(waitForClientSignal(SIGNAL(interrupted())));
}
void tst_QQmlDebugJS::getVersion()
{
//void version()
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene), ConnectSuccess);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(connected())));
m_client->version();
QVERIFY(waitForClientSignal(SIGNAL(result())));
checkVersionParameters();
}
void tst_QQmlDebugJS::getVersionWhenAttaching()
{
//void version()
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene, QLatin1String(TIMER_QMLFILE), false), ConnectSuccess);
m_client->connect();
m_client->version();
QVERIFY(waitForClientSignal(SIGNAL(result())));
checkVersionParameters();
}
void tst_QQmlDebugJS::disconnect()
{
//void disconnect()
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene), ConnectSuccess);
m_client->connect();
m_client->disconnect();
QVERIFY(waitForClientSignal(SIGNAL(result())));
}
void tst_QQmlDebugJS::setBreakpointInScriptOnCompleted()
{
//void setBreakpoint(QString type, QString target, int line = -1, int column = -1, bool enabled = false, QString condition = QString(), int ignoreCount = -1)
QFETCH(bool, qmlscene);
int sourceLine = 9;
QCOMPARE(runAndConnect(qmlscene, ONCOMPLETED_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(ONCOMPLETED_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(ONCOMPLETED_QMLFILE));
}
void tst_QQmlDebugJS::setBreakpointInScriptOnComponentCreated()
{
//void setBreakpoint(QString type, QString target, int line = -1, int column = -1, bool enabled = false, QString condition = QString(), int ignoreCount = -1)
QFETCH(bool, qmlscene);
int sourceLine = 9;
QCOMPARE(runAndConnect(qmlscene, CREATECOMPONENT_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(ONCOMPLETED_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(ONCOMPLETED_QMLFILE));
}
void tst_QQmlDebugJS::setBreakpointInScriptOnTimerCallback()
{
QFETCH(bool, qmlscene);
int sourceLine = 10;
QCOMPARE(runAndConnect(qmlscene, TIMER_QMLFILE), ConnectSuccess);
m_client->connect();
//We can set the breakpoint after connect() here because the timer is repeating and if we miss
//its first iteration we can still catch the second one.
m_client->setBreakpoint(QLatin1String(TIMER_QMLFILE), sourceLine, -1, true);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(TIMER_QMLFILE));
}
void tst_QQmlDebugJS::setBreakpointInScriptInDifferentFile()
{
//void setBreakpoint(QString type, QString target, int line = -1, int column = -1, bool enabled = false, QString condition = QString(), int ignoreCount = -1)
QFETCH(bool, qmlscene);
int sourceLine = 6;
QCOMPARE(runAndConnect(qmlscene, LOADJSFILE_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(TEST_JSFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(TEST_JSFILE));
}
void tst_QQmlDebugJS::setBreakpointInScriptOnComment()
{
//void setBreakpoint(QString type, QString target, int line = -1, int column = -1, bool enabled = false, QString condition = QString(), int ignoreCount = -1)
QFETCH(bool, qmlscene);
int sourceLine = 9;
int actualLine = 11;
QCOMPARE(runAndConnect(qmlscene, BREAKPOINTRELOCATION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(BREAKPOINTRELOCATION_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), actualLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(BREAKPOINTRELOCATION_QMLFILE));
// The hit at the relocated line is still attributed to the breakpoint, so
// the client can move its marker there.
QVERIFY(!body.value("breakpoints").toArray().isEmpty());
}
void tst_QQmlDebugJS::setBreakpointInScriptOnEmptyLine()
{
//void setBreakpoint(QString type, QString target, int line = -1, int column = -1, bool enabled = false, QString condition = QString(), int ignoreCount = -1)
QFETCH(bool, qmlscene);
int sourceLine = 10;
int actualLine = 11;
QCOMPARE(runAndConnect(qmlscene, BREAKPOINTRELOCATION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(BREAKPOINTRELOCATION_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), actualLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(BREAKPOINTRELOCATION_QMLFILE));
// The hit at the relocated line is still attributed to the breakpoint, so
// the client can move its marker there.
QVERIFY(!body.value("breakpoints").toArray().isEmpty());
}
void tst_QQmlDebugJS::setBreakpointInScriptOnDeclaration()
{
QFETCH(bool, qmlscene);
// A literal binding has no code. Its breakpoint must not move into the
// handler below it, which runs at an unrelated time.
int declarationLine = 7;
int handlerLine = 9;
QCOMPARE(runAndConnect(qmlscene, BREAKPOINTONDECLARATION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(BREAKPOINTONDECLARATION_QMLFILE), declarationLine, -1,
true);
m_client->setBreakpoint(QLatin1String(BREAKPOINTONDECLARATION_QMLFILE), handlerLine, -1,
true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), handlerLine);
QCOMPARE(body.value("breakpoints").toArray().size(), 1);
}
void tst_QQmlDebugJS::setBreakpointInScriptOnOptimizedBinding()
{
//void setBreakpoint(QString type, QString target, int line = -1, int column = -1, bool enabled = false, QString condition = QString(), int ignoreCount = -1)
QFETCH(bool, qmlscene);
int sourceLine = 14;
QCOMPARE(runAndConnect(qmlscene, BREAKPOINTRELOCATION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(BREAKPOINTRELOCATION_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(BREAKPOINTRELOCATION_QMLFILE));
}
void tst_QQmlDebugJS::setBreakpointInScriptWithCondition()
{
QFETCH(bool, qmlscene);
int out = 10;
int sourceLine = 12;
QCOMPARE(runAndConnect(qmlscene, CONDITION_QMLFILE), ConnectSuccess);
m_client->connect();
//The breakpoint is in a timer loop so we can set it after connect().
m_client->setBreakpoint(QLatin1String(CONDITION_QMLFILE), sourceLine, 1, true, QLatin1String("a > 10"));
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
//Get the frame index
{
const QJsonObject body = m_client->response().body.toObject();
int frameIndex = body.value("index").toInt();
//Verify the value of 'result'
m_client->evaluate(QLatin1String("a"),frameIndex);
QVERIFY(waitForClientSignal(SIGNAL(result())));
}
const QJsonObject body = m_client->response().body.toObject();
QVERIFY(!body.isEmpty());
QJsonValue val = body.value("value");
QVERIFY(val.isDouble());
const int a = val.toInt();
QVERIFY(a > out);
}
void tst_QQmlDebugJS::setBreakpointInScriptThatQuits()
{
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene, QUIT_QMLFILE), ConnectSuccess);
int sourceLine = 11;
m_client->setBreakpoint(QLatin1String(QUIT_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(), QLatin1String(QUIT_QMLFILE));
m_client->continueDebugging(QV4DebugClient::Continue);
QVERIFY(m_process->waitForFinished());
QCOMPARE(m_process->exitStatus(), QProcess::NormalExit);
}
void tst_QQmlDebugJS::setBreakpointInJavaScript_data()
{
QTest::addColumn<bool>("qmlscene");
QTest::addColumn<bool>("seedCache");
QTest::newRow("custom / immediate") << false << false;
QTest::newRow("qmlscene / immediate") << true << false;
QTest::newRow("custom / seeded") << false << true;
QTest::newRow("qmlscene / seeded") << true << true;
}
void tst_QQmlDebugJS::setBreakpointInJavaScript()
{
QFETCH(bool, qmlscene);
QFETCH(bool, seedCache);
if (seedCache) { // Make sure there is a qmlc file that the engine should _not_ laod.
QProcess process;
process.start(QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/qmlscene",
{ testFile(QUITINJS_QMLFILE) });
QTRY_COMPARE_WITH_TIMEOUT(process.state(), QProcess::NotRunning, 3000);
}
QCOMPARE(runAndConnect(qmlscene, QUITINJS_QMLFILE), ConnectSuccess);
const int sourceLine = 2;
m_client->setBreakpoint(QLatin1String(QUIT_JSFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(QUIT_JSFILE));
m_client->continueDebugging(QV4DebugClient::Continue);
QVERIFY(m_process->waitForFinished());
QCOMPARE(m_process->exitStatus(), QProcess::NormalExit);
}
void tst_QQmlDebugJS::setBreakpointWhenAttaching()
{
int sourceLine = 10;
QCOMPARE(runAndConnect(true, QLatin1String(TIMER_QMLFILE), false), ConnectSuccess);
m_client->connect();
QSKIP("\nThe breakpoint may not hit because the engine may run in JIT mode or not have debug\n"
"instructions, as we've connected in non-blocking mode above. That means we may have\n"
"connected after the engine was already running, with all the QML already compiled.");
//The breakpoint is in a timer loop so we can set it after connect().
m_client->setBreakpoint(QLatin1String(TIMER_QMLFILE), sourceLine);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
}
void tst_QQmlDebugJS::clearBreakpoint()
{
//void clearBreakpoint(int breakpoint);
QFETCH(bool, qmlscene);
int sourceLine1 = 12;
int sourceLine2 = 13;
QCOMPARE(runAndConnect(qmlscene, CHANGEBREAKPOINT_QMLFILE), ConnectSuccess);
m_client->connect();
//The breakpoints are in a timer loop so we can set them after connect().
//Furthermore the breakpoints should be hit in the right order because setting of breakpoints
//can only occur in the QML event loop. (see QCOMPARE for sourceLine2 below)
m_client->setBreakpoint(QLatin1String(CHANGEBREAKPOINT_QMLFILE), sourceLine1, -1, true);
m_client->setBreakpoint(QLatin1String(CHANGEBREAKPOINT_QMLFILE), sourceLine2, -1, true);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
{
//Will hit 1st brakpoint, change this breakpoint enable = false
const QJsonObject body = m_client->response().body.toObject();
const QJsonArray breakpointsHit = body.value("breakpoints").toArray();
int breakpoint = breakpointsHit.at(0).toInt();
m_client->clearBreakpoint(breakpoint);
QVERIFY(waitForClientSignal(SIGNAL(result())));
//Continue with debugging
m_client->continueDebugging(QV4DebugClient::Continue);
//Hit 2nd breakpoint
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
//Continue with debugging
m_client->continueDebugging(QV4DebugClient::Continue);
}
//Should stop at 2nd breakpoint
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
{
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine2);
}
}
void tst_QQmlDebugJS::changeBreakpoint()
{
//void clearBreakpoint(int breakpoint);
QFETCH(bool, qmlscene);
int sourceLine2 = 12;
int sourceLine1 = 13;
const QString file = QLatin1String(CHANGEBREAKPOINT_QMLFILE);
QCOMPARE(runAndConnect(qmlscene, file), ConnectSuccess);
bool isStopped = false;
QObject::connect(m_client.data(), &QV4DebugClient::stopped, this, [&]() { isStopped = true; });
auto continueDebugging = [&]() {
m_client->continueDebugging(QV4DebugClient::Continue);
isStopped = false;
};
m_client->connect();
auto extractBody = [&]() {
return m_client->response().body.toObject();
};
auto extractBreakPointId = [&](const QJsonObject &body) {
const QJsonArray breakpointsHit = body.value("breakpoints").toArray();
if (breakpointsHit.size() != 1)
return -1;
return breakpointsHit[0].toInt();
};
//The breakpoints are in a timer loop so we can set them after connect().
//Furthermore the breakpoints should be hit in the right order because setting of breakpoints
//can only occur in the QML event loop. (see QCOMPARE for sourceLine2 below)
const int breakpoint1 = setBreakPoint(file, sourceLine1, false);
QVERIFY(breakpoint1 >= 0);
const int breakpoint2 = setBreakPoint(file, sourceLine2, true);
QVERIFY(breakpoint2 >= 0);
auto verifyBreakpoint = [&](int sourceLine, int breakpointId) {
QTRY_VERIFY_WITH_TIMEOUT(isStopped, 30000);
const QJsonObject body = extractBody();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine);
QCOMPARE(extractBreakPointId(body), breakpointId);
};
verifyBreakpoint(sourceLine2, breakpoint2);
continueDebugging();
verifyBreakpoint(sourceLine2, breakpoint2);
m_client->changeBreakpoint(breakpoint2, false);
QVERIFY(waitForClientSignal(SIGNAL(result())));
m_client->changeBreakpoint(breakpoint1, true);
QVERIFY(waitForClientSignal(SIGNAL(result())));
continueDebugging();
verifyBreakpoint(sourceLine1, breakpoint1);
continueDebugging();
verifyBreakpoint(sourceLine1, breakpoint1);
m_client->changeBreakpoint(breakpoint2, true);
QVERIFY(waitForClientSignal(SIGNAL(result())));
m_client->changeBreakpoint(breakpoint1, false);
QVERIFY(waitForClientSignal(SIGNAL(result())));
for (int i = 0; i < 3; ++i) {
continueDebugging();
verifyBreakpoint(sourceLine2, breakpoint2);
}
}
void tst_QQmlDebugJS::setExceptionBreak()
{
//void setExceptionBreak(QString type, bool enabled = false);
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene, EXCEPTION_QMLFILE), ConnectSuccess);
m_client->setExceptionBreak(QV4DebugClient::All,true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
}
void tst_QQmlDebugJS::stepNext()
{
//void continueDebugging(StepAction stepAction, int stepCount = 1);
QFETCH(bool, qmlscene);
int sourceLine = 12;
QCOMPARE(runAndConnect(qmlscene, STEPACTION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(STEPACTION_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->continueDebugging(QV4DebugClient::Next);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), sourceLine + 1);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(STEPACTION_QMLFILE));
}
static QJsonObject responseBody(QV4DebugClient *client)
{
return client->response().body.toObject();
}
void tst_QQmlDebugJS::stepIn()
{
//void continueDebugging(StepAction stepAction, int stepCount = 1);
QFETCH(bool, qmlscene);
int sourceLine = 16;
int actualLine = 11;
QCOMPARE(runAndConnect(qmlscene, STEPACTION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(STEPACTION_QMLFILE), sourceLine, 1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
QCOMPARE(responseBody(m_client).value("sourceLine").toInt(), sourceLine);
m_client->continueDebugging(QV4DebugClient::In);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = responseBody(m_client);
QCOMPARE(body.value("sourceLine").toInt(), actualLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(), QLatin1String(STEPACTION_QMLFILE));
}
void tst_QQmlDebugJS::stepOut()
{
//void continueDebugging(StepAction stepAction, int stepCount = 1);
QFETCH(bool, qmlscene);
int sourceLine = 12;
int actualLine = 16;
QCOMPARE(runAndConnect(qmlscene, STEPACTION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(STEPACTION_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
QCOMPARE(responseBody(m_client).value("sourceLine").toInt(), sourceLine);
m_client->continueDebugging(QV4DebugClient::Out);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = responseBody(m_client);
QCOMPARE(body.value("sourceLine").toInt(), actualLine);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(), QLatin1String(STEPACTION_QMLFILE));
}
void tst_QQmlDebugJS::continueDebugging()
{
//void continueDebugging(StepAction stepAction, int stepCount = 1);
QFETCH(bool, qmlscene);
int sourceLine1 = 16;
int sourceLine2 = 13;
QCOMPARE(runAndConnect(qmlscene, STEPACTION_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(STEPACTION_QMLFILE), sourceLine1, -1, true);
m_client->setBreakpoint(QLatin1String(STEPACTION_QMLFILE), sourceLine2, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->continueDebugging(QV4DebugClient::Continue);
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
const QJsonObject body = responseBody(m_client);
QCOMPARE(body.value("sourceLine").toInt(), sourceLine2);
QCOMPARE(QFileInfo(body.value("script").toObject().value("name").toString()).fileName(),
QLatin1String(STEPACTION_QMLFILE));
}
void tst_QQmlDebugJS::backtrace()
{
//void backtrace(int fromFrame = -1, int toFrame = -1, bool bottom = false);
QFETCH(bool, qmlscene);
int sourceLine = 9;
QCOMPARE(runAndConnect(qmlscene, ONCOMPLETED_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(ONCOMPLETED_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->backtrace();
QVERIFY(waitForClientSignal(SIGNAL(result())));
}
void tst_QQmlDebugJS::getFrameDetails()
{
//void frame(int number = -1);
QFETCH(bool, qmlscene);
int sourceLine = 9;
QCOMPARE(runAndConnect(qmlscene, ONCOMPLETED_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(ONCOMPLETED_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->frame();
QVERIFY(waitForClientSignal(SIGNAL(result())));
}
void tst_QQmlDebugJS::getScopeDetails()
{
//void scope(int number = -1, int frameNumber = -1);
QFETCH(bool, qmlscene);
int sourceLine = 9;
QCOMPARE(runAndConnect(qmlscene, ONCOMPLETED_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(ONCOMPLETED_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->scope();
QVERIFY(waitForClientSignal(SIGNAL(result())));
}
void tst_QQmlDebugJS::evaluateInGlobalScope()
{
//void evaluate(QString expr, int frame = -1);
QCOMPARE(runAndConnect(true), ConnectSuccess);
m_client->connect();
for (int i = 0; i < 10; ++i) {
// The engine might not be initialized, yet. We just try until it shows up.
m_client->evaluate(QLatin1String("console.log('Hello World')"));
if (waitForClientSignal(SIGNAL(result()), 500))
break;
}
//Verify the return value of 'console.log()', which is "undefined"
QCOMPARE(responseBody(m_client).value("type").toString(), QLatin1String("undefined"));
}
void tst_QQmlDebugJS::evaluateInLocalScope()
{
//void evaluate(QString expr, bool global = false, bool disableBreak = false, int frame = -1, const QVariantMap &addContext = QVariantMap());
QFETCH(bool, qmlscene);
int sourceLine = 9;
QCOMPARE(runAndConnect(qmlscene, ONCOMPLETED_QMLFILE), ConnectSuccess);
m_client->setBreakpoint(QLatin1String(ONCOMPLETED_QMLFILE), sourceLine, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->frame();
QVERIFY(waitForClientSignal(SIGNAL(result())));
{
//Get the frame index
const QJsonObject body = responseBody(m_client);
int frameIndex = body.value("index").toInt();
m_client->evaluate(QLatin1String("root.a"), frameIndex);
QVERIFY(waitForClientSignal(SIGNAL(result())));
}
{
//Verify the value of 'timer.interval'
const QJsonObject body = responseBody(m_client);
QCOMPARE(body.value("value").toInt(),10);
}
}
void tst_QQmlDebugJS::evaluateInContext()
{
m_connection = new QQmlDebugConnection();
m_process = new QQmlDebugProcess(
QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/qmlscene", this);
m_client = new QV4DebugClient(m_connection);
QScopedPointer<QQmlEngineDebugClient> engineClient(new QQmlEngineDebugClient(m_connection));
m_process->start(QStringList() << QLatin1String(BLOCKMODE) << testFile(ONCOMPLETED_QMLFILE));
QVERIFY(m_process->waitForSessionStart());
m_connection->connectToHost("127.0.0.1", m_process->debugPort());
QVERIFY(m_connection->waitForConnected());
QTRY_COMPARE(m_client->state(), QQmlEngineDebugClient::Enabled);
QTRY_COMPARE(engineClient->state(), QQmlEngineDebugClient::Enabled);
m_client->connect();
// "a" not accessible without extra context
m_client->evaluate(QLatin1String("a + 10"), -1, -1);
QVERIFY(waitForClientSignal(SIGNAL(failure())));
bool success = false;
engineClient->queryAvailableEngines(&success);
QVERIFY(success);
QVERIFY(QQmlDebugTest::waitForSignal(engineClient.data(), SIGNAL(result())));
QVERIFY(engineClient->engines().size());
engineClient->queryRootContexts(engineClient->engines()[0], &success);
QVERIFY(success);
QVERIFY(QQmlDebugTest::waitForSignal(engineClient.data(), SIGNAL(result())));
auto contexts = engineClient->rootContext().contexts;
QCOMPARE(contexts.size(), 1);
auto objects = contexts[0].objects;
QCOMPARE(objects.size(), 1);
engineClient->queryObjectRecursive(objects[0], &success);
QVERIFY(success);
QVERIFY(QQmlDebugTest::waitForSignal(engineClient.data(), SIGNAL(result())));
auto object = engineClient->object();
// "a" accessible in context of surrounding object
m_client->evaluate(QLatin1String("a + 10"), -1, object.debugId);
QVERIFY(waitForClientSignal(SIGNAL(result())));
QTRY_COMPARE(responseBody(m_client).value("value").toInt(), 20);
auto childObjects = object.children;
QVERIFY(childObjects.size() > 0); // QQmlComponentAttached is also in there
QCOMPARE(childObjects[0].className, QString::fromLatin1("Item"));
// "b" accessible in context of surrounding (child) object
m_client->evaluate(QLatin1String("b"), -1, childObjects[0].debugId);
QVERIFY(waitForClientSignal(SIGNAL(result())));
QTRY_COMPARE(responseBody(m_client).value("value").toInt(), 11);
}
void tst_QQmlDebugJS::getScripts()
{
//void scripts(int types = -1, QList<int> ids = QList<int>(), bool includeSource = false, QVariant filter = QVariant());
QFETCH(bool, qmlscene);
QCOMPARE(runAndConnect(qmlscene), ConnectSuccess);
m_client->setBreakpoint(QString(TEST_QMLFILE), 10, -1, true);
m_client->connect();
QVERIFY(waitForClientSignal(SIGNAL(stopped())));
m_client->scripts();
QVERIFY(waitForClientSignal(SIGNAL(result())));
const QJsonArray scripts = m_client->response().body.toArray();
QCOMPARE(scripts.count(), 1);
QVERIFY(scripts.first().toObject()[QStringLiteral("name")].toString()
.endsWith(QStringLiteral("data/test.qml")));
}
void tst_QQmlDebugJS::encodeQmlScope()
{
QString file(ENCODEQMLSCOPE_QMLFILE);
QCOMPARE(runAndConnect(true, file), ConnectSuccess);
int numFrames = 0;
int numExpectedScopes = 0;
int numReceivedScopes = 0;
bool isStopped = false;
bool scopesFailed = false;
QObject::connect(m_client.data(), &QV4DebugClient::failure, this, [&]() {
qWarning() << "received failure" << m_client->response().body;
scopesFailed = true;
m_process->stop();
numFrames = 2;
isStopped = false;
});
QObject::connect(m_client.data(), &QV4DebugClient::stopped, this, [&]() {
m_client->frame();
isStopped = true;
});
QObject::connect(m_client.data(), &QV4DebugClient::result, this, [&]() {
const QV4DebugClient::Response value = m_client->response();
if (value.command == QString("scope")) {
// If the scope commands fail we get a failure() signal above.
if (++numReceivedScopes == numExpectedScopes) {
m_client->continueDebugging(QV4DebugClient::Continue);
isStopped = false;
}
} else if (value.command == QString("frame")) {
// We want at least a global scope and some kind of local scope here.
const QJsonArray scopes = value.body.toObject().value("scopes").toArray();
if (scopes.count() < 2)
scopesFailed = true;
for (const QJsonValue scope : scopes) {
++numExpectedScopes;
m_client->scope(scope.toObject().value("index").toInt());
}
++numFrames;
}
});
m_client->setBreakpoint(file, 6);
m_client->setBreakpoint(file, 8);
m_client->connect();
QTRY_COMPARE(numFrames, 2);
QVERIFY(numExpectedScopes > 3);
QVERIFY(!scopesFailed);
QTRY_VERIFY(!isStopped);
QCOMPARE(numReceivedScopes, numExpectedScopes);
}
void tst_QQmlDebugJS::breakOnAnchor()
{
QString file(BREAKONANCHOR_QMLFILE);
QCOMPARE(runAndConnect(true, file), ConnectSuccess);
int breaks = 0;
bool stopped = false;
QObject::connect(m_client.data(), &QV4DebugClient::stopped, this, [&]() {
stopped = true;
++breaks;
m_client->evaluate("this", 0, -1);
});
QObject::connect(m_client.data(), &QV4DebugClient::result, this, [&]() {
if (stopped) {
m_client->continueDebugging(QV4DebugClient::Continue);
stopped = false;
}
});
QObject::connect(m_client.data(), &QV4DebugClient::failure, this, [&]() {
qWarning() << "received failure" << m_client->response().body;
});
m_client->setBreakpoint(file, 9);
m_client->setBreakpoint(file, 12);
QTRY_COMPARE(m_process->state(), QProcess::Running);
m_client->connect();
QTRY_COMPARE_WITH_TIMEOUT(m_process->state(), QProcess::NotRunning, 10000);
QCOMPARE(m_process->exitStatus(), QProcess::NormalExit);
QCOMPARE(breaks, 2);
}
void tst_QQmlDebugJS::breakPointIds()
{
QString file(BREAKPOINTIDS_QMLFILE);
QCOMPARE(runAndConnect(true, file), ConnectSuccess);
int breaks = 0;
int breakPointIds[] = { -1, -1, -1, -1, -1, -1};
QObject::connect(m_client.data(), &QV4DebugClient::stopped, this, [&]() {
const QJsonObject body = m_client->response().body.toObject();
QCOMPARE(body.value("sourceLine").toInt(), breaks + 4);
const QJsonArray breakpointsHit = body.value("breakpoints").toArray();
QVERIFY(breakpointsHit.size() > 0);
QCOMPARE(breakpointsHit[0].toInt(), breakPointIds[breaks]);
++breaks;
m_client->continueDebugging(QV4DebugClient::Continue);
});
for (int i = 0; i < 6; ++i)
breakPointIds[i] = setBreakPoint(file, i + 4, true);
clearBreakPoint(breakPointIds[2]);
breakPointIds[2] = setBreakPoint(file, 6, true);
QTRY_COMPARE(m_process->state(), QProcess::Running);
m_client->connect();
QTRY_COMPARE(m_process->state(), QProcess::NotRunning);
QCOMPARE(m_process->exitStatus(), QProcess::NormalExit);
QCOMPARE(breaks, 6);
}
void tst_QQmlDebugJS::letConstLocals()
{
QString file(LETCONSTLOCALS_QMLFILE);
QCOMPARE(runAndConnect(true, file), ConnectSuccess);
QObject::connect(m_client.data(), &QV4DebugClient::stopped, this, [&]() {
m_client->frame();
});
int numScopes = 0;
QString expectedMembers = QStringLiteral("abcde");
QObject::connect(m_client.data(), &QV4DebugClient::result, this, [&]() {
const auto value = m_client->response();
if (value.command == QStringLiteral("frame")) {
const auto scopes = value.body.toObject().value(QStringLiteral("scopes")).toArray();
for (const auto scope : scopes) {
const auto scopeObject = scope.toObject();
const int type = scopeObject.value("type").toInt();
if (type == 1 || type == 4) {
m_client->scope(scopeObject.value("index").toInt());
++numScopes;
}
}
QVERIFY(numScopes > 0);
} else if (value.command == QStringLiteral("scope")) {
const auto props = value.body.toObject().value(QStringLiteral("object")).toObject()
.value(QStringLiteral("properties")).toArray();
for (const auto prop : props) {
const auto propObj = prop.toObject();
QString name = propObj.value(QStringLiteral("name")).toString();
QVERIFY(name.size() == 1);
auto i = expectedMembers.indexOf(name.at(0));
QVERIFY(i != -1);
expectedMembers.remove(i, 1);
QCOMPARE(propObj.value(QStringLiteral("type")).toString(),
QStringLiteral("number"));
QCOMPARE(propObj.value(QStringLiteral("value")).toInt(),
int(name.at(0).toLatin1()));
}
if (--numScopes == 0) {
QVERIFY(expectedMembers.isEmpty());
m_client->continueDebugging(QV4DebugClient::Continue);
}
}
});
setBreakPoint(file, 10, true);
QTRY_COMPARE(m_process->state(), QProcess::Running);
m_client->connect();
QTRY_COMPARE(m_process->state(), QProcess::NotRunning);
QCOMPARE(m_process->exitStatus(), QProcess::NormalExit);
}
QList<QQmlDebugClient *> tst_QQmlDebugJS::createClients()
{
m_client = new QV4DebugClient(m_connection);
return QList<QQmlDebugClient *>({m_client});
}
void tst_QQmlDebugJS::targetData()
{
QTest::addColumn<bool>("qmlscene");
QTest::newRow("custom") << false;
QTest::newRow("qmlscene") << true;
}
bool tst_QQmlDebugJS::waitForClientSignal(const char *signal, int timeout)
{
return QQmlDebugTest::waitForSignal(m_client.data(), signal, timeout);
}
void tst_QQmlDebugJS::checkVersionParameters()
{
const QV4DebugClient::Response value = m_client->response();
QCOMPARE(value.command, QString("version"));
const QJsonObject body = value.body.toObject();
QCOMPARE(body.value("UnpausedEvaluate").toBool(), true);
QCOMPARE(body.value("ContextEvaluate").toBool(), true);
QCOMPARE(body.value("ChangeBreakpoint").toBool(), true);
}
int tst_QQmlDebugJS::setBreakPoint(const QString &file, int sourceLine, bool enabled)
{
int id = -1;
auto connection = QObject::connect(m_client.data(), &QV4DebugClient::result, this, [&]() {
id = m_client->response().body.toObject().value("breakpoint").toInt();
});
m_client->setBreakpoint(file, sourceLine, -1, enabled);
bool success = QTest::qWaitFor([&]() { return id >= 0; });
Q_UNUSED(success);
QObject::disconnect(connection);
return id;
}
void tst_QQmlDebugJS::clearBreakPoint(int id)
{
bool ok = false;
auto connection = QObject::connect(m_client.data(), &QV4DebugClient::result, this, [&]() {
ok = true;
});
m_client->clearBreakpoint(id);
bool success = QTest::qWaitFor([&]() { return ok; });
Q_UNUSED(success);
QObject::disconnect(connection);
}
QTEST_MAIN(tst_QQmlDebugJS)
#include "tst_qqmldebugjs.moc"