/******************************************************************************* * Copyright Technophobia Ltd 2012 * * This file is part of the Substeps Eclipse Plugin. * * The Substeps Eclipse Plugin is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License v1.0. * * The Substeps Eclipse Plugin 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 * Eclipse Public License for more details. * * You should have received a copy of the Eclipse Public License * along with the Substeps Eclipse Plugin. If not, see <http://www.eclipse.org/legal/epl-v10.html>. ******************************************************************************/ package com.technophobia.substeps.runner; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.util.Arrays; import java.util.List; import java.util.Vector; import com.technophobia.substeps.model.MessageIds; import com.technophobia.substeps.runner.junit4.JUnit4TestLoader; //import com.technophobia.substeps.junit.ui.SubstepsFeatureMessages; /** * A TestRunner that reports results via a socket connection. See MessageIds for * more information about the protocol. */ public class RemoteTestRunner implements MessageSender, IVisitsTestTrees { /** * Holder for information for a rerun request */ private static class RerunRequest { String rerunClassName; String rerunTestName; int rerunTestId; public RerunRequest(final int testId, final String className, final String testName) { this.rerunTestId = testId; this.rerunClassName = className; this.rerunTestName = testName; } } public enum ReranStatus { OK, FAILURE, ERROR; } /** * The name of the test classes to be executed */ private String[] testClassNames; /** * The name of the test (argument -test) */ private String testName; /** * The current test result */ private TestExecution execution; /** * The version expected by the client */ private String version = ""; //$NON-NLS-1$ /** * The client socket. */ private Socket clientSocket; /** * Print writer for sending messages */ private PrintWriter writer; /** * Reader for incoming messages */ private BufferedReader reader; /** * Host to connect to, default is the localhost */ private String host = ""; //$NON-NLS-1$ /** * Port to connect to. */ private int port = -1; /** * Is the debug mode enabled? */ private boolean debugMode = false; /** * Keep the test run server alive after a test run has finished. This allows * to rerun tests. */ private boolean keepAlive = false; /** * Has the server been stopped */ private boolean stopped = false; /** * Queue of rerun requests. */ private final Vector<RerunRequest> rerunRequests = new Vector<RerunRequest>(10); /** * Thread reading from the socket */ private ReaderThread readerThread; private String rerunTest; private final TestIdMap ids = new TestIdMap(); private String[] failureNames; private ITestLoader loader; private MessageSender sender; private boolean consoleMode = false; /** * Reader thread that processes messages from the client. */ private class ReaderThread extends Thread { public ReaderThread() { super("ReaderThread"); //$NON-NLS-1$ } @Override public void run() { try { String message = null; while (true) { if ((message = reader.readLine()) != null) { if (message.startsWith(MessageIds.TEST_STOP)) { stopped = true; RemoteTestRunner.this.stop(); synchronized (RemoteTestRunner.this) { RemoteTestRunner.this.notifyAll(); } break; } else if (message.startsWith(MessageIds.TEST_RERUN)) { final String arg = message.substring(MessageIds.MSG_HEADER_LENGTH); // format: testId className testName final int c0 = arg.indexOf(' '); final int c1 = arg.indexOf(' ', c0 + 1); final String s = arg.substring(0, c0); final int testId = Integer.parseInt(s); final String className = arg.substring(c0 + 1, c1); final String name = arg.substring(c1 + 1, arg.length()); synchronized (RemoteTestRunner.this) { rerunRequests.add(new RerunRequest(testId, className, name)); RemoteTestRunner.this.notifyAll(); } } } } } catch (final Exception e) { RemoteTestRunner.this.stop(); } } } public RemoteTestRunner() { setMessageSender(this); } public void setMessageSender(final MessageSender sender) { this.sender = sender; } /** * The main entry point. * * @param args * Parameters: * * <pre> * -classnames: the name of the test suite class * -testfilename: the name of a file containing classnames of test suites * -test: the test method name (format classname testname) * -host: the host to connect to default local host * -port: the port to connect to, mandatory argument * -keepalive: keep the process alive after a test run * </pre> */ public static void main(final String[] args) { try { final RemoteTestRunner testRunServer = new RemoteTestRunner(); testRunServer.init(args); testRunServer.run(); } catch (final Throwable e) { e.printStackTrace(); // don't allow System.exit(0) to swallow // exceptions } finally { // fix for 14434 System.exit(0); } } /** * Parse command line arguments. Hook for subclasses to process additional * arguments. * * @param args * the arguments */ protected void init(final String[] args) { defaultInit(args); } /** * The class loader to be used for loading tests. Subclasses may override to * use another class loader. * * @return the class loader to lead test classes */ protected ClassLoader getTestClassLoader() { return getClass().getClassLoader(); } /** * Process the default arguments. * * @param args * arguments */ protected final void defaultInit(final String[] args) { for (int i = 0; i < args.length; i++) { if (args[i].toLowerCase().startsWith("classnames") || args[i].toLowerCase().equals("classname")) { //$NON-NLS-1$ //$NON-NLS-2$ final List<String> list = valueAsList(args[i]); testClassNames = list.toArray(new String[list.size()]); } else if (args[i].toLowerCase().startsWith("test")) { //$NON-NLS-1$ String name = valueAsString(args[i]); final int p = testName.indexOf(':'); if (p == -1) throw new IllegalArgumentException("Testname not separated by \'%\'"); //$NON-NLS-1$ name = name.substring(p + 1); testClassNames = new String[] { name.substring(0, p) }; } else if (args[i].toLowerCase().startsWith("testnamefile")) { //$NON-NLS-1$ final String testNameFile = valueAsString(args[i]); try { readTestNames(testNameFile); } catch (final IOException e) { throw new IllegalArgumentException("Cannot read testname file."); //$NON-NLS-1$ } } else if (args[i].toLowerCase().startsWith("testfailures")) { //$NON-NLS-1$ final String testFailuresFile = valueAsString(args[i]); try { readFailureNames(testFailuresFile); } catch (final IOException e) { throw new IllegalArgumentException("Cannot read testfailures file."); //$NON-NLS-1$ } } else if (args[i].toLowerCase().startsWith("port")) { //$NON-NLS-1$ port = Integer.parseInt(valueAsString(args[i])); } else if (args[i].toLowerCase().startsWith("host")) { //$NON-NLS-1$ host = valueAsString(args[i]); } else if (args[i].toLowerCase().startsWith("rerun")) { //$NON-NLS-1$ rerunTest = valueAsString(args[i]); } else if (args[i].toLowerCase().startsWith("keepalive")) { //$NON-NLS-1$ keepAlive = true; } else if (args[i].toLowerCase().startsWith("debugging") || args[i].toLowerCase().startsWith("debug")) { //$NON-NLS-1$ //$NON-NLS-2$ debugMode = true; } else if (args[i].toLowerCase().startsWith("version")) { //$NON-NLS-1$ version = valueAsString(args[i]); } else if (args[i].toLowerCase().startsWith("junitconsole")) { //$NON-NLS-1$ consoleMode = true; } else if (args[i].toLowerCase().startsWith("testloaderclass")) { //$NON-NLS-1$ final String className = valueAsString(args[i]); createLoader(className); i++; } } if (getTestLoader() == null) initDefaultLoader(); if (testClassNames == null || testClassNames.length == 0) throw new IllegalArgumentException("Error: parameter '-classNames' or '-className' not specified"); if (port == -1) throw new IllegalArgumentException("Error: parameter '-port' not specified"); if (debugMode) System.out.println("keepalive " + keepAlive); //$NON-NLS-1$ } public void initDefaultLoader() { createLoader(JUnit4TestLoader.class.getName()); } public void createLoader(final String className) { setLoader(createRawTestLoader(className)); } protected ITestLoader createRawTestLoader(final String className) { try { return (ITestLoader) loadTestLoaderClass(className).newInstance(); } catch (final Exception e) { final StringWriter trace = new StringWriter(); e.printStackTrace(new PrintWriter(trace)); final String message = "Error: test loader "+className+" not found:\n"+trace.toString(); throw new IllegalArgumentException(message); } } protected Class<?> loadTestLoaderClass(final String className) throws ClassNotFoundException { return Class.forName(className); } public void setLoader(final ITestLoader newInstance) { loader = newInstance; } private void readTestNames(final String testNameFile) throws IOException { final BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(testNameFile)), "UTF-8")); //$NON-NLS-1$ try { String line; final Vector<String> list = new Vector<String>(); while ((line = br.readLine()) != null) { list.add(line); } testClassNames = list.toArray(new String[list.size()]); } finally { br.close(); } if (debugMode) { System.out.println("Tests:"); //$NON-NLS-1$ for (int i = 0; i < testClassNames.length; i++) { System.out.println(" " + testClassNames[i]); //$NON-NLS-1$ } } } private void readFailureNames(final String testFailureFile) throws IOException { final BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream( new File(testFailureFile)), "UTF-8")); //$NON-NLS-1$ try { String line; final Vector<String> list = new Vector<String>(); while ((line = br.readLine()) != null) { list.add(line); } failureNames = list.toArray(new String[list.size()]); } finally { br.close(); } if (debugMode) { System.out.println("Failures:"); //$NON-NLS-1$ for (int i = 0; i < failureNames.length; i++) { System.out.println(" " + failureNames[i]); //$NON-NLS-1$ } } } /** * Connects to the remote ports and runs the tests. */ protected void run() { if (!connect()) return; if (rerunTest != null) { rerunTest(new RerunRequest(Integer.parseInt(rerunTest), testClassNames[0], testName)); return; } final FirstRunExecutionListener listener = firstRunExecutionListener(); execution = new TestExecution(listener, getClassifier()); runTests(execution); if (keepAlive) waitForReruns(); shutDown(); } public FirstRunExecutionListener firstRunExecutionListener() { return new FirstRunExecutionListener(sender, ids); } /** * Waits for rerun requests until an explicit stop request */ private synchronized void waitForReruns() { while (!stopped) { try { wait(); if (!stopped && rerunRequests.size() > 0) { final RerunRequest r = rerunRequests.remove(0); rerunTest(r); } } catch (final InterruptedException e) { // No-op } } } public void runFailed(final String message, final Exception exception) { // TODO: remove System.err.println? System.err.println(message); if (exception != null) exception.printStackTrace(System.err); } protected Class<?>[] loadClasses(final String[] classNames) { final Vector<Class<?>> classes = new Vector<Class<?>>(); for (int i = 0; i < classNames.length; i++) { final String name = classNames[i]; final Class<?> clazz = loadClass(name, this); if (clazz != null) { classes.add(clazz); } } return classes.toArray(new Class[classes.size()]); } protected void notifyListenersOfTestEnd(final TestExecution testExecution, final long testStartTime) { if (testExecution == null || testExecution.shouldStop()) notifyTestRunStopped(System.currentTimeMillis() - testStartTime); else notifyTestRunEnded(System.currentTimeMillis() - testStartTime); } /** * Runs a set of tests. * * @param testClassNames * classes to be run * @param testName * individual method to be run * @param execution * executor */ public void runTests(final String[] classNames, final String name, final TestExecution testExecution) { final ITestReference[] suites = loader.loadTests(loadClasses(classNames), name, failureNames, this); // count all testMethods and inform ITestRunListeners final int count = countTests(suites); notifyTestRunStarted(count); if (count == 0) { notifyTestRunEnded(0); return; } sendTrees(suites); final long testStartTime = System.currentTimeMillis(); testExecution.run(suites); notifyListenersOfTestEnd(testExecution, testStartTime); } private void sendTrees(final ITestReference[] suites) { final long startTime = System.currentTimeMillis(); if (debugMode) System.out.print("start send tree..."); //$NON-NLS-1$ for (int i = 0; i < suites.length; i++) { suites[i].sendTree(this); } if (debugMode) System.out.println("done send tree - time(ms): " + (System.currentTimeMillis() - startTime)); //$NON-NLS-1$ } private int countTests(final ITestReference[] tests) { int count = 0; for (int i = 0; i < tests.length; i++) { final ITestReference test = tests[i]; if (test != null) count = count + test.countTestCases(); } return count; } /** * Reruns a test as defined by the fully qualified class name and the name * of the test. * * @param r * rerun request */ public void rerunTest(final RerunRequest r) { final Class<?>[] classes = loadClasses(new String[] { r.rerunClassName }); final ITestReference rerunTest1 = loader.loadTests(classes, r.rerunTestName, null, this)[0]; final RerunExecutionListener service = rerunExecutionListener(); final TestExecution testExecution = new TestExecution(service, getClassifier()); final ITestReference[] suites = new ITestReference[] { rerunTest1 }; testExecution.run(suites); notifyRerunComplete(r, service.getStatus()); } public RerunExecutionListener rerunExecutionListener() { return new RerunExecutionListener(sender, ids); } protected IClassifiesThrowables getClassifier() { return new DefaultClassifier(version); } @Override public void visitTreeEntry(final ITestIdentifier id, final boolean b, final int i) { notifyTestTreeEntry(getTestId(id) + ',' + escapeComma(id.getName()) + ',' + b + ',' + i); } private String escapeComma(final String s) { if ((s.indexOf(',') < 0) && (s.indexOf('\\') < 0)) return s; final StringBuffer sb = new StringBuffer(s.length() + 10); for (int i = 0; i < s.length(); i++) { final char c = s.charAt(i); if (c == ',') sb.append("\\,"); //$NON-NLS-1$ else if (c == '\\') sb.append("\\\\"); //$NON-NLS-1$ else sb.append(c); } return sb.toString(); } // WANT: work in bug fixes since RC2? private String getTestId(final ITestIdentifier id) { return ids.getTestId(id); } /** * Stop the current test run. */ protected void stop() { if (execution != null) { execution.stop(); } } /** * Connect to the remote test listener. * * @return <code>true</code> if connection successful, <code>false</code> if * failed */ protected boolean connect() { if (consoleMode) { clientSocket = null; writer = new PrintWriter(System.out); reader = new BufferedReader(new InputStreamReader(System.in)); readerThread = new ReaderThread(); readerThread.start(); return true; } if (debugMode) System.out.println("RemoteTestRunner: trying to connect" + host + ":" + port); Exception exception = null; for (int i = 1; i < 20; i++) { try { clientSocket = new Socket(host, port); try { writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8")), false/* true */); //$NON-NLS-1$ } catch (final UnsupportedEncodingException e1) { writer = new PrintWriter( new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())), false/* true */); } try { reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); //$NON-NLS-1$ } catch (final UnsupportedEncodingException e1) { reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); } readerThread = new ReaderThread(); readerThread.start(); return true; } catch (final IOException e) { exception = e; } try { Thread.sleep(2000); } catch (final InterruptedException e) { // No-op } } runFailed("Could not connect to: "+host+" : "+port , exception); return false; } /** * Shutsdown the connection to the remote test listener. */ private void shutDown() { if (writer != null) { writer.close(); writer = null; } try { if (readerThread != null) { // interrupt reader thread so that we don't block on close // on a lock held by the BufferedReader // fix for bug: 38955 readerThread.interrupt(); } if (reader != null) { reader.close(); reader = null; } } catch (final IOException e) { if (debugMode) e.printStackTrace(); } try { if (clientSocket != null) { clientSocket.close(); clientSocket = null; } } catch (final IOException e) { if (debugMode) e.printStackTrace(); } } /* * @see * org.eclipse.jdt.internal.junit.runner.MessageSender#sendMessage(java. * lang.String) */ @Override public void sendMessage(final String msg) { if (writer == null) return; writer.println(msg); // if (!fConsoleMode) // System.out.println(msg); } protected void notifyTestRunStarted(final int testCount) { sender.sendMessage(MessageIds.TEST_RUN_START + testCount + " " + "v2"); //$NON-NLS-1$ //$NON-NLS-2$ } private void notifyTestRunEnded(final long elapsedTime) { sender.sendMessage(MessageIds.TEST_RUN_END + elapsedTime); sender.flush(); // shutDown(); } protected void notifyTestRunStopped(final long elapsedTime) { sender.sendMessage(MessageIds.TEST_STOPPED + elapsedTime); sender.flush(); // shutDown(); } protected void notifyTestTreeEntry(final String treeEntry) { sender.sendMessage(MessageIds.TEST_TREE + treeEntry); } /* * @see org.eclipse.jdt.internal.junit.runner.RerunCompletionListener# * notifyRerunComplete(org.eclipse.jdt.internal.junit.runner.RerunRequest, * java.lang.String) */ public void notifyRerunComplete(final RerunRequest r, final ReranStatus status) { if (port != -1) { sender.sendMessage(MessageIds.TEST_RERAN + r.rerunTestId + " " + r.rerunClassName + " " + r.rerunTestName + " " + status); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ sender.flush(); } } @Override public void flush() { writer.flush(); } /* * (non-Javadoc) * * @see * org.eclipse.jdt.internal.junit.runner.TestRunner#runTests(org.eclipse * .jdt.internal.junit.runner.RemoteTestRunner.TestExecution) */ public void runTests(final TestExecution testExecution) { runTests(testClassNames, testName, testExecution); } public ITestLoader getTestLoader() { return loader; } public Class<?> loadClass(final String className, final RemoteTestRunner listener) { Class<?> clazz = null; try { clazz = getTestClassLoader().loadClass(className); } catch (final ClassNotFoundException e) { listener.runFailed("Class not found "+className, e); } return clazz; } private List<String> valueAsList(String arg){ String[] kv = arg.split("="); if(kv.length != 2){ // No idea what to do - should be key=value return null; } String[] listItems = kv[1].split(","); return Arrays.asList(listItems); } private String valueAsString(String arg){ String[] kv = arg.split("="); if(kv.length != 2){ // No idea what to do - should be key=value return null; } return kv[1]; } }