diff --git a/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageIds.java b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageIds.java
new file mode 100644
index 000000000..65f761052
--- /dev/null
+++ b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageIds.java
@@ -0,0 +1,101 @@
+/*
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 2001 The Apache Software Foundation. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ * any, must include the following acknowlegement:
+ * "This product includes software developed by the
+ * Apache Software Foundation (http://www.apache.org/)."
+ * Alternately, this acknowlegement may appear in the software itself,
+ * if and wherever such third-party acknowlegements normally appear.
+ *
+ * 4. The names "The Jakarta Project", "Ant", and "Apache Software
+ * Foundation" must not be used to endorse or promote products derived
+ * from this software without prior written permission. For written
+ * permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache"
+ * nor may "Apache" appear in their names without prior written
+ * permission of the Apache Group.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ */
+package org.apache.tools.ant.taskdefs.optional.junit.remote;
+
+
+/**
+ * A set of messages identifiers to be used for communication
+ * between server/client(TestRunner).
+ *
+ *
+ * This code is based on the code from Erich Gamma made for the
+ * JUnit plugin for Eclipse. {@link http://www.eclipse.org} and is merged
+ * with code originating from Ant 1.4.x.
+ *
+ *
+ * @author Stephane Bailliez
+ */
+public interface MessageIds {
+ int MSG_HEADER_LENGTH = 8;
+
+ // messages send by TestRunServer
+ String TRACE_START = "%TRACES ";
+ String TRACE_END = "%TRACEE ";
+
+ // a line printed on stdout
+ String STDOUT_START = "%STDOUTS";
+ String STDOUT_END = "%STDOUTE";
+
+ // a line printed on stderr
+ String STDERR_START = "%STDERRS";
+ String STDERR_END = "%STDERRE";
+
+ // JVM system properties used in the VM
+ String PROPS_START = "%SYSPROS";
+ String PROPS_END = "%SYSPROE";
+
+ // test run started...
+ String TEST_COUNT = "%TESTC ";
+ // a test just started
+ String TEST_START = "%TESTS ";
+ // a test is finished
+ String TEST_END = "%TESTE ";
+ String TEST_ERROR = "%ERROR ";
+ String TEST_FAILED = "%FAILED ";
+ String TEST_ELAPSED_TIME = "%RUNTIME";
+ String TEST_STOPPED = "%TSTSTP ";
+
+ // messages understood by the Server
+ String TEST_STOP = ">STOP ";
+}
\ No newline at end of file
diff --git a/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageReader.java b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageReader.java
new file mode 100644
index 000000000..a2aff9178
--- /dev/null
+++ b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageReader.java
@@ -0,0 +1,246 @@
+/*
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 2001 The Apache Software Foundation. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ * any, must include the following acknowlegement:
+ * "This product includes software developed by the
+ * Apache Software Foundation (http://www.apache.org/)."
+ * Alternately, this acknowlegement may appear in the software itself,
+ * if and wherever such third-party acknowlegements normally appear.
+ *
+ * 4. The names "The Jakarta Project", "Ant", and "Apache Software
+ * Foundation" must not be used to endorse or promote products derived
+ * from this software without prior written permission. For written
+ * permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache"
+ * nor may "Apache" appear in their names without prior written
+ * permission of the Apache Group.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ */
+package org.apache.tools.ant.taskdefs.optional.junit.remote;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.ObjectInputStream;
+import java.io.ByteArrayInputStream;
+import java.util.Vector;
+import java.util.Properties;
+
+import org.apache.tools.ant.taskdefs.optional.junit.TestRunListener;
+
+/**
+ * Read and dispatch messages received via an input stream.
+ * The inputstream should be the connection to the remote client.
+ *
+ * All messages are dispatched to the registered listeners.
+ *
+ *
+ * This code is based on the code from Erich Gamma made for the
+ * JUnit plugin for Eclipse. {@link http://www.eclipse.org} and is merged
+ * with code originating from Ant 1.4.x.
+ *
+ *
+ * @author Stephane Bailliez
+ */
+public class MessageReader {
+
+ /** the set of registered listeners */
+ protected Vector listeners = new Vector();
+
+ // communication states with client
+ protected boolean inReadTrace = false;
+ protected boolean inFailedMessage = false;
+ protected String failedTest;
+ protected String failedMessage;
+ protected String failedTrace;
+ protected int failureKind;
+ protected long elapsedTime;
+ protected Properties sysprops;
+
+ public MessageReader() {
+ }
+
+ /**
+ * Add a new listener.
+ * @param listener a listener that will receive events from the client.
+ */
+ public void addListener(TestRunListener listener) {
+ listeners.addElement(listener);
+ }
+
+ public void removeListener(TestRunListener listener) {
+ listeners.removeElement(listener);
+ }
+
+ /**
+ * Read a complete stream from a client, it will only return
+ * once the connection is stopped. You'd better not reuse
+ * an instance of this class since there are instance variables used
+ * to keep track of the client state.
+ * @param in the inputstream to the client.
+ */
+ public void process(InputStream in) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF8"));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ processMessage(line);
+ }
+ }
+
+ /**
+ * Process a message from the client and dispatch the
+ * appropriate message to the listeners.
+ */
+ protected void processMessage(String message) {
+ if (message == null){
+ return;
+ }
+
+ String arg = message.substring(MessageIds.MSG_HEADER_LENGTH);
+ if (message.startsWith(MessageIds.TRACE_START)) {
+ failedTrace = arg.substring(0, arg.indexOf(MessageIds.TRACE_END));
+ failedTrace = new String(Base64.decode(failedTrace.getBytes()));
+ notifyTestFailed(failureKind, failedTest, failedTrace);
+ return;
+ }
+
+ if (message.startsWith(MessageIds.TEST_COUNT)) {
+ int count = Integer.parseInt(arg);
+ notifyTestSuiteStarted(count);
+ return;
+ }
+ if (message.startsWith(MessageIds.TEST_START)) {
+ notifyTestStarted(arg);
+ return;
+ }
+ if (message.startsWith(MessageIds.TEST_END)) {
+ notifyTestEnded(arg);
+ return;
+ }
+ if (message.startsWith(MessageIds.TEST_ERROR)) {
+ failedTest = arg;
+ failureKind = TestRunListener.STATUS_ERROR;
+ return;
+ }
+ if (message.startsWith(MessageIds.TEST_FAILED)) {
+ failedTest = arg;
+ failureKind = TestRunListener.STATUS_FAILURE;
+ return;
+ }
+ if (message.startsWith(MessageIds.TEST_ELAPSED_TIME)) {
+ elapsedTime = Long.parseLong(arg);
+ notifyTestSuiteEnded(elapsedTime);
+ return;
+ }
+ if (message.startsWith(MessageIds.TEST_STOPPED)) {
+ elapsedTime = Long.parseLong(arg);
+ notifyTestSuiteStopped(elapsedTime);
+ return;
+ }
+ if (message.startsWith(MessageIds.PROPS_START)){
+ try {
+ byte[] bytes = arg.substring(0, arg.indexOf(MessageIds.PROPS_END)).getBytes();
+ bytes = Base64.decode(bytes);
+ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
+ sysprops = (Properties)ois.readObject();
+ } catch (Exception e){
+ // ignore now
+ e.printStackTrace();
+ }
+ notifyTestSystemProperties(sysprops);
+ }
+ }
+
+ protected void notifyTestStarted(String testname) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testStarted(testname);
+ }
+ }
+ }
+
+ protected void notifyTestEnded(String testname) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testEnded(testname);
+ }
+ }
+ }
+
+ protected void notifyTestFailed(int kind, String testname, String trace) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testFailed(kind, testname, trace);
+ }
+ }
+ }
+
+ protected void notifyTestSuiteStarted(int count) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testRunStarted(count);
+ }
+ }
+ }
+
+ protected void notifyTestSuiteEnded(long elapsedtime) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testRunEnded(elapsedtime);
+ }
+ }
+ }
+
+ protected void notifyTestSuiteStopped(long elapsedtime) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testRunStopped(elapsedtime);
+ }
+ }
+ }
+
+ protected void notifyTestSystemProperties(Properties props) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((TestRunListener) listeners.elementAt(i)).testRunSystemProperties(props);
+ }
+ }
+ }
+
+}
diff --git a/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageWriter.java b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageWriter.java
new file mode 100644
index 000000000..2780b6c10
--- /dev/null
+++ b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/MessageWriter.java
@@ -0,0 +1,155 @@
+/*
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 2001 The Apache Software Foundation. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ * any, must include the following acknowlegement:
+ * "This product includes software developed by the
+ * Apache Software Foundation (http://www.apache.org/)."
+ * Alternately, this acknowlegement may appear in the software itself,
+ * if and wherever such third-party acknowlegements normally appear.
+ *
+ * 4. The names "The Jakarta Project", "Ant", and "Apache Software
+ * Foundation" must not be used to endorse or promote products derived
+ * from this software without prior written permission. For written
+ * permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache"
+ * nor may "Apache" appear in their names without prior written
+ * permission of the Apache Group.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ */
+package org.apache.tools.ant.taskdefs.optional.junit.remote;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.ObjectOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.tools.ant.taskdefs.optional.junit.TestRunListener;
+
+/**
+ * A wrapper that sends string messages to a given stream.
+ *
+ *
+ * This code is based on the code from Erich Gamma made for the
+ * JUnit plugin for Eclipse. {@link http://www.eclipse.org} and is merged
+ * with code originating from Ant 1.4.x.
+ *
+ *
+ * @author Stephane Bailliez
+ */
+public class MessageWriter implements MessageIds {
+
+ private PrintWriter pw;
+
+ public MessageWriter(OutputStream out) {
+ this.pw = new PrintWriter(out, true);
+ }
+
+ protected void finalize(){
+ close();
+ }
+
+ public void close() {
+ if (pw != null){
+ pw.close();
+ pw = null;
+ }
+ }
+
+ public void sendMessage(String msg) {
+ pw.println(msg);
+ }
+
+// -------- notifier helper methods
+
+ public void notifyTestRunStarted(int testCount) {
+ sendMessage(MessageIds.TEST_COUNT + testCount);
+ }
+
+ public void notifyTestRunEnded(long elapsedTime) {
+ sendMessage(MessageIds.TEST_ELAPSED_TIME + elapsedTime);
+ }
+
+ public void notifyTestRunStopped(long elapsedTime) {
+ sendMessage(MessageIds.TEST_STOPPED + elapsedTime);
+ }
+
+ public void notifyTestStarted(String testName) {
+ sendMessage(MessageIds.TEST_START + testName);
+ }
+
+ public void notifyTestEnded(String testName) {
+ sendMessage(MessageIds.TEST_END + testName);
+ }
+
+ public void notifyTestFailed(int status, String testName, String trace) {
+ if (status == TestRunListener.STATUS_FAILURE) {
+ sendMessage(MessageIds.TEST_FAILED + testName);
+ } else {
+ sendMessage(MessageIds.TEST_ERROR + testName);
+ }
+ sendMessage(MessageIds.TRACE_START + new String(Base64.encode(trace.getBytes())) + MessageIds.TRACE_END);
+ }
+
+ public void notifyStdOutLine(String testname, String line) {
+ sendMessage(MessageIds.STDOUT_START);
+ sendMessage(line);
+ sendMessage(MessageIds.STDOUT_END);
+ }
+
+ public void notifyStdErrLine(String testname, String line) {
+ sendMessage(MessageIds.STDERR_START);
+ sendMessage(line);
+ sendMessage(MessageIds.STDERR_END);
+ }
+
+ public void notifySystemProperties() {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(out);
+ oos.writeObject(System.getProperties());
+ oos.close();
+ String msg = new String(Base64.encode(out.toByteArray()));
+ sendMessage(MessageIds.PROPS_START + msg + MessageIds.PROPS_END);
+ } catch (IOException e){
+ // ignore
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/Server.java b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/Server.java
new file mode 100644
index 000000000..30e5139bb
--- /dev/null
+++ b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/Server.java
@@ -0,0 +1,159 @@
+/*
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 2001 The Apache Software Foundation. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ * any, must include the following acknowlegement:
+ * "This product includes software developed by the
+ * Apache Software Foundation (http://www.apache.org/)."
+ * Alternately, this acknowlegement may appear in the software itself,
+ * if and wherever such third-party acknowlegements normally appear.
+ *
+ * 4. The names "The Jakarta Project", "Ant", and "Apache Software
+ * Foundation" must not be used to endorse or promote products derived
+ * from this software without prior written permission. For written
+ * permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache"
+ * nor may "Apache" appear in their names without prior written
+ * permission of the Apache Group.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ */
+package org.apache.tools.ant.taskdefs.optional.junit.remote;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.apache.tools.ant.taskdefs.optional.junit.TestRunListener;
+
+/**
+ * The server that will receive events from a remote client.
+ *
+ *
+ * This code is based on the code from Erich Gamma made for the
+ * JUnit plugin for Eclipse. {@link http://www.eclipse.org} and is merged
+ * with code originating from Ant 1.4.x.
+ *
+ *
+ * @see TestRunner
+ * @author Stephane Bailliez
+ */
+public class Server {
+
+ /** the port where the server is listening */
+ private int port = -1;
+
+ /** the server socket */
+ private ServerSocket server;
+
+ /** the client that is connected to the server */
+ private Socket client;
+
+ /** the reader in charge of interpreting messages from the client */
+ private MessageReader reader = new MessageReader();
+
+ /** writer used to send message to clients */
+ private PrintWriter writer;
+
+ public Server(int port) {
+ this.port = port;
+ }
+
+ /**
+ * add a new listener
+ * @param listener a instance of a listener.
+ */
+ public void addListener(TestRunListener listener) {
+ reader.addListener(listener);
+ }
+
+ /**
+ * remove an existing listener
+ * @param listener a instance of a listener.
+ */
+ public void removeListener(TestRunListener listener) {
+ reader.removeListener(listener);
+ }
+
+ /** return whether there is a client running or not */
+ public boolean isRunning() {
+ return client != null;
+ }
+
+ /** start a server to the specified port */
+ public void start() {
+ Worker worker = new Worker();
+ worker.start();
+ }
+
+ /** cancel the connection to the client */
+ public void cancel() {
+ if (isRunning()) {
+ //@fixme
+ }
+ }
+
+ /** shutdown the server and any running client */
+ public void shutdown() {
+ try {
+ if (client != null) {
+ client.shutdownInput();
+ client.shutdownOutput();
+ }
+ server.close();
+ } catch (IOException e) {
+ }
+ }
+
+//-----
+
+ private class Worker extends Thread {
+ public void run() {
+ try {
+ server = new ServerSocket(port);
+ client = server.accept();
+ writer = new PrintWriter(client.getOutputStream(), true);
+ reader.process(client.getInputStream());
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ stop();
+ shutdown();
+ }
+ }
+ }
+}
diff --git a/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/TestRunner.java b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/TestRunner.java
new file mode 100644
index 000000000..6e33cdb47
--- /dev/null
+++ b/proposal/sandbox/junit/src/main/org/apache/tools/ant/taskdefs/optional/junit/remote/TestRunner.java
@@ -0,0 +1,428 @@
+/*
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 2001 The Apache Software Foundation. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ * any, must include the following acknowlegement:
+ * "This product includes software developed by the
+ * Apache Software Foundation (http://www.apache.org/)."
+ * Alternately, this acknowlegement may appear in the software itself,
+ * if and wherever such third-party acknowlegements normally appear.
+ *
+ * 4. The names "The Jakarta Project", "Ant", and "Apache Software
+ * Foundation" must not be used to endorse or promote products derived
+ * from this software without prior written permission. For written
+ * permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache"
+ * nor may "Apache" appear in their names without prior written
+ * permission of the Apache Group.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ */
+package org.apache.tools.ant.taskdefs.optional.junit.remote;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.FileInputStream;
+import java.net.Socket;
+import java.util.Vector;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestListener;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import org.apache.tools.ant.util.StringUtils;
+import org.apache.tools.ant.taskdefs.optional.junit.JUnitHelper;
+import org.apache.tools.ant.taskdefs.optional.junit.TestRunListener;
+
+/**
+ * TestRunner for running tests and send results to a remote server.
+ *
+ *
+ * This code is originally based on the code from Erich Gamma made for the
+ * JUnit plugin for Eclipse. {@link http://www.eclipse.org} and is merged
+ * with code ideas originating from Ant 1.4.x.
+ *
+ *
+ * @author Stephane Bailliez
+ */
+public class TestRunner implements TestListener {
+
+ /** host to connect to */
+ private String host = "127.0.0.1";
+
+ /** port to connect to */
+ private int port = -1;
+
+ private boolean debug = false;
+
+ /** the list of test class names to run */
+ private Vector testClassNames = new Vector();
+
+ /** result of the current test */
+ private TestResult testResult;
+
+ /** client socket to communicate with the server */
+ private Socket clientSocket;
+
+ /** writer to send message to the server */
+ private MessageWriter writer;
+
+ /** reader to listen for a shutdown from the server */
+ private BufferedReader reader;
+
+ /** bean constructor */
+ public TestRunner(){
+ }
+
+ /**
+ * Set the debug mode.
+ * @param debug true to set to debug mode otherwise false.
+ */
+ public void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
+ /**
+ * Set the port to connect to the server
+ * @param port a valid port number.
+ */
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Set the hostname of the server
+ * @param host the hostname or ip of the server
+ */
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ /**
+ * Add a test class name to be executed by this runner.
+ * @param classname the class name of the test to run.
+ */
+ public void addTestClassName(String classname) {
+ testClassNames.addElement(classname);
+ }
+
+ /**
+ * Thread listener for a shutdown from the server
+ * Note that it will stop any running test.
+ */
+ private class StopThread extends Thread {
+ public void run() {
+ try {
+ String line = null;
+ if ((line = reader.readLine()) != null) {
+ if (line.startsWith(MessageIds.TEST_STOP)) {
+ TestRunner.this.stop();
+ }
+ }
+ } catch (Exception e) {
+ TestRunner.this.stop();
+ }
+ }
+ }
+
+ /**
+ * Entry point for command line.
+ * Usage:
+ *
+ * TestRunner -classnames -port -host -debug
+ * -file
+ * -classnames
+ * -port
+ * -host
+ * -debug to run in debug mode
+ *
+ */
+ public static void main(String[] args) throws Exception {
+ TestRunner testRunServer = new TestRunner();
+ testRunServer.init(args);
+ testRunServer.run();
+ }
+
+ /**
+ * Parses the arguments of command line.
+ * testClassNames, host, port, listeners and debug mode are set
+ * @see #main(String[])
+ */
+ protected void init(String[] args) throws Exception {
+ for (int i = 0; i < args.length; i++) {
+ if ("-file".equalsIgnoreCase(args[i])) {
+ // @fixme if you mix file and other options it will be a mess,
+ // not important right now.
+ FileInputStream fis = new FileInputStream(args[i + 1]);
+ Properties props = new Properties();
+ props.load(fis);
+ fis.close();
+ init(props);
+ }
+ if ("-classnames".equalsIgnoreCase(args[i])) {
+ for (int j = i + 1; j < args.length; j++) {
+ if (args[j].startsWith("-"))
+ break;
+ addTestClassName(args[j]);
+ }
+ }
+ if ("-port".equalsIgnoreCase(args[i])) {
+ setPort(Integer.parseInt(args[i + 1]));
+ }
+ if ("-host".equalsIgnoreCase(args[i])) {
+ setHost(args[i + 1]);
+ }
+ if ("-debug".equalsIgnoreCase(args[i])) {
+ setDebug(true);
+ }
+ }
+ }
+ /**
+ * Initialize the TestRunner from properties.
+ * @param the properties containing configuration data.
+ * @see #init(String[])
+ */
+ protected void init(Properties props){
+ if ( props.getProperty("debug") != null ){
+ setDebug(true);
+ }
+ String port = props.getProperty("port");
+ if (port != null){
+ setPort(Integer.parseInt(port));
+ }
+ String host = props.getProperty("host");
+ if (host != null){
+ setHost(host);
+ }
+ String classnames = props.getProperty("classnames");
+ if (classnames != null){
+ StringTokenizer st = new StringTokenizer(classnames);
+ while (st.hasMoreTokens()){
+ addTestClassName( st.nextToken() );
+ }
+ }
+ }
+
+ public final void run() throws Exception {
+ if (testClassNames.size() == 0) {
+ throw new IllegalArgumentException("No TestCase specified");
+ }
+ connect();
+
+ testResult = new TestResult();
+ testResult.addListener(this);
+ runTests();
+
+ testResult.removeListener(this);
+ if (testResult != null) {
+ testResult.stop();
+ testResult = null;
+ }
+ }
+
+ /**
+ * Transform all classnames into instantiated Test.
+ * @throws Exception a generic exception that can be thrown while
+ * instantiating a test case.
+ */
+ protected Test[] getSuites() throws Exception {
+ final int count = testClassNames.size();
+ log("Extracting testcases from " + count + " classnames...");
+ final Vector suites = new Vector(count);
+ for (int i = 0; i < count; i++) {
+ String classname = (String) testClassNames.elementAt(i);
+ try {
+ Test test = JUnitHelper.getTest(null, classname);
+ if (test != null){
+ suites.addElement(test);
+ }
+ } catch (Exception e){
+ // notify log error instead ?
+ log("Could not get Test instance from " + classname);
+ log(e);
+ }
+ }
+ log("Extracted " + suites.size() + " testcases.");
+ Test[] array = new Test[suites.size()];
+ suites.copyInto(array);
+ return array;
+ }
+
+ /**
+ * @param testClassNames String array of full qualified class names of test classes
+ */
+ private void runTests() throws Exception {
+
+ Test[] suites = getSuites();
+
+ // count all testMethods and inform TestRunListeners
+ int count = countTests(suites);
+ log("Total tests to run: " + count);
+ writer.notifyTestRunStarted(count);
+
+ // send system properties to know for the JVM status
+ writer.notifySystemProperties();
+
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < suites.length; i++) {
+ if (suites[i] instanceof TestCase){
+ suites[i] = new TestSuite(suites[i].getClass().getName());
+ }
+ suites[i].run(testResult);
+ }
+
+ // inform TestRunListeners of test end
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ if (testResult == null || testResult.shouldStop()) {
+ writer.notifyTestRunStopped(elapsedTime);
+ } else {
+ writer.notifyTestRunEnded(elapsedTime);
+ }
+ log("Finished after " + elapsedTime + "ms");
+ shutDown();
+ }
+
+ /** count the number of test methods in all tests */
+ private final int countTests(Test[] tests) {
+ int count = 0;
+ for (int i = 0; i < tests.length; i++) {
+ count = count + tests[i].countTestCases();
+ }
+ return count;
+ }
+
+ protected void stop() {
+ if (testResult != null) {
+ testResult.stop();
+ }
+ }
+
+ /**
+ * connect to the specified host and port.
+ * @throws IOException if any error occurs during connection.
+ */
+ protected void connect() throws IOException {
+ log("Connecting to " + host + " on port " + port + "...");
+ clientSocket = new Socket(host, port);
+ writer = new MessageWriter(clientSocket.getOutputStream());
+ reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+ new StopThread().start();
+ }
+
+
+ protected void shutDown() {
+
+ if (writer != null) {
+ writer.close();
+ writer = null;
+ }
+
+ try {
+ if (reader != null) {
+ reader.close();
+ reader = null;
+ }
+ } catch (IOException e) {
+ log(e);
+ }
+
+ try {
+ if (clientSocket != null) {
+ clientSocket.close();
+ clientSocket = null;
+ }
+ } catch (IOException e) {
+ log(e);
+ }
+ }
+
+
+// -------- JUnit TestListener implementation
+
+
+ public void startTest(Test test) {
+ String testName = test.toString();
+ writer.notifyTestStarted(testName);
+ }
+
+ public void addError(Test test, Throwable t) {
+ String testName = test.toString();
+ String trace = StringUtils.getStackTrace(t);
+ writer.notifyTestFailed(TestRunListener.STATUS_ERROR, testName, trace);
+ }
+
+ /**
+ * this implementation is for JUnit < 3.4
+ * @see addFailure(Test, Throwable)
+ */
+ public void addFailure(Test test, AssertionFailedError afe) {
+ addFailure(test, (Throwable) afe);
+ }
+
+ /**
+ * This implementation is for JUnit <= 3.4
+ * @see addFailure(Test, AssertionFailedError)
+ */
+ public void addFailure(Test test, Throwable t) {
+ String testName = test.toString();
+ String trace = StringUtils.getStackTrace(t);
+ writer.notifyTestFailed(TestRunListener.STATUS_FAILURE, testName, trace);
+ }
+
+ public void endTest(Test test) {
+ String testName = test.toString();
+ writer.notifyTestEnded(testName);
+ }
+
+ public void log(String msg){
+ if (debug){
+ System.out.println(msg);
+ }
+ }
+
+ public void log(Throwable t){
+ if (debug){
+ t.printStackTrace();
+ }
+ }
+}
+