1 /* 2 * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.webkit.drt; 27 28 import com.sun.javafx.application.PlatformImpl; 29 import com.sun.javafx.webkit.Accessor; 30 import com.sun.javafx.webkit.ThemeClientImpl; 31 import com.sun.webkit.*; 32 import com.sun.webkit.graphics.*; 33 34 import static com.sun.webkit.network.URLs.newURL; 35 import java.io.BufferedReader; 36 import java.io.BufferedWriter; 37 import java.io.File; 38 import java.io.InputStreamReader; 39 import java.io.OutputStreamWriter; 40 import java.io.PrintWriter; 41 import java.io.UnsupportedEncodingException; 42 import java.net.MalformedURLException; 43 import java.net.URL; 44 import java.nio.ByteBuffer; 45 import java.util.Date; 46 import java.util.List; 47 import java.util.concurrent.CountDownLatch; 48 import java.util.logging.FileHandler; 49 import java.util.logging.Formatter; 50 import java.util.logging.Level; 51 import java.util.logging.LogRecord; 52 import java.util.logging.Logger; 53 import javafx.scene.web.WebEngine; 54 55 public final class DumpRenderTree { 56 private final static Logger log = Logger.getLogger("DumpRenderTree"); 57 private final static long PID = (new Date()).getTime() & 0xFFFF; 58 private final static String fileSep = System.getProperty("file.separator"); 59 private static boolean forceDumpAsText = false; 60 61 final static PrintWriter out; 62 static { 63 try { 64 out = new PrintWriter(new BufferedWriter(new OutputStreamWriter( 65 System.out, "UTF-8")), true); 66 } catch (UnsupportedEncodingException ex) { 67 throw new RuntimeException(ex); 68 } 69 } 70 static volatile DumpRenderTree drt; 71 72 private final WebPage webPage; 73 private final UIClientImpl uiClient; 74 private final EventSender eventSender; 75 76 private CountDownLatch latch; 77 private String testPath; 78 private boolean loaded; 79 private boolean waiting; 80 private boolean complete; 81 82 class ThemeClientImplStub extends ThemeClient { 83 @Override 84 protected RenderTheme createRenderTheme() { 85 return new RenderThemeStub(); 86 } 87 88 @Override 89 protected ScrollBarTheme createScrollBarTheme() { 90 return new ScrollBarThemeStub(); 91 } 92 }; 93 94 class RenderThemeStub extends RenderTheme { 95 @Override 96 protected Ref createWidget(long id, int widgetIndex, int state, int w, int h, int bgColor, ByteBuffer extParams) { 97 return null; 98 } 99 100 @Override 101 public void drawWidget(WCGraphicsContext g, Ref widget, int x, int y) { 102 } 103 104 @Override 105 protected int getRadioButtonSize() { 106 return 0; 107 } 108 109 @Override 110 protected int getSelectionColor(int index) { 111 return 0; 112 } 113 114 @Override 115 public WCSize getWidgetSize(Ref widget) { 116 return new WCSize(0, 0); 117 } 118 } 119 120 class ScrollBarThemeStub extends ScrollBarTheme { 121 @Override 122 protected Ref createWidget(long id, int w, int h, int orientation, int value, int visibleSize, int totalSize) { 123 return null; 124 } 125 126 @Override 127 protected void getScrollBarPartRect(long id, int part, int rect[]) {} 128 129 @Override 130 public void paint(WCGraphicsContext g, Ref sbRef, int x, int y, int pressedPart, int hoveredPart) { 131 } 132 133 @Override 134 public WCSize getWidgetSize(Ref widget) { 135 return new WCSize(0, 0); 136 } 137 } 138 139 // called on FX thread 140 private DumpRenderTree() { 141 uiClient = new UIClientImpl(); 142 webPage = new WebPage(new WebPageClientImpl(), uiClient, null, null, 143 new ThemeClientImplStub(), false); 144 uiClient.setWebPage(webPage); 145 eventSender = new EventSender(webPage); 146 147 webPage.setBounds(0, 0, 800, 600); 148 webPage.setUsePageCache(true); 149 webPage.setDeveloperExtrasEnabled(true); 150 webPage.addLoadListenerClient(new DRTLoadListener()); 151 152 } 153 154 private String getTestPath(String testString) { 155 int t = testString.indexOf("'"); 156 String pixelsHash = ""; 157 if ((t > 0) && (t < testString.length() - 1)) { 158 pixelsHash = testString.substring(t + 1); 159 testString = testString.substring(0, t); 160 } 161 this.testPath = testString; 162 init(testString, pixelsHash); 163 return testString; 164 } 165 166 167 private static boolean isDebug() 168 { 169 return log.isLoggable(Level.FINE); 170 } 171 172 private static void mlog(String msg) 173 { 174 if (isDebug()) { 175 log.fine("PID:" + Long.toHexString(PID) 176 + " TID:" + Thread.currentThread().getId() 177 + "(" + Thread.currentThread().getName() + ") " 178 + msg); 179 } 180 } 181 182 private static void initPlatform() throws Exception { 183 // initialize default toolkit 184 final CountDownLatch latch = new CountDownLatch(1); 185 PlatformImpl.startup(() -> { 186 new WebEngine(); // initialize Webkit classes 187 System.loadLibrary("DumpRenderTreeJava"); 188 PageCache.setCapacity(1); 189 drt = new DumpRenderTree(); 190 latch.countDown(); 191 }); 192 // wait for libraries to load 193 latch.await(); 194 } 195 196 private void reset() { 197 mlog("reset"); 198 // Reset native objects associated with WebPage 199 webPage.resetToConsistentStateBeforeTesting(); 200 // Reset zoom factors 201 webPage.setZoomFactor(1.0f, true); 202 webPage.setZoomFactor(1.0f, false); 203 // Reset DRT internal states 204 complete = false; 205 loaded = false; 206 waiting = false; 207 } 208 209 // called on FX thread 210 private void run(final String testString, final CountDownLatch latch) { 211 this.latch = latch; 212 String file = getTestPath(testString); 213 mlog("{runTest: " + file); 214 long mainFrame = webPage.getMainFrame(); 215 try { 216 new URL(file); 217 } catch (MalformedURLException ex) { 218 file = "file:///" + file; 219 } 220 reset(); 221 webPage.open(mainFrame, file); 222 mlog("}runTest"); 223 } 224 225 private void runTest(final String testString) throws Exception { 226 final CountDownLatch l = new CountDownLatch(1); 227 Invoker.getInvoker().invokeOnEventThread(() -> { 228 run(testString, l); 229 }); 230 // wait until test is finished 231 l.await(); 232 Invoker.getInvoker().invokeOnEventThread(() -> { 233 mlog("dispose"); 234 // drt.uiClient.closePage(); 235 dispose(); 236 }); 237 } 238 239 // called from native 240 private static void waitUntilDone() { 241 mlog("waitUntilDone"); 242 drt.setWaiting(true); // TODO: handle timeout 243 } 244 245 // called from native 246 private static void notifyDone() { 247 mlog("notifyDone"); 248 drt.setWaiting(false); 249 } 250 251 private static void overridePreference(String key, String value) { 252 mlog("overridePreference"); 253 drt.webPage.overridePreference(key, value); 254 } 255 256 private synchronized void setLoaded(boolean loaded) { 257 this.loaded = loaded; 258 done(); 259 } 260 261 private synchronized void setWaiting(boolean waiting) { 262 this.waiting = waiting; 263 done(); 264 } 265 266 private synchronized void dump(long frame) { 267 boolean dumpAsText = dumpAsText() || forceDumpAsText; 268 mlog("dumpAsText = " + dumpAsText); 269 if (dumpAsText) { 270 String innerText = webPage.getInnerText(frame); 271 if (frame == webPage.getMainFrame()) { 272 if (innerText != null) { 273 // don't use println() here as it varies from platform 274 // to platform, but DRT expects it always to be 0x0A 275 out.print(innerText + '\n'); 276 } 277 } else { 278 out.printf("\n--------\nFrame: '%s'\n--------\n%s\n", 279 webPage.getName(frame), innerText); 280 } 281 if (dumpChildFramesAsText()) { 282 List<Long> children = webPage.getChildFrames(frame); 283 if (children != null) { 284 for (long child : children) { 285 dump(child); 286 } 287 } 288 } 289 if (dumpBackForwardList() && frame == webPage.getMainFrame()) { 290 drt.dumpBfl(); 291 } 292 } else { 293 String renderTree = webPage.getRenderTree(frame); 294 out.print(renderTree); 295 } 296 } 297 298 private synchronized void done() { 299 if (waiting || !loaded || complete) { 300 return; 301 } 302 mlog("dump"); 303 dump(webPage.getMainFrame()); 304 305 mlog("done"); 306 out.print("#EOF" + '\n'); 307 // TODO: dump pixels here 308 out.print("#EOF" + '\n'); 309 out.flush(); 310 311 System.err.print("#EOF" + '\n'); 312 System.err.flush(); 313 314 complete = true; 315 // notify main thread that test is finished 316 this.latch.countDown(); 317 } 318 319 private static native void init(String testPath, String pixelsHash); 320 private static native void didClearWindowObject(long pContext, 321 long pWindowObject, EventSender eventSender); 322 private static native void dispose(); 323 324 private static native boolean dumpAsText(); 325 private static native boolean dumpChildFramesAsText(); 326 private static native boolean dumpBackForwardList(); 327 protected static native boolean shouldStayOnPageAfterHandlingBeforeUnload(); 328 329 private final class DRTLoadListener implements LoadListenerClient { 330 @Override 331 public void dispatchLoadEvent(long frame, int state, 332 String url, String contentType, 333 double progress, int errorCode) 334 { 335 mlog("dispatchLoadEvent: ENTER"); 336 if (frame == webPage.getMainFrame()) { 337 mlog("dispatchLoadEvent: STATE = " + state); 338 switch (state) { 339 case PAGE_STARTED: 340 mlog("PAGE_STARTED"); 341 setLoaded(false); 342 break; 343 case PAGE_FINISHED: 344 mlog("PAGE_FINISHED"); 345 if (didFinishLoad()) { 346 setLoaded(true); 347 } 348 break; 349 case DOCUMENT_AVAILABLE: 350 dumpUnloadListeners(webPage, frame); 351 break; 352 case LOAD_FAILED: 353 mlog("LOAD_FAILED"); 354 // safety net: if load fails, e.g. command line 355 // parameters were bad, let's not hang forever 356 setLoaded(true); 357 break; 358 } 359 } 360 mlog("dispatchLoadEvent: EXIT"); 361 } 362 @Override 363 public void dispatchResourceLoadEvent(long frame, int state, 364 String url, String contentType, 365 double progress, int errorCode) 366 { 367 } 368 } 369 370 371 public static void main(final String[] args) throws Exception { 372 if ( isDebug() ) { 373 log.setLevel(Level.FINEST); 374 FileHandler handler = new FileHandler("drt.log", true); 375 handler.setFormatter(new Formatter() { 376 @Override 377 public String format(LogRecord record) { 378 return formatMessage(record) + "\n"; 379 } 380 }); 381 log.addHandler(handler); 382 } 383 384 mlog("{main"); 385 initPlatform(); 386 assert drt != null; 387 for (String arg: args) { 388 if ("--dump-as-text".equals(arg)) { 389 forceDumpAsText = true; 390 } else if ("-".equals(arg)) { 391 // read from stdin 392 BufferedReader in = new BufferedReader( 393 new InputStreamReader(System.in)); 394 String testPath; 395 while ((testPath = in.readLine()) != null) { 396 drt.runTest(testPath); 397 } 398 in.close(); 399 } else { 400 drt.runTest(arg); 401 } 402 } 403 PlatformImpl.exit(); 404 mlog("}main"); 405 System.exit(0); // workaround to kill media threads 406 } 407 408 // called from native 409 private static int getWorkerThreadCount() { 410 return drt.webPage.getWorkerThreadCount(); 411 } 412 413 // called from native 414 private static String resolveURL(String relativeURL) { 415 String testDir = new File(drt.testPath).getParentFile().getPath(); 416 File f = new File(testDir, relativeURL); 417 String url = "file:///" + f.toString().replace(fileSep, "/"); 418 mlog("resolveURL: " + url); 419 return url; 420 } 421 422 // called from native 423 private static void loadURL(String url) { 424 drt.webPage.open(drt.webPage.getMainFrame(), url); 425 } 426 427 // called from native 428 private static void goBackForward(int dist) { 429 // TODO: honor the dist 430 if (dist > 0) { 431 drt.webPage.goForward(); 432 } else { 433 drt.webPage.goBack(); 434 } 435 } 436 437 // called from native 438 private static int getBackForwardItemCount() { 439 return drt.getBackForwardList().size(); 440 } 441 442 private static final String TEST_DIR_NAME = "LayoutTests"; 443 private static final int TEST_DIR_LEN = TEST_DIR_NAME.length(); 444 private static final String CUR_ITEM_STR = "curr->"; 445 private static final int CUR_ITEM_STR_LEN = CUR_ITEM_STR.length(); 446 private static final String INDENT = " "; 447 448 private BackForwardList bfl; 449 private BackForwardList getBackForwardList() { 450 if (bfl == null) { 451 bfl = webPage.createBackForwardList(); 452 } 453 return bfl; 454 } 455 456 private void dumpBfl() { 457 out.print("\n============== Back Forward List ==============\n"); 458 getBackForwardList(); 459 BackForwardList.Entry curItem = bfl.getCurrentEntry(); 460 for (BackForwardList.Entry e: bfl.toArray()) { 461 dumpBflItem(e, 2, e == curItem); 462 } 463 out.print("===============================================\n"); 464 } 465 466 private void dumpBflItem(BackForwardList.Entry item, int indent, boolean isCurrent) { 467 StringBuilder str = new StringBuilder(); 468 for (int i = indent; i > 0; i--) str.append(INDENT); 469 470 if (isCurrent) str.replace(0, CUR_ITEM_STR_LEN, CUR_ITEM_STR); 471 472 String url = item.getURL().toString(); 473 if (url.contains("file:/")) { 474 String subUrl = url.substring(url.indexOf(TEST_DIR_NAME) + TEST_DIR_LEN + 1); 475 str.append("(file test):" + subUrl); 476 } else { 477 str.append(url); 478 } 479 if (item.getTarget() != null) { 480 str.append(" (in frame \"" + item.getTarget() + "\")"); 481 } 482 if (item.isTargetItem()) { 483 str.append(" **nav target**\n"); 484 } else { 485 str.append("\n"); 486 } 487 out.print(str); 488 if (item.getChildren() != null) 489 for (BackForwardList.Entry child: item.getChildren()) 490 dumpBflItem(child, indent + 1, false); 491 } 492 493 void dumpUnloadListeners(WebPage page, long frame) { 494 if (waiting == true && dumpAsText()) { 495 String dump = getUnloadListenersDescription(page, frame); 496 if (dump != null) { 497 out.print(dump + '\n'); 498 } 499 } 500 } 501 502 private static String getUnloadListenersDescription(WebPage page, long frame) { 503 int count = page.getUnloadEventListenersCount(frame); 504 if (count > 0) { 505 return getFrameDescription(page, frame) + 506 " - has " + count + " onunload handler(s)"; 507 } 508 return null; 509 } 510 511 private static String getFrameDescription(WebPage page, long frame) { 512 String name = page.getName(frame); 513 if (frame == page.getMainFrame()) { 514 return name == null ? "main frame" : "main frame " + name; 515 } 516 return name == null ? "frame (anonymous)" : "frame " + name; 517 } 518 519 private native static boolean didFinishLoad(); 520 521 private final class WebPageClientImpl implements WebPageClient<Void> { 522 523 @Override 524 public void setCursor(long cursorID) { 525 } 526 527 @Override 528 public void setFocus(boolean focus) { 529 } 530 531 @Override 532 public void transferFocus(boolean forward) { 533 } 534 535 @Override 536 public void setTooltip(String tooltip) { 537 } 538 539 @Override 540 public WCRectangle getScreenBounds(boolean available) { 541 return null; 542 } 543 544 @Override 545 public int getScreenDepth() { 546 return 24; 547 } 548 549 @Override 550 public Void getContainer() { 551 return null; 552 } 553 554 @Override 555 public WCPoint screenToWindow(WCPoint ptScreen) { 556 return ptScreen; 557 } 558 559 @Override 560 public WCPoint windowToScreen(WCPoint ptWindow) { 561 return ptWindow; 562 } 563 564 @Override 565 public WCPageBackBuffer createBackBuffer() { 566 throw new UnsupportedOperationException(); 567 } 568 569 @Override 570 public boolean isBackBufferSupported() { 571 return false; 572 } 573 574 @Override 575 public void addMessageToConsole(String message, int lineNumber, 576 String sourceId) 577 { 578 if (!message.isEmpty()) { 579 int pos = message.indexOf("file://"); 580 if (pos != -1) { 581 String s1 = message.substring(0, pos); 582 String s2 = message.substring(pos); 583 try { 584 // Extract the last path component aka file name 585 s2 = new File(newURL(s2).getPath()).getName(); 586 } catch (MalformedURLException ignore) {} 587 message = s1 + s2; 588 } 589 } 590 if (lineNumber == 0) { 591 out.printf("CONSOLE MESSAGE: %s\n", message); 592 } else { 593 out.printf("CONSOLE MESSAGE: line %d: %s\n", 594 lineNumber, message); 595 } 596 } 597 598 @Override 599 public void didClearWindowObject(long context, long windowObject) { 600 mlog("didClearWindowObject"); 601 DumpRenderTree.didClearWindowObject(context, windowObject, 602 eventSender); 603 } 604 } 605 }