/*
* ome.server.utests.handlers.SessionHandlerMockHibernateTest
*
* Copyright 2006 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.server.utests.handlers;
// Java imports
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import junit.framework.Assert;
import ome.api.ServiceInterface;
import ome.api.StatefulServiceInterface;
import ome.conditions.InternalException;
import ome.services.messages.RegisterServiceCleanupMessage;
import ome.system.OmeroContext;
import ome.tools.hibernate.SessionHandler;
import omeis.providers.re.RenderingEngine;
import org.aopalliance.intercept.MethodInvocation;
import org.hibernate.FlushMode;
import org.hibernate.SessionFactory;
import org.hibernate.classic.Session;
import org.jmock.Mock;
import org.jmock.MockObjectTestCase;
import org.jmock.builder.ArgumentsMatchBuilder;
import org.jmock.core.Invocation;
import org.jmock.core.InvocationMatcher;
import org.jmock.core.Stub;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* @author Josh Moore <a
* href="mailto:josh.moore@gmx.de">josh.moore@gmx.de</a>
* @version 1.0 <small> (<b>Internal version:</b> $Rev$ $Date$) </small>
* @since Omero 2.0
*/
@Test(groups = { "hibernate", "stateful", "ticket:326" })
public class SessionHandlerMockHibernateTest extends MockObjectTestCase {
private OmeroContext context = new OmeroContext(
new String[] { "classpath:ome/services/messaging.xml" });
private ServiceInterface stateless;
private StatefulServiceInterface stateful;
private SessionHandler handler;
private Session session;
private SessionFactory factory;
private DataSource dataSource;
private Connection connection;
private MethodInvocation invocation;
private Mock mockSession, mockFactory, mockInvocation, mockStateful,
mockStateless, mockDataSource, mockTransaction, mockConnection;
private List<RegisterServiceCleanupMessage> cleanups = new ArrayList<RegisterServiceCleanupMessage>();
@Override
@BeforeMethod
protected void setUp() throws Exception {
super.setUp();
newDataSource();
newConnection();
newSession();
newSessionFactory();
handler = new SessionHandler(factory);
// As of r2677 (ticket:1013) session close is delayed
// to servicehandler cleanup. We are simulating this.
handler.setApplicationContext(context);
context.addApplicationListener(new ApplicationListener() {
public void onApplicationEvent(ApplicationEvent arg0) {
if (arg0 instanceof RegisterServiceCleanupMessage) {
cleanups.add((RegisterServiceCleanupMessage)arg0);
}
}
});
// must call newXInvocation in test
// these are reused unless otherwise noted
newStateful();
newStateless();
// Things should always be cleaned up by handler/interceptor
assertFalse(TransactionSynchronizationManager.hasResource(factory));
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.initSynchronization();
}
}
@Override
@AfterMethod
protected void tearDown() throws Exception {
session = null;
reset(mockStateful, mockStateless, mockSession, mockFactory,
mockTransaction, mockDataSource, mockConnection, mockInvocation);
super.tearDown();
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.clearSynchronization();
}
}
// ~ Tests
// =========================================================================
@Test(expectedExceptions = InternalException.class)
public void testStatelessInvocation() throws Throwable {
newStatelessInvocation();
handler = new SessionHandler(factory);
handler.invoke(invocation);
super.verify();
}
@Test(groups = "broken")
public void testStatefulInvocationGetsNewSession() throws Throwable {
newStatefulReadInvocation();
opensSession();
setsFlushMode();
beginsTransaction(1);
// invocation here
disconnectsSession();
handler.invoke(invocation);
super.verify();
}
@Test(groups = "broken")
public void testSecondStatefulInvocationsReusesSession() throws Throwable {
newStatefulReadInvocation();
opensSession();
setsFlushMode();
// getsNewConnection();
beginsTransaction(2);
checksSessionIsOpen();
// invocation here
disconnectsSession();
handler.invoke(invocation);
// assume someone clears thread
TransactionSynchronizationManager.unbindResource(factory);
// And a second call should just work.
newStatefulReadInvocation();
// invocation here
// TODO DELETE
// checksSessionIsConnected(false);
// reconnectsSession();
disconnectsSession();
handler.invoke(invocation);
super.verify();
}
@Test(expectedExceptions = InternalException.class)
public void testStatefulInvocationWithExistingSession() throws Throwable {
prepareThreadWithSession();
newStatefulReadInvocation();
checksSessionIsOpen();
checksSessionIsConnected();
disconnectsSession();
closesSession();
handler.invoke(invocation);
super.verify();
}
@Test
public void testClosedOnException() throws Throwable {
prepareThreadWithSession();
try {
newStatefulReadInvocationThrows();
checksSessionIsOpen();
checksSessionIsConnected();
// here it throws
disconnectsSession();
closesSession();
handler.invoke(invocation);
fail("Should have thrown.");
} catch (Exception e) {
}
}
@Test(groups = "broken")
public void testStatefulInvocationWithSessionThenClosed() throws Throwable {
newStatefulDestroyInvocation();
opensSession();
setsFlushMode(FlushMode.COMMIT);
beginsTransaction(1);
disconnectsSession();
closesSession();
handler.invoke(invocation);
for (RegisterServiceCleanupMessage r : cleanups) {
r.close();
}
super.verify();
}
@Test(expectedExceptions = InternalException.class)
public void testStatefulReentrantCallThrows() throws Throwable {
Method method = RenderingEngine.class.getMethod("getDefaultZ");
newStatefulInvocation(method, new Stub() {
public Object invoke(Invocation dummy) throws Throwable {
handler.invoke(invocation);
return null;
}
public StringBuffer describeTo(StringBuffer buffer) {
return buffer.append(" reentrant call ");
}
});
opensSession();
setsFlushMode();
beginsTransaction(2);
checksSessionIsOpen();
// invocation here
checksSessionIsConnected();
disconnectsSession();
closesSession();
handler.invoke(invocation);
}
// TODO add dirty session on close
// TODO
// ~ Once Expectations (creation events)
// =========================================================================
private void opensSession() {
mockFactory.expects(once()).method("openSession").will(
returnValue(session));
}
private void beginsTransaction(int count) {
mockSession.expects(exactly(count)).method("beginTransaction");
}
// ~ More-than-once Expectations (somewhat idempotent)
// =========================================================================
private void checksSessionIsOpen() {
mockSession.expects(atLeastOnce()).method("isOpen").will(
returnValue(true));
}
private void checksSessionIsConnected(Boolean... connected) {
mockSession.expects(atLeastOnce()).method("isConnected").will(
returnValue(connected.length > 0 ? connected[0] : true));
}
private void getsNewConnection() {
mockDataSource.expects(atLeastOnce()).method("getConnection").will(
returnValue(connection));
}
private void getsSessionsConnection() {
mockSession.expects(atLeastOnce()).method("connection").will(
returnValue(connection));
}
private void reconnectsSession() {
mockSession.expects(atLeastOnce()).method("reconnect");
}
private void setsFlushMode(FlushMode... modes) {
// done by handler see ticket:557
if (modes.length == 0) {
mockSession.expects(atLeastOnce()).method("setFlushMode").with(
eq(FlushMode.COMMIT));
mockSession.expects(atLeastOnce()).method("setFlushMode").with(
eq(FlushMode.MANUAL));
} else {
for (FlushMode mode : modes) {
mockSession.expects(atLeastOnce()).method("setFlushMode").with(
eq(mode));
}
}
}
private void disconnectsSession() {
mockSession.expects(atLeastOnce()).method("disconnect");
}
private void closesSession() {
mockSession.expects(atLeastOnce()).method("close");
}
// ~ Helpers
// =========================================================================
private void newDataSource() {
mockDataSource = mock(DataSource.class);
dataSource = (DataSource) mockDataSource.proxy();
}
private void newConnection() {
mockConnection = mock(Connection.class);
connection = (Connection) mockConnection.proxy();
}
private void newSession() {
mockSession = mock(Session.class);
session = (Session) mockSession.proxy();
}
private void newSessionFactory() {
mockFactory = mock(SessionFactory.class);
factory = (SessionFactory) mockFactory.proxy();
}
private void newStateful() {
mockStateful = mock(StatefulServiceInterface.class);
stateful = (StatefulServiceInterface) mockStateful.proxy();
}
private void newStateless() {
mockStateless = mock(ServiceInterface.class);
stateless = (ServiceInterface) mockStateless.proxy();
}
private void newStatelessInvocation() {
mockInvocation = mock(MethodInvocation.class);
invocation = (MethodInvocation) mockInvocation.proxy();
mockInvocation.expects(once()).method("getThis").will(
returnValue(stateless));
}
private void newStatefulReadInvocation() throws Exception {
Method method = RenderingEngine.class.getMethod("getDefaultZ");
newStatefulInvocation(method);
}
private void newStatefulReadInvocationThrows() throws Exception {
Method method = RenderingEngine.class.getMethod("getDefaultZ");
newStatefulInvocation(method, throwException(new RuntimeException()));
}
private void newStatefulWriteInvocation() throws Exception {
Method method = RenderingEngine.class.getMethod("setDefaultZ");
newStatefulInvocation(method);
}
private void newStatefulWriteInvocationThrows() throws Exception {
Method method = RenderingEngine.class.getMethod("setDefaultZ");
newStatefulInvocation(method, throwException(new RuntimeException()));
}
private void newStatefulDestroyInvocation() throws Exception {
Method method = RenderingEngine.class.getMethod("close");
newStatefulInvocation(method);
}
/** uses the first stub passed (if any) on the will(); clause. */
private void newStatefulInvocation(Method method, Stub... stubs) {
mockInvocation = mock(MethodInvocation.class);
invocation = (MethodInvocation) mockInvocation.proxy();
mockInvocation.expects(atLeastOnce()).method("getThis").will(
returnValue(stateful));
mockInvocation.expects(atLeastOnce()).method("getMethod").will(
returnValue(method));
ArgumentsMatchBuilder amb = mockInvocation.expects(once()).method(
"proceed");
if (stubs != null && stubs.length > 0) {
amb.will(stubs[0]);
}
}
private void prepareThreadWithSession() {
mockSession.expects(once()).method("beginTransaction").id("prep");
SessionHolder sessionHolder = new SessionHolder(session);
sessionHolder.setTransaction(sessionHolder.getSession()
.beginTransaction());
TransactionSynchronizationManager.bindResource(factory, sessionHolder);
}
private void reset(Mock... mocks) {
for (Mock mock : mocks) {
if (mock != null) {
mock.reset();
}
}
}
private Stub printStackTrace() {
return new StackTraceStub();
}
private class StackTraceStub implements Stub {
public StringBuffer describeTo(StringBuffer buffer) {
return buffer.append("prints stack trace");
}
public Object invoke(Invocation invocation) throws Throwable {
new Throwable().printStackTrace();
return null;
}
}
private InvokedRecorder exactly(int count) {
return new InvokedRecorder(count);
}
// TODO refactor out to ome.testing
private class InvokedRecorder implements InvocationMatcher {
private int actual = 0;
private int expected = 0;
public InvokedRecorder(int expected) {
this.expected = expected;
}
public boolean matches(Invocation invocation) {
return true;
}
public void invoked(Invocation invocation) {
actual++;
}
public void verify() {
Assert.assertTrue("expected method was not called " + expected
+ " rather " + actual + " times.", actual == expected);
}
public boolean hasDescription() {
return true;
}
public StringBuffer describeTo(StringBuffer buffer) {
buffer.append("expected " + expected + " times");
buffer.append(" and has been invoked " + actual + " times");
return buffer;
}
}
}