/* * Copyright 2013 Gordon Burgett and individual contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xflatdb.xflat.db; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.xflatdb.xflat.Cursor; import org.xflatdb.xflat.DuplicateKeyException; import org.xflatdb.xflat.KeyNotFoundException; import org.xflatdb.xflat.convert.ConversionService; import org.xflatdb.xflat.convert.DefaultConversionService; import org.xflatdb.xflat.convert.converters.JDOMConverters; import org.xflatdb.xflat.convert.converters.StringConverters; import org.xflatdb.xflat.db.EngineBase.SpinDownEventHandler; import org.xflatdb.xflat.query.XPathQuery; import org.xflatdb.xflat.query.XPathUpdate; import org.xflatdb.xflat.transaction.FakeThreadContextTransactionManager; import org.xflatdb.xflat.transaction.Transaction; import org.xflatdb.xflat.transaction.TransactionException; import org.xflatdb.xflat.transaction.TransactionOptions; import org.xflatdb.xflat.transaction.WriteConflictException; import org.xflatdb.xflat.util.FakeDocumentFileWrapper; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.output.XMLOutputter; import org.jdom2.xpath.XPathFactory; import org.junit.AfterClass; import static org.junit.Assert.*; import org.junit.BeforeClass; import org.junit.Test; import test.Utils; import static org.mockito.Mockito.*; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.xflatdb.xflat.XFlatConstants; import org.xflatdb.xflat.transaction.TransactionScope; /** * * @author gordon */ public abstract class EngineTestsBase<TEngine extends EngineBase> { protected static ConversionService conversionService; protected static org.jdom2.xpath.XPathFactory xpath = org.jdom2.xpath.XPathFactory.instance(); protected static File workspace; @BeforeClass public static void setUpClass() { conversionService = new DefaultConversionService(); StringConverters.registerTo(conversionService); JDOMConverters.registerTo(conversionService); workspace = new File("enginetests"); if(!workspace.exists()){ workspace.mkdirs(); } } @AfterClass public static void tearDownClass(){ Utils.deleteDir(workspace); workspace.delete(); } /** * Gets a new test context for this test, creating the workspace * directory and setting up the engine instance. */ protected TestContext getContext(){ TestContext ctx = new TestContext(); ctx.workspace = new File(workspace, Long.toString(ctx.id)); if(!ctx.workspace.exists()){ ctx.workspace.mkdirs(); } else{ Utils.deleteDir(ctx.workspace); } prepContext(ctx); ctx.instance = setupEngine(ctx); return ctx; } /** * Spins down the Engine, waiting for it to complete and throwing * an exception if it times out. * @param instance The engine to spin down * @throws InterruptedException * @throws TimeoutException */ protected void spinDown(TestContext ctx) throws InterruptedException, TimeoutException { this.spinDown(ctx, true); } /** * Spins down the Engine, optionally waiting for it to complete and throwing * an exception if it times out. * @param engine The engine to spin down * @param synchronous True to wait for the engine to spin down, with a timeout. * @throws InterruptedException * @throws TimeoutException */ protected void spinDown(final TestContext ctx, boolean synchronous) throws InterruptedException, TimeoutException{ final EngineBase engine = ctx.instance; if(!ctx.spinDownInvoked.compareAndSet(false, true)){ return; } if(engine.getState() != EngineState.Running){ throw new UnsupportedOperationException("cannot spin down an engine in state " + engine.getState()); } final AtomicReference<EngineState> didTimeOut = new AtomicReference(null); final AtomicBoolean didSpinDown = new AtomicBoolean(false); final Object notifyMe = new Object(); engine.spinDown(new EngineBase.SpinDownEventHandler(){ @Override public void spinDownComplete(EngineBase.SpinDownEvent event) { didSpinDown.get(); synchronized(notifyMe){ notifyMe.notifyAll(); } } }); if(synchronous){ ScheduledFuture<?> timeout = ctx.executorService.schedule(new Runnable(){ @Override public void run() { if(engine.getState() == EngineState.SpunDown){ return; } if(didSpinDown.get()){ return; } synchronized(notifyMe){ didTimeOut.set(engine.getState()); engine.forceSpinDown(); notifyMe.notifyAll(); } } }, 500, TimeUnit.MILLISECONDS); while(engine.getState() != EngineState.SpunDown){ synchronized(notifyMe){ notifyMe.wait(); } } if(didTimeOut.get() != null){ if(didSpinDown.get()){ fail("Spin down completed, but timeout was called with engine state " + didTimeOut.get()); } throw new TimeoutException("spin down timed out with engine state " + didTimeOut.get()); } else{ if(timeout != null) timeout.cancel(true); } } } protected void verifySpinDownComplete(TestContext ctx) throws InterruptedException{ if(ctx.instance.getState() != EngineState.SpunDown){ //give it a little leeway Thread.sleep(500); } assertEquals("Should have spun down", EngineState.SpunDown, ctx.instance.getState()); } private TEngine setupEngine(TestContext ctx){ TEngine instance = this.createInstance(ctx); instance.setExecutorService(ctx.executorService); instance.setConversionService(conversionService); instance.setTransactionManager(ctx.transactionManager); instance.setIdGenerator(new BigIntIdGenerator()); return instance; } protected void spinUp(TestContext ctx){ ctx.spinDownInvoked.set(false); ctx.instance.spinUp(); ctx.instance.beginOperations(); } /** * Gets the engine instance to test. This may be called more than once per test, * with the expectation that the instance will be for the same table after * spinning down the first instance. * @return */ protected abstract TEngine createInstance(TestContext ctx); /** * Prepares the test context by creating and storing dependencies * @param ctx */ protected abstract void prepContext(TestContext ctx); /** * Prepares the underlying XML file by writing the given contents to it. * @param contents The contents which should be stored in the underlying XML file. */ protected abstract void prepFileContents(TestContext ctx, Document contents) throws IOException; /** * Gets the contents of the written file as a Document. This will be invoked * after spin down to get the final table contents. * @return */ protected abstract Document getFileContents(TestContext ctx) throws IOException, JDOMException; protected String getId(Element row){ return row.getAttributeValue("id", XFlatConstants.xFlatNs); } protected Element setId(Element row, String id){ row.setAttribute("id", id, XFlatConstants.xFlatNs); return row; } protected Element findId(Iterable<Element> rows, String id){ for(Element r : rows){ if(id.equals(getId(r))) return r; } return null; } //<editor-fold desc="tests"> //<editor-fold desc="transactionless"> @Test public void testInsert_NoValuesYet_Inserts() throws Exception { System.out.println("testInsert_NoValuesYet_Inserts"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT ctx.instance.insertRow("1", rowData); //ASSERT Element fromEngine = ctx.instance.readRow("1"); assertEquals("Should have updated in engine", "data", fromEngine.getName()); assertEquals("Should have updated in engine", "some text data", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); assertNotNull("File contents should exist", doc); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have 1 row", 1, children.size()); Element data = children.get(0).getChild("data"); assertNotNull("row should have the data", data); assertEquals("row should have the data", "some text data", data.getText()); }//end testInsert_NoValuesYet_Inserts @Test public void testInsert_HasValues_Inserts() throws Exception { System.out.println("testInsert_HasValues_Inserts"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("other text data") ); prepFileContents(ctx, inFile); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT ctx.instance.insertRow("1", rowData); Element fromEngine = ctx.instance.readRow("1"); assertEquals("Should have updated in engine", "data", fromEngine.getName()); assertEquals("Should have updated in engine", "some text data", fromEngine.getText()); spinDown(ctx); //ASSERT Document doc = getFileContents(ctx); assertNotNull("File contents should exist", doc); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have 2 rows", 2, children.size()); Element data = findId(children, "1"); assertNotNull("doc should have the row", data); assertEquals("row should have the data", "some text data", data.getChild("data").getText()); }//end testInsert_HasValues_Inserts @Test public void testInsert_AlreadyHasId_ThrowsDuplicateKeyException() throws Exception { System.out.println("testInsert_AlreadyHasId_ThrowsDuplicateKeyException"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("other text data") ); prepFileContents(ctx, inFile); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT boolean didThrow = false; try { //ACT ctx.instance.insertRow("0", rowData); } catch (DuplicateKeyException expected) { didThrow = true; } assertTrue("Should have thrown DuplicateKeyException", didThrow); spinDown(ctx); //ASSERT Document doc = getFileContents(ctx); assertNotNull("File contents should exist", doc); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have 1 row", 1, children.size()); Element data = findId(children, "0"); assertNotNull("doc should have the row", data); assertEquals("row should have the original data", "other text data", data.getChild("data").getText()); }//end testInsert_AlreadyHasId_ThrowsDuplicateKeyException @Test public void testReadRow_NoRowExists_ReturnsNull() throws Exception { System.out.println("testReadRow_NoRowExists_ReturnsNull"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); //ACT Element data = ctx.instance.readRow("72"); spinDown(ctx); //ASSERT assertNull("Should not read data that does not exist", data); }//end testReadRow_NoRowExists_ReturnsNull @Test public void testReadRow_ReadsWrongRow_ReturnsNull() throws Exception { System.out.println("testReadRow_ReadsWrongRow_ReturnsNull"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("other text data") ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT Element data = ctx.instance.readRow("72"); spinDown(ctx); //ASSERT assertNull("Should not read data that does not exist", data); }//end testReadRow_ReadsWrongRow_ReturnsNull @Test public void testReadRow_ReadsCorrectRow_ReturnsData() throws Exception { System.out.println("testReadRow_ReadsCorrectRow_ReturnsData"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("some text data"), new Element("data").setText("other text data") ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT Element data = ctx.instance.readRow("1"); spinDown(ctx); //ASSERT assertNotNull("Should have read some data", data); assertEquals("Should have read correct data", "other text data", data.getText()); }//end testReadRow_ReadsCorrectRow_ReturnsData @Test public void testQueryTable_NoData_NoResults() throws Exception { System.out.println("testQueryTable_NoData_NoResults"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); //ACT XPathQuery query = XPathQuery.eq(xpath.compile("data/fooInt"), 17); try(Cursor<Element> cursor = ctx.instance.queryTable(query)){ //cursors ought to still read during spin down //but do it async cause we're using the cursor spinDown(ctx, false); //ASSERT assertNotNull("Should have gotten a cursor", cursor); assertFalse("Cursor should not have any results", cursor.iterator().hasNext()); } //now verify that we spin down shortly after this.verifySpinDownComplete(ctx); }//end testQueryTable_NoData_NoResults @Test public void testQueryTable_WrongData_NoResults() throws Exception { System.out.println("testQueryTable_WrongData_NoResults"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("other text data"), new Element("data").setContent( new Element("fooInt").setText("18") ) ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT XPathQuery query = XPathQuery.eq(xpath.compile("data/fooInt"), 17); try(Cursor<Element> cursor = ctx.instance.queryTable(query)){ //cursors ought to still read during spin down //but do it async cause we're using the cursor spinDown(ctx, false); //ASSERT assertNotNull("Should have gotten a cursor", cursor); assertFalse("Cursor should not have any results", cursor.iterator().hasNext()); } //now verify that we spin down shortly after this.verifySpinDownComplete(ctx); }//end testQueryTable_WrongData_NoResults @Test public void testQueryTable_OneMatch_OneResult() throws Exception { System.out.println("testQueryTable_OneMatch_OneResult"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("other text data"), new Element("data").setContent( new Element("fooInt").setText("17") ), new Element("data").setContent( new Element("fooInt").setText("18") ) ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT XPathQuery query = XPathQuery.eq(xpath.compile("data/fooInt"), 17); try(Cursor<Element> cursor = ctx.instance.queryTable(query)){ //cursors ought to still read during spin down //but do it async cause we're using the cursor spinDown(ctx, false); //ASSERT assertNotNull("Should have gotten a cursor", cursor); Iterator<Element> it = cursor.iterator(); assertTrue("Cursor should have a result", it.hasNext()); assertEquals("Result should be correct", "17", it.next().getChild("fooInt").getText()); assertFalse("Cursor should have only one result", it.hasNext()); } //now verify that we spin down shortly after this.verifySpinDownComplete(ctx); }//end testQueryTable_OneMatch_OneResult @Test public void testQueryTable_MultipleMatches_MultipleResults() throws Exception { System.out.println("testQueryTable_MultipleMatches_MultipleResults"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("data").setText("other text data"), new Element("data").setContent( new Element("fooInt").setText("17") ), new Element("data").setContent( new Element("fooInt").setText("18") ) ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT XPathQuery query = XPathQuery.gte(xpath.compile("data/fooInt"), 17); try(Cursor<Element> cursor = ctx.instance.queryTable(query)){ //cursors ought to still read during spin down, //but do it async cause we're using the cursor spinDown(ctx, false); //ASSERT assertNotNull("Should have gotten a cursor", cursor); assertThat(cursor, Matchers.containsInAnyOrder( hasChildText("fooInt", "17"), hasChildText("fooInt", "18") )); } //now verify that we spin down shortly after this.verifySpinDownComplete(ctx); }//end testQueryTable_MultipleMatches_MultipleResults @Test public void testReplaceRow_NoData_ThrowsKeyNotFoundException() throws Exception { System.out.println("testReplaceRow_NoData_ThrowsKeyNotFoundException"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT boolean didThrow = false; try { //ACT ctx.instance.replaceRow("1", rowData); } catch (KeyNotFoundException expected) { didThrow = true; } assertTrue("Should have thrown KeyNotFoundException", didThrow); spinDown(ctx); //ASSERT Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have no data", 0, children.size()); }//end testReplaceRow_NoData_ThrowsKeyNotFoundException @Test public void testReplaceRow_RowExists_Replaced() throws Exception { System.out.println("testReplaceRow_RowExists_Replaced"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT Element replaced = ctx.instance.replaceRow("0", rowData); //ASSERT assertEquals("Should have returned old data", "other", replaced.getName()); assertEquals("Should have returned old data", "other text data", replaced.getText()); Element fromEngine = ctx.instance.readRow("0"); assertEquals("Should have updated in engine", "data", fromEngine.getName()); assertEquals("Should have updated in engine", "some text data", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have correct data", children, Matchers.containsInAnyOrder( hasChildText("data", "some text data"), hasChildText("third", "third text data") )); }//end testReplaceRow_RowExists_Replaced @Test public void testUpdate_NoElementWithId_ThrowsKeyNotFoundException() throws Exception { System.out.println("testUpdate_NoElementWithId_ThrowsKeyNotFoundException"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathUpdate update = XPathUpdate.set(xpath.compile("other"), "updated text"); //ACT boolean didThrow = false; try { //ACT ctx.instance.update("14", update); } catch (KeyNotFoundException expected) { didThrow = true; } assertTrue("Should have thrown KeyNotFoundException", didThrow); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have old data", children, Matchers.containsInAnyOrder( hasChildText("other", "other text data"), hasChildText("third", "third text data") )); }//end testUpdate_NoElementWithId_ThrowsKeyNotFoundException @Test public void testUpdate_RowHasNoUpdateableField_NoUpdatePerformed() throws Exception { System.out.println("testUpdate_RowHasNoUpdateableField_NoUpdatePerformed"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathUpdate update = XPathUpdate.set(xpath.compile("fourth"), "updated text"); //ACT boolean result = ctx.instance.update("0", update); //ASSERT assertFalse("Should have reported unsuccessful update", result); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have old data", children, Matchers.containsInAnyOrder( hasChildText("other", "other text data"), hasChildText("third", "third text data") )); }//end testUpdate_RowHasNoUpdateableField_NoUpdatePerformed @Test public void testUpdate_ElementHasId_UpdatesElement() throws Exception { System.out.println("testUpdate_ElementHasId_UpdatesElement"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathUpdate update = XPathUpdate.set(xpath.compile("other"), "updated text"); //ACT boolean result = ctx.instance.update("0", update); //ASSERT assertTrue("Should have reported successful update", result); Element fromEngine = ctx.instance.readRow("0"); assertEquals("Should have updated in engine", "other", fromEngine.getName()); assertEquals("Should have updated in engine", "updated text", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have updated data", children, Matchers.containsInAnyOrder( hasChildText("other", "updated text"), hasChildText("third", "third text data") )); }//end testUpdate_ElementHasId_UpdatesElement @Test public void testUpdate_NoMatchingElements_NoUpdates() throws Exception { System.out.println("testUpdate_NoMatchingElements_NoUpdates"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third") .setAttribute("fooInt", "23") .setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); XPathUpdate update = XPathUpdate.set(xpath.compile("other"), "updated text"); //ACT int result = ctx.instance.update(query, update); //ASSERT assertEquals("Should report 0 rows updated", 0, result); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have old data", children, Matchers.containsInAnyOrder( hasChildText("other", "other text data"), hasChildText("third", "third text data") )); }//end testUpdate_NoMatchingElements_NoUpdates @Test public void testUpdate_MatchingRowHasNoUpdateableField_NoUpdatesPerformed() throws Exception { System.out.println("testUpdate_MatchingRowHasNoUpdateableField_NoUpdatesPerformed"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third") .setAttribute("fooInt", "17") .setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); XPathUpdate update = XPathUpdate.set(xpath.compile("fourth"), "updated text"); //ACT int result = ctx.instance.update(query, update); //ASSERT assertEquals("Should report 0 rows updated", 0, result); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have old data", children, Matchers.containsInAnyOrder( hasChildText("other", "other text data"), hasChildText("third", "third text data") )); }//end testUpdate_MatchingRowHasNoUpdateableField_NoUpdatesPerformed @Test public void testUpdate_MatchingRowHasUpdateableField_FieldIsUpdated() throws Exception { System.out.println("testUpdate_MatchingRowHasUpdateableField_FieldIsUpdated"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third") .setAttribute("fooInt", "17") .setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); XPathUpdate update = XPathUpdate.set(xpath.compile("third"), "updated text"); //ACT int result = ctx.instance.update(query, update); //ASSERT assertEquals("Should report 1 row updated", 1, result); Element fromEngine = ctx.instance.readRow("1"); assertEquals("Should have updated in engine", "third", fromEngine.getName()); assertEquals("Should have updated in engine", "updated text", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have updated data", children, Matchers.containsInAnyOrder( hasChildText("other", "other text data"), hasChildText("third", "updated text") )); }//end testUpdate_MatchingRowHasUpdateableField_FieldIsUpdated @Test public void testUpdate_MultipleMatchingUpdateableRows_MultipleUpdatesPerformed() throws Exception { System.out.println("testUpdate_MultipleMatchingUpdateableRows_MultipleUpdatesPerformed"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other") .setAttribute("fooInt", "17") .setContent(new Element("data") .setText("other text data") ), new Element("third") .setAttribute("fooInt", "18") .setContent(new Element("data") .setText("third text data") ), new Element("fourth") .setAttribute("fooInt", "15") .setContent(new Element("data") .setText("fourth text data") ) ); prepFileContents(ctx, inFile); spinUp(ctx); XPathQuery query = XPathQuery.gte(xpath.compile("*/@fooInt"), 17); XPathUpdate update = XPathUpdate.set(xpath.compile("*/data"), "updated text"); //ACT int result = ctx.instance.update(query, update); //ASSERT assertEquals("Should report 2 rows updated", 2, result); Element fromEngine = ctx.instance.readRow("0"); assertEquals("Should have updated in engine", "other", fromEngine.getName()); assertThat("Should have updated in engine", fromEngine, hasChildText("data", "updated text")); fromEngine = ctx.instance.readRow("1"); assertEquals("Should have updated in engine", "third", fromEngine.getName()); assertThat("Should have updated in engine", fromEngine, hasChildText("data", "updated text")); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 3, children.size()); assertThat("Document should have updated data", children, Matchers.containsInAnyOrder( hasChildThat("other", hasChildText("data", "updated text")), hasChildThat("third", hasChildText("data", "updated text")), hasChildThat("fourth", hasChildText("data", "fourth text data")) )); }//end testUpdate_MultipleMatchingUpdateableRows_MultipleUpdatesPerformed @Test public void testUpsertRow_RowDoesntExist_Inserts() throws Exception { System.out.println("testUpsertRow_RowDoesntExist_Inserts"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT boolean inserted = ctx.instance.upsertRow("1", rowData); //ASSERT assertTrue("Should report inserted", inserted); Element fromEngine = ctx.instance.readRow("1"); assertEquals("Should have updated in engine", "data", fromEngine.getName()); assertEquals("Should have updated in engine", "some text data", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); assertNotNull("File contents should exist", doc); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have 1 row", 1, children.size()); Element data = children.get(0).getChild("data"); assertNotNull("row should have the data", data); assertEquals("row should have the data", "some text data", data.getText()); }//end testUpsertRow_RowDoesntExist_Inserts @Test public void testUpsertRow_RowExists_Replaces() throws Exception { System.out.println("testUpsertRow_RowExists_Replaces"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); Element rowData = new Element("data").setText("some text data"); //ACT boolean inserted = ctx.instance.upsertRow("0", rowData); //ASSERT assertFalse("Should report as updated", inserted); Element fromEngine = ctx.instance.readRow("0"); assertEquals("Should have updated in engine", "data", fromEngine.getName()); assertEquals("Should have updated in engine", "some text data", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have correct data", children, Matchers.containsInAnyOrder( hasChildText("data", "some text data"), hasChildText("third", "third text data") )); }//end testUpsertRow_RowExists_Replaces @Test public void testDeleteRow_RowDoesntExist_ThrowsKeyNotFoundException() throws Exception { System.out.println("testDeleteRow_RowDoesntExist_ThrowsKeyNotFoundException"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); //ACT boolean didThrow = false; try { //ACT ctx.instance.deleteRow("1"); } catch (KeyNotFoundException expected) { didThrow = true; } assertTrue("Should have thrown KeyNotFoundException", didThrow); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have no elements", 0, children.size()); }//end testDeleteRow_RowDoesntExist_ThrowsKeyNotFoundException @Test public void testDeleteRow_RowExists_Deletes() throws Exception { System.out.println("testDeleteRow_RowExists_Deletes"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT ctx.instance.deleteRow("0"); //ASSERT Element fromEngine = ctx.instance.readRow("0"); assertNull("Row should be deleted in engine", fromEngine); spinDown(ctx); Document doc = getFileContents(ctx); System.out.println(dumpDoc(doc)); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have one fewer element", 1, children.size()); assertThat("Document should have correct data", children, Matchers.contains( hasChildText("third", "third text data") )); }//end testDeleteRow_RowExists_Deletes @Test public void testDeleteAll_MatchesNone_NoneDeleted() throws Exception { System.out.println("testDeleteAll_MatchesNone_NoneDeleted"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other").setText("other text data"), new Element("third").setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); int numDeleted = ctx.instance.deleteAll(query); //ASSERT assertEquals("Should have deleted none", 0, numDeleted); Element fromEngine = ctx.instance.readRow("0"); assertEquals("Should have not changed in engine", "other", fromEngine.getName()); assertEquals("Should have not changed in engine", "other text data", fromEngine.getText()); fromEngine = ctx.instance.readRow("1"); assertEquals("Should have not changed in engine", "third", fromEngine.getName()); assertEquals("Should have not changed in engine", "third text data", fromEngine.getText()); spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have same number of elements", 2, children.size()); assertThat("Document should have correct data", children, Matchers.containsInAnyOrder( hasChildText("other", "other text data"), hasChildText("third", "third text data") )); }//end testDeleteAll_MatchesNone_NoneDeleted @Test public void testDeleteAll_MatchesMultiple_MultipleDeleted() throws Exception { System.out.println("testDeleteAll_MatchesMultiple_MultipleDeleted"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other") .setAttribute("fooInt", "17") .setText("other text data"), new Element("third") .setAttribute("fooInt", "17") .setText("third text data"), new Element("fourth") .setAttribute("fooInt", "18") .setText("fourth text data") ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); int numDeleted = ctx.instance.deleteAll(query); //ASSERT assertEquals("Should have deleted two", 2, numDeleted); Element fromEngine = ctx.instance.readRow("0"); assertNull("Should have deleted row", fromEngine); fromEngine = ctx.instance.readRow("1"); assertNull("Should have deleted row", fromEngine); fromEngine = ctx.instance.readRow("2"); assertNotNull("Should not have deleted row", fromEngine); spinDown(ctx); Document doc = getFileContents(ctx); System.out.println(dumpDoc(doc)); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Document should have fewer elements", 1, children.size()); assertThat("Document should have correct data", children, Matchers.contains( hasChildText("fourth", "fourth text data") )); }//end testDeleteAll_MatchesMultiple_MultipleDeleted //</editor-fold> //<editor-fold desc="transactional"> @Test public void testUpdate_InTransaction_RevertRemovesData() throws Exception { System.out.println("testUpdate_InTransaction_RevertRemovesData"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("third") .setAttribute("fooInt", "17") .setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); XPathUpdate update = XPathUpdate.set(xpath.compile("third"), "updated text"); //ACT try(TransactionScope tx = ctx.transactionManager.openTransaction()){ int result = ctx.instance.update(query, update); assertEquals("Should update in TX", 1, result); Element row = ctx.instance.readRow("0"); assertEquals("Should update in TX", "updated text", row.getValue()); tx.revert(); row = ctx.instance.readRow("0"); assertEquals("Should have reverted TX", "third text data", row.getValue()); } spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have reverted data", "third text data", children.get(0).getChild("third").getText()); } @Test public void testReplaceRow_InTransaction_CommitModifiesData() throws Exception { System.out.println("testReplaceRow_InTransaction_CommitModifiesData"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("third") .setAttribute("fooInt", "17") .setText("third text data") ); prepFileContents(ctx, inFile); ctx.executorService = mock(ScheduledExecutorService.class); spinUp(ctx); Element fourth = new Element("fourth") .setAttribute("fooInt", "17") .setText("fourth text data"); //ACT try(TransactionScope tx = ctx.transactionManager.openTransaction()){ ctx.instance.replaceRow("0", fourth); Element row = ctx.instance.readRow("0"); assertEquals("Should update in TX", "fourth text data", row.getValue()); tx.commit(); row = ctx.instance.readRow("0"); assertEquals("Should have not reverted TX", "fourth text data", row.getValue()); } spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have committed data", "fourth text data", children.get(0).getChild("fourth").getText()); } @Test public void testDeleteRow_InTransaction_RevertReturnsRow() throws Exception { System.out.println("testDeleteRow_InTransaction_RevertReturnsRow"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("third") .setAttribute("fooInt", "17") .setText("third text data") ); prepFileContents(ctx, inFile); spinUp(ctx); //ACT try(TransactionScope tx = ctx.transactionManager.openTransaction()){ ctx.instance.deleteRow("0"); Element row = ctx.instance.readRow("0"); assertNull("Should delete in TX", row); tx.revert(); row = ctx.instance.readRow("0"); assertNotNull("Should have reverted TX", row); assertEquals("Should have reverted TX", "third text data", row.getValue()); } spinDown(ctx); Document doc = getFileContents(ctx); List<Element> children = doc.getRootElement().getChildren("row", XFlatConstants.xFlatNs); assertEquals("Should have reverted data", "third text data", children.get(0).getChild("third").getText()); } @Test public void testInsert_InTransaction_HasReadIsolation() throws Exception { System.out.println("testInsert_InTransaction_HasReadIsolation"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); Element outsideTransaction; Element insideTransaction; try(TransactionScope tx = ctx.transactionManager.openTransaction()){ Element rowData = new Element("data").setText("some text data"); //ACT ctx.instance.insertRow("1", rowData); //swap out the transaction ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(1L); outsideTransaction = ctx.instance.readRow("1"); //swap the transaction back ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(0L); insideTransaction = ctx.instance.readRow("1"); tx.commit(); } assertNull("Outside the TX, should have no data", outsideTransaction); assertEquals("Inside the TX, should have the data", "data", insideTransaction.getName()); assertEquals("Inside the TX, should have the data", "some text data", insideTransaction.getText()); Element fromEngine = ctx.instance.readRow("1"); assertEquals("Should have committed TX data", "data", fromEngine.getName()); assertEquals("Should have committed TX data", "some text data", fromEngine.getText()); spinDown(ctx); } @Test public void testQuery_InTransaction_HasReadIsolation() throws Exception { System.out.println("testQuery_InTransaction_HasReadIsolation"); TestContext ctx = getContext(); Document inFile = Utils.makeDocument(ctx.instance.getTableName(), new Element("other") .setAttribute("fooInt", "17") .setText("other text data"), new Element("third") .setAttribute("fooInt", "17") .setText("third text data"), new Element("fourth") .setAttribute("fooInt", "18") .setText("fourth text data") ); prepFileContents(ctx, inFile); spinUp(ctx); XPathQuery query = XPathQuery.eq(xpath.compile("*/@fooInt"), 17); List<Element> fromCursor = new ArrayList<>(); try(TransactionScope tx = ctx.transactionManager.openTransaction(TransactionOptions.DEFAULT.withReadOnly(true))){ Element rowData = new Element("data") .setAttribute("fooInt", "17") .setText("some text data"); //now that we've opened the transaction, switch contexts and insert ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(1L); ctx.instance.insertRow("4", rowData); //ACT //switch back and query ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(0L); try(Cursor<Element> cursor = ctx.instance.queryTable(query)){ Iterator<Element> it = cursor.iterator(); fromCursor.add(it.next()); //switch contexts and insert again ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(1L); ctx.instance.insertRow("5", new Element("fifth") .setAttribute("fooInt", "17") .setText("fifth text data")); ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(0L); while(it.hasNext()){ fromCursor.add(it.next()); } } } assertEquals("Should have cursored over 2 items", 2, fromCursor.size()); assertThat("should have correct 2 items", fromCursor, Matchers.containsInAnyOrder( Matchers.allOf( isNamed("other"), hasText("other text data") ), Matchers.allOf( isNamed("third"), hasText("third text data") ))); spinDown(ctx); } @Test public void testConflictingWrite_SnapshotIsolation_ThrowsWriteConflictException() throws Exception { System.out.println("testConflictingWrite_SnapshotIsolation_ThrowsWriteConflictException"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); try(TransactionScope tx = ctx.transactionManager.openTransaction()){ Element rowData = new Element("data").setText("some text data"); ctx.instance.insertRow("1", rowData); //swap out the transaction ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(1L); ctx.instance.insertRow("1", new Element("other").setText("other text data")); //swap the transaction back ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(0L); try{ tx.commit(); fail("should have thrown WriteConflictException"); }catch(WriteConflictException ex){ //expected } } } @Test public void testConflictingWrite_SnapshotIsolation_TwoTransactions_ThrowsWriteConflictException() throws Exception { System.out.println("testConflictingWrite_SnapshotIsolation_ThrowsWriteConflictException"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); try(TransactionScope tx = ctx.transactionManager.openTransaction()){ Element rowData = new Element("data").setText("some text data"); ctx.instance.insertRow("1", rowData); //swap out the transaction ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(1L); try(TransactionScope tx2 = ctx.transactionManager.openTransaction()){ //insert conflicting data ctx.instance.insertRow("1", new Element("other").setText("other text data")); //swap the transaction back ((FakeThreadContextTransactionManager)ctx.transactionManager).setContextId(0L); //should be OK tx.commit(); try{ //ACT tx2.commit(); fail("should have thrown WriteConflictException"); }catch(WriteConflictException ex){ //expected } } } } //</editor-fold> //<editor-fold desc="transaction durability"> @Test public void testCommit_NoSimultaneousTasks_DataIsCommittedImmediately() throws Exception { System.out.println("testCommit_NoSimultaneousTasks_DataIsCommittedImmediately"); TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); //shutdown any running tasks ctx.executorService.shutdown(); ctx.executorService.awaitTermination(5, TimeUnit.SECONDS); //don't start up any new tasks, but don't throw an error if an attempt is made either. ctx.executorService = mock(ScheduledExecutorService.class); ctx.instance.setExecutorService(ctx.executorService); try(TransactionScope tx = ctx.transactionManager.openTransaction()){ ctx.instance.insertRow("0", new Element("data").setText("some text data")); ctx.instance.insertRow("1", new Element("second").setText("second text data")); tx.commit(); } //force it to immediately release all resources ctx.instance.forceSpinDown(); //setup a new engine ctx.executorService = new ScheduledThreadPoolExecutor(2); ctx.instance = setupEngine(ctx); spinUp(ctx); Element row = ctx.instance.readRow("0"); Element row2 = ctx.instance.readRow("1"); spinDown(ctx); assertNotNull("Should have committed first row", row); assertEquals("Should have committed first row", "data", row.getName()); assertEquals("Should have committed first row", "some text data", row.getText()); assertNotNull("Should have committed second row", row2); assertEquals("Should have committed second row", "second", row2.getName()); assertEquals("Should have committed second row", "second text data", row2.getText()); } @Test public void testCommit_EngineHasCommittedData_RevertsCommittedData() throws Exception { System.out.println("testCommit_SecondEngineThrowsError_RecoversOnRestart"); final TestContext ctx = getContext(); prepFileContents(ctx, null); spinUp(ctx); EngineBase eng2 = new EngineBase("bad_engine"){ @Override public int hashCode(){ //set up to return same hash code so that HashSet returns them in order. return ctx.instance.hashCode(); } @Override public void commit(Transaction tx, TransactionOptions options){ //set up the second engine to throw an exception on commit throw new RuntimeException("Test"); } //<editor-fold desc="don't care"> @Override protected boolean spinUp() { throw new UnsupportedOperationException("Not supported yet."); } @Override protected boolean beginOperations() { throw new UnsupportedOperationException("Not supported yet."); } @Override protected boolean spinDown(SpinDownEventHandler completionEventHandler) { throw new UnsupportedOperationException("Not supported yet."); } @Override protected boolean forceSpinDown() { throw new UnsupportedOperationException("Not supported yet."); } @Override protected boolean hasUncomittedData() { throw new UnsupportedOperationException("Not supported yet."); } @Override public void insertRow(String id, Element data) throws DuplicateKeyException { throw new UnsupportedOperationException("Not supported yet."); } @Override public Element readRow(String id) { throw new UnsupportedOperationException("Not supported yet."); } @Override public Cursor<Element> queryTable(XPathQuery query) { throw new UnsupportedOperationException("Not supported yet."); } @Override public Element replaceRow(String id, Element data) throws KeyNotFoundException { throw new UnsupportedOperationException("Not supported yet."); } @Override public boolean update(String id, XPathUpdate update) throws KeyNotFoundException { throw new UnsupportedOperationException("Not supported yet."); } @Override public int update(XPathQuery query, XPathUpdate update) { throw new UnsupportedOperationException("Not supported yet."); } @Override public boolean upsertRow(String id, Element data) { throw new UnsupportedOperationException("Not supported yet."); } @Override public void deleteRow(String id) throws KeyNotFoundException { throw new UnsupportedOperationException("Not supported yet."); } @Override public int deleteAll(XPathQuery query) { throw new UnsupportedOperationException("Not supported yet."); } //</editor-fold> }; Set<EngineBase> s = new HashSet<>(); s.add(eng2); s.add(ctx.instance); Iterator<EngineBase> it = s.iterator(); assertEquals("HashSet doesn't work as i thought", ctx.instance, it.next()); assertEquals("HashSet doesn't work as i thought", eng2, it.next()); try(TransactionScope tx = ctx.transactionManager.openTransaction()){ ((FakeThreadContextTransactionManager)ctx.transactionManager).bindEngineToCurrentTransaction(eng2); ctx.instance.insertRow("0", new Element("data").setText("some text data")); ctx.instance.insertRow("1", new Element("second").setText("second text data")); try{ tx.commit(); fail("Expected TransactionException"); }catch(TransactionException ex){ //expected } } assertNull("Should have reverted data after partial commit", ctx.instance.readRow("0")); assertNull("Should have reverted data after partial commit", ctx.instance.readRow("1")); } /** * Test that the data persisted to disk is full enough that a new engine can revert * a committed transaction. This tests durability of transactions. * @throws Exception */ @Test public void testCommit_EngineShutDown_NewEngineCanRevertCommittedData() throws Exception { System.out.println("testCommit_EngineShutDown_NewEngineCanRevertCommittedData"); final TestContext ctx = getContext(); EngineTransactionManager txManager = mock(EngineTransactionManager.class); ctx.transactionManager = txManager; ctx.instance.setTransactionManager(txManager); final AtomicLong txIds = new AtomicLong(37); when(txManager.transactionlessCommitId()) .then(new Answer<Long>(){ @Override public Long answer(InvocationOnMock invocation) throws Throwable { return txIds.incrementAndGet(); } }); prepFileContents(ctx, null); spinUp(ctx); ctx.instance.insertRow("0", new Element("data").setText("some text data")); ctx.instance.insertRow("1", new Element("second").setText("second text data")); //now we open a transaction - should not need transactionless ID anymore when(txManager.transactionlessCommitId()) .thenThrow(Exception.class); Transaction tx = mock(Transaction.class); //assign TX and commit IDs long txId = txIds.incrementAndGet(); when(tx.getTransactionId()) .thenReturn(txId); long commitId = txIds.incrementAndGet(); when(tx.getCommitId()) .thenReturn(commitId); when(txManager.getTransaction()) .thenReturn(tx); when(txManager.isTransactionCommitted(txId)) .thenReturn(-1L); when(txManager.anyOpenTransactions()) .thenReturn(true); when(txManager.getLowestOpenTransaction()) .thenReturn(txId); //insert the transactional data ctx.instance.insertRow("2", new Element("transactional3").setText("tx text 3")); ctx.instance.update("1", XPathUpdate.set(XPathFactory.instance().compile("second"), "tx updated text")); ctx.instance.deleteRow("0"); //ACT when(txManager.isCommitInProgress(tx.getCommitId())) .thenReturn(true); ctx.instance.commit(tx, TransactionOptions.DEFAULT); //pretend the process is failing, but let it do it's normal tasks (we're still in the process of committing anyways). ctx.executorService.shutdown(); ctx.executorService.awaitTermination(5, TimeUnit.SECONDS); ctx.instance.forceSpinDown(); System.out.println("partially committed data"); System.out.println(dumpDoc(getFileContents(ctx))); //spin up a new engine instance and ask it to revert the old transaction ctx.executorService = new ScheduledThreadPoolExecutor(2); //ctx.executorService = mock(ScheduledExecutorService.class); //uncomment this for debugging ctx.transactionManager = new FakeThreadContextTransactionManager(new FakeDocumentFileWrapper(ctx.transactionJournal)); ctx.instance = setupEngine(ctx); ctx.spinDownInvoked.set(false); ctx.instance.spinUp(); //wait a sec cause it might take us a while to actually end up reverting when we spin up Thread.sleep(1000); ctx.instance.revert(tx.getTransactionId(), true); //now that weve recovered we can begin operations ctx.instance.beginOperations(); //ASSERT Element row = ctx.instance.readRow("0"); assertNotNull("Original data should exist", row); assertThat("Should have original data", row, isNamed("data")); assertThat("Should have original data", row, hasText("some text data")); row = ctx.instance.readRow("1"); assertNotNull("Original data should exist", row); assertThat("Should have original data", row, isNamed("second")); assertThat("Should have original data", row, hasText("second text data")); row = ctx.instance.readRow("2"); assertNull("Should not have kept transactionally inserted data", row); } //</editor-fold> //</editor-fold> private Matcher<Element> hasChildThat(final String childName, final Matcher<Element> matcher){ return new TypeSafeMatcher<Element>(){ @Override protected boolean matchesSafely(Element item) { List<Element> children = item.getChildren(childName); for(Element e : children){ if(matcher.matches(e)) return true; } return false; } @Override public void describeTo(Description description) { description.appendText("has child named ") .appendText(childName) .appendText(" that ") .appendDescriptionOf(matcher); } }; } private Matcher<Element> hasChildText(final String childName, final String text){ return new TypeSafeMatcher<Element>(){ @Override protected boolean matchesSafely(Element item) { Element child = item.getChild(childName); if(child == null) return false; return text.equals(child.getText()); } @Override public void describeTo(Description description) { description.appendText("has a child named ") .appendText(childName) .appendText(" containing text ") .appendText(text); } }; } private Matcher<Element> isNamed(final String name){ return new TypeSafeMatcher<Element>(){ @Override protected boolean matchesSafely(Element item) { return item.getName().equals(name); } @Override public void describeTo(Description description) { description.appendText("is named ") .appendText(name); } }; } private Matcher<Element> hasText(final String text){ return new TypeSafeMatcher<Element>(){ @Override protected boolean matchesSafely(Element item) { return item.getText().equals(text); } @Override public void describeTo(Description description) { description.appendText("has text ") .appendText(text); } }; } private static final XMLOutputter outputter = new XMLOutputter(); /** * Dumps a document to a string for debugging purposes. * @param doc * @return The string representation of the document. */ protected String dumpDoc(Document doc){ return outputter.outputString(doc); } protected class TestContext{ public TEngine instance; public AtomicBoolean spinDownInvoked = new AtomicBoolean(false); public File workspace; public long id; public AtomicReference<Document> transactionJournal = new AtomicReference<>(new Document().setRootElement(new Element("transactionJournal"))); public EngineTransactionManager transactionManager = new FakeThreadContextTransactionManager(new FakeDocumentFileWrapper(transactionJournal)); public final Map<String, Object> additionalContext; public ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(2); public TestContext(){ this.id = Thread.currentThread().getId(); additionalContext = new HashMap<>(); } } }