/******************************************************************************* * 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.model; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.PushbackReader; import java.io.UnsupportedEncodingException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.SafeRunner; import com.technophobia.substeps.FeatureRunnerPlugin; import com.technophobia.substeps.model.MessageIds.MessageStatus; /** * The client side of the RemoteTestRunner. Handles the marshaling of the * different messages. */ public class RemoteTestRunnerClient { public abstract class ListenerSafeRunnable implements ISafeRunnable { @Override public void handleException(final Throwable exception) { FeatureRunnerPlugin.log(exception); } } /** * A simple state machine to process requests from the RemoteTestRunner */ abstract class ProcessingState { abstract ProcessingState readMessage(String message); } class DefaultProcessingState extends ProcessingState { @Override ProcessingState readMessage(final String message) { if (message.startsWith(MessageIds.TRACE_START)) { failedTrace.setLength(0); return traceState; } if (message.startsWith(MessageIds.EXPECTED_START)) { expectedResult.setLength(0); return expectedState; } if (message.startsWith(MessageIds.ACTUAL_START)) { actualResult.setLength(0); return actualState; } if (message.startsWith(MessageIds.RTRACE_START)) { failedRerunTrace.setLength(0); return rerunState; } final String arg = message.substring(MessageIds.MSG_HEADER_LENGTH); if (message.startsWith(MessageIds.TEST_RUN_START)) { // version < 2 format: count // version >= 2 format: count+" "+version int count = 0; final int v = arg.indexOf(' '); if (v == -1) { version = "v1"; //$NON-NLS-1$ count = Integer.parseInt(arg); } else { version = arg.substring(v + 1); final String sc = arg.substring(0, v); count = Integer.parseInt(sc); } notifyTestRunStarted(count); return this; } if (message.startsWith(MessageIds.TEST_START)) { notifyTestStarted(arg); return this; } if (message.startsWith(MessageIds.TEST_END)) { notifyTestEnded(arg); return this; } if (message.startsWith(MessageIds.TEST_ERROR)) { extractFailure(arg, MessageIds.MessageStatus.ERROR); return this; } if (message.startsWith(MessageIds.TEST_FAILED)) { extractFailure(arg, MessageIds.MessageStatus.FAILURE); return this; } if (message.startsWith(MessageIds.TEST_RUN_END)) { final long elapsedTime = Long.parseLong(arg); testRunEnded(elapsedTime); return this; } if (message.startsWith(MessageIds.TEST_STOPPED)) { final long elapsedTime = Long.parseLong(arg); notifyTestRunStopped(elapsedTime); shutDown(); return this; } if (message.startsWith(MessageIds.TEST_TREE)) { notifyTestTreeEntry(arg); return this; } if (message.startsWith(MessageIds.TEST_RERAN)) { if (hasTestId()) scanReranMessage(arg); else scanOldReranMessage(arg); return this; } return this; } } /** * Base class for states in which messages are appended to an internal * string buffer until an end message is read. */ class AppendingProcessingState extends ProcessingState { private final StringBuffer fBuffer; private final String fEndString; AppendingProcessingState(final StringBuffer buffer, final String endString) { this.fBuffer = buffer; this.fEndString = endString; } @Override ProcessingState readMessage(final String message) { if (message.startsWith(fEndString)) { entireStringRead(); return defaultState; } fBuffer.append(message); if (lastLineDelimiter != null) fBuffer.append(lastLineDelimiter); return this; } /** * subclasses can override to do special things when end message is read */ void entireStringRead() { // No-op } } class TraceProcessingState extends AppendingProcessingState { TraceProcessingState() { super(failedTrace, MessageIds.TRACE_END); } @Override void entireStringRead() { notifyTestFailed(); expectedResult.setLength(0); actualResult.setLength(0); } @Override ProcessingState readMessage(final String message) { if (message.startsWith(MessageIds.TRACE_END)) { notifyTestFailed(); failedTrace.setLength(0); actualResult.setLength(0); expectedResult.setLength(0); return defaultState; } failedTrace.append(message); if (lastLineDelimiter != null) failedTrace.append(lastLineDelimiter); return this; } } /** * The failed trace that is currently reported from the RemoteTestRunner */ private final StringBuffer failedTrace = new StringBuffer(); /** * The expected test result */ private final StringBuffer expectedResult = new StringBuffer(); /** * The actual test result */ private final StringBuffer actualResult = new StringBuffer(); /** * The failed trace of a reran test */ private final StringBuffer failedRerunTrace = new StringBuffer(); ProcessingState defaultState = new DefaultProcessingState(); ProcessingState traceState = new TraceProcessingState(); ProcessingState expectedState = new AppendingProcessingState(expectedResult, MessageIds.EXPECTED_END); ProcessingState actualState = new AppendingProcessingState(actualResult, MessageIds.ACTUAL_END); ProcessingState rerunState = new AppendingProcessingState(failedRerunTrace, MessageIds.RTRACE_END); ProcessingState currentState = defaultState; /** * An array of listeners that are informed about test events. */ private SubstepsRunListener[] listeners; /** * The server socket */ private ServerSocket serverSocket; private Socket socket; private int port = -1; private PrintWriter writer; private PushbackReader pushbackReader; private String lastLineDelimiter; /** * The protocol version */ private String version; /** * The failed test that is currently reported from the RemoteTestRunner */ private String failedTest; /** * The Id of the failed test */ private String failedTestId; /** * The kind of failure of the test that is currently reported as failed */ private MessageStatus failureKind; private final boolean debug = false; /** * Reads the message stream from the RemoteTestRunner */ private class ServerConnection extends Thread { int serverPort; public ServerConnection(final int port) { super("ServerConnection"); //$NON-NLS-1$ this.serverPort = port; } @Override public void run() { try { if (debug) System.out.println("Creating server socket " + serverPort); //$NON-NLS-1$ serverSocket = new ServerSocket(serverPort); socket = serverSocket.accept(); try { pushbackReader = new PushbackReader(new BufferedReader(new InputStreamReader( socket.getInputStream(), "UTF-8"))); //$NON-NLS-1$ } catch (final UnsupportedEncodingException e) { pushbackReader = new PushbackReader(new BufferedReader(new InputStreamReader( socket.getInputStream()))); } try { writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); //$NON-NLS-1$ } catch (final UnsupportedEncodingException e1) { writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true); } String message; while (pushbackReader != null && (message = readMessage(pushbackReader)) != null) receiveMessage(message); } catch (final SocketException e) { notifyTestRunTerminated(); } catch (final IOException e) { FeatureRunnerPlugin.log(e); // fall through } shutDown(); } } /** * Start listening to a test run. Start a server connection that the * RemoteTestRunner can connect to. * * @param listeners * listeners to inform * @param port * port on which the server socket will be opened */ public synchronized void startListening(final SubstepsRunListener[] l, final int p) { this.listeners = l; this.port = p; final ServerConnection connection = new ServerConnection(port); connection.start(); } /** * Requests to stop the remote test run. */ public synchronized void stopTest() { if (isRunning()) { writer.println(MessageIds.TEST_STOP); writer.flush(); } } public synchronized void stopWaiting() { if (serverSocket != null && !serverSocket.isClosed() && socket == null) { shutDown(); // will throw a SocketException in Threads that wait in // ServerSocket#accept() } } private synchronized void shutDown() { if (debug) System.out.println("shutdown " + port); //$NON-NLS-1$ if (writer != null) { writer.close(); writer = null; } try { if (pushbackReader != null) { pushbackReader.close(); pushbackReader = null; } } catch (final IOException e) { // No-op } try { if (socket != null) { socket.close(); socket = null; } } catch (final IOException e) { // No-op } try { if (serverSocket != null) { serverSocket.close(); serverSocket = null; } } catch (final IOException e) { // No-op } } public boolean isRunning() { return socket != null; } private String readMessage(final PushbackReader in) throws IOException { final StringBuffer buf = new StringBuffer(128); int ch; while ((ch = in.read()) != -1) { if (ch == '\n') { lastLineDelimiter = "\n"; //$NON-NLS-1$ return buf.toString(); } else if (ch == '\r') { ch = in.read(); if (ch == '\n') { lastLineDelimiter = "\r\n"; //$NON-NLS-1$ } else { in.unread(ch); lastLineDelimiter = "\r"; //$NON-NLS-1$ } return buf.toString(); } else { buf.append((char) ch); } } lastLineDelimiter = null; if (buf.length() == 0) return null; return buf.toString(); } private void receiveMessage(final String message) { this.currentState = currentState.readMessage(message); } private void scanOldReranMessage(final String arg) { // OLD V1 format // format: className" "testName" "status // status: FAILURE, ERROR, OK final int c = arg.indexOf(" "); //$NON-NLS-1$ final int t = arg.indexOf(" ", c + 1); //$NON-NLS-1$ final String className = arg.substring(0, c); final String testName = arg.substring(c + 1, t); final String status = arg.substring(t + 1); final String testId = className + testName; notifyTestReran(testId, className, testName, status); } private void scanReranMessage(final String arg) { // format: testId" "className" "testName" "status // status: FAILURE, ERROR, OK final int i = arg.indexOf(' '); final int c = arg.indexOf(' ', i + 1); final int t = arg.indexOf(' ', c + 1); final String testId = arg.substring(0, i); final String className = arg.substring(i + 1, c); final String testName = arg.substring(c + 1, t); final String status = arg.substring(t + 1); notifyTestReran(testId, className, testName, status); } private void notifyTestReran(final String testId, final String className, final String testName, final String status) { MessageStatus statusCode = MessageIds.MessageStatus.OK; if (status.equals("FAILURE")) //$NON-NLS-1$ statusCode = MessageIds.MessageStatus.FAILURE; else if (status.equals("ERROR")) //$NON-NLS-1$ statusCode = MessageIds.MessageStatus.ERROR; String trace = ""; //$NON-NLS-1$ if (!statusCode.equals(MessageStatus.OK)) trace = failedRerunTrace.toString(); // assumption a rerun trace was sent before notifyTestReran(testId, className, testName, statusCode, trace); } private void extractFailure(final String arg, final MessageStatus status) { final String s[] = extractTestId(arg); failedTestId = s[0]; failedTest = s[1]; failureKind = status; } /** * @param arg * test name * @return an array with two elements. The first one is the testId, the * second one the testName. */ String[] extractTestId(final String arg) { final String[] result = new String[2]; if (!hasTestId()) { result[0] = arg; // use the test name as the test Id result[1] = arg; return result; } final int i = arg.indexOf(','); result[0] = arg.substring(0, i); result[1] = arg.substring(i + 1, arg.length()); return result; } private boolean hasTestId() { if (version == null) // TODO fix me return true; return version.equals("v2"); //$NON-NLS-1$ } private void notifyTestReran(final String testId, final String className, final String testName, final MessageStatus status, final String trace) { for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { listener.testReran(testId, className, testName, status.toStatus(), trace, nullifyEmpty(expectedResult), nullifyEmpty(actualResult)); } }); } } private void notifyTestTreeEntry(final String treeEntry) { for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; if (!hasTestId()) listener.testTreeEntry(fakeTestId(treeEntry)); else listener.testTreeEntry(treeEntry); } } private String fakeTestId(final String treeEntry) { // extract the test name and add it as the testId final int index0 = treeEntry.indexOf(','); final String testName = treeEntry.substring(0, index0).trim(); return testName + "," + treeEntry; //$NON-NLS-1$ } private void notifyTestRunStopped(final long elapsedTime) { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { listener.testRunStopped(elapsedTime); } }); } } private void testRunEnded(final long elapsedTime) { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { listener.testRunEnded(elapsedTime); } }); } } private void notifyTestEnded(final String test) { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { final String s[] = extractTestId(test); listener.testEnded(s[0], s[1]); } }); } } private void notifyTestStarted(final String test) { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { final String s[] = extractTestId(test); listener.testStarted(s[0], s[1]); } }); } } private void notifyTestRunStarted(final int count) { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { listener.testRunStarted(count); } }); } } private void notifyTestFailed() { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { listener.testFailed(failureKind.toStatus(), failedTestId, failedTest, failedTrace.toString(), nullifyEmpty(expectedResult), nullifyEmpty(actualResult)); } }); } } /** * Returns a comparison result from the given buffer. Removes the * terminating line delimiter. * * @param buf * the comparison result * @return the result or <code>null</code> if empty * @since 3.7 */ private static String nullifyEmpty(final StringBuffer buf) { final int length = buf.length(); if (length == 0) return null; final char last = buf.charAt(length - 1); if (last == '\n') { if (length > 1 && buf.charAt(length - 2) == '\r') return buf.substring(0, length - 2); return buf.substring(0, length - 1); } else if (last == '\r') { return buf.substring(0, length - 1); } return buf.toString(); } private void notifyTestRunTerminated() { if (FeatureRunnerPlugin.instance().isStopped()) return; for (int i = 0; i < listeners.length; i++) { final SubstepsRunListener listener = listeners[i]; SafeRunner.run(new ListenerSafeRunnable() { @Override public void run() { listener.testRunTerminated(); } }); } } public void rerunTest(final String testId, final String className, final String testName) { if (isRunning()) { actualResult.setLength(0); expectedResult.setLength(0); writer.println(MessageIds.TEST_RERUN + testId + " " + className + " " + testName); //$NON-NLS-1$ //$NON-NLS-2$ writer.flush(); } } }