text: // Licensed under the Apache License, Version 2.0 (the "License"); you may not
  // use this file except in compliance with the License. You may obtain a copy of
  // the License at
  //
  //   http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  // License for the specific language governing permissions and limitations under
  // the License.
  
  couchTests.replication = function(debug) {
  //  return console.log('TODO');
    if (debug) debugger;
  
    var host = CouchDB.host;
    // as we change names during execution, do NOT use test_suite_db or a 
    // pre-computed value like ''+sourceDb.name (compute only on use)
    var sourceDb;
    var targetDb;
  
    var dbPairsPrefixes = [
      {
        source: "",
        target: ""
      },
      {
        source: CouchDB.protocol + host + "/",
        target: ""
      },
      {
        source: "",
        target: CouchDB.protocol + host + "/"
      },
      {
        source: CouchDB.protocol + host + "/",
        target: CouchDB.protocol + host + "/"
      }
    ];
  
    var att1_data = CouchDB.request("GET", "/_utils/script/test/lorem.txt");
    att1_data = att1_data.responseText;
  
    var att2_data = CouchDB.request("GET", "/_utils/script/test/lorem_b64.txt");
    att2_data = att2_data.responseText;
  
    var sourceInfo, targetInfo;
    var docs, doc, copy;
    var repResult;
    var i, j, k;
  
  
    function makeAttData(minSize) {
      var data = att1_data;
  
      while (data.length < minSize) {
        data = data + att1_data;
      }
      return data;
    }
  
    
    function runAllNodes(callback) {
      // new and fancy: clustered version: pull cluster_members and walk over all of them
      var xhr = CouchDB.request("GET", "/_membership"); 
      T(xhr.status === 200);
      JSON.parse(xhr.responseText).cluster_nodes.forEach(callback);
    }
  
    function runFirstNode(callback) {
      // new and fancy: clustered version: pull cluster_members and walk over all of them
      var xhr = CouchDB.request("GET", "/_membership"); 
      T(xhr.status === 200);
      var node = JSON.parse(xhr.responseText).cluster_nodes[0];
      return callback(node);
    }
  
    function getCompressionInfo() {
      return runFirstNode(function(node) {
        var xhr = CouchDB.request(
          "GET",
          "_node/" + node + "/_config/attachments"
        );
        T(xhr.status === 200);
        var res = JSON.parse(xhr.responseText);
        return {"level": res.compression_level, "types": res.compressible_types};
      });
    }
  
    function enableAttCompression(level, types) {
      runAllNodes(function(node) {
        var xhr = CouchDB.request(
          "PUT",
          "_node/" + node + "/_config/attachments/compression_level",
          {
            body: JSON.stringify(level),
            headers: {"X-Couch-Persist": "false"}
          }
        );
        T(xhr.status === 200);
        xhr = CouchDB.request(
          "PUT",
          "_node/" + node + "/_config/attachments/compressible_types",
          {
            body: JSON.stringify(types),
            headers: {"X-Couch-Persist": "false"}
          }
        );
        T(xhr.status === 200);
      });
    }
  
    function disableAttCompression() {
      runAllNodes(function(node) {
        var xhr = CouchDB.request(
          "PUT",
          "_node/" + node + "/_config/attachments/compression_level",
          {
            body: JSON.stringify("0"),
            headers: {"X-Couch-Persist": "false"}
          }
        );
        T(xhr.status === 200);
      });
    }
  
  
    function populateSourceDb(docs, dontRecreateDb) {
      if(dontRecreateDb !== true) {
        if(sourceDb) {
          sourceDb.deleteDb();
        }
        sourceDb = new CouchDB(get_random_db_name() + "_src",{"X-Couch-Full-Commit":"false"});
        sourceDb.createDb(); 
      }
      for (var i = 0; i < docs.length; i++) {
        var doc = docs[i];
        delete doc._rev;
      }
      if (docs.length > 0) {
        sourceDb.bulkSave(docs);
      }
    }
    function populateTargetDb(docs, dontRecreateDb) {
      if(dontRecreateDb !== true) {
        if(targetDb) {
          targetDb.deleteDb();
        }
        targetDb = new CouchDB(get_random_db_name() + "_tgt",{"X-Couch-Full-Commit":"false"});
        targetDb.createDb();
      }
      for (var i = 0; i < docs.length; i++) {
        var doc = docs[i];
        delete doc._rev;
      }
      if (docs.length > 0) {
        targetDb.bulkSave(docs);
      }
    }
  
  
    function addAtt(db, doc, attName, attData, type) {
      var uri = "/" + db.name + "/" + encodeURIComponent(doc._id) + "/" + attName;
  
      if (doc._rev) {
        uri += "?rev=" + doc._rev;
      }
  
      var xhr = CouchDB.request("PUT", uri, {
        headers: {
          "Content-Type": type
        },
        body: attData
      });
  
      T(xhr.status === 201);
      doc._rev = JSON.parse(xhr.responseText).rev;
    }
  
  
    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;
    }
  
  
    function getTask(rep_id, delay) {
      var t0 = new Date();
      var t1;
      do {
        var xhr = CouchDB.request("GET", "/_active_tasks");
        var tasks = JSON.parse(xhr.responseText);
        for(var i = 0; i < tasks.length; i++) {
          if(tasks[i].replication_id == repResult._local_id) {
            return tasks[i];
          }
        }
        sleep(500);
        t1 = new Date();
      } while((t1 - t0) <= delay);
  
      return null;
    }
  
  
    function waitForSeq(sourceDb, targetDb, rep_id) {
      var sourceSeq = sourceDb.info().update_seq,
          t0 = new Date(),
          t1,
          ms = 3000;
  
      do {
        var task = getTask(rep_id, 0);
        if(task && task["through_seq"] == sourceSeq) {
          return;
        }
        t1 = new Date();
      } while (((t1 - t0) <= ms));
    }
  
    // test simple replications (not continuous, not filtered), including
    // conflict creation
    docs = makeDocs(1, 21);
    docs.push({
      _id: "_design/foo",
      language: "javascript",
      value: "ddoc"
    });
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb(docs);
      populateTargetDb([]);
  
      // add some attachments
      for (j = 10; j < 15; j++) {
        addAtt(sourceDb, docs[j], "readme.txt", att1_data, "text/plain");
      }
  
      repResult = CouchDB.replicate(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      TEquals(true, repResult.ok);
  
      sourceInfo = sourceDb.info();
      targetInfo = targetDb.info();
  
      TEquals(sourceInfo.doc_count, targetInfo.doc_count);
  
      TEquals('string', typeof repResult.session_id);
      // we can't rely on sequences in a cluster
      //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);
      // we can't rely on sequences in a cluster
      //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);
        }
      }
  
  
      // 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);
  
      // add some more attachments
      for (j = 10; j < 15; j++) {
        addAtt(sourceDb, docs[j], "data.dat", att2_data, "application/binary");
      }
  
      repResult = CouchDB.replicate(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      TEquals(true, repResult.ok);
  
      sourceInfo = sourceDb.info();
      targetInfo = targetDb.info();
  
      TEquals(targetInfo.doc_count, sourceInfo.doc_count);
  
      TEquals('string', typeof repResult.session_id);
      // we can't rely on sequences in a cluster
      //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);
      // we can't rely on sequences in a cluster
      //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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      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);
      // we can't rely on sequences in a cluster
      //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});
      // there is no guarantee of ordering also
      // however: the doc has to appear somewhere
      //var idx = changes.results.length - 1;
      var changesResDoc1 = changes.results.filter(function(c){return c.id == docs[1]._id;});
      TEquals(1, changesResDoc1.length);
      TEquals(docs[1]._id, changesResDoc1[0].id);
      TEquals(true, changesResDoc1[0].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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      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);
      // we can't rely on sequences in a cluster
      //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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      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);
      // we can't rely on sequences in a cluster
      //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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      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);
      // we can't rely on sequences in a cluster
      //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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      TEquals(true, repResult.ok);
  
      sourceInfo = sourceDb.info();
      // we can't rely on sequences in a cluster
      //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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      TEquals(true, repResult.ok);
  
      sourceInfo = sourceDb.info();
      // we can't rely on sequences in a cluster
      //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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      TEquals(true, repResult.ok);
      TEquals(true, repResult.no_changes);
      sourceInfo = sourceDb.info();
      // we can't rely on sequences in a cluster
      //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);
    }
  
    // validate COUCHDB-317
    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 {
      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 since_seq parameter
    docs = makeDocs(1, 6);
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb(docs);
      populateTargetDb([]);
      // sequences are no longer simple numbers - so pull #3 from a feed
      var since_seq = sourceDb.changes().results[2].seq;
  
      var expected_ids = [];
      var changes = sourceDb.changes({since: JSON.stringify(since_seq)});
      for (j = 0; j < changes.results.length; j++) {
        expected_ids.push(changes.results[j].id);
      }
      TEquals(2, expected_ids.length, "2 documents since since_seq");
  
      // For OTP < R14B03, temporary child specs are kept in the supervisor
      // after the child terminates, so cancel the replication to delete the
      // child spec in those OTP releases, otherwise since_seq will have no
      // effect.
      try {
        CouchDB.replicate(
          dbPairsPrefixes[i].source+sourceDb.name,
          dbPairsPrefixes[i].target+targetDb.name,
          {body: {cancel: true}}
        );
      } catch (x) {
        // OTP R14B03 onwards
        TEquals("not_found", x.error);
      }
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {body: {since_seq: since_seq}}
      );
      // Same reason as before. But here we don't want since_seq to affect
      // subsequent replications, so we need to delete the child spec from the
      // supervisor (since_seq is not used to calculate the replication ID).
      try {
        CouchDB.replicate(
          dbPairsPrefixes[i].source+sourceDb.name,
          dbPairsPrefixes[i].target+targetDb.name,
          {body: {cancel: true}}
        );
      } catch (x) {
        // OTP R14B03 onwards
        TEquals("not_found", x.error);
      }
      TEquals(true, repResult.ok);
      TEquals(2, repResult.history[0].missing_checked);
      TEquals(2, repResult.history[0].missing_found);
      TEquals(2, repResult.history[0].docs_read);
      TEquals(2, 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);
  
        if (expected_ids.indexOf(doc._id) === -1) {
          T(copy === null);
        } else {
          T(copy !== null);
          TEquals(true, compareObjects(doc, copy));
        }
      }
    }
  
  
    // test errors due to doc validate_doc_update functions in the target endpoint
    docs = makeDocs(1, 8);
    docs[2]["_attachments"] = {
      "hello.txt": {
        "content_type": "text/plain",
        "data": "aGVsbG8gd29ybGQ="  // base64:encode("hello world")
      }
    };
    var ddoc = {
      _id: "_design/test",
      language: "javascript",
      validate_doc_update: (function(newDoc, oldDoc, userCtx, secObj) {
        if ((newDoc.integer % 2) !== 0) {
          throw {forbidden: "I only like multiples of 2."};
        }
      }).toString()
    };
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb(docs);
      populateTargetDb([ddoc]);
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name
      );
      TEquals(true, repResult.ok);
      TEquals(7, repResult.history[0].missing_checked);
      TEquals(7, repResult.history[0].missing_found);
      TEquals(7, repResult.history[0].docs_read);
      TEquals(3, repResult.history[0].docs_written);
      TEquals(4, repResult.history[0].doc_write_failures);
  
      for (j = 0; j < docs.length; j++) {
        doc = docs[j];
        copy = targetDb.open(doc._id);
  
        if (doc.integer % 2 === 0) {
          T(copy !== null);
          TEquals(copy.integer, doc.integer);
        } else {
          T(copy === null);
        }
      }
    }
  
  
    // test create_target option
    docs = makeDocs(1, 2);
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb(docs);
      targetDb.deleteDb();
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {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);
    }
  
  
    // 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 (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb(docs);
      populateTargetDb([]);
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {
          body: {
            filter: "mydesign/myfilter",
            query_params: {
              modulus: "2",
              special: "7"
            }
          }
        }
      );
  
      TEquals(true, repResult.ok);
  
      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);
      // We (incorrectly) don't record update sequences for things
      // that don't pass the changse feed filter. Historically the
      // last document to pass was the second to last doc which has
      // an update sequence of 30. Work that has been applied to avoid
      // conflicts from duplicate IDs breaking _bulk_docs updates added
      // a sort to the logic which changes this. Now the last document
      // to pass has an doc id of "8" and is at update_seq 29 (because only
      // "9" and the design doc are after it).
      //
      // In the future the fix ought to be that we record that update
      // sequence of the database. BigCouch has some existing work on
      // this in the clustered case because if you have very few documents
      // that pass the filter then (given single node's behavior) you end
      // up having to rescan a large portion of the database.
      // we can't rely on sequences in a cluster
      // not only can one figure appear twice (at least for n>1), there's also hashes involved now - so comparing seq==29 is lottery (= cutting off hashes is nonsense)
      // above, there was brute-force comparing all attrs of all docs - now we did check if excluded docs did NOT make it
      // in any way, we can't rely on sequences in a cluster (so leave out)
      //TEquals(29, repResult.source_last_seq);
      //TEquals(0, repResult.history[0].start_last_seq);
      //TEquals(29, repResult.history[0].end_last_seq);
      //TEquals(29, 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);
      populateSourceDb(newDocs, true);
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {
          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
      // cluster - so no seq (ditto above)
      //TEquals(36, repResult.source_last_seq);
      TEquals(true, repResult.history instanceof Array);
      TEquals(2, repResult.history.length);
      //TEquals(29, 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 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;
      } else {
        return false;
      }
    }).toString();
  
    var filterFun2 = (function(doc, req) {
      return true;
    }).toString();
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateTargetDb([]);
      populateSourceDb([]);
  
      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
        }
      };
  
      TEquals(true, sourceDb.save(ddoc).ok);
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {
          body: {
            filter: "mydesign/myfilter",
            query_params: {
              maxvalue: "3"
            }
          }
        }
      );
  
      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);
      TEquals(1, docFoo1.value);
  
      var docFoo2 = targetDb.open("foo2");
      T(docFoo2 !== null);
      TEquals(2, docFoo2.value);
  
      var docFoo3 = targetDb.open("foo3");
      TEquals(null, docFoo3);
  
      var docFoo4 = targetDb.open("foo4");
      TEquals(null, docFoo4);
  
      // replication should start from scratch after the filter's code changed
  
      ddoc.filters.myfilter = filterFun2;
      TEquals(true, sourceDb.save(ddoc).ok);
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {
          body: {
            filter: "mydesign/myfilter",
            query_params : {
              maxvalue: "3"
            }
          }
        }
      );
  
      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);
      TEquals(1, docFoo1.value);
  
      docFoo2 = targetDb.open("foo2");
      T(docFoo2 !== null);
      TEquals(2, docFoo2.value);
  
      docFoo3 = targetDb.open("foo3");
      T(docFoo3 !== null);
      TEquals(3, docFoo3.value);
  
      docFoo4 = targetDb.open("foo4");
      T(docFoo4 !== null);
      TEquals(4, docFoo4.value);
  
      T(targetDb.open("_design/mydesign") !== null);
    }
  
  
    // test replication by doc IDs
    docs = makeDocs(1, 11);
    docs.push({
      _id: "_design/foo",
      language: "javascript",
      integer: 1
    });
  
    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 < dbPairsPrefixes.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;
          }
        }
  
        populateSourceDb(docs);
        populateTargetDb([]);
  
        repResult = CouchDB.replicate(
          dbPairsPrefixes[i].source+sourceDb.name,
          dbPairsPrefixes[i].target+targetDb.name,
          {
            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(
          dbPairsPrefixes[i].source+sourceDb.name,
          dbPairsPrefixes[i].target+targetDb.name,
          {
            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(
          dbPairsPrefixes[i].source+sourceDb.name,
          dbPairsPrefixes[i].target+targetDb.name,
          {
            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(
          dbPairsPrefixes[i].source+sourceDb.name,
          dbPairsPrefixes[i].target+targetDb.name,
          {
            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",
      filters: {
        myfilter: (function(doc, req) { return true; }).toString()
      }
    });
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb(docs);
      populateTargetDb([]);
  
      // add some attachments
      for (j = 10; j < 15; j++) {
        addAtt(sourceDb, docs[j], "readme.txt", att1_data, "text/plain");
      }
  
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {
          body: {
            continuous: true
          }
        }
      );
      TEquals(true, repResult.ok);
      TEquals('string', typeof repResult._local_id);
  
      var rep_id = repResult._local_id;
  
      waitForSeq(sourceDb, targetDb, rep_id);
  
      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");
      }
  
      var ddoc = docs[docs.length - 1]; // design doc
      addAtt(sourceDb, ddoc, "readme.txt", att1_data, "text/plain");
  
      waitForSeq(sourceDb, targetDb, rep_id);
  
      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, rep_id);
  
      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);
      populateSourceDb(newDocs, true);
  
      waitForSeq(sourceDb, targetDb, rep_id);
  
      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, rep_id);
  
      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});
      // quite unfortunately, there is no way on relying on ordering in a cluster
      // but we can assume a length of 2
      var line1 = changes.results[changes.results.length - 2];
      var line2 = changes.results[changes.results.length - 1];
      T(newDocs[0]._id == line1.id || newDocs[0]._id == line2.id);
      T(newDocs[6]._id == line1.id || newDocs[6]._id == line2.id);
      T(line1.deleted && line2.deleted);
  
      // cancel the replication
      repResult = CouchDB.replicate(
        dbPairsPrefixes[i].source+sourceDb.name,
        dbPairsPrefixes[i].target+targetDb.name,
        {
          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);
  
      waitForSeq(sourceDb, targetDb, rep_id);
      copy = targetDb.open(doc._id);
      TEquals(null, copy);
    }
  
    // COUCHDB-1093 - filtered and continuous _changes feed dies when the
    // database is compacted
    // no more relevant when clustering, you can't compact (per se at least)
    /*
    docs = makeDocs(1, 10);
    docs.push({
      _id: "_design/foo",
      language: "javascript",
      filters: {
        myfilter: (function(doc, req) { return true; }).toString()
      }
    });
    populateSourceDb(docs);
    populateTargetDb([]);
  
    repResult = CouchDB.replicate(
      CouchDB.protocol + host + "/" + sourceDb.name,
      targetDb.name,
      {
        body: {
          continuous: true,
          filter: "foo/myfilter"
        }
      }
    );
    TEquals(true, repResult.ok);
    TEquals('string', typeof repResult._local_id);
  
    TEquals(true, sourceDb.compact().ok);
    while (sourceDb.info().compact_running) {};
  
    TEquals(true, sourceDb.save(makeDocs(30, 31)[0]).ok);
  
    var task = getTask(repResult._local_id, 1000);
    T(task != null);
  
    waitForSeq(sourceDb, targetDb, repResult._local_id);
    T(sourceDb.open("30") !== null);
  
    // cancel replication
    repResult = CouchDB.replicate(
      CouchDB.protocol + host + "/" + sourceDb.name,
      targetDb.name,
      {
        body: {
          continuous: true,
          filter: "foo/myfilter",
          cancel: true
        }
      }
    );
    TEquals(true, repResult.ok);
    TEquals('string', typeof repResult._local_id);
    */
  
    //
    // test replication of compressed attachments
    //
    doc = {
      _id: "foobar"
    };
    var bigTextAtt = makeAttData(128 * 1024);
    var attName = "readme.txt";
    var oldSettings = getCompressionInfo();
    var compressionLevel = oldSettings.level;
    var compressibleTypes = oldSettings.types;
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      populateSourceDb([doc]);
      populateTargetDb([]);
  
      // 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(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
      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 defaultUsersDb = new CouchDB("_users", {"X-Couch-Full-Commit":"false"});
    try { defaultUsersDb.createDb(); } catch (e) { /* ignore if exists*/ }
    //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
      }
    ];*/
  
    docs = makeDocs(1, 6);
    docs.push({
      _id: "_design/foo",
      language: "javascript"
    });
  
    dbPairsPrefixes = [
      {
        source: "",
        target: ""
      },
      {
        source: CouchDB.protocol + host + "/",
        target: ""
      },
      {
        source: "",
        target: CouchDB.protocol + "joe:erly@" + host + "/"
      },
      {
        source: CouchDB.protocol + host + "/",
        target: CouchDB.protocol + "joe:erly@" + host + "/"
      }
    ];
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      //usersDb.deleteDb();
      populateSourceDb(docs);
      populateTargetDb([]);
  
      TEquals(true, targetDb.setSecObj({
        admins: {
          names: ["superman"],
          roles: ["god"]
        }
      }).ok);
  
      // do NOT run on modified server b/c we use the default DB
      //run_on_modified_server(server_config, function() {
        delete joeUserDoc._rev;
        var prevJoeUserDoc = defaultUsersDb.open(joeUserDoc._id);
        if (prevJoeUserDoc) {
          joeUserDoc._rev = prevJoeUserDoc._rev;
        }
        TEquals(true, defaultUsersDb.save(joeUserDoc).ok);
  
        TEquals(true, CouchDB.login("joe", "erly").ok);
        TEquals('joe', CouchDB.session().userCtx.name);
  
        repResult = CouchDB.replicate(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
  
        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
    dbPairsPrefixes = [
      {
        source: "",
        target: ""
      },
      {
        source: CouchDB.protocol + "joe:erly@" + host + "/",
        target: ""
      },
      {
        source: "",
        target: CouchDB.protocol + host + "/"
      },
      {
        source: CouchDB.protocol + "joe:erly@" + host + "/",
        target: CouchDB.protocol + host + "/"
      }
    ];
  
    for (i = 0; i < dbPairsPrefixes.length; i++) {
      //usersDb.deleteDb();
      populateSourceDb(docs);
      populateTargetDb([]);
  
      TEquals(true, sourceDb.setSecObj({
        admins: {
          names: ["superman"],
          roles: ["god"]
        },
        readers: {
          names: ["john"],
          roles: ["secret"]
        }
      }).ok);
      // check that we start OK (plus give time for sec object apply 2 avoid Heisenbugs)
      for (j = 0; j < docs.length; j++) {
        doc = docs[j];
        copy = targetDb.open(doc._id);
        TEquals(null, copy);
      }
  
      // do NOT run on modified server b/c we use the default DB
      //run_on_modified_server(server_config, function() {
        delete joeUserDoc._rev;
        var prevJoeUserDoc = defaultUsersDb.open(joeUserDoc._id);
        if (prevJoeUserDoc) {
          joeUserDoc._rev = prevJoeUserDoc._rev;
        }
        TEquals(true, defaultUsersDb.save(joeUserDoc).ok);
  
        TEquals(true, CouchDB.login("joe", "erly").ok);
        TEquals('joe', CouchDB.session().userCtx.name);
  
        try {
          CouchDB.replicate(dbPairsPrefixes[i].source+sourceDb.name, dbPairsPrefixes[i].target+targetDb.name);
          T(false, "should have raised an exception");
        } catch (x) {
          // TODO: small thing: DB exists but is no more found - at least we have an exception, so it's rather minor
          //TEquals("unauthorized", x.error);
          T(!!x);
        }
  
        TEquals(true, CouchDB.logout().ok);
      //});
  
      for (j = 0; j < docs.length; j++) {
        doc = docs[j];
        copy = targetDb.open(doc._id);
        TEquals(null, copy);
      }
    }
  
  
    // COUCHDB-885 - push replication of a doc with attachment causes a
    //               conflict in the target.
    sourceDb = new CouchDB("test_suite_db_a");
    targetDb = new CouchDB("test_suite_db_b");
  
    populateSourceDb([]);
    populateTargetDb([]);
  
    doc = {
      _id: "doc1"
    };
    TEquals(true, sourceDb.save(doc).ok);
  
    repResult = CouchDB.replicate(
      sourceDb.name,
      CouchDB.protocol + host + "/" + targetDb.name
    );
    TEquals(true, repResult.ok);
    TEquals(true, repResult.history instanceof Array);
    TEquals(1, repResult.history.length);
    TEquals(1, repResult.history[0].docs_written);
    TEquals(1, repResult.history[0].docs_read);
    TEquals(0, repResult.history[0].doc_write_failures);
  
    doc["_attachments"] = {
      "hello.txt": {
        "content_type": "text/plain",
        "data": "aGVsbG8gd29ybGQ="  // base64:encode("hello world")
      },
      "foo.dat": {
        "content_type": "not/compressible",
        "data": "aSBhbSBub3QgZ3ppcGVk"  // base64:encode("i am not gziped")
      }
    };
  
    TEquals(true, sourceDb.save(doc).ok);
    repResult = CouchDB.replicate(
      sourceDb.name,
      CouchDB.protocol + host + "/" + targetDb.name
    );
    TEquals(true, repResult.ok);
    TEquals(true, repResult.history instanceof Array);
    TEquals(2, repResult.history.length);
    TEquals(1, repResult.history[0].docs_written);
    TEquals(1, repResult.history[0].docs_read);
    TEquals(0, repResult.history[0].doc_write_failures);
  
    copy = targetDb.open(doc._id, {
      conflicts: true, deleted_conflicts: true,
      attachments: true, att_encoding_info: true});
    T(copy !== null);
    TEquals("undefined", typeof copy._conflicts);
    TEquals("undefined", typeof copy._deleted_conflicts);
    TEquals("text/plain", copy._attachments["hello.txt"]["content_type"]);
    TEquals("aGVsbG8gd29ybGQ=", copy._attachments["hello.txt"]["data"]);
    TEquals("gzip", copy._attachments["hello.txt"]["encoding"]);
    TEquals("not/compressible", copy._attachments["foo.dat"]["content_type"]);
    TEquals("aSBhbSBub3QgZ3ppcGVk", copy._attachments["foo.dat"]["data"]);
    TEquals("undefined", typeof copy._attachments["foo.dat"]["encoding"]);
    // end of test for COUCHDB-885
  
    // Test for COUCHDB-1242 (reject non-string query_params)
    // TODO: non-String params crash CouchDB alltogether
    /*
    try {
      CouchDB.replicate(sourceDb, targetDb, {
        body: {
          filter : "mydesign/myfilter",
          query_params : {
            "maxvalue": 4
          }
        }
      });
    } catch (e) {
      TEquals("bad_request", e.error);
    }
    */
  
  
    // Test that we can cancel a replication just by POSTing an object
    // like  {"replication_id": Id, "cancel": true}. The replication ID
    // can be obtained from a continuous replication request response
    // (_local_id field), from _active_tasks or from the log
    populateSourceDb(makeDocs(1, 6));
    populateTargetDb([]);
  
    repResult = CouchDB.replicate(
      CouchDB.protocol + host + "/" + sourceDb.name,
      targetDb.name,
      {
        body: {
          continuous: true,
          create_target: true
        }
      }
    );
    TEquals(true, repResult.ok);
    TEquals('string', typeof repResult._local_id);
    var repId = repResult._local_id;
  
    var task = getTask(repId, 3000);
    T(task != null);
  
    TEquals(task["replication_id"], repId, "Replication found in _active_tasks");
    xhr = CouchDB.request(
      "POST", "/_replicate", {
        body: JSON.stringify({"replication_id": repId, "cancel": true}),
        headers: {"Content-Type": "application/json"}
    });
    TEquals(200, xhr.status, "Replication cancel request success");
  
    task = getTask(repId);
    TEquals(null, task, "Replication was canceled");
  
    xhr = CouchDB.request(
      "POST", "/_replicate", {
        body: JSON.stringify({"replication_id": repId, "cancel": true}),
        headers: {"Content-Type": "application/json"}
    });
    TEquals(404, xhr.status, "2nd replication cancel failed");
  
    // Non-admin user can not cancel replications triggered by other users
    var userDoc = CouchDB.prepareUserDoc({
      name: "tony",
      roles: ["mafia"]
    }, "soprano");
    // again, due doe _security not there, we use the default users DB
    defaultUsersDb = new CouchDB("_users", {"X-Couch-Full-Commit":"false"});
    //usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"});
    // (and leave the server alone)
    /*server_config = [
      {
        section: "couch_httpd_auth",
        key: "authentication_db",
        value: usersDb.name
      }
    ];*/
  
    //run_on_modified_server(server_config, function() {
      populateSourceDb(makeDocs(1, 6));
      populateTargetDb([]);
      var prevUserDoc = defaultUsersDb.open(userDoc._id);
      if(prevUserDoc) {
        userDoc._rev = prevUserDoc._rev;
      }
      TEquals(true, defaultUsersDb.save(userDoc).ok);
  
      repResult = CouchDB.replicate(
        CouchDB.protocol + host + "/" + sourceDb.name,
        targetDb.name,
        {
          body: {
            continuous: true
          }
        }
      );
      TEquals(true, repResult.ok);
      TEquals('string', typeof repResult._local_id);
  
      TEquals(true, CouchDB.login("tony", "soprano").ok);
      TEquals('tony', CouchDB.session().userCtx.name);
  
      xhr = CouchDB.request(
        "POST", "/_replicate", {
          body: JSON.stringify({"replication_id": repResult._local_id, "cancel": true}),
          headers: {"Content-Type": "application/json"}
      });
      TEquals(401, xhr.status, "Unauthorized to cancel replication");
      TEquals("unauthorized", JSON.parse(xhr.responseText).error);
  
      TEquals(true, CouchDB.logout().ok);
  
      xhr = CouchDB.request(
        "POST", "/_replicate", {
          body: JSON.stringify({"replication_id": repResult._local_id, "cancel": true}),
          headers: {"Content-Type": "application/json"}
      });
      TEquals(200, xhr.status, "Authorized to cancel replication");
    //});
  
    // cleanup
    //usersDb.deleteDb();
    sourceDb.deleteDb();
    targetDb.deleteDb();
    // (not sure what this is - cleanup after 'file not found tests' poss. - not harmful anyway) 
    (new CouchDB("test_suite_db")).deleteDb();
  };
  