/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.commons.test.tck; import com.google.inject.AbstractModule; import com.google.inject.ConfigurationException; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.name.Names; import org.testng.ITestContext; import org.testng.ITestNGMethod; import java.util.Iterator; import java.util.ServiceLoader; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.lang.String.format; /** * The listener is designed to instantiate {@link TckModule tck modules} * using {@link ServiceLoader} mechanism. The components provided by those * modules will be injected into a test class whether it's necessary to do so. * For each test class will be used new instance of injector. * Listener requires tck test to be in own separated suite, if it finds more tests * in suite it'll throw {@link IllegalArgumentException} on suite start. * After test suite is finished listener'll try to find test specific or common * instance of {@link TckResourcesCleaner}. It is optional and can be bound in modules. * * <p>The listener expects at least one implementation of {@code TckModule} * to be configured, if it doesn't find any of the {@code TckModule} * implementations then it will report an appropriate exception * and TckTest will fail(as it requires components to be injected into it). * If it finds more than one {@code TckModule} implementation it will * use all of the found. * * <p>The usage example: * <pre> * package org.eclipse.mycomponent; * * @org.testng.annotations.Listeners(TckListener) * // Tck test must have own suite because of cleaning resources on suite finishing * @org.testng.annotations.Test(suiteName = "MySuite") * class SubjectTest { * * @javax.inject.Inject * private Component1 component1. * @javax.inject.Inject * private Component2 component2; * * @org.testng.annotations.Test * public void test() { * // use components * } * } * * class MyTckModule extends TckModule { * public void configure() { * bind(Component1.class).to(...); * bind(Component2.class).toInstance(new Component2(() -> testContext.getAttribute("server_url").toString())); * bind(TckResourcesCleaner.class).to(...); * bind(TckResourcesCleaner.class).annotatedWith(Names.named(SubjectTest.class.getName())).to(...); * } * } * * // Allows to add pre/post test actions like db server start/stop * class DBServerListener implements ITestListener { * // ... * public void onStart(ITestContext context) { * String url = dbServer.start(); * context.setAttribute("server_url", url)l * } * * public void onFinish(ITestContext context) { * dbServer.stop(); * } * // ... * } * </pre> * * <p>Configuring: * <pre> * <i>META-INF/services/org.eclipse.che.commons.test.tck.TckModule</i> * org.eclipse.mycomponent.MyTckModule * * <i>META-INF/services/org.testng.ITestNGListener</i> * org.eclipse.mycomponent.DBServerListener * </pre> * * @author Yevhenii Voevodin * @author Sergii Leschenko * @see org.testng.annotations.Listeners * @see org.testng.IInvokedMethodListener * @see TckResourcesCleaner */ public class TckListener extends TestListenerAdapter { private Injector injector; private Object instance; @Override public void onStart(ITestContext context) { final Set<Object> instances = Stream.of(context.getAllTestMethods()) .map(ITestNGMethod::getInstance) .collect(Collectors.toSet()); if (instances.size() != 1) { throw new IllegalStateException("Tck test should be one and only one in suite."); } instance = instances.iterator().next(); injector = Guice.createInjector(createModule(context, instance.getClass().getName())); injector.injectMembers(instance); } @Override public void onFinish(ITestContext context) { if (injector == null || instance == null) { throw new IllegalStateException("Looks like onFinish method is invoked before onStart."); } // try to get test specific resources cleaner TckResourcesCleaner resourcesCleaner = getResourcesCleaner(injector, Key.get(TckResourcesCleaner.class, Names.named(instance.getClass().getName()))); if (resourcesCleaner == null) { // try to get common resources cleaner resourcesCleaner = getResourcesCleaner(injector, Key.get(TckResourcesCleaner.class)); } if (resourcesCleaner != null) { resourcesCleaner.clean(); } } private TckResourcesCleaner getResourcesCleaner(Injector injector, Key<TckResourcesCleaner> key) { try { return injector.getInstance(key); } catch (ConfigurationException ignored) { } return null; } private Module createModule(ITestContext testContext, String name) { final Iterator<TckModule> moduleIterator = ServiceLoader.load(TckModule.class).iterator(); if (!moduleIterator.hasNext()) { throw new IllegalStateException(format("Couldn't find a TckModule configuration. " + "You probably forgot to configure resources/META-INF/services/%s, or even " + "provide an implementation of the TckModule which is required by the jpa test class %s", TckModule.class.getName(), name)); } return new CompoundModule(testContext, moduleIterator); } private static class CompoundModule extends AbstractModule { private final ITestContext testContext; private final Iterator<TckModule> moduleIterator; private CompoundModule(ITestContext testContext, Iterator<TckModule> moduleIterator) { this.testContext = testContext; this.moduleIterator = moduleIterator; } @Override protected void configure() { bind(ITestContext.class).toInstance(testContext); while (moduleIterator.hasNext()) { final TckModule module = moduleIterator.next(); module.setTestContext(testContext); install(module); } } } }