You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by fd...@apache.org on 2011/02/16 21:05:32 UTC

svn commit: r1071375 [1/3] - in /couchdb/trunk: etc/couchdb/ share/www/script/test/ src/couchdb/ test/etap/

Author: fdmanana
Date: Wed Feb 16 20:05:31 2011
New Revision: 1071375

URL: http://svn.apache.org/viewvc?rev=1071375&view=rev
Log:
Added the new replicator implementation

Closes COUCHDB-1024. An introduction to this new implementation was given
in the development mailing list:  http://s.apache.org/KsY


Added:
    couchdb/trunk/src/couchdb/couch_api_wrap.erl
    couchdb/trunk/src/couchdb/couch_api_wrap.hrl
    couchdb/trunk/src/couchdb/couch_api_wrap_httpc.erl
    couchdb/trunk/src/couchdb/couch_httpc_pool.erl
    couchdb/trunk/src/couchdb/couch_httpd_replicator.erl
    couchdb/trunk/src/couchdb/couch_replication_notifier.erl
    couchdb/trunk/src/couchdb/couch_replicator.erl
    couchdb/trunk/src/couchdb/couch_replicator.hrl
    couchdb/trunk/src/couchdb/couch_replicator_doc_copier.erl
    couchdb/trunk/src/couchdb/couch_replicator_rev_finder.erl
    couchdb/trunk/src/couchdb/couch_replicator_utils.erl
    couchdb/trunk/src/couchdb/json_stream_parse.erl
    couchdb/trunk/test/etap/190-json-stream-parse.t
Removed:
    couchdb/trunk/src/couchdb/couch_rep.erl
    couchdb/trunk/src/couchdb/couch_rep_att.erl
    couchdb/trunk/src/couchdb/couch_rep_changes_feed.erl
    couchdb/trunk/src/couchdb/couch_rep_httpc.erl
    couchdb/trunk/src/couchdb/couch_rep_missing_revs.erl
    couchdb/trunk/src/couchdb/couch_rep_reader.erl
    couchdb/trunk/src/couchdb/couch_rep_writer.erl
    couchdb/trunk/test/etap/110-replication-httpc.t
    couchdb/trunk/test/etap/111-replication-changes-feed.t
    couchdb/trunk/test/etap/112-replication-missing-revs.t
    couchdb/trunk/test/etap/113-replication-attachment-comp.t
Modified:
    couchdb/trunk/etc/couchdb/default.ini.tpl.in
    couchdb/trunk/share/www/script/test/replication.js
    couchdb/trunk/src/couchdb/Makefile.am
    couchdb/trunk/src/couchdb/couch_db.hrl
    couchdb/trunk/src/couchdb/couch_doc.erl
    couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
    couchdb/trunk/src/couchdb/couch_primary_sup.erl
    couchdb/trunk/src/couchdb/couch_rep_db_listener.erl
    couchdb/trunk/src/couchdb/couch_work_queue.erl
    couchdb/trunk/test/etap/001-load.t
    couchdb/trunk/test/etap/Makefile.am

Modified: couchdb/trunk/etc/couchdb/default.ini.tpl.in
URL: http://svn.apache.org/viewvc/couchdb/trunk/etc/couchdb/default.ini.tpl.in?rev=1071375&r1=1071374&r2=1071375&view=diff
==============================================================================
--- couchdb/trunk/etc/couchdb/default.ini.tpl.in (original)
+++ couchdb/trunk/etc/couchdb/default.ini.tpl.in Wed Feb 16 20:05:31 2011
@@ -71,7 +71,7 @@ _utils = {couch_httpd_misc_handlers, han
 _all_dbs = {couch_httpd_misc_handlers, handle_all_dbs_req}
 _active_tasks = {couch_httpd_misc_handlers, handle_task_status_req}
 _config = {couch_httpd_misc_handlers, handle_config_req}
-_replicate = {couch_httpd_misc_handlers, handle_replicate_req}
+_replicate = {couch_httpd_replicator, handle_req}
 _uuids = {couch_httpd_misc_handlers, handle_uuids_req}
 _restart = {couch_httpd_misc_handlers, handle_restart_req}
 _stats = {couch_httpd_stats_handlers, handle_stats_req}
@@ -137,8 +137,27 @@ compressible_types = text/*, application
 [replicator]
 db = _replicator
 max_replication_retry_count = 10
-max_http_sessions = 20
-max_http_pipeline_size = 50
+; More worker processes can give higher network throughput but can also
+; imply more disk and network IO.
+worker_processes = 4
+; With lower batch sizes checkpoints are done more frequently. Lower batch sizes
+; also reduce the total amount of used RAM memory.
+worker_batch_size = 1000
+; Maximum number of HTTP connections and pipeline size (for each connection)
+; per replication. These two settings have more impact on pull replications.
+http_connections = 20
+http_pipeline_size = 50
+; HTTP connection timeout per replication.
+; Even for very fast/reliable networks it might need to be increased if a remote
+; database is too busy.
+connection_timeout = 30000
+; Some socket options that might boost performance in some scenarios:
+;       {nodelay, boolean()}
+;       {sndbuf, integer()}
+;       {recbuf, integer()}
+;       {priority, integer()}
+; See the `inet` Erlang module's man page for the full list of options.
+socket_options = [{keepalive, true}, {nodelay, false}]
 ; set to true to validate peer certificates
 verify_ssl_certificates = false
 ; file containing a list of peer trusted certificates (PEM format)

Modified: couchdb/trunk/share/www/script/test/replication.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/replication.js?rev=1071375&r1=1071374&r2=1071375&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/replication.js (original)
+++ couchdb/trunk/share/www/script/test/replication.js Wed Feb 16 20:05:31 2011
@@ -11,447 +11,633 @@
 // the License.
 
 couchTests.replication = function(debug) {
+
   if (debug) debugger;
+
   var host = CouchDB.host;
+  var sourceDb = new CouchDB("test_suite_db_a",{"X-Couch-Full-Commit":"false"});
+  var targetDb = new CouchDB("test_suite_db_b",{"X-Couch-Full-Commit":"false"});
+
   var dbPairs = [
-    {source:"test_suite_db_a",
-      target:"test_suite_db_b"},
-    {source:"test_suite_db_a",
-      target:CouchDB.protocol + host + "/test_suite_db_b"},
-    {source:CouchDB.protocol + host + "/test_suite_db_a",
-      target:"test_suite_db_b"},
-    {source:CouchDB.protocol + host + "/test_suite_db_a",
-      target:CouchDB.protocol + host + "/test_suite_db_b"}
+    {
+      source: sourceDb.name,
+      target: targetDb.name
+    },
+    {
+      source: CouchDB.protocol + host + "/" + sourceDb.name,
+      target: targetDb.name
+    },
+    {
+      source: sourceDb.name,
+      target: CouchDB.protocol + host + "/" + targetDb.name
+    },
+    {
+      source: CouchDB.protocol + host + "/" + sourceDb.name,
+      target: CouchDB.protocol + host + "/" + targetDb.name
+    }
   ];
-  var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
-  var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
-  var numDocs = 10;
-  var xhr;
-  for (var testPair = 0; testPair < dbPairs.length; testPair++) {
-    var A = dbPairs[testPair].source;
-    var B = dbPairs[testPair].target;
-
-    dbA.deleteDb();
-    dbA.createDb();
-    dbB.deleteDb();
-    dbB.createDb();
-
-    var repTests = {
-      // copy and paste and put your code in. delete unused steps.
-      test_template: new function () {
-        this.init = function(dbA, dbB) {
-          // before anything has happened
-        };
-        this.afterAB1 = function(dbA, dbB) {
-          // called after replicating src=A  tgt=B first time.
-        };
-        this.afterBA1 = function(dbA, dbB) {
-          // called after replicating src=B  tgt=A first time.
-        };
-        this.afterAB2 = function(dbA, dbB) {
-          // called after replicating src=A  tgt=B second time.
-        };
-        this.afterBA2 = function(dbA, dbB) {
-          // etc...
-        };
-      },
 
-      simple_test: new function () {
-        this.init = function(dbA, dbB) {
-          var docs = makeDocs(0, numDocs);
-          dbA.bulkSave(docs);
-        };
-
-        this.afterAB1 = function(dbA, dbB) {
-          for (var j = 0; j < numDocs; j++) {
-            var docA = dbA.open("" + j);
-            var docB = dbB.open("" + j);
-            T(docA._rev == docB._rev);
-          }
-        };
-      },
+  var att1_data = CouchDB.request("GET", "/_utils/script/test/lorem.txt");
+  att1_data = att1_data.responseText;
 
-     deletes_test: new function () {
-        // make sure deletes are replicated
-        this.init = function(dbA, dbB) {
-          T(dbA.save({_id:"foo1",value:"a"}).ok);
-        };
-
-        this.afterAB1 = function(dbA, dbB) {
-          var docA = dbA.open("foo1");
-          var docB = dbB.open("foo1");
-          T(docA._rev == docB._rev);
-
-          dbA.deleteDoc(docA);
-        };
-
-        this.afterAB2 = function(dbA, dbB) {
-          T(dbA.open("foo1") == null);
-          T(dbB.open("foo1") == null);
-        };
-      },
+  var att2_data = CouchDB.request("GET", "/_utils/script/test/lorem_b64.txt");
+  att2_data = att2_data.responseText;
 
-      deleted_test : new function() {
-        // docs created and deleted on a single node are also replicated
-        this.init = function(dbA, dbB) {
-          T(dbA.save({_id:"del1",value:"a"}).ok);
-          var docA = dbA.open("del1");
-          dbA.deleteDoc(docA);
-        };
-
-        this.afterAB1 = function(dbA, dbB) {
-          var rows = dbB.changes().results;
-          var rowCnt = 0;
-          for (var i=0; i < rows.length; i++) {
-            if (rows[i].id == "del1") {
-              rowCnt += 1;
-              T(rows[i].deleted == true);
-            }
-          };
-          T(rowCnt == 1);
-        };
-      },
+  var sourceInfo, targetInfo;
+  var docs, doc, copy;
+  var repResult;
+  var i, j, k;
 
-      slashes_in_ids_test: new function () {
-        // make sure docs with slashes in id replicate properly
-        this.init = function(dbA, dbB) {
-          dbA.save({ _id:"abc/def", val:"one" });
-        };
-
-        this.afterAB1 = function(dbA, dbB) {
-          var docA = dbA.open("abc/def");
-          var docB = dbB.open("abc/def");
-          T(docA._rev == docB._rev);
-        };
-      },
 
-      design_docs_test: new function() {
-        // make sure design docs replicate properly
-        this.init = function(dbA, dbB) {
-          dbA.save({ _id:"_design/test" });
-        };
-
-        this.afterAB1 = function() {
-          var docA = dbA.open("_design/test");
-          var docB = dbB.open("_design/test");
-          T(docA._rev == docB._rev);
-        };
-      },
+  function makeAttData(minSize) {
+    var data = att1_data;
+
+    while (data.length < minSize) {
+      data = data + att1_data;
+    }
+    return data;
+  }
 
-      attachments_test: new function () {
-        // Test attachments
-        this.init = function(dbA, dbB) {
-          dbA.save({
-            _id:"bin_doc",
-            _attachments:{
-              "foo+bar.txt": {
-                "type":"base64",
-                "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
-              }
-            }
-          });
-          // make sure on design docs as well
-          dbA.save({
-            _id:"_design/with_bin",
-            _attachments:{
-              "foo+bar.txt": {
-                "type":"base64",
-                "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
-              }
-            }
-          });
-        };
-
-        this.afterAB1 = function(dbA, dbB) {
-          var xhr = CouchDB.request("GET",
-            "/test_suite_db_a/bin_doc/foo%2Bbar.txt");
-          T(xhr.responseText == "This is a base64 encoded text");
-
-          xhr = CouchDB.request("GET",
-            "/test_suite_db_b/bin_doc/foo%2Bbar.txt");
-          T(xhr.responseText == "This is a base64 encoded text");
-
-          // and the design-doc
-          xhr = CouchDB.request("GET",
-            "/test_suite_db_a/_design/with_bin/foo%2Bbar.txt");
-          T(xhr.responseText == "This is a base64 encoded text");
-
-          xhr = CouchDB.request("GET",
-            "/test_suite_db_b/_design/with_bin/foo%2Bbar.txt");
-          T(xhr.responseText == "This is a base64 encoded text");
-        };
-      },
 
-      conflicts_test: new function () {
-        // test conflicts
-        this.init = function(dbA, dbB) {
-          dbA.save({_id:"foo",value:"a"});
-          dbB.save({_id:"foo",value:"b"});
-        };
-
-        this.afterBA1 = function(dbA, dbB) {
-          var docA = dbA.open("foo", {conflicts: true});
-          var docB = dbB.open("foo", {conflicts: true});
-
-          // make sure the same rev is in each db
-          T(docA._rev === docB._rev);
-
-          // make sure the conflicts are the same in each db
-          T(docA._conflicts[0] === docB._conflicts[0]);
-
-          // delete a conflict.
-          dbA.deleteDoc({_id:"foo", _rev:docA._conflicts[0]});
-        };
-
-        this.afterBA2 = function(dbA, dbB) {
-          // open documents and include the conflict meta data
-          var docA = dbA.open("foo", {conflicts: true, deleted_conflicts: true});
-          var docB = dbB.open("foo", {conflicts: true, deleted_conflicts: true});
-
-          // We should have no conflicts this time
-          T(typeof docA._conflicts === "undefined");
-          T(typeof docB._conflicts === "undefined");
-
-          // They show up as deleted conflicts instead
-          T(docA._deleted_conflicts[0] == docB._deleted_conflicts[0]);
-        };
+  function enableAttCompression(level, types) {
+    var xhr = CouchDB.request(
+      "PUT",
+      "/_config/attachments/compression_level",
+      {
+        body: JSON.stringify(level),
+        headers: {"X-Couch-Persist": "false"}
       }
-    };
+    );
+    T(xhr.status === 200);
+    xhr = CouchDB.request(
+      "PUT",
+      "/_config/attachments/compressible_types",
+      {
+        body: JSON.stringify(types),
+        headers: {"X-Couch-Persist": "false"}
+      }
+    );
+    T(xhr.status === 200);
+  }
+
 
-    var test;
-    for(test in repTests) {
-      if(repTests[test].init) {
-        repTests[test].init(dbA, dbB);
+  function disableAttCompression() {
+    var xhr = CouchDB.request(
+      "PUT",
+      "/_config/attachments/compression_level",
+      {
+        body: JSON.stringify("0"),
+        headers: {"X-Couch-Persist": "false"}
       }
+    );
+    T(xhr.status === 200);
+  }
+
+
+  function populateDb(db, docs, dontRecreateDb) {
+    if (dontRecreateDb !== true) {
+      db.deleteDb();
+      db.createDb();
     }
+    for (var i = 0; i < docs.length; i++) {
+      var doc = docs[i];
+      delete doc._rev;
+    }
+    if (docs.length > 0) {
+      db.bulkSave(docs);
+    }
+  }
 
-    var result = CouchDB.replicate(A, B);
 
-    var seqA = result.source_last_seq;
-    T(0 == result.history[0].start_last_seq);
-    T(typeof result.history[1] === "undefined");
+  function addAtt(db, doc, attName, attData, type) {
+    var uri = "/" + db.name + "/" + encodeURIComponent(doc._id) + "/" + attName;
 
-    for(test in repTests) {
-      if(repTests[test].afterAB1) repTests[test].afterAB1(dbA, dbB);
+    if (doc._rev) {
+      uri += "?rev=" + doc._rev;
     }
 
-    result = CouchDB.replicate(B, A);
+    var xhr = CouchDB.request("PUT", uri, {
+      headers: {
+        "Content-Type": type
+      },
+      body: attData
+    });
+
+    T(xhr.status === 201);
+    doc._rev = JSON.parse(xhr.responseText).rev;
+  }
 
-    var seqB = result.source_last_seq;
-    T(0 == result.history[0].start_last_seq);
-    T(typeof result.history[1] === "undefined");
 
-    for(test in repTests) {
-      if(repTests[test].afterBA1) repTests[test].afterBA1(dbA, dbB);
+  function compareObjects(o1, o2) {
+    for (var p in o1) {
+      if (o1[p] === null && o2[p] !== null) {
+        return false;
+      } else if (typeof o1[p] === "object") {
+        if ((typeof o2[p] !== "object") || o2[p] === null) {
+          return false;
+        }
+        if (!arguments.callee(o1[p], o2[p])) {
+          return false;
+        }
+      } else {
+        if (o1[p] !== o2[p]) {
+          return false;
+        }
+      }
     }
+    return true;
+  }
+
 
-    var result2 = CouchDB.replicate(A, B);
+  function waitForSeq(sourceDb, targetDb) {
+    var targetSeq,
+        sourceSeq = sourceDb.info().update_seq,
+        t0 = new Date(),
+        t1,
+        ms = 1000;
 
-    // each successful replication produces a new session id
-    T(result2.session_id != result.session_id);
+    do {
+      targetSeq = targetDb.info().update_seq;
+      t1 = new Date();
+    } while (((t1 - t0) <= ms) && targetSeq < sourceSeq);
+  }
+
+
+  function wait(ms) {
+    var t0 = new Date(), t1;
+    do {
+      CouchDB.request("GET", "/");
+      t1 = new Date();
+    } while ((t1 - t0) <= ms);
+  }
 
-    T(seqA < result2.source_last_seq);
-    T(seqA == result2.history[0].start_last_seq);
-    T(result2.history[1].end_last_seq == seqA);
 
-    seqA = result2.source_last_seq;
+  // test simple replications (not continuous, not filtered), including
+  // conflict creation
+  docs = makeDocs(1, 21);
+  docs.push({
+    _id: "_design/foo",
+    language: "javascript",
+    value: "ddoc"
+  });
 
-    for(test in repTests) {
-      if(repTests[test].afterAB2) repTests[test].afterAB2(dbA, dbB);
+  for (i = 0; i < dbPairs.length; i++) {
+    populateDb(sourceDb, docs);
+    populateDb(targetDb, []);
+
+    // add some attachments
+    for (j = 10; j < 15; j++) {
+      addAtt(sourceDb, docs[j], "readme.txt", att1_data, "text/plain");
     }
 
-    result = CouchDB.replicate(B, A);
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
 
-    T(seqB < result.source_last_seq);
-    T(seqB == result.history[0].start_last_seq);
-    T(result.history[1].end_last_seq == seqB);
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
 
-    seqB = result.source_last_seq;
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
 
-    for(test in repTests) {
-      if(repTests[test].afterBA2) repTests[test].afterBA2(dbA, dbB);
+    TEquals('string', typeof repResult.session_id);
+    TEquals(repResult.source_last_seq, sourceInfo.update_seq);
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(1, repResult.history.length);
+    TEquals(repResult.history[0].session_id, repResult.session_id);
+    TEquals('string', typeof repResult.history[0].start_time);
+    TEquals('string', typeof repResult.history[0].end_time);
+    TEquals(0, repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(sourceInfo.doc_count, repResult.history[0].missing_checked);
+    TEquals(sourceInfo.doc_count, repResult.history[0].missing_found);
+    TEquals(sourceInfo.doc_count, repResult.history[0].docs_read);
+    TEquals(sourceInfo.doc_count, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    for (j = 0; j < docs.length; j++) {
+      doc = docs[j];
+      copy = targetDb.open(doc._id);
+
+      T(copy !== null);
+      TEquals(true, compareObjects(doc, copy));
+
+      if (j >= 10 && j < 15) {
+        var atts = copy._attachments;
+        TEquals('object', typeof atts);
+        TEquals('object', typeof atts["readme.txt"]);
+        TEquals(2, atts["readme.txt"].revpos);
+        TEquals(0, atts["readme.txt"].content_type.indexOf("text/plain"));
+        TEquals(true, atts["readme.txt"].stub);
+
+        var att_copy = CouchDB.request(
+          "GET", "/" + targetDb.name + "/" + copy._id + "/readme.txt"
+        ).responseText;
+        TEquals(att1_data.length, att_copy.length);
+        TEquals(att1_data, att_copy);
+      }
     }
 
-    // do an replication where nothing has changed
-    result2 = CouchDB.replicate(B, A);
-    T(result2.no_changes == true);
-    T(result2.session_id == result.session_id);
-  }
 
-  // test optional automatic creation of the target db
+    // add one more doc to source, more attachments to some existing docs
+    // and replicate again
+    var newDoc = {
+      _id: "foo666",
+      value: "d"
+    };
+    TEquals(true, sourceDb.save(newDoc).ok);
 
-  var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
-  var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
-  
-  dbA.deleteDb();
-  dbA.createDb();
-  dbB.deleteDb();
-  
-  // local
-  CouchDB.replicate(dbA.name, "test_suite_db_b", {
-    body: {"create_target": true}
-  });
-  TEquals("test_suite_db_b", dbB.info().db_name,
-    "Target database should exist");
+    // add some more attachments
+    for (j = 10; j < 15; j++) {
+      addAtt(sourceDb, docs[j], "data.dat", att2_data, "application/binary");
+    }
 
-  // remote
-  dbB.deleteDb();
-  CouchDB.replicate(dbA.name, CouchDB.protocol + CouchDB.host + "/test_suite_db_b", {
-    body: {"create_target": true}
-  });
-  TEquals("test_suite_db_b", dbB.info().db_name,
-    "Target database should exist");
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
 
-  // continuous
-  var continuousResult = CouchDB.replicate(dbA.name, "test_suite_db_b", {
-    body: {"continuous": true}
-  });
-  T(continuousResult.ok);
-  T(continuousResult._local_id);
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
 
-  var cancelResult = CouchDB.replicate(dbA.name, "test_suite_db_b", {
-    body: {"cancel": true}
-  });
-  T(cancelResult.ok);
-  T(continuousResult._local_id == cancelResult._local_id);
+    TEquals(targetInfo.doc_count, sourceInfo.doc_count);
 
+    TEquals('string', typeof repResult.session_id);
+    TEquals(sourceInfo.update_seq, repResult.source_last_seq);
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(2, repResult.history.length);
+    TEquals(repResult.history[0].session_id, repResult.session_id);
+    TEquals('string', typeof repResult.history[0].start_time);
+    TEquals('string', typeof repResult.history[0].end_time);
+    TEquals((sourceInfo.update_seq - 6), repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(6, repResult.history[0].missing_checked);
+    TEquals(6, repResult.history[0].missing_found);
+    TEquals(6, repResult.history[0].docs_read);
+    TEquals(6, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    copy = targetDb.open(newDoc._id);
+    T(copy !== null);
+    TEquals(newDoc._id, copy._id);
+    TEquals(newDoc.value, copy.value);
+
+    for (j = 10; j < 15; j++) {
+      doc = docs[j];
+      copy = targetDb.open(doc._id);
+
+      T(copy !== null);
+      TEquals(true, compareObjects(doc, copy));
+
+      var atts = copy._attachments;
+      TEquals('object', typeof atts);
+      TEquals('object', typeof atts["readme.txt"]);
+      TEquals(2, atts["readme.txt"].revpos);
+      TEquals(0, atts["readme.txt"].content_type.indexOf("text/plain"));
+      TEquals(true, atts["readme.txt"].stub);
+
+      var att1_copy = CouchDB.request(
+        "GET", "/" + targetDb.name + "/" + copy._id + "/readme.txt"
+      ).responseText;
+      TEquals(att1_data.length, att1_copy.length);
+      TEquals(att1_data, att1_copy);
+
+      TEquals('object', typeof atts["data.dat"]);
+      TEquals(3, atts["data.dat"].revpos);
+      TEquals(0, atts["data.dat"].content_type.indexOf("application/binary"));
+      TEquals(true, atts["data.dat"].stub);
+
+      var att2_copy = CouchDB.request(
+        "GET", "/" + targetDb.name + "/" + copy._id + "/data.dat"
+      ).responseText;
+      TEquals(att2_data.length, att2_copy.length);
+      TEquals(att2_data, att2_copy);
+    }
+
+    // test deletion is replicated
+    doc = sourceDb.open(docs[1]._id);
+    TEquals(true, sourceDb.deleteDoc(doc).ok);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(targetInfo.doc_count, sourceInfo.doc_count);
+    TEquals(targetInfo.doc_del_count, sourceInfo.doc_del_count);
+    TEquals(1, targetInfo.doc_del_count);
+
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(3, repResult.history.length);
+    TEquals((sourceInfo.update_seq - 1), repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(1, repResult.history[0].missing_checked);
+    TEquals(1, repResult.history[0].missing_found);
+    TEquals(1, repResult.history[0].docs_read);
+    TEquals(1, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    copy = targetDb.open(docs[1]._id);
+    TEquals(null, copy);
+
+    var changes = targetDb.changes({since: 0});
+    var idx = changes.results.length - 1;
+    TEquals(docs[1]._id, changes.results[idx].id);
+    TEquals(true, changes.results[idx].deleted);
+
+    // test conflict
+    doc = sourceDb.open(docs[0]._id);
+    doc.value = "white";
+    TEquals(true, sourceDb.save(doc).ok);
+
+    copy = targetDb.open(docs[0]._id);
+    copy.value = "black";
+    TEquals(true, targetDb.save(copy).ok);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(4, repResult.history.length);
+    TEquals((sourceInfo.update_seq - 1), repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(1, repResult.history[0].missing_checked);
+    TEquals(1, repResult.history[0].missing_found);
+    TEquals(1, repResult.history[0].docs_read);
+    TEquals(1, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    copy = targetDb.open(docs[0]._id, {conflicts: true});
+
+    TEquals(0, copy._rev.indexOf("2-"));
+    TEquals(true, copy._conflicts instanceof Array);
+    TEquals(1, copy._conflicts.length);
+    TEquals(0, copy._conflicts[0].indexOf("2-"));
+
+    // replicate again with conflict
+    doc.value = "yellow";
+    TEquals(true, sourceDb.save(doc).ok);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(5, repResult.history.length);
+    TEquals((sourceInfo.update_seq - 1), repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(1, repResult.history[0].missing_checked);
+    TEquals(1, repResult.history[0].missing_found);
+    TEquals(1, repResult.history[0].docs_read);
+    TEquals(1, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    copy = targetDb.open(docs[0]._id, {conflicts: true});
+
+    TEquals(0, copy._rev.indexOf("3-"));
+    TEquals(true, copy._conflicts instanceof Array);
+    TEquals(1, copy._conflicts.length);
+    TEquals(0, copy._conflicts[0].indexOf("2-"));
+
+    // resolve the conflict
+    TEquals(true, targetDb.deleteDoc({_id: copy._id, _rev: copy._conflicts[0]}).ok);
+
+    // replicate again, check there are no more conflicts
+    doc.value = "rainbow";
+    TEquals(true, sourceDb.save(doc).ok);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(6, repResult.history.length);
+    TEquals((sourceInfo.update_seq - 1), repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(1, repResult.history[0].missing_checked);
+    TEquals(1, repResult.history[0].missing_found);
+    TEquals(1, repResult.history[0].docs_read);
+    TEquals(1, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    copy = targetDb.open(docs[0]._id, {conflicts: true});
+
+    TEquals(0, copy._rev.indexOf("4-"));
+    TEquals('undefined', typeof copy._conflicts);
+
+    // test that revisions already in a target are not copied
+    TEquals(true, sourceDb.save({_id: "foo1", value: 111}).ok);
+    TEquals(true, targetDb.save({_id: "foo1", value: 111}).ok);
+    TEquals(true, sourceDb.save({_id: "foo2", value: 222}).ok);
+    TEquals(true, sourceDb.save({_id: "foo3", value: 333}).ok);
+    TEquals(true, targetDb.save({_id: "foo3", value: 333}).ok);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    TEquals(sourceInfo.update_seq, repResult.source_last_seq);
+    TEquals(sourceInfo.update_seq - 3, repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(3, repResult.history[0].missing_checked);
+    TEquals(1, repResult.history[0].missing_found);
+    TEquals(1, repResult.history[0].docs_read);
+    TEquals(1, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    TEquals(true, sourceDb.save({_id: "foo4", value: 444}).ok);
+    TEquals(true, targetDb.save({_id: "foo4", value: 444}).ok);
+    TEquals(true, sourceDb.save({_id: "foo5", value: 555}).ok);
+    TEquals(true, targetDb.save({_id: "foo5", value: 555}).ok);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    TEquals(sourceInfo.update_seq, repResult.source_last_seq);
+    TEquals(sourceInfo.update_seq - 2, repResult.history[0].start_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].end_last_seq);
+    TEquals(sourceInfo.update_seq, repResult.history[0].recorded_seq);
+    TEquals(2, repResult.history[0].missing_checked);
+    TEquals(0, repResult.history[0].missing_found);
+    TEquals(0, repResult.history[0].docs_read);
+    TEquals(0, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+    TEquals(true, repResult.no_changes);
+    sourceInfo = sourceDb.info();
+    TEquals(sourceInfo.update_seq, repResult.source_last_seq);
+  }
+
+
+  // test error when source database does not exist
+  try {
+    CouchDB.replicate("foobar", "test_suite_db");
+    T(false, "should have failed with db_not_found error");
+  } catch (x) {
+    TEquals("db_not_found", x.error);
+  }
   try {
-   var cancelResult2 = CouchDB.replicate(dbA.name, "test_suite_db_b", {
-     body: {"cancel": true}
-   });
-  } catch (e) {
-    T(e.error == "not_found");
+    CouchDB.replicate(CouchDB.protocol + host + "/foobar", "test_suite_db");
+    T(false, "should have failed with db_not_found error");
+  } catch (x) {
+    TEquals("db_not_found", x.error);
   }
-  // test replication object option doc_ids
 
-  var dbA = new CouchDB("test_suite_rep_docs_db_a", {"X-Couch-Full-Commit":"false"});
-  var dbB = new CouchDB("test_suite_rep_docs_db_b", {"X-Couch-Full-Commit":"false"});
 
-  dbA.deleteDb();
-  dbA.createDb();
+  // test create_target option
+  docs = makeDocs(1, 2);
 
-  var all_docs = [
-    {
-      _id: "foo1",
-      value: "a"
-    },
-    {
-      _id: "foo2",
-      value: "b"
-    },
-    {
-      _id: "foo3",
-      value: "c"
-    },
-    {
-      _id: "slashed/foo",
-      value: "s"
-    },
-    {
-      _id: "_design/foobar",
-      language: "javascript",
-      value: "I am a design doc",
-      filters: {
-        idfilter: (function(doc, req) {
-          return doc.value == Number(req.filter_value);
-        }).toString()
-      },
-      views: {
-        countview: (function(doc) {
-          emit(doc.value, 1);
-        }).toString()
-      }
-    }
-  ];
+  for (i = 0; i < dbPairs.length; i++) {
+    populateDb(sourceDb, docs);
+    targetDb.deleteDb();
 
-  for (var i = 0; i < all_docs.length; i++) {
-    T(dbA.save(all_docs[i]).ok);
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {body: {create_target: true}}
+    );
+    TEquals(true, repResult.ok);
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+    TEquals(sourceInfo.update_seq, targetInfo.update_seq);
   }
 
-  var dbPairs = [
-    {source:"test_suite_rep_docs_db_a",
-      target:"test_suite_rep_docs_db_b"},
-    {source:"test_suite_rep_docs_db_a",
-      target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"},
-    {source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a",
-      target:"test_suite_rep_docs_db_b"},
-    {source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a",
-      target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"}
-  ];
 
-  var target_doc_ids = [
-    ["foo1", "foo3", "foo666"],
-    ["foo1", "foo666"],
-    ["foo666", "foo2"],
-    ["foo2", "foo9999", "foo1"],
-    ["foo3", "slashed/foo"],
-    ["foo3", "slashed%2Ffoo"],
-    ["foo1", "_design/foobar"],
-    ["foo1", "foo1001", "_design%2Ffoobar"]
-  ];
+  // test filtered replication
+  docs = makeDocs(1, 31);
+  docs.push({
+    _id: "_design/mydesign",
+    language: "javascript",
+    filters: {
+      myfilter: (function(doc, req) {
+        var modulus = Number(req.query.modulus);
+        var special = req.query.special;
+        return (doc.integer % modulus === 0) || (doc.string === special);
+      }).toString()
+    }
+  });
 
-  for (var i = 0; i < dbPairs.length; i++) {
-    var src_db = dbPairs[i].source;
-    var tgt_db = dbPairs[i].target;
-
-    for (var j = 0; j < target_doc_ids.length; j++) {
-      var doc_ids = target_doc_ids[j];
-      var valid_doc_ids = [];
-      var invalid_doc_ids = [];
-
-        for (var p = 0; p < doc_ids.length; p++) {
-            var id = doc_ids[p];
-            var found = false;
-
-            for (var k = 0; k < all_docs.length; k++) {
-                var doc = all_docs[k];
-
-                if (id === doc._id) {
-                    found = true;
-                    break;
-                }
-            }
-
-            if (found) {
-                valid_doc_ids.push(id);
-            } else {
-                invalid_doc_ids.push(id);
-            }
-        };
-
-      dbB.deleteDb();
-      dbB.createDb();
-
-      var repResult = CouchDB.replicate(src_db, tgt_db, {
-        body: {"doc_ids": doc_ids}
-      });
-
-      T(repResult.ok);
-      T(repResult.docs_written === valid_doc_ids.length);
-      T(repResult.docs_read === valid_doc_ids.length);
-      T(repResult.doc_write_failures === 0);
-
-      for (var k = 0; k < all_docs.length; k++) {
-        var doc = all_docs[k];
-        var tgt_doc = dbB.open(doc._id);
-
-        if (doc_ids.indexOf(doc._id) >= 0) {
-          T(tgt_doc !== null);
-          T(tgt_doc.value === doc.value);
-        } else {
-          T(tgt_doc === null);
+  for (i = 0; i < dbPairs.length; i++) {
+    populateDb(sourceDb, docs);
+    populateDb(targetDb, []);
+
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {
+        body: {
+          filter: "mydesign/myfilter",
+          query_params: {
+            modulus: 2,
+            special: "7"
+          }
         }
       }
+    );
 
-      for (var k = 0; k < invalid_doc_ids.length; k++) {
-        var tgt_doc = dbB.open(invalid_doc_ids[k]);
+    TEquals(true, repResult.ok);
 
-        T(tgt_doc === null);
+    for (j = 0; j < docs.length; j++) {
+      doc = docs[j];
+      copy = targetDb.open(doc._id);
+
+      if ((doc.integer && (doc.integer % 2 === 0)) || (doc.string === "7")) {
+
+        T(copy !== null);
+        TEquals(true, compareObjects(doc, copy));
+      } else {
+        TEquals(null, copy);
       }
     }
+
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(1, repResult.history.length);
+    // NOT 31 (31 is db seq for last doc - the ddoc, which was not replicated)
+    TEquals(30, repResult.source_last_seq);
+    TEquals(0, repResult.history[0].start_last_seq);
+    TEquals(30, repResult.history[0].end_last_seq);
+    TEquals(30, repResult.history[0].recorded_seq);
+    // 16 => 15 docs with even integer field  + 1 doc with string field "7"
+    TEquals(16, repResult.history[0].missing_checked);
+    TEquals(16, repResult.history[0].missing_found);
+    TEquals(16, repResult.history[0].docs_read);
+    TEquals(16, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+
+    // add new docs to source and resume the same replication
+    var newDocs = makeDocs(50, 56);
+    populateDb(sourceDb, newDocs, true);
+
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {
+        body: {
+          filter: "mydesign/myfilter",
+          query_params: {
+            modulus: 2,
+            special: "7"
+          }
+        }
+      }
+    );
+
+    TEquals(true, repResult.ok);
+
+    for (j = 0; j < newDocs.length; j++) {
+      doc = newDocs[j];
+      copy = targetDb.open(doc._id);
+
+      if (doc.integer && (doc.integer % 2 === 0)) {
+
+        T(copy !== null);
+        TEquals(true, compareObjects(doc, copy));
+      } else {
+        TEquals(null, copy);
+      }
+    }
+
+    // last doc has even integer field, so last replicated seq is 36
+    TEquals(36, repResult.source_last_seq);
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(2, repResult.history.length);
+    TEquals(30, repResult.history[0].start_last_seq);
+    TEquals(36, repResult.history[0].end_last_seq);
+    TEquals(36, repResult.history[0].recorded_seq);
+    TEquals(3, repResult.history[0].missing_checked);
+    TEquals(3, repResult.history[0].missing_found);
+    TEquals(3, repResult.history[0].docs_read);
+    TEquals(3, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
   }
 
-  // test filtered replication
+
+  // test filtered replication works as expected after changing the filter's
+  // code (ticket COUCHDB-892)
   var filterFun1 = (function(doc, req) {
     if (doc.value < Number(req.query.maxvalue)) {
       return true;
@@ -464,281 +650,771 @@ couchTests.replication = function(debug)
     return true;
   }).toString();
 
-  function wait(ms) {
-    var t0 = new Date(), t1;
-    do {
-      CouchDB.request("GET", "/");
-      t1 = new Date();
-    } while ((t1 - t0) <= ms);
-  }
-
-  var dbPairs = [
-    {source:"test_suite_filtered_rep_db_a",
-      target:"test_suite_filtered_rep_db_b"},
-    {source:"test_suite_filtered_rep_db_a",
-      target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"},
-    {source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a",
-      target:"test_suite_filtered_rep_db_b"},
-    {source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a",
-      target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"}
-  ];
-  var sourceDb = new CouchDB("test_suite_filtered_rep_db_a");
-  var targetDb = new CouchDB("test_suite_filtered_rep_db_b");
-
-
-  for (var i = 0; i < dbPairs.length; i++) {
-    sourceDb.deleteDb();
-    
-    // wait some time to make sure we deleted the db.
-    // fix issue COUCHDB-1002
-    wait(1000);
-    sourceDb.createDb();
-
-    T(sourceDb.save({_id: "foo1", value: 1}).ok);
-    T(sourceDb.save({_id: "foo2", value: 2}).ok);
-    T(sourceDb.save({_id: "foo3", value: 3}).ok);
-    T(sourceDb.save({_id: "foo4", value: 4}).ok);
+  for (i = 0; i < dbPairs.length; i++) {
+    populateDb(targetDb, []);
+    populateDb(sourceDb, []);
+
+    TEquals(true, sourceDb.save({_id: "foo1", value: 1}).ok);
+    TEquals(true, sourceDb.save({_id: "foo2", value: 2}).ok);
+    TEquals(true, sourceDb.save({_id: "foo3", value: 3}).ok);
+    TEquals(true, sourceDb.save({_id: "foo4", value: 4}).ok);
 
     var ddoc = {
       "_id": "_design/mydesign",
       "language": "javascript",
       "filters": {
         "myfilter": filterFun1
-       }
+      }
     };
 
-    T(sourceDb.save(ddoc).ok);
+    TEquals(true, sourceDb.save(ddoc).ok);
 
-    targetDb.deleteDb();
-    targetDb.createDb();
-
-    var dbA = dbPairs[i].source;
-    var dbB = dbPairs[i].target;
-
-    var repResult = CouchDB.replicate(dbA, dbB, {
-      body: {
-        "filter" : "mydesign/myfilter",
-        "query_params" : {
-          "maxvalue": "3"
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {
+        body: {
+          filter: "mydesign/myfilter",
+          query_params: {
+            maxvalue: "3"
+          }
         }
       }
-    });
+    );
 
-    T(repResult.ok);
-    T(repResult.history instanceof Array);
-    T(repResult.history.length === 1);
-    T(repResult.history[0].docs_written === 2);
-    T(repResult.history[0].docs_read === 2);
-    T(repResult.history[0].doc_write_failures === 0);
+    TEquals(true, repResult.ok);
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(1, repResult.history.length);
+    TEquals(2, repResult.history[0].docs_written);
+    TEquals(2, repResult.history[0].docs_read);
+    TEquals(0, repResult.history[0].doc_write_failures);
 
     var docFoo1 = targetDb.open("foo1");
     T(docFoo1 !== null);
-    T(docFoo1.value === 1);
+    TEquals(1, docFoo1.value);
 
     var docFoo2 = targetDb.open("foo2");
     T(docFoo2 !== null);
-    T(docFoo2.value === 2);
+    TEquals(2, docFoo2.value);
 
     var docFoo3 = targetDb.open("foo3");
-    T(docFoo3 === null);
+    TEquals(null, docFoo3);
 
     var docFoo4 = targetDb.open("foo4");
-    T(docFoo4 === null);
+    TEquals(null, docFoo4);
 
     // replication should start from scratch after the filter's code changed
 
     ddoc.filters.myfilter = filterFun2;
-    T(sourceDb.save(ddoc).ok);
+    TEquals(true, sourceDb.save(ddoc).ok);
 
-    repResult = CouchDB.replicate(dbA, dbB, {
-      body: {
-        "filter" : "mydesign/myfilter",
-        "query_params" : {
-          "maxvalue": "3"
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {
+        body: {
+          filter: "mydesign/myfilter",
+          query_params : {
+            maxvalue: "3"
+          }
         }
       }
-    });
+    );
 
-    T(repResult.ok);
-    T(repResult.history instanceof Array);
-    T(repResult.history.length === 1);
-    T(repResult.history[0].docs_written === 3);
-    T(repResult.history[0].docs_read === 3);
-    T(repResult.history[0].doc_write_failures === 0);
+    TEquals(true, repResult.ok);
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(1, repResult.history.length);
+    TEquals(3, repResult.history[0].docs_written);
+    TEquals(3, repResult.history[0].docs_read);
+    TEquals(0, repResult.history[0].doc_write_failures);
 
     docFoo1 = targetDb.open("foo1");
     T(docFoo1 !== null);
-    T(docFoo1.value === 1);
+    TEquals(1, docFoo1.value);
 
     docFoo2 = targetDb.open("foo2");
     T(docFoo2 !== null);
-    T(docFoo2.value === 2);
+    TEquals(2, docFoo2.value);
 
     docFoo3 = targetDb.open("foo3");
     T(docFoo3 !== null);
-    T(docFoo3.value === 3);
+    TEquals(3, docFoo3.value);
 
     docFoo4 = targetDb.open("foo4");
     T(docFoo4 !== null);
-    T(docFoo4.value === 4);
+    TEquals(4, docFoo4.value);
 
     T(targetDb.open("_design/mydesign") !== null);
   }
 
-  // test for COUCHDB-868 - design docs' attachments not getting replicated
-  // when doing a pull replication with HTTP basic auth
-  dbA = new CouchDB("test_suite_db_a");
-  dbB = new CouchDB("test_suite_db_b");
-  var usersDb = new CouchDB("test_suite_auth");
-  var lorem = CouchDB.request(
-    "GET", "/_utils/script/test/lorem.txt").responseText;
-  var lorem_b64 = CouchDB.request(
-    "GET", "/_utils/script/test/lorem_b64.txt").responseText;
 
-  usersDb.deleteDb();
-  usersDb.createDb();
-  dbA.deleteDb();
-  dbA.createDb();
-  dbB.deleteDb();
-  dbB.createDb();
+  // test replication by doc IDs
+  docs = makeDocs(1, 11);
+  docs.push({
+    _id: "_design/foo",
+    language: "javascript",
+    integer: 1
+  });
 
-  var atts_ddoc = {
-    _id: "_design/i_have_atts",
-    language: "javascript"
-  };
-  T(dbA.save(atts_ddoc).ok);
+  var target_doc_ids = [
+    { initial: ["1", "2", "10"], after: [], conflict_id: "2" },
+    { initial: ["1", "2"], after: ["7"], conflict_id: "1" },
+    { initial: ["1", "foo_666", "10"], after: ["7"], conflict_id: "10" },
+    { initial: ["_design/foo", "8"], after: ["foo_5"], conflict_id: "8" },
+    { initial: ["_design%2Ffoo", "8"], after: ["foo_5"], conflict_id: "8" },
+    { initial: [], after: ["foo_1000", "_design/foo", "1"], conflict_id: "1" }
+  ];
+  var doc_ids, after_doc_ids;
+  var id, num_inexistent_docs, after_num_inexistent_docs;
+  var total, after_total;
+
+  for (i = 0; i < dbPairs.length; i++) {
+
+    for (j = 0; j < target_doc_ids.length; j++) {
+      doc_ids = target_doc_ids[j].initial;
+      num_inexistent_docs = 0;
+
+      for (k = 0; k < doc_ids.length; k++) {
+        id = doc_ids[k];
+        if (id.indexOf("foo_") === 0) {
+          num_inexistent_docs += 1;
+        }
+      }
 
-  var rev = atts_ddoc._rev;
-  var att_1_name = "lorem.txt";
-  var att_2_name = "lorem.dat";
-  var xhr = CouchDB.request(
-    "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_1_name + "?rev=" + rev, {
-      headers: {"Content-Type": "text/plain;charset=utf-8"},
-      body: lorem
-  });
-  rev = JSON.parse(xhr.responseText).rev;
-  T(xhr.status === 201);
-  xhr = CouchDB.request(
-    "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_2_name + "?rev=" + rev, {
-      headers: {"Content-Type": "application/data"},
-      body: lorem_b64
+      populateDb(sourceDb, docs);
+      populateDb(targetDb, []);
+
+      repResult = CouchDB.replicate(
+        dbPairs[i].source,
+        dbPairs[i].target,
+        {
+          body: {
+            doc_ids: doc_ids
+          }
+        }
+      );
+
+      total = doc_ids.length - num_inexistent_docs;
+      TEquals(true, repResult.ok);
+      if (total === 0) {
+        TEquals(true, repResult.no_changes);
+      } else {
+        TEquals('string', typeof repResult.start_time);
+        TEquals('string', typeof repResult.end_time);
+        TEquals(total, repResult.docs_read);
+        TEquals(total, repResult.docs_written);
+        TEquals(0, repResult.doc_write_failures);
+      }
+
+      for (k = 0; k < doc_ids.length; k++) {
+        id = decodeURIComponent(doc_ids[k]);
+        doc = sourceDb.open(id);
+        copy = targetDb.open(id);
+
+        if (id.indexOf("foo_") === 0) {
+          TEquals(null, doc);
+          TEquals(null, copy);
+        } else {
+          T(doc !== null);
+          T(copy !== null);
+          TEquals(true, compareObjects(doc, copy));
+        }
+      }
+
+      // be absolutely sure that other docs were not replicated
+      for (k = 0; k < docs.length; k++) {
+        var base_id = docs[k]._id;
+        id = encodeURIComponent(base_id);
+        doc = targetDb.open(base_id);
+
+        if ((doc_ids.indexOf(id) >= 0) || (doc_ids.indexOf(base_id) >= 0)) {
+            T(doc !== null);
+        } else {
+            TEquals(null, doc);
+        }
+      }
+
+      targetInfo = targetDb.info();
+      TEquals(total, targetInfo.doc_count);
+
+
+      // add more docs throught replication by doc IDs
+      after_doc_ids = target_doc_ids[j].after;
+      after_num_inexistent_docs = 0;
+
+      for (k = 0; k < after_doc_ids.length; k++) {
+        id = after_doc_ids[k];
+        if (id.indexOf("foo_") === 0) {
+          after_num_inexistent_docs += 1;
+        }
+      }
+
+      repResult = CouchDB.replicate(
+        dbPairs[i].source,
+        dbPairs[i].target,
+        {
+          body: {
+            doc_ids: after_doc_ids
+          }
+        }
+      );
+
+      after_total = after_doc_ids.length - after_num_inexistent_docs;
+      TEquals(true, repResult.ok);
+      if (after_total === 0) {
+        TEquals(true, repResult.no_changes);
+      } else {
+        TEquals('string', typeof repResult.start_time);
+        TEquals('string', typeof repResult.end_time);
+        TEquals(after_total, repResult.docs_read);
+        TEquals(after_total, repResult.docs_written);
+        TEquals(0, repResult.doc_write_failures);
+      }
+
+      for (k = 0; k < after_doc_ids.length; k++) {
+        id = after_doc_ids[k];
+        doc = sourceDb.open(id);
+        copy = targetDb.open(id);
+
+        if (id.indexOf("foo_") === 0) {
+          TEquals(null, doc);
+          TEquals(null, copy);
+        } else {
+          T(doc !== null);
+          T(copy !== null);
+          TEquals(true, compareObjects(doc, copy));
+        }
+      }
+
+      // be absolutely sure that other docs were not replicated
+      for (k = 0; k < docs.length; k++) {
+        var base_id = docs[k]._id;
+        id = encodeURIComponent(base_id);
+        doc = targetDb.open(base_id);
+
+        if ((doc_ids.indexOf(id) >= 0) || (after_doc_ids.indexOf(id) >= 0) ||
+            (doc_ids.indexOf(base_id) >= 0) ||
+            (after_doc_ids.indexOf(base_id) >= 0)) {
+            T(doc !== null);
+        } else {
+            TEquals(null, doc);
+        }
+      }
+
+      targetInfo = targetDb.info();
+      TEquals((total + after_total), targetInfo.doc_count);
+
+
+      // replicate again the same doc after updated on source (no conflict)
+      id = target_doc_ids[j].conflict_id;
+      doc = sourceDb.open(id);
+      T(doc !== null);
+      doc.integer = 666;
+      TEquals(true, sourceDb.save(doc).ok);
+      addAtt(sourceDb, doc, "readme.txt", att1_data, "text/plain");
+      addAtt(sourceDb, doc, "data.dat", att2_data, "application/binary");
+
+      repResult = CouchDB.replicate(
+        dbPairs[i].source,
+        dbPairs[i].target,
+        {
+          body: {
+            doc_ids: [id]
+          }
+        }
+      );
+
+      TEquals(true, repResult.ok);
+      TEquals(1, repResult.docs_read);
+      TEquals(1, repResult.docs_written);
+      TEquals(0, repResult.doc_write_failures);
+
+      copy = targetDb.open(id, {conflicts: true});
+
+      TEquals(666, copy.integer);
+      TEquals(0, copy._rev.indexOf("4-"));
+      TEquals('undefined', typeof copy._conflicts);
+
+      var atts = copy._attachments;
+      TEquals('object', typeof atts);
+      TEquals('object', typeof atts["readme.txt"]);
+      TEquals(3, atts["readme.txt"].revpos);
+      TEquals(0, atts["readme.txt"].content_type.indexOf("text/plain"));
+      TEquals(true, atts["readme.txt"].stub);
+
+      var att1_copy = CouchDB.request(
+        "GET", "/" + targetDb.name + "/" + copy._id + "/readme.txt"
+      ).responseText;
+      TEquals(att1_data.length, att1_copy.length);
+      TEquals(att1_data, att1_copy);
+
+      TEquals('object', typeof atts["data.dat"]);
+      TEquals(4, atts["data.dat"].revpos);
+      TEquals(0, atts["data.dat"].content_type.indexOf("application/binary"));
+      TEquals(true, atts["data.dat"].stub);
+
+      var att2_copy = CouchDB.request(
+        "GET", "/" + targetDb.name + "/" + copy._id + "/data.dat"
+      ).responseText;
+      TEquals(att2_data.length, att2_copy.length);
+      TEquals(att2_data, att2_copy);
+
+
+      // generate a conflict throught replication by doc IDs
+      id = target_doc_ids[j].conflict_id;
+      doc = sourceDb.open(id);
+      copy = targetDb.open(id);
+      T(doc !== null);
+      T(copy !== null);
+      doc.integer += 100;
+      copy.integer += 1;
+      TEquals(true, sourceDb.save(doc).ok);
+      TEquals(true, targetDb.save(copy).ok);
+
+      repResult = CouchDB.replicate(
+        dbPairs[i].source,
+        dbPairs[i].target,
+        {
+          body: {
+            doc_ids: [id]
+          }
+        }
+      );
+
+      TEquals(true, repResult.ok);
+      TEquals(1, repResult.docs_read);
+      TEquals(1, repResult.docs_written);
+      TEquals(0, repResult.doc_write_failures);
+
+      copy = targetDb.open(id, {conflicts: true});
+
+      TEquals(0, copy._rev.indexOf("5-"));
+      TEquals(true, copy._conflicts instanceof Array);
+      TEquals(1, copy._conflicts.length);
+      TEquals(0, copy._conflicts[0].indexOf("5-"));
+    }
+  }
+
+
+  docs = makeDocs(1, 25);
+  docs.push({
+    _id: "_design/foo",
+    language: "javascript"
   });
-  T(xhr.status === 201);
 
-  var fdmananaUserDoc = CouchDB.prepareUserDoc({
-    name: "fdmanana",
-    roles: ["reader"]
-  }, "qwerty");
-  T(usersDb.save(fdmananaUserDoc).ok);
-
-  T(dbA.setSecObj({
-    admins: {
-      names: [],
-      roles: ["admin"]
-    },
-    members: {
-      names: [],
-      roles: ["reader"]
-    }
-  }).ok);
-  T(dbB.setSecObj({
-    admins: {
-      names: ["fdmanana"],
-      roles: []
+  for (i = 0; i < dbPairs.length; i++) {
+    populateDb(sourceDb, docs);
+    populateDb(targetDb, []);
+
+    // add some attachments
+    for (j = 10; j < 15; j++) {
+      addAtt(sourceDb, docs[j], "readme.txt", att1_data, "text/plain");
+    }
+
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {
+        body: {
+          continuous: true
+        }
+      }
+    );
+    TEquals(true, repResult.ok);
+    TEquals('string', typeof repResult._local_id);
+
+    var rep_id = repResult._local_id;
+
+    waitForSeq(sourceDb, targetDb);
+
+    for (j = 0; j < docs.length; j++) {
+      doc = docs[j];
+      copy = targetDb.open(doc._id);
+
+      T(copy !== null);
+      TEquals(true, compareObjects(doc, copy));
+
+      if (j >= 10 && j < 15) {
+        var atts = copy._attachments;
+        TEquals('object', typeof atts);
+        TEquals('object', typeof atts["readme.txt"]);
+        TEquals(2, atts["readme.txt"].revpos);
+        TEquals(0, atts["readme.txt"].content_type.indexOf("text/plain"));
+        TEquals(true, atts["readme.txt"].stub);
+
+        var att_copy = CouchDB.request(
+          "GET", "/" + targetDb.name + "/" + copy._id + "/readme.txt"
+        ).responseText;
+        TEquals(att1_data.length, att_copy.length);
+        TEquals(att1_data, att_copy);
+      }
+    }
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+    // add attachments to docs in source
+    for (j = 10; j < 15; j++) {
+      addAtt(sourceDb, docs[j], "data.dat", att2_data, "application/binary");
     }
-  }).ok);
 
+    var ddoc = docs[docs.length - 1]; // design doc
+    addAtt(sourceDb, ddoc, "readme.txt", att1_data, "text/plain");
+
+    waitForSeq(sourceDb, targetDb);
+
+    var modifDocs = docs.slice(10, 15).concat([ddoc]);
+    for (j = 0; j < modifDocs.length; j++) {
+      doc = modifDocs[j];
+      copy = targetDb.open(doc._id);
+
+      T(copy !== null);
+      TEquals(true, compareObjects(doc, copy));
+
+      var atts = copy._attachments;
+      TEquals('object', typeof atts);
+      TEquals('object', typeof atts["readme.txt"]);
+      TEquals(2, atts["readme.txt"].revpos);
+      TEquals(0, atts["readme.txt"].content_type.indexOf("text/plain"));
+      TEquals(true, atts["readme.txt"].stub);
+
+      var att1_copy = CouchDB.request(
+        "GET", "/" + targetDb.name + "/" + copy._id + "/readme.txt"
+      ).responseText;
+      TEquals(att1_data.length, att1_copy.length);
+      TEquals(att1_data, att1_copy);
+
+      if (doc._id.indexOf("_design/") === -1) {
+        TEquals('object', typeof atts["data.dat"]);
+        TEquals(3, atts["data.dat"].revpos);
+        TEquals(0, atts["data.dat"].content_type.indexOf("application/binary"));
+        TEquals(true, atts["data.dat"].stub);
+
+        var att2_copy = CouchDB.request(
+          "GET", "/" + targetDb.name + "/" + copy._id + "/data.dat"
+        ).responseText;
+        TEquals(att2_data.length, att2_copy.length);
+        TEquals(att2_data, att2_copy);
+      }
+    }
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+    // add another attachment to the ddoc on source
+    addAtt(sourceDb, ddoc, "data.dat", att2_data, "application/binary");
+
+    waitForSeq(sourceDb, targetDb);
+
+    copy = targetDb.open(ddoc._id);
+    var atts = copy._attachments;
+    TEquals('object', typeof atts);
+    TEquals('object', typeof atts["readme.txt"]);
+    TEquals(2, atts["readme.txt"].revpos);
+    TEquals(0, atts["readme.txt"].content_type.indexOf("text/plain"));
+    TEquals(true, atts["readme.txt"].stub);
+
+    var att1_copy = CouchDB.request(
+      "GET", "/" + targetDb.name + "/" + copy._id + "/readme.txt"
+    ).responseText;
+    TEquals(att1_data.length, att1_copy.length);
+    TEquals(att1_data, att1_copy);
+
+    TEquals('object', typeof atts["data.dat"]);
+    TEquals(3, atts["data.dat"].revpos);
+    TEquals(0, atts["data.dat"].content_type.indexOf("application/binary"));
+    TEquals(true, atts["data.dat"].stub);
+
+    var att2_copy = CouchDB.request(
+      "GET", "/" + targetDb.name + "/" + copy._id + "/data.dat"
+    ).responseText;
+    TEquals(att2_data.length, att2_copy.length);
+    TEquals(att2_data, att2_copy);
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+
+    // add more docs to source
+    var newDocs = makeDocs(25, 35);
+    populateDb(sourceDb, newDocs, true);
+
+    waitForSeq(sourceDb, targetDb);
+
+    for (j = 0; j < newDocs.length; j++) {
+      doc = newDocs[j];
+      copy = targetDb.open(doc._id);
+
+      T(copy !== null);
+      TEquals(true, compareObjects(doc, copy));
+    }
+
+    sourceInfo = sourceDb.info();
+    targetInfo = targetDb.info();
+
+    TEquals(sourceInfo.doc_count, targetInfo.doc_count);
+
+    // delete docs from source
+    TEquals(true, sourceDb.deleteDoc(newDocs[0]).ok);
+    TEquals(true, sourceDb.deleteDoc(newDocs[6]).ok);
+
+    waitForSeq(sourceDb, targetDb);
+
+    copy = targetDb.open(newDocs[0]._id);
+    TEquals(null, copy);
+    copy = targetDb.open(newDocs[6]._id);
+    TEquals(null, copy);
+
+    var changes = targetDb.changes({since: targetInfo.update_seq});
+    var line1 = changes.results[changes.results.length - 2];
+    var line2 = changes.results[changes.results.length - 1];
+    TEquals(newDocs[0]._id, line1.id);
+    TEquals(true, line1.deleted);
+    TEquals(newDocs[6]._id, line2.id);
+    TEquals(true, line2.deleted);
+
+    // cancel the replication
+    repResult = CouchDB.replicate(
+      dbPairs[i].source,
+      dbPairs[i].target,
+      {
+        body: {
+          continuous: true,
+          cancel: true
+        }
+      }
+    );
+    TEquals(true, repResult.ok);
+    TEquals(rep_id, repResult._local_id);
+
+    doc = {
+      _id: 'foobar',
+      value: 666
+    };
+    TEquals(true, sourceDb.save(doc).ok);
+
+    wait(2000);
+    copy = targetDb.open(doc._id);
+    TEquals(null, copy);
+  }
+
+
+  //
+  // test replication of compressed attachments
+  //
+  doc = {
+    _id: "foobar"
+  };
+  var bigTextAtt = makeAttData(128 * 1024);
+  var attName = "readme.txt";
+  var xhr = CouchDB.request("GET", "/_config/attachments/compression_level");
+  var compressionLevel = JSON.parse(xhr.responseText);
+  xhr = CouchDB.request("GET", "/_config/attachments/compressible_types");
+  var compressibleTypes = JSON.parse(xhr.responseText);
+
+  for (i = 0; i < dbPairs.length; i++) {
+    populateDb(sourceDb, [doc]);
+    populateDb(targetDb, []);
+
+    // enable compression of text types
+    enableAttCompression("8", "text/*");
+
+    // add text attachment to foobar doc
+    xhr = CouchDB.request(
+      "PUT",
+      "/" + sourceDb.name + "/" + doc._id + "/" + attName + "?rev=" + doc._rev,
+      {
+        body: bigTextAtt,
+        headers: {"Content-Type": "text/plain"}
+      }
+    );
+    TEquals(201, xhr.status);
+
+    // disable compression and replicate
+    disableAttCompression();
+
+    repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+    TEquals(true, repResult.ok);
+    TEquals(true, repResult.history instanceof Array);
+    TEquals(1, repResult.history.length);
+    TEquals(1, repResult.history[0].missing_checked);
+    TEquals(1, repResult.history[0].missing_found);
+    TEquals(1, repResult.history[0].docs_read);
+    TEquals(1, repResult.history[0].docs_written);
+    TEquals(0, repResult.history[0].doc_write_failures);
+
+    copy = targetDb.open(
+      doc._id,
+      {att_encoding_info: true, bypass_cache: Math.round(Math.random() * 1000)}
+    );
+    T(copy !== null);
+    T(attName in copy._attachments);
+    TEquals("gzip", copy._attachments[attName].encoding);
+    TEquals("number", typeof copy._attachments[attName].length);
+    TEquals("number", typeof copy._attachments[attName].encoded_length);
+    T(copy._attachments[attName].encoded_length < copy._attachments[attName].length);
+  }
+
+  delete bigTextAtt;
+  // restore original settings
+  enableAttCompression(compressionLevel, compressibleTypes);
+
+
+  //
+  // test replication triggered by non admins
+  //
+
+  // case 1) user triggering the replication is not a DB admin of the target DB
+  var joeUserDoc = CouchDB.prepareUserDoc({
+    name: "joe",
+    roles: ["erlanger"]
+  }, "erly");
+  var usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"});
   var server_config = [
     {
       section: "couch_httpd_auth",
       key: "authentication_db",
       value: usersDb.name
-    },
-    // to prevent admin party mode
-    {
-      section: "admins",
-      key: "joe",
-      value: "erlang"
     }
   ];
 
-  var test_fun = function() {
-    T(CouchDB.login("fdmanana", "qwerty").ok);
-    T(CouchDB.session().userCtx.name === "fdmanana");
-    T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1);
-
-    var repResult = CouchDB.replicate(
-      CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name,
-      dbB.name
-    );
-    T(repResult.ok === true);
-    T(repResult.history instanceof Array);
-    T(repResult.history.length === 1);
-    T(repResult.history[0].docs_written === 1);
-    T(repResult.history[0].docs_read === 1);
-    T(repResult.history[0].doc_write_failures === 0);
-
-    var atts_ddoc_copy = dbB.open(atts_ddoc._id);
-    T(atts_ddoc_copy !== null);
-    T(typeof atts_ddoc_copy._attachments === "object");
-    T(atts_ddoc_copy._attachments !== null);
-    T(att_1_name in atts_ddoc_copy._attachments);
-    T(att_2_name in atts_ddoc_copy._attachments);
+  docs = makeDocs(1, 6);
+  docs.push({
+    _id: "_design/foo",
+    language: "javascript"
+  });
 
-    var xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_1_name);
-    T(xhr.status === 200);
-    T(xhr.responseText === lorem);
+  dbPairs = [
+    {
+      source: sourceDb.name,
+      target: targetDb.name
+    },
+    {
+      source: CouchDB.protocol + host + "/" + sourceDb.name,
+      target: targetDb.name
+    },
+    {
+      source: sourceDb.name,
+      target: CouchDB.protocol + "joe:erly@" + host + "/" + targetDb.name
+    },
+    {
+      source: CouchDB.protocol + host + "/" + sourceDb.name,
+      target: CouchDB.protocol + "joe:erly@" + host + "/" + targetDb.name
+    }
+  ];
 
-    xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_2_name);
-    T(xhr.status === 200);
-    T(xhr.responseText === lorem_b64);
+  for (i = 0; i < dbPairs.length; i++) {
+    usersDb.deleteDb();
+    populateDb(sourceDb, docs);
+    populateDb(targetDb, []);
 
-    CouchDB.logout();
-    T(CouchDB.login("joe", "erlang").ok);
-    T(dbA.setSecObj({
+    TEquals(true, targetDb.setSecObj({
       admins: {
-        names: [],
-        roles: ["bar"]
-      },
-      members: {
-        names: [],
-        roles: ["foo"]
+        names: ["superman"],
+        roles: ["god"]
       }
     }).ok);
-    T(dbB.deleteDb().ok === true);
-    T(dbB.createDb().ok === true);
-    T(dbB.setSecObj({
+
+    run_on_modified_server(server_config, function() {
+      delete joeUserDoc._rev;
+      TEquals(true, usersDb.save(joeUserDoc).ok);
+
+      TEquals(true, CouchDB.login("joe", "erly").ok);
+      TEquals('joe', CouchDB.session().userCtx.name);
+
+      repResult = CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+
+      TEquals(true, CouchDB.logout().ok);
+
+      TEquals(true, repResult.ok);
+      TEquals(docs.length, repResult.history[0].docs_read);
+      TEquals((docs.length - 1), repResult.history[0].docs_written); // 1 ddoc
+      TEquals(1, repResult.history[0].doc_write_failures);
+    });
+
+    for (j = 0; j < docs.length; j++) {
+      doc = docs[j];
+      copy = targetDb.open(doc._id);
+
+      if (doc._id.indexOf("_design/") === 0) {
+        TEquals(null, copy);
+      } else {
+        T(copy !== null);
+        TEquals(true, compareObjects(doc, copy));
+      }
+    }
+  }
+
+  // case 2) user triggering the replication is not a reader (nor admin) of the
+  //         source DB
+  dbPairs = [
+    {
+      source: sourceDb.name,
+      target: targetDb.name
+    },
+    {
+      source: CouchDB.protocol + "joe:erly@" + host + "/" + sourceDb.name,
+      target: targetDb.name
+    },
+    {
+      source: sourceDb.name,
+      target: CouchDB.protocol + host + "/" + targetDb.name
+    },
+    {
+      source: CouchDB.protocol + "joe:erly@" + host + "/" + sourceDb.name,
+      target: CouchDB.protocol + host + "/" + targetDb.name
+    }
+  ];
+
+  for (i = 0; i < dbPairs.length; i++) {
+    usersDb.deleteDb();
+    populateDb(sourceDb, docs);
+    populateDb(targetDb, []);
+
+    TEquals(true, sourceDb.setSecObj({
       admins: {
-        names: ["fdmanana"],
-        roles: []
+        names: ["superman"],
+        roles: ["god"]
+      },
+      readers: {
+        names: ["john"],
+        roles: ["secret"]
       }
     }).ok);
-    CouchDB.logout();
 
-    T(CouchDB.login("fdmanana", "qwerty").ok);
-    T(CouchDB.session().userCtx.name === "fdmanana");
-    T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1);
-    try {
-      repResult = CouchDB.replicate(
-        CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name,
-        dbB.name
-      );
-      T(false, "replication should have failed");
-    } catch(x) {
-      T(x.error === "unauthorized");
-    }
+    run_on_modified_server(server_config, function() {
+      delete joeUserDoc._rev;
+      TEquals(true, usersDb.save(joeUserDoc).ok);
+
+      TEquals(true, CouchDB.login("joe", "erly").ok);
+      TEquals('joe', CouchDB.session().userCtx.name);
+
+      try {
+        CouchDB.replicate(dbPairs[i].source, dbPairs[i].target);
+        T(false, "should have raised an exception");
+      } catch (x) {
+        TEquals("unauthorized", x.error);
+      }
 
-    atts_ddoc_copy = dbB.open(atts_ddoc._id);
-    T(atts_ddoc_copy === null);
+      TEquals(true, CouchDB.logout().ok);
+    });
 
-    CouchDB.logout();
-    T(CouchDB.login("joe", "erlang").ok);
-  };
+    for (j = 0; j < docs.length; j++) {
+      doc = docs[j];
+      copy = targetDb.open(doc._id);
+      TEquals(null, copy);
+    }
+  }
 
-  run_on_modified_server(server_config, test_fun);
 
   // cleanup
-  dbA.deleteDb();
-  dbB.deleteDb();
   usersDb.deleteDb();
+  sourceDb.deleteDb();
+  targetDb.deleteDb();
 };

Modified: couchdb/trunk/src/couchdb/Makefile.am
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/Makefile.am?rev=1071375&r1=1071374&r2=1071375&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/Makefile.am (original)
+++ couchdb/trunk/src/couchdb/Makefile.am Wed Feb 16 20:05:31 2011
@@ -17,7 +17,7 @@ couchlibdir = $(localerlanglibdir)/couch
 couchincludedir = $(couchlibdir)/include
 couchebindir = $(couchlibdir)/ebin
 
-couchinclude_DATA = couch_db.hrl couch_js_functions.hrl
+couchinclude_DATA = couch_api_wrap.hrl couch_db.hrl couch_js_functions.hrl couch_replicator.hrl
 couchebin_DATA = $(compiled_files)
 
 # dist_devdoc_DATA = $(doc_base) $(doc_modules)
@@ -28,6 +28,8 @@ CLEANFILES = $(compiled_files) $(doc_bas
 
 source_files = \
     couch.erl \
+    couch_api_wrap.erl \
+    couch_api_wrap_httpc.erl \
     couch_app.erl \
     couch_auth_cache.erl \
     couch_btree.erl \
@@ -43,6 +45,7 @@ source_files = \
     couch_external_manager.erl \
     couch_external_server.erl \
     couch_file.erl \
+    couch_httpc_pool.erl \
     couch_httpd.erl \
     couch_httpd_db.erl \
     couch_httpd_auth.erl \
@@ -52,6 +55,7 @@ source_files = \
     couch_httpd_view.erl \
     couch_httpd_misc_handlers.erl \
     couch_httpd_proxy.erl \
+    couch_httpd_replicator.erl \
     couch_httpd_rewrite.erl \
     couch_httpd_stats_handlers.erl \
     couch_httpd_vhost.erl \
@@ -63,15 +67,13 @@ source_files = \
     couch_primary_sup.erl \
     couch_query_servers.erl \
     couch_ref_counter.erl \
-    couch_rep.erl \
-    couch_rep_att.erl \
-    couch_rep_changes_feed.erl \
-    couch_rep_httpc.erl \
-    couch_rep_missing_revs.erl \
-    couch_rep_reader.erl \
-    couch_rep_sup.erl \
-    couch_rep_writer.erl \
     couch_rep_db_listener.erl \
+    couch_rep_sup.erl \
+    couch_replication_notifier.erl \
+    couch_replicator.erl \
+    couch_replicator_doc_copier.erl \
+    couch_replicator_rev_finder.erl \
+    couch_replicator_utils.erl \
     couch_secondary_sup.erl \
     couch_server.erl \
     couch_server_sup.erl \
@@ -86,13 +88,16 @@ source_files = \
     couch_view_updater.erl \
     couch_view_group.erl \
     couch_db_updater.erl \
-    couch_work_queue.erl
+    couch_work_queue.erl \
+    json_stream_parse.erl
 
-EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl
+EXTRA_DIST = $(source_files) couch_api_wrap.hrl couch_db.hrl couch_js_functions.hrl couch_replicator.hrl
 
 compiled_files = \
     couch.app \
     couch.beam \
+    couch_api_wrap.beam \
+    couch_api_wrap_httpc.beam \
     couch_app.beam \
     couch_auth_cache.beam \
     couch_btree.beam \
@@ -108,6 +113,7 @@ compiled_files = \
     couch_external_manager.beam \
     couch_external_server.beam \
     couch_file.beam \
+    couch_httpc_pool.beam \
     couch_httpd.beam \
     couch_httpd_db.beam \
     couch_httpd_auth.beam \
@@ -117,6 +123,7 @@ compiled_files = \
     couch_httpd_show.beam \
     couch_httpd_view.beam \
     couch_httpd_misc_handlers.beam \
+    couch_httpd_replicator.beam \
     couch_httpd_rewrite.beam \
     couch_httpd_stats_handlers.beam \
     couch_httpd_vhost.beam \
@@ -128,15 +135,13 @@ compiled_files = \
     couch_primary_sup.beam \
     couch_query_servers.beam \
     couch_ref_counter.beam \
-    couch_rep.beam \
-    couch_rep_att.beam \
-    couch_rep_changes_feed.beam \
-    couch_rep_httpc.beam \
-    couch_rep_missing_revs.beam \
-    couch_rep_reader.beam \
-    couch_rep_sup.beam \
-    couch_rep_writer.beam \
     couch_rep_db_listener.beam \
+    couch_rep_sup.beam \
+    couch_replication_notifier.beam \
+    couch_replicator.beam \
+    couch_replicator_doc_copier.beam \
+    couch_replicator_rev_finder.beam \
+    couch_replicator_utils.beam \
     couch_secondary_sup.beam \
     couch_server.beam \
     couch_server_sup.beam \
@@ -151,7 +156,8 @@ compiled_files = \
     couch_view_updater.beam \
     couch_view_group.beam \
     couch_db_updater.beam \
-    couch_work_queue.beam
+    couch_work_queue.beam \
+    json_stream_parse.beam
 
 # doc_base = \
 #     erlang.png \
@@ -210,6 +216,6 @@ endif
 
 # $(ERL) -noshell -run edoc_run files [\"$<\"]
 
-%.beam: %.erl couch_db.hrl couch_js_functions.hrl
+%.beam: %.erl couch_api_wrap.hrl couch_db.hrl couch_js_functions.hrl couch_replicator.hrl
 	$(ERLC) $(ERLC_FLAGS) ${TEST} $<;
 



Re: svn commit: r1071375 [1/3] - in /couchdb/trunk: etc/couchdb/ share/www/script/test/ src/couchdb/ test/etap/

Posted by Jan Lehnardt <ja...@apache.org>.
On 16 Feb 2011, at 21:05, fdmanana@apache.org wrote:

> Author: fdmanana
> Date: Wed Feb 16 20:05:31 2011
> New Revision: 1071375
> 
> URL: http://svn.apache.org/viewvc?rev=1071375&view=rev
> Log:
> Added the new replicator implementation

\o/

Jan
--