All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robovm.junit.client.TestClient Maven / Gradle / Ivy

There is a newer version: 2.3.22
Show newest version
/*
 * Copyright (C) 2014 RoboVM AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.robovm.junit.client;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;

import org.apache.commons.io.IOUtils;
import org.junit.runner.notification.RunListener;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.OS;
import org.robovm.compiler.plugin.LaunchPlugin;
import org.robovm.compiler.plugin.PluginArgument;
import org.robovm.compiler.plugin.PluginArguments;
import org.robovm.compiler.target.LaunchParameters;
import org.robovm.compiler.target.ios.IOSTarget;
import org.robovm.compiler.util.io.Fifos;
import org.robovm.compiler.util.io.OpenOnReadFileInputStream;
import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream;
import org.robovm.junit.protocol.Command;
import org.robovm.junit.protocol.ResultObject;
import org.robovm.junit.protocol.ResultType;
import org.robovm.libimobiledevice.IDevice;
import org.robovm.libimobiledevice.IDeviceConnection;

import rx.Observable;
import rx.Subscriber;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

/**
 * Client side of the bridge between the tester (IDE, Maven, Gradle, etc) and
 * the testee (console, simulator, device).
 */
public class TestClient extends LaunchPlugin {

    private static class Waiter implements Runnable {
        CountDownLatch c = new CountDownLatch(1);

        public void run() {
            c.countDown();
        }

        public void await() throws InterruptedException {
            c.await();
        }
    }

    private static class Terminator extends Waiter {}

    public static final String SERVER_WRAPPER_CLASS_NAME = "org.robovm.objc.NonUICodeWrapper";
    public static final String SERVER_CLASS_NAME = "org.robovm.junit.server.TestServer";

    private ServerPortReader serverPortReader;
    private File oldStdOutFifo;
    private File newStdOutFifo;
    private OutputStream defaultStdOutStream;
    private LinkedBlockingQueue runQueue = new LinkedBlockingQueue<>();
    private RunListener runListener;
    private String mainClassName = SERVER_CLASS_NAME;
    private List runArgs = Collections.emptyList();

    public TestClient() {}

    public void setMainClass(Class mainClass) {
        this.mainClassName = mainClass.getName();
    }

    public void setRunArgs(List runArgs) {
        this.runArgs = runArgs;
    }

    public TestClient runTests(String... testsToRun) {
        runQueue.addAll(Arrays.asList(testsToRun));
        return this;
    }

    public void terminate() throws InterruptedException {
        Terminator t = new Terminator();
        runQueue.add(t);
        t.await();
    }

    public TestClient flush() throws InterruptedException {
        Waiter w = new Waiter();
        runQueue.add(w);
        w.await();
        return this;
    }

    public void setRunListener(RunListener runListener) {
        this.runListener = runListener;
    }

    @Override
    public void beforeLaunch(Config config, LaunchParameters parameters) {
        parameters.getArguments().add("-rvm:log=fatal");
        /*
         * Set this system property to true let the TestServer detect if it gets
         * restarted when running in the iOS simulator. See the comment in
         * TestServer for more info.
         */
        parameters.getArguments().add("-rvm:Drobovm.launchedFromTestClient=true");

        parameters.getArguments().addAll(runArgs);

        // we always use the NonUICodeWrapper to run the TestServer
        // on iOS so the process watchdog is not triggered. The mainClass
        // will be set to NonUICodeWrapper, which takes the actual test
        // runner as an environment variable.
        if (config.getOs() == OS.ios) {
            Map env = new HashMap<>(
                    parameters.getEnvironment() == null ? new HashMap() : parameters.getEnvironment());
            env.put("robovm.wrappedClass", mainClassName);
            parameters.setEnvironment(env);
        }

        try {
            oldStdOutFifo = parameters.getStdoutFifo();
            newStdOutFifo = Fifos.mkfifo("junit-out-proxy");
            parameters.setStdoutFifo(newStdOutFifo);
            defaultStdOutStream = System.out;
        } catch (IOException e) {
            throw new TestClientException("Couldn't create stderr pipe", e);
        }
    }

    @Override
    public void afterLaunch(Config config, LaunchParameters parameters, Process process) {
        try {
            serverPortReader = new ServerPortReader(config, parameters, process,
                    oldStdOutFifo, newStdOutFifo, defaultStdOutStream);

            while (!serverPortReader.stopped && serverPortReader.port == -1) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
            }
        } catch (Throwable t) {
            throw new TestClientException("Failed to get test server port", t);
        }

        // process stopped without providing a port
        if (serverPortReader.port == -1) {
            throw new TestClientException("Process stopped prematurely");
        }

        // Run the tests asynchronously
        runTests(config).subscribeOn(Schedulers.newThread()).subscribe(new Action1() {
            public void call(ResultObject o) {
                try {
                    switch (o.getResultType()) {
                    case AssumptionFailure:
                        runListener.testAssumptionFailure(o.getFailure());
                        break;
                    case Failure:
                        runListener.testFailure(o.getFailure());
                        break;
                    case Finished:
                        runListener.testFinished(o.getDescription());
                        break;
                    case Ignored:
                        runListener.testIgnored(o.getDescription());
                        break;
                    case RunFinished:
                        runListener.testRunFinished(o.getResult());
                        break;
                    case RunStarted:
                        runListener.testRunStarted(o.getDescription());
                        break;
                    case Started:
                        runListener.testStarted(o.getDescription());
                        break;
                    default:
                        break;
                    }
                } catch (Exception e) {
                    // Swallow
                }
            }
        });
    }

    @Override
    public void cleanup() {
        if (serverPortReader != null) {
            serverPortReader.running = false;
            serverPortReader.thread.interrupt();
            serverPortReader = null;
        }
    }

    @Override
    public PluginArguments getArguments() {
        return new PluginArguments("junit", Collections. emptyList());
    }

    @Override
    public void launchFailed(Config config, LaunchParameters parameters) {}

    private Observable runTests(final Config config) {
        return Observable.create(new Observable.OnSubscribe() {
            @Override
            public void call(Subscriber subscriber) {
                int port = serverPortReader.port;

                try {
                    if (config.getTarget() instanceof IOSTarget && IOSTarget.isDeviceArch(config.getArch())) {
                        // iOS device launch. Use libimobiledevice to set up the
                        // connection.
                        IDevice device = ((IOSTarget) config.getTarget()).getDevice();
                        config.getLogger().debug("Connecting to test server running on port %d "
                                + "on device with id %s", port, device.getUdid());
                        try (IDeviceConnection conn = device.connect(port)) {
                            config.getLogger().debug("Connected to test server on device %s", device.getUdid());
                            runTests(config, subscriber, conn.getInputStream(), conn.getOutputStream());
                        }
                    } else {
                        // Local launch. Use sockets.
                        config.getLogger().debug("Connecting to test server running on localhost:%d", port);
                        try (Socket socket = new Socket("localhost", port)) {
                            config.getLogger().debug("Connected to test server at %s", socket.getRemoteSocketAddress());
                            runTests(config, subscriber, socket.getInputStream(), socket.getOutputStream());
                        }
                    }
                    config.getLogger().debug("Test client finished.");
                } catch (Throwable t) {
                    t.printStackTrace();
                    subscriber.onError(t);
                }
                subscriber.onCompleted();
            }
        });
    }

    public Config.Builder configure(Config.Builder configBuilder, boolean isIOS) {
        if (configBuilder == null) {
            throw new IllegalArgumentException("RoboVM configuration cannot be null");
        }

        configBuilder.addForceLinkClass("org.robovm.junit.server.TestServer");
        if (isIOS) {
            configBuilder.mainClass(SERVER_WRAPPER_CLASS_NAME);
        } else {
            configBuilder.mainClass(mainClassName);
        }
        configBuilder.addForceLinkClass("com.android.org.conscrypt.OpenSSLProvider");
        configBuilder.addForceLinkClass("com.android.org.conscrypt.OpenSSLMessageDigestJDK**");

        configBuilder.addLaunchPlugin(this);

        return configBuilder;
    }

    private void runTests(final Config config, Subscriber subscriber, InputStream in,
            OutputStream out) throws IOException {

        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));

        String line = null;
        Object action = null;
        try {
            while (!subscriber.isUnsubscribed() && (action = runQueue.take()) != null) {
                if (action instanceof String) {
                    String testToRun = (String) action;
                    config.getLogger().debug("Running test %s", testToRun);
                    writer.write(Command.run + " " + testToRun + "\n");
                    writer.flush();

                    while ((line = reader.readLine()) != null) {
                        ResultObject resultObject = ResultObject.fromJson(line);
                        if (!subscriber.isUnsubscribed()) {
                            subscriber.onNext(resultObject);
                        }
                        if (resultObject.getResultType() == ResultType.RunFinished) {
                            break;
                        }
                    }
                } else if (action instanceof Terminator) {
                    ((Terminator) action).run();
                    break;
                } else if (action instanceof Waiter) {
                    ((Waiter) action).run();
                }
            }
        } catch (InterruptedException e) {
        }

        config.getLogger().debug("Test run completed. Shutting down test server...");

        writer.write(Command.terminate + "\n");
        writer.flush();
        writer.close();
    }

    /**
     * Wraps the stdout stream of the server and reads the port which the server
     * will be print to stdout. Will continue to wrap the stdout stream until
     * the server process has finished.
     */
    private static class ServerPortReader {
        volatile boolean running = false;
        volatile boolean stopped = false;
        volatile int port = -1;
        Thread thread;
        volatile boolean closeOutOnExit = true;

        public ServerPortReader(final Config config, final LaunchParameters params, final Process process,
                final File oldStdOutFifo, final File newStdOutFifo,
                final OutputStream defaultStdOutStream) throws IOException {

            final BufferedReader in = new BufferedReader(
                    new InputStreamReader(new OpenOnReadFileInputStream(newStdOutFifo)));
            BufferedWriter writer = null;
            if (oldStdOutFifo != null) {
                writer = new BufferedWriter(new OutputStreamWriter(new OpenOnWriteFileOutputStream(oldStdOutFifo)));
            } else {
                writer = new BufferedWriter(new OutputStreamWriter(defaultStdOutStream));
                closeOutOnExit = false;
            }
            final BufferedWriter out = writer;

            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        running = true;
                        while (running && !isProcessStopped(process)) {
                            String line = in.readLine();
                            if (line != null) {
                                if (line.startsWith(SERVER_CLASS_NAME + ": port=")
                                        || line.startsWith(config.getMainClass() + ": port=")) {
                                    port = Integer.parseInt(line.split("=")[1]);
                                    config.getLogger().debug("Test server port: " + port);
                                } else {
                                    out.write(line + "\n");
                                    out.flush();
                                }
                            }
                        }
                    } catch (Throwable t) {
                        config.getLogger().error("Couldn't forward stdout stream", t.getMessage());
                    } finally {
                        if (closeOutOnExit) {
                            IOUtils.closeQuietly(out);
                        }
                        IOUtils.closeQuietly(in);
                        stopped = true;
                    }
                }

                private boolean isProcessStopped(Process process) {
                    try {
                        process.exitValue();
                        return true;
                    } catch (IllegalThreadStateException e) {
                        return false;
                    }
                }
            });
            thread.setName("JUnit Port Reader");
            thread.setDaemon(true);
            thread.start();
        }
    }
}