
com.oracle.bedrock.junit.JUnitTestRunner Maven / Gradle / Ivy
/*
* File: JUnitTestRunner.java
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* The contents of this file are subject to the terms and conditions of
* the Common Development and Distribution License 1.0 (the "License").
*
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the License by consulting the LICENSE.txt file
* distributed with this file, or by consulting https://oss.oracle.com/licenses/CDDL
*
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file LICENSE.txt.
*
* MODIFICATIONS:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*/
package com.oracle.bedrock.junit;
import com.oracle.bedrock.Option;
import com.oracle.bedrock.OptionsByType;
import com.oracle.bedrock.junit.options.TestClasses;
import com.oracle.bedrock.junit.options.Tests;
import com.oracle.bedrock.options.Decoration;
import com.oracle.bedrock.runtime.concurrent.RemoteCallable;
import com.oracle.bedrock.runtime.concurrent.RemoteChannel;
import com.oracle.bedrock.runtime.concurrent.RemoteRunnable;
import com.oracle.bedrock.runtime.concurrent.options.StreamName;
import com.oracle.bedrock.runtime.java.JavaVirtualMachine;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
/**
* A class that runs a set of JUnit tests.
*
* Copyright (c) 2016. All Rights Reserved. Oracle Corporation.
* Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
*
* @author Jonathan Knight
*/
public class JUnitTestRunner implements Runnable
{
/**
* The name of the event stream that will be used to send JUnit test events.
*/
public static final StreamName STREAM_NAME = StreamName.of("JUnit");
/**
* The singleton instance of the {@link JUnitTestRunner}.
*/
public static final JUnitTestRunner INSTANCE = new JUnitTestRunner();
/**
* The {@link RemoteChannel} to use to publish JUnit test events.
*/
public static RemoteChannel channel;
/**
* The current state of this {@link JUnitTestRunner}.
*/
private State state = State.NotRunning;
/**
* The lock used for various synchronized operations.
*/
private final Object MONITOR = new Object();
/**
* The set of {@link OptionsByType} controlling the test run
*/
private OptionsByType optionsByType;
/**
* The different states that this {@link JUnitTestRunner} can be in.
*/
public enum State
{
/**
* Waiting to start run.
*/
NotRunning,
/**
* Waiting to start tests.
*/
Waiting,
/**
* Running a set of tests.
*/
Running,
/**
* Completed a set of tests.
*/
Completed,
/**
* The application has stopped.
*/
Stopped;
}
/**
* Obtain the current {@link State}.
*
* @return the current {@link State}
*/
public State getState()
{
return state;
}
/**
* The main run method.
*/
@Override
public void run()
{
synchronized (MONITOR)
{
// Make sure we have not already changed to Stopped
if (state == State.NotRunning)
{
// Change to waiting
state = State.Waiting;
}
// Wait for the state to change from waiting to something else
awaitStateChangeFrom(State.Waiting);
}
// If the state changed to running the run the tests
if (state == State.Running)
{
long start = System.currentTimeMillis();
try
{
if (channel != null)
{
// Raise an event to signal that the tests are starting
channel.raise(JUnitTestListener.Event.junitStarted(), STREAM_NAME);
}
runTests(this.optionsByType);
}
catch (Throwable e)
{
e.printStackTrace();
}
finally
{
synchronized (MONITOR)
{
if (state == State.Running)
{
state = State.Completed;
MONITOR.notifyAll();
}
}
long end = System.currentTimeMillis();
RemoteChannel.AcknowledgeWhen acknowledge = (state != State.Stopped)
? RemoteChannel.AcknowledgeWhen.PROCESSED
: RemoteChannel.AcknowledgeWhen.SENT;
if (channel != null)
{
// Raise an event to signal that the tests are complete
// We will use the future to change the final state
CompletableFuture future = channel.raise(JUnitTestListener.Event.junitCompleted(end - start),
STREAM_NAME,
acknowledge);
// When the future completes, regardless of how, change the state to stopped
future.whenComplete((_void, throwable) -> changeState(State.Stopped));
}
else
{
changeState(State.Stopped);
}
}
// Wait for the state to change to Stopped. Either we will already be stopped
// or the state will chage when the futre returned from the final event
// is completed.
awaitStateChangeTo(State.Stopped);
}
}
private void changeState(State state)
{
synchronized (MONITOR)
{
this.state = state;
MONITOR.notifyAll();
}
}
private void awaitStateChangeFrom(State currentState)
{
synchronized (MONITOR)
{
while (state == currentState)
{
try
{
MONITOR.wait(1000);
}
catch (InterruptedException e)
{
state = State.Stopped;
break;
}
}
}
}
private void awaitStateChangeTo(State nextState)
{
synchronized (MONITOR)
{
while (state != nextState)
{
try
{
MONITOR.wait(1000);
}
catch (InterruptedException e)
{
break;
}
}
}
}
/**
* Run the tests specified by the given {@link OptionsByType}.
*
* @param optionsByType the {@link OptionsByType} controlling the test run
*/
private void runTests(OptionsByType optionsByType)
{
// If the options are null there is nothing to do
if (optionsByType == null)
{
return;
}
Tests tests = optionsByType.get(Tests.class);
// If there are no tests to execute then return
if (tests.isEmpty())
{
return;
}
JUnitCore jUnitCore = new JUnitCore();
Result result = new Result();
Listener listener = new Listener();
jUnitCore.addListener(result.createListener());
jUnitCore.addListener(listener);
for (RunListener runListener : optionsByType.getInstancesOf(RunListener.class))
{
jUnitCore.addListener(runListener);
}
// Run the tests in each TestClasses instance
for (TestClasses testClasses : tests)
{
Set> classes = testClasses.resolveTestClasses();
Filter filter = testClasses.getTestFilter();
for (Class> testClass : classes)
{
Request request = Request.aClass(testClass);
if (filter != null)
{
request = request.filterWith(filter);
}
jUnitCore.run(request);
// If the state has changed to stopped then we should exit
if (state == State.Stopped)
{
return;
}
}
}
}
/**
* Wait until the {@link JUnitTestRunner} is in the {@link State#Waiting} state
* and then start a test run using the specified {@link OptionsByType}.
*
* @param optionsByType the {@link OptionsByType} controlling the test run
*/
public void run(OptionsByType optionsByType)
{
synchronized (MONITOR)
{
while (state == State.NotRunning)
{
try
{
MONITOR.wait(1000);
}
catch (InterruptedException e)
{
break;
}
}
if (state == State.Waiting)
{
this.optionsByType = optionsByType;
this.state = State.Running;
MONITOR.notifyAll();
}
else
{
throw new IllegalStateException("JUnit runner is not in a valid state to accept tests. State=" + state);
}
}
}
/**
* Set the current state to {@link State#Stopped}, which will cause
* the application to exit.
*
* If a test run is in progress it will be halted after the current test class has completed.
*/
public void stop()
{
if (state == State.Stopped)
{
return;
}
synchronized (MONITOR)
{
state = State.Stopped;
MONITOR.notifyAll();
}
}
/**
* The main entry point for this application.
*
* @param args the application arguments
*/
public static void main(String[] args)
{
// acquire the RemoteChannel for the JUnitTestRunner
channel = RemoteChannel.get();
// run the JUnit tests
JUnitTestRunner.INSTANCE.run();
}
/**
* An instance of a JUnit {@link RunListener} that listens for JUnit run events
* and forwards them on the the {@link RemoteChannel} as {@link JUnitTestListener.Event}s.
*/
public static class Listener extends RunListener
{
/**
* The {@link Description} of the current test run.
*/
private final ThreadLocal testRunDescription = new InheritableThreadLocal<>();
/**
* The name of the current class being tested.
*/
private final ThreadLocal currentTestClass = new InheritableThreadLocal<>();
/**
* A flag indicating whether the current test failed.
*/
private final ThreadLocal testFailed = new InheritableThreadLocal<>();
/**
* The start time of the current test run.
*/
private final ThreadLocal runStartTime = new InheritableThreadLocal<>();
/**
* The start time for the current test class.
*/
private final ThreadLocal classStartTime = new InheritableThreadLocal<>();
/**
* The start time of the current test.
*/
private final ThreadLocal testStartTime = new InheritableThreadLocal<>();
/**
* Obtain the {@link Description} of the current test run.
*
* @return the {@link Description} of the current test run
*/
public Description getTestRunDescription()
{
return testRunDescription.get();
}
/**
* Obtain the name of the current test class.
*
* @return the name of the current test class
*/
public String getCurrentTestClass()
{
return currentTestClass.get();
}
/**
* Obtain the start time of the current test class.
*
* @return the start time of the current test class
*/
public Long getClassStartTime()
{
return classStartTime.get();
}
/**
* Obtain the start time of the current test run.
*
* @return the start time of the current test run
*/
public Long getRunStartTime()
{
return runStartTime.get();
}
/**
* Obtain the start time of the current test.
*
* @return the start time of the current test
*/
public Long getTestStartTime()
{
return testStartTime.get();
}
/**
* Determine whether the current test has failed.
*
* @return true
if the current test
* has failed, otherwise false
*/
public Boolean hasTestFailed()
{
return testFailed.get();
}
/**
* Set whether the current test has failed.
*
* @param failed true
if the test has
* failed, false
if the
* test has not failed
*/
public void setTestFailed(boolean failed)
{
testFailed.set(failed);
}
@Override
public void testRunStarted(Description description) throws Exception
{
currentTestClass.set(null);
setTestFailed(false);
testRunDescription.set(description);
raiseEvent(JUnitTestListener.Event.testRunStarted(description.getDisplayName(), System.getProperties()));
runStartTime.set(System.currentTimeMillis());
}
@Override
public void testRunFinished(Result result) throws Exception
{
long endTime = System.currentTimeMillis();
long time = endTime - runStartTime.get();
Description description = testRunDescription.get();
Long classStart = classStartTime.get();
long classRunTime = classStart != null ? endTime - classStart : 0L;
String currentClassName = currentTestClass.get();
if (currentClassName != null)
{
raiseEvent(JUnitTestListener.Event.testClassFinished(currentClassName, classRunTime));
}
raiseEvent(JUnitTestListener.Event.testRunFinsihed(description.getDisplayName(), time));
}
@Override
public void testStarted(Description description) throws Exception
{
checkClassChange(description);
setTestFailed(false);
raiseEvent(JUnitTestListener.Event.testStarted(description.getDisplayName(),
JUnitUtils.findClassName(description)));
testStartTime.set(System.currentTimeMillis());
}
@Override
public void testFinished(Description description) throws Exception
{
// If there were no failures, errors or Assume errors then raise a success event
Boolean isFailure = hasTestFailed();
if (isFailure == null ||!isFailure)
{
long endTime = System.currentTimeMillis();
long time = endTime - testStartTime.get();
raiseEvent(JUnitTestListener.Event.testSucceded(description.getDisplayName(),
JUnitUtils.findClassName(description),
time));
}
}
@Override
public void testIgnored(Description description) throws Exception
{
checkClassChange(description);
raiseEvent(JUnitTestListener.Event.ignored(description.getDisplayName(),
JUnitUtils.findClassName(description),
JUnitUtils.getIgnoredMessage(description)));
}
@Override
public void testAssumptionFailure(Failure failure)
{
long endTime = System.currentTimeMillis();
long time = endTime - testStartTime.get();
Description description = failure.getDescription();
Throwable throwable = failure.getException();
String testHeader = failure.getTestHeader();
if (testHeader == null || "null".equals(testHeader))
{
testHeader = "Failed to construct test";
}
raiseEvent(JUnitTestListener.Event.assumptionFailure(testHeader,
JUnitUtils.findClassName(description),
time,
throwable.getMessage(),
throwable.getStackTrace()));
setTestFailed(true);
}
@Override
public void testFailure(Failure failure) throws Exception
{
long endTime = System.currentTimeMillis();
long time = endTime - testStartTime.get();
Description description = failure.getDescription();
Throwable cause = failure.getException();
// If the cause was an AssertionError then the test failed otherwise it was an error
if (cause instanceof AssertionError)
{
raiseEvent(JUnitTestListener.Event.failure(JUnitUtils.getFailureMessage(failure),
JUnitUtils.findClassName(description),
time,
cause.getClass().getCanonicalName(),
cause.getMessage(),
cause.getStackTrace()));
}
else
{
raiseEvent(JUnitTestListener.Event.error(JUnitUtils.getFailureMessage(failure),
JUnitUtils.findClassName(description),
time,
cause.getClass().getCanonicalName(),
cause.getMessage(),
cause.getStackTrace()));
}
setTestFailed(true);
}
/**
* Determine from the {@link Description} whether the {@link Class} being
* tested has changed.
*
* If the the previous test class is not null then a {@link JUnitTestListener.Event}
* will be raised to signal the end of that class.
* If the class has changed the a {@link JUnitTestListener.Event} will be raised
* to signify the start of a test class.
*
* @param description the JUnit {@link Description} to use to check for a
* change of test {@link Class}.
*/
protected void checkClassChange(Description description)
{
long endTime = System.currentTimeMillis();
String testClass = JUnitUtils.findClassName(description);
String current = currentTestClass.get();
if (current == null ||!current.equals(testClass))
{
if (current != null)
{
long time = endTime - classStartTime.get();
raiseEvent(JUnitTestListener.Event.testClassFinished(current, time));
}
raiseEvent(JUnitTestListener.Event.testClassStarted(JUnitUtils.findClassName(description)));
classStartTime.set(System.currentTimeMillis());
currentTestClass.set(testClass);
}
}
/**
* Raise the specified {@link JUnitTestListener.Event} on
* the {@link #STREAM_NAME} event stream.
*
* @param event the {@link JUnitTestListener.Event} to raise
*/
protected void raiseEvent(JUnitTestListener.Event event)
{
if (channel != null)
{
channel.raise(event, STREAM_NAME);
}
}
}
/**
* A {@link RemoteCallable} to use to start a test run.
*/
public static class StartTests implements RemoteCallable
{
/**
* The {@link JUnitTestRunner} to use to run tests.
*/
private transient JUnitTestRunner runner = JUnitTestRunner.INSTANCE;
/**
* The {@link Option}s to use to start the test run.
*/
private Option[] options;
/**
* Create a {@link StartTests} runnable with the
* specified options.
*
* @param optionsByType the {@link OptionsByType}s to use
*/
public StartTests(OptionsByType optionsByType)
{
List