Warning: That file was not part of the compilation database. It may have many parsing errors.

1/**
2 * This file is part of the KDE project
3 *
4 * Copyright (C) 2006 Nikolas Zimmermann <zimmermann@kde.org>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
15 *
16 * You should have received a copy of the GNU Library General Public License
17 * along with this library; see the file COPYING.LIB. If not, write to
18 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
20 *
21 */
22
23#include <assert.h>
24#include <signal.h>
25
26#include <QtCore/QFile>
27#include <QtCore/QTimer>
28#include <QtCore/QProcess>
29#include <QtCore/QFileInfo>
30#include <QtCore/QTextStream>
31#include <QtGui/QMainWindow>
32
33#include <kiconloader.h>
34#include <kmessagebox.h>
35#include <kconfig.h>
36#include <kfiledialog.h>
37
38#include "test_regression_gui_window.moc"
39
40// Taken from QUrl
41#define Q_HAS_FLAG(a, b) ( ((a) & (b)) == (b) )
42#define Q_SET_FLAG(a, b) { (a) |= (b); }
43#define Q_UNSET_FLAG(a, b) { (a) &= ~(b); }
44
45TestRegressionWindow::TestRegressionWindow(QWidget *parent)
46: QMainWindow(parent), m_flags(None), m_runCounter(0), m_testCounter(0), m_totalTests(0),
47 m_totalTestsJS(0), m_totalTestsDOMTS(0), m_lastResult(Unknown),
48 m_browserPart(0), m_activeProcess(0), m_activeTreeItem(0),
49 m_suspended(false), m_justProcessingQueue(false)
50{
51 m_ui.setupUi(this);
52
53 // Setup actions/connections
54 connect(m_ui.actionOnly_run_JS_tests, SIGNAL(toggled(bool)), SLOT(toggleJSTests(bool)));
55 connect(m_ui.actionOnly_run_HTML_tests, SIGNAL(toggled(bool)), SLOT(toggleHTMLTests(bool)));
56 connect(m_ui.actionDo_not_suppress_debug_output, SIGNAL(toggled(bool)), SLOT(toggleDebugOutput(bool)));
57 connect(m_ui.actionDo_not_use_Xvfb, SIGNAL(toggled(bool)), SLOT(toggleNoXvfbUse(bool)));
58 connect(m_ui.actionSpecify_tests_directory, SIGNAL(triggered(bool)), SLOT(setTestsDirectory()));
59 connect(m_ui.actionSpecify_khtml_directory, SIGNAL(triggered(bool)), SLOT(setKHTMLDirectory()));
60 connect(m_ui.actionSpecify_output_directory, SIGNAL(triggered(bool)), SLOT(setOutputDirectory()));
61 connect(m_ui.actionRun_tests, SIGNAL(triggered(bool)), SLOT(runTests()));
62
63 connect(m_ui.pauseContinueButton, SIGNAL(clicked(bool)), SLOT(pauseContinueButtonClicked()));
64 connect(m_ui.saveLogButton, SIGNAL(clicked(bool)), SLOT(saveLogButtonClicked()));
65
66 connect(m_ui.treeWidget, SIGNAL(customContextMenuRequested(QPoint)),
67 this, SLOT(treeWidgetContextMenuRequested(QPoint)));
68
69 // Setup actions' default state
70 m_ui.progressBar->setValue(0);
71 m_ui.textEdit->setReadOnly(true);
72 m_ui.actionRun_tests->setEnabled(false);
73 m_ui.pauseContinueButton->setEnabled(false);
74
75 m_ui.treeWidget->headerItem()->setTextAlignment(0, Qt::AlignLeft);
76 m_ui.treeWidget->headerItem()->setText(0, i18n("Available Tests: 0"));
77
78 // Load default values for tests directory/khtml directory...
79 KConfig config("testregressiongui", KConfig::SimpleConfig);
80 KConfigGroup grp = config.group("<default>");
81
82 m_testsUrl = KUrl::fromPath(grp.readPathEntry("TestsDirectory", QString()));
83 m_khtmlUrl = KUrl::fromPath(grp.readPathEntry("KHTMLDirectory", QString()));
84
85 initTestsDirectory();
86
87 // Init early visible items in the text edit...
88 initLegend();
89 initOutputBrowser();
90}
91
92TestRegressionWindow::~TestRegressionWindow()
93{
94 if(m_activeProcess)
95 {
96 m_activeProcess->kill();
97
98 /* This leads to:
99 * QProcess object destroyed while process is still running.
100 * Any idea why??
101 delete m_activeProcess;
102 */
103
104 m_activeProcess = 0;
105 }
106}
107
108void TestRegressionWindow::toggleJSTests(bool checked)
109{
110 if(checked)
111 {
112 Q_SET_FLAG(m_flags, JSTests)
113 Q_UNSET_FLAG(m_flags, HTMLTests)
114
115 m_ui.actionOnly_run_HTML_tests->setChecked(false);
116 }
117 else
118 Q_UNSET_FLAG(m_flags, JSTests)
119
120 // Eventually update progress bar range...
121 updateProgressBarRange();
122}
123
124void TestRegressionWindow::toggleHTMLTests(bool checked)
125{
126 if(checked)
127 {
128 Q_SET_FLAG(m_flags, HTMLTests)
129 Q_UNSET_FLAG(m_flags, JSTests)
130
131 m_ui.actionOnly_run_JS_tests->setChecked(false);
132 }
133 else
134 Q_UNSET_FLAG(m_flags, HTMLTests)
135
136 // Eventually update progress bar range...
137 updateProgressBarRange();
138}
139
140void TestRegressionWindow::toggleDebugOutput(bool checked)
141{
142 if(checked)
143 Q_SET_FLAG(m_flags, DebugOutput)
144 else
145 Q_UNSET_FLAG(m_flags, DebugOutput)
146}
147
148void TestRegressionWindow::toggleNoXvfbUse(bool checked)
149{
150 if(checked)
151 Q_SET_FLAG(m_flags, NoXvfbUse)
152 else
153 Q_UNSET_FLAG(m_flags, NoXvfbUse)
154}
155
156void TestRegressionWindow::setTestsDirectory()
157{
158 m_testsUrl = KFileDialog::getExistingDirectory();
159
160 initTestsDirectory();
161 loadOutputHTML();
162}
163
164void TestRegressionWindow::setOutputDirectory()
165{
166 m_outputUrl = KFileDialog::getExistingDirectory();
167 loadOutputHTML();
168}
169
170void TestRegressionWindow::initTestsDirectory()
171{
172 bool okay = !m_testsUrl.isEmpty();
173 if(okay)
174 {
175 const char *subdirs[] = { "tests", "baseline", "output", "resources" };
176 for(int i = 0; i <= 3; i++)
177 {
178 QFileInfo sourceDir(m_testsUrl.path() + "/" + subdirs[i]); //krazy:exclude=duoblequote_chars DOM demands chars
179 if(!sourceDir.exists() || !sourceDir.isDir())
180 {
181 KMessageBox::error(0, i18n("Please choose a valid 'khtmltests/regression/' directory."));
182
183 okay = false;
184 m_testsUrl = KUrl();
185 break;
186 }
187 }
188 }
189
190 if(okay)
191 {
192 // Clean up...
193 m_itemMap.clear();
194 m_ignoreMap.clear();
195 m_failureMap.clear();
196 m_directoryMap.clear();
197
198 m_ui.treeWidget->clear();
199
200 if(!m_khtmlUrl.isEmpty())
201 m_ui.actionRun_tests->setEnabled(true);
202
203 // Initialize map (to prevent assert below)...
204 m_directoryMap.insert(QString(), QStringList());
205
206 // Setup root tree widget item...
207 (void) new QTreeWidgetItem(m_ui.treeWidget, QStringList(m_testsUrl.path() + "/tests"));
208
209 // Check for ignore & failure file in root directory...
210 QString ignoreFile = m_testsUrl.path() + "/tests/ignore";
211 QString failureFile = m_testsUrl.path() + "/tests/KNOWN_FAILURES";
212
213 QStringList ignoreFileList = readListFile(ignoreFile);
214 QStringList failureFileList = readListFile(failureFile);
215
216 if(!ignoreFileList.isEmpty())
217 m_ignoreMap.insert(QString(), ignoreFileList);
218
219 if(!failureFileList.isEmpty())
220 m_failureMap.insert(QString(), failureFileList);
221
222 // Remember directory...
223 KConfig config("testregressiongui", KConfig::SimpleConfig);
224 KConfigGroup grp = config.group("<default>");
225 grp.writePathEntry("TestsDirectory", m_testsUrl.path());
226
227 // Start listing directory...
228 KUrl listUrl = m_testsUrl; listUrl.addPath("tests");
229 KIO::ListJob *job = KIO::listRecursive(listUrl, KIO::HideProgressInfo, false /* no hidden files */);
230
231 connect(job, SIGNAL(result(KJob*)), SLOT(directoryListingFinished(KJob*)));
232
233 connect(job, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)),
234 this, SLOT(directoryListingResult(KIO::Job*,KIO::UDSEntryList)));
235 }
236}
237
238void TestRegressionWindow::setKHTMLDirectory()
239{
240 m_khtmlUrl = KFileDialog::getExistingDirectory();
241
242 if(!m_khtmlUrl.isEmpty())
243 {
244 const char *subdirs[] = { "css", "dom", "xml", "html" }; // That's enough ;-)
245 for(int i = 0; i <= 3; i++)
246 {
247 QFileInfo sourceDir(m_khtmlUrl.path() + "/" + subdirs[i]); //krazy:exclude=duoblequote_chars DOM demands chars
248 if(!sourceDir.exists() || !sourceDir.isDir())
249 {
250 KMessageBox::error(0, i18n("Please choose a valid 'khtml/' build directory."));
251
252 m_khtmlUrl = KUrl();
253 break;
254 }
255 }
256
257 // Remember directory...
258 KConfig config("testregressiongui", KConfig::SimpleConfig);
259 KConfigGroup grp = config.group("<default>");
260 grp.writePathEntry("KHTMLDirectory", m_khtmlUrl.path());
261
262 if(!m_testsUrl.isEmpty() && !m_khtmlUrl.isEmpty())
263 m_ui.actionRun_tests->setEnabled(true);
264 }
265}
266
267void TestRegressionWindow::directoryListingResult(KIO::Job *, const KIO::UDSEntryList &list)
268{
269 KIO::UDSEntryList::ConstIterator it = list.constBegin();
270 const KIO::UDSEntryList::ConstIterator end = list.constEnd();
271
272 for(; it != end; ++it)
273 {
274 const KIO::UDSEntry &entry = *it;
275
276 QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
277 if(entry.isDir()) // Create new map entry...
278 {
279 assert(m_directoryMap.constFind(name) == m_directoryMap.constEnd());
280 m_directoryMap.insert(name, QStringList());
281
282 QString ignoreFile = m_testsUrl.path() + "/tests/" + name + "/ignore";
283 QString failureFile = m_testsUrl.path() + "/tests/" + name + "/KNOWN_FAILURES";
284
285 QStringList ignoreFileList = readListFile(ignoreFile);
286 QStringList failureFileList = readListFile(failureFile);
287
288 if(!ignoreFileList.isEmpty())
289 m_ignoreMap.insert(name, ignoreFileList);
290
291 if(!failureFileList.isEmpty())
292 m_failureMap.insert(name, failureFileList);
293 }
294 else if(name.endsWith(".html") || name.endsWith(".htm") ||
295 name.endsWith(".xhtml") || name.endsWith(".xml") || name.endsWith(".js"))
296 {
297 int lastSlashPos = name.lastIndexOf('/');
298
299 QString cachedDirectory = (lastSlashPos > 0 ? name.mid(0, lastSlashPos) : QString());
300 QString cachedFilename = name.mid(lastSlashPos + 1);
301
302 assert(m_directoryMap.constFind(cachedDirectory) != m_directoryMap.constEnd());
303 m_directoryMap[cachedDirectory].append(cachedFilename);
304 }
305 }
306}
307
308void TestRegressionWindow::directoryListingFinished(KJob *)
309{
310 QTreeWidgetItem *topLevelItem = m_ui.treeWidget->topLevelItem(0);
311
312 // Gather a lot of statistics...
313 unsigned long availableDomFiles = 0;
314 unsigned long availableDumpFiles = 0;
315 unsigned long availableRenderFiles = 0;
316
317 unsigned long ignoredJSTests = 0;
318 unsigned long availableJSTests = 0;
319
320 unsigned long ignoredXMLTests = 0;
321 unsigned long availableXMLTests = 0;
322
323 unsigned long ignoredHTMLTests = 0;
324 unsigned long availableHTMLTests = 0;
325
326 unsigned long ignoredDOMTSTests = 0;
327 unsigned long availableDOMTSTests = 0;
328
329 // Start the actual data processing...
330 QMap<QString, QStringList>::const_iterator it = m_directoryMap.constBegin();
331 const QMap<QString, QStringList>::const_iterator end = m_directoryMap.constEnd();
332
333 for(; it != end; ++it)
334 {
335 QString directory = it.key();
336 QStringList filenames = it.value();
337
338 if(filenames.isEmpty()) // Do not add empty directories at all...
339 continue;
340
341 bool hasIgnores = (m_ignoreMap.constFind(directory) != m_directoryMap.constEnd());
342 bool hasFailures = (m_failureMap.constFind(directory) != m_failureMap.constEnd());
343
344 // Extract parent directory...
345 int position = directory.lastIndexOf('/');
346
347 QString parentDirectory = directory.mid(0, (position == -1 ? 0 : position));
348 QString parentDirectoryItem = directory.mid(position + 1);
349
350 bool hasParentIgnores = (m_ignoreMap.constFind(parentDirectory) != m_directoryMap.constEnd());
351 bool hasParentFailures = (m_failureMap.constFind(parentDirectory) != m_failureMap.constEnd());
352
353 // Sort in ascending order...
354 filenames.sort();
355
356 QStringList::const_iterator it2 = filenames.constBegin();
357 const QStringList::const_iterator end2 = filenames.constEnd();
358
359 // Create new tree widget item for the active directory...
360 QTreeWidgetItem *parent = topLevelItem;
361
362 if(!directory.isEmpty())
363 {
364 parent = new QTreeWidgetItem(topLevelItem, QStringList(directory));
365
366 // Directory is completely ignored, mark it 'yellow'...
367 if(hasParentIgnores && m_ignoreMap[parentDirectory].contains(parentDirectoryItem))
368 parent->setIcon(0, m_ignorePixmap);
369
370 // Directory is completely known to fail, mark it 'red'...
371 if(hasParentFailures && m_failureMap[parentDirectory].contains(parentDirectoryItem))
372 parent->setIcon(0, m_failKnownPixmap);
373 }
374
375 // Add all contained files as new items below 'parent'...
376 for(; it2 != end2; ++it2)
377 {
378 QString test = (*it2);
379 QString cacheName = directory + "/" + test; //krazy:exclude=duoblequote_chars DOM demands chars
380
381 QTreeWidgetItem *testItem = new QTreeWidgetItem(parent, QStringList(KUrl(test).path()));
382
383 // Remember name <-> item pair...
384 assert(m_itemMap.contains(cacheName));
385 m_itemMap.insert(cacheName, testItem);
386
387 bool ignore = (hasIgnores && m_ignoreMap[directory].contains(test));
388 bool ignoreParent = (hasParentIgnores && m_ignoreMap[parentDirectory].contains(parentDirectoryItem));
389
390 bool failure = (hasFailures && m_failureMap[directory].contains(test));
391
392 // Check baseline directory for this test...
393 QString baseLinePath = m_testsUrl.path() + "/baseline/" + cacheName;
394
395 bool dom[9], render[9];
396 for(unsigned int i = 0; i < 9; ++i)
397 {
398 if(i == 0)
399 {
400 dom[i] = (QFileInfo(baseLinePath + "-dom").exists());
401 render[i] = (QFileInfo(baseLinePath + "-render").exists());
402 }
403 else
404 {
405 dom[i] = (QFileInfo(baseLinePath + "-" + QString::number(i) + "-dom").exists()); //krazy:exclude=duoblequote_chars DOM demands chars
406 render[i] = (QFileInfo(baseLinePath + "-" + QString::number(i) + "-render").exists()); //krazy:exclude=duoblequote_chars DOM demands chars
407 }
408 }
409
410 bool dump = (QFileInfo(baseLinePath + "-dump.png").exists());
411
412 // Ignored tests are marked 'yellow'...
413 if(ignore)
414 testItem->setIcon(0, m_ignorePixmap);
415
416 // Tests, known to fail, are marked 'red'...
417 if(failure)
418 testItem->setIcon(0, m_failKnownPixmap);
419
420 // Detect whether the tests has no corresponding baseline items...
421 if(!ignore && !failure)
422 {
423 if(!dom[0] && !dump && !render && !cacheName.endsWith(".js") && !cacheName.startsWith("domts"))
424 {
425 // See if parent directory is completely ignored...
426 if(!ignoreParent)
427 testItem->setIcon(0, m_noBaselinePixmap);
428 }
429 }
430
431 // Update statistics...
432 if(dump)
433 availableDumpFiles++;
434
435 for(unsigned i = 0; i < 9; ++i)
436 {
437 if(dom[i])
438 availableDomFiles++;
439
440 if(render[i])
441 availableRenderFiles++;
442 }
443
444 // Count DOM Testsuite files separated... (these have no baseline items!)
445 if(cacheName.startsWith("domts"))
446 {
447 // See if parent directory is completely ignored...
448 if(ignore || ignoreParent)
449 ignoredDOMTSTests++;
450 else
451 availableDOMTSTests++;
452 }
453
454 if(cacheName.endsWith(".html") || cacheName.endsWith(".htm") || cacheName.endsWith(".xhtml"))
455 {
456 if(ignore || ignoreParent)
457 ignoredHTMLTests++;
458 else
459 availableHTMLTests++;
460 }
461 else if(cacheName.endsWith(".xml"))
462 {
463 if(ignore || ignoreParent)
464 ignoredXMLTests++;
465 else
466 availableXMLTests++;
467 }
468 else if(cacheName.endsWith(".js"))
469 {
470 unsigned long containedTests = 0;
471
472 // Try hard to _ESTIMATE_ the number of tests...
473 // I really meant estimate, no way to calculate it perfectly.
474
475 QString jsFilePath = m_testsUrl.path() + "/tests/" + cacheName;
476 assert(QFileInfo(jsFilePath).exists() == true);
477
478 QStringList fileList = readListFile(jsFilePath);
479 QString fileContent = fileList.join("");
480
481 // #1 -> Check js file for the 'reportResult' calls...
482 containedTests = fileContent.count("reportResult");
483
484 // #2 -> Check js file for 'openPage' calls...
485 containedTests += fileContent.count("openPage");
486
487 // #3 -> Check js file for 'checkOutput' calls...
488 containedTests += fileContent.count("checkOutput");
489
490 // #4 -> Fallback for ie. mozilla/ecma files...
491 if(containedTests == 0) // Doesn't use 'reportResult' scheme...
492 containedTests++;
493
494 if(ignore || ignoreParent)
495 ignoredJSTests += containedTests;
496 else
497 availableJSTests += containedTests;
498 }
499 }
500 }
501
502 // Now we can calculate all ignored/available tests...
503 unsigned long ignoredTests = ignoredJSTests + ignoredXMLTests + ignoredHTMLTests;
504 unsigned long availableTests = availableJSTests + availableXMLTests + availableHTMLTests;
505
506 // This estimates the number of total tests, depending on the mode...
507 m_totalTests = availableDomFiles + availableDumpFiles + availableRenderFiles +
508 availableDOMTSTests + availableJSTests;
509
510 m_totalTestsJS = availableJSTests;
511 m_totalTestsDOMTS = availableDOMTSTests;
512
513 // Update progress bar range...
514 updateProgressBarRange();
515
516 QString statistics = QString("<body><table border='0' align='center' cellspacing='15'>") +
517 QString("<tr valign='top'><td colspan='3'><center><b>Statistics</b></center></td></tr>") +
518 QString("<tr valign='middle'><td>JS Tests</td><td>" + QString::number(availableJSTests) + "</td><td>(" + QString::number(ignoredJSTests) + " ignored)</td></tr>") +
519 QString("<tr valign='middle'><td>XML Tests</td><td>" + QString::number(availableXMLTests) + "</td><td>(" + QString::number(ignoredXMLTests) + " ignored)</td></tr>") +
520 QString("<tr valign='middle'><td>HTML Tests</td><td>" + QString::number(availableHTMLTests) + "</td><td>(" + QString::number(ignoredHTMLTests) + " ignored)</td></tr>") +
521 QString("</table></body>");
522
523 // Go to end...
524 QTextCursor cursor = m_ui.textEdit->textCursor();
525 cursor.movePosition(QTextCursor::End);
526 m_ui.textEdit->setTextCursor(cursor);
527
528 // Insert statistics...
529 m_ui.textEdit->insertHtml(statistics);
530
531 // Update treeview...
532 m_ui.treeWidget->headerItem()->setText(0, i18n("Available Tests: %1 (ignored: %2)", availableTests, ignoredTests));
533}
534
535void TestRegressionWindow::updateProgressBarRange() const
536{
537 if(m_totalTests != 0 && m_totalTestsJS != 0)
538 {
539 unsigned long totalTests = m_totalTests;
540
541 if(Q_HAS_FLAG(m_flags, JSTests))
542 totalTests = m_totalTestsJS;
543 else if(Q_HAS_FLAG(m_flags, HTMLTests))
544 {
545 totalTests -= m_totalTestsJS;
546 totalTests -= m_totalTestsDOMTS;
547 }
548
549 m_ui.progressBar->setRange(0, totalTests);
550 }
551}
552
553void TestRegressionWindow::pauseContinueButtonClicked()
554{
555 assert(m_activeProcess != 0);
556
557 if(!m_suspended)
558 {
559 // Suspend process
560 kill(m_activeProcess->pid(), SIGSTOP);
561
562 m_suspended = true;
563 m_ui.pauseContinueButton->setText(i18n("Continue"));
564 }
565 else
566 {
567 // Continue process
568 kill(m_activeProcess->pid(), SIGCONT);
569
570 m_suspended = false;
571 m_ui.pauseContinueButton->setText(i18n("Pause"));
572 }
573}
574
575void TestRegressionWindow::saveLogButtonClicked()
576{
577 assert(m_activeProcess == 0);
578 m_saveLogUrl = KFileDialog::getExistingDirectory();
579
580 QString fileName = m_saveLogUrl.path() + "/logOutput.html";
581 if(QFileInfo(fileName).exists())
582 {
583 // Remove file if already existent...
584 QFile file(fileName);
585 if(!file.remove())
586 {
587 kError() << " Can't remove " << fileName << endl;
588 exit(1);
589 }
590 }
591}
592
593void TestRegressionWindow::runTests()
594{
595 // Run in all-in-one mode...
596 m_runCounter = 0;
597 m_testCounter = 0;
598
599 initRegressionTesting(QString());
600}
601
602void TestRegressionWindow::runSingleTest()
603{
604 assert(m_activeTreeItem != 0);
605
606 QString testFileName = pathFromItem(m_activeTreeItem);
607
608 // Run in single-test mode...
609 m_runCounter = 0;
610 m_testCounter = -1;
611
612 initRegressionTesting(testFileName);
613}
614
615void TestRegressionWindow::initRegressionTesting(const QString &testFileName)
616{
617 assert(m_activeProcess == 0);
618
619 m_activeProcess = new QProcess();
620 m_activeProcess->setReadChannelMode(QProcess::MergedChannels);
621
622 QStringList environment = QProcess::systemEnvironment();
623 environment << "KDE_DEBUG=false"; // No Dr. Konqi please!
624
625 QString program = m_khtmlUrl.path() + "/.libs/testregression";
626 QString program2 = m_khtmlUrl.path() + "/testregression"; // with CMake, it's in $buildir/bin
627
628 if(!QFileInfo(program).exists())
629 {
630 if(!QFileInfo(program2).exists())
631 {
632 KMessageBox::error(0, i18n("Cannot find testregression executable."));
633 return;
634 }
635 else
636 {
637 program = program2;
638 }
639 }
640
641 QStringList arguments;
642 arguments << "--base" << m_testsUrl.path();
643
644 if(!m_outputUrl.isEmpty())
645 arguments << "--output" << m_outputUrl.path();
646 if(!testFileName.isEmpty())
647 arguments << "--test" << testFileName;
648
649 if(Q_HAS_FLAG(m_flags, JSTests))
650 arguments << "--js";
651 if(Q_HAS_FLAG(m_flags, HTMLTests))
652 arguments << "--html";
653 if(Q_HAS_FLAG(m_flags, DebugOutput))
654 arguments << "--debug";
655 if(Q_HAS_FLAG(m_flags, NoXvfbUse))
656 arguments << "--noxvfb";
657
658 connect(m_activeProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(testerExited(int,QProcess::ExitStatus)));
659 connect(m_activeProcess, SIGNAL(readyReadStandardOutput()), SLOT(testerReceivedData()));
660
661 // Clear processing queue before starting...
662 m_processingQueue.clear();
663
664 // Clean up gui...
665 m_ui.textEdit->clear();
666 m_ui.progressBar->reset();
667 m_ui.saveLogButton->setEnabled(false);
668 m_ui.actionRun_tests->setEnabled(false);
669 m_ui.pauseContinueButton->setEnabled(true);
670
671 // Start regression testing process...
672 m_activeProcess->setEnvironment(environment);
673 m_activeProcess->start(program, arguments, QIODevice::ReadOnly);
674}
675
676void TestRegressionWindow::initOutputBrowser()
677{
678 assert(m_browserPart == 0);
679 m_browserPart = new KHTMLPart(m_ui.secondTab, m_ui.secondTab, KHTMLPart::BrowserViewGUI);
680
681 // Setup vertical layout for the browser widget...
682 QVBoxLayout *layout = new QVBoxLayout();
683 layout->addWidget(m_browserPart->widget());
684 m_ui.secondTab->setLayout(layout);
685
686 m_browserPart->setJavaEnabled(true);
687 m_browserPart->setJScriptEnabled(true);
688 m_browserPart->setPluginsEnabled(true);
689 m_browserPart->setURLCursor(QCursor(Qt::PointingHandCursor));
690
691 m_browserPart->widget()->show();
692
693 // Check if there is already an output/index.html present...
694 loadOutputHTML();
695}
696
697void TestRegressionWindow::loadOutputHTML() const
698{
699 if(m_testsUrl.isEmpty())
700 return;
701
702 QString fileName = m_testsUrl.path() + "/output/index.html";
703 if(!m_outputUrl.isEmpty())
704 fileName = m_outputUrl.path() + "/index.html";
705
706 QFileInfo indexHtml(fileName);
707 if(indexHtml.exists())
708 {
709 m_browserPart->openUrl(KUrl::fromPath(fileName));
710 m_ui.tabWidget->setTabEnabled(1, true);
711 }
712 else
713 m_ui.tabWidget->setTabEnabled(1, false);
714}
715
716void TestRegressionWindow::updateItemStatus(TestResult result, QTreeWidgetItem *item, const QString &testFileName)
717{
718 if(!item)
719 return;
720
721 // Ensure item is visible...
722 QTreeWidgetItem *parent = item;
723 while(parent != 0)
724 {
725 m_ui.treeWidget->setItemExpanded(parent, true);
726 parent = parent->parent();
727 }
728
729 m_ui.treeWidget->scrollToItem(item);
730
731 bool updateIcon = true;
732 if(m_lastName == testFileName && !m_lastName.isEmpty())
733 {
734 if(m_lastResult == result)
735 updateIcon = false;
736 else if((m_lastResult == Pass || m_lastResult == PassUnexpected) &&
737 (result == Fail || result == FailKnown || result == Crash))
738 {
739 // If one part of the test (render/dom/paint) passed,
740 // and the current part fails, update to 'failed' icon...
741 updateIcon = true;
742 }
743 else if((m_lastResult == Fail || m_lastResult == FailKnown || m_lastResult == Crash) &&
744 (result == Pass || result == PassUnexpected))
745 {
746 // If one part of the test (render/dom/paint) failed,
747 // and the current part passes, don't update to 'passed' icon...
748 updateIcon = false;
749 }
750 }
751
752 // Update icon, if necessary...
753 if(updateIcon)
754 {
755 if(result == Crash)
756 item->setIcon(0, m_crashPixmap);
757 else if(result == Fail)
758 item->setIcon(0, m_failPixmap);
759 else if(result == FailKnown)
760 item->setIcon(0, m_failKnownPixmap);
761 else if(result == Pass)
762 item->setIcon(0, m_passPixmap);
763 else if(result == PassUnexpected)
764 item->setIcon(0, m_passUnexpectedPixmap);
765 else // Unhandled state...
766 assert(false);
767 }
768
769 // Remember test & result...
770 m_lastResult = result;
771 m_lastName = testFileName;
772 m_activeTreeItem = item;
773}
774
775void TestRegressionWindow::initLegend()
776{
777 // Init pixmaps...
778 m_failPixmap = QPixmap(":/test/pics/fail.xpm");
779 m_failKnownPixmap = QPixmap(":/test/pics/failKnown.xpm");
780 m_passPixmap = QPixmap(":/test/pics/pass.xpm");
781 m_passUnexpectedPixmap = QPixmap(":/test/pics/passUnexpected.xpm");
782 m_ignorePixmap = QPixmap(":/test/pics/ignore.xpm");
783 m_crashPixmap = QPixmap(":/test/pics/crash.xpm");
784 m_noBaselinePixmap = QPixmap(":/test/pics/noBaseline.xpm");
785
786 QString legend = QLatin1String("<body><center><font size='8'>Welcome to the khtml<br/>") +
787 QLatin1String("regression testing tool!</font></center><br/><br/>") +
788 QLatin1String("<table border='0' align='center' cellspacing='15'>") +
789 QLatin1String("<tr valign='top'><td colspan='2'><center><b>Legend</b></center></td></tr>") +
790 QLatin1String("<tr valign='middle'><td>Pass</td><td><img src=':/test/pics/pass.xpm'></td></tr>") +
791 QLatin1String("<tr valign='middle'><td>Pass unexpected</td><td><img src=':/test/pics/passUnexpected.xpm'></td></tr>") +
792 QLatin1String("<tr valign='middle'><td>Fail</td><td><img src=':/test/pics/fail.xpm'></td></tr>") +
793 QLatin1String("<tr valign='middle'><td>Fail known</td><td><img src=':/test/pics/failKnown.xpm'></td></tr>") +
794 QLatin1String("<tr valign='middle'><td>Ignore</td><td><img src=':/test/pics/ignore.xpm'></td></tr>") +
795 QLatin1String("<tr valign='middle'><td>Baseline missing</td><td><img src=':/test/pics/noBaseline.xpm'></td></tr>") +
796 QLatin1String("<tr valign='middle'><td>Crash</td><td><img src=':/test/pics/crash.xpm'></td></tr>") +
797 QLatin1String("</table></body>");
798
799 m_ui.textEdit->setHtml(legend);
800}
801
802void TestRegressionWindow::testerExited(int /* exitCode */, QProcess::ExitStatus exitStatus)
803{
804 assert(m_activeProcess != 0);
805 assert(m_activeTreeItem != 0);
806
807 if(exitStatus == QProcess::CrashExit) // Special case: crash!
808 {
809 QTreeWidgetItem *useItem = m_activeTreeItem;
810
811 if(m_testCounter >= 0 || m_runCounter > 0) // Single-tests mode invoked on a directory OR All-test-mode
812 {
813 QTreeWidgetItem *parent = useItem->parent();
814 assert(parent != 0);
815
816 useItem = parent->child(parent->indexOfChild(useItem) + 1);
817 assert(useItem != 0);
818 }
819
820 // Reflect crashed test...
821 updateItemStatus(Crash, useItem, QString());
822 }
823
824 if(m_testCounter >= 0) // All-tests mode
825 m_ui.progressBar->setValue(m_ui.progressBar->maximum());
826
827 // Eventually save log output...
828 if(!m_saveLogUrl.isEmpty())
829 {
830 // We should close our written log with </body></html>.
831 m_processingQueue.enqueue(QString::fromLatin1("\n</body>\n</html>"));
832
833 if(!m_justProcessingQueue)
834 {
835 m_justProcessingQueue = true;
836 QTimer::singleShot(50, this, SLOT(processQueue()));
837 }
838 }
839
840 // Cleanup gui...
841 m_ui.saveLogButton->setEnabled(true);
842 m_ui.actionRun_tests->setEnabled(true);
843 m_ui.pauseContinueButton->setEnabled(false);
844
845 // Check if there is already an output/index.html present...
846 loadOutputHTML();
847
848 // Cleanup data..
849 delete m_activeProcess;
850 m_activeProcess = 0;
851
852 m_runCounter = 0;
853 m_testCounter = 0;
854 m_activeTreeItem = 0;
855}
856
857void TestRegressionWindow::testerReceivedData()
858{
859 assert(m_activeProcess != 0);
860
861 QString data(m_activeProcess->readAllStandardOutput());
862 QStringList list = data.split('\n');
863
864 QStringList::const_iterator it = list.constBegin();
865 const QStringList::const_iterator end = list.constEnd();
866
867 for(; it != end; ++it)
868 {
869 QString temp = *it;
870 if(!temp.isEmpty())
871 m_processingQueue.enqueue(temp);
872 }
873
874 if(!m_justProcessingQueue)
875 {
876 m_justProcessingQueue = true;
877 QTimer::singleShot(50, this, SLOT(processQueue()));
878 }
879}
880
881void TestRegressionWindow::processQueue()
882{
883 while(!m_processingQueue.isEmpty())
884 {
885 QString data = m_processingQueue.dequeue();
886 TestResult result = Unknown;
887
888 QString cacheName = extractTestNameFromData(data, result);
889
890 if(result != Unknown) // Yes, we're dealing with a test result...
891 {
892 if(cacheName.isEmpty()) // Make sure everything is alright!
893 {
894 kError() << "Couldn't extract cacheName from data=\"" << data << "\"! Ignoring!" << endl;
895 continue;
896 }
897 }
898
899 parseRegressionTestingOutput(data, result, cacheName);
900 }
901
902 m_justProcessingQueue = false;
903}
904
905void TestRegressionWindow::addToIgnores()
906{
907 assert(m_activeTreeItem != 0);
908
909 QString treeItemText = pathFromItem(m_activeTreeItem);
910
911 // Extract directory/file name...
912 int position = treeItemText.lastIndexOf('/');
913
914 QString directory = treeItemText.mid(0, (position == -1 ? 0 : position));
915 QString fileName = treeItemText.mid(position + 1);
916
917 // Read corresponding ignore file..
918 QString ignoreFile = m_testsUrl.path() + "/tests/" + directory + "/ignore";
919 QStringList ignoreFileList = readListFile(ignoreFile);
920
921 if(!ignoreFileList.contains(fileName))
922 ignoreFileList.append(fileName);
923
924 // Commit changes...
925 writeListFile(ignoreFile, ignoreFileList);
926
927 // Reset icon status...
928 m_activeTreeItem->setIcon(0, m_ignorePixmap);
929}
930
931void TestRegressionWindow::removeFromIgnores()
932{
933 assert(m_activeTreeItem != 0);
934
935 QString treeItemText = pathFromItem(m_activeTreeItem);
936
937 // Extract directory/file name...
938 int position = treeItemText.lastIndexOf('/');
939
940 QString directory = treeItemText.mid(0, (position == -1 ? 0 : position));
941 QString fileName = treeItemText.mid(position + 1);
942
943 // Read corresponding ignore file..
944 QString ignoreFile = m_testsUrl.path() + "/tests/" + directory + "/ignore";
945 QStringList ignoreFileList = readListFile(ignoreFile);
946
947 if(ignoreFileList.contains(fileName))
948 ignoreFileList.removeAll(fileName);
949
950 // Commit changes...
951 writeListFile(ignoreFile, ignoreFileList);
952
953 // Reset icon status...
954 m_activeTreeItem->setIcon(0, QPixmap());
955}
956
957QString TestRegressionWindow::pathFromItem(const QTreeWidgetItem *item) const
958{
959 QString path = item->text(0);
960
961 QTreeWidgetItem *parent = item->parent();
962 while(parent != 0)
963 {
964 if(parent->parent() != 0)
965 path.prepend(parent->text(0) + "/"); //krazy:exclude=duoblequote_chars DOM demands chars
966
967 parent = parent->parent();
968 }
969
970 return path;
971}
972
973QString TestRegressionWindow::extractTestNameFromData(QString &data, TestResult &result) const
974{
975 if(data.indexOf("PASS") >= 0 || data.indexOf("FAIL") >= 0)
976 {
977 // Name extraction regexps...
978 QString bracesSelector("[0-9a-zA-Z-_<>\\* +-,.:!?$'\"=/\\[\\]\\(\\)]*");
979
980 QRegExp expPass("PASS: (" + bracesSelector + ")"); //krazy:exclude=duoblequote_chars DOM demands chars
981 QRegExp expPassUnexpected("PASS \\(unexpected!\\): (" + bracesSelector + ")"); //krazy:exclude=duoblequote_chars DOM demands chars
982
983 QRegExp expFail("FAIL: (" + bracesSelector + ")"); //krazy:exclude=duoblequote_chars DOM demands chars
984 QRegExp expFailKnown("FAIL \\(known\\): (" + bracesSelector + ")"); //krazy:exclude=duoblequote_chars DOM demands chars
985
986 // Extract name of test... (while using regexps as rare as possible!)
987 int pos = -1;
988 QString test;
989
990 QRegExp cleanTest(" \\[" + bracesSelector + "\\]");
991
992 pos = expPass.indexIn(data);
993 if(pos > -1) { test = expPass.cap(1); result = Pass; }
994
995 if(result == Unknown)
996 {
997 pos = expPassUnexpected.indexIn(data);
998 if(pos > -1) { test = expPassUnexpected.cap(1); result = PassUnexpected; }
999 }
1000
1001 if(result == Unknown)
1002 {
1003 pos = expFail.indexIn(data);
1004 if(pos > -1) { test = expFail.cap(1); result = Fail; }
1005 }
1006
1007 if(result == Unknown)
1008 {
1009 pos = expFailKnown.indexIn(data);
1010 if(pos > -1) { test = expFailKnown.cap(1); result = FailKnown; }
1011 }
1012
1013 if(!test.isEmpty() && result != Unknown) // Got information about test...
1014 {
1015 // Clean up first, so we only get the file name...
1016 test.replace(cleanTest, QString());
1017
1018 // Extract cached directory/filename pair...
1019 int lastSlashPos = test.lastIndexOf('/');
1020
1021 QString cachedDirectory = (lastSlashPos > 0 ? test.mid(0, lastSlashPos) : QString());
1022 QString cachedFilename = test.mid(lastSlashPos + 1);
1023
1024 if(cachedDirectory == ".") // Handle cases like "./empty.html"
1025 cachedDirectory.clear();
1026
1027 assert(m_directoryMap.constFind(cachedDirectory) != m_directoryMap.constEnd());
1028
1029 QString cacheName = cachedDirectory + "/" + cachedFilename; //krazy:exclude=duoblequote_chars DOM demands chars
1030 if(m_itemMap.constFind(cacheName) != m_itemMap.constEnd())
1031 {
1032 // Highlight test...
1033 data.replace(expPass, "<b><font color='green'>PASS:\t\\1</font></b>");
1034 data.replace(expPassUnexpected, "<b><font color='green'>PASS (unexpected!):\t\\1</font></b>");
1035 data.replace(expFail, "<b><font color='red'>FAIL:\t\\1</font></b>");
1036 data.replace(expFailKnown, "<b><font color='red'>FAIL (known):\t\\1</font></b>");
1037
1038 return cacheName;
1039 }
1040 }
1041 }
1042
1043 return QString();
1044}
1045
1046void TestRegressionWindow::parseRegressionTestingOutput(QString data, TestResult result, const QString &cacheName)
1047{
1048 if(!cacheName.isEmpty())
1049 {
1050 if(m_testCounter >= 0) // Only increment in all-tests mode...
1051 m_testCounter++;
1052
1053 m_runCounter++; // Always increment...
1054
1055 // Update the icon...
1056 updateItemStatus(result, m_itemMap[cacheName], cacheName);
1057 }
1058
1059 // Apply some nice formatting for the statistics...
1060 if(data.indexOf("Total") >= 0 || data.indexOf("Passes") >= 0 || data.indexOf("Tests completed") >= 0 ||
1061 data.indexOf("Errors:") >= 0 || data.indexOf("Failures:") >= 0)
1062 {
1063 QRegExp expTotal("Total: ([0-9]*)");
1064 QRegExp expPasses("Passes: ([0-9 a-z\\(\\)]*)");
1065 QRegExp expErrors("Errors: ([0-9 a-z]*)");
1066 QRegExp expFailures("Failures: ([0-9 a-z\\(\\)]*)");
1067
1068 data.replace("Tests completed.", "<br><center><h2>Tests completed.</h2></center>");
1069 data.replace(expTotal, "<b><font size='4'>Total:&nbsp;\\1</font></b>");
1070 data.replace(expPasses, "<b><font size='4' color='green'>Passes:&nbsp;\\1</font></b>");
1071 data.replace(expErrors, "<b><font size='4' color='blue'>Errors:&nbsp;\\1</font></b>");
1072 data.replace(expFailures, "<b><font size='4' color='red'>Failures:&nbsp;\\1</font></b>");
1073 }
1074
1075 if(!data.contains("</body>\n</html>")) // Don't put <br> behind </html>!
1076 data.append("<br>");
1077
1078 // Update text edit...
1079 updateLogOutput(data);
1080
1081 // Update progressbar...
1082 if(m_testCounter > 0)
1083 m_ui.progressBar->setValue(m_testCounter);
1084}
1085
1086void TestRegressionWindow::treeWidgetContextMenuRequested(const QPoint &pos)
1087{
1088 if((m_testCounter == -1 && m_activeProcess) || (m_testCounter > 0 && m_activeProcess) ||
1089 m_testsUrl.isEmpty() || m_khtmlUrl.isEmpty()) // Still processing/not ready yet...
1090 {
1091 return;
1092 }
1093
1094 QTreeWidgetItem *item = m_ui.treeWidget->itemAt(pos);
1095 if(item && item != m_ui.treeWidget->topLevelItem(0))
1096 {
1097 m_activeTreeItem = item;
1098
1099 // Build & show popup menu...
1100 QMenu menu(m_ui.treeWidget);
1101
1102 menu.addAction(SmallIcon("media-playback-start"), i18n("Run test..."), this, SLOT(runSingleTest()));
1103 menu.addSeparator();
1104 menu.addAction(SmallIcon("list-add"), i18n("Add to ignores..."), this, SLOT(addToIgnores()));
1105 menu.addAction(SmallIcon("dialog-cancel"), i18n("Remove from ignores..."), this, SLOT(removeFromIgnores()));
1106
1107 if(!menu.exec(m_ui.treeWidget->mapToGlobal(pos)))
1108 m_activeTreeItem = 0; // Needs reset...
1109 }
1110}
1111
1112void TestRegressionWindow::updateLogOutput(const QString &data)
1113{
1114 QTextCursor cursor = m_ui.textEdit->textCursor();
1115
1116 // Append 'data'...
1117 m_ui.textEdit->insertHtml(data);
1118
1119 // Keep a maximum of 100 lines in the log...
1120 const int maxLogLines = 100;
1121
1122 long logLines = countLogLines();
1123 if(logLines > maxLogLines)
1124 {
1125 cursor.movePosition(QTextCursor::Start);
1126 cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, logLines - maxLogLines);
1127 cursor.removeSelectedText();
1128 }
1129
1130 // Go to end...
1131 cursor.movePosition(QTextCursor::End);
1132 m_ui.textEdit->setTextCursor(cursor);
1133
1134 // Eventually save log output...
1135 if(!m_saveLogUrl.isEmpty())
1136 {
1137 QString fileName = m_saveLogUrl.path() + "/logOutput.html";
1138 QIODevice::OpenMode fileFlags = QIODevice::WriteOnly;
1139
1140 bool fileExists = QFileInfo(fileName).exists();
1141 if(fileExists)
1142 fileFlags |= QIODevice::Append;
1143
1144 QFile file(fileName);
1145 if(!file.open(fileFlags))
1146 {
1147 kError() << " Can't open " << fileName << endl;
1148 exit(1);
1149 }
1150
1151 if(!fileExists)
1152 file.write(QString::fromLatin1("<html>\n<body>\n").toLatin1());
1153
1154 file.write(QString(data + "\n").toLatin1()); //krazy:exclude=duoblequote_chars DOM demands chars
1155 file.close();
1156
1157 // Reset save log url, if we reached the end...
1158 if(data.contains("</body>\n</html>"))
1159 m_saveLogUrl = KUrl();
1160 }
1161}
1162
1163unsigned long TestRegressionWindow::countLogLines() const
1164{
1165 QTextCursor cursor = m_ui.textEdit->textCursor();
1166 cursor.movePosition(QTextCursor::Start);
1167
1168 unsigned long lines = 0;
1169 while(cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor))
1170 lines++;
1171
1172 return lines;
1173}
1174
1175QStringList TestRegressionWindow::readListFile(const QString &fileName) const
1176{
1177 QStringList files;
1178
1179 QFileInfo fileInfo(fileName);
1180 if(fileInfo.exists())
1181 {
1182 QFile file(fileName);
1183 if(!file.open(QIODevice::ReadOnly))
1184 {
1185 kError() << " Can't open " << fileName << endl;
1186 exit(1);
1187 }
1188
1189 QString line;
1190
1191 QTextStream fileStream(&file);
1192 while(!(line = fileStream.readLine()).isNull())
1193 files.append(line);
1194
1195 file.close();
1196 }
1197
1198 return files;
1199}
1200
1201void TestRegressionWindow::writeListFile(const QString &fileName, const QStringList &content) const
1202{
1203 QFile file(fileName);
1204 if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
1205 {
1206 kError() << " Can't open " << fileName << endl;
1207 exit(1);
1208 }
1209
1210 file.write(content.join("\n").toLatin1());
1211 file.close();
1212}
1213
1214// vim:ts=4:tw=4:noet
1215

Warning: That file was not part of the compilation database. It may have many parsing errors.