/*
* Copyright 2012 Harald Wellmann.
*
* 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.ops4j.pax.exam.spi.reactors;
import static org.ops4j.pax.exam.Constants.EXAM_REACTOR_STRATEGY_KEY;
import static org.ops4j.pax.exam.Constants.EXAM_REACTOR_STRATEGY_PER_CLASS;
import static org.ops4j.pax.exam.Constants.EXAM_REACTOR_STRATEGY_PER_METHOD;
import static org.ops4j.pax.exam.Constants.EXAM_REACTOR_STRATEGY_PER_SUITE;
import static org.ops4j.pax.exam.Constants.EXAM_SERVICE_TIMEOUT_DEFAULT;
import static org.ops4j.pax.exam.Constants.EXAM_SERVICE_TIMEOUT_KEY;
import static org.ops4j.pax.exam.Constants.EXAM_SYSTEM_CDI;
import static org.ops4j.pax.exam.Constants.EXAM_SYSTEM_DEFAULT;
import static org.ops4j.pax.exam.Constants.EXAM_SYSTEM_JAVAEE;
import static org.ops4j.pax.exam.Constants.EXAM_SYSTEM_KEY;
import static org.ops4j.pax.exam.Constants.EXAM_SYSTEM_TEST;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.ops4j.pax.exam.Configuration;
import org.ops4j.pax.exam.ConfigurationManager;
import org.ops4j.pax.exam.ExamConfigurationException;
import org.ops4j.pax.exam.ExamFactory;
import org.ops4j.pax.exam.ExamSystem;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.ProbeBuilder;
import org.ops4j.pax.exam.ProbeInvoker;
import org.ops4j.pax.exam.TestAddress;
import org.ops4j.pax.exam.TestContainerException;
import org.ops4j.pax.exam.TestContainerFactory;
import org.ops4j.pax.exam.TestProbeBuilder;
import org.ops4j.pax.exam.options.SystemPropertyOption;
import org.ops4j.pax.exam.options.WarProbeOption;
import org.ops4j.pax.exam.spi.DefaultExamReactor;
import org.ops4j.pax.exam.spi.DefaultExamSystem;
import org.ops4j.pax.exam.spi.ExamReactor;
import org.ops4j.pax.exam.spi.PaxExamRuntime;
import org.ops4j.pax.exam.spi.StagedExamReactor;
import org.ops4j.pax.exam.spi.StagedExamReactorFactory;
import org.ops4j.pax.exam.util.Injector;
import org.ops4j.pax.exam.util.InjectorFactory;
import org.ops4j.spi.ServiceProviderFinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages the exam system and reactor required by a test driver. This class is a singleton and
* keeps track of all tests in the current test suite and lets a reactor reuse the Exam system and
* the test probe, where applicable.
*
* <p>
* This class was factored out from the JUnit4TestRunner of Pax Exam 2.x and does not depend on
* JUnit.
* <p>
* TODO check if there are any concurrency issues. Some methods are synchronized, which is just
* inherited from the 2.1.0 implementation. The use cases are not quite clear.
*
* @author Harald Wellmann
*/
public class ReactorManager {
private static final Logger LOG = LoggerFactory.getLogger(ReactorManager.class);
/** Singleton instance of this manager. */
private static ReactorManager instance;
private Map<String, StagedExamReactorFactory> reactorStrategies;
/** Exam system, containing system and user configuration options. */
private ExamSystem system;
/** The system type, which determines the kind of probe to be used. */
private String systemType;
/** The current test class. */
private Class<?> currentTestClass;
/** The reactor. */
private ExamReactor reactor;
/**
* A probe builder for the current test probe. A probe builder contains a number of test classes
* and their dependent classes and a list of test methods to be executed.
* <p>
* Test methods are added incrementally as classes are scanned. By default, the same probe
* builder is reused for all test classes, unless a given class overrides the default probe
* configuration.
*/
private TestProbeBuilder defaultProbeBuilder;
/**
* Maps test addresses to driver-dependent test method wrappers. A test address is a unique
* identifier for a test method in a given container which is used by a {@link ProbeInvoker} for
* indirectly invoking the test method in the container.
* <p>
* This map is not used when tests are executed directly, i.e. without invoker.
*/
private Map<TestAddress, Object> testAddressToMethodMap = new LinkedHashMap<TestAddress, Object>();
/**
* Set of test classes in suite.
*/
private Set<Class<?>> testClasses = new HashSet<Class<?>>();
/**
* Has the suite been started? Set to true when the first test class is about to run.
*/
private boolean suiteStarted;
/**
* Configuration property access.
*/
private ConfigurationManager cm;
private boolean waitForAfterSuiteEvent;
private int numConfigurations;
/**
* Private constructor for singleton.
*/
private ReactorManager() {
try {
cm = new ConfigurationManager();
system = createExamSystem();
reactorStrategies = new HashMap<String, StagedExamReactorFactory>(3);
reactorStrategies.put(EXAM_REACTOR_STRATEGY_PER_SUITE, new PerSuite());
reactorStrategies.put(EXAM_REACTOR_STRATEGY_PER_CLASS, new PerClass());
reactorStrategies.put(EXAM_REACTOR_STRATEGY_PER_METHOD, new PerMethod());
}
catch (IOException exc) {
throw new TestContainerException("cannot create Exam system", exc);
}
}
/**
* Returns the singleton ReactorManager instance.
*
* @return reactor manager
*/
public static synchronized ReactorManager getInstance() {
if (instance == null) {
instance = new ReactorManager();
}
return instance;
}
/**
* Prepares the unstaged reactor for the given test class instance. Any configurations from
* {@code Configuration} methods of the class are added to the reactor.
*
* @param _testClass
* test class
* @param testClassInstance
* instance of test class
* @return reactor
*/
public synchronized ExamReactor prepareReactor(Class<?> _testClass, Object testClassInstance) {
this.currentTestClass = _testClass;
this.reactor = createReactor(_testClass);
testClasses.add(_testClass);
try {
addConfigurationsToReactor(_testClass, testClassInstance);
}
catch (IllegalAccessException | InvocationTargetException exc) {
throw new TestContainerException(exc);
}
return reactor;
}
/**
* Stages the reactor for the current class.
*
* @return staged reactor
*/
public StagedExamReactor stageReactor() {
StagedExamReactor stagedReactor = reactor.stage(getStagingFactory(currentTestClass));
return stagedReactor;
}
private ExamSystem createExamSystem() throws IOException {
systemType = cm.getProperty(EXAM_SYSTEM_KEY, EXAM_SYSTEM_TEST);
String timeout = cm.getProperty(EXAM_SERVICE_TIMEOUT_KEY, EXAM_SERVICE_TIMEOUT_DEFAULT);
Option timeoutOption = new SystemPropertyOption(EXAM_SERVICE_TIMEOUT_KEY).value(timeout);
if (EXAM_SYSTEM_DEFAULT.equals(systemType)) {
system = DefaultExamSystem.create(new Option[] { timeoutOption });
}
else if (EXAM_SYSTEM_JAVAEE.equals(systemType)) {
system = DefaultExamSystem.create(new Option[] { new WarProbeOption() });
}
else {
system = PaxExamRuntime.createTestSystem(timeoutOption);
}
return system;
}
/**
* Scans the current test class for declared or inherited {@code @Configuration} methods and
* invokes them, adding the returned configuration to the reactor.
*
* @param testClass
* test class
* @param testClassInstance
* instance of test class
* @throws IllegalAccessException
* when configuration method is not public
* @throws InvocationTargetException
* when configuration method cannot be invoked
*/
private void addConfigurationsToReactor(Class<?> testClass, Object testClassInstance)
throws IllegalAccessException, InvocationTargetException {
numConfigurations = 0;
Method[] methods = testClass.getMethods();
for (Method m : methods) {
if (isConfiguration(m)) {
// consider as option, so prepare that one:
reactor.addConfiguration(((Option[]) m.invoke(testClassInstance)));
numConfigurations++;
}
}
}
/**
* Returns the number of configurations for the current reactor.
*
* @return number of configurations
*/
public int getNumConfigurations() {
return numConfigurations;
}
private boolean isConfiguration(Method m) {
Configuration conf = m.getAnnotation(Configuration.class);
return (conf != null);
}
/**
* Creates a staging factory indicated by the {@link ExamReactorStrategy} annotation of the test
* class.
*
* @param testClass
* @return staging factory
*/
private StagedExamReactorFactory getStagingFactory(Class<?> testClass) {
ExamReactorStrategy strategy = testClass.getAnnotation(ExamReactorStrategy.class);
String strategyName = cm.getProperty(EXAM_REACTOR_STRATEGY_KEY);
StagedExamReactorFactory fact;
try {
if (strategy != null) {
fact = strategy.value()[0].newInstance();
return fact;
}
}
catch (IllegalAccessException | InstantiationException exc) {
throw new TestContainerException(exc);
}
if (strategyName == null) {
if (systemType.equals(EXAM_SYSTEM_CDI) || systemType.equals(EXAM_SYSTEM_JAVAEE)) {
strategyName = EXAM_REACTOR_STRATEGY_PER_SUITE;
}
else {
// OSGi default from Pax Exam 2.x
strategyName = EXAM_REACTOR_STRATEGY_PER_METHOD;
}
}
fact = reactorStrategies.get(strategyName);
if (fact == null) {
throw new IllegalArgumentException("unknown reactor strategy " + strategyName);
}
return fact;
}
/**
* Creates an unstaged reactor for the given test class.
*
* @param testClass
* @return unstaged reactor
*/
private DefaultExamReactor createReactor(Class<?> testClass) {
return new DefaultExamReactor(system, createsTestContainerFactory(testClass));
}
/**
* Creates the test container factory to be used by the reactor.
* <p>
* TODO Do we really need this?
*
* @param testClass
* @return test container factory
*/
private TestContainerFactory createsTestContainerFactory(Class<?> testClass) {
try {
ExamFactory f = testClass.getAnnotation(ExamFactory.class);
TestContainerFactory fact = null;
if (f != null) {
fact = f.value().newInstance();
return fact;
}
if (fact == null) {
// default:
fact = PaxExamRuntime.getTestContainerFactory();
}
return fact;
}
catch (InstantiationException | IllegalAccessException exc) {
throw new TestContainerException(exc);
}
}
/**
* Lazily creates a probe builder. The same probe builder will be reused for all test classes,
* unless the default builder is overridden in a given class.
*
* @param testClassInstance instance of test class
* @return probe builder
* @throws IOException when probe cannot be created
* @throws ExamConfigurationException when user-defined probe cannot be created
*/
public TestProbeBuilder createProbeBuilder(Object testClassInstance) throws IOException,
ExamConfigurationException {
if (defaultProbeBuilder == null) {
defaultProbeBuilder = system.createProbe();
}
TestProbeBuilder probeBuilder = overwriteWithUserDefinition(currentTestClass,
testClassInstance);
if (probeBuilder.getTempDir() == null) {
probeBuilder.setTempDir(defaultProbeBuilder.getTempDir());
}
return probeBuilder;
}
private TestProbeBuilder overwriteWithUserDefinition(Class<?> testClass, Object testInstance)
throws ExamConfigurationException {
Method[] methods = testClass.getMethods();
for (Method m : methods) {
if (isProbeBuilder(m)) {
LOG.debug("User defined probe hook found: " + m.getName());
TestProbeBuilder probeBuilder;
try {
probeBuilder = (TestProbeBuilder) m.invoke(testInstance, defaultProbeBuilder);
}
// CHECKSTYLE:SKIP : catch all wanted
catch (Exception e) {
throw new ExamConfigurationException("Invoking custom probe hook "
+ m.getName() + " failed", e);
}
if (probeBuilder != null) {
return probeBuilder;
}
else {
throw new ExamConfigurationException("Invoking custom probe hook "
+ m.getName() + " succeeded but returned null");
}
}
}
LOG.debug("No User defined probe hook found");
return defaultProbeBuilder;
}
private boolean isProbeBuilder(Method m) {
ProbeBuilder builder = m.getAnnotation(ProbeBuilder.class);
return (builder != null);
}
/**
* @return the systemType
*/
public String getSystemType() {
return systemType;
}
/**
* Looks up a test method for a given address.
*
* @param address
* test method address used by probe
* @return test method wrapper - the type is only known to the test driver.
*/
public Object lookupTestMethod(TestAddress address) {
return testAddressToMethodMap.get(address);
}
/**
* Stores the test method wrapper for a given test address
*
* @param address
* test method address used by probe
* @param testMethod
* test method wrapper - the type is only known to the test driver
*/
public void storeTestMethod(TestAddress address, Object testMethod) {
testAddressToMethodMap.put(address, testMethod);
}
public void beforeSuite(StagedExamReactor stagedReactor) {
stagedReactor.beforeSuite();
suiteStarted = true;
waitForAfterSuiteEvent = true;
}
public void afterSuite(StagedExamReactor stagedReactor) {
waitForAfterSuiteEvent = false;
stagedReactor.afterSuite();
}
public void afterClass(StagedExamReactor stagedReactor, Class<?> klass) {
stagedReactor.afterClass();
testClasses.remove(klass);
if (!waitForAfterSuiteEvent && testClasses.isEmpty()) {
LOG.info("suite finished");
stagedReactor.afterSuite();
suiteStarted = false;
testClasses.clear();
testAddressToMethodMap.clear();
}
}
public void beforeClass(StagedExamReactor stagedReactor, Object testClassInstance) {
if (!suiteStarted) {
suiteStarted = true;
stagedReactor.beforeSuite();
}
stagedReactor.beforeClass();
}
/**
* Performs field injection on the given test class instance.
*
* @param test
* test class instance
*/
public void inject(Object test) {
Injector injector = findInjector();
injector.injectFields(test);
}
/**
* Finds an injector factory and creates an injector.
*
* @return injector
*/
private Injector findInjector() {
InjectorFactory injectorFactory = ServiceProviderFinder
.loadUniqueServiceProvider(InjectorFactory.class);
return injectorFactory.createInjector();
}
}