package com.maxifier.guice.jpa;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceException;
import java.lang.reflect.Field;
import java.sql.SQLNonTransientException;
import java.sql.SQLTransientException;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.maxifier.guice.jpa.DB.Transaction.*;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
/**
* @author Konstantin Lyamshin (2015-11-16 23:54)
*/
@Guice(modules = {JPAModule.class, DBInterceptorTest.MockEMFModule.class})
public class DBInterceptorTest {
@Inject EntityManagerFactory emf;
@Inject EntityManager em;
EntityManager emm;
EntityTransaction etm;
AtomicBoolean active;
AtomicBoolean rollback;
@BeforeMethod
public void setUp() throws Exception {
reset(emf);
emm = mock(EntityManager.class);
etm = mock(EntityTransaction.class);
active = new AtomicBoolean();
rollback = new AtomicBoolean();
when(emf.createEntityManager()).thenReturn(emm);
when(emm.getEntityManagerFactory()).thenReturn(emf);
when(emm.getTransaction()).thenReturn(etm);
when(etm.isActive()).thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
return active.get();
}
});
when(etm.getRollbackOnly()).thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
return rollback.get();
}
});
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
active.set(true);
return null;
}
}).when(etm).begin();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
active.set(false);
rollback.set(false);
return null;
}
}).when(etm).commit();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
active.set(false);
rollback.set(false);
return null;
}
}).when(etm).rollback();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
rollback.set(true);
return null;
}
}).when(etm).setRollbackOnly();
// Hack DBInterceptors internals to reduce timeouts significantly
Field f = DBInterceptor.class.getDeclaredField("RETRY_TIMEOUTS");
f.setAccessible(true);
int[] dest = (int[]) f.get(null);
System.arraycopy(new int[]{0, 100, 300, 5000}, 0, dest, 0, 4);
}
private static PersistenceException newTransientException() {
return new PersistenceException(new SQLTransientException());
}
@Test
public void testNoRetry() throws Exception {
Callable<?> mock1 = mock(Callable.class);
when(mock1.call()).thenReturn("OK");
assertEquals(retry0(mock1), "OK");
verify(mock1).call();
Callable<?> mock2 = mock(Callable.class);
when(mock2.call()).thenThrow(newTransientException());
try { retry0(mock2); fail("Exception expected"); } catch (PersistenceException ignored) {}
verify(mock2).call();
}
@Test
public void testRetry() throws Exception {
Callable<?> mock1 = mock(Callable.class);
when(mock1.call()).thenReturn("OK1");
assertEquals(retry2(mock1), "OK1");
verify(mock1, times(1)).call();
Callable<?> mock2 = mock(Callable.class);
when(mock2.call())
.thenThrow(newTransientException())
.thenReturn("OK2");
assertEquals(retry2(mock2), "OK2");
verify(mock2, times(2)).call();
Callable<?> mock3 = mock(Callable.class);
when(mock3.call())
.thenThrow(newTransientException())
.thenThrow(newTransientException())
.thenReturn("OK3");
assertEquals(retry2(mock3), "OK3");
verify(mock3, times(3)).call();
Callable<?> mock4 = mock(Callable.class);
when(mock4.call())
.thenThrow(newTransientException())
.thenThrow(newTransientException())
.thenThrow(newTransientException())
.thenReturn("OK4");
try { retry2(mock4); fail("Exception expected"); } catch (PersistenceException ignored) {}
verify(mock4, times(3)).call();
}
@Test
public void testRetryTimeouts() throws Exception {
final ArrayList<Long> ticks = new ArrayList<Long>();
Callable<Void> recorder = new Callable<Void>() {
@Override
public Void call() throws Exception {
ticks.add(System.nanoTime());
throw newTransientException();
}
};
ticks.add(System.nanoTime());
try { retry2(recorder); fail("Exception expected"); } catch (PersistenceException ignored) {}
assertEquals(ticks.size(), 4);
assertTrue(TimeUnit.NANOSECONDS.toMillis(ticks.get(1) - ticks.get(0)) >= 0); // in future
assertTrue(TimeUnit.NANOSECONDS.toMillis(ticks.get(2) - ticks.get(1)) >= 100); // with timeout
assertTrue(TimeUnit.NANOSECONDS.toMillis(ticks.get(3) - ticks.get(2)) >= 300); // with timeout
}
@Test
public void testRetryInterrupt() throws Exception {
Callable<?> mock = mock(Callable.class);
when(mock.call()).thenThrow(newTransientException());
Thread.currentThread().interrupt();
try { retry2(mock); fail("Exception expected"); } catch (PersistenceException ignored) {}
assertTrue(Thread.interrupted());
verify(mock, times(1)).call();
}
@Test(expectedExceptions = PersistenceException.class)
public void testRetryNonTransient() throws Exception {
Callable<?> mock = mock(Callable.class);
when(mock.call())
.thenThrow(new PersistenceException(new SQLNonTransientException()))
.thenReturn("OK");
retry2(mock);
}
@DB(retries = 0)
Object retry0(Callable<?> callable) throws Exception {
return callable.call();
}
@DB(retries = 2)
Object retry2(Callable<?> callable) throws Exception {
return callable.call();
}
@Test
public void testNoConnection() throws Exception {
dbNoConnection();
verify(emf, never()).createEntityManager();
verify(emm.getTransaction(), never()).begin();
verify(emm.getTransaction(), never()).commit();
verify(emm, never()).close();
}
@DB(transaction = REQUIRED)
void dbNoConnection() {
assertEquals(em.toString(), "EntityManagerProxy{UnitOfWork{transactional}}");
}
@Test
public void testNoTransaction() throws Exception {
dbNoTransaction();
verify(emf).createEntityManager();
verify(emm.getTransaction(), never()).begin();
verify(emm.getTransaction(), never()).commit();
verify(emm).close();
}
@DB(transaction = NOT_REQUIRED)
void dbNoTransaction() {
assertEquals(em.getTransaction().isActive(), false);
}
@Test
public void testTransaction() throws Exception {
dbTransaction();
verify(emf).createEntityManager();
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).commit();
verify(emm).close();
}
@DB(transaction = REQUIRED)
void dbTransaction() {
assertEquals(em.getTransaction().isActive(), true);
}
@Test
public void testTransactionNew() throws Exception {
dbTransactionNew();
verify(emf).createEntityManager();
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).commit();
verify(emm).close();
}
@DB(transaction = REQUIRES_NEW)
void dbTransactionNew() {
assertEquals(em.getTransaction().isActive(), true);
}
@Test(dependsOnMethods = "testNoTransaction")
public void testNestedNoTransactions() throws Exception {
dbNestedNoTransactions1();
verify(emf).createEntityManager();
verify(emm.getTransaction(), never()).begin();
verify(emm.getTransaction(), never()).commit();
verify(emm).close();
}
@DB(transaction = NOT_REQUIRED)
void dbNestedNoTransactions1() {
dbNestedNoTransactions2();
assertEquals(em.getTransaction().isActive(), false);
}
@DB(transaction = NOT_REQUIRED)
void dbNestedNoTransactions2() {
assertEquals(em.getTransaction().isActive(), false);
}
@Test(dependsOnMethods = "testTransaction")
public void testNestedTransactions() throws Exception {
dbNestedTransactions1();
verify(emf).createEntityManager();
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).commit();
verify(emm).close();
}
@DB(transaction = REQUIRED)
void dbNestedTransactions1() {
dbNestedTransactions2();
assertEquals(em.getTransaction().isActive(), true);
}
@DB(transaction = REQUIRED)
void dbNestedTransactions2() {
assertEquals(em.getTransaction().isActive(), true);
}
@Test(dependsOnMethods = "testTransactionNew")
public void testNestedTransactionsNew() throws Exception {
dbNestedTransactionsNew1();
verify(emf, times(2)).createEntityManager();
verify(emm.getTransaction(), times(2)).begin();
verify(emm.getTransaction(), times(2)).commit();
verify(emm, times(2)).close();
}
@DB(transaction = REQUIRES_NEW)
void dbNestedTransactionsNew1() {
assertEquals(em.getTransaction().isActive(), true);
verify(emf, times(1)).createEntityManager();
verify(emm.getTransaction(), times(1)).begin();
dbNestedTransactionsNew2();
verify(emm.getTransaction(), times(1)).commit();
verify(emm, times(1)).close();
}
@DB(transaction = REQUIRES_NEW)
void dbNestedTransactionsNew2() {
assertEquals(em.getTransaction().isActive(), true);
verify(emf, times(2)).createEntityManager();
verify(emm.getTransaction(), times(2)).begin();
}
@Test(dependsOnMethods = "testTransaction")
public void testStartTransaction() throws Exception {
dbStartTransaction1();
verify(emf).createEntityManager();
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).commit();
verify(emm).close();
}
@DB(transaction = NOT_REQUIRED)
void dbStartTransaction1() {
assertEquals(em.getTransaction().isActive(), false);
dbStartTransaction2();
assertEquals(em.getTransaction().isActive(), false);
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).commit();
}
@DB(transaction = REQUIRED)
void dbStartTransaction2() {
assertEquals(em.getTransaction().isActive(), true);
}
@Test(dependsOnMethods = "testTransaction")
public void testSupportsTransaction() throws Exception {
dbSupportsTransaction1();
verify(emf).createEntityManager();
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).commit();
verify(emm).close();
}
@DB(transaction = REQUIRED)
void dbSupportsTransaction1() {
assertEquals(em.getTransaction().isActive(), true);
dbSupportsTransaction2();
assertEquals(em.getTransaction().isActive(), true);
}
@DB(transaction = NOT_REQUIRED)
void dbSupportsTransaction2() {
assertEquals(em.getTransaction().isActive(), true);
}
@Test(dependsOnMethods = "testNestedTransactionsNew")
public void testDeepNesting() throws Exception {
dbDeepNesting1();
verify(emf, times(2)).createEntityManager();
verify(emm.getTransaction(), times(2)).begin();
verify(emm.getTransaction(), times(2)).commit();
verify(emm, times(2)).close();
}
@DB(transaction = NOT_REQUIRED)
void dbDeepNesting1() {
assertEquals(em.getTransaction().isActive(), false);
verify(emf, times(1)).createEntityManager();
verify(emm.getTransaction(), times(0)).begin();
verify(emm.getTransaction(), times(0)).commit();
verify(emm, times(0)).close();
dbDeepNesting2();
assertEquals(em.getTransaction().isActive(), false);
}
@DB(transaction = REQUIRED)
void dbDeepNesting2() {
assertEquals(em.getTransaction().isActive(), true);
dbDeepNesting3();
active.set(true); // manually reset state because we have only one connection instance
verify(emf, times(2)).createEntityManager();
verify(emm.getTransaction(), times(2)).begin();
verify(emm.getTransaction(), times(1)).commit();
verify(emm, times(1)).close();
}
@DB(transaction = REQUIRES_NEW)
void dbDeepNesting3() {
assertEquals(em.getTransaction().isActive(), true);
verify(emf, times(2)).createEntityManager();
verify(emm.getTransaction(), times(2)).begin();
verify(emm.getTransaction(), times(0)).commit();
verify(emm, times(0)).close();
}
@Test
public void testNoTransactionException() throws Exception {
// ignore exceptions when no transaction is active
dbNoTransactionException();
verify(emm.getTransaction(), never()).begin();
verify(emm.getTransaction(), never()).rollback();
}
@DB(transaction = NOT_REQUIRED)
void dbNoTransactionException() {
assertEquals(em.getTransaction().getRollbackOnly(), false);
try { dbException(); fail("Exception expected"); } catch (PersistenceException ignored) {}
assertEquals(em.getTransaction().getRollbackOnly(), false);
}
@Test(dependsOnMethods = "testTransaction")
public void testTransactionException() throws Exception {
// rollback transaction in case of exception
dbTransactionException();
verify(emm.getTransaction()).begin();
verify(emm.getTransaction()).rollback();
}
@DB(transaction = REQUIRED)
void dbTransactionException() {
assertEquals(em.getTransaction().getRollbackOnly(), false);
try { dbException(); fail("Exception expected"); } catch (PersistenceException ignored) {}
assertEquals(em.getTransaction().getRollbackOnly(), true);
}
@DB(transaction = NOT_REQUIRED)
void dbException() {
assertEquals(em.getTransaction().getRollbackOnly(), false);
throw new PersistenceException();
}
@Test(expectedExceptions = IllegalStateException.class)
public void testNoDB() throws Exception {
em.flush();
}
static class MockEMFModule extends AbstractModule {
@Override
protected void configure() {
bind(EntityManagerFactory.class).toInstance(mock(EntityManagerFactory.class));
}
}
}