/*
 *
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

package org.jboss.cache.transaction;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.*;
import org.jboss.cache.lock.IdentityLock;
import org.jboss.cache.lock.IsolationLevel;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.transaction.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * Tests transactional access to a local TreeCache.
 * Note: we use DummpyTranasctionManager to replace jta
 *
 * @version $Id: TransactionTest.java,v 1.9 2006/02/09 16:01:45 bela Exp $
 */
public class TransactionTest extends TestCase {
   TreeCache cache=null;
   UserTransaction tx=null;
   Properties p=null;
   String old_factory=null;
   final String FACTORY="org.jboss.cache.transaction.DummyContextFactory";
   Exception thread_ex;


   public TransactionTest(String name) {
      super(name);
   }

   public void setUp() throws Exception {
      super.setUp();
      old_factory=System.getProperty(Context.INITIAL_CONTEXT_FACTORY);
      System.setProperty(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
      DummyTransactionManager.getInstance();
      if(p == null) {
         p=new Properties();
         p.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.cache.transaction.DummyContextFactory");
      }

      tx=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
      cache=new TreeCache("test", null, 10000);
      cache.setTransactionManagerLookupClass("org.jboss.cache.DummyTransactionManagerLookup");
      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
      cache.createService();
      cache.startService();
      thread_ex=null;
   }

   public void tearDown() throws Exception {
      super.tearDown();
      if(cache != null)
         cache.stopService();

      // BW. kind of a hack to destroy jndi binding and thread local tx before next run.
      DummyTransactionManager.destroy();
      if(old_factory != null) {
         System.setProperty(Context.INITIAL_CONTEXT_FACTORY, old_factory);
         old_factory=null;
      }

      if(tx != null) {
         try {
            tx.rollback();
         }
         catch(Throwable t) {
         }
         tx=null;
      }
   }


   public void testPutTx() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         // the tx interceptor should know that we're in the same tx.
         assertEquals(new Integer(38), cache.get("/a/b/c", "age"));

         cache.put("/a/b/c", "age", new Integer(39));
         tx.commit();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertEquals(new Integer(39), cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         fail(t.toString());
      }
   }


   public void testRollbackTx1() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         cache.put("/a/b/c", "age", new Integer(39));
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRollbackTx2() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         cache.remove("/a/b/c", "age");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRollbackTx2a() {
      try {
          System.out.println("locks " + cache.printLockInfo());
         cache.put("/a/b/c", "age", new Integer(38));
          System.out.println("locks " + cache.printLockInfo());
         tx.begin();
         cache.remove("/a/b/c", "age");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertEquals(new Integer(38), cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRollbackTx3() {
      try {
         java.util.Map map1=new java.util.HashMap();
         map1.put("age", new Integer(38));
         java.util.Map map2=new java.util.HashMap();
         map2.put("age", new Integer(39));
         tx.begin();
         cache.put("/a/b/c", map1);
         cache.put("/a/b/c", map2);
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRollbackTx4() {
      try {
         Map map=new HashMap();
         map.put("age", new Integer(38));
         tx.begin();
         cache.put("/a/b/c", map);
         cache.remove("/a/b/c");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeCreationRollback() {
      try {
         tx.begin();
         System.out.println("initial state:\n" + cache);
         cache.put("/bela/ban", null);
         System.out.println("after put():\n" + cache);
         tx.rollback();
         System.out.println("after rollback():\n" + cache);

         assertNull("node should be not existent", cache.get("/bela/ban"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeCreationRollback2() {
      try {
         cache.put("/bela/ban", null);
         tx.begin();
         cache.put("/bela/ban/michelle", null);
         tx.rollback();
         assertNotNull("node should be not null", cache.get("/bela/ban"));
         assertNull("node should be not existent", cache.get("/bela/ban/michelle"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeDeletionRollback() {
      try {
         cache.put("/a/b/c", null);
         tx.begin();
         cache.remove("/a/b/c");
         assertNull(cache.get("/a/b/c"));
         cache.remove("/a/b");
         assertNull(cache.get("/a/b"));
         cache.remove("/a");
         assertNull(cache.get("/a"));
         tx.rollback();
         assertNotNull(cache.get("/a/b/c"));
         assertNotNull(cache.get("/a/b"));
         assertNotNull(cache.get("/a"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeDeletionRollback2() {
      try {
         cache.put("/a/b/c", null);
         cache.put("/a/b/c1", null);
         cache.put("/a/b/c2", null);
         tx.begin();
         cache.remove("/a");
         assertNull(cache.get("/a/b/c"));
         assertNull(cache.get("/a/b/c1"));
         assertNull(cache.get("/a/b/c2"));
         assertNull(cache.get("/a/b"));
         assertNull(cache.get("/a"));
         Set children=cache.getChildrenNames("/a/b");
         assertNull(children);
         children=cache.getChildrenNames("/a");
         assertNull(children);
         tx.rollback();
         assertNotNull(cache.get("/a"));
         assertNotNull(cache.get("/a/b"));
         assertNotNull(cache.get("/a/b/c"));
         assertNotNull(cache.get("/a/b/c1"));
         assertNotNull(cache.get("/a/b/c2"));
         children=cache.getChildrenNames("/a/b");
         assertEquals(3, children.size());
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }



   public void testNodeDeletionRollback3() {
      GlobalTransaction gtx;
      try {
         tx.begin();
         gtx=cache.getCurrentTransaction();
         cache.put("/a/b/c1", null);
         checkLock(gtx, "/a", false);
         checkLock(gtx, "/a/b", false);
         checkLock(gtx, "/a/b/c1", true);

         cache.put("/a/b/c2", null);
         checkLock(gtx, "/a/b/c2", true);

         cache.put("/a/b/c3", null);
         cache.put("/a/b/c1/one", null);
         checkLock(gtx, "/a/b/c1", true);
         checkLock(gtx, "/a/b/c1/one", true);

         cache.put("/a/b/c1/two", null);
         cache.put("/a/b/c1/one/1", null);
         checkLock(gtx, "/a/b/c1", true);
         checkLock(gtx, "/a/b/c1/one", true);
         checkLock(gtx, "/a/b/c1/one/1", true);

         cache.put("/a/b/c1/two/2/3/4", null);
         checkLock(gtx, "/a/b/c1", true);
         checkLock(gtx, "/a/b/c1/two", true);
         checkLock(gtx, "/a/b/c1/two/2", false);
         checkLock(gtx, "/a/b/c1/two/2/3", false);
         checkLock(gtx, "/a/b/c1/two/2/3/4", true);

         System.out.println("locks: " + cache.printLockInfo());

         cache.remove("/a/b");
         tx.rollback();
         assertNull(cache.getChildrenNames("/a/b"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testDoubleLocks() {
      try {
         tx.begin();
         cache.put("/a/b/c", null);
         cache.put("/a/b/c", null);

         DataNode n=cache.get("/a");
         IdentityLock lock=n.getLock();
         int num=lock.getReaderOwners().size();
         assertEquals(1, num);

         n=cache.get("/a/b");
         lock=n.getLock();
         num=lock.getReaderOwners().size();
         assertEquals(1, num);
         // added this here since we had stale gtxs lurking about. - Manik, 02Jan06  
         tx.rollback();
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   private void checkLock(Object owner, String fqn, boolean write_locked) throws Exception {
      DataNode n=cache.get(fqn);
      IdentityLock lock=n.getLock();
      if(owner == null)
         owner=Thread.currentThread();
      if(lock.isLocked() == false)
         throw new Exception("node " + fqn + " is not locked");
      if(write_locked) {
         if(lock.isWriteLocked() == false)
            throw new Exception("node " + fqn + " is not write-locked");
      }
      else {
         if(lock.isReadLocked() == false)
            throw new Exception("node " + fqn + " is not read-locked");
      }
      if(lock.isOwner(owner) == false)
         throw new Exception("owner " + owner + "is not owner");
   }


   public void testRemove() throws CacheException, SystemException, NotSupportedException, HeuristicMixedException, HeuristicRollbackException, RollbackException {
      cache.put("/a/b/c", null);
      cache.put("/a/b/c/1", null);
      cache.put("/a/b/c/2", null);
      cache.put("/a/b/c/3", null);
      cache.put("/a/b/c/3/a/b/c", null);

      assertEquals(0, cache.getNumberOfLocksHeld());
      assertEquals(0, cache.getLockTable().size());

      tx.begin();
      cache.remove("/a/b/c");
      System.out.println("locks held (after removing /a/b/c): \n" + cache.printLockInfo());
      assertEquals(2, cache.getNumberOfLocksHeld());
      tx.commit();
      System.out.println("locks held (after committing /a/b/c): \n" + cache.printLockInfo());
      assertEquals(0, cache.getNumberOfLocksHeld());
   }


   public void testRemoveAndRollback() throws CacheException, SystemException, NotSupportedException, HeuristicMixedException, HeuristicRollbackException, RollbackException {
      cache.put("/a/b/c", null);
      cache.put("/a/b/c/1", null);
      cache.put("/a/b/c/2", null);
      cache.put("/a/b/c/3", null);
      cache.put("/a/b/c/3/a/b/c", null);

      assertEquals(0, cache.getNumberOfLocksHeld());
      assertEquals(0, cache.getLockTable().size());

      tx.begin();
      System.out.println("locks held (before removing /a/b/c): \n" + cache.printLockInfo());
      cache.remove("/a/b/c");
      System.out.println("locks held (after removing /a/b/c): \n" + cache.printLockInfo());
      assertEquals(2, cache.getNumberOfLocksHeld());
      tx.rollback();
      System.out.println("locks held (after rollback): \n" + cache.printLockInfo());
      assertEquals(0, cache.getNumberOfLocksHeld());

      assertTrue(cache.exists("/a/b/c"));
      assertTrue(cache.exists("/a/b/c/1"));
      assertTrue(cache.exists("/a/b/c/2"));
      assertTrue(cache.exists("/a/b/c/3"));
      assertTrue(cache.exists("/a/b/c/3/a"));
      assertTrue(cache.exists("/a/b/c/3/a/b"));
      assertTrue(cache.exists("/a/b/c/3/a/b/c"));
   }


   public void testRemoveKeyRollback() throws CacheException, SystemException, NotSupportedException {
      cache.put("/bela/ban", "name", "Bela");
      tx.begin();
      cache.remove("/bela/ban", "name");
      assertNull(cache.get("/bela/ban", "name"));
      tx.rollback();
      assertEquals("Bela", cache.get("/bela/ban", "name"));
   }


   public void testRemoveKeyRollback2() {
      try {
         Map m=new HashMap();
         m.put("name", "Bela");
         m.put("id", new Integer(322649));
         cache.put("/bela/ban", m);
         tx.begin();
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRemoveKeyRollback3() {
      try {
         cache.put("/bela/ban", "name", "Bela");
         tx.begin();
         cache.put("/bela/ban", "name", "Michelle");
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }




   public void testDoubleRemovalOfSameData() {
      try {
         tx.begin();
         cache.put("/foo/1", "item", new Integer(1));
         assertEquals(cache.get("/foo/1", "item"), new Integer(1));
         cache.remove("/foo/1");
         assertNull(cache.get("/foo/1", "item"));
         cache.remove("/foo/1");
         assertNull(cache.get("/foo/1", "item"));
         tx.rollback();
         assertFalse(cache.exists("/foo/1"));
         assertNull(cache.get("/foo/1", "item"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   /**
    * put(Fqn, Map) with a previous null map
    */
   public void testPutDataRollback1() {
      try {
         cache.put("/bela/ban", null); // create a node /bela/ban with a null map
         tx.begin();
         Map m=new HashMap();
         m.put("name", "Bela");
         m.put("id", new Integer(322649));
         cache.put("/bela/ban", m);
         tx.rollback();

         DataNode n=cache.get("/bela/ban");
         if(n.getData() == null) return;
         assertEquals("map should be empty", 0, n.getData().size());
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   /**
    * put(Fqn, Map) with a previous non-null map
    */
   public void testputDataRollback2() {
      Map m1, m2;
      m1=new HashMap();
      m1.put("name", "Bela");
      m1.put("id", new Integer(322649));
      m2=new HashMap();
      m2.put("other", "bla");
      m2.put("name", "Michelle");

      try {
         cache.put("/bela/ban", m1);
         tx.begin();

         cache.put("/bela/ban", m2);
         Map tmp=cache.get("/bela/ban").getData();
         assertEquals(3, tmp.size());
         assertEquals("Michelle", tmp.get("name"));
         assertEquals(tmp.get("id"), new Integer(322649));
         assertEquals("bla", tmp.get("other"));
         tx.rollback();

         tmp=cache.get("/bela/ban").getData();
         assertEquals(2, tmp.size());
         assertEquals("Bela", tmp.get("name"));
         assertEquals(tmp.get("id"), new Integer(322649));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testPutRollback() {
      try {
         cache.put("/bela/ban", null); // /bela/ban needs to exist
         tx.begin();
         cache.put("/bela/ban", "name", "Bela");
         assertEquals("Bela", cache.get("/bela/ban", "name"));
         tx.rollback();
         assertNull(cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testPutRollback2() {
      try {
         cache.put("/bela/ban", "name", "Bela"); // /bela/ban needs to exist
         tx.begin();
         cache.put("/bela/ban", "name", "Michelle");
         assertEquals("Michelle", cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testSimpleRollbackTransactions() throws Exception {
      final Fqn FQN=Fqn.fromString("/a/b/c");
      tx.begin();
      cache.put(FQN, "entry", "commit");
      tx.commit();

      tx.begin();
      cache.put(FQN, "entry", "rollback");
      cache.remove(FQN);
      tx.rollback();
      assertEquals("Node should keep the commited value", "commit", cache.get(FQN).get("entry"));

      tx.begin();
      cache.remove(FQN);
      cache.put(FQN, "entry", "rollback");
      tx.rollback();
      assertEquals("Node should keep the commited value", "commit", cache.get(FQN).get("entry")); // THIS FAILS
   }


   private Transaction startTransaction() throws Exception {
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      return mgr.getTransaction();
   }


   public void testConcurrentReadAndWriteAccess() throws Exception {
      cache.setIsolationLevel(IsolationLevel.REPEATABLE_READ);

      cache.put("/1/2/3/4", "foo", "bar"); // no TX, no locks held after put() returns


      class Reader extends Thread {
         Transaction thread_tx;

         public Reader() {
            super("Reader");
         }

         public void run() {
            try {
               thread_tx=startTransaction();
               log("acquiring RL");
               cache.get("/1/2/3", "foo"); // acquires RLs on all 3 nodes
               log("RL acquired successfully");
               sleep(2000);
               log("committing TX");
               thread_tx.commit(); // releases RLs
               log("committed TX");
            }
            catch(Exception e) {
               thread_ex=e;
            }
         }
      }

      class Writer extends Thread {
         Transaction thread_tx;

         public Writer() {
            super("Writer");
         }

         public void run() {
            try {
               sleep(500); // give the Reader a chance to acquire the RLs
               thread_tx=startTransaction();
               log("acquiring WL");
               cache.put("/1", "foo", "bar2"); // needs to acquired a WL on /1
               log("acquired WL successfully");
               log("committing TX");
               thread_tx.commit();
               log("committed TX");
            }
            catch(Exception e) {
               thread_ex=e;
            }
         }
      }

      Reader reader=new Reader();
      Writer writer=new Writer();
      reader.start();
      writer.start();
      reader.join();
      writer.join();
      if(thread_ex != null)
         throw thread_ex;
   }


 /*  public void testConcurrentReadAccess() throws Exception {
      cache.setIsolationLevel(IsolationLevel.REPEATABLE_READ);
      cache.setUseInterceptorMbeans(false);

      cache.put("/1/2/3/4", "foo", "bar"); // no TX, no locks held after put() returns


      class Reader extends Thread {
         Transaction thread_tx;

         public Reader(String name) {
            super(name);
         }

         public void run() {
            try {
               thread_tx=startTransaction();
               log("acquiring RL");
               cache.get("/1/2/3", "foo"); // acquires RLs on all 3 nodes
               log("RL acquired successfully");
               sleep(200000);
               log("committing TX");
               thread_tx.commit(); // releases RLs
               log("committed TX");
            }
            catch(Exception e) {
               thread_ex=e;
            }
         }
      }


      Reader reader=new Reader("R1");
      Reader reader2=new Reader("R2");
      reader.start();
      reader2.start();
      reader.join();
      reader2.join();
      if(thread_ex != null)
         throw thread_ex;
   }

*/
   private static void log(String msg) {
      System.out.println(Thread.currentThread().getName() + ": " + msg);
   }

//   public void testRaceConditionOnNotInCacheCondition() throws Exception {
//      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
//
//      tx.begin();
//      // we now read the null entry, and decide that we need to go do something.
//
//      Object cachedObject=cache.get("/SecurityInfo/", Integer.toString(23));
//      assertNull(cachedObject); // we expect this in this test
//
//      /**
//       * now start another Thread to go do the same action, looking for the value, but it SHOULD
//       * see the result of the main thread put once it commits.
//       */
//      Thread thread=new Thread(new Runnable() {
//         UserTransaction tx2=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
//         public void run() {
//            try {
//               tx2.begin();
//               log("OtherThread: inspecting the cache");
//               Object cachedObject=cache.get("/SecurityInfo", Integer.toString(23));
//
//               log("OtherThread: read from cache: " + cachedObject);
//               Thread.sleep(3000);
//
//               cachedObject=cache.get("/SecurityInfo", Integer.toString(23));
//               log("OtherThread: read(second time) from cache:" + cachedObject);
//
//               /**
//                * This should really fail because the other thread should actually have put something else there.
//                */
//               cache.put("/SecurityInfo", Integer.toString(23), "HelloWorldDIRTY!");
//
//               log("OtherThread: Has put something in the cache tha shouldn't be there");
//            }
//            catch(Exception e) {
//               e.printStackTrace();
//            }
//            finally {
//               if(tx2 != null)
//                  try {tx2.commit();} catch(Exception e) {e.printStackTrace();}
//            }
//            log("OthreThread: exiting");
//
//         }
//      });
//
//      thread.start();
//      log("MainThread is now waiting a little bit");
//      Thread.sleep(2000); // wait long enough for the other thread to block a bit. Simulate a DB read
//      log("MainThread is now putting something in the cache");
//      cache.put("/SecurityInfo", Integer.toString(23), "HelloWorld");
//      Thread.sleep(2000); // wait long enough for the other thread to block a bit. Simulate a DB read
//      tx.commit();
//      log("MainThread: committed");
//      thread.join(30000);
//
//      log(cache.get("/SecurityInfo", Integer.toString(23)).toString());
//   }


//   public void testConcurrentReadsWithSerializableIsolationLevel() throws CacheException, InterruptedException {
//      final long TIMEOUT=5000;
//      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
//      cache.put("/testfqn", "testkey", "testvalue"); // add initial value, to be read by threads
//      Reader r1, r2;
//      r1=new Reader("reader1", 3000);
//      r2=new Reader("reader2", 0);
//      r1.start();
//      pause(100); // make sure thread1 starts and acquires the lock before thread2
//      r2.start();
//      r1.join(TIMEOUT);
//      r2.join(TIMEOUT);
//   }
//
//   class Reader extends Thread {
//      long timeout;
//
//      public Reader(String name, long timeout) {
//         super(name);
//         this.timeout=timeout;
//      }
//
//      public void run() {
//         UserTransaction trans=null;
//         try {
//            trans=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
//            trans.begin();
//            Object retval=null;
//            log2("accessing tree");
//            retval=cache.get("testfqn", "testkey");
//            log2("retval: " + retval);
//            if(timeout > 0) {
//               log2("sleeping for " + timeout + " ms");
//               pause(timeout);
//            }
//         }
//         catch(Exception e) {
//            e.printStackTrace();
//         }
//         finally {
//            try {trans.commit();} catch(Throwable t) {}
//            log2("done");
//         }
//      }
//   }

//   private void pause(long timeout) {
//      try {
//         Thread.sleep(timeout);
//      }
//      catch(InterruptedException e) {
//      }
//   }
//
//   private void log(String msg) {
//      System.out.println("-- [" + Thread.currentThread() + "]: " + msg);
//   }
//
//   private void log2(String msg) {
//      System.out.println("-- [" + System.currentTimeMillis() + " " + Thread.currentThread() + "]: " + msg);
//   }


   public static Test suite() throws Exception {
      return new TestSuite(TransactionTest.class);
   }

   public static void main(String[] args) throws Exception {
      junit.textui.TestRunner.run(suite());
   }


}
