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(); + } + } +} +