// Copyright (c) 2005 Dustin Sallings <dustin@spy.net> package net.spy.db; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import net.spy.db.savables.CollectionSavable; import net.spy.db.savables.SavableHashMap; import net.spy.db.savables.SavableHashSet; import net.spy.util.SpyConfig; import org.jmock.Mock; import org.jmock.MockObjectTestCase; import org.jmock.core.constraint.IsEqual; import org.jmock.core.constraint.IsInstanceOf; import org.jmock.core.matcher.InvokeOnceMatcher; import org.jmock.core.stub.ReturnStub; /** * Test savable. */ public class SaverTest extends MockObjectTestCase { private SpyConfig successConfig=null; private SpyConfig failConfig=null; private SpyConfig isoConfig=null; private SpyConfig brokenConfig=null; private MockConnectionSource successSource=null; private MockConnectionSource failSource=null; private MockConnectionSource isoSource=null; /** * Get an instance of SaverTest. */ public SaverTest(String name) { super(name); } /** * Set up the tests. */ @Override protected void setUp() { ConnectionSourceFactory cnf=ConnectionSourceFactory.getInstance(); successConfig=new SpyConfig(); successConfig.put("dbConnectionSource", SuccessConnectionSource.class.getName()); successSource= (MockConnectionSource)cnf.getConnectionSource(successConfig); isoConfig=new SpyConfig(); isoConfig.put("dbConnectionSource", MockConnectionSourceWithIso.class.getName()); isoSource=(MockConnectionSource)cnf.getConnectionSource(isoConfig); failConfig=new SpyConfig(); failConfig.put("dbConnectionSource", MockFailingConnectionSource.class.getName()); failSource=(MockConnectionSource)cnf.getConnectionSource(failConfig); brokenConfig=new SpyConfig(); brokenConfig.put("dbConnectionSource", BrokenConnectionSource.class.getName()); } /** * Shut down the tests. */ @Override protected void tearDown() { successSource.clearSeenObjects(); isoSource.clearSeenObjects(); failSource.clearSeenObjects(); } private void verifyAllConnections() throws Exception { successSource.verifyConnections(); failSource.verifyConnections(); isoSource.verifyConnections(); } /** * Test a basic run with a success. */ public void testSuccessfulConnection() throws Exception { Connection conn=successSource.getConnection(successConfig); conn.setAutoCommit(false); conn.commit(); conn.setAutoCommit(true); successSource.returnConnection(conn); verifyAllConnections(); } /** * Test a basic run with a rollback. */ public void testFailingConnection() throws Exception { Connection conn=failSource.getConnection(failConfig); conn.setAutoCommit(false); conn.rollback(); conn.setAutoCommit(true); failSource.returnConnection(conn); verifyAllConnections(); } /** * Test an empty saver. */ public void testEmptySaver() throws Exception { Saver s=new Saver(successConfig); Mock mockSavable=mock(Savable.class); mockSavable.expects(once()).method("isNew") .will(returnValue(true)); mockSavable.expects(once()).method("save") .with(new IsInstanceOf(Connection.class), new IsInstanceOf(SaveContext.class)); // Return an empty list of pre savs mockSavable.expects(once()).method("getPreSavables") .with(new IsInstanceOf(SaveContext.class)) .will(returnValue(Collections.EMPTY_LIST)); // Return null for post savs, which should do the same thing mockSavable.expects(once()).method("getPostSavables") .with(new IsInstanceOf(SaveContext.class)) .will(returnValue(null)); s.save((Savable)mockSavable.proxy()); verifyAllConnections(); } /** * Test an empty saver with a context. */ @SuppressWarnings("unchecked") public void testEmptySaverWithContext() throws Exception { SaveContext context=new SaveContext(); context.put("a", "b"); Saver s=new Saver(successConfig, context); Mock mockSavable=mock(Savable.class); mockSavable.expects(once()).method("isNew") .will(returnValue(true)); mockSavable.expects(once()).method("save") .with(new IsInstanceOf(Connection.class), eq(context)); mockSavable.expects(once()).method("getPreSavables") .with(eq(context)) .will(returnValue(Collections.EMPTY_LIST)); mockSavable.expects(once()).method("getPostSavables") .with(eq(context)) .will(returnValue(Collections.EMPTY_LIST)); s.save((Savable)mockSavable.proxy()); verifyAllConnections(); } /** * Test an empty saver with an isolation level (and context). */ @SuppressWarnings("unchecked") public void testEmptySaverWithIsolation() throws Exception { SaveContext context=new SaveContext(); context.put("c", "d"); Saver s=new Saver(isoConfig, context); Mock mockSavable=mock(Savable.class); mockSavable.expects(once()).method("isNew") .will(returnValue(true)); mockSavable.expects(once()).method("save") .with(new IsInstanceOf(Connection.class), eq(context)); mockSavable.expects(once()).method("getPreSavables") .with(eq(context)) .will(returnValue(Collections.EMPTY_LIST)); mockSavable.expects(once()).method("getPostSavables") .with(eq(context)) .will(returnValue(Collections.EMPTY_LIST)); s.save((Savable)mockSavable.proxy(), Connection.TRANSACTION_SERIALIZABLE); verifyAllConnections(); } private void failingSaverTest(Throwable t) throws Exception { Saver s=new Saver(failConfig); Mock mockSavable=mock(Savable.class); // These objects will present themselves as not new, but modified mockSavable.expects(once()).method("isNew") .will(returnValue(false)); mockSavable.expects(once()).method("isModified") .will(returnValue(true)); mockSavable.expects(once()).method("save") .with(new IsInstanceOf(Connection.class), new IsInstanceOf(SaveContext.class)) .will(throwException(t)); mockSavable.expects(once()).method("getPreSavables") .with(new IsInstanceOf(SaveContext.class)) .will(returnValue(Collections.EMPTY_LIST)); // Note: getPostSavables will not be called due to the exception try { s.save((Savable)mockSavable.proxy()); fail("Expected a SaveException"); } catch(SaveException e) { // pass } verifyAllConnections(); } /** * Test an empty saver that fails. */ public void testEmptyFailingSaverWithSaveException() throws Exception { failingSaverTest(new SaveException("Testing a failure")); } /** * Test an empty saver that fails with a SQLException. */ public void testEmptyFailingSaverWithSQLException() throws Exception { failingSaverTest(new SQLException("Testing a failure")); } private void assertAllSaved(Collection<?> c) { for(Iterator<?> i=c.iterator(); i.hasNext(); ) { TestSavable ts=(TestSavable)i.next(); assertTrue(ts.committed); } } /** * Test a complex sequence of savables. */ @SuppressWarnings("unchecked") public void testComplexSequence() throws Exception { // Our root object is going to be TestSavable 1 TestSavable ts1=new TestSavable(1); // It's going to have a collection of preSavables ts1.preSavs.add(new TestSavable(2)); ts1.preSavs.add(new TestSavable(3)); // It's going to have a collection of and postsavables ts1.postSavs.add(new TestSavable(4)); ts1.postSavs.add(new TestSavable(5)); // One of the postsavables is going to have presavables TestSavable ts6=new TestSavable(6); ts6.preSavs.add(new TestSavable(7)); ts6.preSavs.add(new TestSavable(8)); ts1.postSavs.add(ts6); // And one postsavable with a presavable and a postsavable, but it // should, itself, not be saved. TestSavable ts9=new TestSavable(9); ts9.setNew(false); ts9.setModified(false); ts9.postSavs.add(new TestSavable(10)); ts9.preSavs.add(new TestSavable(11)); ts6.postSavs.add(ts9); // Add the same object again, make sure we don't try to save it the // second time ts9.postSavs.add(ts6); // OK, and for our final trick, we're going to add a collection Collection someSavables=new ArrayList(); someSavables.add(new TestSavable(12)); someSavables.add(new TestSavable(13)); // Including one we've seen before. someSavables.add(ts6); ts9.postSavs.add(someSavables); // Perform the save SaveContext context=new SaveContext(); context.put("sequence", new ArrayList()); Saver s=new Saver(successConfig, context); s.save(ts1); // That should yield the following sequence: int[] seq={2, 3, 1, 4, 5, 7, 8, 6, 11, 10, 12, 13}; ArrayList al=new ArrayList(); for(int i=0; i<seq.length; i++) { al.add(new Integer(seq[i])); } assertEquals(al, context.get("sequence")); assertTrue(ts1.committed); verifyAllConnections(); } /** * Test an object getting resaved. */ public void testResavable() throws Exception { Saver s=new Saver(successConfig); BasicSavable s1=new BasicSavable(); assertFalse(s1.saved); s.save(s1); // After the first save, it should be considered saved assertTrue(s1.saved); s1.saved=false; s.save(s1); // A second save should not modify it assertFalse(s1.saved); // ...but if we consider it modified, we should be able to save again s1.modify(); s.save(s1); assertTrue(s1.saved); } /** * Test a save with an invalid savable. */ @SuppressWarnings("unchecked") public void testInvalidObject() throws Exception { TestSavable ts1=new TestSavable(1); // This is some arbitrary object that is not savable, but more // importantly, it's also really big and will exercise the debug // stringifier a bit more. ts1.postSavs.add(new Integer(13)); Saver s=new Saver(failConfig); try { s.save(ts1); fail("Shouldn't allow me to save this object."); } catch(SaveException e) { assertTrue(e.getMessage().startsWith("Invalid object type")); } assertFalse(ts1.committed); verifyAllConnections(); } /** * Test a save with a null object. */ @SuppressWarnings("unchecked") public void testNullObject() throws Exception { TestSavable ts1=new TestSavable(1); // This is some arbitrary object that is not savable, but more // importantly, it's also really big and will exercise the debug // stringifier a bit more. ts1.postSavs.add(null); Saver s=new Saver(failConfig); try { s.save(ts1); fail("Shouldn't allow me to save this object."); } catch(NullPointerException e) { assertNotNull(e.getMessage()); assertTrue(e.getMessage().startsWith("Got a null object")); } assertFalse(ts1.committed); verifyAllConnections(); } /** * Test a save with a broken connection source. */ public void testBrokenConnSrc() throws Exception { TestSavable ts1=new TestSavable(1); Saver s=new Saver(brokenConfig); try { s.save(ts1); fail("Shouldn't allow me to save this object."); } catch(SaveException e) { assertNotNull(e.getMessage()); assertEquals("Error saving object", e.getMessage()); } assertFalse(ts1.committed); verifyAllConnections(); } /** * Validate the right thing happens when we try too complicated of an * object graph. */ @SuppressWarnings("unchecked") public void testExcessiveDepth() throws Exception { // Build out a deep object graph. Max depth is 100 TestSavable ts1=new TestSavable(1); TestSavable currentSavable=ts1; for(int i=2; i<110; i++) { TestSavable newSavable=new TestSavable(i); currentSavable.postSavs.add(newSavable); currentSavable=newSavable; } // Perform the save Saver s=new Saver(failConfig); try { s.save(ts1); fail("Shouldn't allow me to save really deep objects"); } catch(SaveException e) { assertTrue(e.getMessage().startsWith("Recursing too deep!")); } verifyAllConnections(); } /** * Test the CollectionSavable implementation. */ @SuppressWarnings("unchecked") public void testCollectionSavable() throws Exception { ArrayList al=new ArrayList(); ArrayList expectedSequence=new ArrayList(); for(int i=0; i<10; i++) { al.add(new TestSavable(i)); expectedSequence.add(new Integer(i)); } SaveContext ctx=new SaveContext(); ctx.put("sequence", new ArrayList()); Saver s=new Saver(successConfig, ctx); s.save(new CollectionSavable(al)); ArrayList seenSequence=new ArrayList(); for(Iterator i=al.iterator(); i.hasNext();) { TestSavable ts=(TestSavable)i.next(); seenSequence.add(new Integer(ts.id)); } assertAllSaved(al); assertEquals("Save order was wrong", expectedSequence, seenSequence); } @SuppressWarnings("unchecked") private void populateMapAndTest(SavableHashMap shm) throws Exception { shm.put("1", new TestSavable(1)); shm.put("2", new TestSavable(2)); Saver s=new Saver(successConfig); s.save(shm); assertAllSaved(shm.values()); } /** * Test the savable HashMap implementation. */ @SuppressWarnings("unchecked") public void testSavableHashMap() throws Exception { // Map constructor Map<String, Savable> m=new HashMap<String, Savable>(); m.put("1", new TestSavable(1)); m.put("2", new TestSavable(2)); SavableHashMap<String, Savable> shm =new SavableHashMap<String, Savable>(m); shm.save((Connection)mock(Connection.class).proxy(), new SaveContext()); Saver s=new Saver(successConfig); s.save(shm); assertAllSaved(m.values()); populateMapAndTest(new SavableHashMap()); populateMapAndTest(new SavableHashMap(2)); populateMapAndTest(new SavableHashMap(2, 0.5f)); } private void populateSetAndTest(SavableHashSet shs) throws Exception { shs.add(new TestSavable(1)); shs.add(new TestSavable(2)); Saver s=new Saver(successConfig); s.save(shs); assertAllSaved(shs); } /** * Test the savable HashSet implementation. */ public void testSavableHashSet() throws Exception { // Map constructor Collection<Savable> c=new HashSet<Savable>(); c.add(new TestSavable(1)); c.add(new TestSavable(2)); SavableHashSet shs=new SavableHashSet(c); shs.save((Connection)mock(Connection.class).proxy(), new SaveContext()); Saver s=new Saver(successConfig); s.save(shs); assertAllSaved(c); populateSetAndTest(new SavableHashSet()); populateSetAndTest(new SavableHashSet(2)); populateSetAndTest(new SavableHashSet(2, 0.5f)); } /** * A connection source for mock connections. */ public static class SuccessConnectionSource extends MockConnectionSource { @Override protected void setupMock(Mock connMock, SpyConfig conf) { // autocommit will be enabled, and then disabled connMock.expects(new InvokeOnceMatcher()).method("setAutoCommit") .with(new IsEqual(Boolean.FALSE)).id("disableAutocommit"); connMock.expects(new InvokeOnceMatcher()).method("commit") .after("disableAutocommit").id("commitSuccess"); connMock.expects(new InvokeOnceMatcher()).method("setAutoCommit") .with(new IsEqual(Boolean.TRUE)).after("commitSuccess"); connMock.expects(new InvokeOnceMatcher()).method("close"); } } public static class MockConnectionSourceWithIso extends MockConnectionSource { @Override protected void setupMock(Mock connMock, SpyConfig conf) { // autocommit will be enabled, and then disabled connMock.expects(new InvokeOnceMatcher()) .method("getTransactionIsolation") .will(new ReturnStub( new Integer(Connection.TRANSACTION_READ_UNCOMMITTED))); connMock.expects(new InvokeOnceMatcher()) .method("setTransactionIsolation") .with(new IsEqual( new Integer(Connection.TRANSACTION_SERIALIZABLE))) .id("initialIsolationSet"); connMock.expects(new InvokeOnceMatcher()).method("setAutoCommit") .with(new IsEqual(Boolean.FALSE)).id("disableAutocommit"); connMock.expects(new InvokeOnceMatcher()).method("commit") .after("disableAutocommit").id("commitSuccess"); connMock.expects(new InvokeOnceMatcher()).method("setAutoCommit") .with(new IsEqual(Boolean.TRUE)).after("commitSuccess") .id("resetAutoCommit"); connMock.expects(new InvokeOnceMatcher()) .method("setTransactionIsolation") .with(new IsEqual( new Integer(Connection.TRANSACTION_READ_UNCOMMITTED))) .after("resetAutoCommit") .id("resetIsolation"); connMock.expects(new InvokeOnceMatcher()).method("close"); } } public static class MockFailingConnectionSource extends MockConnectionSource { @Override protected void setupMock(Mock connMock, SpyConfig conf) { // autocommit will be enabled, and then disabled connMock.expects(new InvokeOnceMatcher()).method("setAutoCommit") .with(new IsEqual(Boolean.FALSE)).id("disableAutocommit"); connMock.expects(new InvokeOnceMatcher()).method("rollback") .after("disableAutocommit").id("rollbackFail"); connMock.expects(new InvokeOnceMatcher()).method("setAutoCommit") .with(new IsEqual(Boolean.TRUE)).after("rollbackFail"); connMock.expects(new InvokeOnceMatcher()).method("close"); } } public static class BrokenConnectionSource extends Object implements ConnectionSource { public Connection getConnection(SpyConfig conf) throws SQLException { throw new SQLException("Can't get a connection"); } public void returnConnection(Connection conn) { throw new RuntimeException("Can't returna connection here"); } } private static class BasicSavable extends AbstractSavable { public boolean saved=false; public BasicSavable() { super(); setNew(true); } @Override public void setNew(boolean to) { super.setNew(to); } @Override public void setModified(boolean to) { super.setModified(to); } public void save(Connection conn, SaveContext ctx) { saved=true; } } // // A flexible savable was can set up a predicable test with // private static final class TestSavable extends BasicSavable implements TransactionListener { public Collection<Savable> preSavs=null; @SuppressWarnings("unchecked") public Collection postSavs=null; public int id=0; public boolean committed=false; public TestSavable(int i) { super(); this.id=i; this.preSavs=new ArrayList<Savable>(); this.postSavs=new ArrayList<Savable>(); } @Override public Collection<? extends Savable> getPreSavables(SaveContext ctx) { return(preSavs); } @Override @SuppressWarnings("unchecked") public Collection<? extends Savable> getPostSavables(SaveContext ctx) { return(postSavs); } @Override @SuppressWarnings("unchecked") public void save(Connection conn, SaveContext ctx) { super.save(conn, ctx); Collection sequence=(Collection)ctx.get("sequence"); if(sequence != null) { sequence.add(new Integer(id)); } } @Override public String toString() { return "TestSavable is an object that gets exposes " + "enough of itself to make for a pretty useful test suite"; } @Override public void transactionCommited() { super.transactionCommited(); committed=true; } } }