/** * This file is part of the KDE project * * Copyright (C) 2001,2003 Peter Kelly (pmk@post.com) * Copyright (C) 2003,2004 Stephan Kulow (coolo@kde.org) * Copyright (C) 2004 Dirk Mueller ( mueller@kde.org ) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "test_regression.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "css/cssstyleselector.h" #include #include "rendering/render_style.h" #include "rendering/render_layer.h" #include "khtmldefaults.h" #include #include #include #include #include #include #include //We don't use the default fonts, though, but traditional testregression ones #undef HTML_DEFAULT_VIEW_FONT #undef HTML_DEFAULT_VIEW_FIXED_FONT #undef HTML_DEFAULT_VIEW_SERIF_FONT #undef HTML_DEFAULT_VIEW_SANSSERIF_FONT #undef HTML_DEFAULT_VIEW_CURSIVE_FONT #undef HTML_DEFAULT_VIEW_FANTASY_FONT #define HTML_DEFAULT_VIEW_FONT "helvetica" #define HTML_DEFAULT_VIEW_FIXED_FONT "courier" #define HTML_DEFAULT_VIEW_SERIF_FONT "times" #define HTML_DEFAULT_VIEW_SANSSERIF_FONT "helvetica" #define HTML_DEFAULT_VIEW_CURSIVE_FONT "helvetica" #define HTML_DEFAULT_VIEW_FANTASY_FONT "helvetica" #ifdef __GNUC__ #warning "Kill this at some point" #endif struct PalInfo { QPalette::ColorRole role; quint32 color; }; PalInfo palInfo[] = { {QPalette::WindowText, 0xff000000}, {QPalette::Button, 0xffc0c0c0}, {QPalette::Light, 0xffffffff}, {QPalette::Midlight, 0xffdfdfdf}, {QPalette::Dark, 0xff808080}, {QPalette::Mid, 0xffa0a0a4}, {QPalette::Text, 0xff000000}, {QPalette::BrightText, 0xffffffff}, {QPalette::ButtonText, 0xff000000}, {QPalette::Base, 0xffffffff}, {QPalette::Window, 0xffc0c0c0}, {QPalette::Shadow, 0xff000000}, {QPalette::Highlight, 0xff000080}, {QPalette::HighlightedText, 0xffffffff}, {QPalette::Link, 0xff0000ff}, {QPalette::LinkVisited, 0xffff00ff}, {QPalette::LinkVisited, 0} }; PalInfo disPalInfo[] = { {QPalette::WindowText, 0xff808080}, {QPalette::Button, 0xffc0c0c0}, {QPalette::Light, 0xffffffff}, {QPalette::Midlight, 0xffdfdfdf}, {QPalette::Dark, 0xff808080}, {QPalette::Mid, 0xffa0a0a4}, {QPalette::Text, 0xff808080}, {QPalette::BrightText, 0xffffffff}, {QPalette::ButtonText, 0xff808080}, {QPalette::Base, 0xffc0c0c0}, {QPalette::Window, 0xffc0c0c0}, {QPalette::Shadow, 0xff000000}, {QPalette::Highlight, 0xff000080}, {QPalette::HighlightedText, 0xffffffff}, {QPalette::Link, 0xff0000ff}, {QPalette::LinkVisited, 0xffff00ff}, {QPalette::LinkVisited, 0} }; class TestStyle: public QWindowsStyle { public: TestStyle() {} virtual void drawControl(ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const { switch (element) { case CE_ScrollBarSubLine: case CE_ScrollBarAddLine: case CE_ScrollBarSubPage: case CE_ScrollBarAddPage: case CE_ScrollBarFirst: case CE_ScrollBarLast: case CE_ScrollBarSlider: { const QStyleOptionSlider* sbOpt = qstyleoption_cast(option); if (sbOpt->minimum == sbOpt->maximum) { const_cast(sbOpt)->state ^= QStyle::State_Enabled; if (element == CE_ScrollBarSlider) element = CE_ScrollBarAddPage; } if (element == CE_ScrollBarAddPage || element == CE_ScrollBarSubPage) { //Fun. in Qt4, the brush offset seems to be sensitive to window position?? painter->setBrushOrigin(0,1); } break; } default: //shaddup break; } QWindowsStyle::drawControl(element, option, painter, widget); } virtual QRect subControlRect(ComplexControl control, const QStyleOptionComplex* option, SubControl subControl, const QWidget* widget) const { QRect rect = QWindowsStyle::subControlRect(control, option, subControl, widget); switch (control) { case CC_ComboBox: if (subControl == SC_ComboBoxEditField) return rect.translated(3,0); else return rect; default: return rect; } } virtual QSize sizeFromContents(ContentsType type, const QStyleOption* option, const QSize& contentsSize, const QWidget* widget) const { QSize size = QWindowsStyle::sizeFromContents(type, option, contentsSize, widget); switch (type) { case CT_PushButton: return QSize(size.width(), size.height() - 1); case CT_LineEdit: if (widget && widget->parentWidget() && !qstricmp(widget->parentWidget()->metaObject()->className(), "KUrlRequester")) return QSize(size.width() + 1, size.height()); return QSize(size.width() + 2, size.height() + 2); case CT_ComboBox: { const QStyleOptionComboBox* cbOpt = qstyleoption_cast(option); Q_UNUSED(cbOpt); // is 'cbOpt' needed at all here? return QSize(qMax(43, size.width() + 6), size.height()); } default: return size; } } virtual int pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const { if (metric == PM_ButtonMargin) return 7; return QWindowsStyle::pixelMetric(metric, option, widget); } }; const char* imageMissingIcon = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAzrAAAM6wHl1kTSAAAB2ElEQVQ4jZWTMWsiQRTHfxlDCln2iFhIClksRUIQC6t8BkurzTewEBGx2GKrICmsUsg218rBWV2xdQqLRVJYhUQkBCKLeN6iixB5d0WyYOKGcP9mmHm83/u/NzMHvOobcMz/6Tfw5/Btc+z7/oWIoJRCKQWwt0ZSSqHr+vddACLyZck44GFc8OnpiX6/z3Q6RSmFYRhUq1UMw9gDqF2AUorRaES73WYymVAsFsnlcnieR6PRwPO8dy3uAcIwxHEcADRNo1qt0mq16PV6ZDIZut0uYRh+Dri7u2O1WlGr1QCwLIsgCMhkMlQqFebzOePx+P1cdgG+7wNQKpWwbRsRodlsslgsOD8/R0R4fHyMdwBwcnKCiHBzc0M6neby8hIRoV6vMxwOERGy2eznDvL5PJqmMRgMmM1mpFIprq6uEBFs20bXdc7OzkgkEvvXGA2uVqth2zamaVIul9E0jeVyiVKKdrtNMplkvV7HAwDK5TLX19c4jsN4PEZEKBQKmKbJdrvl/v4e13UfgAXAwVueEYbhRdwzTiQSvLy8cHt7y+npKZ1O59myrB8RQH10EKcgCDg6OoqSf/L6keJbiNNms8F13QfLsn69Jf+NYlELGpD+grMAgo+H/wARELhn1VB8lwAAAABJRU5ErkJggg=="; //r 727816 of oxygen's image-missing, base64'd PNG #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "localization/kencodingdetector.h" #include "dom/dom2_range.h" #include "dom/dom_exception.h" #include "dom/html_document.h" #include "html/htmltokenizer.h" #include "khtml_part.h" #include "khtmlpart_p.h" #include #include "khtmlview.h" #include "rendering/render_replaced.h" #include "xml/dom_docimpl.h" #include "html/html_baseimpl.h" #include "dom/dom_doc.h" #include "misc/loader.h" #include "ecma/kjs_binding.h" #include "ecma/kjs_dom.h" #include "ecma/kjs_window.h" #include "ecma/kjs_proxy.h" using namespace khtml; using namespace DOM; using namespace KJS; static bool visual = false; static pid_t xvfb; // ------------------------------------------------------------------------- PartMonitor *PartMonitor::sm_highestMonitor = NULL; PartMonitor::PartMonitor(KHTMLPart *_part) { m_part = _part; m_completed = false; connect(m_part,SIGNAL(completed()),this,SLOT(partCompleted())); m_timer_waits = 200; m_timeout_timer = new QTimer(this); } PartMonitor::~PartMonitor() { if (this == sm_highestMonitor) sm_highestMonitor = 0; while (!m_eventLoopStack.isEmpty()) exitLoop(); } void PartMonitor::waitForCompletion() { if (!m_completed) { if (sm_highestMonitor) return; sm_highestMonitor = this; enterLoop(); //connect(m_timeout_timer, SIGNAL(timeout()), this, SLOT(timeout()) ); //m_timeout_timer->stop(); //m_timeout_timer->start( visual ? 100 : 2, true ); } QTimer::singleShot( 0, this, SLOT(finishTimers()) ); enterLoop(); } void PartMonitor::enterLoop() { if (m_eventLoopStack.isEmpty() || m_eventLoopStack.top()->isRunning()) m_eventLoopStack.push(new QEventLoop()); m_eventLoopStack.top()->exec(); } void PartMonitor::exitLoop() { while (!m_eventLoopStack.isEmpty() && !m_eventLoopStack.top()->isRunning()) delete m_eventLoopStack.pop(); if (!m_eventLoopStack.isEmpty()) m_eventLoopStack.top()->exit(); } void PartMonitor::timeout() { exitLoop(); } void PartMonitor::finishTimers() { KJS::Window *w = KJS::Window::retrieveWindow( m_part ); --m_timer_waits; if ( m_timer_waits && ((w && w->winq->hasTimers()) || m_part->inProgress())) { // wait a bit QTimer::singleShot( 10, this, SLOT(finishTimers()) ); return; } exitLoop(); } void PartMonitor::partCompleted() { m_completed = true; m_timeout_timer->stop(); connect(m_timeout_timer, SIGNAL(timeout()),this, SLOT(timeout()) ); m_timeout_timer->setSingleShot(true); m_timeout_timer->start(visual ? 100 : 2); disconnect(m_part,SIGNAL(completed()),this,SLOT(partCompleted())); } static void signal_handler( int ) { printf( "timeout - this should *NOT* happen, it is likely the part's completed() signal was not emitted - FIXME!!\n" ); if (PartMonitor::sm_highestMonitor) PartMonitor::sm_highestMonitor->exitLoop(); else abort(); } // ------------------------------------------------------------------------- RegTestObject::RegTestObject(ExecState *exec, RegressionTest *_regTest) { m_regTest = _regTest; putDirect("print",new RegTestFunction(exec,m_regTest,RegTestFunction::Print,1), DontEnum); putDirect("reportResult",new RegTestFunction(exec,m_regTest,RegTestFunction::ReportResult,3), DontEnum); putDirect("checkOutput",new RegTestFunction(exec,m_regTest,RegTestFunction::CheckOutput,1), DontEnum); // add "quit" for compatibility with the mozilla js shell putDirect("quit", new RegTestFunction(exec,m_regTest,RegTestFunction::Quit,1), DontEnum ); } RegTestFunction::RegTestFunction(ExecState* /*exec*/, RegressionTest *_regTest, int _id, int length) { m_regTest = _regTest; id = _id; putDirect("length",length); } bool RegTestFunction::implementsCall() const { return true; } JSValue* RegTestFunction::callAsFunction(ExecState *exec, JSObject* /*thisObj*/, const List &args) { JSValue* result = jsUndefined(); if ( m_regTest->ignore_errors ) return result; switch (id) { case Print: { UString str = args[0]->toString(exec); if ( str.qstring().toLower().indexOf( "failed!" ) >= 0 ) m_regTest->saw_failure = true; QString res = str.qstring().replace('\007', ""); m_regTest->m_currentOutput += res + "\n"; //krazy:exclude=duoblequote_chars DOM demands chars break; } case ReportResult: { bool passed = args[0]->toBoolean(exec); QString description = args[1]->toString(exec).qstring(); if (args[1]->type() == UndefinedType || args[1]->type() == NullType) description.clear(); m_regTest->reportResult(passed,description); if ( !passed ) m_regTest->saw_failure = true; break; } case CheckOutput: { DOM::DocumentImpl* docimpl = static_cast( m_regTest->m_part->document().handle() ); if ( docimpl && docimpl->view() && docimpl->renderer() ) { docimpl->updateRendering(); } QString filename = args[0]->toString(exec).qstring(); filename = RegressionTest::curr->m_currentCategory+"/"+filename; //krazy:exclude=duoblequote_chars DOM demands chars int failures = RegressionTest::NoFailure; if ( m_regTest->m_genOutput ) { if ( !m_regTest->reportResult( m_regTest->checkOutput(filename+"-dom"), "Script-generated " + filename + "-dom") ) failures |= RegressionTest::DomFailure; if ( !m_regTest->reportResult( m_regTest->checkOutput(filename+"-render"), "Script-generated " + filename + "-render") ) failures |= RegressionTest::RenderFailure; } else { // compare with output file if ( !m_regTest->reportResult( m_regTest->checkOutput(filename+"-dom"), "DOM") ) failures |= RegressionTest::DomFailure; if ( !m_regTest->reportResult( m_regTest->checkOutput(filename+"-render"), "RENDER") ) failures |= RegressionTest::RenderFailure; } RegressionTest::curr->doFailureReport( filename, failures ); break; } case Quit: m_regTest->reportResult(true, "Called quit" ); if ( !m_regTest->saw_failure ) m_regTest->ignore_errors = true; break; } return result; } // ------------------------------------------------------------------------- KHTMLPartObject::KHTMLPartObject(ExecState *exec, KHTMLPart *_part) { m_part = _part; putDirect("openPage", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::OpenPage,1), DontEnum); putDirect("openPageAsUrl", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::OpenPageAsUrl,1), DontEnum); putDirect("begin", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::Begin,1), DontEnum); putDirect("write", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::Write,1), DontEnum); putDirect("end", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::End,0), DontEnum); putDirect("executeScript", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::ExecuteScript,0), DontEnum); putDirect("processEvents", new KHTMLPartFunction(exec,m_part,KHTMLPartFunction::ProcessEvents,0), DontEnum); } KJS::JSValue *KHTMLPartObject::winGetter(KJS::ExecState *, KJS::JSObject*, const KJS::Identifier&, const KJS::PropertySlot& slot) { KHTMLPartObject* thisObj = static_cast(slot.slotBase()); return KJS::Window::retrieveWindow(thisObj->m_part); } KJS::JSValue *KHTMLPartObject::docGetter(KJS::ExecState *exec, KJS::JSObject*, const KJS::Identifier&, const KJS::PropertySlot& slot) { KHTMLPartObject* thisObj = static_cast(slot.slotBase()); return getDOMNode(exec, thisObj->m_part->document().handle()); } bool KHTMLPartObject::getOwnPropertySlot(KJS::ExecState *exec, const KJS::Identifier& propertyName, KJS::PropertySlot& slot) { if (propertyName == "document") { slot.setCustom(this, docGetter); return true; } else if (propertyName == "window") { slot.setCustom(this, winGetter); return true; } return JSObject::getOwnPropertySlot(exec, propertyName, slot); } KHTMLPartFunction::KHTMLPartFunction(ExecState */*exec*/, KHTMLPart *_part, int _id, int length) { m_part = _part; id = _id; putDirect("length",length); } bool KHTMLPartFunction::implementsCall() const { return true; } JSValue* KHTMLPartFunction::callAsFunction(ExecState *exec, JSObject*/*thisObj*/, const List &args) { JSValue* result = jsUndefined(); switch (id) { case OpenPage: { if (args[0]->type() == NullType || args[0]->type() == NullType) { exec->setException(Error::create(exec, GeneralError,"No filename specified")); return jsUndefined(); } QString filename = args[0]->toString(exec).qstring(); QString fullFilename = QFileInfo(RegressionTest::curr->m_currentBase+"/"+filename).absoluteFilePath(); //krazy:exclude=duoblequote_chars DOM demands chars KUrl url; url.setProtocol("file"); url.setPath(fullFilename); PartMonitor pm(m_part); m_part->openUrl(url); pm.waitForCompletion(); kapp->processEvents(QEventLoop::AllEvents); break; } case OpenPageAsUrl: { if (args[0]->type() == NullType || args[0]->type() == UndefinedType) { exec->setException(Error::create(exec, GeneralError,"No filename specified")); return jsUndefined(); } if (args[1]->type() == NullType || args[1]->type() == UndefinedType) { exec->setException(Error::create(exec, GeneralError,"No url specified")); return jsUndefined(); } QString filename = args[0]->toString(exec).qstring(); QString url = args[1]->toString(exec).qstring(); QFile file(RegressionTest::curr->m_currentBase+"/"+filename); //krazy:exclude=duoblequote_chars DOM demands chars if (!file.open(QIODevice::ReadOnly)) { exec->setException(Error::create(exec, GeneralError, qPrintable(QString("Error reading " + filename)))); } else { QByteArray fileData; QDataStream stream(&fileData,QIODevice::WriteOnly); char buf[1024]; int bytesread; while (!file.atEnd()) { bytesread = file.read(buf,1024); stream.writeRawData(buf,bytesread); } file.close(); QString contents(fileData); PartMonitor pm(m_part); m_part->begin(KUrl( url )); m_part->write(contents); m_part->end(); pm.waitForCompletion(); } kapp->processEvents(QEventLoop::AllEvents); break; } case Begin: { QString url = args[0]->toString(exec).qstring(); m_part->begin(KUrl( url )); break; } case Write: { QString str = args[0]->toString(exec).qstring(); m_part->write(str); break; } case End: { m_part->end(); kapp->processEvents(QEventLoop::AllEvents); break; } case ExecuteScript: { QString code = args[0]->toString(exec).qstring(); Completion comp; KJSProxy *proxy = m_part->jScript(); proxy->evaluate("",0,code,0,&comp); if (comp.complType() == Throw) exec->setException(comp.value()); kapp->processEvents(QEventLoop::AllEvents); break; } case ProcessEvents: { kapp->processEvents(QEventLoop::AllEvents); break; } } return result; } // ------------------------------------------------------------------------- int main(int argc, char *argv[]) { // forget about any settings passwd* pw = getpwuid( getuid() ); if (!pw) { fprintf(stderr, "dang, I don't even know who I am.\n"); exit(1); } QString kh("/var/tmp/%1_non_existant"); kh = kh.arg( pw->pw_name ); setenv( "KDEHOME", kh.toLatin1(), 1 ); setenv( "LC_ALL", "C", 1 ); setenv( "LANG", "C", 1 ); // We want KIO to be in the slave-forking mode since // then it'll ask KProtocolInfo::exec for the binary to run, // and we intercept that, limiting the I/O to file:// // and the magic data://. See Slave::createSlave in KIO's slave.cpp setenv( "KDE_FORK_SLAVES", "true", 1); signal( SIGALRM, signal_handler ); // workaround various Qt crashes by always enforcing a TrueColor visual QApplication::setColorSpec( QApplication::ManyColor ); KCmdLineOptions options; options.add("b"); options.add("base ", ki18n("Directory containing tests, basedir and output directories.")); options.add("d"); options.add("debug", ki18n("Do not suppress debug output")); options.add("g"); options.add("genoutput", ki18n("Regenerate baseline (instead of checking)")); options.add("s"); options.add("noshow", ki18n("Do not show the window while running tests")); options.add("t"); options.add("test ", ki18n("Only run a single test. Multiple options allowed.")); options.add("js", ki18n("Only run .js tests")); options.add("html", ki18n("Only run .html tests")); options.add("noxvfb", ki18n("Do not use Xvfb")); options.add("o"); options.add("output ", ki18n("Put output in <directory> instead of <base_dir>/output")); options.add("r"); options.add("reference ", ki18n("Use <directory> as reference instead of <base_dir>/baseline")); options.add("+[base_dir]", ki18n("Directory containing tests, basedir and output directories. Only regarded if -b is not specified.")); options.add("+[testcases]", ki18n("Relative path to testcase, or directory of testcases to be run (equivalent to -t).")); KCmdLineArgs::init(argc, argv, "testregression", 0, ki18n("TestRegression"), "1.0", ki18n("Regression tester for khtml")); KCmdLineArgs::addCmdLineOptions(options); KCmdLineArgs *args = KCmdLineArgs::parsedArgs( ); QString baseDir = args->getOption("base"); if ( args->count() < 1 && baseDir.isEmpty() ) { KCmdLineArgs::usage(); ::exit( 1 ); } int testcase_index = 0; if (baseDir.isEmpty()) baseDir = args->arg(testcase_index++); QFileInfo bdInfo(baseDir); // font pathes passed to Xvfb must be absolute if (bdInfo.isRelative()) baseDir = bdInfo.dir().absolutePath(); const char *subdirs[] = {"tests", "baseline", "output", "resources"}; for ( int i = 0; i < 3; i++ ) { QFileInfo sourceDir(baseDir + QLatin1Char('/') + QLatin1String(subdirs[i])); if ( !sourceDir.exists() || !sourceDir.isDir() ) { fprintf(stderr,"ERROR: Source directory \"%s\": no such directory.\n", sourceDir.filePath().toLocal8Bit().data()); exit(1); } } if (args->isSet("xvfb")) { QString xvfbPath = KStandardDirs::findExe("Xvfb"); if ( xvfbPath.isEmpty() ) { fprintf( stderr, "ERROR: We need Xvfb to be installed for reliable results\n" ); exit( 1 ); } QByteArray xvfbPath8 = QFile::encodeName(xvfbPath); QStringList fpaths; fpaths.append(baseDir+"/resources"); const char* const fontdirs[] = { "75dpi", "misc", "Type1" }; const char* const fontpaths[] = {"/usr/share/fonts/", "/usr/X11/lib/X11/fonts/", "/usr/lib/X11/fonts/", "/usr/share/fonts/X11/" }; for (size_t fp=0; fp < sizeof(fontpaths)/sizeof(*fontpaths); ++fp) for (size_t fd=0; fd < sizeof(fontdirs)/sizeof(*fontdirs); ++fd) if (QFile::exists(QLatin1String(fontpaths[fp])+QLatin1String(fontdirs[fd]))) { if (strcmp(fontdirs[fd] , "Type1")) fpaths.append(QLatin1String(fontpaths[fp])+QLatin1String(fontdirs[fd])+":unscaled"); else fpaths.append(QLatin1String(fontpaths[fp])+QLatin1String(fontdirs[fd])); } xvfb = fork(); if ( !xvfb ) { QByteArray buffer = fpaths.join(",").toLatin1(); execl( xvfbPath8.data(), xvfbPath8.data(), "-once", "-dpi", "100", "-screen", "0", "1024x768x16", "-ac", "-fp", buffer.data(), ":47", (char*)NULL ); } setenv( "DISPLAY", ":47", 1 ); } KApplication a; // a.disableAutoDcopRegistration(); a.setStyle( new TestStyle ); KConfig sc1( "cryptodefaults", KConfig::SimpleConfig ); KConfigGroup grp = sc1.group("Warnings"); grp.writeEntry( "OnUnencrypted", false ); KSharedConfigPtr config = KGlobal::mainComponent().config(); grp = config->group("Notification Messages" ); grp.writeEntry( "kjscupguard_alarmhandler", true ); grp.writeEntry("ReportJSErrors", false); KConfig cfg( "khtmlrc" ); grp = cfg.group("HTML Settings"); grp.writeEntry( "StandardFont", HTML_DEFAULT_VIEW_SANSSERIF_FONT ); grp.writeEntry( "FixedFont", HTML_DEFAULT_VIEW_FIXED_FONT ); grp.writeEntry( "SerifFont", HTML_DEFAULT_VIEW_SERIF_FONT ); grp.writeEntry( "SansSerifFont", HTML_DEFAULT_VIEW_SANSSERIF_FONT ); grp.writeEntry( "CursiveFont", HTML_DEFAULT_VIEW_CURSIVE_FONT ); grp.writeEntry( "FantasyFont", HTML_DEFAULT_VIEW_FANTASY_FONT ); grp.writeEntry( "MinimumFontSize", HTML_DEFAULT_MIN_FONT_SIZE ); grp.writeEntry( "MediumFontSize", 10 ); grp.writeEntry( "Fonts", QStringList() ); grp.writeEntry( "DefaultEncoding", "" ); grp = cfg.group("Java/JavaScript Settings"); grp.writeEntry( "WindowOpenPolicy", (int)KHTMLSettings::KJSWindowOpenAllow); cfg.sync(); grp.sync(); KJS::ScriptInterpreter::turnOffCPUGuard(); QPalette pal = a.palette(); for (int c = 0; palInfo[c].color; ++c) { pal.setColor(QPalette::Active, palInfo[c].role, QColor(palInfo[c].color)); pal.setColor(QPalette::Inactive, palInfo[c].role, QColor(palInfo[c].color)); pal.setColor(QPalette::Disabled, palInfo[c].role, QColor(disPalInfo[c].color)); } a.setPalette(pal); int rv = 1; bool outputDebug = args->isSet( "debug" ); KConfig dc( "kdebugrc", KConfig::SimpleConfig ); static int areas[] = { 1000, 6000, 6005, 6010, 6020, 6030, 6031, 6035, 6036, 6040, 6041, 6045, 6050, 6060, 6061, 7000, 7006, 170, 171, 7101, 7002, 7019, 7027, 7014, 7011, 6070, 6080, 6090, 0}; for ( int i = 0; areas[i]; ++i ) { grp = dc.group( QString::number( areas[i] ) ); grp.writeEntry( "InfoOutput", outputDebug ? 2 : 4 ); grp.sync(); } dc.sync(); kClearDebugConfig(); // make sure the missing image icon is independent of the icon theme.. QByteArray brokenImData = QByteArray::fromBase64(imageMissingIcon); QImage brokenIm; brokenIm.loadFromData(brokenImData); khtml::Cache::brokenPixmap = new QPixmap(QPixmap::fromImage(brokenIm)); // create widgets KMainWindow *toplevel = new KMainWindow(); KHTMLPart *part = new KHTMLPart( toplevel, 0, KHTMLPart::BrowserViewGUI ); toplevel->setCentralWidget( part->widget() ); KAcceleratorManager::setNoAccel ( part->widget() ); part->setJScriptEnabled(true); part->executeScript(DOM::Node(), ""); // force the part to create an interpreter part->setPluginsEnabled(false); if (args->isSet("show")) visual = true; a.setTopWidget(part->widget()); if ( visual ) toplevel->show(); // we're not interested toplevel->statusBar()->hide(); if (!getenv("KDE_DEBUG")) { // set ulimits rlimit vmem_limit = { 512*1024*1024, RLIM_INFINITY }; // 512Mb Memory should suffice setrlimit(RLIMIT_AS, &vmem_limit); rlimit stack_limit = { 8*1024*1024, RLIM_INFINITY }; // 8Mb Memory should suffice setrlimit(RLIMIT_STACK, &stack_limit); } // run the tests RegressionTest *regressionTest = new RegressionTest(part, baseDir, args->getOption("output"), args->getOption("reference"), args->isSet("genoutput"), !args->isSet( "html" ), !args->isSet( "js" )); QObject::connect(part->browserExtension(), SIGNAL(openUrlRequest(KUrl,KParts::OpenUrlArguments,KParts::BrowserArguments)), regressionTest, SLOT(slotOpenURL(KUrl,KParts::OpenUrlArguments,KParts::BrowserArguments))); QObject::connect(part->browserExtension(), SIGNAL(resizeTopLevelWidget(int,int)), regressionTest, SLOT(resizeTopLevelWidget(int,int))); bool result = false; QStringList tests = args->getOptionList("test"); // merge testcases specified on command line for (; testcase_index < args->count(); testcase_index++) tests << args->arg(testcase_index); if (tests.count() > 0) foreach (QString test, tests) { result = regressionTest->runTests(test,true); if (!result) break; } else result = regressionTest->runTests(); if (result) { if (args->isSet("genoutput")) { printf("\nOutput generation completed.\n"); } else { printf("\nTests completed.\n"); printf("Total: %d\n", regressionTest->m_passes_work+ regressionTest->m_passes_fail+ regressionTest->m_failures_work+ regressionTest->m_failures_fail+ regressionTest->m_errors); printf("Passes: %d",regressionTest->m_passes_work); if ( regressionTest->m_passes_fail ) printf( " (%d unexpected passes)\n", regressionTest->m_passes_fail ); else printf( "\n" ); printf("Failures: %d",regressionTest->m_failures_work); if ( regressionTest->m_failures_fail ) printf( " (%d expected failures)\n", regressionTest->m_failures_fail ); else printf( "\n" ); if ( regressionTest->m_errors ) printf("Errors: %d\n",regressionTest->m_errors); QFile list( regressionTest->m_outputDir + "/links.html" ); list.open( QIODevice::WriteOnly|QIODevice::Append ); QString link, cl; link = QString( "
%1 failures. (%2 expected failures)" ) .arg(regressionTest->m_failures_work ) .arg( regressionTest->m_failures_fail ); list.write( link.toLatin1(), link.length() ); list.close(); } } // Only return a 0 exit code if all tests were successful if (regressionTest->m_failures_work == 0 && regressionTest->m_errors == 0) rv = 0; // cleanup delete regressionTest; delete part; delete toplevel; khtml::Cache::clear(); khtml::CSSStyleSelector::clear(); khtml::RenderStyle::cleanup(); kill( xvfb, SIGINT ); return rv; } // ------------------------------------------------------------------------- RegressionTest *RegressionTest::curr = 0; RegressionTest::RegressionTest(KHTMLPart *part, const QString &baseDir, const QString &outputDir, const QString &baselineDir, bool _genOutput, bool runJS, bool runHTML) : QObject(part) { m_part = part; m_baseDir = baseDir; m_baseDir = m_baseDir.replace( "//", "/" ); if ( m_baseDir.endsWith( "/" ) ) //krazy:exclude=duoblequote_chars DOM demands chars m_baseDir = m_baseDir.left( m_baseDir.length() - 1 ); if (outputDir.isEmpty()) m_outputDir = m_baseDir + "/output"; else { createMissingDirs(outputDir + "/"); //krazy:exclude=duoblequote_chars DOM demands chars m_outputDir = outputDir; } m_baselineDir = baselineDir; m_baselineDir = m_baselineDir.replace( "//", "/" ); if (m_baselineDir.endsWith( "/" ) ) m_baselineDir = m_baselineDir.left( m_baselineDir.length() - 1 ); if (m_baselineDir.isEmpty()) m_baselineDir = m_baseDir + "/baseline"; else { createMissingDirs(m_baselineDir + "/"); } m_genOutput = _genOutput; m_runJS = runJS; m_runHTML = runHTML; m_passes_work = m_passes_fail = 0; m_failures_work = m_failures_fail = 0; m_errors = 0; if (!m_genOutput) ::unlink( QFile::encodeName( m_outputDir + "/links.html" ) ); QFile f( m_outputDir + "/empty.html" ); QString s; f.open( QIODevice::WriteOnly | QIODevice::Truncate ); s = "Follow the white rabbit"; f.write( s.toLatin1(), s.length() ); f.close(); f.setFileName( m_outputDir + "/index.html" ); f.open( QIODevice::WriteOnly | QIODevice::Truncate ); s = ""; f.write( s.toLatin1(), s.length() ); f.close(); m_paintBuffer = 0; curr = this; m_part->view()->setFrameStyle(QFrame::StyledPanel | QFrame::Plain); resizeTopLevelWidget(800, 598 ); } static QStringList readListFile( const QString &filename ) { // Read ignore file for this directory QString ignoreFilename = filename; QFileInfo ignoreInfo(ignoreFilename); QStringList ignoreFiles; if (ignoreInfo.exists()) { QFile ignoreFile(ignoreFilename); if (!ignoreFile.open(QIODevice::ReadOnly)) { fprintf(stderr,"Can't open %s\n",qPrintable(ignoreFilename)); exit(1); } QTextStream ignoreStream(&ignoreFile); QString line; while (!(line = ignoreStream.readLine()).isNull()) ignoreFiles.append(line); ignoreFile.close(); } return ignoreFiles; } RegressionTest::~RegressionTest() { delete m_paintBuffer; } bool RegressionTest::runTests(QString relPath, bool mustExist, QStringList failureFileList) { m_currentOutput.clear(); QString fullPath = m_baseDir + "/tests/" + relPath; if (!QFile(fullPath).exists()) { fprintf(stderr,"%s: No such file or directory\n",qPrintable(relPath)); return false; } QFileInfo info(fullPath); if (!info.exists() && mustExist) { fprintf(stderr,"%s: No such file or directory\n",qPrintable(relPath)); return false; } if (!info.isReadable() && mustExist) { fprintf(stderr,"%s: Access denied\n",qPrintable(relPath)); return false; } if (info.isDir()) { QStringList ignoreFiles = readListFile( fullPath + "/ignore" ); QStringList failureFiles = readListFile( fullPath + "/KNOWN_FAILURES" ); // Run each test in this directory, recusively QDir sourceDir(m_baseDir + "/tests/"+relPath); for (uint fileno = 0; fileno < sourceDir.count(); fileno++) { QString filename = sourceDir[fileno]; QString relFilename = relPath.isEmpty() ? filename : relPath+"/"+filename; //krazy:exclude=duoblequote_chars DOM demands chars if (filename == "." || filename == ".." || ignoreFiles.contains(filename) ) continue; runTests(relFilename, false, failureFiles); } } else if (info.isFile()) { alarm( 12 ); khtml::Cache::init(); QString relativeDir = QFileInfo(relPath).path(); QString filename = info.fileName(); m_currentBase = m_baseDir + "/tests/"+relativeDir; m_currentCategory = relativeDir; m_currentTest = filename; if (failureFileList.isEmpty() && QFile(info.path() + "/KNOWN_FAILURES").exists()) { failureFileList = readListFile( info.path() + "/KNOWN_FAILURES" ); } int known_failure = NoFailure; if ( failureFileList.contains( filename ) ) known_failure |= AllFailure; if ( failureFileList.contains ( filename + "-render" ) ) known_failure |= RenderFailure; if ( failureFileList.contains ( filename + "-dump.png" ) ) known_failure |= PaintFailure; if ( failureFileList.contains ( filename + "-dom" ) ) known_failure |= DomFailure; m_known_failures = known_failure; if ( filename.endsWith(".html") || filename.endsWith( ".htm" ) || filename.endsWith( ".xhtml" ) || filename.endsWith( ".xml" ) ) { if ( relPath.startsWith( "domts/" ) && !m_runJS ) return true; if ( relPath.startsWith( "ecma/" ) && !m_runJS ) return true; if ( m_runHTML ) testStaticFile(relPath); } else if (filename.endsWith(".js")) { if ( m_runJS ) { alarm( 120 ); testJSFile(relPath); } } else if (mustExist) { fprintf(stderr,"%s: Not a valid test file (must be .htm(l) or .js)\n",qPrintable(relPath)); return false; } } else if (mustExist) { fprintf(stderr,"%s: Not a regular file\n",qPrintable(relPath)); return false; } return true; } void RegressionTest::getPartDOMOutput( QTextStream &outputStream, KHTMLPart* part, uint indent ) { DOM::Node node = part->document(); while (!node.isNull()) { // process for (uint i = 0; i < indent; i++) outputStream << " "; // Make doctype's visually different from elements if (node.nodeType() == DOM::Node::DOCUMENT_TYPE_NODE) outputStream << "!doctype "; outputStream << node.nodeName().string(); switch (node.nodeType()) { case DOM::Node::ELEMENT_NODE: { // Sort strings to ensure consistent output QStringList attrNames; NamedNodeMap attrs = node.attributes(); for (uint a = 0; a < attrs.length(); a++) attrNames.append(attrs.item(a).nodeName().string()); attrNames.sort(); QStringList::iterator it; Element elem(node); for (it = attrNames.begin(); it != attrNames.end(); ++it) { QString name = *it; QString value = elem.getAttribute(*it).string(); outputStream << " " << name << "=\"" << value << "\""; } if ( node.handle()->id() == ID_FRAME ) { outputStream << endl; QString frameName = static_cast( node.handle() )->name.string(); KHTMLPart* frame = part->findFrame( frameName ); if ( frame ) getPartDOMOutput( outputStream, frame, indent ); else outputStream << "(FRAME NOT FOUND)"; } break; } case DOM::Node::ATTRIBUTE_NODE: // Should not be present in tree assert(false); break; case DOM::Node::TEXT_NODE: outputStream << " \"" << Text(node).data().string() << "\""; break; case DOM::Node::CDATA_SECTION_NODE: outputStream << " \"" << CDATASection(node).data().string() << "\""; break; case DOM::Node::ENTITY_REFERENCE_NODE: break; case DOM::Node::ENTITY_NODE: break; case DOM::Node::PROCESSING_INSTRUCTION_NODE: break; case DOM::Node::COMMENT_NODE: outputStream << " \"" << Comment(node).data().string() << "\""; break; case DOM::Node::DOCUMENT_NODE: break; case DOM::Node::DOCUMENT_TYPE_NODE: break; case DOM::Node::DOCUMENT_FRAGMENT_NODE: // Should not be present in tree assert(false); break; case DOM::Node::NOTATION_NODE: break; default: assert(false); break; } outputStream << endl; if (!node.firstChild().isNull()) { node = node.firstChild(); indent++; } else if (!node.nextSibling().isNull()) { node = node.nextSibling(); } else { while (!node.isNull() && node.nextSibling().isNull()) { node = node.parentNode(); indent--; } if (!node.isNull()) node = node.nextSibling(); } } } void RegressionTest::dumpRenderTree( QTextStream &outputStream, KHTMLPart* part ) { DOM::DocumentImpl* doc = static_cast( part->document().handle() ); if ( !doc || !doc->renderer() ) return; doc->renderer()->layer()->dump( outputStream ); // Dump frames if any // Get list of names instead of frames() to sort the list alphabetically QStringList names = part->frameNames(); names.sort(); for ( QStringList::iterator it = names.begin(); it != names.end(); ++it ) { outputStream << "FRAME: " << (*it) << "\n"; KHTMLPart* frame = part->findFrame( (*it) ); // Q_ASSERT( frame ); if ( frame ) dumpRenderTree( outputStream, frame ); } } QString RegressionTest::getPartOutput( OutputType type) { // dump out the contents of the rendering & DOM trees QString dump; QTextStream outputStream(&dump, QIODevice::WriteOnly); if ( type == RenderTree ) { dumpRenderTree( outputStream, m_part ); } else { assert( type == DOMTree ); getPartDOMOutput( outputStream, m_part, 0 ); } dump.replace( m_baseDir + "/tests", QLatin1String( "REGRESSION_SRCDIR" ) ); return dump; } QImage RegressionTest::renderToImage() { int ew = m_part->view()->contentsWidth(); int eh = m_part->view()->contentsHeight(); if (ew * eh > 4000 * 4000) // don't DoS us return QImage(); QImage img( ew, eh, QImage::Format_ARGB32 ); img.fill( 0xff0000 ); if (!m_paintBuffer ) m_paintBuffer = new QPixmap( 512, 128 ); for ( int py = 0; py < eh; py += 128 ) { for ( int px = 0; px < ew; px += 512 ) { QPainter* tp = new QPainter; tp->begin( m_paintBuffer ); tp->translate( -px, -py ); tp->fillRect(px, py, 512, 128, Qt::magenta); m_part->document().handle()->renderer()->layer()->paint( tp, QRect( px, py, 512, 128 ) ); tp->end(); delete tp; // now fill the chunk into our image QImage chunk = m_paintBuffer->toImage(); assert( chunk.depth() == 32 ); for ( int y = 0; y < 128 && py + y < eh; ++y ) memcpy( img.scanLine( py+y ) + px*4, chunk.scanLine( y ), qMin( 512, ew-px )*4 ); } } assert( img.depth() == 32 ); return img; } bool RegressionTest::imageEqual( const QImage &lhsi, const QImage &rhsi ) { if ( lhsi.width() != rhsi.width() || lhsi.height() != rhsi.height() ) { kDebug() << "dimensions different " << lhsi.size() << " " << rhsi.size(); return false; } int w = lhsi.width(); int h = lhsi.height(); int bytes = lhsi.bytesPerLine(); const unsigned char* origLs = lhsi.bits(); const unsigned char* origRs = rhsi.bits(); for ( int y = 0; y < h; ++y ) { const QRgb* ls = (const QRgb*)(origLs + y * bytes); const QRgb* rs = (const QRgb*)(origRs + y * bytes); if ( memcmp( ls, rs, bytes ) ) { for ( int x = 0; x < w; ++x ) { QRgb l = ls[x]; QRgb r = rs[x]; if ( ( abs( qRed( l ) - qRed(r ) ) < 20 ) && ( abs( qGreen( l ) - qGreen(r ) ) < 20 ) && ( abs( qBlue( l ) - qBlue(r ) ) < 20 ) ) continue; kDebug() << "pixel (" << x << ", " << y << ") is different " << QColor( lhsi.pixel ( x, y ) ) << " " << QColor( rhsi.pixel ( x, y ) ); return false; } } } return true; } void RegressionTest::createLink( const QString& test, int failures ) { createMissingDirs( m_outputDir + "/" + test + "-compare.html" ); //krazy:exclude=duoblequote_chars DOM demands chars QFile list( m_outputDir + "/links.html" ); list.open( QIODevice::WriteOnly|QIODevice::Append ); QString link; link = QString( "" ) .arg( test + "-compare.html" ) .arg( test ); link += m_currentTest; link += " ["; if ( failures & DomFailure ) link += "D"; //krazy:exclude=duoblequote_chars DOM demands chars if ( failures & RenderFailure ) link += "R"; //krazy:exclude=duoblequote_chars DOM demands chars if ( failures & PaintFailure ) link += "P"; //krazy:exclude=duoblequote_chars DOM demands chars link += "]
\n"; list.write( link.toLatin1(), link.length() ); list.close(); } void RegressionTest::doJavascriptReport( const QString &test ) { QFile compare( m_outputDir + "/" + test + "-compare.html" ); //krazy:exclude=duoblequote_chars DOM demands chars if ( !compare.open( QIODevice::WriteOnly|QIODevice::Truncate ) ) kDebug() << "failed to open " << m_outputDir + "/" + test + "-compare.html"; //krazy:exclude=duoblequote_chars DOM demands chars QString cl; cl = QString( "%1" ).arg( test ); cl += ""; QString text = "\n" + m_currentOutput; //krazy:exclude=duoblequote_chars DOM demands chars text.replace( '<', "<" ); text.replace( '>', ">" ); text.replace( QRegExp( "\nFAILED" ), "\nFAILED" ); text.replace( QRegExp( "\nFAIL" ), "\nFAIL" ); text.replace( QRegExp( "\nPASSED" ), "\nPASSED" ); text.replace( QRegExp( "\nPASS" ), "\nPASS" ); if ( text.at( 0 ) == '\n' ) text = text.mid( 1, text.length() ); text.replace( '\n', "
\n" ); cl += text; cl += "
"; compare.write( cl.toLatin1(), cl.length() ); compare.close(); } /** returns the path in a way that is relatively reachable from base. * @param base base directory (must not include trailing slash) * @param path directory/file to be relatively reached by base * @return path with all elements replaced by .. and concerning path elements * to be relatively reachable from base. */ static QString makeRelativePath(const QString &base, const QString &path) { QString absBase = QFileInfo(base).absoluteFilePath(); QString absPath = QFileInfo(path).absoluteFilePath(); // kDebug() << "absPath: \"" << absPath << "\""; // kDebug() << "absBase: \"" << absBase << "\""; // walk up to common ancestor directory int pos = 0; do { pos++; int newpos = absBase.indexOf('/', pos); if (newpos == -1) newpos = absBase.length(); QString cmpPathComp(absPath.unicode() + pos, newpos - pos); QString cmpBaseComp(absBase.unicode() + pos, newpos - pos); // kDebug() << "cmpPathComp: \"" << cmpPathComp.string() << "\""; // kDebug() << "cmpBaseComp: \"" << cmpBaseComp.string() << "\""; // kDebug() << "pos: " << pos << " newpos: " << newpos; if (cmpPathComp != cmpBaseComp) { pos--; break; } pos = newpos; } while (pos < (int)absBase.length() && pos < (int)absPath.length()); int basepos = pos < (int)absBase.length() ? pos + 1 : pos; int pathpos = pos < (int)absPath.length() ? pos + 1 : pos; // kDebug() << "basepos " << basepos << " pathpos " << pathpos; QString rel; { QString relBase(absBase.unicode() + basepos, absBase.length() - basepos); QString relPath(absPath.unicode() + pathpos, absPath.length() - pathpos); // generate as many .. as there are path elements in relBase if (relBase.length() > 0) { for (int i = relBase.count('/'); i > 0; --i) rel += "../"; rel += ".."; if (relPath.length() > 0) rel += "/"; //krazy:exclude=duoblequote_chars DOM demands chars } rel += relPath; } return rel; } static QString getDiff(QString cmdLine) { QProcess p; p.start(cmdLine, QIODevice::ReadOnly); p.waitForFinished(); QString text = QString::fromLocal8Bit(p.readAllStandardOutput()); text = text.replace( '<', "<" ); text = text.replace( '>', ">" ); return text; } void RegressionTest::doFailureReport( const QString& test, int failures ) { if ( failures == NoFailure ) { ::unlink( QFile::encodeName( m_outputDir + "/" + test + "-compare.html" ) ); //krazy:exclude=duoblequote_chars DOM demands chars return; } createLink( test, failures ); if ( failures & JSFailure ) { doJavascriptReport( test ); return; // no support for both kind } QFile compare( m_outputDir + "/" + test + "-compare.html" ); //krazy:exclude=duoblequote_chars DOM demands chars QString testFile = QFileInfo(test).fileName(); QString renderDiff; QString domDiff; QString relOutputDir = makeRelativePath(m_baseDir, m_outputDir); QString relBaselineDir = makeRelativePath(m_baseDir, m_baselineDir); // are blocking reads possible with K3Process? QString pwd = QDir::currentPath(); chdir( QFile::encodeName( m_baseDir ) ); if ( failures & RenderFailure ) { renderDiff += "
";
        renderDiff += getDiff( QString::fromLatin1( "diff -u %4/%1-render %3/%2-render" )
                            .arg ( test, test, relOutputDir, relBaselineDir ) );
        renderDiff += "
"; } if ( failures & DomFailure ) { domDiff += "
";
        domDiff += getDiff( QString::fromLatin1( "diff -u %4/%1-dom %3/%2-dom" )
                            .arg ( test, test, relOutputDir, relBaselineDir ) );
        domDiff += "
"; } chdir( QFile::encodeName( pwd ) ); // create a relative path so that it works via web as well. ugly QString relpath = makeRelativePath(m_outputDir + "/" //krazy:exclude=duoblequote_chars DOM demands chars + QFileInfo(test).path(), m_baseDir); compare.open( QIODevice::WriteOnly|QIODevice::Truncate ); QString cl; cl = QString( "%1" ).arg( test ); cl += QString( "\n"); cl += QString ("\n" ); if ( failures & PaintFailure ) cl += QString( "\n

%3

\n" ).arg( test ); if ( failures & PaintFailure ) cl += QString ( "FLICKER \n" "BASE \n" "OUT \n" ); if ( renderDiff.length() ) cl += "R-DIFF \n"; if ( domDiff.length() ) cl += "D-DIFF \n"; // The test file always exists - except for checkOutput called from *.js files if ( QFile::exists( m_baseDir + "/tests/"+ test ) ) cl += QString( "HTML " ) .arg( relpath+"/tests/"+test ); cl += QString( "
" "" ) .arg( relpath+"/"+relBaselineDir+"/"+test+"-dump.png" ); cl += "
" + renderDiff + "
"; cl += "
" + domDiff + "
"; cl += ""; compare.write( cl.toLatin1(), cl.length() ); compare.close(); } void RegressionTest::testStaticFile(const QString & filename) { qDebug("TESTING:%s", filename.toLatin1().data()); resizeTopLevelWidget( 800, 598 ); // restore size // Set arguments KParts::OpenUrlArguments args; if (filename.endsWith(".html") || filename.endsWith(".htm")) args.setMimeType("text/html"); else if (filename.endsWith(".xhtml")) args.setMimeType("application/xhtml+xml"); else if (filename.endsWith(".xml")) args.setMimeType("text/xml"); m_part->setArguments(args); // load page KUrl url; url.setProtocol("file"); url.setPath(QFileInfo(m_baseDir + "/tests/"+filename).absoluteFilePath()); PartMonitor pm(m_part); m_part->openUrl(url); pm.waitForCompletion(); m_part->closeUrl(); if ( filename.startsWith( "domts/" ) ) { QString functionname; KJS::Completion comp = m_part->jScriptInterpreter()->evaluate(filename, 0, "exposeTestFunctionNames();"); /* * Error handling */ KJS::ExecState *exec = m_part->jScriptInterpreter()->globalExec(); if ( comp.complType() == ReturnValue || comp.complType() == Normal ) { if (comp.value() && comp.value()->type() == ObjectType && comp.value()->toObject(exec)->className() == "Array" ) { JSObject* argArrayObj = comp.value()->toObject(exec); unsigned int length = argArrayObj-> get(exec, "length")-> toUInt32(exec); if ( length == 1 ) functionname = argArrayObj->get(exec, 0)->toString(exec).qstring(); } } if ( functionname.isNull() ) { kDebug() << "DOM " << filename << " doesn't expose 1 function name - ignoring"; return; } KJS::Completion comp2 = m_part->jScriptInterpreter()->evaluate(filename, 0, QString("setUpPage(); " + functionname + "();") ); bool success = ( comp2.complType() == ReturnValue || comp2.complType() == Normal ); QString description = "DOMTS"; if ( comp2.complType() == Throw ) { KJS::JSValue* val = comp2.value(); KJS::JSObject* obj = val->toObject(exec); if ( obj && obj->hasProperty( exec, "jsUnitMessage" ) ) description = obj->get( exec, "jsUnitMessage" )->toString( exec ).qstring(); else description = comp2.value()->toString( exec ).qstring(); } reportResult( success, description ); if (!success && !m_known_failures) doFailureReport( filename, JSFailure ); return; } int back_known_failures = m_known_failures; if ( m_genOutput ) { if ( m_known_failures & DomFailure) m_known_failures = AllFailure; reportResult( checkOutput(filename+"-dom"), "DOM" ); if ( m_known_failures & RenderFailure ) m_known_failures = AllFailure; reportResult( checkOutput(filename+"-render"), "RENDER" ); if ( m_known_failures & PaintFailure ) m_known_failures = AllFailure; renderToImage().save(m_baselineDir + "/" + filename + "-dump.png","PNG", 60); printf("Generated %s\n", qPrintable(QString( m_baselineDir + "/" + filename + "-dump.png" )) ); reportResult( true, "PAINT" ); } else { int failures = NoFailure; // compare with output file if ( m_known_failures & DomFailure) m_known_failures = AllFailure; if ( !reportResult( checkOutput(filename+"-dom"), "DOM" ) ) failures |= DomFailure; if ( m_known_failures & RenderFailure ) m_known_failures = AllFailure; if ( !reportResult( checkOutput(filename+"-render"), "RENDER" ) ) failures |= RenderFailure; if ( m_known_failures & PaintFailure ) m_known_failures = AllFailure; if (!reportResult( checkPaintdump(filename), "PAINT") ) failures |= PaintFailure; doFailureReport(filename, failures ); } m_known_failures = back_known_failures; } void RegressionTest::evalJS( ScriptInterpreter &interp, const QString &filename, bool report_result ) { QString fullSourceName = filename; QFile sourceFile(fullSourceName); if (!sourceFile.open(QIODevice::ReadOnly)) { fprintf(stderr,"Error reading file %s\n",qPrintable(fullSourceName)); exit(1); } QTextStream stream ( &sourceFile ); stream.setCodec( "UTF-8" ); QString code = stream.readAll(); sourceFile.close(); saw_failure = false; ignore_errors = false; Completion c = interp.evaluate(filename, 0, UString( code ) ); if ( report_result && !ignore_errors) { bool expected_failure = filename.endsWith( "-n.js" ); if (c.complType() == Throw) { ExecState* exec = interp.globalExec(); QString errmsg = c.value()->toString(exec).qstring(); if ( !expected_failure ) { int line = -1; JSObject* obj = c.value()->toObject(exec); if (obj) line = obj->get(exec, "line")->toUInt32(exec); printf( "ERROR: %s (%s) at line:%d\n",qPrintable(filename), qPrintable(errmsg), line); doFailureReport( m_currentCategory + "/" + m_currentTest, JSFailure ); //krazy:exclude=duoblequote_chars DOM demands chars m_errors++; } else { reportResult( true, QString( "Expected Failure: %1" ).arg( errmsg ) ); } } else if ( saw_failure ) { if ( !expected_failure ) doFailureReport( m_currentCategory + "/" + m_currentTest, JSFailure ); //krazy:exclude=duoblequote_chars DOM demands chars reportResult( expected_failure, "saw 'failed!'" ); } else { reportResult( !expected_failure, "passed" ); } } } class GlobalImp : public JSGlobalObject { public: virtual UString className() const { return "global"; } }; void RegressionTest::testJSFile(const QString & filename ) { qDebug("TEST JS:%s", filename.toLatin1().data()); resizeTopLevelWidget( 800, 598 ); // restore size // create interpreter // note: this is different from the interpreter used by the part, // it contains regression test-specific objects & functions ProtectedPtr global(new GlobalImp()); khtml::ChildFrame frame; frame.m_part = m_part; ScriptInterpreter interp(global,&frame); ExecState *exec = interp.globalExec(); global->put(exec, "part", new KHTMLPartObject(exec,m_part)); global->put(exec, "regtest", new RegTestObject(exec,this)); global->put(exec, "debug", new RegTestFunction(exec,this,RegTestFunction::Print,1) ); global->put(exec, "print", new RegTestFunction(exec,this,RegTestFunction::Print,1) ); QStringList dirs = filename.split( '/' ); // NOTE: the basename is of little interest here, but the last basedir change // isn't taken in account QString basedir = m_baseDir + "/tests/"; for ( QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it ) { if ( ! ::access( QFile::encodeName( basedir + "shell.js" ), R_OK ) ) evalJS( interp, basedir + "shell.js", false ); basedir += *it + "/"; //krazy:exclude=duoblequote_chars DOM demands chars } evalJS( interp, m_baseDir + "/tests/"+ filename, true ); } RegressionTest::CheckResult RegressionTest::checkPaintdump(const QString &filename) { QString againstFilename( filename + "-dump.png" ); QString absFilename = QFileInfo(m_baselineDir + "/" + againstFilename).absoluteFilePath(); if ( svnIgnored( absFilename ) ) { m_known_failures = NoFailure; return Ignored; } CheckResult result = Failure; QImage baseline; baseline.load( absFilename, "PNG"); QImage output = renderToImage(); if ( !imageEqual( baseline, output ) ) { //krazy:exclude=duoblequote_chars DOM demands chars QString outputFilename = m_outputDir + "/" + againstFilename; createMissingDirs(outputFilename ); bool kf = false; if ( m_known_failures & AllFailure ) kf = true; else if ( m_known_failures & PaintFailure ) kf = true; if ( kf ) outputFilename += "-KF"; output.save(outputFilename, "PNG", 60); } else { ::unlink( QFile::encodeName( m_outputDir + "/" + againstFilename ) ); //krazy:exclude=duoblequote_chars DOM demands chars result = Success; } return result; } RegressionTest::CheckResult RegressionTest::checkOutput(const QString &againstFilename) { QString absFilename = QFileInfo(m_baselineDir + "/" + againstFilename).absoluteFilePath(); if ( svnIgnored( absFilename ) ) { m_known_failures = NoFailure; return Ignored; } bool domOut = againstFilename.endsWith( "-dom" ); QString data = getPartOutput( domOut ? DOMTree : RenderTree ); data.remove( char( 13 ) ); CheckResult result = Success; // compare result to existing file QString outputFilename = QFileInfo(m_outputDir + "/" + againstFilename).absoluteFilePath(); //krazy:exclude=duoblequote_chars DOM demands chars bool kf = false; if ( m_known_failures & AllFailure ) kf = true; else if ( domOut && ( m_known_failures & DomFailure ) ) kf = true; else if ( !domOut && ( m_known_failures & RenderFailure ) ) kf = true; if ( kf ) outputFilename += "-KF"; if ( m_genOutput ) outputFilename = absFilename; QFile file(absFilename); if (file.open(QIODevice::ReadOnly)) { QTextStream stream ( &file ); stream.setCodec( "UTF-8" ); QString fileData = stream.readAll(); result = ( fileData == data ) ? Success : Failure; if ( !m_genOutput && result == Success ) { ::unlink( QFile::encodeName( outputFilename ) ); return Success; } } // generate result file createMissingDirs( outputFilename ); QFile file2(outputFilename); if (!file2.open(QIODevice::WriteOnly)) { fprintf(stderr,"Error writing to file %s\n",qPrintable(outputFilename)); exit(1); } QTextStream stream2(&file2); stream2.setCodec( "UTF-8" ); stream2 << data; if ( m_genOutput ) printf("Generated %s\n", qPrintable(outputFilename)); return result; } bool RegressionTest::reportResult(CheckResult result, const QString & description) { if ( result == Ignored ) { //printf("IGNORED: "); //printDescription( description ); return true; // no error } else return reportResult( result == Success, description ); } bool RegressionTest::reportResult(bool passed, const QString & description) { if (m_genOutput) return true; if (passed) { if ( m_known_failures & AllFailure ) { printf("PASS (unexpected!): "); m_passes_fail++; } else { printf("PASS: "); m_passes_work++; } } else { if ( m_known_failures & AllFailure ) { printf("FAIL (known): "); m_failures_fail++; passed = true; // we knew about it } else { printf("FAIL: "); m_failures_work++; } } printDescription( description ); return passed; } void RegressionTest::printDescription(const QString& description) { if (!m_currentCategory.isEmpty()) printf("%s/", qPrintable(m_currentCategory)); printf("%s", qPrintable(m_currentTest)); if (!description.isEmpty()) { QString desc = description; desc.replace( '\n', ' ' ); printf(" [%s]", qPrintable(desc)); } printf("\n"); fflush(stdout); } void RegressionTest::createMissingDirs(const QString & filename) { QFileInfo dif(filename); QFileInfo dirInfo( dif.path() ); if (dirInfo.exists()) return; QStringList pathComponents; QFileInfo parentDir = dirInfo; pathComponents.prepend(parentDir.absoluteFilePath()); while (!parentDir.exists()) { QString parentPath = parentDir.absoluteFilePath(); int slashPos = parentPath.lastIndexOf('/'); if (slashPos < 0) break; parentPath = parentPath.left(slashPos); pathComponents.prepend(parentPath); parentDir = QFileInfo(parentPath); } for (int pathno = 1; pathno < pathComponents.count(); pathno++) { if (!QFileInfo(pathComponents[pathno]).exists() && !QDir(pathComponents[pathno-1]).mkdir(pathComponents[pathno])) { fprintf(stderr,"Error creating directory %s\n",qPrintable(pathComponents[pathno])); exit(1); } } } void RegressionTest::slotOpenURL(const KUrl &url, const KParts::OpenUrlArguments& args, const KParts::BrowserArguments& browserArgs) { m_part->setArguments(args); m_part->browserExtension()->setBrowserArguments(browserArgs); PartMonitor pm(m_part); m_part->openUrl(url); pm.waitForCompletion(); } bool RegressionTest::svnIgnored( const QString &filename ) { QFileInfo fi( filename ); QString ignoreFilename = fi.path() + "/svnignore"; QFile ignoreFile(ignoreFilename); if (!ignoreFile.open(QIODevice::ReadOnly)) return false; QTextStream ignoreStream(&ignoreFile); QString line; while (!(line = ignoreStream.readLine()).isNull()) { if ( line == fi.fileName() ) return true; } ignoreFile.close(); return false; } void RegressionTest::resizeTopLevelWidget( int w, int h ) { m_part->widget()->parentWidget()->resize( w, h ); m_part->widget()->resize( w, h ); // Since we're not visible, this doesn't have an immediate effect, QWidget posts the event QApplication::sendPostedEvents( 0, QEvent::Resize ); } #include "moc_test_regression.cpp"