• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2005, 2006 Apple Computer, Inc.  All rights reserved.
3  * Copyright (C) 2006 Nikolas Zimmermann <zimmermann@kde.org>
4  * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
5  * Copyright (C) 2009 Torch Mobile Inc. http://www.torchmobile.com/
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * 1.  Redistributions of source code must retain the above copyright
12  *     notice, this list of conditions and the following disclaimer.
13  * 2.  Redistributions in binary form must reproduce the above copyright
14  *     notice, this list of conditions and the following disclaimer in the
15  *     documentation and/or other materials provided with the distribution.
16  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
17  *     its contributors may be used to endorse or promote products derived
18  *     from this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
21  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
24  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 #include "DumpRenderTree.h"
33 #include "jsobjects.h"
34 #include "testplugin.h"
35 #include "WorkQueue.h"
36 
37 #include <QBuffer>
38 #include <QCryptographicHash>
39 #include <QDir>
40 #include <QFile>
41 #include <QTimer>
42 #include <QBoxLayout>
43 #include <QScrollArea>
44 #include <QApplication>
45 #include <QUrl>
46 #include <QFocusEvent>
47 #include <QFontDatabase>
48 
49 #include <qwebpage.h>
50 #include <qwebframe.h>
51 #include <qwebview.h>
52 #include <qwebsettings.h>
53 #include <qwebsecurityorigin.h>
54 
55 #ifdef Q_WS_X11
56 #include <fontconfig/fontconfig.h>
57 #endif
58 
59 #include <unistd.h>
60 #include <qdebug.h>
61 
62 extern void qt_drt_run(bool b);
63 extern void qt_dump_set_accepts_editing(bool b);
64 extern void qt_dump_frame_loader(bool b);
65 extern void qt_drt_clearFrameName(QWebFrame* qFrame);
66 extern void qt_drt_overwritePluginDirectories();
67 
68 namespace WebCore {
69 
70 // Choose some default values.
71 const unsigned int maxViewWidth = 800;
72 const unsigned int maxViewHeight = 600;
73 
74 class WebPage : public QWebPage {
75     Q_OBJECT
76 public:
77     WebPage(QWidget *parent, DumpRenderTree *drt);
78 
79     QWebPage *createWindow(QWebPage::WebWindowType);
80 
81     void javaScriptAlert(QWebFrame *frame, const QString& message);
82     void javaScriptConsoleMessage(const QString& message, int lineNumber, const QString& sourceID);
83     bool javaScriptConfirm(QWebFrame *frame, const QString& msg);
84     bool javaScriptPrompt(QWebFrame *frame, const QString& msg, const QString& defaultValue, QString* result);
85 
86 public slots:
shouldInterruptJavaScript()87     bool shouldInterruptJavaScript() { return false; }
88 
89 private slots:
setViewGeometry(const QRect & r)90     void setViewGeometry(const QRect &r)
91     {
92         QWidget *v = view();
93         if (v)
94             v->setGeometry(r);
95     }
96 private:
97     DumpRenderTree *m_drt;
98 };
99 
WebPage(QWidget * parent,DumpRenderTree * drt)100 WebPage::WebPage(QWidget *parent, DumpRenderTree *drt)
101     : QWebPage(parent), m_drt(drt)
102 {
103     settings()->setFontSize(QWebSettings::MinimumFontSize, 5);
104     settings()->setFontSize(QWebSettings::MinimumLogicalFontSize, 5);
105     // To get DRT compliant to some layout tests lets set the default fontsize to 13.
106     settings()->setFontSize(QWebSettings::DefaultFontSize, 13);
107     settings()->setFontSize(QWebSettings::DefaultFixedFontSize, 13);
108     settings()->setAttribute(QWebSettings::JavascriptCanOpenWindows, true);
109     settings()->setAttribute(QWebSettings::JavascriptCanAccessClipboard, true);
110     settings()->setAttribute(QWebSettings::LinksIncludedInFocusChain, false);
111     settings()->setAttribute(QWebSettings::PluginsEnabled, true);
112     connect(this, SIGNAL(geometryChangeRequested(const QRect &)),
113             this, SLOT(setViewGeometry(const QRect & )));
114 
115     setPluginFactory(new TestPlugin(this));
116 }
117 
createWindow(QWebPage::WebWindowType)118 QWebPage *WebPage::createWindow(QWebPage::WebWindowType)
119 {
120     return m_drt->createWindow();
121 }
122 
javaScriptAlert(QWebFrame *,const QString & message)123 void WebPage::javaScriptAlert(QWebFrame*, const QString& message)
124 {
125     fprintf(stdout, "ALERT: %s\n", message.toUtf8().constData());
126 }
127 
javaScriptConsoleMessage(const QString & message,int lineNumber,const QString &)128 void WebPage::javaScriptConsoleMessage(const QString& message, int lineNumber, const QString&)
129 {
130     fprintf (stdout, "CONSOLE MESSAGE: line %d: %s\n", lineNumber, message.toUtf8().constData());
131 }
132 
javaScriptConfirm(QWebFrame *,const QString & msg)133 bool WebPage::javaScriptConfirm(QWebFrame*, const QString& msg)
134 {
135     fprintf(stdout, "CONFIRM: %s\n", msg.toUtf8().constData());
136     return true;
137 }
138 
javaScriptPrompt(QWebFrame *,const QString & msg,const QString & defaultValue,QString * result)139 bool WebPage::javaScriptPrompt(QWebFrame*, const QString& msg, const QString& defaultValue, QString* result)
140 {
141     fprintf(stdout, "PROMPT: %s, default text: %s\n", msg.toUtf8().constData(), defaultValue.toUtf8().constData());
142     *result = defaultValue;
143     return true;
144 }
145 
DumpRenderTree()146 DumpRenderTree::DumpRenderTree()
147     : m_dumpPixels(false)
148     , m_stdin(0)
149     , m_notifier(0)
150 {
151     qt_drt_overwritePluginDirectories();
152 
153     m_controller = new LayoutTestController(this);
154     connect(m_controller, SIGNAL(done()), this, SLOT(dump()));
155 
156     QWebView *view = new QWebView(0);
157     view->resize(QSize(maxViewWidth, maxViewHeight));
158     m_page = new WebPage(view, this);
159     view->setPage(m_page);
160     connect(m_page, SIGNAL(frameCreated(QWebFrame *)), this, SLOT(connectFrame(QWebFrame *)));
161     connectFrame(m_page->mainFrame());
162 
163     connect(m_page->mainFrame(), SIGNAL(loadFinished(bool)), m_controller, SLOT(maybeDump(bool)));
164 
165     m_page->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);
166     m_page->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);
167     connect(m_page->mainFrame(), SIGNAL(titleChanged(const QString&)),
168             SLOT(titleChanged(const QString&)));
169     connect(m_page, SIGNAL(databaseQuotaExceeded(QWebFrame*,QString)),
170             this, SLOT(dumpDatabaseQuota(QWebFrame*,QString)));
171 
172     m_eventSender = new EventSender(m_page);
173     m_textInputController = new TextInputController(m_page);
174     m_gcController = new GCController(m_page);
175 
176     QObject::connect(this, SIGNAL(quit()), qApp, SLOT(quit()), Qt::QueuedConnection);
177     qt_drt_run(true);
178     QFocusEvent event(QEvent::FocusIn, Qt::ActiveWindowFocusReason);
179     QApplication::sendEvent(view, &event);
180 }
181 
~DumpRenderTree()182 DumpRenderTree::~DumpRenderTree()
183 {
184     delete m_page;
185 
186     delete m_stdin;
187     delete m_notifier;
188 }
189 
open()190 void DumpRenderTree::open()
191 {
192     if (!m_stdin) {
193         m_stdin = new QFile;
194         m_stdin->open(stdin, QFile::ReadOnly);
195     }
196 
197     if (!m_notifier) {
198         m_notifier = new QSocketNotifier(STDIN_FILENO, QSocketNotifier::Read);
199         connect(m_notifier, SIGNAL(activated(int)), this, SLOT(readStdin(int)));
200     }
201 }
202 
resetToConsistentStateBeforeTesting()203 void DumpRenderTree::resetToConsistentStateBeforeTesting()
204 {
205     closeRemainingWindows();
206 
207     // Reset so that any current loads are stopped
208     m_page->blockSignals(true);
209     m_page->triggerAction(QWebPage::Stop);
210     m_page->blockSignals(false);
211 
212     m_page->mainFrame()->setZoomFactor(1.0);
213     qt_drt_clearFrameName(m_page->mainFrame());
214 
215     WorkQueue::shared()->clear();
216     // Causes timeout, why?
217     //WorkQueue::shared()->setFrozen(false);
218 
219     m_controller->reset();
220 }
221 
open(const QUrl & aurl)222 void DumpRenderTree::open(const QUrl& aurl)
223 {
224     resetToConsistentStateBeforeTesting();
225 
226     QUrl url = aurl;
227     m_expectedHash = QString();
228     if (m_dumpPixels) {
229         // single quote marks the pixel dump hash
230         QString str = url.toString();
231         int i = str.indexOf('\'');
232         if (i > -1) {
233             m_expectedHash = str.mid(i + 1, str.length());
234             str.remove(i, str.length());
235             url = QUrl(str);
236         }
237     }
238 
239     // W3C SVG tests expect to be 480x360
240     bool isW3CTest = url.toString().contains("svg/W3C-SVG-1.1");
241     int width = isW3CTest ? 480 : maxViewWidth;
242     int height = isW3CTest ? 360 : maxViewHeight;
243     m_page->view()->resize(QSize(width, height));
244     m_page->setFixedContentsSize(QSize());
245     m_page->setViewportSize(QSize(width, height));
246 
247     QFocusEvent ev(QEvent::FocusIn);
248     m_page->event(&ev);
249 
250     QFontDatabase::removeAllApplicationFonts();
251 #if defined(Q_WS_X11)
252     initializeFonts();
253 #endif
254 
255     qt_dump_frame_loader(url.toString().contains("loading/"));
256     m_page->mainFrame()->load(url);
257 }
258 
readStdin(int)259 void DumpRenderTree::readStdin(int /* socket */)
260 {
261     // Read incoming data from stdin...
262     QByteArray line = m_stdin->readLine();
263     if (line.endsWith('\n'))
264         line.truncate(line.size()-1);
265     //fprintf(stderr, "\n    opening %s\n", line.constData());
266     if (line.isEmpty())
267         quit();
268 
269     if (line.startsWith("http:") || line.startsWith("https:"))
270         open(QUrl(line));
271     else {
272         QFileInfo fi(line);
273         open(QUrl::fromLocalFile(fi.absoluteFilePath()));
274     }
275 
276     fflush(stdout);
277 }
278 
setDumpPixels(bool dump)279 void DumpRenderTree::setDumpPixels(bool dump)
280 {
281     m_dumpPixels = dump;
282 }
283 
closeRemainingWindows()284 void DumpRenderTree::closeRemainingWindows()
285 {
286     foreach(QWidget *widget, windows)
287         delete widget;
288     windows.clear();
289 }
290 
initJSObjects()291 void DumpRenderTree::initJSObjects()
292 {
293     QWebFrame *frame = qobject_cast<QWebFrame*>(sender());
294     Q_ASSERT(frame);
295     frame->addToJavaScriptWindowObject(QLatin1String("layoutTestController"), m_controller);
296     frame->addToJavaScriptWindowObject(QLatin1String("eventSender"), m_eventSender);
297     frame->addToJavaScriptWindowObject(QLatin1String("textInputController"), m_textInputController);
298     frame->addToJavaScriptWindowObject(QLatin1String("GCController"), m_gcController);
299 }
300 
301 
dumpFramesAsText(QWebFrame * frame)302 QString DumpRenderTree::dumpFramesAsText(QWebFrame* frame)
303 {
304     if (!frame)
305         return QString();
306 
307     QString result;
308     QWebFrame *parent = qobject_cast<QWebFrame *>(frame->parent());
309     if (parent) {
310         result.append(QLatin1String("\n--------\nFrame: '"));
311         result.append(frame->frameName());
312         result.append(QLatin1String("'\n--------\n"));
313     }
314 
315     result.append(frame->toPlainText());
316     result.append(QLatin1String("\n"));
317 
318     if (m_controller->shouldDumpChildrenAsText()) {
319         QList<QWebFrame *> children = frame->childFrames();
320         for (int i = 0; i < children.size(); ++i)
321             result += dumpFramesAsText(children.at(i));
322     }
323 
324     return result;
325 }
326 
dumpBackForwardList()327 QString DumpRenderTree::dumpBackForwardList()
328 {
329     QString result;
330     result.append(QLatin1String("\n============== Back Forward List ==============\n"));
331     result.append(QLatin1String("FIXME: Unimplemented!\n"));
332     result.append(QLatin1String("===============================================\n"));
333     return result;
334 }
335 
methodNameStringForFailedTest(LayoutTestController * controller)336 static const char *methodNameStringForFailedTest(LayoutTestController *controller)
337 {
338     const char *errorMessage;
339     if (controller->shouldDumpAsText())
340         errorMessage = "[documentElement innerText]";
341     // FIXME: Add when we have support
342     //else if (controller->dumpDOMAsWebArchive())
343     //    errorMessage = "[[mainFrame DOMDocument] webArchive]";
344     //else if (controller->dumpSourceAsWebArchive())
345     //    errorMessage = "[[mainFrame dataSource] webArchive]";
346     else
347         errorMessage = "[mainFrame renderTreeAsExternalRepresentation]";
348 
349     return errorMessage;
350 }
351 
dump()352 void DumpRenderTree::dump()
353 {
354     QWebFrame *mainFrame = m_page->mainFrame();
355 
356     //fprintf(stderr, "    Dumping\n");
357     if (!m_notifier) {
358         // Dump markup in single file mode...
359         QString markup = mainFrame->toHtml();
360         fprintf(stdout, "Source:\n\n%s\n", markup.toUtf8().constData());
361     }
362 
363     // Dump render text...
364     QString resultString;
365     if (m_controller->shouldDumpAsText())
366         resultString = dumpFramesAsText(mainFrame);
367     else
368         resultString = mainFrame->renderTreeDump();
369 
370     if (!resultString.isEmpty()) {
371         fprintf(stdout, "%s", resultString.toUtf8().constData());
372 
373         if (m_controller->shouldDumpBackForwardList())
374             fprintf(stdout, "%s", dumpBackForwardList().toUtf8().constData());
375 
376     } else
377         printf("ERROR: nil result from %s", methodNameStringForFailedTest(m_controller));
378 
379     // signal end of text block
380     fputs("#EOF\n", stdout);
381     fputs("#EOF\n", stderr);
382 
383     if (m_dumpPixels) {
384         QImage image(m_page->viewportSize(), QImage::Format_ARGB32);
385         image.fill(Qt::white);
386         QPainter painter(&image);
387         mainFrame->render(&painter);
388         painter.end();
389 
390         QCryptographicHash hash(QCryptographicHash::Md5);
391         for (int row = 0; row < image.height(); ++row)
392             hash.addData(reinterpret_cast<const char*>(image.scanLine(row)), image.width() * 4);
393         QString actualHash = hash.result().toHex();
394 
395         fprintf(stdout, "\nActualHash: %s\n", qPrintable(actualHash));
396 
397         bool dumpImage = true;
398 
399         if (!m_expectedHash.isEmpty()) {
400             Q_ASSERT(m_expectedHash.length() == 32);
401             fprintf(stdout, "\nExpectedHash: %s\n", qPrintable(m_expectedHash));
402 
403             if (m_expectedHash == actualHash)
404                 dumpImage = false;
405         }
406 
407         if (dumpImage) {
408             QBuffer buffer;
409             buffer.open(QBuffer::WriteOnly);
410             image.save(&buffer, "PNG");
411             buffer.close();
412             const QByteArray &data = buffer.data();
413 
414             printf("Content-Type: %s\n", "image/png");
415             printf("Content-Length: %lu\n", static_cast<unsigned long>(data.length()));
416 
417             const char *ptr = data.data();
418             for(quint32 left = data.length(); left; ) {
419                 quint32 block = qMin(left, quint32(1 << 15));
420                 quint32 written = fwrite(ptr, 1, block, stdout);
421                 ptr += written;
422                 left -= written;
423                 if (written == block)
424                     break;
425             }
426         }
427 
428         fflush(stdout);
429     }
430 
431     puts("#EOF");   // terminate the (possibly empty) pixels block
432 
433     fflush(stdout);
434     fflush(stderr);
435 
436     if (!m_notifier)
437         quit(); // Exit now in single file mode...
438 }
439 
titleChanged(const QString & s)440 void DumpRenderTree::titleChanged(const QString &s)
441 {
442     if (m_controller->shouldDumpTitleChanges())
443         printf("TITLE CHANGED: %s\n", s.toUtf8().data());
444 }
445 
connectFrame(QWebFrame * frame)446 void DumpRenderTree::connectFrame(QWebFrame *frame)
447 {
448     connect(frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(initJSObjects()));
449     connect(frame, SIGNAL(provisionalLoad()),
450             layoutTestController(), SLOT(provisionalLoad()));
451 }
452 
dumpDatabaseQuota(QWebFrame * frame,const QString & dbName)453 void DumpRenderTree::dumpDatabaseQuota(QWebFrame* frame, const QString& dbName)
454 {
455     if (!m_controller->shouldDumpDatabaseCallbacks())
456         return;
457     QWebSecurityOrigin origin = frame->securityOrigin();
458     printf("UI DELEGATE DATABASE CALLBACK: exceededDatabaseQuotaForSecurityOrigin:{%s, %s, %i} database:%s\n",
459            origin.scheme().toUtf8().data(),
460            origin.host().toUtf8().data(),
461            origin.port(),
462            dbName.toUtf8().data());
463     origin.setDatabaseQuota(5 * 1024 * 1024);
464 }
465 
createWindow()466 QWebPage *DumpRenderTree::createWindow()
467 {
468     if (!m_controller->canOpenWindows())
469         return 0;
470     QWidget *container = new QWidget(0);
471     container->resize(0, 0);
472     container->move(-1, -1);
473     container->hide();
474     QWebPage *page = new WebPage(container, this);
475     connectFrame(page->mainFrame());
476     connect(m_page, SIGNAL(frameCreated(QWebFrame *)), this, SLOT(connectFrame(QWebFrame *)));
477     windows.append(container);
478     return page;
479 }
480 
windowCount() const481 int DumpRenderTree::windowCount() const
482 {
483     int count = 0;
484     foreach(QWidget *w, windows) {
485         if (w->children().count())
486             ++count;
487     }
488     return count + 1;
489 }
490 
491 #if defined(Q_WS_X11)
initializeFonts()492 void DumpRenderTree::initializeFonts()
493 {
494     static int numFonts = -1;
495 
496     // Some test cases may add or remove application fonts (via @font-face).
497     // Make sure to re-initialize the font set if necessary.
498     FcFontSet* appFontSet = FcConfigGetFonts(0, FcSetApplication);
499     if (appFontSet && numFonts >= 0 && appFontSet->nfont == numFonts)
500         return;
501 
502     QByteArray fontDir = getenv("WEBKIT_TESTFONTS");
503     if (fontDir.isEmpty() || !QDir(fontDir).exists()) {
504         fprintf(stderr,
505                 "\n\n"
506                 "----------------------------------------------------------------------\n"
507                 "WEBKIT_TESTFONTS environment variable is not set correctly.\n"
508                 "This variable has to point to the directory containing the fonts\n"
509                 "you can clone from git://gitorious.org/qtwebkit/testfonts.git\n"
510                 "----------------------------------------------------------------------\n"
511                );
512         exit(1);
513     }
514     char currentPath[PATH_MAX+1];
515     getcwd(currentPath, PATH_MAX);
516     QByteArray configFile = currentPath;
517     FcConfig *config = FcConfigCreate();
518     configFile += "/WebKitTools/DumpRenderTree/qt/fonts.conf";
519     if (!FcConfigParseAndLoad (config, (FcChar8*) configFile.data(), true))
520         qFatal("Couldn't load font configuration file");
521     if (!FcConfigAppFontAddDir (config, (FcChar8*) fontDir.data()))
522         qFatal("Couldn't add font dir!");
523     FcConfigSetCurrent(config);
524 
525     appFontSet = FcConfigGetFonts(config, FcSetApplication);
526     numFonts = appFontSet->nfont;
527 }
528 #endif
529 
530 }
531 
532 #include "DumpRenderTree.moc"
533