http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a4893aa/test/javascript/tests/recreate_doc.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/recreate_doc.js b/test/javascript/tests/recreate_doc.js new file mode 100644 index 0000000..f972379 --- /dev/null +++ b/test/javascript/tests/recreate_doc.js @@ -0,0 +1,145 @@ +// 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.recreate_doc = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + // First create a new document with the ID "foo", and delete it again + var doc = {_id: "foo", a: "bar", b: 42}; + var result = db.save(doc); + T(result.ok); + var firstRev = result.rev; + T(db.deleteDoc(doc).ok); + + // Now create a new document with the same ID, save it, and then modify it + for (var i = 0; i < 10; i++) { + doc = {_id: "foo"}; + T(db.save(doc).ok); + doc = db.open("foo"); + doc.a = "baz"; + T(db.save(doc).ok); + T(db.deleteDoc(doc).rev != undefined); + } + + try { + // COUCHDB-292 now attempt to save the document with a prev that's since + // been deleted and this should generate a conflict exception + db.save({_id:"foo", _rev:firstRev, bar:1}); + T("no save conflict 1" && false); // we shouldn't hit here + } catch (e) { + T(e.error == "conflict"); + } + + var binAttDoc = { + _id: "foo", + _rev:firstRev, + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + try { + // same as before, but with binary + db.save(binAttDoc); + T("no save conflict 2" && false); // we shouldn't hit here + } catch (e) { + T(e.error == "conflict"); + } + + + try { + // random non-existant prev rev + db.save({_id:"foo", _rev:"1-asfafasdf", bar:1}); + T("no save conflict 3" && false); // we shouldn't hit here + } catch (e) { + T(e.error == "conflict"); + } + + try { + // random non-existant prev rev with bin + binAttDoc._rev = "1-aasasfasdf"; + db.save(binAttDoc); + T("no save conflict 4" && false); // we shouldn't hit here + } catch (e) { + T(e.error == "conflict"); + } + + db.deleteDb(); + db.createDb(); + + // Helper function to create a doc with multiple revisions + // that are compacted away to ?REV_MISSING. + + var createDoc = function(docid) { + var ret = [{_id: docid, count: 0}]; + T(db.save(ret[0]).ok); + for(var i = 0; i < 2; i++) { + ret[ret.length] = { + _id: docid, + _rev: ret[ret.length-1]._rev, + count: ret[ret.length-1].count+1 + }; + T(db.save(ret[ret.length-1]).ok); + } + db.compact(); + while(db.info().compact_running) {} + return ret; + } + + // Helper function to check that there are no duplicates + // in the changes feed and that it has proper update + // sequence ordering. + + var checkChanges = function() { + // Assert that there are no duplicates in _changes. + var req = CouchDB.request("GET", "/test_suite_db/_changes"); + var resp = JSON.parse(req.responseText); + var docids = {}; + var prev_seq = -1; + for(var i = 0; i < resp.results.length; i++) { + row = resp.results[i]; + T(row.seq > prev_seq, "Unordered _changes feed."); + T(docids[row.id] === undefined, "Duplicates in _changes feed."); + prev_seq = row.seq; + docids[row.id] = true; + } + }; + + // COUCHDB-1265 - Check that the changes feed remains proper + // after we try and break the update_seq tree. + + // This first case is the one originally reported and "fixed" + // in COUCHDB-1265. Reinserting an old revision into the + // revision tree causes duplicates in the update_seq tree. + + var revs = createDoc("a"); + T(db.save(revs[1], {new_edits: false}).ok); + T(db.save(revs[revs.length-1]).ok); + checkChanges(); + + // The original fix for COUCHDB-1265 is not entirely correct + // as it didn't consider the possibility that a compaction + // might run after the original tree screw up. + + revs = createDoc("b"); + T(db.save(revs[1], {new_edits: false}).ok); + db.compact(); + while(db.info().compact_running) {} + T(db.save(revs[revs.length-1]).ok); + checkChanges(); + +};
http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a4893aa/test/javascript/tests/reduce.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/reduce.js b/test/javascript/tests/reduce.js new file mode 100644 index 0000000..36e5cb7 --- /dev/null +++ b/test/javascript/tests/reduce.js @@ -0,0 +1,414 @@ +// 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.reduce = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + var numDocs = 500; + var docs = makeDocs(1,numDocs + 1); + db.bulkSave(docs); + var summate = function(N) {return (N+1)*N/2;}; + + var map = function (doc) { + emit(doc.integer, doc.integer); + emit(doc.integer, doc.integer); + }; + var reduce = function (keys, values) { return sum(values); }; + var result = db.query(map, reduce); + T(result.rows[0].value == 2*summate(numDocs)); + + result = db.query(map, reduce, {startkey: 4, endkey: 4}); + T(result.rows[0].value == 8); + + result = db.query(map, reduce, {startkey: 4, endkey: 5}); + T(result.rows[0].value == 18); + + result = db.query(map, reduce, {startkey: 4, endkey: 6}); + T(result.rows[0].value == 30); + + result = db.query(map, reduce, {group:true, limit:3}); + T(result.rows[0].value == 2); + T(result.rows[1].value == 4); + T(result.rows[2].value == 6); + + for(var i=1; i<numDocs/2; i+=30) { + result = db.query(map, reduce, {startkey: i, endkey: numDocs - i}); + T(result.rows[0].value == 2*(summate(numDocs-i) - summate(i-1))); + } + + db.deleteDb(); + db.createDb(); + + for(var i=1; i <= 5; i++) { + + for(var j=0; j < 10; j++) { + // these docs are in the order of the keys collation, for clarity + var docs = []; + docs.push({keys:["a"]}); + docs.push({keys:["a"]}); + docs.push({keys:["a", "b"]}); + docs.push({keys:["a", "b"]}); + docs.push({keys:["a", "b", "c"]}); + docs.push({keys:["a", "b", "d"]}); + docs.push({keys:["a", "c", "d"]}); + docs.push({keys:["d"]}); + docs.push({keys:["d", "a"]}); + docs.push({keys:["d", "b"]}); + docs.push({keys:["d", "c"]}); + db.bulkSave(docs); + T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11)); + } + + map = function (doc) { emit(doc.keys, 1); }; + reduce = function (keys, values) { return sum(values); }; + + var results = db.query(map, reduce, {group:true}); + + //group by exact key match + T(equals(results.rows[0], {key:["a"],value:20*i})); + T(equals(results.rows[1], {key:["a","b"],value:20*i})); + T(equals(results.rows[2], {key:["a", "b", "c"],value:10*i})); + T(equals(results.rows[3], {key:["a", "b", "d"],value:10*i})); + + // test to make sure group reduce and limit params provide valid json + var results = db.query(map, reduce, {group: true, limit: 2}); + T(equals(results.rows[0], {key: ["a"], value: 20*i})); + T(equals(results.rows.length, 2)); + + //group by the first element in the key array + var results = db.query(map, reduce, {group_level:1}); + T(equals(results.rows[0], {key:["a"],value:70*i})); + T(equals(results.rows[1], {key:["d"],value:40*i})); + + //group by the first 2 elements in the key array + var results = db.query(map, reduce, {group_level:2}); + T(equals(results.rows[0], {key:["a"],value:20*i})); + T(equals(results.rows[1], {key:["a","b"],value:40*i})); + T(equals(results.rows[2], {key:["a","c"],value:10*i})); + T(equals(results.rows[3], {key:["d"],value:10*i})); + T(equals(results.rows[4], {key:["d","a"],value:10*i})); + T(equals(results.rows[5], {key:["d","b"],value:10*i})); + T(equals(results.rows[6], {key:["d","c"],value:10*i})); + + // endkey test with inclusive_end=true + var results = db.query(map, reduce, {group_level:2,endkey:["d"],inclusive_end:true}); + T(equals(results.rows[0], {key:["a"],value:20*i})); + T(equals(results.rows[1], {key:["a","b"],value:40*i})); + T(equals(results.rows[2], {key:["a","c"],value:10*i})); + T(equals(results.rows[3], {key:["d"],value:10*i})); + TEquals(4, results.rows.length); + + // endkey test with inclusive_end=false + var results = db.query(map, reduce, {group_level:2,endkey:["d"],inclusive_end:false}); + T(equals(results.rows[0], {key:["a"],value:20*i})); + T(equals(results.rows[1], {key:["a","b"],value:40*i})); + T(equals(results.rows[2], {key:["a","c"],value:10*i})); + TEquals(3, results.rows.length); + } + + // now test out more complex reductions that need to use the combine option. + + db.deleteDb(); + db.createDb(); + + + var map = function (doc) { emit(doc.val, doc.val); }; + var reduceCombine = function (keys, values, rereduce) { + // This computes the standard deviation of the mapped results + var stdDeviation=0.0; + var count=0; + var total=0.0; + var sqrTotal=0.0; + + if (!rereduce) { + // This is the reduce phase, we are reducing over emitted values from + // the map functions. + for(var i in values) { + total = total + values[i]; + sqrTotal = sqrTotal + (values[i] * values[i]); + } + count = values.length; + } + else { + // This is the rereduce phase, we are re-reducing previosuly + // reduced values. + for(var i in values) { + count = count + values[i].count; + total = total + values[i].total; + sqrTotal = sqrTotal + values[i].sqrTotal; + } + } + + var variance = (sqrTotal - ((total * total)/count)) / count; + stdDeviation = Math.sqrt(variance); + + // the reduce result. It contains enough information to be rereduced + // with other reduce results. + return {"stdDeviation":stdDeviation,"count":count, + "total":total,"sqrTotal":sqrTotal}; + }; + + // Save a bunch a docs. + + for(var i=0; i < 10; i++) { + var docs = []; + docs.push({val:10}); + docs.push({val:20}); + docs.push({val:30}); + docs.push({val:40}); + docs.push({val:50}); + docs.push({val:60}); + docs.push({val:70}); + docs.push({val:80}); + docs.push({val:90}); + docs.push({val:100}); + db.bulkSave(docs); + } + + var results = db.query(map, reduceCombine); + + var difference = results.rows[0].value.stdDeviation - 28.722813232690143; + // account for floating point rounding error + T(Math.abs(difference) < 0.0000000001); + + function testReducePagination() { + var ddoc = { + "_id": "_design/test", + "language": "javascript", + "views": { + "view1": { + "map": "function(doc) {" + + "emit(doc.int, doc._id);" + + "emit(doc.int + 1, doc._id);" + + "emit(doc.int + 2, doc._id);" + + "}", + "reduce": "_count" + } + } + }; + var result, docs = []; + + function randVal() { + return Math.random() * 100000000; + } + + db.deleteDb(); + db.createDb(); + + for (var i = 0; i < 1123; i++) { + docs.push({"_id": String(i), "int": i}); + } + db.bulkSave(docs.concat([ddoc])); + + // ?group=false tests + result = db.view('test/view1', {startkey: 400, endkey: 402, foobar: randVal()}); + TEquals(9, result.rows[0].value); + result = db.view('test/view1', {startkey: 402, endkey: 400, descending: true, + foobar: randVal()}); + TEquals(9, result.rows[0].value); + + result = db.view('test/view1', {startkey: 400, endkey: 402, inclusive_end: false, + foobar: randVal()}); + TEquals(6, result.rows[0].value); + result = db.view('test/view1', {startkey: 402, endkey: 400, inclusive_end: false, + descending: true, foobar: randVal()}); + TEquals(6, result.rows[0].value); + + result = db.view('test/view1', {startkey: 400, endkey: 402, endkey_docid: "400", + foobar: randVal()}); + TEquals(7, result.rows[0].value); + result = db.view('test/view1', {startkey: 400, endkey: 402, endkey_docid: "400", + inclusive_end: false, foobar: randVal()}); + TEquals(6, result.rows[0].value); + + result = db.view('test/view1', {startkey: 400, endkey: 402, endkey_docid: "401", + foobar: randVal()}); + TEquals(8, result.rows[0].value); + result = db.view('test/view1', {startkey: 400, endkey: 402, endkey_docid: "401", + inclusive_end: false, foobar: randVal()}); + TEquals(7, result.rows[0].value); + + result = db.view('test/view1', {startkey: 400, endkey: 402, endkey_docid: "402", + foobar: randVal()}); + TEquals(9, result.rows[0].value); + result = db.view('test/view1', {startkey: 400, endkey: 402, endkey_docid: "402", + inclusive_end: false, foobar: randVal()}); + TEquals(8, result.rows[0].value); + + result = db.view('test/view1', {startkey: 402, endkey: 400, endkey_docid: "398", + descending: true, foobar: randVal()}); + TEquals(9, result.rows[0].value); + result = db.view('test/view1', {startkey: 402, endkey: 400, endkey_docid: "398", + descending: true, inclusive_end: false, foobar: randVal()}), + TEquals(8, result.rows[0].value); + + result = db.view('test/view1', {startkey: 402, endkey: 400, endkey_docid: "399", + descending: true, foobar: randVal()}); + TEquals(8, result.rows[0].value); + result = db.view('test/view1', {startkey: 402, endkey: 400, endkey_docid: "399", + descending: true, inclusive_end: false, foobar: randVal()}), + TEquals(7, result.rows[0].value); + + result = db.view('test/view1', {startkey: 402, endkey: 400, endkey_docid: "400", + descending: true, foobar: randVal()}), + TEquals(7, result.rows[0].value); + result = db.view('test/view1', {startkey: 402, endkey: 400, endkey_docid: "400", + descending: true, inclusive_end: false, foobar: randVal()}), + TEquals(6, result.rows[0].value); + + result = db.view('test/view1', {startkey: 402, startkey_docid: "400", endkey: 400, + descending: true, foobar: randVal()}); + TEquals(7, result.rows[0].value); + + result = db.view('test/view1', {startkey: 402, startkey_docid: "401", endkey: 400, + descending: true, inclusive_end: false, foobar: randVal()}); + TEquals(5, result.rows[0].value); + + // ?group=true tests + result = db.view('test/view1', {group: true, startkey: 400, endkey: 402, + foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(400, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(402, result.rows[2].key); + TEquals(3, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, endkey: 400, + descending: true, foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(400, result.rows[2].key); + TEquals(3, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 400, endkey: 402, + inclusive_end: false, foobar: randVal()}); + TEquals(2, result.rows.length); + TEquals(400, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + + result = db.view('test/view1', {group: true, startkey: 402, endkey: 400, + descending: true, inclusive_end: false, foobar: randVal()}); + TEquals(2, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + + result = db.view('test/view1', {group: true, startkey: 400, endkey: 402, + endkey_docid: "401", foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(400, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(402, result.rows[2].key); + TEquals(2, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 400, endkey: 402, + endkey_docid: "400", foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(400, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(402, result.rows[2].key); + TEquals(1, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, startkey_docid: "401", + endkey: 400, descending: true, foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(2, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(400, result.rows[2].key); + TEquals(3, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, startkey_docid: "400", + endkey: 400, descending: true, foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(1, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(400, result.rows[2].key); + TEquals(3, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, startkey_docid: "401", + endkey: 400, descending: true, inclusive_end: false, foobar: randVal()}); + TEquals(2, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(2, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + + result = db.view('test/view1', {group: true, startkey: 402, startkey_docid: "400", + endkey: 400, descending: true, inclusive_end: false, foobar: randVal()}); + TEquals(2, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(1, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + + result = db.view('test/view1', {group: true, startkey: 402, endkey: 400, + endkey_docid: "398", descending: true, inclusive_end: true, foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(400, result.rows[2].key); + TEquals(3, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, endkey: 400, + endkey_docid: "399", descending: true, inclusive_end: true, foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(400, result.rows[2].key); + TEquals(2, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, endkey: 400, + endkey_docid: "399", descending: true, inclusive_end: false, foobar: randVal()}); + TEquals(3, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + TEquals(400, result.rows[2].key); + TEquals(1, result.rows[2].value); + + result = db.view('test/view1', {group: true, startkey: 402, endkey: 400, + endkey_docid: "400", descending: true, inclusive_end: false, foobar: randVal()}); + TEquals(2, result.rows.length); + TEquals(402, result.rows[0].key); + TEquals(3, result.rows[0].value); + TEquals(401, result.rows[1].key); + TEquals(3, result.rows[1].value); + + db.deleteDb(); + } + + testReducePagination(); + +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a4893aa/test/javascript/tests/reduce_builtin.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/reduce_builtin.js b/test/javascript/tests/reduce_builtin.js new file mode 100644 index 0000000..b3cc3cc --- /dev/null +++ b/test/javascript/tests/reduce_builtin.js @@ -0,0 +1,179 @@ +// 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.reduce_builtin = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + var numDocs = 500; + var docs = makeDocs(1,numDocs + 1); + db.bulkSave(docs); + + var summate = function(N) {return (N+1)*N/2;}; + + var sumsqr = function(N) { + var acc = 0; + for (var i=1; i<=N; ++i) { + acc += i*i; + } + return acc; + }; + + // this is the same test as the reduce.js test + // only we'll let CouchDB run reduce in Erlang + var map = function (doc) { + emit(doc.integer, doc.integer); + emit(doc.integer, doc.integer); + }; + + var result = db.query(map, "_sum"); + T(result.rows[0].value == 2*summate(numDocs)); + result = db.query(map, "_count"); + T(result.rows[0].value == 1000); + result = db.query(map, "_stats"); + T(result.rows[0].value.sum == 2*summate(numDocs)); + T(result.rows[0].value.count == 1000); + T(result.rows[0].value.min == 1); + T(result.rows[0].value.max == 500); + T(result.rows[0].value.sumsqr == 2*sumsqr(numDocs)); + + result = db.query(map, "_sum", {startkey: 4, endkey: 4}); + T(result.rows[0].value == 8); + result = db.query(map, "_count", {startkey: 4, endkey: 4}); + T(result.rows[0].value == 2); + + result = db.query(map, "_sum", {startkey: 4, endkey: 5}); + T(result.rows[0].value == 18); + result = db.query(map, "_count", {startkey: 4, endkey: 5}); + T(result.rows[0].value == 4); + + result = db.query(map, "_sum", {startkey: 4, endkey: 6}); + T(result.rows[0].value == 30); + result = db.query(map, "_count", {startkey: 4, endkey: 6}); + T(result.rows[0].value == 6); + + result = db.query(map, "_sum", {group:true, limit:3}); + T(result.rows[0].value == 2); + T(result.rows[1].value == 4); + T(result.rows[2].value == 6); + + for(var i=1; i<numDocs/2; i+=30) { + result = db.query(map, "_sum", {startkey: i, endkey: numDocs - i}); + T(result.rows[0].value == 2*(summate(numDocs-i) - summate(i-1))); + } + + // test for trailing characters after builtin functions, desired behaviour + // is to disregard any trailing characters + // I think the behavior should be a prefix test, so that even "_statsorama" + // or "_stats\nare\awesome" should work just as "_stats" does. - JChris + + var trailing = ["\u000a", "orama", "\nare\nawesome", " ", " \n "]; + + for(var i=0; i < trailing.length; i++) { + result = db.query(map, "_sum" + trailing[i]); + T(result.rows[0].value == 2*summate(numDocs)); + result = db.query(map, "_count" + trailing[i]); + T(result.rows[0].value == 1000); + result = db.query(map, "_stats" + trailing[i]); + T(result.rows[0].value.sum == 2*summate(numDocs)); + T(result.rows[0].value.count == 1000); + T(result.rows[0].value.min == 1); + T(result.rows[0].value.max == 500); + T(result.rows[0].value.sumsqr == 2*sumsqr(numDocs)); + } + + db.deleteDb(); + db.createDb(); + + for(var i=1; i <= 5; i++) { + + for(var j=0; j < 10; j++) { + // these docs are in the order of the keys collation, for clarity + var docs = []; + docs.push({keys:["a"]}); + docs.push({keys:["a"]}); + docs.push({keys:["a", "b"]}); + docs.push({keys:["a", "b"]}); + docs.push({keys:["a", "b", "c"]}); + docs.push({keys:["a", "b", "d"]}); + docs.push({keys:["a", "c", "d"]}); + docs.push({keys:["d"]}); + docs.push({keys:["d", "a"]}); + docs.push({keys:["d", "b"]}); + docs.push({keys:["d", "c"]}); + db.bulkSave(docs); + T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11)); + } + + map = function (doc) { emit(doc.keys, 1); }; + // with emitted values being 1, count should be the same as sum + var builtins = ["_sum", "_count"]; + + for (var b=0; b < builtins.length; b++) { + var fun = builtins[b]; + var results = db.query(map, fun, {group:true}); + + //group by exact key match + T(equals(results.rows[0], {key:["a"],value:20*i})); + T(equals(results.rows[1], {key:["a","b"],value:20*i})); + T(equals(results.rows[2], {key:["a", "b", "c"],value:10*i})); + T(equals(results.rows[3], {key:["a", "b", "d"],value:10*i})); + + // test to make sure group reduce and limit params provide valid json + var results = db.query(map, fun, {group: true, limit: 2}); + T(equals(results.rows[0], {key: ["a"], value: 20*i})); + T(equals(results.rows.length, 2)); + + //group by the first element in the key array + var results = db.query(map, fun, {group_level:1}); + T(equals(results.rows[0], {key:["a"],value:70*i})); + T(equals(results.rows[1], {key:["d"],value:40*i})); + + //group by the first 2 elements in the key array + var results = db.query(map, fun, {group_level:2}); + T(equals(results.rows[0], {key:["a"],value:20*i})); + T(equals(results.rows[1], {key:["a","b"],value:40*i})); + T(equals(results.rows[2], {key:["a","c"],value:10*i})); + T(equals(results.rows[3], {key:["d"],value:10*i})); + T(equals(results.rows[4], {key:["d","a"],value:10*i})); + T(equals(results.rows[5], {key:["d","b"],value:10*i})); + T(equals(results.rows[6], {key:["d","c"],value:10*i})); + }; + + map = function (doc) { emit(doc.keys, [1, 1]); }; + + var results = db.query(map, "_sum", {group:true}); + T(equals(results.rows[0], {key:["a"],value:[20*i,20*i]})); + T(equals(results.rows[1], {key:["a","b"],value:[20*i,20*i]})); + T(equals(results.rows[2], {key:["a", "b", "c"],value:[10*i,10*i]})); + T(equals(results.rows[3], {key:["a", "b", "d"],value:[10*i,10*i]})); + + var results = db.query(map, "_sum", {group: true, limit: 2}); + T(equals(results.rows[0], {key: ["a"], value: [20*i,20*i]})); + T(equals(results.rows.length, 2)); + + var results = db.query(map, "_sum", {group_level:1}); + T(equals(results.rows[0], {key:["a"],value:[70*i,70*i]})); + T(equals(results.rows[1], {key:["d"],value:[40*i,40*i]})); + + var results = db.query(map, "_sum", {group_level:2}); + T(equals(results.rows[0], {key:["a"],value:[20*i,20*i]})); + T(equals(results.rows[1], {key:["a","b"],value:[40*i,40*i]})); + T(equals(results.rows[2], {key:["a","c"],value:[10*i,10*i]})); + T(equals(results.rows[3], {key:["d"],value:[10*i,10*i]})); + T(equals(results.rows[4], {key:["d","a"],value:[10*i,10*i]})); + T(equals(results.rows[5], {key:["d","b"],value:[10*i,10*i]})); + T(equals(results.rows[6], {key:["d","c"],value:[10*i,10*i]})); + } +} http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a4893aa/test/javascript/tests/reduce_false.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/reduce_false.js b/test/javascript/tests/reduce_false.js new file mode 100644 index 0000000..699b258 --- /dev/null +++ b/test/javascript/tests/reduce_false.js @@ -0,0 +1,44 @@ +// 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.reduce_false = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + var numDocs = 5; + var docs = makeDocs(1,numDocs + 1); + db.bulkSave(docs); + var summate = function(N) {return (N+1)*N/2;}; + + var designDoc = { + _id:"_design/test", + language: "javascript", + views: { + summate: {map:"function (doc) { emit(doc.integer, doc.integer); }", + reduce:"function (keys, values) { return sum(values); }"}, + } + }; + T(db.save(designDoc).ok); + + // Test that the reduce works + var res = db.view('test/summate'); + T(res.rows.length == 1 && res.rows[0].value == summate(5)); + + //Test that we get our docs back + res = db.view('test/summate', {reduce: false}); + T(res.rows.length == 5); + for(var i=0; i<5; i++) { + T(res.rows[i].value == i+1); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a4893aa/test/javascript/tests/reduce_false_temp.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/reduce_false_temp.js b/test/javascript/tests/reduce_false_temp.js new file mode 100644 index 0000000..d45f05b --- /dev/null +++ b/test/javascript/tests/reduce_false_temp.js @@ -0,0 +1,37 @@ +// 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.reduce_false_temp = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + var numDocs = 5; + var docs = makeDocs(1,numDocs + 1); + db.bulkSave(docs); + var summate = function(N) {return (N+1)*N/2;}; + + var mapFun = "function (doc) { emit(doc.integer, doc.integer); }"; + var reduceFun = "function (keys, values) { return sum(values); }"; + + // Test that the reduce works + var res = db.query(mapFun, reduceFun); + T(res.rows.length == 1 && res.rows[0].value == summate(5)); + + //Test that we get our docs back + res = db.query(mapFun, reduceFun, {reduce: false}); + T(res.rows.length == 5); + for(var i=0; i<5; i++) { + T(res.rows[i].value == i+1); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a4893aa/test/javascript/tests/replication.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/replication.js b/test/javascript/tests/replication.js new file mode 100644 index 0000000..21a4304 --- /dev/null +++ b/test/javascript/tests/replication.js @@ -0,0 +1,1795 @@ +// 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) { + + 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: 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 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 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); + } + + + 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); + } + } + + + 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]; + } + } + 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 < 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); + TEquals(true, repResult.ok); + + sourceInfo = sourceDb.info(); + targetInfo = targetDb.info(); + + TEquals(sourceInfo.doc_count, targetInfo.doc_count); + + 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); + } + } + + + // 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(dbPairs[i].source, dbPairs[i].target); + TEquals(true, repResult.ok); + + sourceInfo = sourceDb.info(); + targetInfo = targetDb.info(); + + 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); + } + + // 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 < dbPairs.length; i++) { + var since_seq = 3; + populateDb(sourceDb, docs); + populateDb(targetDb, []); + + var expected_ids = []; + var changes = sourceDb.changes({since: 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( + dbPairs[i].source, + dbPairs[i].target, + {body: {cancel: true}} + ); + } catch (x) { + // OTP R14B03 onwards + TEquals("not found", x.error); + } + repResult = CouchDB.replicate( + dbPairs[i].source, + dbPairs[i].target, + {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( + dbPairs[i].source, + dbPairs[i].target, + {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 < dbPairs.length; i++) { + populateDb(sourceDb, docs); + populateDb(targetDb, [ddoc]); + + repResult = CouchDB.replicate( + dbPairs[i].source, + dbPairs[i].target + ); + 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 < dbPairs.length; i++) { + populateDb(sourceDb, docs); + targetDb.deleteDb(); + + 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); + } + + + // 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 < 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" + } + } + } + ); + + 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. + 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); + 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(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 < 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 + } + }; + + TEquals(true, sourceDb.save(ddoc).ok); + + repResult = CouchDB.replicate( + dbPairs[i].source, + dbPairs[i].target, + { + 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( + dbPairs[i].source, + dbPairs[i].target, + { + 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 < 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; + } + } + + 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", + filters: { + myfilter: (function(doc, req) { return true; }).toString() + } + }); + + 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, 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); + populateDb(sourceDb, 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}); + 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); + + 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 + docs = makeDocs(1, 10); + docs.push({ + _id: "_design/foo", + language: "javascript", + filters: { + myfilter: (function(doc, req) { return true; }).toString() + } + }); + populateDb(sourceDb, docs); + populateDb(targetDb, []); + + 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 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 + } + ]; + + docs = makeDocs(1, 6); + docs.push({ + _id: "_design/foo", + language: "javascript" + }); + + 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 + } + ]; + + for (i = 0; i < dbPairs.length; i++) { + usersDb.deleteDb(); + populateDb(sourceDb, docs); + populateDb(targetDb, []); + + TEquals(true, targetDb.setSecObj({ + admins: { + names: ["superman"], + roles: ["god"] + } + }).ok); + + 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: ["superman"], + roles: ["god"] + }, + readers: { + names: ["john"], + roles: ["secret"] + } + }).ok); + + 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); + } + + 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"); + + sourceDb.deleteDb(); + sourceDb.createDb(); + targetDb.deleteDb(); + targetDb.createDb(); + + 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) + 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 + populateDb(sourceDb, makeDocs(1, 6)); + populateDb(targetDb, []); + + 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"); + usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"}); + server_config = [ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: usersDb.name + } + ]; + + run_on_modified_server(server_config, function() { + populateDb(sourceDb, makeDocs(1, 6)); + populateDb(targetDb, []); + TEquals(true, usersDb.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(); +};
