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