SVN commit 1146719 by beschow:
[kate:kate1.git] / part / tests / test_regression.cpp
1 /**
2  * This file is part of the KDE project
3  *
4  * Copyright (C) 2001,2003 Peter Kelly (pmk@post.com)
5  * Copyright (C) 2003,2004 Stephan Kulow (coolo@kde.org)
6  * Copyright (C) 2004 Dirk Mueller ( mueller@kde.org )
7  * Copyright 2006, 2007 Leo Savernik (l.savernik@aon.at)
8  *
9  * This library is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Library General Public
11  * License as published by the Free Software Foundation; either
12  * version 2 of the License, or (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Library General Public License for more details.
18  *
19  * You should have received a copy of the GNU Library General Public License
20  * along with this library; see the file COPYING.LIB.  If not, write to
21  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22  * Boston, MA 02110-1301, USA.
23  *
24  */
25
26 //BEGIN Includes
27 #include "test_regression.h"
28 #include "testutils.h"
29
30 #include "kateview.h"
31 #include "katedocument.h"
32 #include "katedocumenthelpers.h"
33 #include "kateconfig.h"
34 #include "katecmd.h"
35 #include "kateglobal.h"
36 #include <ktexteditor/commandinterface.h>
37
38 #include <kapplication.h>
39 #include <kglobal.h>
40 #include <kstandarddirs.h>
41 #include <kaction.h>
42 #include <kcmdlineargs.h>
43 #include <kmainwindow.h>
44 #include <kconfig.h>
45 #include <kconfiggroup.h>
46 #include <kglobalsettings.h>
47 #include <kdefakes.h>
48 #include <kstatusbar.h>
49 #include <kio/job.h>
50
51 #include <memory>
52 #include <cstdio>
53 #include <cstdlib>
54 #include <climits>
55 #include <limits.h>
56 #include <sys/time.h>
57 #include <sys/resource.h>
58 #include <sys/types.h>
59 #include <sys/wait.h>
60 #include <unistd.h>
61 #include <pwd.h>
62 #include <signal.h>
63
64 #include <QtCore/QObject>
65 #include <QtCore/QFile>
66 #include <QtCore/QDir>
67 #include <QtCore/QString>
68 #include <QtCore/QRegExp>
69 #include <QtCore/QTextStream>
70 #include <QtCore/QList>
71 #include <QtCore/QTimer>
72 #include <QtCore/QFileInfo>
73 #include <QtCore/Q_PID>
74 #include <QtCore/QEvent>
75 #include <QtCore/QTimer>
76
77 #include <QtScript/QScriptEngine>
78 #include <QTest>
79
80 //END Includes
81
82 #define BASE_DIR_CONFIG "/.testkateregression"
83 #define UNIQUE_HOME_DIR "/var/tmp/%1_kate4_non_existent"
84
85 static KMainWindow* toplevel;
86
87 // -------------------------------------------------------------------------
88
89 const char failureSnapshotPrefix[] = "testkateregressionrc-FS.";
90
91 static QString findMostRecentFailureSnapshot()
92 {
93   QDir dir(KGlobal::dirs()->saveLocation("config"),
94            QString(failureSnapshotPrefix) + '*',
95            QDir::Time, QDir::Files);
96   QStringList entries = dir.entryList();
97   return entries.isEmpty() ? QString() : dir[0].mid(sizeof failureSnapshotPrefix - 1);
98 }
99
100 int main(int argc, char *argv[])
101 {
102   KCmdLineOptions options;
103   options.add("b");
104   options.add("base <base_dir>", ki18n("Directory containing tests, basedir and output directories."));
105   options.add("cmp-failures <snapshot>", ki18n("Compare failures of this testrun against snapshot <snapshot>. Defaults to the most recently captured failure snapshot or none if none exists."));
106   options.add("d");
107   options.add("debug", ki18n("Do not suppress debug output"));
108   options.add("g");
109   options.add("genoutput", ki18n("Regenerate baseline (instead of checking)"));
110   options.add("keep-output", ki18n("Keep output files even on success"));
111   options.add("save-failures <snapshot>", ki18n("Save failures of this testrun as failure snapshot <snapshot>"));
112   options.add("s");
113   options.add("show", ki18n("Show the window while running tests"));
114   options.add("t");
115   options.add("test <filename>", ki18n("Only run a single test. Multiple options allowed."));
116   options.add("o");
117   options.add("output <directory>", ki18n("Put output in <directory> instead of <base_dir>/output"));
118   options.add("fork", ki18n("Run each test case in a separate process."));
119   options.add("+[base_dir]", ki18n("Directory containing tests, basedir and output directories. Only regarded if -b is not specified."));
120   options.add("+[testcases]", ki18n("Relative path to testcase, or directory of testcases to be run (equivalent to -t)."));
121
122   // forget about any settings
123   passwd* password = getpwuid( getuid() );
124   if (!password) {
125     fprintf(stderr, "dang, I don't even know who I am.\n");
126     exit(1);
127   }
128
129   QString kdeHome(UNIQUE_HOME_DIR);
130   kdeHome = kdeHome.arg( password->pw_name );
131   setenv( "KDEHOME", kdeHome.toLatin1().constData(), 1 );
132   setenv( "LC_ALL", "C", 1 );
133   setenv( "LANG", "C", 1 );
134
135 //   signal( SIGALRM, signal_handler );
136
137   KCmdLineArgs::init(argc, argv, "testregression", 0, ki18n("TestRegression"),
138                      "1.0", ki18n("Regression tester for kate"));
139   KCmdLineArgs::addCmdLineOptions(options);
140
141   KCmdLineArgs *args = KCmdLineArgs::parsedArgs( );
142
143   QString baseDir = args->getOption("base");
144   QByteArray homeDir = qgetenv("HOME");
145   QByteArray baseDirConfigFile(homeDir + QByteArray(BASE_DIR_CONFIG));
146   {
147     QFile baseDirConfig(baseDirConfigFile);
148     if (baseDirConfig.open(QFile::ReadOnly)) {
149       QTextStream bds(&baseDirConfig);
150       baseDir = bds.readLine();
151     }
152   }
153
154   if ( args->count() < 1 && baseDir.isEmpty() ) {
155     printf("For regression testing, make sure to have checked out the kate regression\n"
156            "testsuite from svn:\n"
157            "\tsvn co \"https://<user>@svn.kde.org:/home/kde/trunk/tests/katetests/regression\"\n"
158            "Remember the root path into which you checked out the testsuite.\n"
159            "\n");
160     printf("%s needs the root path of the kate regression\n"
161            "testsuite to function properly\n"
162            "By default, the root path is looked up in the file\n"
163            "\t%s\n"
164            "If it doesn't exist yet, create it by invoking\n"
165            "\techo \"<root-path>\" > %s\n"
166            "You may override the location by specifying the root explicitly on the\n"
167            "command line with option -b\n"
168            "", argv[0],
169            baseDirConfigFile.constData(),
170            baseDirConfigFile.constData());
171     ::exit( 1 );
172   }
173
174   int testcase_index = 0;
175   if (baseDir.isEmpty()) baseDir = args->arg(testcase_index++);
176
177   QFileInfo bdInfo(baseDir);
178   baseDir = bdInfo.absoluteFilePath();
179
180   const char *subdirs[] = {"tests", "baseline", "output", "resources"};
181   for ( int i = 0; i < 2; i++ ) {
182     QFileInfo sourceDir(baseDir + '/' + subdirs[i]);
183     if ( !sourceDir.exists() || !sourceDir.isDir() ) {
184       fprintf(stderr,"ERROR: Source directory \"%s/%s\": no such directory.\n", baseDir.toLatin1().constData(), subdirs[i]);
185       exit(1);
186     }
187   }
188
189   KateTestApp a(args, baseDir, testcase_index);
190
191   // queue quit action
192   QTimer::singleShot(0, &a, SLOT(quit()));
193
194   a.exec();
195
196   return a.allTestsSucceeded() ? 0 : 1;
197 }
198
199 // -------------------------------------------------------------------------
200
201 RegressionTest::RegressionTest(KateDocument *part, const KConfig *baseConfig,
202                                const QString &baseDir, const KCmdLineArgs *args)
203   : QObject(part)
204   , m_part(part)
205   , m_view( static_cast<KateView *>(m_part->widget()) )
206   , m_baseConfig(baseConfig)
207   , m_baseDir(baseDir)
208   , m_outputDir(args->getOption("output"))
209   , m_genOutput(args->isSet("genoutput"))
210   , m_fork(args->isSet("fork"))
211   , m_failureComp(0)
212   , m_failureSave(0)
213   , m_keepOutput(args->isSet("keep-output"))
214   , m_passes_work(0)
215   , m_passes_fail(0)
216   , m_passes_new(0)
217   , m_failures_work(0)
218   , m_failures_fail(0)
219   , m_failures_new(0)
220   , m_errors(0)
221 {
222   m_baseDir = m_baseDir.replace( "//", "/" );
223   if ( m_baseDir.endsWith( "/" ) )
224     m_baseDir = m_baseDir.left( m_baseDir.length() - 1 );
225
226   if (m_outputDir.isEmpty())
227     m_outputDir = m_baseDir + "/output";
228
229   QFile::remove( m_outputDir + "/links.html" );
230   QFile f( m_outputDir + "/empty.html" );
231   QString s;
232   f.open( QFile::WriteOnly | QFile::Truncate );
233   s = "<html><body>Follow the white rabbit";
234   f.write( s.toLatin1(), s.length() );
235   f.close();
236   f.setFileName( m_outputDir + "/index.html" );
237   f.open( QFile::WriteOnly | QFile::Truncate );
238   s = "<html><frameset cols=150,*><frame src=links.html><frame name=content src=empty.html>";
239   f.write( s.toLatin1(), s.length() );
240   f.close();
241 }
242
243 static QStringList readListFile( const QString &filename )
244 {
245   // Read ignore file for this directory
246   QString ignoreFilename = filename;
247   QFileInfo ignoreInfo(ignoreFilename);
248   QStringList ignoreFiles;
249   if (ignoreInfo.exists()) {
250     QFile ignoreFile(ignoreFilename);
251     if (!ignoreFile.open(QFile::ReadOnly)) {
252       fprintf(stderr,"Can't open %s\n",ignoreFilename.toLatin1().constData());
253       exit(1);
254     }
255     QTextStream ignoreStream(&ignoreFile);
256     QString line;
257     while (!(line = ignoreStream.readLine()).isNull())
258       ignoreFiles.append(line);
259     ignoreFile.close();
260   }
261   return ignoreFiles;
262 }
263
264 RegressionTest::~RegressionTest()
265 {
266   // Important! Delete comparison config *first* as saver config
267   // might point to the same physical file.
268   KConfig *tmp = m_failureComp ? m_failureComp->config() : 0;
269   delete m_failureComp;
270   if (m_failureSave && tmp != m_failureSave->config()) {
271     delete tmp;
272     tmp = m_failureSave->config();
273   }
274   delete m_failureSave;
275   delete tmp;
276 }
277
278 void RegressionTest::setFailureSnapshotConfig(KConfig *cfg, const QString &sname)
279 {
280   Q_ASSERT(cfg);
281   m_failureComp = new KConfigGroup(cfg, sname);
282 }
283
284 void RegressionTest::setFailureSnapshotSaver(KConfig *cfg, const QString &sname)
285 {
286   Q_ASSERT(cfg);
287   m_failureSave = new KConfigGroup(cfg, sname);
288 }
289
290 QStringList RegressionTest::concatListFiles(const QString &relPath, const QString &filename)
291 {
292   QStringList cmds;
293   int pos = relPath.lastIndexOf('/');
294   if (pos >= 0)
295     cmds += concatListFiles(relPath.left(pos), filename);
296   cmds += readListFile(m_baseDir + "/tests/" + relPath + '/' + filename);
297   return cmds;
298 }
299
300 bool RegressionTest::runTests(QString relPath, bool mustExist, int known_failure)
301 {
302   if (!QFile(m_baseDir + "/tests/" + relPath).exists()) {
303     fprintf(stderr,"%s: No such file or directory\n",relPath.toLatin1().constData());
304     return false;
305   }
306
307   QString fullPath = m_baseDir + "/tests/"+relPath;
308   QFileInfo info(fullPath);
309
310   if (!info.exists() && mustExist) {
311     fprintf(stderr,"%s: No such file or directory\n",relPath.toLatin1().constData());
312     return false;
313   }
314
315   if (!info.isReadable() && mustExist) {
316     fprintf(stderr,"%s: Access denied\n",relPath.toLatin1().constData());
317     return false;
318   }
319
320   if (info.isDir()) {
321     QStringList ignoreFiles = readListFile(  m_baseDir + "/tests/"+relPath+"/ignore" );
322     QStringList failureFiles = readListFile(  m_baseDir + "/tests/"+relPath+"/KNOWN_FAILURES" );
323
324     // Run each test in this directory, recusively
325     QDir sourceDir(m_baseDir + "/tests/"+relPath);
326     for (uint fileno = 0; fileno < sourceDir.count(); fileno++) {
327       QString filename = sourceDir[fileno];
328       QString relFilename = relPath.isEmpty() ? filename : relPath+'/'+filename;
329
330       if (filename.startsWith(".") || ignoreFiles.contains(filename) )
331         continue;
332       int failure_type = NoFailure;
333       if ( failureFiles.contains( filename ) )
334         failure_type |= AllFailure;
335       if ( failureFiles.contains ( filename + "-result" ) )
336         failure_type |= ResultFailure;
337       runTests(relFilename, false, failure_type);
338     }
339   } else if (info.isFile()) {
340
341     QString relativeDir = QFileInfo(relPath).dir().path();
342     QString filename = info.fileName();
343     QString currentBase = m_baseDir + "/tests/"+relativeDir;
344     m_currentCategory = relativeDir;
345     m_currentTest = filename;
346     m_known_failures = known_failure;
347     m_outputCustomised = false;
348     // gather commands
349     // directory-specific commands
350     QStringList commands = concatListFiles(relPath, ".kateconfig-commands");
351     // testcase-specific commands
352     commands += readListFile(currentBase + '/' + filename + "-commands");
353
354     rereadConfig(); // reset options to default
355     if ( filename.endsWith(".txt") ) {
356 #if 0
357       if ( relPath.startsWith( "domts/" ) && !m_runJS )
358         return true;
359       if ( relPath.startsWith( "ecma/" ) && !m_runJS )
360         return true;
361 #endif
362 //       if ( m_runHTML )
363       testStaticFile(relPath, commands);
364     } else if (mustExist) {
365       fprintf(stderr,"%s: Not a valid test file (must be .txt)\n",relPath.toLatin1().constData());
366       return false;
367     }
368   } else if (mustExist) {
369     fprintf(stderr,"%s: Not a regular file\n",relPath.toLatin1().constData());
370     return false;
371   }
372
373   return true;
374 }
375
376 bool RegressionTest::allTestsSucceeded() const
377 {
378   return m_failures_work == 0 && m_errors == 0;
379 }
380
381 void RegressionTest::createLink( const QString& test, int failures )
382 {
383   OutputObject::createMissingDirs( m_outputDir + '/' + test + "-compare.html" );
384
385   QFile list( m_outputDir + "/links.html" );
386   list.open( QFile::WriteOnly|QFile::Append );
387   QString link;
388   link = QString( "<a href=\"%1\" target=\"content\" title=\"%2\">" )
389       .arg( test + "-compare.html" )
390       .arg( test );
391   link += m_currentTest;
392   link += "</a> ";
393   if (failures & NewFailure)
394     link += "<span style=\"font-weight:bold;color:red\">";
395   link += '[';
396   if ( failures & ResultFailure )
397     link += 'R';
398   link += ']';
399   if (failures & NewFailure)
400     link += "</span>";
401   link += "<br>\n";
402   list.write( link.toLatin1(), link.length() );
403   list.close();
404 }
405
406 /** returns the path in a way that is relatively reachable from base.
407  * @param base base directory (must not include trailing slash)
408  * @param path directory/file to be relatively reached by base
409  * @return path with all elements replaced by .. and concerning path elements
410  *      to be relatively reachable from base.
411  */
412 static QString makeRelativePath(const QString &base, const QString &path)
413 {
414   QString absBase = QFileInfo(base).absoluteFilePath();
415   QString absPath = QFileInfo(path).absoluteFilePath();
416 //   kDebug() << "absPath: \"" << absolutePath << "\"";
417 //   kDebug() << "absBase: \"" << absoluteBase << "\"";
418
419   // walk up to common ancestor directory
420   int pos = 0;
421   do {
422     pos++;
423     int newpos = absBase.indexOf('/', pos);
424     if (newpos == -1) newpos = absBase.length();
425     QString cmpPathComp = QString::fromRawData(absPath.unicode() + pos, newpos - pos);
426     QString cmpBaseComp = QString::fromRawData(absBase.unicode() + pos, newpos - pos);
427 //       kDebug() << "cmpPathComp: \"" << cmpPathComp << "\"";
428 //       kDebug() << "cmpBaseComp: \"" << cmpBaseComp << "\"";
429 //       kDebug() << "pos: " << pos << " newpos: " << newpos;
430     if (cmpPathComp != cmpBaseComp) { pos--; break; }
431     pos = newpos;
432   } while (pos < (int)absBase.length() && pos < (int)absPath.length());
433   int basepos = pos < (int)absBase.length() ? pos + 1 : pos;
434   int pathpos = pos < (int)absPath.length() ? pos + 1 : pos;
435
436 //   kDebug() << "basepos " << basepos << " pathpos " << pathpos;
437
438   QString rel;
439   {
440     QString relBase = QString::fromRawData(absBase.unicode() + basepos, absBase.length() - basepos);
441     QString relPath = QString::fromRawData(absPath.unicode() + pathpos, absPath.length() - pathpos);
442     // generate as many .. as there are path elements in relBase
443     if (relBase.length() > 0) {
444       for (int i = relBase.count('/'); i > 0; --i)
445         rel += "../";
446       rel += "..";
447       if (relPath.length() > 0) rel += '/';
448     }
449     rel += relPath;
450   }
451   return rel;
452 }
453
454 /**
455  * returns a unique file name
456  * (Cannot have QTemporaryFile as it won't return a file name without actually
457  * opening the file. Besides, it contains an indeterminate id which differs
458  * between processes.)
459  */
460 static QString getTempFileName(const QString &name)
461 {
462   return QDir::tempPath()+"/testkateregression-"+name;
463 }
464
465 /** writes an ipc-variable */
466 static void writeVariable(const QString &varName, const QString &content)
467 {
468   QString fn = getTempFileName(varName);
469   QFile::remove(fn);
470   QFile f(fn);
471   if (!f.open(QFile::WriteOnly))
472     return;   // FIXME be more elaborate
473   f.write(content.toLatin1());
474 //   fprintf(stderr, "writing: %s: %s\n", fn.toLatin1().constData(), content.toLatin1().constData());
475 }
476
477 /** reads an ipc-variable */
478 static QString readVariable(const QString &varName)
479 {
480   QString fn = getTempFileName(varName);
481   QFile f(fn);
482   if (!f.open(QFile::ReadOnly))
483     return QString();   // FIXME be more elaborate
484   QByteArray content = f.readAll();
485   f.close();
486   QFile::remove(fn);
487 //   fprintf(stderr, "reading: %s: %s\n", fn.toLatin1().constData(), content.constData());
488   return QLatin1String(content.constData());
489 }
490
491 void RegressionTest::doFailureReport( const QString& test, int failures )
492 {
493   if ( failures == NoFailure ) {
494     QFile::remove( m_outputDir + '/' + test + "-compare.html" );
495     return;
496   }
497
498   createLink( test, failures );
499
500   QFile compare( m_outputDir + '/' + test + "-compare.html" );
501
502   QString testFile = QFileInfo(test).fileName();
503
504   QString renderDiff;
505   QString domDiff;
506
507   QString pwd = QDir::currentPath();
508   QDir::setCurrent( m_baseDir );
509   QString resolvedBaseDir = QDir::currentPath();
510
511   QString relOutputDir = makeRelativePath(resolvedBaseDir/*m_baseDir*/, m_outputDir);
512
513   // are blocking reads possible with K3Process?
514
515   if ( failures & ResultFailure ) {
516     domDiff += "<pre>";
517     QProcess diff;
518     QStringList args;
519     args << "-u" << QString::fromLatin1("baseline/%1-result").arg(test)
520         << QString::fromLatin1("%3/%2-result").arg ( test, relOutputDir );
521     diff.start("diff", args);
522     diff.waitForFinished();
523     QByteArray out = diff.readAllStandardOutput();
524     QByteArray err = diff.readAllStandardError();
525     QTextStream *is = new QTextStream( out, QFile::ReadOnly );
526     for ( int line = 0; line < 100 && !is->atEnd(); ++line ) {
527       QString l = is->readLine();
528       l = l.replace( '<', "&lt;" );
529       l = l.replace( '>', "&gt;" );
530       l = l.replace( QRegExp("(\t+)"), "<span style=\"background:lightblue\">\\1</span>" );
531       domDiff += l  + '\n';
532     }
533     delete is;
534     domDiff += "</pre>";
535     if (!err.isEmpty()) {
536       qWarning() << "cwd: " << resolvedBaseDir << ", basedir " << m_baseDir;
537       qWarning() << "diff " << args.join(" ");
538       qWarning() << "Errors: " << err;
539     }
540   }
541
542   QDir::setCurrent( pwd );
543
544     // create a relative path so that it works via web as well. ugly
545   QString relpath = makeRelativePath(m_outputDir + '/'
546       + QFileInfo(test).dir().path(), resolvedBaseDir/*m_baseDir*/);
547
548   compare.open( QFile::WriteOnly|QFile::Truncate );
549   QString cl;
550   cl = QString( "<html><head><title>%1</title>" ).arg( test );
551   cl += QString( "<script>\n"
552       "var pics = new Array();\n"
553       "pics[0]=new Image();\n"
554       "pics[0].src = '%1';\n"
555       "pics[1]=new Image();\n"
556       "pics[1].src = '%2';\n"
557       "var doflicker = 1;\n"
558       "var t = 1;\n"
559       "var lastb=0;\n" )
560       .arg( relpath+"/baseline/"+test+"-dump.png" )
561       .arg( testFile+"-dump.png" );
562   cl += QString( "function toggleVisible(visible) {\n"
563                  "     document.getElementById('render').style.visibility= visible == 'render' ? 'visible' : 'hidden';\n"
564                  "     document.getElementById('image').style.visibility= visible == 'image' ? 'visible' : 'hidden';\n"
565                  "     document.getElementById('dom').style.visibility= visible == 'dom' ? 'visible' : 'hidden';\n"
566                  "}\n"
567                  "function show() { document.getElementById('image').src = pics[t].src; "
568                  "document.getElementById('image').style.borderColor = t && !doflicker ? 'red' : 'gray';\n"
569                  "toggleVisible('image');\n"
570                  "}" );
571   cl += QString ( "function runSlideShow(){\n"
572                   "   document.getElementById('image').src = pics[t].src;\n"
573                   "   if (doflicker)\n"
574                   "       t = 1 - t;\n"
575                   "   setTimeout('runSlideShow()', 200);\n"
576                   "}\n"
577                   "function m(b) { if (b == lastb) return; document.getElementById('b'+b).className='buttondown';\n"
578                   "                var e = document.getElementById('b'+lastb);\n"
579                   "                 if(e) e.className='button';\n"
580                   "                 lastb = b;\n"
581                   "}\n"
582                   "function showRender() { doflicker=0;toggleVisible('render')\n"
583                   "}\n"
584                   "function showDom() { doflicker=0;toggleVisible('dom')\n"
585                   "}\n"
586                   "</script>\n");
587
588   cl += QString ("<style>\n"
589                  ".buttondown { cursor: pointer; padding: 0px 20px; color: white; background-color: blue; border: inset blue 2px;}\n"
590                  ".button { cursor: pointer; padding: 0px 20px; color: black; background-color: white; border: outset blue 2px;}\n"
591                  ".diff { position: absolute; left: 10px; top: 100px; visibility: hidden; border: 1px black solid; background-color: white; color: black; /* width: 800; height: 600; overflow: scroll; */ }\n"
592                  "</style>\n" );
593
594   cl += QString( "<body onload=\"m(5); toggleVisible('dom');\"" );
595   cl += QString(" text=black bgcolor=gray>\n<h1>%3</h1>\n" ).arg( test );
596   if ( renderDiff.length() )
597     cl += "<span id='b4' class='button' onclick='showRender();m(4)'>R-DIFF</span>&nbsp;\n";
598   if ( domDiff.length() )
599     cl += "<span id='b5' class='button' onclick='showDom();m(5);'>D-DIFF</span>&nbsp;\n";
600   // The test file always exists - except for checkOutput called from *.js files
601   if ( QFile::exists( m_baseDir + "/tests/"+ test ) )
602     cl += QString( "<a class=button href=\"%1\">HTML</a>&nbsp;" )
603         .arg( relpath+"/tests/"+test );
604
605   cl += QString( "<hr>"
606                  "<img style='border: solid 5px gray' src=\"%1\" id='image'>" )
607     .arg( relpath+"/baseline/"+test+"-dump.png" );
608
609   cl += "<div id='render' class='diff'>" + renderDiff + "</div>";
610   cl += "<div id='dom' class='diff'>" + domDiff + "</div>";
611
612   cl += "</body></html>";
613   compare.write( cl.toLatin1(), cl.length() );
614   compare.close();
615 }
616
617 void RegressionTest::testStaticFile(const QString & filename, const QStringList &commands)
618 {
619   toplevel->resize( 800, 600); // restore size
620
621   // Set arguments
622   KParts::OpenUrlArguments args;
623   if (filename.endsWith(".txt")) args.setMimeType("text/plain");
624   m_part->setArguments(args);
625   // load page
626   KUrl url;
627   url.setProtocol("file");
628   url.setPath(QFileInfo(m_baseDir + "/tests/"+filename).absoluteFilePath());
629   m_part->openUrl(url);
630
631   // inject commands
632   for (QStringList::ConstIterator cit = commands.begin(); cit != commands.end(); ++cit) {
633     QString str = (*cit).trimmed();
634 //     kDebug() << "command: " << str;
635     if (str.isEmpty() || str.startsWith("#")) continue;
636     KTextEditor::Command *cmd = KateCmd::self()->queryCommand(str);
637     if (cmd) {
638       QString msg;
639       if (!cmd->exec(m_view, str, msg))
640         fprintf(stderr, "ERROR executing command '%s': %s\n", str.toLatin1().constData(), msg.toLatin1().constData());
641     }
642   }
643
644 //   pause(200);
645
646 //   Q_ASSERT(m_part->config()->configFlags() & KateDocumentConfig::cfDoxygenAutoTyping);
647
648   bool script_error = false;
649   pid_t pid = m_fork ? fork() : 0;
650   if (pid == 0) {
651     // Execute script
652     TestScriptEnv jsenv(m_part, m_outputCustomised);
653     jsenv.output()->setOutputFile( ( m_genOutput ? QString(m_baseDir + "/baseline/") : QString(m_outputDir + '/') ) + filename + "-result" );
654     script_error = evalJS(jsenv.engine(), m_baseDir + "/tests/"+QFileInfo(filename).dir().path()+"/.kateconfig-script", true)
655         && evalJS(jsenv.engine(), m_baseDir + "/tests/"+filename+"-script");
656
657     if (m_fork) {
658       writeVariable("script_error", QString::number(script_error));
659       writeVariable("m_errors", QString::number(m_errors));
660       writeVariable("m_outputCustomised", QString::number(m_outputCustomised));
661       writeVariable("m_part.text", m_part->text());
662       signal(SIGABRT, SIG_DFL);   // Dr. Konqi, no interference please
663       ::abort();  // crash, don't let Qt/KDE do any fancy deinit stuff
664     }
665   } else if (pid == -1) {
666     // whoops, will fail later on comparison
667     m_errors++;
668   } else {
669     // wait for child to finish
670     int status;
671     waitpid(pid, &status, 0);
672     // read in potentially changed variables
673     script_error = (bool)readVariable("script_error").toInt();
674     m_errors = readVariable("m_errors").toInt();
675     m_outputCustomised = (bool)readVariable("m_outputCustomised").toInt();
676     m_part->setText(readVariable("m_part.text"));
677 //     fprintf(stderr, "script_error = %d, m_errors = %d, m_outputCustomised = %d\n", script_error, m_errors, m_outputCustomised);
678   }
679
680   int back_known_failures = m_known_failures;
681
682   if (!script_error) goto bail_out;
683
684   kapp->processEvents();
685
686   if ( m_genOutput ) {
687     reportResult(checkOutput(filename+"-result"), "result");
688   } else {
689     int failures = NoFailure;
690
691     // compare with output file
692     if ( m_known_failures & ResultFailure)
693       m_known_failures = AllFailure;
694     bool newfail;
695     if ( !reportResult( checkOutput(filename+"-result"), "result", &newfail ) )
696       failures |= ResultFailure;
697     if (newfail)
698       failures |= NewFailure;
699
700     doFailureReport(filename, failures );
701   }
702
703 bail_out:
704   m_known_failures = back_known_failures;
705   m_part->setModified(false);
706   m_part->closeUrl();
707 }
708
709 bool RegressionTest::evalJS(QScriptEngine *engine, const QString &filename, bool ignore_nonexistent)
710 {
711   QFile sourceFile(filename);
712
713   if (!sourceFile.open(QFile::ReadOnly)) {
714     if (!ignore_nonexistent) {
715       fprintf(stderr,"ERROR reading file %s\n",filename.toLatin1().constData());
716       m_errors++;
717     }
718     return ignore_nonexistent;
719   }
720
721   QTextStream stream(&sourceFile);
722   stream.setCodec("UTF8");
723   QString code = stream.readAll();
724   sourceFile.close();
725
726   QScriptValue result = engine->evaluate(code, filename, 1);
727
728   if (result.isError()) {
729     fprintf(stderr, "eval script failed\n");
730     QString errmsg = result.toString();
731     printf( "ERROR: %s (%s)\n", filename.toLatin1().constData(), errmsg.toLatin1().constData());
732     m_errors++;
733     return false;
734   }
735   return true;
736 }
737
738 RegressionTest::CheckResult RegressionTest::checkOutput(const QString &againstFilename)
739 {
740   QString absFilename = QFileInfo(m_baseDir + "/baseline/" + againstFilename).absoluteFilePath();
741   if ( svnIgnored( absFilename ) ) {
742     m_known_failures = NoFailure;
743     return Ignored;
744   }
745
746   CheckResult result = Success;
747
748   // compare result to existing file
749   QString outputFilename = QFileInfo(m_outputDir + '/' + againstFilename).absoluteFilePath();
750   bool kf = false;
751   if ( m_known_failures & AllFailure )
752     kf = true;
753   if ( kf )
754     outputFilename += "-KF";
755
756   if ( m_genOutput )
757     outputFilename = absFilename;
758
759   // get existing content
760   QString data;
761   if (m_outputCustomised) {
762     QFile file2(outputFilename);
763     if (!file2.open(QFile::ReadOnly)) {
764       fprintf(stderr,"Error reading file %s\n",outputFilename.toLatin1().constData());
765       exit(1);
766     }
767     data = file2.readAll();
768   } else {
769     data = m_part->text();
770   }
771
772   QFile file(absFilename);
773   if (file.open(QFile::ReadOnly)) {
774     QTextStream stream ( &file );
775     stream.setCodec( "UTF8" );
776
777     QString fileData = stream.readAll();
778
779     result = ( fileData == data ) ? Success : Failure;
780     if ( !m_genOutput && result == Success && !m_keepOutput ) {
781         QFile::remove( outputFilename );
782         return Success;
783     }
784   } else if (!m_genOutput) {
785     fprintf(stderr, "Error reading file %s\n", absFilename.toLatin1().constData());
786     result = Failure;
787   }
788
789   // generate result file
790   OutputObject::createMissingDirs( outputFilename );
791   QFile file2(outputFilename);
792   if (!file2.open(QFile::WriteOnly)) {
793     fprintf(stderr,"Error writing to file %s\n",outputFilename.toLatin1().constData());
794     exit(1);
795   }
796
797   QTextStream stream2(&file2);
798   stream2.setCodec( "UTF8" );
799   stream2 << data;
800   if ( m_genOutput )
801     printf("Generated %s\n", outputFilename.toLatin1().constData());
802
803   return result;
804 }
805
806 void RegressionTest::rereadConfig()
807 {
808   KConfigGroup g = m_baseConfig->group("Kate Document Defaults");
809   m_part->config()->readConfig(g);
810   g = m_baseConfig->group("Kate View Defaults");
811   m_view->config()->readConfig(g);
812 }
813
814 bool RegressionTest::reportResult(CheckResult result, const QString & description, bool *newfail)
815 {
816   if ( result == Ignored ) {
817 //     printf("IGNORED: ");
818 //     printDescription( description );
819     return true; // no error
820   } else {
821     return reportResult( result == Success, description, newfail );
822   }
823 }
824
825 bool RegressionTest::reportResult(bool passed, const QString & description, bool *newfail)
826 {
827   if (newfail) *newfail = false;
828
829   if (m_genOutput)
830     return true;
831
832   QString filename(m_currentTest + '-' + description);
833   if (!m_currentCategory.isEmpty())
834     filename = m_currentCategory + '/' + filename;
835
836   const bool oldfailed = m_failureComp && m_failureComp->readEntry(filename, 0);
837   if (passed) {
838     if ( m_known_failures & AllFailure ) {
839       printf("PASS (unexpected!)");
840       m_passes_fail++;
841     } else {
842       printf("PASS");
843       m_passes_work++;
844     }
845     if (oldfailed) {
846       printf(" (new)");
847       m_passes_new++;
848     }
849     if (m_failureSave)
850       m_failureSave->deleteEntry(filename);
851   } else {
852     if ( m_known_failures & AllFailure ) {
853       printf("FAIL (known)");
854       m_failures_fail++;
855       passed = true; // we knew about it
856     } else {
857       printf("FAIL");
858       m_failures_work++;
859     }
860     if (!oldfailed && m_failureComp) {
861       printf(" (new)");
862       m_failures_new++;
863       if (newfail) *newfail = true;
864     }
865     if (m_failureSave)
866       m_failureSave->writeEntry(filename, 1);
867   }
868   printf(": ");
869
870   printDescription( description );
871   return passed;
872 }
873
874 void RegressionTest::printDescription(const QString& description)
875 {
876   if (!m_currentCategory.isEmpty())
877     printf("%s/", m_currentCategory.toLatin1().constData());
878
879   printf("%s", m_currentTest.toLatin1().constData());
880
881   if (!description.isEmpty()) {
882     QString desc = description;
883     desc.replace( '\n', ' ' );
884     printf(" [%s]", desc.toLatin1().constData());
885   }
886
887   printf("\n");
888   fflush(stdout);
889 }
890
891 void RegressionTest::printSummary()
892 {
893   QTextStream out(stdout, QIODevice::WriteOnly);
894
895   out << endl;
896   out << "Tests completed." << endl;
897   out << "Total:    " << m_passes_work + m_passes_fail +
898       m_failures_work + m_failures_fail + m_errors << endl;
899
900   //BEGIN Passes
901   out << "Passes:   " << m_passes_work;
902   if (m_passes_fail > 0)
903     out << QString(" (%1 unexpected passes)").arg(m_passes_fail);
904   if (m_passes_new > 0)
905     out << QString(" (%1 new since %2)").arg(m_passes_new).arg(m_failureComp->name());
906   out << endl;
907   //END Passes
908
909   //BEGIN Failures
910   out << "Failures: " << m_failures_work;
911   if (m_failures_fail > 0)
912     out << QString(" (%1 expected failures)").arg(m_failures_fail);
913   if (m_failures_new > 0)
914     out << QString(" (%d new since %s)").arg(m_failures_new).arg(m_failureComp->name());
915   out << endl;
916   //END Failures
917
918   if (m_errors > 0)
919     out << "Errors:   " << m_errors << endl;
920
921   //BEGIN html
922   QFile list(m_outputDir + "/links.html");
923   list.open(QFile::WriteOnly | QFile::Append);
924
925   QTextStream ts(&list);
926   ts << QString("<hr>%1 failures. (%2 expected failures)")
927       .arg(m_failures_work)
928       .arg(m_failures_fail);
929   if (m_failures_new > 0) {
930     ts << QString(" <span style=\"color:red;font-weight:bold\">(%1 new failures since %2)</span>")
931         .arg(m_failures_new)
932         .arg(m_failureComp->name());
933   }
934   if (m_passes_new > 0) {
935     ts << QString(" <p style=\"color:green;font-weight:bold\">%1 new passes since %2</p>")
936         .arg(m_passes_new)
937         .arg(m_failureComp->name());
938   }
939   list.close();
940   //END html
941 }
942
943 void RegressionTest::slotOpenURL(const KUrl &url, const KParts::OpenUrlArguments & args, const KParts::BrowserArguments&)
944 {
945   m_part->setArguments(args);
946   m_part->openUrl(url);
947 }
948
949 bool RegressionTest::svnIgnored( const QString &filename )
950 {
951   QFileInfo fi( filename );
952   QString ignoreFilename = fi.path() + "/svnignore";
953   QFile ignoreFile(ignoreFilename);
954   if (!ignoreFile.open(QFile::ReadOnly))
955     return false;
956
957   QTextStream ignoreStream(&ignoreFile);
958   QString line;
959   while (!(line = ignoreStream.readLine()).isNull()) {
960     if ( line == fi.fileName() )
961       return true;
962   }
963   ignoreFile.close();
964   return false;
965 }
966
967 void RegressionTest::resizeTopLevelWidget( int w, int h )
968 {
969   toplevel->resize( w, h );
970   // Since we're not visible, this doesn't have an immediate effect, QWidget posts the event
971   QApplication::sendPostedEvents( 0, QEvent::Resize );
972 }
973
974
975
976 KateTestApp::KateTestApp(KCmdLineArgs *args, const QString& baseDir, int testcaseIndex)
977   : KApplication()
978   , m_args(args)
979   , m_cfg("testkateregressionrc", KConfig::SimpleConfig)
980   , m_baseDir(baseDir)
981   , m_testcaseIndex(testcaseIndex)
982 {
983   // FIXME: Any analogous call for dbus?
984   //   a.disableAutoDcopRegistration();
985   setStyle("windows");
986   KConfigGroup group = m_cfg.group("Kate Document Defaults");
987   KateDocumentConfig config(group);
988   config.setBackspaceIndents(true);
989   config.setWordWrap(false);
990   config.setWrapCursor(true);
991   config.setKeepExtraSpaces(true);
992   config.setShowTabs(true);
993   config.setSmartHome(true);
994   config.setTabIndents(true);
995   config.setIndentPastedText(true);
996
997   config.setAutoBrackets(false);
998   config.setShowSpaces(false);
999   config.setReplaceTabsDyn(false);
1000   config.setRemoveTrailingDyn(false);
1001   config.setRemoveSpaces(false);
1002   config.setOvr(false);
1003
1004   m_cfg.sync();
1005   config.writeConfig(group);
1006
1007 //   {
1008 //     KConfig dc( "kdebugrc", KConfig::SimpleConfig );
1009 //     // FIXME adapt to kate
1010 //     static int areas[] = { 1000, 13000, 13001, 13002, 13010,
1011 //     13020, 13025, 13030, 13033, 13035,
1012 //     13040, 13050, 13051, 13070, 7000, 7006, 170,
1013 //     171, 7101, 7002, 7019, 7027, 7014,
1014 //     7001, 7011, 6070, 6080, 6090, 0};
1015 //     int channel = args->isSet( "debug" ) ? 2 : 4;
1016 //     for ( int i = 0; areas[i]; ++i ) {
1017 //       KConfigGroup group = dc.group( QString::number( areas[i] ) );
1018 //       group.writeEntry( "InfoOutput", channel );
1019 //     }
1020 //     dc.sync();
1021 //
1022 //     kClearDebugConfig();
1023 //   }
1024
1025   // create widgets
1026   toplevel = new KMainWindow();
1027   m_document = new KateDocument(true, false, false, toplevel);
1028   m_document->setObjectName("testkate");
1029
1030   toplevel->setCentralWidget( m_document->widget() );
1031
1032   if (args->isSet("show"))
1033     toplevel->show();
1034
1035   // we're not interested
1036   toplevel->statusBar()->hide();
1037
1038   if (false && qgetenv("KDE_DEBUG").isEmpty()) {
1039     // set ulimits
1040     rlimit vmem_limit = { 256*1024*1024, RLIM_INFINITY };   // 256Mb Memory should suffice
1041 #if defined(RLIMIT_AS)
1042     setrlimit(RLIMIT_AS, &vmem_limit);
1043 #endif
1044 #if defined(RLIMIT_DATA)
1045     setrlimit(RLIMIT_DATA, &vmem_limit);
1046 #endif
1047     rlimit stack_limit = { 8*1024*1024, RLIM_INFINITY };    // 8Mb Memory should suffice
1048     setrlimit(RLIMIT_STACK, &stack_limit);
1049   }
1050
1051   // run the tests
1052   m_regressionTest = new RegressionTest(m_document, &m_cfg, baseDir, args);
1053
1054   {
1055     QString failureSnapshot = args->getOption("cmp-failures");
1056     if (failureSnapshot.isEmpty())
1057       failureSnapshot = findMostRecentFailureSnapshot();
1058     if (!failureSnapshot.isEmpty())
1059       m_regressionTest->setFailureSnapshotConfig(new KConfig(failureSnapshotPrefix + failureSnapshot,
1060                                                              KConfig::SimpleConfig),
1061                                                  failureSnapshot);
1062   }
1063
1064   if (args->isSet("save-failures")) {
1065     QString failureSaver = args->getOption("save-failures");
1066     m_regressionTest->setFailureSnapshotSaver(new KConfig(failureSnapshotPrefix + failureSaver,
1067                                                           KConfig::SimpleConfig),
1068                                               failureSaver);
1069   }
1070
1071   QTimer::singleShot(0, this, SLOT(runTests()));
1072 }
1073
1074 KateTestApp::~KateTestApp()
1075 {
1076   delete m_document;
1077   m_document = 0;
1078
1079   Q_ASSERT(m_regressionTest == 0);
1080 }
1081
1082 bool KateTestApp::allTestsSucceeded()
1083 {
1084   return m_regressionTest->allTestsSucceeded();
1085 }
1086
1087 void KateTestApp::runTests()
1088 {
1089   bool result = false;
1090   QStringList tests = m_args->getOptionList("test");
1091   // merge testcases specified on command line
1092   for (; m_testcaseIndex < m_args->count(); m_testcaseIndex++)
1093     tests << m_args->arg(m_testcaseIndex);
1094   if (tests.count() > 0) {
1095     foreach (const QString &test, tests) {
1096       result = m_regressionTest->runTests(test, true);
1097       if (!result) break;
1098     }
1099   } else {
1100     result = m_regressionTest->runTests();
1101   }
1102
1103   if (result) {
1104     if (m_args->isSet("genoutput")) {
1105       printf("\nOutput generation completed.\n");
1106     } else {
1107       m_regressionTest->printSummary();
1108     }
1109   }
1110 }
1111
1112 #include "test_regression.moc"
1113 // kate: space-indent on; indent-width 2; replace-tabs on;