diff --git a/Makefile.am b/Makefile.am --- a/Makefile.am +++ b/Makefile.am @@ -758,7 +758,7 @@ stamps/test-extensions-compile.stamp: st ln -s $(TEST_EXTENSIONS_DIR) $(TEST_EXTENSIONS_COMPATIBILITY_SYMLINK); $(BOOT_DIR)/bin/javac $(IT_JAVACFLAGS) \ -d $(TEST_EXTENSIONS_DIR) \ - -classpath $(JUNIT_JAR):$(NETX_DIR)/lib/classes.jar \ + -classpath $(JUNIT_JAR):$(NETX_DIR)/lib/classes.jar:$(abs_top_builddir)/liveconnect/lib/classes.jar \ @test-extensions-source-files.txt && \ mkdir -p stamps && \ touch $@ diff --git a/tests/netx/unit/sun/applet/PluginAppletViewerTest.java b/tests/netx/unit/sun/applet/PluginAppletViewerTest.java new file mode 100644 --- /dev/null +++ b/tests/netx/unit/sun/applet/PluginAppletViewerTest.java @@ -0,0 +1,155 @@ +package sun.applet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import net.sourceforge.jnlp.AsyncCallWithTimeout; +import net.sourceforge.jnlp.AsyncCallWithTimeout.ExitStatus; + +import org.junit.Test; + +import sun.applet.mock.PluginPipeMock; + +public class PluginAppletViewerTest { + + static private PluginPipeMock installPipeMock() throws Exception { + AppletSecurityContextManager.addContext(0, + new PluginAppletSecurityContext(0, false /* no security */)); + + PluginPipeMock pipeMock = new PluginPipeMock(); + + // TODO: Ensure the thread this creates is stopped + PluginStreamHandler streamHandler = new PluginStreamHandler( + pipeMock.getInputStream(), pipeMock.getOutputStream()); + PluginAppletViewer.setStreamhandler(streamHandler); + PluginAppletViewer + .setPluginCallRequestFactory(new PluginCallRequestFactory()); + + streamHandler.startProcessing(); + + return pipeMock; + } + + /* + * Helpers for manipulating the object mapping using to refer to objects in + * the plugin + */ + private static Object getStoredObject(int id) { + return PluginObjectStore.getInstance().getObject(id); + } + + private static int storeObject(Object obj) { + PluginObjectStore.getInstance().reference(obj); + return PluginObjectStore.getInstance().getIdentifier(obj); + } + + @Test + public void testJavascriptCall() throws Exception { + final PluginPipeMock pipeMock = installPipeMock(); + + /* JS call parameters */ + final int jsObjectID = 0; + final String callName = "testfunction"; + final Object[] arguments = { "testargument", 1 }; // Arbitrary objects + + /* JS call result */ + final Object[] returnValue = { null }; + + AsyncCallWithTimeout call = new AsyncCallWithTimeout(new Runnable() { + public void run() { + returnValue[0] = PluginAppletViewer.call(jsObjectID, callName, + arguments); + } + }); + + String message = pipeMock.getNextRequest(); + + int reference = PluginPipeMessages.parseAndCheckJSCall(message, + jsObjectID, callName, arguments); + + Object expectedReturn = new Object(); + pipeMock.sendResponse("context 0 reference " + reference + + " JavaScriptCall " + storeObject(expectedReturn)); + + /* Check if we returned correctly (and have the right value) */ + assertEquals(ExitStatus.FINISHED_CORRECTLY, call.joinAndGetExitStatus()); + assertEquals(expectedReturn, returnValue[0]); + } + + @Test + public void testJavascriptEval() throws Exception { + final PluginPipeMock pipeMock = installPipeMock(); + + /* JS eval parameters */ + final int jsObjectID = 0; + final String callName = "testfunction"; + + /* JS eval result */ + final Object[] returnValue = { null }; + + AsyncCallWithTimeout call = new AsyncCallWithTimeout(new Runnable() { + public void run() { + returnValue[0] = PluginAppletViewer.eval(jsObjectID, callName); + } + }); + + String message = pipeMock.getNextRequest(); + + int reference = PluginPipeMessages.parseAndCheckJSEval(message, + jsObjectID, callName); + + Object expectedReturn = new Object(); + pipeMock.sendResponse("context 0 reference " + reference + + " JavaScriptEval " + storeObject(expectedReturn)); + + /* Check if we returned correctly (and have the right value) */ + assertEquals(ExitStatus.FINISHED_CORRECTLY, call.joinAndGetExitStatus()); + assertEquals(expectedReturn, returnValue[0]); + } + + @Test + public void testJavascriptFinalize() throws Exception { + final PluginPipeMock pipeMock = installPipeMock(); + + final int jsObjectID = 0; + AsyncCallWithTimeout call = new AsyncCallWithTimeout(new Runnable() { + public void run() { + PluginAppletViewer.JavaScriptFinalize(jsObjectID); + } + }); + + String message = pipeMock.getNextRequest(); + + int reference = PluginPipeMessages.parseAndCheckJSFinalize(message, + jsObjectID); + + pipeMock.sendResponse("context 0 reference " + reference + + " JavaScriptFinalize "); + + /* Check if we returned correctly (and have the right value) */ + assertEquals(ExitStatus.FINISHED_CORRECTLY, call.joinAndGetExitStatus()); + } + + @Test + public void testJavascriptToString() throws Exception { + final PluginPipeMock pipeMock = installPipeMock(); + + final int jsObjectID = 0; + AsyncCallWithTimeout call = new AsyncCallWithTimeout(new Runnable() { + public void run() { + PluginAppletViewer.javascriptToString(jsObjectID); + } + }); + + String message = pipeMock.getNextRequest(); + + int reference = PluginPipeMessages.parseAndCheckJSToString(message, + jsObjectID); + + String expectedReturn = "testreturn"; + pipeMock.sendResponse("context 0 reference " + reference + + " JavaScriptToString " + storeObject(expectedReturn)); + + /* Check if we returned correctly (and have the right value) */ + assertEquals(ExitStatus.FINISHED_CORRECTLY, call.joinAndGetExitStatus()); + } +} \ No newline at end of file diff --git a/tests/test-extensions/net/sourceforge/jnlp/AsyncCallWithTimeout.java b/tests/test-extensions/net/sourceforge/jnlp/AsyncCallWithTimeout.java new file mode 100644 --- /dev/null +++ b/tests/test-extensions/net/sourceforge/jnlp/AsyncCallWithTimeout.java @@ -0,0 +1,75 @@ +package net.sourceforge.jnlp; + +/** + * Creates an async call with a certain timeout. It takes a runnable and allows + * joining & querying the exit status. + * + * It starts immediately after creation. + */ +public class AsyncCallWithTimeout { + public enum ExitStatus { + FINISHED_CORRECTLY, THREW_EXCEPTION, TIMED_OUT + } + + private Thread handler; + private Runnable task; + private long timeout; + private ExitStatus exitStatus; + + public AsyncCallWithTimeout(Runnable task) { + this(task, 1000); // Default timeout is 1 second + } + + public AsyncCallWithTimeout(Runnable task, long timeout) { + this.task = task; + this.timeout = timeout; + this.handler = new HandlerThread(); + this.handler.start(); + } + + public ExitStatus joinAndGetExitStatus() throws InterruptedException { + handler.join(); + return exitStatus; + } + + private class HandlerThread extends Thread { + @Override + public void run() { + // We deduce that we threw an exception if we did not + // time-out nor complete correctly. + exitStatus = ExitStatus.THREW_EXCEPTION; + + Thread thread = new Thread() { + @Override + public void run() { + try { + task.run(); + exitStatus = ExitStatus.FINISHED_CORRECTLY; + } catch (Exception e) { + exitStatus = ExitStatus.THREW_EXCEPTION; + e.printStackTrace(); + } + handler.interrupt(); // Finish early + } + }; + + thread.start(); + + try { + Thread.sleep(timeout); + } catch (InterruptedException e) { + // Finish early + return; + } + + if (thread.isAlive()) { + exitStatus = ExitStatus.TIMED_OUT; + } + + // Make sure the thread is finished + while (thread.isAlive()) { + thread.interrupt(); + } + } + } +} diff --git a/tests/test-extensions/sun/applet/PluginPipeMessages.java b/tests/test-extensions/sun/applet/PluginPipeMessages.java new file mode 100644 --- /dev/null +++ b/tests/test-extensions/sun/applet/PluginPipeMessages.java @@ -0,0 +1,95 @@ +package sun.applet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Simple utility for parsing & verifying plugin messages. + * + * Assumes it is run in a JUnit-test. + */ +public class PluginPipeMessages { + + private static Object getStoredObject(int id) { + return PluginObjectStore.getInstance().getObject(id); + } + + /* + * Asserts that the message is a valid javascript request and returns the + * reference number + */ + private static int parseAndCheckJSMessage(String message, int messageLength, + String messageType, int contextObjectID) { + String[] parts = message.split(" "); + assertEquals(messageLength, parts.length); + + assertEquals("instance", parts[0]); + assertEquals("0", parts[1]); // JSCall's are prefixed with a dummy '0' + // instance + assertEquals("reference", parts[2]); + int reference = Integer.parseInt(parts[3]); + assertEquals(messageType, parts[4]); + + assertEquals(contextObjectID, Integer.parseInt(parts[5])); + return reference; + } + + /* + * Asserts that the message is a valid javascript request and returns the + * reference number + */ + private static int parseAndCheckJSMessage(String message, + String messageType, int contextObjectID, String stringArg, + Object[] arguments) { + int expectedLength = 7 + arguments.length; + int reference = parseAndCheckJSMessage(message, expectedLength, messageType, contextObjectID); + + String[] parts = message.split(" "); + assertEquals(stringArg, getStoredObject(Integer.parseInt(parts[6]))); + + for (int i = 0; i < arguments.length; i++) { + int objectID = Integer.parseInt(parts[7+i]); + assertEquals(arguments[i], getStoredObject(objectID)); + } + + return reference; + } + + /* + * Asserts that the message is a valid javascript method call request, and + * returns the reference number + */ + public static int parseAndCheckJSCall(String message, int contextObjectID, + String callName, Object[] arguments) { + return parseAndCheckJSMessage(message, "Call", contextObjectID, + callName, arguments); + } + + /* + * Asserts that the message is a valid javascript Eval request, and returns + * the reference number + */ + public static int parseAndCheckJSEval(String message, int contextObjectID, + String evalString) { + return parseAndCheckJSMessage(message, "Eval", contextObjectID, + evalString, new Object[] {}); + } + + /* + * Asserts that the message is a valid javascript Finalize request, and returns + * the reference number + */ + public static int parseAndCheckJSFinalize(String message, int contextObjectID) { + int expectedLength = 6; + return parseAndCheckJSMessage(message, expectedLength, "Finalize", contextObjectID); + } + + /* + * Asserts that the message is a valid javascript ToString request, and returns + * the reference number + */ + public static int parseAndCheckJSToString(String message, int contextObjectID) { + int expectedLength = 6; + return parseAndCheckJSMessage(message, expectedLength, "ToString", contextObjectID); + } +} diff --git a/tests/test-extensions/sun/applet/mock/PluginPipeMock.java b/tests/test-extensions/sun/applet/mock/PluginPipeMock.java new file mode 100644 --- /dev/null +++ b/tests/test-extensions/sun/applet/mock/PluginPipeMock.java @@ -0,0 +1,120 @@ +package sun.applet.mock; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; + +/** + * Helper for getting an input & output stream for use with PluginStreamHandler. + * Provides a convenient way of reading the Java requests and sending mocked plugin responses. + * + * The handling of these requests should be done on a different thread from the tested method, + * as icedtea-web will block waiting for a reply after sending a request. + */ +public class PluginPipeMock { + private PluginInputPipeMock inputStream = new PluginInputPipeMock(); + private PluginOutputPipeMock outputStream = new PluginOutputPipeMock(); + + public PluginPipeMock() { + } + + public InputStream getInputStream() { + return inputStream; + } + + public OutputStream getOutputStream() { + return outputStream; + } + + public String getNextRequest() { + return outputStream.getNextRequest(); + } + + public void sendResponse(String response) { + inputStream.sendReponse(response); + } + + /** + * Queues up mocked responses and sends them as replies to icedtea-web + */ + private class PluginInputPipeMock extends InputStream { + private StringReader reader = null; + private Deque responses = new ArrayDeque(); + + @Override + public int read() throws IOException { + while (true) { + if (reader == null) { + reader = new StringReader(getNextResponse() + '\n'); + } + int chr = reader.read(); + if (chr == -1) { + reader = null; + continue; + } + return chr; + } + } + + /* Necessary for correct behaviour with BufferedReader! */ + @Override + public int read(byte b[], int off, int len) throws IOException { + if (len == 0) { + return 0; + } + b[off] = (byte)read(); + return 1; + } + + /* Blocks until we get a line of input. */ + private String getNextResponse() { + while (true) { + synchronized (this) { + String line = responses.pollFirst(); + if (line != null) { + return line; + } + } + } + } + + synchronized void sendReponse(String response) { + responses.add(response); + } + } + + /** + * Splits requests from icedtea-web into lines + */ + private class PluginOutputPipeMock extends OutputStream { + private StringBuilder lineBuffer = new StringBuilder(); + private Deque lines = new ArrayDeque(); + + @Override + public synchronized void write(int b) throws IOException { + char chr = (char) b; + if (chr == '\0') { + lines.addLast(lineBuffer.toString()); + lineBuffer.setLength(0); + } else { + lineBuffer.append((char) b); + } + } + + /* Blocks until we get a line of output. */ + String getNextRequest() { + while (true) { + synchronized (this) { + String line = lines.pollFirst(); + if (line != null) { + return line; + } + } + } + } + } +}