/**

 * JDBM LICENSE v1.00

 *

 * Redistribution and use of this software and associated documentation

 * ("Software"), with or without modification, are permitted provided

 * that the following conditions are met:

 *

 * 1. Redistributions of source code must retain copyright

 *    statements and notices.  Redistributions must also contain a

 *    copy of this document.

 *

 * 2. Redistributions in binary form must reproduce the

 *    above copyright notice, this list of conditions and the

 *    following disclaimer in the documentation and/or other

 *    materials provided with the distribution.

 *

 * 3. The name "JDBM" must not be used to endorse or promote

 *    products derived from this Software without prior written

 *    permission of Cees de Groot.  For written permission,

 *    please contact cg@cdegroot.com.

 *

 * 4. Products derived from this Software may not be called "JDBM"

 *    nor may "JDBM" appear in their names without prior written

 *    permission of Cees de Groot.

 *

 * 5. Due credit should be given to the JDBM Project

 *    (http://jdbm.sourceforge.net/).

 *

 * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS

 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT

 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND

 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL

 * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,

 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR

 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)

 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,

 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)

 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED

 * OF THE POSSIBILITY OF SUCH DAMAGE.

 *

 * Copyright 2000 (C) Cees de Groot. All Rights Reserved.

 * Contributions are Copyright (C) 2000 by their associated contributors.

 *

 */



package jdbm.btree;



import jdbm.RecordManager;

import jdbm.RecordManagerFactory;

import jdbm.recman.TestRecordFile;

import jdbm.helper.ByteArrayComparator;

import jdbm.helper.StringComparator;

import jdbm.helper.TupleBrowser;

import jdbm.helper.Tuple;



import junit.framework.*;



import java.io.IOException;

import java.io.Serializable;



import java.util.Map;

import java.util.TreeMap;

import java.util.Iterator;



/**

 *  This class contains all Unit tests for {@link BTree}.

 *

 *  @author <a href="mailto:boisvert@exoffice.com">Alex Boisvert</a>

 *  @version $Id: TestBTree.java,v 1.8 2003/09/21 15:49:02 boisvert Exp $

 */

public class TestBTree

    extends TestCase

{



      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_;



    public TestBTree( String name )

    {

        super( name );

    }



    public void setUp()

    {

        TestRecordFile.deleteTestFile();

    }



    public void tearDown()

    {

        TestRecordFile.deleteTestFile();

    }



    /**

     * 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 {

        RecordManager  recman;

        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("TestBTree.testBasics");

        }



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new ByteArrayComparator() );



        tree.insert( test1, value1, false );

        tree.insert( test2, value2, false );



        byte[] result;

        result = (byte[]) tree.find( test0 );

        if ( result != null ) {

            throw new Error( "Test0 shouldn't be found" );

        }



        result = (byte[]) tree.find( test1 );

        if ( result == null || ByteArrayComparator.compareByteArray( result, value1 ) != 0 ) {

            throw new Error( "Invalid value for test1: " + result );

        }



        result = (byte[]) tree.find( test2 );

        if ( result == null || ByteArrayComparator.compareByteArray( result, value2 ) != 0 ) {

            throw new Error( "Invalid value for test2: " + result );

        }



        result = (byte[]) tree.find( test3 );

        if ( result != null ) {

            throw new Error( "Test3 shouldn't be found" );

        }



        recman.close();

    }



    /**

     *  Basic tests, just use the simple test possibilities of junit (cdaller)

     */

    public void testBasics2() throws IOException {

        RecordManager  recman;

        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("TestBTree.testBasics2");



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new ByteArrayComparator() );



        tree.insert( test1, value1, false);

        tree.insert( test2, value2, false);



        assertEquals( null, tree.find( test0 ) );

        assertEquals( 0, ByteArrayComparator.compareByteArray( value1, (byte[]) tree.find( test1 ) ) );

        assertEquals( 0, ByteArrayComparator.compareByteArray( value2, (byte[]) tree.find( test2 ) ) );

        assertEquals( null, (byte[]) tree.find( test3 ) );



        recman.close();

     }





    /**

     *  Test what happens after the recmanager has been closed but the

     *  btree is accessed. WHAT SHOULD HAPPEN???????????

     * (cdaller)

     */

    public void testClose()

        throws IOException

    {

        RecordManager  recman;

        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("TestBTree.testClose");



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new ByteArrayComparator() );



        tree.insert( test1, value1, false );

        tree.insert( test2, value2, false );



        assertEquals( null, tree.find( test0 ) );

        assertEquals( 0, ByteArrayComparator.compareByteArray( value1, (byte[]) tree.find( test1 ) ) );

        assertEquals( 0, ByteArrayComparator.compareByteArray( value2, (byte[]) tree.find( test2 ) ) );

        assertEquals( null, (byte[]) tree.find( test3 ) );



        recman.close();



        try {

            tree.browse();

            fail("Should throw an IllegalStateException on access on not opened btree");

        } catch( IllegalStateException except ) {

            // ignore

        }



        try {

            tree.find( 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

    {

        RecordManager  recman;

        BTree          tree;



        if ( DEBUG )

            System.out.println("TestBTree.testInsert");



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new StringComparator() );



        // 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 TestObject(new Integer(234)),false);



        assertEquals("value2",(String)tree.find("test2"));

        assertEquals("value1",(String)tree.find("test1"));

        assertEquals(new Integer(1),(Integer)tree.find("one"));

        assertEquals(new Long(2),(Long)tree.find("two"));



        // what happens here? must not be replaced, does it return anything?

        // probably yes!

        assertEquals("value1",tree.insert("test1","value11",false));

        assertEquals("value1",tree.find("test1")); // still the old value?

        assertEquals("value1",tree.insert("test1","value11",true));

        assertEquals("value11",tree.find("test1")); // now the new value!



        TestObject expected_obj = new TestObject(new Integer(234));

        TestObject btree_obj = (TestObject)tree.find("myownobject");

        assertEquals(expected_obj, btree_obj);



        recman.close();

    }





    /**

     *  Test to remove  objects from the btree. (cdaller)

     */

    public void testRemove()

        throws IOException

    {

        RecordManager  recman;

        BTree          tree;



        if ( DEBUG ) {

            System.out.println( "TestBTree.testRemove" );

        }



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new StringComparator() );



        tree.insert("test1", "value1",false);

        tree.insert("test2","value2",false);

        assertEquals("value1",(String)tree.find("test1"));

        assertEquals("value2",(String)tree.find("test2"));

        tree.remove("test1");

        assertEquals(null,(String)tree.find("test1"));

        assertEquals("value2",(String)tree.find("test2"));

        tree.remove("test2");

        assertEquals(null,(String)tree.find("test2"));



        int iterations = 1000;



        for ( int count = 0; count < iterations; count++ ) {

            tree.insert( "num"+count, new Integer( count ), false );

        }



        assertEquals( iterations, tree.size() );



        for ( int count = 0; count < iterations; count++ ) {

            assertEquals( new Integer( count ), tree.find( "num" + count ) );

        }



        for ( int count = 0; count < iterations; count++ ) {

           tree.remove( "num" + count );

        }



        assertEquals( 0, tree.size() );



        recman.close();

    }



    /**

     *  Test to find differents objects in the btree. (cdaller)

     */

    public void testFind()

        throws IOException

    {

        RecordManager  recman;

        BTree          tree;



        if ( DEBUG )

            System.out.println("TestBTree.testFind");



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new StringComparator() );



        tree.insert("test1", "value1",false);

        tree.insert("test2","value2",false);



        Object value = tree.find("test1");

        assertTrue(value instanceof String);

        assertEquals("value1",value);



        tree.insert("","Empty String as key",false);

        assertEquals("Empty String as key",(String)tree.find(""));



        assertEquals(null,(String)tree.find("someoneelse"));



        recman.close();

    }





    /**

     *  Test to insert, retrieve and remove a large amount of data. (cdaller)

     */

    public void testLargeDataAmount()

        throws IOException

    {



        RecordManager  recman;

        BTree          tree;



        if ( DEBUG )

            System.out.println("TestBTree.testLargeDataAmount");



        recman = RecordManagerFactory.createRecordManager( "test" );

        // recman = new jdbm.recman.BaseRecordManager( "test" );

        

        tree = BTree.createInstance( recman, new StringComparator() );

        // 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;

           }

        }



           // find data

         for(int count = 0; count < iterations; count++)

         {

           assertEquals(new Integer(count), tree.find("num"+count));

         }



             // delete data

         for(int count = 0; count < iterations; count++)

         {

           assertEquals(new Integer(count),tree.remove("num"+count));

         }



         assertEquals(0,tree.size());



         recman.close();

   }



/**

 * 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

  {

        RecordManager  recman;

        BTree          tree;



        if ( DEBUG )

            System.out.println("TestBTree.testMultithreadAccess");



        recman = RecordManagerFactory.createRecordManager( "test" );

        tree = BTree.createInstance( recman, new StringComparator() );



        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();

        }

        recman.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.find( 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

    synchronized ( btree ) {

        TupleBrowser browser = btree.browse();

        Tuple tuple = new Tuple();

        while(browser.getNext(tuple)) {

          if(tuple.getValue().equals(value))

            return(true);

        }

    }

    //    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.find(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() ) );

    }



    /**

     *  Runs all tests in this class

     */

    public static void main( String[] args ) {

        junit.textui.TestRunner.run( new TestSuite( TestBTree.class ) );

    }



    /**

     * 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.size() );



            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.find( 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

}





/**

 * class for testing puroposes only (store as value in btree) not

 * implemented as inner class, as this prevents Serialization if

 * outer class is not Serializable.

 */

class TestObject

    implements Serializable

{



    Object _content;



    private TestObject()

    {

        // empty

    }





    public TestObject( Object content )

    {

        _content = content;

    }





    Object getContent()

    {

        return _content;

    }





    public boolean equals( Object obj )

    {

        if ( ! ( obj instanceof TestObject ) ) {

            return false;

        }

        return _content.equals( ( (TestObject) obj ).getContent() );

    }



    public String toString()

    {

        return( "TestObject {content='" + _content + "'}" );

    }



} // TestObject



