/* * Copyright 2010 - 2013 Toni Menzel, 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.junit.impl; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.internal.runners.model.ReflectiveCallable; import org.junit.internal.runners.statements.Fail; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.ops4j.pax.exam.ExamConfigurationException; import org.ops4j.pax.exam.ExceptionHelper; import org.ops4j.pax.exam.TestAddress; import org.ops4j.pax.exam.TestContainerException; import org.ops4j.pax.exam.TestDirectory; import org.ops4j.pax.exam.TestInstantiationInstruction; import org.ops4j.pax.exam.TestProbeBuilder; import org.ops4j.pax.exam.spi.ExamReactor; import org.ops4j.pax.exam.spi.StagedExamReactor; import org.ops4j.pax.exam.spi.reactors.ReactorManager; import org.ops4j.pax.exam.util.InjectorFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * JUnit runner for parameterized Pax Exam tests executed via an invoker. This runner is used for * all operation modes except CDI. See {@link Parameterized} for more details on specifying * parameter sets. * * @author Harald Wellmann * */ public class ParameterizedProbeRunner extends BlockJUnit4ClassRunner { private static final Logger LOG = LoggerFactory.getLogger(ParameterizedProbeRunner.class); /** * Reactor manager singleton. */ private ReactorManager manager; /** * Staged reactor for this test class. This may actually be a reactor already staged for a * previous test class, depending on the reactor strategy. */ private StagedExamReactor stagedReactor; private Map<FrameworkMethod, TestAddress> methodToTestAddressMap = new LinkedHashMap<FrameworkMethod, TestAddress>(); private Object[] parameters; public ParameterizedProbeRunner(Class<?> klass) throws InitializationError { super(klass); LOG.info("creating PaxExam runner for {}", klass); manager = ReactorManager.getInstance(); try { ExamReactor examReactor = manager.prepareReactor(klass, null); addTestsToReactor(examReactor, klass, null); stagedReactor = manager.stageReactor(); } catch (IOException | ExamConfigurationException exc) { throw new InitializationError(exc); } } /** * We decorate the super method by reactor setup and teardown. This method is called once per * class. Note that the given reactor strategy decides whether or not the setup and teardown * actually happens at this level. */ @Override public void run(RunNotifier notifier) { LOG.info("running test class {}", getTestClass().getName()); Class<?> testClass = getTestClass().getJavaClass(); try { manager.beforeClass(stagedReactor, testClass); super.run(notifier); } // CHECKSTYLE:SKIP : catch all wanted catch (Throwable e) { // rethrowing the exception does not help, we have to use the notifier here Description description = Description.createSuiteDescription(testClass); notifier.fireTestFailure(new Failure(description, e)); } finally { manager.afterClass(stagedReactor, testClass); } } /** * Override to avoid running BeforeClass and AfterClass by the driver. They shall only be run by * the container when using a probe invoker. */ protected Statement classBlock(final RunNotifier notifier) { Statement statement = childrenInvoker(notifier); return statement; } /** * Override to avoid running Before, After and Rule methods by the driver. They shall only be * run by the container when using a probe invoker. */ protected Statement methodBlock(FrameworkMethod method) { Object test; try { ReflectiveCallable reflectiveCallable = new ReflectiveCallable() { @Override // CHECKSTYLE:SKIP : Base class API protected Object runReflectiveCall() throws Throwable { return createTest(); } }; test = reflectiveCallable.run(); } // CHECKSTYLE:SKIP : ReflectiveCallable API catch (Throwable e) { return new Fail(e); } Statement statement = methodInvoker(method, test); return statement; } /** * When using a probe invoker, we replace the test methods of this class by a potentially larger * set of decorated test methods. Each original test method may give rise to multiple copies per * test container or configuration. */ @Override protected List<FrameworkMethod> getChildren() { if (methodToTestAddressMap.isEmpty()) { fillChildren(); } return new ArrayList<FrameworkMethod>(methodToTestAddressMap.keySet()); } private void fillChildren() { Set<TestAddress> targets = stagedReactor.getTargets(); TestDirectory testDirectory = TestDirectory.getInstance(); for (TestAddress address : targets) { FrameworkMethod frameworkMethod = (FrameworkMethod) manager.lookupTestMethod(address .root()); // The reactor may contain targets which do not belong to the current test class if (frameworkMethod == null) { continue; } Class<?> frameworkMethodClass = frameworkMethod.getMethod().getDeclaringClass(); String className = frameworkMethodClass.getName(); String methodName = frameworkMethod.getName(); if (frameworkMethodClass.isAssignableFrom(getTestClass().getJavaClass())) { FrameworkMethod method = new ParameterizedFrameworkMethod(address, frameworkMethod); testDirectory.add(address, new TestInstantiationInstruction(className + ";" + methodName)); methodToTestAddressMap.put(method, address); } } } /** * Adds test methods to the reactor, mapping method names to test addresses which are used by * the probe invoker. * <p> * Note that when a collection of test classes is passed to an external JUnit runner like * Eclipse or Maven Surefire, this method is invoked (via the constructor of this runner) for * each class <em>before</em> the {@link #run(RunNotifier)} method is invoked for any class. * <p> * This way, we can register all test methods in the reactor before the actual test execution * starts. * * @param reactor * @param testClass * @param testClassInstance * @throws IOException * @throws ExamConfigurationException */ private void addTestsToReactor(ExamReactor reactor, Class<?> testClass, Object testClassInstance) throws IOException, ExamConfigurationException { TestProbeBuilder probe = manager.createProbeBuilder(testClassInstance); Iterator<Object[]> it = null; int index = 0; try { it = allParameters().iterator(); } // CHECKSTYLE:SKIP : JUnit API catch (Throwable t) { throw new ExamConfigurationException(t.getMessage()); } while (it.hasNext()) { parameters = it.next(); // probe.setAnchor( testClass ); for (FrameworkMethod s : super.getChildren()) { // record the method -> adress matching TestAddress address = probe.addTest(testClass, s.getMethod().getName(), index); manager.storeTestMethod(address, s); } index++; } reactor.addProbe(probe); } /** * When using a probe invoker, we replace the super method and invoke the test method indirectly * via the reactor. */ protected synchronized Statement methodInvoker(final FrameworkMethod method, final Object test) { return new Statement() { @Override // CHECKSTYLE:SKIP : Statement API public void evaluate() throws Throwable { TestAddress address = methodToTestAddressMap.get(method); TestAddress root = address.root(); LOG.debug("Invoke " + method.getName() + " @ " + address + " Arguments: " + root.arguments()); try { stagedReactor.invoke(address); } // CHECKSTYLE:SKIP : StagedExamReactor API catch (Exception e) { Throwable t = ExceptionHelper.unwind(e); throw t; } } }; } /** * Creates an instance of the current test class. When using a probe invoker, this simply * delegates to super. Otherwise, we perform injection on the instance created by the super * method. * <p> * In this case, an {@link InjectorFactory} is obtained via SPI lookup. */ @Override protected Object createTest() throws Exception { return getTestClass().getOnlyConstructor().newInstance(parameters); } @SuppressWarnings("unchecked") // CHECKSTYLE:SKIP - JUnit API private Iterable<Object[]> allParameters() throws Throwable { Object params = getParametersMethod().invokeExplosively(null); if (params instanceof Iterable) { return (Iterable<Object[]>) params; } else { throw parametersMethodReturnedWrongType(); } } private FrameworkMethod getParametersMethod() throws Exception { List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(Parameters.class); for (FrameworkMethod each : methods) { if (each.isStatic() && each.isPublic()) { return each; } } throw new Exception("No public static parameters method on class " + getTestClass().getName()); } private Exception parametersMethodReturnedWrongType() throws Exception { String className = getTestClass().getName(); String methodName = getParametersMethod().getName(); String message = MessageFormat.format("{0}.{1}() must return an Iterable of arrays.", className, methodName); return new TestContainerException(message); } @Override protected void validateConstructor(List<Throwable> errors) { validateOnlyOneConstructor(errors); if (fieldsAreAnnotated()) { validateZeroArgConstructor(errors); } } private List<FrameworkField> getAnnotatedFieldsByParameter() { return getTestClass().getAnnotatedFields(Parameter.class); } private boolean fieldsAreAnnotated() { return !getAnnotatedFieldsByParameter().isEmpty(); } }