/*
* Hibernate OGM, Domain model persistence for NoSQL datastores
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.ogm.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.transaction.TransactionManager;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform;
import org.hibernate.ogm.OgmSessionFactory;
import org.hibernate.ogm.exception.impl.Exceptions;
import org.hibernate.ogm.util.impl.Log;
import org.hibernate.ogm.util.impl.LoggerFactory;
import org.hibernate.ogm.utils.TestSessionFactory.Scope;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.TestClass;
/**
* A JUnit 4 runner for OGM tests. Based on a given set of entities, it manages a session factory, which is used
* throughout all test methods of the given test class.
* <p>
* The entities of the test are to be returned by a parameterless method annotated with {@link TestEntities} in form of
* a {@code Class<?>[]}.
* <p>
* The used session factory can be obtained by annotating a field of type {@link SessionFactory} with the
* {@link TestSessionFactory} annotation. The runner will inject the factory in this field then. Depending on the
* {@link TestSessionFactory#scope() } setting, either the same session factory instance will be used for all test
* methods of a given test class or a new session factory will be created and injected for each individual test method.
* <p>
* Finally the configuration used for bootstrapping the factory can optionally be modified by annotating a
* configuration method with the {@link TestSessionFactoryConfiguration} a shown in the example below.
* <p>
* Usage example:
*
* <pre>
* {@code
* @RunWith(OgmTestRunner.class)
* public class AnimalFarmTest {
*
* @TestSessionFactory
* public SessionFactory sessionFactory;
*
* @Test
* public void shouldCountAnimals() throws Exception {
* Session session = sessionFactory.openSession();
* ...
* session.close();
* }
*
* @TestSessionFactoryConfiguration
* public static void configure(Map<String, Object> cfg) {
* cfg.put( Environment.MONGODB_ASSOCIATIONS_STORE, AssociationStorage.COLLECTION.name() );
* }
*
* @TestEntities
* public Class<?>[] getTestEntities() {
* return new Class<?>[]{ PolarBear.class, Giraffe.class };
* }
* }
* }
* </pre>
*
* @see OgmTestCase Base class for tests which is configured with this runner for ease of use
* @author Gunnar Morling
*/
public class OgmTestRunner extends SkippableTestRunner {
private static final Log LOG = LoggerFactory.make();
private final Set<Field> testScopedFactoryFields;
private final Set<Field> testMethodScopedFactoryFields;
private SessionFactory testScopedSessionFactory;
private SessionFactory testMethodScopedSessionFactory;
public OgmTestRunner(Class<?> klass) throws InitializationError {
super( klass );
testScopedFactoryFields = getTestFactoryFields( getTestClass(), Scope.TEST_CLASS );
testMethodScopedFactoryFields = getTestFactoryFields( getTestClass(), Scope.TEST_METHOD );
}
private static Set<Field> getTestFactoryFields(TestClass testClass, TestSessionFactory.Scope scope) {
Set<Field> testFactoryFields = new HashSet<Field>();
for ( FrameworkField frameworkField : testClass.getAnnotatedFields( TestSessionFactory.class ) ) {
Field field = frameworkField.getField();
if ( scope == field.getAnnotation( TestSessionFactory.class ).scope() ) {
field.setAccessible( true );
testFactoryFields.add( field );
}
}
return testFactoryFields;
}
@Override
public void run(RunNotifier notifier) {
if ( isTestScopedSessionFactoryRequired() ) {
testScopedSessionFactory = buildSessionFactory();
injectSessionFactory( null, testScopedFactoryFields, testScopedSessionFactory );
}
try {
super.run( notifier );
}
finally {
if ( testScopedSessionFactory != null ) {
cleanUpPendingTransactionIfRequired();
TestHelper.dropSchemaAndDatabase( testScopedSessionFactory );
testScopedSessionFactory.close();
}
}
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
// create test method scoped SF if required; it will be injected in createTest()
if ( isTestMethodScopedSessionFactoryRequired( method ) ) {
testMethodScopedSessionFactory = buildSessionFactory();
}
try {
super.runChild( method, notifier );
}
finally {
if ( testMethodScopedSessionFactory != null ) {
cleanUpPendingTransactionIfRequired();
TestHelper.dropSchemaAndDatabase( testScopedSessionFactory );
testMethodScopedSessionFactory.close();
}
}
}
private boolean isTestScopedSessionFactoryRequired() {
return !isTestClassSkipped() && !areAllTestMethodsSkipped();
}
private boolean isTestMethodScopedSessionFactoryRequired(FrameworkMethod method) {
return !testMethodScopedFactoryFields.isEmpty() && !super.isTestMethodSkipped( method );
}
private void cleanUpPendingTransactionIfRequired() {
TransactionManager transactionManager = ( (SessionFactoryImplementor) testScopedSessionFactory )
.getServiceRegistry()
.getService( JtaPlatform.class )
.retrieveTransactionManager();
try {
if ( transactionManager != null && transactionManager.getTransaction() != null ) {
LOG.warn( "The test started a transaction but failed to commit it or roll it back. Going to roll it back." );
transactionManager.rollback();
}
}
catch (Exception e) {
throw new RuntimeException( e );
}
}
protected OgmSessionFactory buildSessionFactory() {
return TestHelper.getDefaultTestSessionFactory( getTestSpecificSettings(), getConfiguredEntityTypes() );
}
private Class<?>[] getConfiguredEntityTypes() {
for ( FrameworkMethod frameworkMethod : getTestClass().getAnnotatedMethods( TestEntities.class ) ) {
Class<?>[] entityTypes = invokeTestEntitiesMethod( frameworkMethod );
if ( entityTypes == null || entityTypes.length == 0 ) {
throw new IllegalArgumentException( "Define at least a single annotated entity" );
}
return entityTypes;
}
throw new IllegalStateException( "The entities of the test must be retrievable via a parameterless method which is annotated with "
+ TestEntities.class.getSimpleName() + " and returns Class<?>[]." );
}
private Class<?>[] invokeTestEntitiesMethod(FrameworkMethod frameworkMethod) {
Method method = frameworkMethod.getMethod();
method.setAccessible( true );
if ( method.getReturnType() != Class[].class || method.getParameterTypes().length > 0 ) {
throw new IllegalStateException( "Method annotated with " + TestEntities.class.getSimpleName()
+ " must have no parameters and must return Class<?>[]." );
}
Class<?>[] entityTypes = null;
try {
entityTypes = (Class<?>[]) method.invoke( super.createTest() );
}
catch (Exception e) {
Exceptions.<RuntimeException>sneakyThrow( e );
}
return entityTypes;
}
private Map<String, Object> getTestSpecificSettings() {
Map<String, Object> testSpecificSettings = new HashMap<>();
try {
for ( FrameworkMethod frameworkMethod : getTestClass().getAnnotatedMethods( TestSessionFactoryConfiguration.class ) ) {
Method method = frameworkMethod.getMethod();
method.setAccessible( true );
method.invoke( super.createTest(), testSpecificSettings );
}
}
catch (Exception e) {
Exceptions.<RuntimeException>sneakyThrow( e );
}
return testSpecificSettings;
}
@Override
protected Object createTest() throws Exception {
Object test = super.createTest();
// inject SFs as per given scopes
if ( !testScopedFactoryFields.isEmpty() ) {
injectSessionFactory( test, testScopedFactoryFields, testScopedSessionFactory );
}
if ( !testMethodScopedFactoryFields.isEmpty() ) {
injectSessionFactory( test, testMethodScopedFactoryFields, testMethodScopedSessionFactory );
}
return test;
}
private void injectSessionFactory(Object test, Iterable<Field> fields, SessionFactory sessionFactory) {
for ( Field field : fields ) {
try {
if ( ( test == null && Modifier.isStatic( field.getModifiers() ) ) ||
( test != null && !Modifier.isStatic( field.getModifiers() ) ) ) {
field.set( test, sessionFactory );
}
}
catch (Exception e) {
throw new RuntimeException( "Can't inject session factory into field " + field );
}
}
}
}