diff --git a/src/commands.hxx b/src/commands.hxx index dab9a2a..d8b14b0 100644 --- a/src/commands.hxx +++ b/src/commands.hxx @@ -100,7 +100,6 @@ class Logger { ~Logger(); private: Command* _command; - Command* _previous; Script* _script; }; @@ -488,7 +487,7 @@ class Script: public QObject { public: Script(): _step(0), _clicktype(JAVASCRIPT_CLICK), - _screenshots(true), _command(0), _defaultTimeout(20) { + _screenshots(true), _defaultTimeout(20) { initPrototypes(); } Script(const Script& o): @@ -496,8 +495,7 @@ class Script: public QObject { _prototypes(o._prototypes), _step(0), _script(o._script), - _screenshots(true), - _command(0) { + _screenshots(true) { set(o); } QString syntax() const { @@ -597,8 +595,7 @@ class Script: public QObject { command->indent(indent); return command; } catch (Exception& e) { - e.line(linenr); - e.file(filename); + e.prependFileLine(filename, linenr); throw; } void parse(QStringList in, QString filename, int line = 1, int indent = 0) { @@ -662,6 +659,7 @@ class Script: public QObject { if (!_ignores.size() || (*cmd)->tag()=="label") { // not ignored _timer.start(_timeout*1000); try { + command(*cmd); if (!(res=(*cmd)->execute(this, frame))) { _timer.stop(); if (!back) retries = 0; else --back; @@ -755,8 +753,7 @@ class Script: public QObject { if ((*cmd)->isTestcase()) _testsuites->last()<line()); - e.file((*cmd)->file()); + e.prependFileLine((*cmd)->file(), (*cmd)->line()); if (screenshots) try { // write html source and take a last screenshot on error { @@ -783,11 +780,15 @@ class Script: public QObject { progress("success", 0, 0); return res; } - Command* command() { + std::shared_ptr command() { return _command; } - void command(Command* cmd) { + void command(std::shared_ptr cmd) { // maintained by Logger _command = cmd; + if (_parent) _parent->command(cmd); + } + void parent(Script* p) { + _parent = p; } QString& cout() { return _cout; @@ -1033,7 +1034,7 @@ class Script: public QObject { void log(QString text, Command* command = 0) { QString prefix (QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss ")); - Command* cmd(command?command:_command); + Command* cmd(command?command:_command.get()); for (QChar& c: text) if (c<32&&c!='\n') c='?'; if (cmd) prefix += QString("%2:%3%1 ") @@ -1149,7 +1150,7 @@ class Script: public QObject { QStringList(url.toString()))); } void timedout() { - error(TimeOut()); + error(TimeOut(_command->command())); } private: struct AuthRealm { @@ -1179,10 +1180,11 @@ class Script: public QObject { QString _targetdir; std::shared_ptr _testsuites; ///< only valid within run QString _testclass; - Command* _command; + std::shared_ptr _command; QString _path; QString _filename; QMap _auth; + Script* _parent = nullptr; }; class CommandContainer: public Command { @@ -3153,8 +3155,6 @@ inline bool Screenshot::execute(Script* script, QWebFrame* frame) { inline Logger::Logger(Command* command, Script* script, bool showLines): _command(command), _script(script) { if (command) { - _previous = _script->command(); - _script->command(command); if (_command->log()) { if (showLines) _script->log("\\ "+_command->command(), _command); @@ -3172,7 +3172,6 @@ inline void Logger::operator[](QString txt) { inline Logger::~Logger() { if (_command) { if (_command->log()) _script->log("/ "+_command->tag(), _command); - _script->command(_previous); } } @@ -3200,6 +3199,7 @@ inline bool Command::runScript(Logger& log, Command* parentCommand, QStringList args) { Script scriptCopy(*script); // only work with a copy of script scriptCopy.set(*parent); + scriptCopy.parent(parent); if (args.size()!=vars.size()) error(log, WrongNumberOfArguments(vars, args)); for (QStringList::iterator var(vars.begin()), arg(args.begin()); diff --git a/src/exceptions.hxx b/src/exceptions.hxx index de461d0..9885d82 100644 --- a/src/exceptions.hxx +++ b/src/exceptions.hxx @@ -20,11 +20,27 @@ class Exception: public std::exception { bytes = _what.toUtf8(); return bytes.data(); } - void line(int linenr) { - if (linenr>0) _what=QString::number(linenr)+" "+_what; + void prependFileLine(QString filename, int linenr) { + if (filename.size()) { + if (linenr>0) + _what = "→ in file "+filename+" at line "+QString::number(linenr)+":\n→ "+_what; + else + _what = "→ in file "+filename+":\n→ "+_what; + } else if (linenr>0) + _what = "→ at line "+QString::number(linenr)+":\n→ "+_what; } - void file(QString filename) { - if (filename.size()) _what=filename+":"+_what; + protected: + QString q(const QString& text) const { + return '"'+text+'"'; + } + QString p(const QString& text) const { + return QString("\n"+text).replace("\n", "\n "); + } + QString pq(const QString& text) const { + return p(q(text)); + } + QString pq(const QStringList& texts) const { + return pq(texts.join("\"\n\"")); } protected: QString _what; @@ -32,23 +48,25 @@ class Exception: public std::exception { class ParseError: public Exception { public: - ParseError(QString w): Exception("parse error: "+w) {} + ParseError(QString w): Exception("parse error:"+p(w)) {} }; class UnknownCommand: public ParseError { public: - UnknownCommand(QString line): ParseError("unknown command: \""+line+"\"") {} + UnknownCommand(QString line): ParseError("unknown command:"+p(q(line))) {} }; class BadArgument: public ParseError { public: - BadArgument(QString arg): ParseError("bad argument: "+arg) {} + BadArgument(QString arg): ParseError("bad argument:"+p(arg)) {} }; class MissingArguments: public ParseError { public: MissingArguments(QString args, QString req): - ParseError("missing arguments, requires "+req+", got: "+args) {} + ParseError("missing arguments:" + +p("requires: "+req) + +p("got: "+q(args))) {} }; class MissingLine: public ParseError { @@ -59,7 +77,7 @@ class MissingLine: public ParseError { class TestFailed: public Exception { public: - TestFailed(QString why): Exception("Test Failed: "+why) {} + TestFailed(QString why): Exception("test failed:"+p(why)) {} }; class PossibleRetryLoad: public TestFailed { @@ -72,10 +90,10 @@ class WrongSignal: public PossibleRetryLoad { WrongSignal(QString signal, QStringList args, std::pair received, QStringList sigs): - PossibleRetryLoad - ("expected: \""+signal+" "+args.join(' ')+"\"; " - "received: \""+received.first+" "+received.second.join(' ') - +"\"; queue: "+sigs.join(", ")) { + PossibleRetryLoad("wrong signal received:" + +p("expected: "+q(signal+" "+args.join(' '))) + +p("received: "+q(received.first+" "+received.second.join(' '))) + +p("queue:"+pq(sigs))) { } }; @@ -86,7 +104,7 @@ class UnhandledSignals: public TestFailed { TestFailed("unhandled signals:") { while (!sigs.empty()) { Signal res(sigs.front()); - _what += "\n"+res.first+" "+res.second.join(' '); + _what += q(res.first+" "+res.second.join(' ')); sigs.pop(); } } @@ -94,49 +112,49 @@ class UnhandledSignals: public TestFailed { class TimeOut: public PossibleRetryLoad { public: - TimeOut(): PossibleRetryLoad("command timeout") {} + TimeOut(QString cmd): PossibleRetryLoad("command timeout:"+p(cmd)) {} }; class ElementNotFound: public TestFailed { public: ElementNotFound(QString selector): - TestFailed("element not found: "+selector) {} + TestFailed("element not found:"+pq(selector)) {} }; class DirectoryCannotBeCreated: public TestFailed { public: DirectoryCannotBeCreated(QString name): - TestFailed("cannot create directory: "+name) {} + TestFailed("cannot create directory:"+pq(name)) {} }; class CannotWriteScreenshot: public TestFailed { public: CannotWriteScreenshot(QString name): - TestFailed("cannot write screenshot: "+name) {} + TestFailed("cannot write screenshot:"+pq(name)) {} }; class CannotWriteSouceHTML: public TestFailed { public: CannotWriteSouceHTML(QString name): - TestFailed("cannot write html source code: "+name) {} + TestFailed("cannot write html source code:"+pq(name)) {} }; class FileNotFound: public TestFailed { public: - FileNotFound(QString arg): TestFailed("file not found: "+arg) {} + FileNotFound(QString arg): TestFailed("file not found:"+pq(arg)) {} }; class NotACertificate: public TestFailed { public: NotACertificate(QString arg): - TestFailed("file is not a certificate: "+arg) { + TestFailed("file is not a certificate:"+pq(arg)) { } }; class KeyNotReadable: public TestFailed { public: KeyNotReadable(QString arg): - TestFailed("key file is not readable (password?): "+arg) { + TestFailed("key file is not readable (password?):"+pq(arg)) { } }; @@ -148,7 +166,7 @@ class NotUnattended: public TestFailed { class LastFileNotUploaded: public TestFailed { public: LastFileNotUploaded(QString arg): - TestFailed("last specified upload file has not been uploaded: "+arg) { + TestFailed("last specified upload file has not been uploaded:"+pq(arg)) { } }; @@ -165,38 +183,40 @@ class NoUploadFile: public TestFailed { class SetFileUploadFailed: public TestFailed { public: SetFileUploadFailed(QString selector, QString filename): - TestFailed("set file upload failed for selector "+selector - +" and file "+filename) { + TestFailed("set file upload failed for:" + +p("selector: "+q(selector)) + +p("file: "+q(filename))) { } }; class AssertionFailed: public TestFailed { public: AssertionFailed(QString text): - TestFailed("assertion failed: "+text) { + TestFailed("assertion failed:"+pq(text)) { } }; class DownloadFailed: public TestFailed { public: DownloadFailed(QString file): - TestFailed("download failed of file \""+file+"\"") { + TestFailed("download failed of file:"+pq(file)) { } }; class WriteFileFailed: public TestFailed { public: WriteFileFailed(QString file): - TestFailed("write failed of file \""+QDir(file).absolutePath()+"\"") { + TestFailed("write failed of file:"+pq(QDir(file).absolutePath())) { } }; class ScriptFailed: public TestFailed { public: ScriptFailed(QString dsc, QString cmd, QStringList args, QString script): - TestFailed(dsc+"; command: "+cmd - +(args.size()?" "+args.join(' '):QString()) - +(script.size()?"\n"+script:QString())) { + TestFailed(dsc + +p("command: " + +pq(cmd+(args.size()?" "+args.join(' '):QString()))) + +(script.size()?p("script:"+p(script)):QString())) { } }; @@ -226,8 +246,8 @@ class ScriptExecutionFailed: public ScriptFailed { ScriptExecutionFailed(QString command, QStringList args, QString script, int code, QString sout, QString serr): ScriptFailed("failed with exit code "+QString::number(code) - +(sout.size()?"; sout=\""+sout+"\"":"") - +(serr.size()?"; serr=\""+serr+"\"":""), + +(sout.size()?p("stdout: "+q(sout)):"") + +(serr.size()?p("stderr: "+q(serr)):""), command, args, script) { } }; @@ -244,53 +264,53 @@ class FunctionCallFailed: public TestFailed { public: FunctionCallFailed(QString name, QStringList vars, QStringList args, const std::exception& x): - TestFailed("function call failed: "+name) { - for (QStringList::iterator var(vars.begin()), arg(args.begin()); - varsetChecked(false); _record->setEnabled(false); _run->setEnabled(false); + Script script; try { - Script script; connect(&script, SIGNAL(logging(QString)), SLOT(logging(QString))); connect(&script, SIGNAL(progress(QString, int, int)), SLOT(progress(QString, int, int))); std::shared_ptr testsuites(new xml::Node("testsuite")); @@ -176,9 +176,33 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI { _status->setCurrentIndex(STATUS_SUCCESS); } catch (std::exception &x) { _status->setCurrentIndex(STATUS_ERROR); - QMessageBox::critical(this, tr("Test Failed"), tr("Error [%1]: %2") - .arg(demangle(typeid(x).name())) - .arg(x.what())); + std::shared_ptr cmd(script.command()); + if (cmd) + QMessageBox::critical(this, + tr("Test Failed"), + tr("" + "

Error [%1]

" + "
" + "
Command:
%3
" + "
File:
%4
" + "
Line:
%5
" + "
Error Message:
%2
" + "
" + "") + .arg(demangle(typeid(x).name())) + .arg(x.what()) + .arg(cmd->command()) + .arg(cmd->file()) + .arg(cmd->line())); + else + QMessageBox::critical(this, + tr("Test Failed"), + tr("" + "

Error [%1]

" + "

%2

" + "") + .arg(demangle(typeid(x).name())) + .arg(QString(x.what()).replace("\n", "
"))); } _run->setEnabled(true); _record->setEnabled(true);