org.eclipse.flux.client.java.AbstractFluxClientTest.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.flux.client.java.AbstractFluxClientTest.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Pivotal Software, Inc. and others.
 * All rights reserved. This program and the accompanying materials are made 
 * available under the terms of the Eclipse Public License v1.0 
 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
 * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
 *
 * Contributors:
 *     Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.flux.client.java;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import junit.framework.TestCase;

import org.eclipse.flux.client.MessageConnector;
import org.eclipse.flux.client.MessageHandler;
import org.eclipse.flux.client.SingleResponseHandler;
import org.eclipse.flux.client.util.BasicFuture;
import org.eclipse.flux.client.util.ExceptionUtil;
import org.json.JSONObject;

/**
 * Test harness for FluxClient. Subclass this class to use it.
 *  
 * @author Kris De Volder
 */
public abstract class AbstractFluxClientTest extends TestCase {

    public static abstract class ResponseHandler<T> {
        protected abstract T handle(String messageType, JSONObject msg) throws Exception;
    }

    /** 
     * Limits the duration of various operations in the test harness so that we
     * can, for example also write 'negative' tests that succeed only if
     * certain messages are not received (within the timeout).
     */
    public static final long TIMEOUT = 2000;

    /**
     * The expectation in the test harness is that Processes are meant to terminate naturally 
     * without raising exceptions, within a reasonable time. When a process gets stuck
     * this timeout kicks in to allow operations that are waiting for processes to terminate to proceed. 
     */
    public static final long STUCK_PROCESS_TIMEOUT = 60000;

    public static <T> void assertError(Class<? extends Throwable> expected, BasicFuture<T> r) {
        T result = null;
        Throwable error = null;
        try {
            result = r.get();
        } catch (Throwable e) {
            error = e;
        }
        assertTrue("Should have thrown " + expected.getName() + " but returned " + result,
                error != null && (expected.isAssignableFrom(error.getClass())
                        || expected.isAssignableFrom(ExceptionUtil.getDeepestCause(error).getClass())));
    }

    public static <T> void assertError(String expectContains, BasicFuture<T> r) {
        T result = null;
        Throwable error = null;
        try {
            result = r.get();
        } catch (Throwable e) {
            error = e;
        }
        assertTrue("Should have thrown '..." + expectContains + "...' but returned " + result,
                error != null && (contains(error.getMessage(), expectContains)
                        || contains(ExceptionUtil.getDeepestCause(error).getMessage(), expectContains)));
    }

    private Timer timer;

    /**
     * Javascript style 'setTimeout' useful for tests that are doing 'callback' style things rather thread-style waiting.
     */
    public void setTimeout(long delay, TimerTask task) {
        timer().schedule(task, delay);
    }

    private synchronized Timer timer() {
        if (timer == null) {
            timer = new Timer();
        }
        return timer;
    }

    private static boolean contains(String message, String expectContains) {
        return message != null && message.contains(expectContains);
    }

    private final List<Process<?>> processes = new ArrayList<>();

    protected abstract MessageConnector createConnection(String user) throws Exception;

    @Override
    protected void tearDown() throws Exception {
        try {
            super.tearDown();

            //ensure all processes are terminated.
            synchronized (processes) {
                for (Process<?> process : processes) {
                    assertTrue("Process not started", process.hasRun);
                    assertFalse("Poorly behaved tests, left a processes running", process.isAlive());
                }
            }
        } finally {
            //Make sure this gets executed no matter what or there will be Thread leakage!
            if (timer != null) {
                timer.cancel();
            }
        }
    }

    /**
     * A 'test process' is essentially a thread with some convenient methods to be
     * able to easily script test sequences that send / receive messages to / from 
     * a flux connection. There is also a built-in timeout mechanism that ensures
     * no test process runs forever. To use this class simply subclass (typically
     * with a anonymous class) and implement the 'execute' method.
     * <p>
     * A well behaved process should terminate naturally without throwing an exception.
     * The test harness tries to detect if a process is not well behaved.
     */
    public abstract class Process<T> extends Thread {

        protected MessageConnector conn;
        public final BasicFuture<T> result;
        boolean hasRun = false; //To be able to detect mistakes in tests where a process is created but never started.
        // It doesn't really make sense to create a Process if this process is never being run
        // so this almost certainly means there's a bug in the test that created the process.

        public Process(String user) throws Exception {
            this.result = new BasicFuture<>();
            this.conn = createConnection(user);
            this.conn.connectToChannelSync(user);
            this.result.setTimeout(STUCK_PROCESS_TIMEOUT);
            synchronized (processes) {
                processes.add(this);
            }
        }

        @Override
        public final void run() {
            hasRun = true;
            try {
                this.result.resolve(execute());
            } catch (Throwable e) {
                result.reject(e);
            } finally {
                this.conn.disconnect();
            }
        }

        public void send(String type, JSONObject msg) throws Exception {
            conn.send(type, msg);
        }

        /**
         * Asynchronously send a request and return a Future with the response.
         */
        public <R> BasicFuture<R> asendRequest(final String messageType, JSONObject msg,
                final ResponseHandler<R> responseHandler) throws Exception {
            SingleResponseHandler<R> response = new SingleResponseHandler<R>(conn, responseType(messageType)) {
                @Override
                protected R parse(String messageType, JSONObject message) throws Exception {
                    return responseHandler.handle(messageType, message);
                }
            };
            conn.addMessageHandler(response);
            send(messageType, msg);
            return response.getFuture();
        }

        /**
         * Synchronously send a request and return the response.
         */
        public <R> R sendRequest(String messageType, JSONObject msg, ResponseHandler<R> responseHandler)
                throws Exception {
            return asendRequest(messageType, msg, responseHandler).get();
        }

        private String responseType(String messageType) {
            if (messageType.endsWith("Request")) {
                return messageType.substring(0, messageType.length() - "Request".length()) + "Response";
            }
            throw new IllegalArgumentException("Not a 'Request' message type: " + messageType);
        }

        /**
         * Asynchronous receive. Returns a BasicFuture that resolves when message
         * is received.
         */
        public BasicFuture<JSONObject> areceive(String type) {
            final BasicFuture<JSONObject> result = new BasicFuture<JSONObject>();
            result.setTimeout(TIMEOUT);
            once(new MessageHandler(type) {
                public void handle(String type, JSONObject message) {
                    result.resolve(message);
                }
            });
            return result;
        }

        /**
         * Synchronous receive. Blocks until message of given type is received.
         */
        public JSONObject receive(String type) throws Exception {
            return areceive(type).get();
        }

        public void once(final MessageHandler messageHandler) {
            conn.addMessageHandler(new MessageHandler(messageHandler.getMessageType()) {
                @Override
                public boolean canHandle(String type, JSONObject message) {
                    return messageHandler.canHandle(type, message);
                }

                @Override
                public void handle(String type, JSONObject message) {
                    conn.removeMessageHandler(this);
                    messageHandler.handle(type, message);
                }
            });
        }

        protected abstract T execute() throws Exception;

    }

    /**
     * Run a bunch of processes by starting them in the provided order. Once all processes are running,
     * block until all of them complete. If any one of the processes is terminated by an Exception then
     * 'run' guarantees that at least one of the exceptions is re-thrown
     */
    public void run(Process<?>... processes) throws Exception {
        for (Process<?> process : processes) {
            process.start();
        }
        await(processes);
    }

    public void await(Process<?>... processes) throws Exception {
        Throwable error = null;
        for (Process<?> process : processes) {
            try {
                process.result.get();
            } catch (Throwable e) {
                e.printStackTrace();
                if (error == null) {
                    error = e;
                }
            }
        }
        if (error != null) {
            throw ExceptionUtil.exception(error);
        }
        //Allthough the work the Processes are doing is 'finished' It is possible the threads themselves
        // are still 'busy' for a brief time thereafter so wait for the threads to die.
        for (Process<?> process : processes) {
            process.join(500); //shouldn't be long (unless test is ill-behaved and process is 'stuck', but then it would 
            // not be possible to reach this point, since at least a TimeoutException will be raised
            // above as a result of that 'stuck' Process's result.promise timing out.
        }
    }

}