/*! @file
@id $Id$
*/
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
#ifndef COMMANDS_HXX
#define COMMANDS_HXX
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
")
.replace("\n", "
")
+"
";
} break;
}
return cmds.trimmed();
}
void reset() {
_script.clear();
while (!_signals.empty()) _signals.pop();
_timer.stop();
_ignores.clear();
_cout.clear();
_cerr.clear();
_ignoreSignalsUntil.clear();
}
void cleanup() {
reset();
_variables.clear();
_rvariables.clear();
_functions.clear();
_timeout = 20;
_clicktype = JAVASCRIPT_CLICK;
}
std::shared_ptr parseLine(QStringList& in,
QString filename, int linenr,
int indent) try {
std::shared_ptr command;
if (!in.size()) throw MissingLine();
QString line(in.takeFirst().trimmed());
QString cmd(line), args;
int space(line.indexOf(' '));
if (space>0) {
cmd = line.left(space);
args = line.right(line.size()-space-1);
}
Prototypes::const_iterator it(_prototypes.find(cmd));
if (it!=_prototypes.end()) {
command = it->second->parse(this, args, in, filename, linenr, indent);
} else {
command = unknown(line);
}
command->file(filename);
command->line(linenr);
command->indent(indent);
return command;
} catch (Exception& e) {
e.line(linenr);
e.file(filename);
throw;
}
void parse(QStringList in, QString filename, int line = 1, int indent = 0) {
for (int linenr(0), oldsize(0);
oldsize=in.size(), in.size();
linenr+=oldsize-in.size())
_script.push_back(parseLine(in, filename, line+linenr, indent));
}
QStringList print() {
QStringList result;
for (auto cmd(_script.begin()); cmd!=_script.end(); ++cmd) {
result += (*cmd)->command();
}
return result;
}
bool run(QWebFrame* frame) {
return run(frame, _testsuites, targetdir(), _screenshots, _maxretries);
}
bool run(QWebFrame* frame, std::shared_ptr testsuites,
QString td = QString(), bool screenshots = true,
int maxretries = 0) {
bool res(true);
_testsuites = testsuites;
_timeout = 20; // defaults to 20s
_ignoreSignalsUntil.clear();
addSignals(frame);
_screenshots = screenshots;
_maxretries = maxretries;
_timer.setSingleShot(true);
targetdir(!td.isEmpty()
? td
: _testsuites->children()
? xmlstr(_testsuites->last().attr("name"))
: "attachments");
if (!_testsuites->children()) {
xml::Node testsuite("testsuite");
testsuite.attr("name") = "Unnamed Test Suite";
(*_testsuites)<file()).arg((*cmd)->line()),
step, steps());
xml::Node testcase("testcase");
try {
testcase.attr("classname") =
xmlattr(_testclass.size()
?_testclass
:"file."+(*cmd)->file()
.replace(QRegularExpression(".wt$"), "")
.replace(".", "-"), true)
.toStdString();
testcase.attr("name") =
xmlattr(QString("%1: %2")
.arg((*cmd)->line())
.arg((*cmd)->command().split('\n').takeFirst(), true))
.toStdString();
if (!_ignores.size() || (*cmd)->tag()=="label") { // not ignored
_timer.start(_timeout*1000);
try {
if (!(res=(*cmd)->execute(this, frame))) {
_timer.stop();
if (!back) retries = 0; else --back;
testcase<<(xml::String("system-out") =
xmlattr(_cout).toStdString());
testcase<<(xml::String("system-err") =
xmlattr(_cerr).toStdString());
_cout.clear();
_cerr.clear();
_testsuites->last()<file()).arg((*cmd)->line()),
step, steps());
} catch (PossibleRetryLoad& e) {
_timer.stop();
// timeout may happen during load due to bad internet connection
if (screenshots)
try { // take a screenshot on error
Logger log(0, this);
QString filename(Screenshot::screenshot
(log, (*cmd)->line(), targetdir(),
_testclass,
QString("retry-%1")
.arg((ulong)retries, 2, 10,
QLatin1Char('0')),
frame));
plainlog("[[ATTACHMENT|"+filename+"]]");
} catch (... ) {} // ignore exception in screenshot
if (++retries<=maxretries) { // retry in that case
QUrl url(frame->url());
if ((*cmd)->command()=="expect loadFinished true") {
------cmd;
------step;
back += 3;
_ignoreSignalsUntil = "loadStarted";
frame->load(url);
} else if ((*cmd)->command()=="expect loadStarted") {
----cmd;
----step;
back += 2;
_ignoreSignalsUntil = "loadStarted";
frame->page()->triggerAction(QWebPage::Stop);
} else if ((*cmd)->command().startsWith("expect urlChanged")) {
QString url2((*cmd)->command());
url2.remove("expect urlChanged");
if (url2.size()) url=url2.trimmed();
----cmd;
----step;
back += 2;
_ignoreSignalsUntil = "loadStarted";
frame->load(url);
} else if ((*cmd)->command().startsWith("expect load")) {
QString url2((*cmd)->command());
url2.remove("expect load");
if (url2.size()) url=url2.trimmed();
----cmd;
----step;
back += 2;
_ignoreSignalsUntil = "loadStarted";
frame->load(url);
} else {
throw;
}
} else {
throw;
}
log(QString("WARNING: retry#%1, redo last %2 steps; error: %3")
.arg(retries).arg(back).arg(e.what()));
}
_timer.stop();
if (!back) retries = 0; else --back;
testcase<<(xml::String("system-out") =
xmlattr(_cout).toStdString());
testcase<<(xml::String("system-err") =
xmlattr(_cerr).toStdString());
_cout.clear();
_cerr.clear();
if ((*cmd)->isTestcase())
_testsuites->last()<isTestcase())
_testsuites->last()<line());
e.file((*cmd)->file());
if (screenshots)
try { // write html source and take a last screenshot on error
{
Logger log(0, this);
QString filename(Screenshot::sourceHtml
(log, (*cmd)->line(), targetdir(),
_testclass,
"error", frame));
plainlog("[[ATTACHMENT|"+filename+"]]");
} {
Logger log(0, this);
QString filename(Screenshot::screenshot
(log, (*cmd)->line(), targetdir(),
_testclass,
"error", frame));
plainlog("[[ATTACHMENT|"+filename+"]]");
}
} catch (... ) {} // ignore exception in screenshot
throw;
}
}
removeSignals(frame);
if (!_signals.empty()) error(UnhandledSignals(_signals));
progress("success", 0, 0);
return res;
}
Command* command() {
return _command;
}
void command(Command* cmd) {
_command = cmd;
}
QString& cout() {
return _cout;
}
QString& cerr() {
return _cerr;
}
int steps() {
return _script.size();
}
bool screenshots() {
return _screenshots;
}
void targetdir(QString name) {
_targetdir = name;
}
QString targetdir() {
return _targetdir;
}
void testclass(QString tc) {
_testclass = tc;
}
QString testclass() {
return _testclass;
}
void testsuite(QString name) {
xml::Node testsuite("testsuite");
testsuite.attr("name") = xmlattr(name, true).toStdString();
testsuite.attr("timestamp") =
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
_testsuites->last().attr("failures") = "0";
(*_testsuites)<::iterator it(_variables.find(name));
if (it==_variables.end()) error(VariableNotFound(name));
return *it;
}
/// Copy context from other script
void set(const Script& o) {
_variables = o._variables;
_rvariables = o._rvariables;
_timeout = o._timeout;
_clicktype = o._clicktype;
_testsuites = o._testsuites;
_testclass = o._testclass;
_targetdir = o._targetdir;
_maxretries = o._maxretries;
_screenshots = o._screenshots;
_cout.clear();
_cerr.clear();
_ignoreSignalsUntil.clear();
_functions.unite(o._functions);
}
void unset(QString name) {
_rvariables.remove(_variables[name]);
_variables.remove(name);
}
QStringList functions() {
return _functions.keys();
}
void function(QString name, std::shared_ptr f) {
_functions[name] = f;
}
std::shared_ptr function(Logger& log, QString name) {
QMap >::iterator
it(_functions.find(name));
if (it==_functions.end()) error(FunctionNotFound(name));
return *it;
}
void timeout(int t) {
_timeout = t;
}
void clicktype(ClickType c) {
_clicktype = c;
}
ClickType clicktype() {
return _clicktype;
}
QString replacevars(QString txt) {
for (QMap::iterator it(_variables.begin());
it!=_variables.end(); ++it)
txt.replace(it.key(), it.value(), Qt::CaseSensitive);
return txt;
}
QStringList replacevars(QStringList txts) {
for (QStringList::iterator txt(txts.begin()); txt!=txts.end(); ++txt)
*txt = replacevars(*txt);
return txts;
}
QString insertvars(QString txt) {
QMapIterator it(_rvariables);
it.toBack();
while (it.hasPrevious()) {
it.previous();
txt.replace(it.key(), it.value(), Qt::CaseSensitive);
}
return txt;
}
QString result() {
if (_script.size()) return (*_script.rbegin())->result();
return QString();
}
void addSignals(QWebFrame* frame) {
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()));
}
void removeSignals(QWebFrame* frame) {
disconnect(dynamic_cast
(frame->page()->networkAccessManager()),
SIGNAL(log(QString)),
this, SLOT(log(QString)));
disconnect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
this, SLOT(contentsSizeChanged(const QSize&)));
disconnect(frame, SIGNAL(iconChanged()),
this, SLOT(iconChanged()));
disconnect(frame, SIGNAL(initialLayoutCompleted()),
this, SLOT(initialLayoutCompleted()));
disconnect(frame, SIGNAL(javaScriptWindowObjectCleared()),
this, SLOT(javaScriptWindowObjectCleared()));
disconnect(frame, SIGNAL(loadFinished(bool)),
this, SLOT(loadFinished(bool)));
disconnect(frame, SIGNAL(loadStarted()),
this, SLOT(loadStarted()));
disconnect(frame, SIGNAL(titleChanged(const QString&)),
this, SLOT(titleChanged(const QString&)));
disconnect(frame, SIGNAL(urlChanged(const QUrl&)),
this, SLOT(urlChanged(const QUrl&)));
disconnect(frame, SIGNAL(urlChanged(const QUrl&)),
this, SLOT(urlChanged(const QUrl&)));
disconnect(&_timer, SIGNAL(timeout()), this, SLOT(timeout()));
}
public Q_SLOTS:
void log(QString text, Command* command = 0) {
QString prefix
(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss "));
Command* cmd(command?command:_command);
for (QChar& c: text) if (c<32&&c!='\n') c='?';
if (cmd)
prefix += QString("%2:%3%1 ")
.arg(QString(cmd->indent(), QChar(' ')))
.arg(cmd->file(), 20, QChar(' '))
.arg(cmd->line(), -4, 10, QChar(' '));
text = prefix+text.split('\n').join("\n"+prefix+" ");
logging(text);
std::cout< unknown(QString command) {
if (!command.size())
return std::shared_ptr(new Empty());
if (command[0]=='#')
return std::shared_ptr(new Comment(command));
throw UnknownCommand(command); // error
}
void initPrototypes();
void add(Command* c) {
_prototypes[c->tag()] = std::shared_ptr(c);
}
private Q_SLOTS:
void contentsSizeChanged(const QSize&) {
}
void iconChanged() {
}
void initialLayoutCompleted() {
}
void javaScriptWindowObjectCleared() {
}
void loadFinished(bool ok) {
QString sig(ok?"true":"false");
if (_ignoreSignalsUntil.size() &&
_ignoreSignalsUntil != "loadFinished "+sig) {
log("warning: ignored loadFinished, waiting for "+_ignoreSignalsUntil);
return;
}
_ignoreSignalsUntil.clear();
log(" signal received: loadFinished "+QString(ok?"true":"false"));
_signals.push(std::make_pair("loadFinished", QStringList(sig)));
}
void loadStarted() {
if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "loadStarted") {
log("warning: ignored loadStarted, waiting for "+_ignoreSignalsUntil);
return;
}
_ignoreSignalsUntil.clear();
log(" signal received: loadStarted");
_signals.push(std::make_pair("loadStarted", QStringList()));
}
void frameChanged() {
}
void titleChanged(const QString&) {
//_signals.push(std::make_pair("titleChanged", QStringList(title)));
}
void urlChanged(const QUrl& url) {
if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "urlChanged") {
log("warning: ignored urlChanged, waiting for "+_ignoreSignalsUntil);
return;
}
_ignoreSignalsUntil.clear();
log(" signal received: urlChanged "+url.toString());
_signals.push(std::make_pair("urlChanged",
QStringList(url.toString())));
}
void timeout() {
error(TimeOut());
}
private:
typedef std::map> Prototypes;
typedef std::vector> Commands;
Prototypes _prototypes;
Commands _script;
std::queue _signals;
QTimer _timer;
QSet _ignores;
QString _cout;
QString _cerr;
bool _screenshots;
int _maxretries;
QString _ignoreSignalsUntil;
QMap _variables; ///< variable mapping
QMap _rvariables; ///< reverse variable mapping
QMap > _functions;
int _timeout;
ClickType _clicktype;
QString _targetdir;
std::shared_ptr _testsuites; ///< only valid within run
QString _testclass;
Command* _command;
QString _path;
};
class Do: public Command {
public:
QString tag() const {
return "do";
}
QString description() const {
return
tag()+" []\n \n "
"\n\n"
"Execute JavaScript on a CSS selected object. The object is the first "
"object in the DOM tree that matches the given CSS selector. You can "
"refere to the selected object within the scripy by \"this\". The "
"JavaScript code is on the following lines and at least intended by "
"one space";
}
QString command() const {
return tag()+" "+_selector+(_javascript.size()?"\n"+_javascript:"");
}
std::shared_ptr parse(Script*, QString args,
QStringList& in, QString, int, int) {
std::shared_ptr cmd(new Do());
cmd->_selector = args.trimmed();
cmd->_javascript = subCommandBlock(in).join("\n");
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QWebElement element(frame->documentElement());
if (_selector.size()) {
element = find(frame, script->replacevars(_selector));
if (element.isNull())
error(log, ElementNotFound(script->replacevars(_selector)));
}
_result =
element.evaluateJavaScript(script->replacevars(_javascript)).toString();
log("result: "+(_result.size()?_result:"(void)"));
return true;
}
private:
QString _selector;
QString _javascript;
};
class Load: public Command {
public:
QString tag() const {
return "load";
}
QString description() const {
return
tag()+" "
"\n\n"
"Load an URL, the URL is given as parameter in full syntax.";
}
QString command() const {
return tag()+" "+_url;
}
std::shared_ptr parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr cmd(new Load());
cmd->_url = args;
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
frame->load(script->replacevars(_url));
return true;
}
private:
QString _url;
};
class Expect: public Command {
public:
QString tag() const {
return "expect";
}
QString description() const {
return
tag()+" []"
"\n\n"
"Expect a signal. Signals are emitted by webkit and may contain "
"parameter. If a parameter is given in the script, then the parameter "
"must match exactly. If no parameter is given, then the signal must "
"be emitted, but the parameters of the signal are not checked."
"\n\n"
"Known signals and parameters are:\n"
" - loadFinished \n"
" - loadStarted\n"
" - urlChanged \n"
"There is a short hand:\n"
" - load \n"
" stands for the three signals:\n"
" - loadStarted (optional)\n"
" - urlChanged \n"
" - loadFinished true";
}
QString command() const {
return tag()+" "+_signal._signal
+(_signal._args.size()?" "+_signal._args.join(' '):QString());
}
std::shared_ptr parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr cmd(new Expect());
cmd->_signal._args = args.split(" ");
cmd->_signal._signal = cmd->_signal._args.takeFirst();
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
QStringList args(script->replacevars(_signal._args));
Script::Signal lastsignal(script->getSignal());
if (_signal._signal=="load") { // special signal load
while (lastsignal.first=="loadStarted") {
log("ignore signal: loadStarted");
lastsignal = script->getSignal(); // ignore optional loadStarted
}
if (lastsignal.first!="urlChanged" || (args.size() && lastsignal.second!=args))
error(log, WrongSignal("urlChanged", args, lastsignal));
lastsignal = script->getSignal();
args=QStringList("true");
if (lastsignal.first!="loadFinished" || (lastsignal.second!=args))
error(log, WrongSignal("loadFinished", args, lastsignal));
} else {
if (lastsignal.first!=_signal._signal || (args.size() && lastsignal.second!=args))
error(log, WrongSignal(_signal._signal, args, lastsignal));
}
return true;
}
private:
struct Signal {
Signal(QString s, QStringList a): _signal(s), _args(a) {}
Signal(QString s, QString a): _signal(s), _args(a) {}
Signal(QString s): _signal(s) {}
Signal() {}
QString _signal;
QStringList _args;
};
Signal _signal;
};
class Open: public Command {
public:
QString tag() const {
return "open";
}
QString description() const {
return
tag()+
"\n\n"
"Open the browser window, so you can follow the test steps visually.";
}
QString command() const {
return tag();
}
std::shared_ptr parse(Script*, QString,
QStringList&, QString, int, int) {
std::shared_ptr cmd(new Open());
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
frame->page()->view()->show();
return true;
}
};
class Sleep: public Command {
public:
QString tag() const {
return "sleep";
}
QString description() const {
return
tag()+" "
"\n\n"
"Sleep for a certain amount of seconds. This helps, if you must wait "
"for some javascript actions, i.e. AJAX or slow pages, and the "
"excpeted signals are not sufficient.";
}
QString command() const {
return tag()+" "+_time;
}
std::shared_ptr parse(Script*, QString time,
QStringList&, QString, int, int) {
std::shared_ptr cmd(new Sleep());
cmd->_time = "10"; // default: 10s
if (time.size()) cmd->_time = time;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->timer().stop();
bool ok;
unsigned int time(script->replacevars(_time).toUInt(&ok));
if (!ok)
error(log, BadArgument(script->replacevars(_time)
+" should be a number of seconds"));
sleep(time);
return true;
}
private:
QString _time;
};
class Exit: public Command {
public:
QString tag() const {
return "exit";
}
QString description() const {
return
tag()+
"\n\n"
"Successfully terminate script immediately. The following commands "
"are not executed. This helps when you debug your scripts and you "
"want the script stop at a certain point for investigations.";
}
QString command() const {
return tag();
}
std::shared_ptr parse(Script*, QString,
QStringList&, QString, int, int) {
std::shared_ptr cmd(new Exit());
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
return false;
}
};
class IgnoreTo: public Command {
public:
QString tag() const {
return "ignoreto";
}
QString description() const {
return
tag()+"