From 6fde75c6138d9f81009935d8d782209d8426e4de Mon Sep 17 00:00:00 2001
From: Jean-Noel Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Tue, 29 Apr 2014 12:06:03 +0000
Subject: [PATCH] Added unit tests for encoding/decoding changelog state DB entries.

---
 opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnv.java                             |  341 ++++++++++++++++++++++++-------------
 opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnvTest.java |  129 ++++++++++++++
 opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDB.java                                |   20 +-
 3 files changed, 359 insertions(+), 131 deletions(-)

diff --git a/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDB.java b/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDB.java
index a1dd6ec..71becb1 100644
--- a/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDB.java
+++ b/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDB.java
@@ -60,7 +60,7 @@
 {
 
   private Database db;
-  private ReplicationDbEnv dbenv;
+  private ReplicationDbEnv dbEnv;
   private ReplicationServer replicationServer;
   private int serverId;
   private DN baseDN;
@@ -117,22 +117,22 @@
    * @param serverId The identifier of the LDAP server.
    * @param baseDN The baseDN of the replication domain.
    * @param replicationServer The ReplicationServer that needs to be shutdown.
-   * @param dbenv The Db environment to use to create the db.
+   * @param dbEnv The Db environment to use to create the db.
    * @throws ChangelogException If a database problem happened
    */
   public ReplicationDB(int serverId, DN baseDN,
-      ReplicationServer replicationServer, ReplicationDbEnv dbenv)
+      ReplicationServer replicationServer, ReplicationDbEnv dbEnv)
       throws ChangelogException
   {
     this.serverId = serverId;
     this.baseDN = baseDN;
-    this.dbenv = dbenv;
+    this.dbEnv = dbEnv;
     this.replicationServer = replicationServer;
 
     // Get or create the associated ReplicationServerDomain and Db.
     final ReplicationServerDomain domain =
         replicationServer.getReplicationServerDomain(baseDN, true);
-    db = dbenv.getOrAddDb(serverId, baseDN, domain.getGenerationId());
+    db = dbEnv.getOrAddReplicationDB(serverId, baseDN, domain.getGenerationId());
 
     intializeCounters();
   }
@@ -649,7 +649,7 @@
 
         // Create the transaction that will protect whatever done with this
         // write cursor.
-        localTxn = dbenv.beginTransaction();
+        localTxn = dbEnv.beginTransaction();
         localCursor = db.openCursor(localTxn, null);
 
         txn = localTxn;
@@ -701,7 +701,7 @@
         }
         catch (DatabaseException e)
         {
-          dbenv.shutdownOnException(e);
+          dbEnv.shutdownOnException(e);
         }
       }
     }
@@ -855,14 +855,14 @@
       }
 
       // Clears the reference to this serverID
-      dbenv.clearServerId(baseDN, serverId);
+      dbEnv.clearServerId(baseDN, serverId);
 
       final Database oldDb = db;
       db = null; // In case there's a failure between here and recreation.
-      dbenv.clearDb(oldDb);
+      dbEnv.clearDb(oldDb);
 
       // RE-create the db
-      db = dbenv.getOrAddDb(serverId, baseDN, -1);
+      db = dbEnv.getOrAddReplicationDB(serverId, baseDN, -1);
     }
     catch (Exception e)
     {
diff --git a/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnv.java b/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnv.java
index fb11b44..ba63c39 100644
--- a/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnv.java
+++ b/opends/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnv.java
@@ -28,7 +28,9 @@
 
 import java.io.File;
 import java.io.UnsupportedEncodingException;
-import java.util.List;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.*;
+import java.util.Map.Entry;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -87,58 +89,7 @@
 
     try
     {
-      EnvironmentConfig envConfig = new EnvironmentConfig();
-
-      /*
-       * Create the DB Environment that will be used for all the
-       * ReplicationServer activities related to the db
-       */
-      envConfig.setAllowCreate(true);
-      envConfig.setTransactional(true);
-      envConfig.setConfigParam(STATS_COLLECT, "false");
-      envConfig.setConfigParam(CLEANER_THREADS, "2");
-      envConfig.setConfigParam(CHECKPOINTER_HIGH_PRIORITY, "true");
-      /*
-       * Tests have shown that since the parsing of the Replication log is
-       * always done sequentially, it is not necessary to use a large DB cache.
-       */
-      if (Runtime.getRuntime().maxMemory() > 256 * 1024 * 1024)
-      {
-        /*
-         * If the JVM is reasonably large then we can safely default to bigger
-         * read buffers. This will result in more scalable checkpointer and
-         * cleaner performance.
-         */
-        envConfig.setConfigParam(CLEANER_LOOK_AHEAD_CACHE_SIZE, mb(2));
-        envConfig.setConfigParam(LOG_ITERATOR_READ_SIZE, mb(2));
-        envConfig.setConfigParam(LOG_FAULT_READ_SIZE, kb(4));
-
-        /*
-         * The cache size must be bigger in order to accommodate the larger
-         * buffers - see OPENDJ-943.
-         */
-        envConfig.setConfigParam(MAX_MEMORY, mb(16));
-      }
-      else
-      {
-        /*
-         * Use 5M so that the replication can be used with 64M total for the
-         * JVM.
-         */
-        envConfig.setConfigParam(MAX_MEMORY, mb(5));
-      }
-
-      // Since records are always added at the end of the Replication log and
-      // deleted at the beginning of the Replication log, this should never
-      // cause any deadlock.
-      envConfig.setTxnTimeout(0, TimeUnit.SECONDS);
-      envConfig.setLockTimeout(0, TimeUnit.SECONDS);
-
-      // Since replication provides durability, we can reduce the DB durability
-      // level so that we are immune to application / JVM crashes.
-      envConfig.setDurability(Durability.COMMIT_WRITE_NO_SYNC);
-
-      dbEnvironment = new Environment(new File(path), envConfig);
+      dbEnvironment = openJEEnvironment(path);
 
       /*
        * One database is created to store the update from each LDAP server in
@@ -153,6 +104,71 @@
     }
   }
 
+  /**
+   * Open a JE environment.
+   * <p>
+   * protected so it can be overridden by tests.
+   *
+   * @param path
+   *          the path to the JE environment in the filesystem
+   * @return the opened JE environment
+   */
+  protected Environment openJEEnvironment(String path)
+  {
+    final EnvironmentConfig envConfig = new EnvironmentConfig();
+
+    /*
+     * Create the DB Environment that will be used for all the
+     * ReplicationServer activities related to the db
+     */
+    envConfig.setAllowCreate(true);
+    envConfig.setTransactional(true);
+    envConfig.setConfigParam(STATS_COLLECT, "false");
+    envConfig.setConfigParam(CLEANER_THREADS, "2");
+    envConfig.setConfigParam(CHECKPOINTER_HIGH_PRIORITY, "true");
+    /*
+     * Tests have shown that since the parsing of the Replication log is
+     * always done sequentially, it is not necessary to use a large DB cache.
+     */
+    if (Runtime.getRuntime().maxMemory() > 256 * 1024 * 1024)
+    {
+      /*
+       * If the JVM is reasonably large then we can safely default to bigger
+       * read buffers. This will result in more scalable checkpointer and
+       * cleaner performance.
+       */
+      envConfig.setConfigParam(CLEANER_LOOK_AHEAD_CACHE_SIZE, mb(2));
+      envConfig.setConfigParam(LOG_ITERATOR_READ_SIZE, mb(2));
+      envConfig.setConfigParam(LOG_FAULT_READ_SIZE, kb(4));
+
+      /*
+       * The cache size must be bigger in order to accommodate the larger
+       * buffers - see OPENDJ-943.
+       */
+      envConfig.setConfigParam(MAX_MEMORY, mb(16));
+    }
+    else
+    {
+      /*
+       * Use 5M so that the replication can be used with 64M total for the
+       * JVM.
+       */
+      envConfig.setConfigParam(MAX_MEMORY, mb(5));
+    }
+
+    // Since records are always added at the end of the Replication log and
+    // deleted at the beginning of the Replication log, this should never
+    // cause any deadlock.
+    envConfig.setTxnTimeout(0, TimeUnit.SECONDS);
+    envConfig.setLockTimeout(0, TimeUnit.SECONDS);
+
+    // Since replication provides durability, we can reduce the DB durability
+    // level so that we are immune to application / JVM crashes.
+    envConfig.setDurability(Durability.COMMIT_WRITE_NO_SYNC);
+
+    return new Environment(new File(path), envConfig);
+  }
+
   private String kb(int sizeInKb)
   {
     return String.valueOf(sizeInKb * 1024);
@@ -163,8 +179,21 @@
     return String.valueOf(sizeInMb * 1024 * 1024);
   }
 
-  private Database openDatabase(String databaseName) throws ChangelogException,
-      RuntimeException
+  /**
+   * Open a JE database.
+   * <p>
+   * protected so it can be overridden by tests.
+   *
+   * @param databaseName
+   *          the databaseName to open
+   * @return the opened JE database
+   * @throws ChangelogException
+   *           if a problem happened opening the database
+   * @throws RuntimeException
+   *           if a problem happened with the JE database
+   */
+  protected Database openDatabase(String databaseName)
+      throws ChangelogException, RuntimeException
   {
     if (isShuttingDown.get())
     {
@@ -197,59 +226,90 @@
    */
   public ChangelogState readChangelogState() throws ChangelogException
   {
+    return decodeChangelogState(readWholeState());
+  }
+
+  /**
+   * Decode the whole changelog state DB.
+   *
+   * @param wholeState
+   *          the whole changelog state DB as a Map.
+   *          The Map is only used as a convenient collection of key => data objects 
+   * @return the decoded changelog state
+   * @throws ChangelogException
+   *           if a problem occurred while decoding
+   */
+  ChangelogState decodeChangelogState(Map<byte[], byte[]> wholeState)
+      throws ChangelogException
+  {
+    try
+    {
+      final ChangelogState result = new ChangelogState();
+      for (Entry<byte[], byte[]> entry : wholeState.entrySet())
+      {
+        final String stringKey = toString(entry.getKey());
+        final String stringData = toString(entry.getValue());
+
+        if (debugEnabled())
+        {
+          debug("read (key, value)=(" + stringKey + ", " + stringData + ")");
+        }
+
+        final String[] str = stringData.split(FIELD_SEPARATOR, 3);
+        if (str[0].equals(GENERATION_ID_TAG))
+        {
+          final long generationId = toLong(str[1]);
+          final DN baseDN = DN.decode(str[2]);
+          if (debugEnabled())
+          {
+            debug("has read generationId: baseDN=" + baseDN + " generationId="
+                + generationId);
+          }
+          result.setDomainGenerationId(baseDN, generationId);
+        }
+        else
+        {
+          final int serverId = toInt(str[0]);
+          final DN baseDN = DN.decode(str[1]);
+          if (debugEnabled())
+          {
+            debug("has read replica: baseDN=" + baseDN + " serverId=" + serverId);
+          }
+          result.addServerIdToDomain(serverId, baseDN);
+        }
+      }
+      return result;
+    }
+    catch (DirectoryException e)
+    {
+      throw new ChangelogException(e.getMessageObject(), e);
+    }
+  }
+
+  private Map<byte[], byte[]> readWholeState() throws ChangelogException
+  {
     DatabaseEntry key = new DatabaseEntry();
     DatabaseEntry data = new DatabaseEntry();
     Cursor cursor = changelogStateDb.openCursor(null, null);
 
     try
     {
-      final ChangelogState result = new ChangelogState();
+      final Map<byte[], byte[]> results = new LinkedHashMap<byte[], byte[]>();
 
       OperationStatus status = cursor.getFirst(key, data, LockMode.DEFAULT);
       while (status == OperationStatus.SUCCESS)
       {
-        final String stringData = toString(data.getData());
-
-        if (debugEnabled())
-          debug("read (" + GENERATION_ID_TAG + " generationId baseDN) OR "
-              + "(serverId baseDN): " + stringData);
-
-        final String[] str = stringData.split(FIELD_SEPARATOR, 3);
-        if (str[0].equals(GENERATION_ID_TAG))
-        {
-          long generationId = toLong(str[1]);
-          DN baseDN = DN.decode(str[2]);
-
-          if (debugEnabled())
-            debug("has read baseDN=" + baseDN + " generationId=" +generationId);
-
-          result.setDomainGenerationId(baseDN, generationId);
-        }
-        else
-        {
-          int serverId = toInt(str[0]);
-          DN baseDN = DN.decode(str[1]);
-
-          if (debugEnabled())
-            debug("has read: baseDN=" + baseDN + " serverId=" + serverId);
-
-          result.addServerIdToDomain(serverId, baseDN);
-        }
-
+        results.put(key.getData(), data.getData());
         status = cursor.getNext(key, data, LockMode.DEFAULT);
       }
 
-      return result;
+      return results;
     }
     catch (RuntimeException e)
     {
       final Message message = ERR_JEB_DATABASE_EXCEPTION.get(e.getMessage());
       throw new ChangelogException(message, e);
     }
-    catch (DirectoryException e)
-    {
-      throw new ChangelogException(e.getMessageObject(), e);
-    }
     finally
     {
       close(cursor);
@@ -261,7 +321,8 @@
     try
     {
       return Integer.parseInt(data);
-    } catch (NumberFormatException e)
+    }
+    catch (NumberFormatException e)
     {
       // should never happen
       // TODO: i18n
@@ -301,7 +362,14 @@
     }
   }
 
-  private byte[] toBytes(String s)
+  /**
+   * Converts the string to a UTF8-encoded byte array.
+   *
+   * @param s
+   *          the string to convert
+   * @return the byte array representation of the UTF8-encoded string
+   */
+  static byte[] toBytes(String s)
   {
     try
     {
@@ -315,8 +383,8 @@
   }
 
   /**
-   * Finds or creates the database used to store changes from the server with
-   * the given serverId and the given baseDN.
+   * Finds or creates the database used to store changes for a replica with the
+   * given baseDN and serverId.
    *
    * @param serverId
    *          The server id that identifies the server.
@@ -328,26 +396,27 @@
    * @throws ChangelogException
    *           in case of underlying Exception.
    */
-  public Database getOrAddDb(int serverId, DN baseDN, long generationId)
+  public Database getOrAddReplicationDB(int serverId, DN baseDN, long generationId)
       throws ChangelogException
   {
     if (debugEnabled())
+    {
       debug("ReplicationDbEnv.getOrAddDb(" + serverId + ", " + baseDN + ", "
           + generationId + ")");
+    }
     try
     {
       // JNR: redundant info is stored between the key and data down below.
       // It is probably ok since "changelogstate" DB does not receive a high
       // volume of inserts.
-      final String serverIdToBaseDn = buildServerIdKey(baseDN, serverId);
+      Entry<String, String> replicaEntry = toReplicaEntry(baseDN, serverId);
 
       // Opens the DB for the changes received from this server on this domain.
-      Database db = openDatabase(serverIdToBaseDn);
+      final Database replicaDB = openDatabase(replicaEntry.getKey());
 
-      putInChangelogStateDBIfNotExist(serverIdToBaseDn, serverIdToBaseDn);
-      putInChangelogStateDBIfNotExist(buildGenIdKey(baseDN),
-                                      buildGenIdData(baseDN, generationId));
-      return db;
+      putInChangelogStateDBIfNotExist(replicaEntry);
+      putInChangelogStateDBIfNotExist(toGenIdEntry(baseDN, generationId));
+      return replicaDB;
     }
     catch (RuntimeException e)
     {
@@ -355,36 +424,57 @@
     }
   }
 
-  private String buildGenIdKey(DN baseDN)
+  /**
+   * Return an entry to store in the changelog state database representing a
+   * replica in the topology.
+   *
+   * @param baseDN
+   *          the replica's baseDN
+   * @param serverId
+   *          the replica's serverId
+   * @return a database entry for the replica
+   */
+  Entry<String, String> toReplicaEntry(DN baseDN, int serverId)
   {
-    return GENERATION_ID_TAG + FIELD_SEPARATOR + baseDN.toNormalizedString();
+    final String key = serverId + FIELD_SEPARATOR + baseDN.toNormalizedString();
+    return new SimpleImmutableEntry<String, String>(key, key);
   }
 
-  private String buildServerIdKey(DN baseDN, int serverId)
+  /**
+   * Return an entry to store in the changelog state database representing the
+   * domain generation id.
+   *
+   * @param baseDN
+   *          the domain's baseDN
+   * @param generationId
+   *          the domain's generationId
+   * @return a database entry for the generationId
+   */
+  Entry<String, String> toGenIdEntry(DN baseDN, long generationId)
   {
-    return serverId + FIELD_SEPARATOR + baseDN.toNormalizedString();
+    final String normDn = baseDN.toNormalizedString();
+    final String key = GENERATION_ID_TAG + FIELD_SEPARATOR + normDn;
+    final String data = GENERATION_ID_TAG + FIELD_SEPARATOR + generationId
+        + FIELD_SEPARATOR + normDn;
+    return new SimpleImmutableEntry<String, String>(key, data);
   }
 
-  private String buildGenIdData(DN baseDN, long generationId)
+  private void putInChangelogStateDBIfNotExist(Entry<String, String> entry)
+      throws RuntimeException
   {
-    return GENERATION_ID_TAG + FIELD_SEPARATOR + generationId + FIELD_SEPARATOR
-        + baseDN.toNormalizedString();
-  }
-
-  private void putInChangelogStateDBIfNotExist(String keyString,
-      String dataString) throws RuntimeException
-  {
-    DatabaseEntry key = new DatabaseEntry(toBytes(keyString));
+    DatabaseEntry key = new DatabaseEntry(toBytes(entry.getKey()));
     DatabaseEntry data = new DatabaseEntry();
     if (changelogStateDb.get(null, key, data, LockMode.DEFAULT) == NOTFOUND)
     {
       Transaction txn = dbEnvironment.beginTransaction(null, null);
       try
       {
-        data.setData(toBytes(dataString));
+        data.setData(toBytes(entry.getValue()));
         if (debugEnabled())
-          debug("putting record in the changelogstate Db key=[" + keyString
-              + "] value=[" + dataString + "]");
+        {
+          debug("putting record in the changelogstate Db key=["
+              + entry.getKey() + "] value=[" + entry.getValue() + "]");
+        }
         changelogStateDb.put(txn, key, data);
         txn.commit(Durability.COMMIT_WRITE_NO_SYNC);
       }
@@ -475,7 +565,8 @@
    */
   public void clearGenerationId(DN baseDN) throws ChangelogException
   {
-    deleteFromChangelogStateDB(buildGenIdKey(baseDN),
+    final int unusedGenId = 0;
+    deleteFromChangelogStateDB(toGenIdEntry(baseDN, unusedGenId),
         "clearGenerationId(baseDN=" + baseDN + ")");
   }
 
@@ -492,19 +583,21 @@
    */
   public void clearServerId(DN baseDN, int serverId) throws ChangelogException
   {
-    deleteFromChangelogStateDB(buildServerIdKey(baseDN, serverId),
+    deleteFromChangelogStateDB(toReplicaEntry(baseDN, serverId),
         "clearServerId(baseDN=" + baseDN + " , serverId=" + serverId + ")");
   }
 
-  private void deleteFromChangelogStateDB(String keyString,
+  private void deleteFromChangelogStateDB(Entry<String, ?> entry,
       String methodInvocation) throws ChangelogException
   {
     if (debugEnabled())
+    {
       debug(methodInvocation + " starting");
+    }
 
     try
     {
-      final DatabaseEntry key = new DatabaseEntry(toBytes(keyString));
+      final DatabaseEntry key = new DatabaseEntry(toBytes(entry.getKey()));
       final DatabaseEntry data = new DatabaseEntry();
       if (changelogStateDb.get(null, key, data, LockMode.DEFAULT) == SUCCESS)
       {
@@ -514,7 +607,9 @@
           changelogStateDb.delete(txn, key);
           txn.commit(Durability.COMMIT_WRITE_NO_SYNC);
           if (debugEnabled())
+          {
             debug(methodInvocation + " succeeded");
+          }
         }
         catch (RuntimeException dbe)
         {
@@ -526,8 +621,10 @@
       else
       {
         if (debugEnabled())
-          debug(methodInvocation + " failed: key=[ " + keyString
+        {
+          debug(methodInvocation + " failed: key=[ " + entry.getKey()
               + "] not found");
+        }
       }
     }
     catch (RuntimeException dbe)
@@ -570,7 +667,9 @@
         try
         {
           if (txn != null)
+          {
             txn.abort();
+          }
         }
         catch(Exception e)
         { /* do nothing */ }
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnvTest.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnvTest.java
new file mode 100644
index 0000000..1a13313
--- /dev/null
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/changelog/je/ReplicationDbEnvTest.java
@@ -0,0 +1,129 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *      Copyright 2014 ForgeRock AS
+ */
+package org.opends.server.replication.server.changelog.je;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.replication.server.ChangelogState;
+import org.opends.server.replication.server.changelog.api.ChangelogException;
+import org.opends.server.types.DN;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.sleepycat.je.Database;
+import com.sleepycat.je.Environment;
+
+import static java.util.Arrays.*;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.opends.server.replication.server.changelog.je.ReplicationDbEnv.*;
+
+@SuppressWarnings("javadoc")
+public class ReplicationDbEnvTest extends DirectoryServerTestCase
+{
+
+	/**
+	 * Bypass heavyweight setup. 
+	 */
+	private final class TestableReplicationDbEnv extends ReplicationDbEnv
+	{
+		private TestableReplicationDbEnv() throws ChangelogException
+		{
+			super(null, null);
+		}
+
+		@Override
+		protected Environment openJEEnvironment(String path)
+		{
+			return null;
+		}
+
+		@Override
+		protected Database openDatabase(String databaseName)
+				throws ChangelogException, RuntimeException
+		{
+			return null;
+		}
+	}
+
+	@BeforeClass
+	public void setup() throws Exception
+	{
+		TestCaseUtils.startFakeServer();
+	}
+
+	@AfterClass
+	public void teardown()
+	{
+		TestCaseUtils.shutdownFakeServer();
+	}
+
+	@DataProvider
+	public Object[][] changelogStateDataProvider() throws Exception
+	{
+		return new Object[][] {
+			{ DN.decode("dc=example,dc=com"), 524157415, asList(42, 346) },
+			// test with a space in the baseDN (space is the field separator in the DB)
+			// FIXME does not work yet (gosh!!)
+			// { DN.decode("cn=admin data"), 524157415, asList(42, 346) },
+	  };
+	}
+
+	@Test(dataProvider = "changelogStateDataProvider")
+	public void encodeDecodeChangelogState(DN baseDN, long generationId,
+			List<Integer> serverIds) throws Exception
+	{
+		final ReplicationDbEnv changelogStateDB = new TestableReplicationDbEnv();
+
+		// encode data
+		final Map<byte[], byte[]> wholeState = new LinkedHashMap<byte[], byte[]>();
+		put(wholeState, changelogStateDB.toGenIdEntry(baseDN, generationId));
+		for (Integer serverId : serverIds)
+		{
+			put(wholeState, changelogStateDB.toReplicaEntry(baseDN, serverId));
+		}
+
+		// decode data
+		final ChangelogState state =
+				changelogStateDB.decodeChangelogState(wholeState);
+		assertThat(state.getDomainToGenerationId()).containsExactly(
+				entry(baseDN, generationId));
+		assertThat(state.getDomainToServerIds()).containsExactly(
+				entry(baseDN, serverIds));
+	}
+
+	private void put(Map<byte[], byte[]> map, Entry<String, String> entry)
+	{
+		map.put(toBytes(entry.getKey()), toBytes(entry.getValue()));
+	}
+
+}

--
Gitblit v1.10.0