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.logging.PlatformLogger; 30 import com.sun.javafx.logging.PlatformLogger.Level; 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 javafx.scene.web.WebEngine; 49 50 public final class DumpRenderTree { 51 private final static PlatformLogger log = PlatformLogger.getLogger("DumpRenderTree"); 52 private final static long PID = (new Date()).getTime() & 0xFFFF; 53 private final static String fileSep = System.getProperty("file.separator"); 54 private static boolean forceDumpAsText = false; 55 56 final static PrintWriter out; 57 static { 58 try { 59 out = new PrintWriter(new BufferedWriter(new OutputStreamWriter( 60 System.out, "UTF-8")), true); 61 } catch (UnsupportedEncodingException ex) { 62 throw new RuntimeException(ex); 63 } 64 } 65 static volatile DumpRenderTree drt; 66 67 private final WebPage webPage; 68 private final UIClientImpl uiClient; 69 private final EventSender eventSender; 70 71 private CountDownLatch latch; 72 private String testPath; 73 private boolean loaded; 74 private boolean waiting; 75 private boolean complete; 76 77 static class ThemeClientImplStub extends ThemeClient { 78 @Override 79 protected RenderTheme createRenderTheme() { 80 return new RenderThemeStub(); 81 } 82 83 @Override 84 protected ScrollBarTheme createScrollBarTheme() { 85 return new ScrollBarThemeStub(); 86 } 87 }; 88 89 static class RenderThemeStub extends RenderTheme { 90 @Override 91 protected Ref createWidget(long id, int widgetIndex, int state, int w, int h, int bgColor, ByteBuffer extParams) { 92 return null; 93 } 94 95 @Override 96 public void drawWidget(WCGraphicsContext g, Ref widget, int x, int y) { 97 } 98 99 @Override 100 protected int getRadioButtonSize() { 101 return 0; 102 } 103 104 @Override 105 protected int getSelectionColor(int index) { 106 return 0; 107 } 108 109 @Override 110 public WCSize getWidgetSize(Ref widget) { 111 return new WCSize(0, 0); 112 } 113 } 114 115 static class ScrollBarThemeStub extends ScrollBarTheme { 116 @Override 117 protected Ref createWidget(long id, int w, int h, int orientation, int value, int visibleSize, int totalSize) { 118 return null; 119 } 120 121 @Override 122 protected void getScrollBarPartRect(long id, int part, int rect[]) {} 123 124 @Override 125 public void paint(WCGraphicsContext g, Ref sbRef, int x, int y, int pressedPart, int hoveredPart) { 126 } 127 128 @Override 129 public WCSize getWidgetSize(Ref widget) { 130 return new WCSize(0, 0); 131 } 132 } 133 134 // called on FX thread 135 private DumpRenderTree() { 136 uiClient = new UIClientImpl(); 137 webPage = new WebPage(new WebPageClientImpl(), uiClient, null, null, 138 new ThemeClientImplStub(), false); 139 uiClient.setWebPage(webPage); 140 eventSender = new EventSender(webPage); 141 142 webPage.setBounds(0, 0, 800, 600); 143 webPage.setDeveloperExtrasEnabled(true); 144 webPage.addLoadListenerClient(new DRTLoadListener()); 145 146 } 147 148 private String getTestPath(String testString) { 149 int t = testString.indexOf("'"); 150 String pixelsHash = ""; 151 if ((t > 0) && (t < testString.length() - 1)) { 152 pixelsHash = testString.substring(t + 1); 153 testString = testString.substring(0, t); 154 } 155 this.testPath = testString; 156 init(testString, pixelsHash); 157 return testString; 158 } 159 160 /* 161 private static boolean isDebug() 162 { 163 return log.isLoggable(Level.FINE); 164 } 165 */ 166 167 private static void mlog(String msg) { 168 if (log.isLoggable(Level.FINE)) { 169 log.fine("PID:" + Long.toHexString(PID) 170 + " TID:" + Thread.currentThread().getId() 171 + "(" + Thread.currentThread().getName() + ") " 172 + msg); 173 } 174 } 175 176 private static void initPlatform() throws Exception { 177 // initialize default toolkit 178 final CountDownLatch latch = new CountDownLatch(1); 179 PlatformImpl.startup(() -> { 180 new WebEngine(); // initialize Webkit classes 181 System.loadLibrary("DumpRenderTreeJava"); 182 drt = new DumpRenderTree(); 183 PageCache.setCapacity(1); 184 latch.countDown(); 185 }); 186 // wait for libraries to load 187 latch.await(); 188 } 189 190 boolean complete() { return this.complete; } 191 192 private void reset() { 193 mlog("reset"); 194 // Reset native objects associated with WebPage 195 webPage.resetToConsistentStateBeforeTesting(); 196 // Clear frame name 197 webPage.reset(webPage.getMainFrame()); 198 // Reset zoom factors 199 webPage.setZoomFactor(1.0f, true); 200 webPage.setZoomFactor(1.0f, false); 201 // Reset DRT internal states 202 complete = false; 203 loaded = false; 204 waiting = false; 205 } 206 207 // called on FX thread 208 private void run(final String testString, final CountDownLatch latch) { 209 this.latch = latch; 210 String file = getTestPath(testString); 211 mlog("{runTest: " + file); 212 long mainFrame = webPage.getMainFrame(); 213 try { 214 new URL(file); 215 } catch (MalformedURLException ex) { 216 file = "file:///" + file; 217 } 218 reset(); 219 webPage.open(mainFrame, file); 220 mlog("}runTest"); 221 } 222 223 private void runTest(final String testString) throws Exception { 224 final CountDownLatch l = new CountDownLatch(1); 225 Invoker.getInvoker().invokeOnEventThread(() -> { 226 run(testString, l); 227 }); 228 // wait until test is finished 229 l.await(); 230 Invoker.getInvoker().invokeOnEventThread(() -> { 231 mlog("dispose"); 232 webPage.stop(); 233 dispose(); 234 }); 235 } 236 237 // called from native 238 private static void waitUntilDone() { 239 mlog("waitUntilDone"); 240 drt.setWaiting(true); // TODO: handle timeout 241 } 242 243 // called from native 244 private static void notifyDone() { 245 mlog("notifyDone"); 246 drt.setWaiting(false); 247 } 248 249 private static void overridePreference(String key, String value) { 250 mlog("overridePreference"); 251 drt.webPage.overridePreference(key, value); 252 } 253 254 private synchronized void setLoaded(boolean loaded) { 255 this.loaded = loaded; 256 done(); 257 } 258 259 private synchronized void setWaiting(boolean waiting) { 260 this.waiting = waiting; 261 done(); 262 } 263 264 private synchronized void dump(long frame) { 265 boolean dumpAsText = dumpAsText() || forceDumpAsText; 266 mlog("dumpAsText = " + dumpAsText); 267 if (dumpAsText) { 268 String innerText = webPage.getInnerText(frame); 269 if (frame == webPage.getMainFrame()) { 270 if (innerText != null) { 271 // don't use println() here as it varies from platform 272 // to platform, but DRT expects it always to be 0x0A 273 out.print(innerText + '\n'); 274 } 275 } else { 276 out.printf("\n--------\nFrame: '%s'\n--------\n%s\n", 277 webPage.getName(frame), innerText); 278 } 279 if (dumpChildFramesAsText()) { 280 List<Long> children = webPage.getChildFrames(frame); 281 if (children != null) { 282 for (long child : children) { 283 dump(child); 284 } 285 } 286 } 287 if (dumpBackForwardList() && frame == webPage.getMainFrame()) { 288 drt.dumpBfl(); 289 } 290 } else { 291 String renderTree = webPage.getRenderTree(frame); 292 out.print(renderTree); 293 } 294 } 295 296 private synchronized void done() { 297 if (waiting || !loaded || complete) { 298 return; 299 } 300 mlog("dump"); 301 dump(webPage.getMainFrame()); 302 303 mlog("done"); 304 out.print("#EOF" + '\n'); 305 // TODO: dump pixels here 306 out.print("#EOF" + '\n'); 307 out.flush(); 308 309 System.err.print("#EOF" + '\n'); 310 System.err.flush(); 311 312 complete = true; 313 // notify main thread that test is finished 314 this.latch.countDown(); 315 } 316 317 private static native void init(String testPath, String pixelsHash); 318 private static native void didClearWindowObject(long pContext, 319 long pWindowObject, EventSender eventSender); 320 private static native void dispose(); 321 322 private static native boolean dumpAsText(); 323 private static native boolean dumpChildFramesAsText(); 324 private static native boolean dumpBackForwardList(); 325 protected static native boolean shouldStayOnPageAfterHandlingBeforeUnload(); 326 327 private final class DRTLoadListener implements LoadListenerClient { 328 @Override 329 public void dispatchLoadEvent(long frame, int state, 330 String url, String contentType, 331 double progress, int errorCode) 332 { 333 mlog("dispatchLoadEvent: ENTER"); 334 if (frame == webPage.getMainFrame()) { 335 mlog("dispatchLoadEvent: STATE = " + state); 336 switch (state) { 337 case PAGE_STARTED: 338 mlog("PAGE_STARTED"); 339 setLoaded(false); 340 break; 341 case PAGE_FINISHED: 342 mlog("PAGE_FINISHED"); 343 if (didFinishLoad()) { 344 setLoaded(true); 345 } 346 break; 347 case DOCUMENT_AVAILABLE: 348 dumpUnloadListeners(webPage, frame); 349 break; 350 case LOAD_FAILED: 351 mlog("LOAD_FAILED"); 352 // safety net: if load fails, e.g. command line 353 // parameters were bad, let's not hang forever 354 setLoaded(true); 355 break; 356 } 357 } 358 mlog("dispatchLoadEvent: EXIT"); 359 } 360 @Override 361 public void dispatchResourceLoadEvent(long frame, int state, 362 String url, String contentType, 363 double progress, int errorCode) 364 { 365 } 366 } 367 368 369 public static void main(final String[] args) throws Exception { 370 /* 371 if ( isDebug() ) { 372 // 'log' here is from java.util.logging 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 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 // called from native 443 private static void clearBackForwardList() { 444 drt.getBackForwardList().clearBackForwardListForDRT(); 445 } 446 447 private static final String TEST_DIR_NAME = "LayoutTests"; 448 private static final int TEST_DIR_LEN = TEST_DIR_NAME.length(); 449 private static final String CUR_ITEM_STR = "curr->"; 450 private static final int CUR_ITEM_STR_LEN = CUR_ITEM_STR.length(); 451 private static final String INDENT = " "; 452 453 private BackForwardList bfl; 454 private BackForwardList getBackForwardList() { 455 if (bfl == null) { 456 bfl = webPage.createBackForwardList(); 457 } 458 return bfl; 459 } 460 461 private void dumpBfl() { 462 out.print("\n============== Back Forward List ==============\n"); 463 getBackForwardList(); 464 BackForwardList.Entry curItem = bfl.getCurrentEntry(); 465 for (BackForwardList.Entry e: bfl.toArray()) { 466 dumpBflItem(e, 2, e == curItem); 467 } 468 out.print("===============================================\n"); 469 } 470 471 private void dumpBflItem(BackForwardList.Entry item, int indent, boolean isCurrent) { 472 StringBuilder str = new StringBuilder(); 473 for (int i = indent; i > 0; i--) str.append(INDENT); 474 475 if (isCurrent) str.replace(0, CUR_ITEM_STR_LEN, CUR_ITEM_STR); 476 477 String url = item.getURL().toString(); 478 if (url.contains("file:/")) { 479 String subUrl = url.substring(url.indexOf(TEST_DIR_NAME) + TEST_DIR_LEN + 1); 480 str.append("(file test):" + subUrl); 481 } else { 482 str.append(url); 483 } 484 if (item.getTarget() != null) { 485 str.append(" (in frame \"" + item.getTarget() + "\")"); 486 } 487 if (item.isTargetItem()) { 488 str.append(" **nav target**\n"); 489 } else { 490 str.append("\n"); 491 } 492 out.print(str); 493 if (item.getChildren() != null) 494 for (BackForwardList.Entry child: item.getChildren()) 495 dumpBflItem(child, indent + 1, false); 496 } 497 498 void dumpUnloadListeners(WebPage page, long frame) { 499 if (waiting == true && dumpAsText()) { 500 String dump = getUnloadListenersDescription(page, frame); 501 if (dump != null) { 502 out.print(dump + '\n'); 503 } 504 } 505 } 506 507 private static String getUnloadListenersDescription(WebPage page, long frame) { 508 int count = page.getUnloadEventListenersCount(frame); 509 if (count > 0) { 510 return getFrameDescription(page, frame) + 511 " - has " + count + " onunload handler(s)"; 512 } 513 return null; 514 } 515 516 private static String getFrameDescription(WebPage page, long frame) { 517 String name = page.getName(frame); 518 if (frame == page.getMainFrame()) { 519 return name == null ? "main frame" : "main frame " + name; 520 } 521 return name == null ? "frame (anonymous)" : "frame " + name; 522 } 523 524 private native static boolean didFinishLoad(); 525 526 private final class WebPageClientImpl implements WebPageClient<Void> { 527 528 @Override 529 public void setCursor(long cursorID) { 530 } 531 532 @Override 533 public void setFocus(boolean focus) { 534 } 535 536 @Override 537 public void transferFocus(boolean forward) { 538 } 539 540 @Override 541 public void setTooltip(String tooltip) { 542 } 543 544 @Override 545 public WCRectangle getScreenBounds(boolean available) { 546 return null; 547 } 548 549 @Override 550 public int getScreenDepth() { 551 return 24; 552 } 553 554 @Override 555 public Void getContainer() { 556 return null; 557 } 558 559 @Override 560 public WCPoint screenToWindow(WCPoint ptScreen) { 561 return ptScreen; 562 } 563 564 @Override 565 public WCPoint windowToScreen(WCPoint ptWindow) { 566 return ptWindow; 567 } 568 569 @Override 570 public WCPageBackBuffer createBackBuffer() { 571 throw new UnsupportedOperationException(); 572 } 573 574 @Override 575 public boolean isBackBufferSupported() { 576 return false; 577 } 578 579 @Override 580 public void addMessageToConsole(String message, int lineNumber, 581 String sourceId) 582 { 583 if (complete) { 584 return; 585 } 586 if (!message.isEmpty()) { 587 int pos = message.indexOf("file://"); 588 if (pos != -1) { 589 String s1 = message.substring(0, pos); 590 String s2 = message.substring(pos); 591 try { 592 // Extract the last path component aka file name 593 s2 = new File(newURL(s2).getPath()).getName(); 594 } catch (MalformedURLException ignore) {} 595 message = s1 + s2; 596 } 597 } 598 if (lineNumber == 0) { 599 out.printf("CONSOLE MESSAGE: %s\n", message); 600 } else { 601 out.printf("CONSOLE MESSAGE: line %d: %s\n", 602 lineNumber, message); 603 } 604 } 605 606 @Override 607 public void didClearWindowObject(long context, long windowObject) { 608 mlog("didClearWindowObject"); 609 DumpRenderTree.didClearWindowObject(context, windowObject, 610 eventSender); 611 } 612 } 613 }