com.sun.javafx.webkit.drt.DumpRenderTree Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javafx.webkit.drt;
import com.sun.javafx.application.PlatformImpl;
import com.sun.javafx.logging.PlatformLogger;
import com.sun.javafx.logging.PlatformLogger.Level;
import com.sun.webkit.*;
import com.sun.webkit.graphics.*;
import static com.sun.webkit.network.URLs.newURL;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.Map;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import javafx.scene.web.WebEngine;
public final class DumpRenderTree {
private final static PlatformLogger log = PlatformLogger.getLogger("DumpRenderTree");
private final static long PID = (new Date()).getTime() & 0xFFFF;
private final static String fileSep = System.getProperty("file.separator");
private static boolean forceDumpAsText = false;
final static PrintWriter out;
static {
try {
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
System.out, "UTF-8")), true);
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
}
static volatile DumpRenderTree drt;
private final WebPage webPage;
private final UIClientImpl uiClient;
private EventSender eventSender;
private CountDownLatch latch;
private Timer timer;
private String testPath;
private boolean loaded;
private boolean waiting;
private boolean complete;
static class RenderUpdateHelper extends TimerTask {
private WebPage webPage;
public RenderUpdateHelper(WebPage webPage) {
this.webPage = webPage;
}
@Override
public void run() {
Invoker.getInvoker().invokeOnEventThread(() -> {
webPage.forceRepaint();
});
}
}
static class ThemeClientImplStub extends ThemeClient {
@Override
protected RenderTheme createRenderTheme() {
return new RenderThemeStub();
}
@Override
protected ScrollBarTheme createScrollBarTheme() {
return new ScrollBarThemeStub();
}
}
static class RenderThemeStub extends RenderTheme {
@Override
protected Ref createWidget(long id, int widgetIndex, int state, int w, int h, int bgColor, ByteBuffer extParams) {
return null;
}
@Override
public void drawWidget(WCGraphicsContext g, Ref widget, int x, int y) {
}
@Override
protected int getRadioButtonSize() {
return 0;
}
@Override
protected int getSelectionColor(int index) {
return 0;
}
@Override
public WCSize getWidgetSize(Ref widget) {
return new WCSize(0, 0);
}
}
static class ScrollBarThemeStub extends ScrollBarTheme {
@Override
protected Ref createWidget(long id, int w, int h, int orientation, int value, int visibleSize, int totalSize) {
return null;
}
@Override
protected void getScrollBarPartRect(long id, int part, int rect[]) {}
@Override
public void paint(WCGraphicsContext g, Ref sbRef, int x, int y, int pressedPart, int hoveredPart) {
}
@Override
public WCSize getWidgetSize(Ref widget) {
return new WCSize(0, 0);
}
}
// called on FX thread
private DumpRenderTree() {
uiClient = new UIClientImpl();
webPage = new WebPage(new WebPageClientImpl(), uiClient, null, null,
new ThemeClientImplStub(), false);
uiClient.setWebPage(webPage);
webPage.setBounds(0, 0, 800, 600);
webPage.setDeveloperExtrasEnabled(true);
webPage.addLoadListenerClient(new DRTLoadListener());
}
private String getTestPath(String testString) {
int t = testString.indexOf("'");
String pixelsHash = "";
if ((t > 0) && (t < testString.length() - 1)) {
pixelsHash = testString.substring(t + 1);
testString = testString.substring(0, t);
}
this.testPath = testString;
initTest(testString, pixelsHash);
return testString;
}
protected String getTestURL() {
return testPath;
}
/*
private static boolean isDebug()
{
return log.isLoggable(Level.FINE);
}
*/
private static void mlog(String msg) {
if (log.isLoggable(Level.FINE)) {
log.fine("PID:" + Long.toHexString(PID)
+ " TID:" + Thread.currentThread().threadId()
+ "(" + Thread.currentThread().getName() + ") "
+ msg);
}
}
private static void initPlatform() throws Exception {
// initialize default toolkit
final CountDownLatch latch = new CountDownLatch(1);
PlatformImpl.startup(() -> {
// initialize Webkit classes
try {
Class.forName(WebEngine.class.getName());
Class.forName(WebPage.class.getName());
} catch (Exception e) {}
System.loadLibrary("DumpRenderTreeJava");
initDRT();
new WebEngine();
drt = new DumpRenderTree();
PageCache.setCapacity(1);
latch.countDown();
});
// wait for libraries to load
latch.await();
}
boolean complete() { return this.complete; }
private void resetToConsistentStateBeforeTesting(final TestOptions options) {
// Reset native objects associated with WebPage
webPage.resetToConsistentStateBeforeTesting();
// Assign default values for all supported TestOptions
webPage.overridePreference("experimental:CSSCustomPropertiesAndValuesEnabled", "false");
webPage.overridePreference("enableColorFilter", "false");
webPage.overridePreference("enableIntersectionObserver", "false");
// Enable features based on TestOption
for (Map.Entry option : options.getOptions().entrySet()) {
webPage.overridePreference(option.getKey(), option.getValue());
}
}
private void reset(final TestOptions options) {
mlog("reset");
// newly create EventSender for each test
eventSender = new EventSender(webPage);
resetToConsistentStateBeforeTesting(options);
// Clear frame name
webPage.reset(webPage.getMainFrame());
// Reset zoom factors
webPage.setZoomFactor(1.0f, true);
webPage.setZoomFactor(1.0f, false);
// Reset DRT internal states
complete = false;
loaded = false;
waiting = false;
}
// called on FX thread
private void run(final String testString, final CountDownLatch latch) {
this.latch = latch;
String file = getTestPath(testString);
mlog("{runTest: " + file);
long mainFrame = webPage.getMainFrame();
try {
new URL(file);
} catch (MalformedURLException ex) {
file = "file:///" + file;
}
// parse test options from the html test header
final TestOptions options = new TestOptions(file);
reset(options);
webPage.open(mainFrame, file);
mlog("}runTest");
}
private void runTest(final String testString) throws Exception {
final CountDownLatch l = new CountDownLatch(1);
Invoker.getInvoker().invokeOnEventThread(() -> {
run(testString, l);
});
timer = new Timer();
TimerTask task = new RenderUpdateHelper(webPage);
timer.schedule(task, 1000/60, 1000/60);
// wait until test is finished
l.await();
task.cancel();
timer.cancel();
final CountDownLatch latchForEvents = new CountDownLatch(1);
Invoker.getInvoker().invokeOnEventThread(() -> {
mlog("dispose");
webPage.stop();
dispose();
latchForEvents.countDown();
});
latchForEvents.await();
}
// called from native
private static void waitUntilDone() {
mlog("waitUntilDone");
drt.setWaiting(true); // TODO: handle timeout
}
// called from native
private static void notifyDone() {
mlog("notifyDone");
drt.setWaiting(false);
}
private static void overridePreference(String key, String value) {
mlog("overridePreference");
drt.webPage.overridePreference(key, value);
}
private synchronized void setLoaded(boolean loaded) {
this.loaded = loaded;
done();
}
private synchronized void setWaiting(boolean waiting) {
this.waiting = waiting;
done();
}
private synchronized StringBuilder dumpFramesAsText(long frame) {
StringBuilder str = new StringBuilder();
String innerText = webPage.getInnerText(frame);
if (frame == webPage.getMainFrame()) {
if (innerText != null) {
// don't use println() here as it varies from platform
// to platform, but DRT expects it always to be 0x0A
str.append(innerText + '\n');
}
} else {
str.append("\n--------\nFrame: '");
str.append(webPage.getName(frame));
str.append("'\n--------\n");
str.append(innerText + "\n");
}
if (dumpChildFramesAsText()) {
List children = webPage.getChildFrames(frame);
if (children != null) {
for (long child : children) {
str.append(dumpFramesAsText(child));
}
}
}
// To keep things tidy, strip all trailing spaces: they are not a meaningful part of dumpAsText test output.
int spacePosition;
while ((spacePosition = str.indexOf(" \n")) != -1)
str.deleteCharAt(spacePosition);
while (str.length() != 0 && str.charAt(str.length() - 1) == ' ')
str.deleteCharAt(str.length() - 1);
return str;
}
private synchronized void dump(long frame) {
boolean dumpAsText = dumpAsText() || forceDumpAsText;
mlog("dumpAsText = " + dumpAsText);
if (dumpAsText) {
out.print(dumpFramesAsText(frame));
if (dumpBackForwardList() && frame == webPage.getMainFrame()) {
drt.dumpBfl();
}
} else {
String renderTree = webPage.getRenderTree(frame);
out.print(renderTree);
}
}
private synchronized void done() {
if (waiting || !loaded || complete) {
return;
}
mlog("dump");
dump(webPage.getMainFrame());
mlog("done");
out.print("#EOF" + '\n');
// TODO: dump pixels here
out.print("#EOF" + '\n');
out.flush();
System.err.print("#EOF" + '\n');
System.err.flush();
complete = true;
// notify main thread that test is finished
this.latch.countDown();
}
private static native void initDRT();
private static native void initTest(String testPath, String pixelsHash);
private static native void didClearWindowObject(long pContext,
long pWindowObject, EventSender eventSender);
private static native void dispose();
private static native boolean dumpAsText();
private static native boolean dumpChildFramesAsText();
private static native boolean dumpBackForwardList();
protected static native boolean shouldStayOnPageAfterHandlingBeforeUnload();
protected static native String[] openPanelFiles();
private final class DRTLoadListener implements LoadListenerClient {
@Override
public void dispatchLoadEvent(long frame, int state,
String url, String contentType,
double progress, int errorCode)
{
mlog("dispatchLoadEvent: ENTER");
if (frame == webPage.getMainFrame()) {
mlog("dispatchLoadEvent: STATE = " + state);
switch (state) {
case PAGE_STARTED:
mlog("PAGE_STARTED");
setLoaded(false);
break;
case PAGE_FINISHED:
mlog("PAGE_FINISHED");
if (didFinishLoad()) {
setLoaded(true);
}
break;
case DOCUMENT_AVAILABLE:
dumpUnloadListeners(webPage, frame);
break;
case LOAD_FAILED:
mlog("LOAD_FAILED");
// safety net: if load fails, e.g. command line
// parameters were bad, let's not hang forever
setLoaded(true);
break;
}
}
mlog("dispatchLoadEvent: EXIT");
}
@Override
public void dispatchResourceLoadEvent(long frame, int state,
String url, String contentType,
double progress, int errorCode)
{
}
}
public static void main(final String[] args) throws Exception {
/*
if ( isDebug() ) {
// 'log' here is from java.util.logging
log.setLevel(Level.FINEST);
FileHandler handler = new FileHandler("drt.log", true);
handler.setFormatter(new Formatter() {
@Override
public String format(LogRecord record) {
return formatMessage(record) + "\n";
}
});
log.addHandler(handler);
}
*/
mlog("{main");
initPlatform();
assert drt != null;
for (String arg: args) {
if ("--dump-as-text".equals(arg)) {
forceDumpAsText = true;
} else if ("-".equals(arg)) {
// read from stdin
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
String testPath;
while ((testPath = in.readLine()) != null) {
drt.runTest(testPath);
}
in.close();
} else {
drt.runTest(arg);
}
}
PlatformImpl.exit();
mlog("}main");
System.exit(0); // workaround to kill media threads
}
// called from native
private static int getWorkerThreadCount() {
return WebPage.getWorkerThreadCount();
}
// called from native
private static String resolveURL(String relativeURL) {
String testDir = new File(drt.testPath).getParentFile().getPath();
File f = new File(testDir, relativeURL);
String url = "file:///" + f.toString().replace(fileSep, "/");
mlog("resolveURL: " + url);
return url;
}
// called from native
private static void loadURL(String url) {
drt.webPage.open(drt.webPage.getMainFrame(), url);
}
// called from native
private static void goBackForward(int dist) {
// TODO: honor the dist
if (dist > 0) {
drt.webPage.goForward();
} else {
drt.webPage.goBack();
}
}
// called from native
private static int getBackForwardItemCount() {
return drt.getBackForwardList().size();
}
// called from native
private static void clearBackForwardList() {
drt.getBackForwardList().clearBackForwardListForDRT();
}
private static final String TEST_DIR_NAME = "LayoutTests";
private static final int TEST_DIR_LEN = TEST_DIR_NAME.length();
private static final String CUR_ITEM_STR = "curr->";
private static final int CUR_ITEM_STR_LEN = CUR_ITEM_STR.length();
private static final String INDENT = " ";
private BackForwardList bfl;
private BackForwardList getBackForwardList() {
if (bfl == null) {
bfl = webPage.createBackForwardList();
}
return bfl;
}
private void dumpBfl() {
out.print("\n============== Back Forward List ==============\n");
getBackForwardList();
BackForwardList.Entry curItem = bfl.getCurrentEntry();
for (BackForwardList.Entry e: bfl.toArray()) {
dumpBflItem(e, 2, e == curItem);
}
out.print("===============================================\n");
}
private void dumpBflItem(BackForwardList.Entry item, int indent, boolean isCurrent) {
StringBuilder str = new StringBuilder();
for (int i = indent; i > 0; i--) str.append(INDENT);
if (isCurrent) str.replace(0, CUR_ITEM_STR_LEN, CUR_ITEM_STR);
String url = item.getURL().toString();
if (url.contains("file:/")) {
String subUrl = url.substring(url.indexOf(TEST_DIR_NAME) + TEST_DIR_LEN + 1);
str.append("(file test):" + subUrl);
} else {
str.append(url);
}
if (item.getTarget() != null) {
str.append(" (in frame \"" + item.getTarget() + "\")");
}
if (item.isTargetItem()) {
str.append(" **nav target**\n");
} else {
str.append("\n");
}
out.print(str);
if (item.getChildren() != null)
for (BackForwardList.Entry child: item.getChildren())
dumpBflItem(child, indent + 1, false);
}
void dumpUnloadListeners(WebPage page, long frame) {
if (waiting == true && dumpAsText()) {
String dump = getUnloadListenersDescription(page, frame);
if (dump != null) {
out.print(dump + '\n');
}
}
}
private static String getUnloadListenersDescription(WebPage page, long frame) {
int count = page.getUnloadEventListenersCount(frame);
if (count > 0) {
return getFrameDescription(page, frame) +
" - has " + count + " onunload handler(s)";
}
return null;
}
private static String getFrameDescription(WebPage page, long frame) {
String name = page.getName(frame);
if (frame == page.getMainFrame()) {
return name == null ? "main frame" : "main frame " + name;
}
return name == null ? "frame (anonymous)" : "frame " + name;
}
private native static boolean didFinishLoad();
private final class WebPageClientImpl implements WebPageClient {
@Override
public void setCursor(long cursorID) {
}
@Override
public void setFocus(boolean focus) {
}
@Override
public void transferFocus(boolean forward) {
}
@Override
public void setTooltip(String tooltip) {
}
@Override
public WCRectangle getScreenBounds(boolean available) {
return new WCRectangle(0, 0, 800, 600);
}
@Override
public int getScreenDepth() {
return 24;
}
@Override
public Void getContainer() {
return null;
}
@Override
public WCPoint screenToWindow(WCPoint ptScreen) {
return ptScreen;
}
@Override
public WCPoint windowToScreen(WCPoint ptWindow) {
return ptWindow;
}
@Override
public WCPageBackBuffer createBackBuffer() {
throw new UnsupportedOperationException();
}
@Override
public boolean isBackBufferSupported() {
return false;
}
@Override
public void addMessageToConsole(String message, int lineNumber,
String sourceId)
{
if (complete) {
return;
}
if (!message.isEmpty()) {
int pos = message.indexOf("file://");
if (pos != -1) {
String s1 = message.substring(0, pos);
String s2 = message.substring(pos);
try {
// Extract the last path component aka file name
s2 = new File(newURL(s2).getPath()).getName();
} catch (MalformedURLException ignore) {}
message = s1 + s2;
}
}
out.printf("CONSOLE MESSAGE:%s\n",
(message.isEmpty() || message.startsWith("\n")) ? message : " " + message);
}
@Override
public void didClearWindowObject(long context, long windowObject) {
mlog("didClearWindowObject");
if (eventSender != null) {
DumpRenderTree.didClearWindowObject(context, windowObject,
eventSender);
}
}
}
}