/**
* Copyright (c) 2013-2016, The SeedStack authors <http://seedstack.org>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.seedstack.seed.it;
import com.google.inject.Injector;
import com.google.inject.ProvisionException;
import io.nuun.kernel.api.Kernel;
import io.nuun.kernel.api.config.KernelConfiguration;
import io.nuun.kernel.core.NuunCore;
import org.junit.Before;
import org.junit.rules.MethodRule;
import org.junit.rules.TestRule;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.seedstack.seed.SeedException;
import org.seedstack.seed.core.Seed;
import org.seedstack.seed.core.internal.configuration.ConfigurationPlugin;
import org.seedstack.seed.it.internal.ITErrorCode;
import org.seedstack.seed.it.internal.ITPlugin;
import org.seedstack.seed.it.spi.ITKernelMode;
import org.seedstack.seed.it.spi.ITRunnerPlugin;
import org.seedstack.seed.it.spi.KernelRule;
import org.seedstack.seed.it.spi.PausableStatement;
import org.seedstack.shed.ClassLoaders;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
/**
* This runner can be used to run JUnit tests with Seed integration. Tests
* launched with this runner will benefit from Seed features (injection, aop
* interception, test extensions, ...).
*/
public class SeedITRunner extends BlockJUnit4ClassRunner {
private final ServiceLoader<ITRunnerPlugin> plugins;
private final Expect.TestingStep expectedTestingStep;
private final Class<? extends Throwable> expectedClass;
private ITKernelMode kernelMode = ITKernelMode.ANY;
private Map<String, String> defaultConfiguration;
private Kernel kernel;
/**
* Creates the runner for the corresponding test class.
*
* @param klass the test class.
* @throws InitializationError if an initialization error occurs.
*/
@SuppressWarnings("unchecked")
public SeedITRunner(Class<?> klass) throws InitializationError {
super(klass);
this.plugins = ServiceLoader.load(ITRunnerPlugin.class, ClassLoaders.findMostCompleteClassLoader(SeedITRunner.class));
Expect annotation = getTestClass().getJavaClass().getAnnotation(Expect.class);
if (annotation != null) {
this.expectedClass = annotation.value();
this.expectedTestingStep = annotation.step();
} else {
this.expectedClass = null;
this.expectedTestingStep = null;
}
}
protected void collectInitializationErrors(List<Throwable> errors) {
super.collectInitializationErrors(errors);
validatePublicVoidNoArgMethods(BeforeKernel.class, true, errors);
validatePublicVoidNoArgMethods(AfterKernel.class, true, errors);
}
@Override
public void run(RunNotifier notifier) {
try {
// Determine kernel mode to use
for (ITRunnerPlugin plugin : plugins) {
ITKernelMode pluginKernelMode = plugin.kernelMode(getTestClass());
if (pluginKernelMode == null) {
pluginKernelMode = ITKernelMode.ANY;
}
switch (pluginKernelMode) {
case NONE:
if (kernelMode == ITKernelMode.PER_TEST_CLASS || kernelMode == ITKernelMode.PER_TEST) {
throw SeedException.createNew(ITErrorCode.TEST_PLUGINS_MISMATCH)
.put("requestedKernelMode", ITKernelMode.NONE)
.put("incompatibleKernelMode", kernelMode.toString())
.put("testClass", getTestClass().getJavaClass().getCanonicalName());
}
kernelMode = ITKernelMode.NONE;
break;
case PER_TEST_CLASS:
if (kernelMode == ITKernelMode.NONE || kernelMode == ITKernelMode.PER_TEST) {
throw SeedException.createNew(ITErrorCode.TEST_PLUGINS_MISMATCH)
.put("requestedKernelMode", ITKernelMode.PER_TEST_CLASS)
.put("incompatibleKernelMode", kernelMode.toString())
.put("testClass", getTestClass().getJavaClass().getCanonicalName());
}
kernelMode = ITKernelMode.PER_TEST_CLASS;
break;
case PER_TEST:
if (kernelMode == ITKernelMode.NONE || kernelMode == ITKernelMode.PER_TEST_CLASS) {
throw SeedException.createNew(ITErrorCode.TEST_PLUGINS_MISMATCH)
.put("requestedKernelMode", ITKernelMode.PER_TEST)
.put("incompatibleKernelMode", kernelMode.toString())
.put("testClass", getTestClass().getJavaClass().getCanonicalName());
}
kernelMode = ITKernelMode.PER_TEST;
break;
default:
break;
}
}
// Default kernel mode is per test class
if (kernelMode == ITKernelMode.ANY) {
kernelMode = ITKernelMode.PER_TEST_CLASS;
}
if (kernelMode == ITKernelMode.PER_TEST_CLASS) {
initKernel(gatherConfiguration(null));
}
super.run(notifier);
if (kernelMode == ITKernelMode.PER_TEST_CLASS) {
stopKernel();
}
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable t) {
notifyFailure(t, notifier);
}
}
/**
* @return the active kernel
*/
protected Kernel getKernel() {
return this.kernel;
}
@Override
protected List<TestRule> getTestRules(Object target) {
List<TestRule> testRules = super.getTestRules(target);
for (ITRunnerPlugin plugin : plugins) {
testRules.addAll(buildRules(plugin.provideTestRulesToApply(getTestClass(), target)));
}
return testRules;
}
@Override
protected List<TestRule> classRules() {
List<TestRule> classRules = super.classRules();
for (ITRunnerPlugin plugin : plugins) {
classRules.addAll(buildRules(plugin.provideClassRulesToApply(getTestClass())));
}
return classRules;
}
private List<TestRule> buildRules(List<Class<? extends TestRule>> classRulesToApply) {
List<TestRule> rules = new ArrayList<>();
if (classRulesToApply != null) {
for (Class<? extends TestRule> testRuleClass : classRulesToApply) {
try {
TestRule testRule = instantiate(testRuleClass);
if (testRule instanceof KernelRule) {
((KernelRule) testRule).acceptKernelConfiguration(provideKernelConfiguration(defaultConfiguration));
}
rules.add(testRule);
} catch (Exception e) {
throw SeedException.wrap(e, ITErrorCode.FAILED_TO_INSTANTIATE_TEST_RULE).put("ruleClass", testRuleClass.getCanonicalName());
}
}
}
return rules;
}
@Override
protected List<MethodRule> rules(Object target) {
List<MethodRule> methodRules = super.rules(target);
for (ITRunnerPlugin plugin : plugins) {
List<Class<? extends MethodRule>> methodRulesToApply = plugin.provideMethodRulesToApply(getTestClass(), target);
if (methodRulesToApply != null) {
for (Class<? extends MethodRule> methodRuleClass : methodRulesToApply) {
try {
MethodRule methodRule = instantiate(methodRuleClass);
if (methodRule instanceof KernelRule) {
((KernelRule) methodRule).acceptKernelConfiguration(provideKernelConfiguration(defaultConfiguration));
}
methodRules.add(methodRule);
} catch (Exception e) {
throw SeedException.wrap(e, ITErrorCode.FAILED_TO_INSTANTIATE_TEST_RULE).put("ruleClass", methodRuleClass.getCanonicalName());
}
}
}
}
return methodRules;
}
@Override
protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) {
List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(Before.class);
return befores.isEmpty() ? statement : new PausableStatement(statement, befores, target);
}
@Override
protected Object createTest() throws Exception {
Exception eventualException = null;
Object test = null;
try {
test = instantiate(getTestClass().getJavaClass());
} catch (Exception e) {
eventualException = e;
}
processException(eventualException, Expect.TestingStep.INSTANTIATION);
// we still want a non-injected test instance to verify that things have failed
if (test == null) {
test = super.createTest();
}
return test;
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
try {
defaultConfiguration = gatherConfiguration(method);
if (kernelMode == ITKernelMode.PER_TEST) {
initKernel(gatherConfiguration(method));
}
super.runChild(method, notifier);
if (kernelMode == ITKernelMode.PER_TEST) {
stopKernel();
}
} catch (Throwable t) {
notifyFailure(t, notifier);
}
}
private <T> T instantiate(Class<T> toInstantiate) throws IllegalAccessException, InstantiationException {
if (kernel != null && kernel.isStarted()) {
return kernel.objectGraph().as(Injector.class).getInstance(toInstantiate);
} else {
return toInstantiate.newInstance();
}
}
private void initKernel(Map<String, String> configuration) {
List<FrameworkMethod> beforeKernelMethods = getTestClass().getAnnotatedMethods(BeforeKernel.class);
for (FrameworkMethod beforeKernelMethod : beforeKernelMethods) {
try {
beforeKernelMethod.invokeExplosively(null);
} catch (Throwable throwable) {
throw SeedException.wrap(throwable, ITErrorCode.EXCEPTION_OCCURRED_BEFORE_KERNEL);
}
}
Exception eventualException = null;
try {
kernel = Seed.createKernel(null, provideKernelConfiguration(configuration), true);
} catch (Exception e) {
eventualException = e;
kernel = null;
}
try {
processException(eventualException, Expect.TestingStep.STARTUP);
} catch (Exception e) {
throw SeedException.wrap(e, ITErrorCode.FAILED_TO_INITIALIZE_KERNEL);
}
}
private void stopKernel() {
try {
if (kernel != null && kernel.isStarted()) {
Exception eventualException = null;
try {
Seed.disposeKernel(kernel);
} catch (Exception e) {
eventualException = e;
}
// Execute the AfterKernel methods before processing eventual exception
List<FrameworkMethod> afterKernelMethods = getTestClass().getAnnotatedMethods(AfterKernel.class);
for (FrameworkMethod afterKernelMethod : afterKernelMethods) {
try {
afterKernelMethod.invokeExplosively(null);
} catch (Throwable t) {
throw SeedException.wrap(t, ITErrorCode.EXCEPTION_OCCURRED_AFTER_KERNEL);
}
}
try {
processException(eventualException, Expect.TestingStep.SHUTDOWN);
} catch (Exception e) {
throw SeedException.wrap(e, ITErrorCode.FAILED_TO_STOP_KERNEL);
}
}
} finally {
kernel = null;
}
}
private void processException(Exception e, Expect.TestingStep testingStep) {
Throwable unwrappedThrowable = e;
// Unwrap known Guice exceptions to access the real one
if (unwrappedThrowable != null && ProvisionException.class.isAssignableFrom(e.getClass()) && e.getCause() != null) {
unwrappedThrowable = e.getCause();
}
if (expectedClass == null && unwrappedThrowable != null) {
if (unwrappedThrowable instanceof SeedException) {
throw (SeedException) unwrappedThrowable;
} else {
throw SeedException.wrap(unwrappedThrowable, ITErrorCode.UNEXPECTED_EXCEPTION_OCCURRED).put("occurredClass", unwrappedThrowable.getClass().getCanonicalName());
}
}
if (expectedClass != null) {
if (unwrappedThrowable == null) {
if (expectedTestingStep == testingStep) {
throw SeedException.createNew(ITErrorCode.EXPECTED_EXCEPTION_DID_NOT_OCCURRED).put("expectedClass", expectedClass.getCanonicalName());
}
} else if (!expectedClass.isAssignableFrom(unwrappedThrowable.getClass())) {
throw SeedException.createNew(ITErrorCode.ANOTHER_EXCEPTION_THAN_EXPECTED_OCCURRED).put("expectedClass", expectedClass.getCanonicalName()).put("occurredClass", unwrappedThrowable.getClass().getCanonicalName());
}
}
}
private KernelConfiguration provideKernelConfiguration(Map<String, String> configuration) {
KernelConfiguration kernelConfiguration = NuunCore.newKernelConfiguration();
if (getTestClass().getJavaClass().getAnnotation(WithoutSpiPluginsLoader.class) != null) {
kernelConfiguration.withoutSpiPluginsLoader();
}
WithPlugins annotation = getTestClass().getJavaClass().getAnnotation(WithPlugins.class);
if (annotation != null) {
kernelConfiguration.plugins(annotation.value());
}
kernelConfiguration.param(ITPlugin.IT_CLASS_NAME, getTestClass().getJavaClass().getName());
for (Map.Entry<String, String> defaultConfigurationEntry : configuration.entrySet()) {
kernelConfiguration.param(ConfigurationPlugin.EXTERNAL_CONFIG_PREFIX + defaultConfigurationEntry.getKey(), defaultConfigurationEntry.getValue());
}
return kernelConfiguration;
}
private Map<String, String> gatherConfiguration(FrameworkMethod frameworkMethod) {
Map<String, String> configuration = new HashMap<>();
for (ITRunnerPlugin plugin : plugins) {
Map<String, String> pluginConfiguration = plugin.provideConfiguration(getTestClass(), frameworkMethod);
if (pluginConfiguration != null) {
configuration.putAll(pluginConfiguration);
}
}
return configuration;
}
private void notifyFailure(Throwable t, RunNotifier notifier) {
notifier.fireTestFailure(new Failure(getDescription(), t));
}
}