From 6e8e665da7b6ab33da36e075836b9c7b44049041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20W=C3=A4ckerlin?= Date: Fri, 20 Jan 2017 08:30:49 +0000 Subject: [PATCH] new option for timeout, longer timeout in shell, new untested command auth to set basic authentication credentials --- COPYING | 2 +- INSTALL | 2 +- README | 33 +++-- configure.ac | 1 + src/commands.hxx | 233 ++++++++++++++++++++++++++--------- src/networkaccessmanager.hxx | 6 +- src/testgui.hxx | 1 + src/webrunner.cxx | 6 + 8 files changed, 209 insertions(+), 75 deletions(-) diff --git a/COPYING b/COPYING index 88798ab..caeca07 120000 --- a/COPYING +++ b/COPYING @@ -1 +1 @@ -/usr/share/automake-1.15/COPYING \ No newline at end of file +/usr/share/automake-1.14/COPYING \ No newline at end of file diff --git a/INSTALL b/INSTALL index ddcdb76..f812f5a 120000 --- a/INSTALL +++ b/INSTALL @@ -1 +1 @@ -/usr/share/automake-1.15/INSTALL \ No newline at end of file +/usr/share/automake-1.14/INSTALL \ No newline at end of file diff --git a/README b/README index aa5112c..9973499 100644 --- a/README +++ b/README @@ -1,18 +1,25 @@ -Test Your Web Application: GUI Web Testing Environment + Script Runner - -Webtester consists of two binaries: webtester to interactively create your web application tests and webrunner to run your test scripts. +# Framework for Automated Web Application Testing There is a test GUI including browser to record user input while he surfs on the web and a test runner to run (recorded) test scripts. The tests can be integrated e.g. in a jenkins build job. It has been tested on Wordpress, Dokuwiki and Joomla pages. Joomla is difficult due to Javascript-Moo-Tools pollution. There's some specific support, that may help a bit, but to test Joomla sites, you need a lot of experience. Concluson: Avoid Joomla. Sample Script to search my old homepage on Google, klick on the link, there click on tab «Computer» and check the title for the text «Marcs Computerblog»: -load https://google.com -expect load -setvalue input[name="q"] -> 'Marc Wäckerlin'; -click input[name="btnG"] -expect load -click a[href^="/url?q=https://marc.waeckerlin.org/&"] -expect load https://marc.waeckerlin.org/doku.php -click a[href="/computer/index"] -expect load https://marc.waeckerlin.org/computer/index -exists h1.sectionedit1 -> Marcs Computerblog + load https://google.com + expect loadStarted + expect urlChanged + expect loadFinished true + do input[name="q"] + this.value='Marc Wäckerlin'; + click input[name="btnG"] + expect loadStarted + expect urlChanged + expect loadFinished true + click a[href^="/url?q=https://marc.waeckerlin.org/&"] + expect loadStarted + expect urlChanged https://marc.waeckerlin.org/doku.php + expect loadFinished true + click a[href="/computer/index"] + expect loadStarted + expect urlChanged https://marc.waeckerlin.org/computer/index + expect loadFinished true + exists h1.sectionedit1 -> Marcs Computerblog diff --git a/configure.ac b/configure.ac index cb1288c..1d909a9 100644 --- a/configure.ac +++ b/configure.ac @@ -41,6 +41,7 @@ AM_CPPFLAGS="${AM_CPPFLAGS} -DQT_NO_KEYWORDS" # libraries used AX_PKG_REQUIRE([mrwcxx], [mrw-c++]) AX_PKG_REQUIRE([xmlcxx], [libxml-cxx]) +AC_CHECK_HEADERS([cxxabi.h]) # create output AC_OUTPUT diff --git a/src/commands.hxx b/src/commands.hxx index efa96ee..dc0a197 100644 --- a/src/commands.hxx +++ b/src/commands.hxx @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,20 @@ #include #include +#ifdef HAVE_CXXABI_H +#include +inline QString demangle(const char* mangled) { + int status; + std::unique_ptr result( + abi::__cxa_demangle(mangled, 0, 0, &status), free); + return QString(result.get() ? result.get() : mangled); +} +#else +inline QString demangle(const char* mangled) { + return QString(mangled); +} +#endif + namespace std { template class optional { private: @@ -99,7 +114,7 @@ class Command: public QObject { QStringList&, QString, int, int) = 0; virtual bool execute(Script*, QWebFrame*) = 0; static void error(Logger& log, const Exception& e) { - log(QString(" FAILED: ")+e.what()); + log(QString(" FAILED[")+demangle(typeid(e).name())+"]: "+e.what()); throw e; } void line(int linenr) { @@ -187,20 +202,38 @@ class Command: public QObject { } return commands; } - QStringList commaSeparatedList(QString value) { - value=value.trimmed(); - if (!value.size()) return QStringList(); - switch (value.size()>1&&value.at(0)==value.at(value.size()-1) - ?value.at(0).toLatin1():'\0') { - case '"': case '\'': { - return value.mid(1, value.size()-2) - .split(QRegularExpression(QString(value[0])+", *" - +QString(value[0]))); - } break; - default: { - return value.split(QRegularExpression(", *")); + QStringList quotedStrings(QString value, + QString delimiter = " ", + bool keepDelimiters = false) { + QStringList res; + QString quot("'\""); + while (value=value.trimmed(), value.size()) { + QRegularExpression re; + int start(0); + if (quot.contains(value[0])) { + re = QRegularExpression(value[0]+" *(("+delimiter+" *)|$)"); + start = 1; + } else { + re = QRegularExpression("( *"+delimiter+" *)|$"); } + int pos(value.indexOf(re, start)); + if (pos parseLine(QStringList& in, @@ -586,7 +620,7 @@ class Script: public QObject { int maxretries = 0) { bool res(true); _testsuites = testsuites; - _timeout = 20; // defaults to 20s + _timeout = _defaultTimeout; // defaults to 20s _ignoreSignalsUntil.clear(); addSignals(frame); _screenshots = screenshots; @@ -815,6 +849,7 @@ class Script: public QObject { _variables = o._variables; _rvariables = o._rvariables; _timeout = o._timeout; + _defaultTimeout = o._timeout; _clicktype = o._clicktype; _testsuites = o._testsuites; _testclass = o._testclass; @@ -845,6 +880,15 @@ class Script: public QObject { void timeout(int t) { _timeout = t; } + void defaultTimeout(int t) { + _defaultTimeout = t; + } + void auth(const QString& realm, const QString& username, const QString& password) { + if (!username.isEmpty() && !password.isEmpty()) + _auth[realm] = {username, password}; + else if (_auth.contains(realm)) + _auth.erase(_auth.find(realm)); + } void clicktype(ClickType c) { _clicktype = c; } @@ -876,33 +920,39 @@ class Script: public QObject { return QString(); } void addSignals(QWebFrame* frame) { - connect(dynamic_cast + assert(connect(dynamic_cast (frame->page()->networkAccessManager()), SIGNAL(log(QString)), - SLOT(log(QString))); - connect(frame, SIGNAL(contentsSizeChanged(const QSize&)), - SLOT(contentsSizeChanged(const QSize&))); - connect(frame, SIGNAL(iconChanged()), - SLOT(iconChanged())); - connect(frame, SIGNAL(initialLayoutCompleted()), - SLOT(initialLayoutCompleted())); - connect(frame, SIGNAL(javaScriptWindowObjectCleared()), - SLOT(javaScriptWindowObjectCleared())); - connect(frame, SIGNAL(loadFinished(bool)), - SLOT(loadFinished(bool))); - connect(frame, SIGNAL(loadStarted()), - SLOT(loadStarted())); - connect(frame, SIGNAL(titleChanged(const QString&)), - SLOT(titleChanged(const QString&))); - connect(frame, SIGNAL(urlChanged(const QUrl&)), - SLOT(urlChanged(const QUrl&))); - connect(&_timer, SIGNAL(timeout()), SLOT(timeout())); + SLOT(log(QString)))); + assert(connect(frame->page()->networkAccessManager(), + SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), + SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*)))); + assert(connect(frame, SIGNAL(contentsSizeChanged(const QSize&)), + SLOT(contentsSizeChanged(const QSize&)))); + assert(connect(frame, SIGNAL(iconChanged()), + SLOT(iconChanged()))); + assert(connect(frame, SIGNAL(initialLayoutCompleted()), + SLOT(initialLayoutCompleted()))); + assert(connect(frame, SIGNAL(javaScriptWindowObjectCleared()), + SLOT(javaScriptWindowObjectCleared()))); + assert(connect(frame, SIGNAL(loadFinished(bool)), + SLOT(loadFinished(bool)))); + assert(connect(frame, SIGNAL(loadStarted()), + SLOT(loadStarted()))); + assert(connect(frame, SIGNAL(titleChanged(const QString&)), + SLOT(titleChanged(const QString&)))); + assert(connect(frame, SIGNAL(urlChanged(const QUrl&)), + SLOT(urlChanged(const QUrl&)))); + assert(connect(&_timer, SIGNAL(timeout()), SLOT(timeout()))); } void removeSignals(QWebFrame* frame) { disconnect(dynamic_cast (frame->page()->networkAccessManager()), SIGNAL(log(QString)), this, SLOT(log(QString))); + disconnect(frame->page()->networkAccessManager(), + SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), + this, SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*))); disconnect(frame, SIGNAL(contentsSizeChanged(const QSize&)), this, SLOT(contentsSizeChanged(const QSize&))); disconnect(frame, SIGNAL(iconChanged()), @@ -931,9 +981,11 @@ class Script: public QObject { for (QChar& c: text) if (c<32&&c!='\n') c='?'; if (cmd) prefix += QString("%2:%3%1 ") - .arg(QString(cmd->indent(), QChar(' '))) + .arg(QString(cmd->indent()+2, QChar(' '))) .arg(cmd->file(), 20, QChar(' ')) .arg(cmd->line(), -4, 10, QChar(' ')); + else + prefix += " .... "; text = prefix+text.split('\n').join("\n"+prefix+" "); logging(text); std::cout< unknown(QString command) { @@ -965,6 +1017,15 @@ class Script: public QObject { _prototypes[c->tag()] = std::shared_ptr(c); } private Q_SLOTS: + void authenticationRequired(QNetworkReply*, QAuthenticator* a) { + if (_auth.contains(a->realm())) { + log("network: login to "+a->realm()); + a->setUser(_auth[a->realm()].username); + a->setPassword(_auth[a->realm()].password); + } else { + log("network: no credentials for "+a->realm()); + } + } void contentsSizeChanged(const QSize&) { } void iconChanged() { @@ -1012,6 +1073,10 @@ class Script: public QObject { error(TimeOut()); } private: + struct AuthRealm { + QString username; + QString password; + }; typedef std::map> Prototypes; typedef std::vector> Commands; Prototypes _prototypes; @@ -1028,12 +1093,14 @@ class Script: public QObject { QMap _rvariables; ///< reverse variable mapping QMap > _functions; int _timeout; + int _defaultTimeout; ClickType _clicktype; QString _targetdir; std::shared_ptr _testsuites; ///< only valid within run QString _testclass; Command* _command; QString _path; + QMap _auth; }; class Do: public Command { @@ -1587,8 +1654,8 @@ class Download: public Command { _realfilename = script->replacevars(_filename); log("REALFILENAME="+_realfilename); frame->page()->setForwardUnsupportedContent(true); - connect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)), - this, SLOT(unsupportedContent(QNetworkReply*))); + assert(connect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)), + this, SLOT(unsupportedContent(QNetworkReply*)))); try { bool res(_next->execute(script, frame)); // start download script->timer().stop(); // no timeout during download @@ -1626,8 +1693,8 @@ class Download: public Command { } } } - connect(new RunDownload(reply, _realfilename), - SIGNAL(completed(bool, bool)), SLOT(completed(bool, bool))); + assert(connect(new RunDownload(reply, _realfilename), + SIGNAL(completed(bool, bool)), SLOT(completed(bool, bool)))); } private: QString _filename; @@ -2312,14 +2379,22 @@ class Check: public Command { int indent) { std::shared_ptr cmd(new Check()); cmd->_next = 0; - int pos(args.indexOf(QRegularExpression("[=!.^~<>]"))); - if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args); - cmd->_value1 = args.left(pos).trimmed(); - cmd->_cmp = args[pos].toLatin1(); - cmd->_value2 = args.mid(pos+1).trimmed(); - if (in.size() && in.first().contains(QRegularExpression("^ "))) { - cmd->_next = script->parseLine(in, file, line+1, indent+1); - cmd->_next->log(false); // suppress logging of subcommand + QString comp("[=!.^~<>]"); + QStringList allargs = quotedStrings(args, comp, true); + if (allargs.size()<2 || allargs[1].size()!=1 || + !QRegularExpression("^"+comp+"$").match(allargs[1]).hasMatch()) + throw BadArgument(tag()+" needs a comparision, not: "+args); + if (allargs.size()>3) + throw BadArgument(tag()+" has at most three arguments"); + cmd->_value1 = allargs[0]; + cmd->_cmp = allargs[1][0].toLatin1(); + if (allargs.size()==3) { + cmd->_value2 = allargs[2]; + } else { + if (in.size() && in.first().contains(QRegularExpression("^ "))) { + cmd->_next = script->parseLine(in, file, line+1, indent+1); + cmd->_next->log(false); // suppress logging of subcommand + } else throw BadArgument(tag()+" needs a third argument or a following command"); } return cmd; } @@ -2704,6 +2779,49 @@ class Fail: public Command { QString _text; }; +class Auth: public Command { + public: + QString tag() const { + return "auth"; + } + QString description() const { + return + tag()+" " + "\n\n"+ + tag()+" " + "\n\n" + "Set basic authentication credentials for to" + " and . If no realm is given," + " the credentials for the given realm are removed."; + } + QString command() const { + return tag()+" "+_username+" "+_password; + } + std::shared_ptr parse(Script*, QString args, + QStringList&, QString, int, int) { + std::shared_ptr cmd(new Auth()); + QStringList allargs = args.split(" "); + if (!allargs.size()) throw BadArgument("requires at least a "); + cmd->_realm = allargs.takeFirst(); + if (allargs.size() && allargs.size()==2) { + cmd->_username=allargs[0]; + cmd->_password=allargs[1]; + } else { + throw BadArgument(QString("requires and , but %1 was given") + .arg(args)); + } + return cmd; + } + bool execute(Script* script, QWebFrame* frame) { + Logger log(this, script); + script->auth(_realm, _username, _password); + return true; + } + private: + QString _realm; + QString _username; + QString _password; +}; /* Template: class : public Command { @@ -2786,8 +2904,8 @@ inline bool Command::runScript(Logger& log, Command* parentCommand, scriptCopy.set(*var, parent->replacevars(*arg)); } try { - connect(&scriptCopy, SIGNAL(logging(QString)), - parent, SLOT(parentlog(QString))); + assert(connect(&scriptCopy, SIGNAL(logging(QString)), + parent, SLOT(parentlog(QString)))); parent->removeSignals(frame); bool res(scriptCopy.run(frame)); parent->addSignals(frame); @@ -2845,6 +2963,7 @@ inline void Script::initPrototypes() { add(new Include); add(new Case); add(new Fail); + add(new Auth); } #endif diff --git a/src/networkaccessmanager.hxx b/src/networkaccessmanager.hxx index f290ac0..4159efb 100644 --- a/src/networkaccessmanager.hxx +++ b/src/networkaccessmanager.hxx @@ -60,13 +60,13 @@ class NetworkAccessManager: public QNetworkAccessManager { return QNetworkAccessManager::configuration(); } void connectToHost(const QString& hostName, quint16 port = 80) { - //log(__PRETTY_FUNCTION__); + //log(__PRETTY_FUNCTION__ + QString(" -> ") + hostName); QNetworkAccessManager::connectToHost(hostName, port); } void connectToHostEncrypted(const QString& hostName, quint16 port = 443, const QSslConfiguration& sslConfiguration = QSslConfiguration::defaultConfiguration()) { - //log(__PRETTY_FUNCTION__); + //log(__PRETTY_FUNCTION__ + QString(" -> ") + hostName); QNetworkAccessManager::connectToHostEncrypted(hostName, port, sslConfiguration); } @@ -171,7 +171,7 @@ class NetworkAccessManager: public QNetworkAccessManager { virtual QNetworkReply* createRequest(Operation op, const QNetworkRequest& req, QIODevice* outgoingData = 0) { - //log(__PRETTY_FUNCTION__); + //log(__PRETTY_FUNCTION__ + QString(" -> ") + req.url().url()); switch (op) { case QNetworkAccessManager::HeadOperation: break; case QNetworkAccessManager::GetOperation: break; diff --git a/src/testgui.hxx b/src/testgui.hxx index 9c9f3b5..333f8be 100644 --- a/src/testgui.hxx +++ b/src/testgui.hxx @@ -195,6 +195,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI { //std::cout<<"titleChanged: "<setText(url.url()); enterText(true); if (_record->isChecked()) appendCommand("expect "+map("urlChanged "+url.url())); diff --git a/src/webrunner.cxx b/src/webrunner.cxx index f1c0d4b..dfc76e8 100644 --- a/src/webrunner.cxx +++ b/src/webrunner.cxx @@ -115,6 +115,10 @@ int main(int argc, char *argv[]) try { (QStringList()<<"r"<<"retries", "on error retry up to times", "maxretries", "0")); + parser.addOption(QCommandLineOption + (QStringList()<<"timeout", + "set default timeout in seconds", + "timeout", "40")); parser.addOption(QCommandLineOption (QStringList()<<"W"<<"width", "set screenshot size to pixel", "width", "2048")); @@ -141,8 +145,10 @@ int main(int argc, char *argv[]) try { } if (parser.isSet("path")) script.path(parser.value("path")); int retries(parser.value("retries").toInt()); + int timeout(parser.value("timeout").toInt()); int width(parser.value("width").toInt()); int height(parser.value("height").toInt()); + script.defaultTimeout(parser.value("timeout").toInt()); QString target(parser.value("target-path")); p.resize(width, height); std::shared_ptr testsuites(new xml::Node("testsuites"));