/* * Copyright 2011 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.testng.listener; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.transaction.NotSupportedException; import javax.transaction.SystemException; import javax.transaction.UserTransaction; import org.ops4j.pax.exam.Constants; 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.Injector; import org.ops4j.pax.exam.util.InjectorFactory; import org.ops4j.pax.exam.util.Transactional; import org.ops4j.spi.ServiceProviderFinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.IHookCallBack; import org.testng.IHookable; import org.testng.IMethodInstance; import org.testng.IMethodInterceptor; import org.testng.ISuite; import org.testng.ISuiteListener; import org.testng.ITestClass; import org.testng.ITestContext; import org.testng.ITestNGMethod; import org.testng.ITestResult; import org.testng.internal.MethodInstance; import org.testng.internal.NoOpTestClass; /** * TestNG driver for Pax Exam, implementing a number of ITestNGListener interfaces. To run a TestNG * test class with Pax Exam, add this class as a listener to your test class: * * <pre> * @Listeners(PaxExam.class) * public class MyTest { * * @BeforeMethod * public void setUp() { * } * * @AfterMethod * public void tearDown() { * } * * @Test * public void test1() { * } * } * </pre> * * In OSGi and Java EE modes, Pax Exam processes each test class twice, once by test driver and then * again inside the test container. The driver delegates each test method invocation to a probe * invoker which excutes the test method inside the container via the probe. * <p> * It would be nice to separate these two aspects and handle them in two separate listeners, but * TestNG has no way to override or disable the listener annotated on the test class. * <p> * TestNG provides a listener callback for configuration methods, but it does not let us intercept * them. For this reason, we use an ugly reflection hack to disable them when running under the * driver and to make sure they get executed inside the test container only. * <p> * Dependencies annotated by {@link javax.inject.Inject} get injected into the test class in the * container (OSGi and Java EE modes) or by the driver (CDI mode). * * @author Harald Wellmann * @since 2.3.0 * */ public class PaxExam implements ISuiteListener, IMethodInterceptor, IHookable { public static final String PAX_EXAM_SUITE_NAME = "PaxExamInternal"; private static final Logger LOG = LoggerFactory.getLogger(PaxExam.class); /** * 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; /** * Maps method names to test addresses. The method names are qualified by class and container * names. Each method of the test class is cloned for each container. */ private Map<String, TestAddress> methodToAddressMap = new LinkedHashMap<String, TestAddress>(); /** * Reactor manager singleton. */ private ReactorManager manager; /** * Shall we use a probe invoker, or invoke test methods directly? */ private boolean useProbeInvoker; /** * TestNG calls our intercept() method twice. We remember the first call and do nothing when * called again. */ private boolean methodInterceptorCalled; /** * The test class currently executed. We use this to generate beforeClass and afterClass events, * which we do not receive from TestNG. */ private Object currentTestClassInstance; private List<ITestNGMethod> methods; public PaxExam() { LOG.debug("created ExamTestNGListener"); } /** * Are we running in the test container or directly under the driver? * * @param suite * current test suite * @return true if running in container */ private boolean isRunningInTestContainer(ISuite suite) { return suite.getName().equals(PAX_EXAM_SUITE_NAME); } /** * Are we running in the test container or directly under the driver? * * @param method * current test method * @return true if running in container */ private boolean isRunningInTestContainer(ITestNGMethod method) { return method.getXmlTest().getSuite().getName().equals(PAX_EXAM_SUITE_NAME); } /** * Called by TestNG before the suite starts. When running in the container, this is a no op. * Otherwise, we create and stage the reactor. * * @param suite * test suite */ @Override public void onStart(ISuite suite) { if (!isRunningInTestContainer(suite)) { manager = ReactorManager.getInstance(); stagedReactor = stageReactor(suite); manager.beforeSuite(stagedReactor); } } /** * Called by TestNG after the suite has finished. When running in the container, this is a no * op. Otherwise, we stop the reactor. * * @param suite * test suite */ @Override public void onFinish(ISuite suite) { if (!isRunningInTestContainer(suite)) { // fire an afterClass event for the last test class if (currentTestClassInstance != null) { manager.afterClass(stagedReactor, currentTestClassInstance.getClass()); } manager.afterSuite(stagedReactor); } } /** * Stages the reactor. This involves building the probe including all test methods of the suite * and creating one or more test containers. * <p> * When using a probe invoker, we register the tests with the reactor. * <p> * Hack: As there is no way to intercept configuration methods, we disable them by reflection. * * @param suite * test suite * @return staged reactor */ private synchronized StagedExamReactor stageReactor(ISuite suite) { try { methods = suite.getAllMethods(); Class<?> testClass = methods.get(0).getRealClass(); LOG.debug("test class = {}", testClass); disableConfigurationMethods(suite); Object testClassInstance = testClass.newInstance(); return stageReactorForClass(testClass, testClassInstance); } catch (InstantiationException | IllegalAccessException exc) { throw new TestContainerException(exc); } } private StagedExamReactor stageReactorForClass(Class<?> testClass, Object testClassInstance) { try { ExamReactor examReactor = manager.prepareReactor(testClass, testClassInstance); useProbeInvoker = !manager.getSystemType().equals(Constants.EXAM_SYSTEM_CDI); if (useProbeInvoker) { addTestsToReactor(examReactor, testClassInstance, methods); } return manager.stageReactor(); } catch (IOException | ExamConfigurationException exc) { throw new TestContainerException(exc); } } /** * Disables the {@code @BeforeMethod} and {@code @AfterMethod} configuration methods of all test * classes, overriding the corresponding private fields of {@code TestClass}. * <p> * These methods shall run only once inside the test container, but not directly under the * driver. * <p> * This is a rather ugly hack, but there does not seem to be any other way. * * @param suite * test suite */ private void disableConfigurationMethods(ISuite suite) { Set<ITestClass> seen = new HashSet<ITestClass>(); for (ITestNGMethod method : suite.getAllMethods()) { ITestClass testClass = method.getTestClass(); if (!seen.contains(testClass)) { disableConfigurationMethods(testClass); seen.add(testClass); } } } /** * Adds all tests of the suite to the reactor and creates a probe builder. * <p> * TODO This driver currently assumes that all test classes of the suite use the default probe * builder. It builds one probe containing all tests of the suite. This is why the * testClassInstance argument is just an arbitrary instance of one of the classes of the suite. * * @param reactor * unstaged reactor * @param testClassInstance * not used * @param testMethods * all methods of the suite. * @throws IOException * @throws ExamConfigurationException */ private void addTestsToReactor(ExamReactor reactor, Object testClassInstance, List<ITestNGMethod> testMethods) throws IOException, ExamConfigurationException { TestProbeBuilder probe = manager.createProbeBuilder(testClassInstance); for (ITestNGMethod m : testMethods) { TestAddress address = probe.addTest(m.getRealClass(), m.getMethodName()); manager.storeTestMethod(address, m); } reactor.addProbe(probe); } /** * Callback from TestNG which lets us intercept a test method invocation. The two cases of * running in the container or under the driver are handled in separate methods. */ @Override public void run(IHookCallBack callBack, ITestResult testResult) { if (isRunningInTestContainer(testResult.getMethod())) { runInTestContainer(callBack, testResult); } else { runByDriver(callBack, testResult); } } /** * Runs a test method in the container. Before invoking the method, we inject its dependencies. * <p> * TODO Unlike JUnit, TestNG instantiates each test class only once, so maybe we should also * inject the dependencies just once. * * @param callBack * TestNG callback for test method * @param testResult * test result container */ private void runInTestContainer(IHookCallBack callBack, ITestResult testResult) { Object testClassInstance = testResult.getInstance(); inject(testClassInstance); if (isTransactional(testResult)) { runInTransaction(callBack, testResult); } else { callBack.runTestMethod(testResult); } return; } /** * Checks if the current test method is transactional. * * @param testResult * TestNG method and result wrapper * @return true if the method or the enclosing class is annotated with {@link Transactional}. */ private boolean isTransactional(ITestResult testResult) { boolean transactional = false; Method method = testResult.getMethod().getConstructorOrMethod().getMethod(); if (method.getAnnotation(Transactional.class) != null) { transactional = true; } else { if (method.getDeclaringClass().getAnnotation(Transactional.class) != null) { transactional = true; } } return transactional; } /** * Runs a test method enclosed by a Java EE auto-rollback transaction obtained from the JNDI * context. * * @param callBack * TestNG callback for test method * @param testResult * test result container */ private void runInTransaction(IHookCallBack callBack, ITestResult testResult) { UserTransaction tx = null; try { InitialContext ctx = new InitialContext(); tx = (UserTransaction) ctx.lookup("java:comp/UserTransaction"); tx.begin(); callBack.runTestMethod(testResult); } catch (NamingException | NotSupportedException | SystemException exc) { throw new TestContainerException(exc); } finally { rollback(tx); } } /** * Rolls back the given transaction, if not null. * * @param tx * transaction */ private void rollback(UserTransaction tx) { if (tx != null) { try { tx.rollback(); } catch (IllegalStateException | SecurityException | SystemException exc) { throw new TestContainerException(exc); } } } /** * Performs field injection on the given object. The injection method is looked up via the Java * SE service loader. * * @param testClassInstance * test class instance */ private void inject(Object testClassInstance) { InjectorFactory injectorFactory = ServiceProviderFinder .loadUniqueServiceProvider(InjectorFactory.class); Injector injector = injectorFactory.createInjector(); injector.injectFields(testClassInstance); } /** * Runs a test method under the driver. * <p> * Fires beforeClass and afterClass events when the current class changes, as we do not get * these events from TestNG. This requires the test methods to be sorted by class, see * {@link #intercept(List, ITestContext)}. * <p> * When using a probe invoker, we delegate the test method invocation to the invoker so that the * test will be executed in the container context. * <p> * Otherwise, we directly run the test method. * * @param callBack * TestNG callback for test method * @param testResult * test result container * @throws ExamConfigurationException * @throws IOException */ private void runByDriver(IHookCallBack callBack, ITestResult testResult) { LOG.info("running {}", testResult.getName()); Object testClassInstance = testResult.getMethod().getInstance(); if (testClassInstance != currentTestClassInstance) { if (currentTestClassInstance != null) { manager.afterClass(stagedReactor, currentTestClassInstance.getClass()); } Class<?> testClass = testClassInstance.getClass(); stagedReactor = stageReactorForClass(testClass, testClassInstance); if (!useProbeInvoker) { manager.inject(testClassInstance); } manager.beforeClass(stagedReactor, testClassInstance); currentTestClassInstance = testClassInstance; } if (!useProbeInvoker) { callBack.runTestMethod(testResult); return; } TestAddress address = methodToAddressMap.get(testResult.getName()); TestAddress root = address.root(); LOG.debug("Invoke " + testResult.getName() + " @ " + address + " Arguments: " + root.arguments()); try { stagedReactor.invoke(address); testResult.setStatus(ITestResult.SUCCESS); } // CHECKSTYLE:SKIP : StagedExamReactor API catch (Exception e) { Throwable t = ExceptionHelper.unwind(e); LOG.error("Exception", e); testResult.setStatus(ITestResult.FAILURE); testResult.setThrowable(t); } } /** * Callback from TestNG which lets us manipulate the list of test methods in the suite. When * running under the driver and using a probe invoker, we now construct the test addresses to be * used be the probe invoker, and we sort the methods by class to make sure we can fire * beforeClass and afterClass events later on. * <p> * For some reason, TestNG invokes this callback twice. The second time over, we return the * unchanged method list. */ @Override public List<IMethodInstance> intercept(List<IMethodInstance> testMethods, ITestContext context) { if (methodInterceptorCalled || !useProbeInvoker || isRunningInTestContainer(context.getSuite())) { return testMethods; } methodInterceptorCalled = true; boolean mangleMethodNames = manager.getNumConfigurations() > 1; TestDirectory testDirectory = TestDirectory.getInstance(); List<IMethodInstance> newInstances = new ArrayList<IMethodInstance>(); Set<TestAddress> targets = stagedReactor.getTargets(); for (TestAddress address : targets) { ITestNGMethod frameworkMethod = (ITestNGMethod) manager .lookupTestMethod(address.root()); if (frameworkMethod == null) { continue; } Method javaMethod = frameworkMethod.getConstructorOrMethod().getMethod(); if (mangleMethodNames) { frameworkMethod = new ReactorTestNGMethod(frameworkMethod, javaMethod, address); } MethodInstance newInstance = new MethodInstance(frameworkMethod); newInstances.add(newInstance); methodToAddressMap.put(frameworkMethod.getMethodName(), address); testDirectory.add(address, new TestInstantiationInstruction(frameworkMethod .getRealClass().getName() + ";" + javaMethod.getName())); } Collections.sort(newInstances, new IMethodInstanceComparator()); return newInstances; } /** * Disables BeforeMethod and AfterMethod configuration methods in the given test class. * <p> * NOTE: Ugly reflection hack, as TestNG does not provide an API for overriding before and after * methods. * * @param testClass * TestNG test class wrapper */ private void disableConfigurationMethods(ITestClass testClass) { NoOpTestClass instance = (NoOpTestClass) testClass; ITestNGMethod[] noMethods = new ITestNGMethod[0]; Class<?> javaClass = NoOpTestClass.class; setPrivateField(javaClass, instance, "m_beforeTestMethods", noMethods); setPrivateField(javaClass, instance, "m_afterTestMethods", noMethods); } /** * Sets a private field by injection * * @param klass * Java class where the field is declared * @param instance * instance of (a subclass of) klass * @param fieldName * name of field to be set * @param value * new value for field */ private void setPrivateField(Class<?> klass, Object instance, String fieldName, Object value) { try { Field field = NoOpTestClass.class.getDeclaredField(fieldName); field.setAccessible(true); field.set(instance, value); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException exc) { throw new TestContainerException(exc); } } }