/******************************************************************************* * Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek * * 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.apache.jdbm; import junit.framework.AssertionFailedError; import junit.framework.TestResult; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.AbstractMap.SimpleEntry; import java.util.*; import java.util.concurrent.locks.Lock; /** * This class contains all Unit tests for {@link BTree}. * * @author Alex Boisvert */ public class BTreeTest extends TestCaseWithTestFile { static final boolean DEBUG = false; // the number of threads to be started in the synchronization test static final int THREAD_NUMBER = 5; // the size of the content of the maps for the synchronization // test. Beware that THREAD_NUMBER * THREAD_CONTENT_COUNT < Integer.MAX_VALUE. static final int THREAD_CONTENT_SIZE = 150; // for how long should the threads run. static final int THREAD_RUNTIME = 10 * 1000; protected TestResult result_; /** * Overrides TestCase.run(TestResult), so the errors from threads * started from this thread can be added to the testresult. This is * shown in * http://www.javaworld.com/javaworld/jw-12-2000/jw-1221-junit.html * * @param result the testresult */ public void run(TestResult result) { result_ = result; super.run(result); result_ = null; } //---------------------------------------------------------------------- /** * Handles the exceptions from other threads, so they are not ignored * in the junit test result. This method must be called from every * thread's run() method, if any throwables were throws. * * @param t the throwable (either from an assertEquals, assertTrue, * fail, ... method, or an uncaught exception to be added to the test * result of the junit test. */ protected void handleThreadException(final Throwable t) { synchronized (result_) { if (t instanceof AssertionFailedError) result_.addFailure(this, (AssertionFailedError) t); else result_.addError(this, t); } } /** * Basic tests */ public void testBasics() throws IOException { DBAbstract db; BTree tree; byte[] test, test0, test1, test2, test3; byte[] value1, value2; test = "test".getBytes(); test0 = "test0".getBytes(); test1 = "test1".getBytes(); test2 = "test2".getBytes(); test3 = "test3".getBytes(); value1 = "value1".getBytes(); value2 = "value2".getBytes(); if (DEBUG) { System.out.println("BTreeTest.testBasics"); } db = newDBCache(); tree = BTree.createInstance(db, new ByteArrayComparator(),null,null,true); tree.insert(test1, value1, false); tree.insert(test2, value2, false); byte[] result; result = (byte[]) tree.get(test0); if (result != null) { throw new Error("Test0 shouldn't be found"); } result = (byte[]) tree.get(test1); if (result == null || ByteArrayComparator.compareByteArray(result, value1) != 0) { throw new Error("Invalid value for test1: " + result); } result = (byte[]) tree.get(test2); if (result == null || ByteArrayComparator.compareByteArray(result, value2) != 0) { throw new Error("Invalid value for test2: " + result); } result = (byte[]) tree.get(test3); if (result != null) { throw new Error("Test3 shouldn't be found"); } db.close(); } /** * Basic tests, just use the simple test possibilities of junit (cdaller) */ public void testBasics2() throws IOException { DBAbstract db; BTree tree; byte[] test, test0, test1, test2, test3; byte[] value1, value2; test = "test".getBytes(); test0 = "test0".getBytes(); test1 = "test1".getBytes(); test2 = "test2".getBytes(); test3 = "test3".getBytes(); value1 = "value1".getBytes(); value2 = "value2".getBytes(); if (DEBUG) System.out.println("BTreeTest.testBasics2"); db = newDBCache(); tree = BTree.createInstance(db, new ByteArrayComparator(),null,null,true); tree.insert(test1, value1, false); tree.insert(test2, value2, false); assertEquals(null, tree.get(test0)); assertEquals(0, ByteArrayComparator.compareByteArray(value1, (byte[]) tree.get(test1))); assertEquals(0, ByteArrayComparator.compareByteArray(value2, (byte[]) tree.get(test2))); assertEquals(null, (byte[]) tree.get(test3)); db.close(); } /** * Test what happens after the dbager has been closed but the * btree is accessed. WHAT SHOULD HAPPEN??????????? * (cdaller) */ public void testClose() throws IOException { DBAbstract db; BTree tree; byte[] test, test0, test1, test2, test3; byte[] value1, value2; test = "test".getBytes(); test0 = "test0".getBytes(); test1 = "test1".getBytes(); test2 = "test2".getBytes(); test3 = "test3".getBytes(); value1 = "value1".getBytes(); value2 = "value2".getBytes(); if (DEBUG) System.out.println("BTreeTest.testClose"); db = newDBCache(); tree = BTree.createInstance(db, new ByteArrayComparator(),null,null,true); tree.insert(test1, value1, false); tree.insert(test2, value2, false); assertEquals(null, tree.get(test0)); assertEquals(0, ByteArrayComparator.compareByteArray(value1, (byte[]) tree.get(test1))); assertEquals(0, ByteArrayComparator.compareByteArray(value2, (byte[]) tree.get(test2))); assertEquals(null, (byte[]) tree.get(test3)); db.close(); try { tree.browse(); fail("Should throw an IllegalStateException on access on not opened btree"); } catch (IllegalStateException except) { // ignore } try { tree.get(test0); fail("Should throw an IllegalStateException on access on not opened btree"); } catch (IllegalStateException except) { // ignore } try { tree.findGreaterOrEqual(test0); fail("Should throw an IllegalStateException on access on not opened btree"); } catch (IllegalStateException except) { // ignore } try { tree.insert(test2, value2, false); fail("Should throw an IllegalStateException on access on not opened btree"); } catch (IllegalStateException except) { // ignore } try { tree.remove(test0); fail("Should throw an IllegalStateException on access on not opened btree"); } catch (IllegalStateException except) { // ignore } /* try { tree.size(); fail( "Should throw an IllegalStateException on access on not opened btree" ); } catch( IllegalStateException except ) { // ignore } */ } /** * Test to insert different objects into one btree. (cdaller) */ public void testInsert() throws IOException { DBAbstract db; BTree tree; if (DEBUG) System.out.println("BTreeTest.testInsert"); db = newDBCache(); tree = BTree.createInstance(db); // insert differnt objects and retrieve them tree.insert("test1", "value1", false); tree.insert("test2", "value2", false); tree.insert("one", new Integer(1), false); tree.insert("two", new Long(2), false); tree.insert("myownobject", new ObjectTT(new Integer(234)), false); assertEquals("value2", (String) tree.get("test2")); assertEquals("value1", (String) tree.get("test1")); assertEquals(new Integer(1), (Integer) tree.get("one")); assertEquals(new Long(2), (Long) tree.get("two")); // what happens here? must not be replaced, does it return anything? // probably yes! assertEquals("value1", tree.insert("test1", "value11", false)); assertEquals("value1", tree.get("test1")); // still the old value? assertEquals("value1", tree.insert("test1", "value11", true)); assertEquals("value11", tree.get("test1")); // now the new value! ObjectTT expected_obj = new ObjectTT(new Integer(234)); ObjectTT btree_obj = (ObjectTT) tree.get("myownobject"); assertEquals(expected_obj, btree_obj); db.close(); } /** * Test to remove objects from the btree. (cdaller) */ public void testRemove() throws IOException { DBAbstract db; BTree tree; if (DEBUG) { System.out.println("BTreeTest.testRemove"); } db = newDBCache(); tree = BTree.createInstance(db); tree.insert("test1", "value1", false); tree.insert("test2", "value2", false); assertEquals("value1", (String) tree.get("test1")); assertEquals("value2", (String) tree.get("test2")); tree.remove("test1"); assertEquals(null, (String) tree.get("test1")); assertEquals("value2", (String) tree.get("test2")); tree.remove("test2"); assertEquals(null, (String) tree.get("test2")); int iterations = 1000; for (int count = 0; count < iterations; count++) { tree.insert("num" + count, new Integer(count), false); } assertEquals(iterations, tree._entries); for (int count = 0; count < iterations; count++) { assertEquals(new Integer(count), tree.get("num" + count)); } for (int count = 0; count < iterations; count++) { tree.remove("num" + count); } assertEquals(0, tree._entries); db.close(); } /** * Test to get differents objects in the btree. (cdaller) */ public void testFind() throws IOException { DBAbstract db; BTree tree; if (DEBUG) System.out.println("BTreeTest.testFind"); db = newDBCache(); tree = BTree.createInstance(db); tree.insert("test1", "value1", false); tree.insert("test2", "value2", false); Object value = tree.get("test1"); assertTrue(value instanceof String); assertEquals("value1", value); tree.insert("", "Empty String as key", false); assertEquals("Empty String as key", (String) tree.get("")); assertEquals(null, (String) tree.get("someoneelse")); db.close(); } /** * Test deletion of btree from record manager. (kday) * <p/> * After deletion, the BTree and all of it's BTreeNode children (and their children) * should be removed from the recordmanager. */ public void testDelete() throws IOException { if (DEBUG) System.out.println("BTreeTest.testFind"); DBAbstract db = newDBCache(); BTree<String, Serializable> tree = BTree.createInstance(db); // put enough data into the tree so we definitely have multiple nodes for (int count = 1; count <= 1000; count++) { tree.insert("num" + count, new Integer(count), false); if (count % 100 == 0) db.commit(); } List<Long> out = new ArrayList<Long>(); tree.dumpChildNodeRecIDs(out); assertTrue(out.size() > 0); } /** * Test to insert, retrieve and remove a large amount of data. (cdaller) */ public void testLargeDataAmount() throws IOException { DBAbstract db; BTree tree; if (DEBUG) System.out.println("BTreeTest.testLargeDataAmount"); db = newDBCache(); // db = new jdbm.db.BaseRecordManager( "test" ); tree = BTree.createInstance(db); // tree.setSplitPoint( 4 ); int iterations = 10000; // insert data for (int count = 0; count < iterations; count++) { try { assertEquals(null, tree.insert("num" + count, new Integer(count), false)); } catch (IOException except) { except.printStackTrace(); throw except; } } // get data for (int count = 0; count < iterations; count++) { assertEquals(new Integer(count), tree.get("num" + count)); } // delete data for (int count = 0; count < iterations; count++) { assertEquals(new Integer(count), tree.remove("num" + count)); } assertEquals(0, tree._entries); db.close(); } public void testRecordListener() throws IOException { DBAbstract db = newDBCache(); BTree<Integer, String> tree = BTree.createInstance(db); final List<SimpleEntry<Integer, String>> dels = new ArrayList(); final List<SimpleEntry<Integer, String>> ins = new ArrayList(); final List<SimpleEntry<Integer, String>> updNew = new ArrayList(); final List<SimpleEntry<Integer, String>> updOld = new ArrayList(); tree.addRecordListener(new RecordListener<Integer, String>() { public void recordUpdated(Integer key, String oldValue, String newValue) throws IOException { updOld.add(new SimpleEntry<Integer, String>(key, oldValue)); updNew.add(new SimpleEntry<Integer, String>(key, newValue)); } public void recordRemoved(Integer key, String value) throws IOException { dels.add(new SimpleEntry<Integer, String>(key, value)); } public void recordInserted(Integer key, String value) throws IOException { ins.add(new SimpleEntry<Integer, String>(key, value)); } }); //test insert tree.insert(11, "aa11", true); tree.insert(12, "aa12", true); assertTrue(ins.contains(new SimpleEntry(11, "aa11"))); assertTrue(ins.contains(new SimpleEntry(12, "aa12"))); assertTrue(ins.size() == 2); ins.clear(); assertTrue(dels.isEmpty()); assertTrue(updNew.isEmpty()); assertTrue(updOld.isEmpty()); //test update tree.insert(12, "aa123", true); assertTrue(ins.isEmpty()); assertTrue(dels.isEmpty()); assertTrue(updOld.contains(new SimpleEntry(12, "aa12"))); assertTrue(updOld.size() == 1); updOld.clear(); assertTrue(updNew.contains(new SimpleEntry(12, "aa123"))); assertTrue(updNew.size() == 1); updNew.clear(); //test remove tree.remove(11); assertTrue(dels.contains(new SimpleEntry(11, "aa11"))); assertTrue(dels.size() == 1); dels.clear(); assertTrue(ins.isEmpty()); assertTrue(updOld.isEmpty()); assertTrue(updNew.isEmpty()); } /** * Tests the corner case of deleting all nodes from the tree. In this case, all BTreeNodes * associated with the tree should be removed from the db. * <p/> * We are also going to test to make sure the db file doesn't grow (leak) if we repeat the * process a number of times. * * @throws Exception */ public void testDeleteAllNodes() throws Exception { // we are going to run this test without object cache enabled. If it is turned on, // we will have problems with using a different deserializer for BTreeNodes than the standard // serializer. String recordManagerBasename = newTestFile(); String recordManagerDBname = recordManagerBasename + ".d.0"; long previousdbSize = 0; for (int i = 0; i < 5; i++) { DBAbstract db = (DBAbstract) DBMaker.openFile(recordManagerBasename).disableCache().make(); BTree<String, Serializable> tree = BTree.createInstance(db); String[] keys = new String[1000]; for (int count = 0; count < 1000; count++) { keys[count] = "num" + count; } // put enough data into the tree so we definitely have multiple nodes for (int count = 0; count < 1000; count++) { tree.insert(keys[count], new Integer(count), false); if (count % 100 == 0) db.commit(); } db.commit(); long currentdbSize = new File(recordManagerDBname).length(); assertTrue("file size too small " + currentdbSize, currentdbSize > 0); // now remove it all for (int count = 0; count < 1000; count++) { tree.remove(keys[count]); if (count % 100 == 0) db.commit(); } db.commit(); BTreeNode root = tree.getRoot(); assertNull(root); db.close(); currentdbSize = new File(recordManagerDBname).length(); assertTrue("file size too small " + currentdbSize, currentdbSize > 0); if (previousdbSize != 0) { assertTrue(currentdbSize == previousdbSize); } } } /** * Test access from multiple threads. Assertions only work, when the * run() method is overridden and the exceptions of the threads are * added to the resultset of the TestCase. see run() and * handleException(). */ public void testMultithreadAccess() throws IOException { DBAbstract db; BTree tree; if (DEBUG) System.out.println("BTreeTest.testMultithreadAccess"); db = newDBCache(); tree = BTree.createInstance(db); TestThread[] thread_pool = new TestThread[THREAD_NUMBER]; String name; Map content; // create content for the tree, different content for different threads! for (int thread_count = 0; thread_count < THREAD_NUMBER; thread_count++) { name = "thread" + thread_count; content = new TreeMap(); for (int content_count = 0; content_count < THREAD_CONTENT_SIZE; content_count++) { // guarantee, that keys and values do not overleap, // otherwise one thread removes some keys/values of // other threads! content.put(name + "_" + content_count, new Integer(thread_count * THREAD_CONTENT_SIZE + content_count)); } thread_pool[thread_count] = new TestThread(name, tree, content); thread_pool[thread_count].start(); } try { Thread.sleep(THREAD_RUNTIME); } catch (InterruptedException ignore) { ignore.printStackTrace(); } // stop threads: for (int thread_count = 0; thread_count < THREAD_NUMBER; thread_count++) { if (DEBUG) System.out.println("Stop threads"); thread_pool[thread_count].setStop(); } // wait until the threads really stop: try { for (int thread_count = 0; thread_count < THREAD_NUMBER; thread_count++) { if (DEBUG) System.out.println("Join thread " + thread_count); thread_pool[thread_count].join(); if (DEBUG) System.out.println("Joined thread " + thread_count); } } catch (InterruptedException ignore) { ignore.printStackTrace(); } db.close(); } /** * Helper method to 'simulate' the methods of an entry set of the btree. */ protected static boolean containsKey(Object key, BTree btree) throws IOException { return (btree.get(key) != null); } /** * Helper method to 'simulate' the methods of an entry set of the btree. */ protected static boolean containsValue(Object value, BTree btree) throws IOException { // we must synchronize on the BTree while browsing Lock readLock = btree.getLock().readLock(); try { readLock.lock(); BTree.BTreeTupleBrowser browser = btree.browse(); BTree.BTreeTuple tuple = new BTree.BTreeTuple(); while (browser.getNext(tuple)) { if (tuple.value.equals(value)) return (true); } } finally { readLock.unlock(); } // System.out.println("Comparation of '"+value+"' with '"+ tuple.getValue()+"' FAILED"); return (false); } /** * Helper method to 'simulate' the methods of an entry set of the btree. */ protected static boolean contains(Map.Entry entry, BTree btree) throws IOException { Object tree_obj = btree.get(entry.getKey()); if (tree_obj == null) { // can't distuingish, if value is null or not found!!!!!! return (entry.getValue() == null); } return (tree_obj.equals(entry.getValue())); } /** * Inner class for testing puroposes only (multithreaded access) */ class TestThread extends Thread { Map _content; BTree _btree; volatile boolean _continue = true; int THREAD_SLEEP_TIME = 50; // in ms String _name; TestThread(String name, BTree btree, Map content) { _content = content; _btree = btree; _name = name; } public void setStop() { _continue = false; } private void action() throws IOException { Iterator iterator = _content.entrySet().iterator(); Map.Entry entry; if (DEBUG) { System.out.println("Thread " + _name + ": fill btree."); } while (iterator.hasNext()) { entry = (Map.Entry) iterator.next(); assertEquals(null, _btree.insert(entry.getKey(), entry.getValue(), false)); } // as other threads are filling the btree as well, the size // of the btree is unknown (but must be at least the size of // the content map) assertTrue(_content.size() <= _btree._entries); iterator = _content.entrySet().iterator(); if (DEBUG) { System.out.println("Thread " + _name + ": iterates btree."); } while (iterator.hasNext()) { entry = (Map.Entry) iterator.next(); assertEquals(entry.getValue(), _btree.get(entry.getKey())); assertTrue(contains(entry, _btree)); assertTrue(containsKey(entry.getKey(), _btree)); assertTrue(containsValue(entry.getValue(), _btree)); } iterator = _content.entrySet().iterator(); Object key; if (DEBUG) { System.out.println("Thread " + _name + ": removes his elements from the btree."); } while (iterator.hasNext()) { key = ((Map.Entry) iterator.next()).getKey(); _btree.remove(key); assertTrue(!containsKey(key, _btree)); } } public void run() { if (DEBUG) System.out.println("Thread " + _name + ": started."); try { while (_continue) { action(); try { Thread.sleep(THREAD_SLEEP_TIME); } catch (InterruptedException except) { except.printStackTrace(); } } } catch (Throwable t) { if (DEBUG) { System.err.println("Thread " + _name + " threw an exception:"); t.printStackTrace(); } handleThreadException(t); } if (DEBUG) System.out.println("Thread " + _name + ": stopped."); } } // end of class TestThread static class ObjectTT implements Serializable { Object _content; private ObjectTT() { // empty } public ObjectTT(Object content) { _content = content; } Object getContent() { return _content; } public boolean equals(Object obj) { if (!(obj instanceof ObjectTT)) { return false; } return _content.equals(((ObjectTT) obj).getContent()); } public String toString() { return ("ObjectTT {content='" + _content + "'}"); } } // ObjectTT public void testIssue2(){ //this causes stack overflow // https://github.com/jankotek/JDBM3/issues/2 DB build = DBMaker.openFile(newTestFile()).setMRUCacheSize(100).make(); Map<String, String> treeMap = build.createTreeMap("treeMap"); for (int i = 0; i < 100000; i++) { treeMap.put(i + "asdddfffffffffffffffffffdgf" + i + "sddfdfsfddddddddddddddddd" + i, "dsfgfg.dfcdfsgfgfffffffffffffffffdddddddddd"); if (i % 10000 == 0) { build.commit(); } } build.commit(); build.close(); } }