Skip to content

Instantly share code, notes, and snippets.

@jchris
Created October 2, 2008 06:58
Show Gist options
  • Save jchris/14299 to your computer and use it in GitHub Desktop.
Save jchris/14299 to your computer and use it in GitHub Desktop.
diff --git a/trunk/share/server/test.js b/trunk/share/server/test.js
new file mode 100644
index 0000000..90cc010
--- /dev/null
+++ b/trunk/share/server/test.js
@@ -0,0 +1,2181 @@
+// 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.
+
+// couch.js, with modifications
+
+// 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.
+
+// some monkeypatches
+var JSON = {
+ parse : function(string) {
+ return eval('('+string+')');
+ },
+ stringify : function(obj) {
+ return toJSON(obj||null);
+ }
+};
+
+var HTTP = (function() {
+ function parseCurl(string) {
+ var parts = string.split(/\r\n\r\n/);
+ var body = parts.pop();
+ var header = parts.pop();
+ var headers = header.split(/\n/);
+
+ var status = /HTTP\/1.\d (\d*)/.exec(header)[1];
+ return {
+ responseText: body,
+ status: parseInt(status),
+ getResponseHeader: function(key) {
+ for (var i in headers) {
+ var h = headers[i];
+ if (h.indexOf(key) == 0) {
+ var value = h.substr(key.length+2);
+ value = value.slice(0, value.length-1);
+ return value;
+ }
+ }
+ }
+ }
+ };
+ return {
+ GET : function(url, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = gethttp(urx, hx);
+ return parseCurl(st);
+ },
+ DELETE : function(url, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = delhttp(urx, hx);
+ return parseCurl(st);
+ },
+ POST : function(url, body, headers) {
+ var st, urx = url, bx = (body || ""), hx = (headers || {});
+ hx['Content-Type'] = hx['Content-Type'] || "application/json";
+ st = posthttp(urx, bx, hx);
+ return parseCurl(st);
+ },
+ PUT : function(url, body, headers) {
+ var st, urx = url, bx = (body || ""), hx = (headers || {});
+ hx['Content-Type'] = hx['Content-Type'] || "application/json";
+ st = puthttp(urx, bx, hx);
+ return parseCurl(st);
+ }
+ };
+})();
+
+// A simple class to represent a database. Uses XMLHttpRequest to interface with
+// the CouchDB server.
+
+function CouchDB(name) {
+ this.name = name;
+ this.uri = "/" + encodeURIComponent(name) + "/";
+ request = CouchDB.request;
+
+ // Creates the database on the server
+ this.createDb = function() {
+ var req = request("PUT", this.uri);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 201)
+ throw result;
+ return result;
+ }
+
+ // Deletes the database on the server
+ this.deleteDb = function() {
+ var req = request("DELETE", this.uri);
+ if (req.status == 404)
+ return false;
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ // Save a document to the database
+ this.save = function(doc, options) {
+ var req;
+ if (doc._id == undefined)
+ req = request("POST", this.uri + encodeOptions(options), {
+ body: JSON.stringify(doc)
+ });
+ else
+ req = request("PUT", this.uri + encodeURIComponent(doc._id) + encodeOptions(options), {
+ body: JSON.stringify(doc)
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 201)
+ throw result;
+ // set the _id and _rev members on the input object, for caller convenience.
+ doc._id = result.id;
+ doc._rev = result.rev;
+ return result;
+ }
+
+ // Open a document from the database
+ this.open = function(docId, options) {
+ var req = request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options));
+ if (req.status == 404)
+ return null;
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ // Deletes a document from the database
+ this.deleteDoc = function(doc) {
+ var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ doc._rev = result.rev; //record rev in input document
+ doc._deleted = true;
+ return result;
+ }
+
+ this.bulkSave = function(docs, options) {
+ var req = request("POST", this.uri + "_bulk_docs" + encodeOptions(options), {
+ body: JSON.stringify({"docs": docs})
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 201)
+ throw result;
+ for (var i = 0; i < docs.length; i++) {
+ docs[i]._id = result.new_revs[i].id;
+ docs[i]._rev = result.new_revs[i].rev;
+ }
+ return result;
+ }
+
+ // Applies the map function to the contents of database and returns the results.
+ this.query = function(mapFun, reduceFun, options) {
+ var body = {language: "javascript"};
+ if (typeof(mapFun) != "string")
+ mapFun = mapFun.toSource ? mapFun.toSource() : "(" + mapFun.toString() + ")";
+ body.map = mapFun;
+ if (reduceFun != null) {
+ if (typeof(reduceFun) != "string")
+ reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")";
+ body.reduce = reduceFun;
+ }
+ var req = request("POST", this.uri + "_temp_view" + encodeOptions(options), {
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify(body)
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ this.view = function(viewname, options) {
+ var req = request("GET", this.uri + "_view/" + viewname + encodeOptions(options));
+ if (req.status == 404)
+ return null;
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ // gets information about the database
+ this.info = function() {
+ var req = request("GET", this.uri);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ this.allDocs = function(options) {
+ var req = request("GET", this.uri + "_all_docs" + encodeOptions(options));
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ this.compact = function() {
+ var req = request("POST", this.uri + "_compact");
+ var result = JSON.parse(req.responseText);
+ if (req.status != 202)
+ throw result;
+ return result;
+ }
+
+ // Convert a options object to an url query string.
+ // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"'
+ function encodeOptions(options) {
+ var buf = []
+ if (typeof(options) == "object" && options !== null) {
+ for (var name in options) {
+ if (!options.hasOwnProperty(name)) continue;
+ var value = options[name];
+ if (name == "key" || name == "startkey" || name == "endkey") {
+ value = toJSON(value);
+ }
+ buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value));
+ }
+ }
+ if (!buf.length) {
+ return "";
+ }
+ return "?" + buf.join("&");
+ }
+
+ function toJSON(obj) {
+ return obj !== null ? JSON.stringify(obj) : null;
+ }
+}
+
+CouchDB.allDbs = function() {
+ var req = CouchDB.request("GET", "/_all_dbs");
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+}
+
+CouchDB.getVersion = function() {
+ var req = CouchDB.request("GET", "/");
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result.version;
+}
+
+CouchDB.replicate = function(source, target) {
+ var req = CouchDB.request("POST", "/_replicate", {
+ body: JSON.stringify({source: source, target: target})
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+}
+CouchDB.host = (typeof window == 'undefined' || !window) ? "127.0.0.1" : window;
+CouchDB.port = 5984;
+
+CouchDB.request = function(method, uri, options) {
+ var full_uri = "http://" + CouchDB.host + ":" + CouchDB.port + uri;
+ options = options || {};
+ var response = HTTP[method](full_uri, options.body, options.headers);
+ return response;
+}
+
+
+function toJSON(val) {
+ if (typeof(val) == "undefined") {
+ throw {error:"bad_value", reason:"Cannot encode 'undefined' value as JSON"};
+ }
+ var subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f',
+ '\r': '\\r', '"' : '\\"', '\\': '\\\\'};
+ if (typeof(val) == "xml") { // E4X support
+ val = val.toXMLString();
+ }
+ return {
+ "Array": function(v) {
+ var buf = [];
+ for (var i = 0; i < v.length; i++) {
+ buf.push(toJSON(v[i]));
+ }
+ return "[" + buf.join(",") + "]";
+ },
+ "Boolean": function(v) {
+ return v.toString();
+ },
+ "Date": function(v) {
+ var f = function(n) { return n < 10 ? '0' + n : n }
+ return '"' + v.getUTCFullYear() + '-' +
+ f(v.getUTCMonth() + 1) + '-' +
+ f(v.getUTCDate()) + 'T' +
+ f(v.getUTCHours()) + ':' +
+ f(v.getUTCMinutes()) + ':' +
+ f(v.getUTCSeconds()) + 'Z"';
+ },
+ "Number": function(v) {
+ return isFinite(v) ? v.toString() : "null";
+ },
+ "Object": function(v) {
+ if (v === null) return "null";
+ var buf = [];
+ for (var k in v) {
+ if (!v.hasOwnProperty(k) || typeof(k) !== "string" || v[k] === undefined) {
+ continue;
+ }
+ buf.push(toJSON(k, val) + ": " + toJSON(v[k]));
+ }
+ return "{" + buf.join(",") + "}";
+ },
+ "String": function(v) {
+ if (/["\\\x00-\x1f]/.test(v)) {
+ v = v.replace(/([\x00-\x1f\\"])/g, function(a, b) {
+ var c = subs[b];
+ if (c) return c;
+ c = b.charCodeAt();
+ return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
+ });
+ }
+ return '"' + v + '"';
+ }
+ }[val != null ? val.constructor.name : "Object"](val);
+}
+
+var p = print;
+
+var tests = {
+
+ // Do some basic tests.
+ basics: function(debug) {
+ var result = JSON.parse(CouchDB.request("GET", "/").responseText);
+ T(result.couchdb == "Welcome");
+
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+
+ // bug COUCHDB-100: DELETE on non-existent DB returns 500 instead of 404
+ db.deleteDb();
+
+ db.createDb();
+ if (debug) debugger;
+
+ // Get the database info, check the doc_count
+ T(db.info().doc_count == 0);
+
+ // create a document and save it to the database
+ var doc = {_id:"0",a:1,b:1};
+ var result = db.save(doc);
+
+ T(result.ok==true); // return object has an ok member with a value true
+ T(result.id); // the _id of the document is set.
+ T(result.rev); // the revision id of the document is set.
+
+ // Verify the input doc is now set with the doc id and rev
+ // (for caller convenience).
+ T(doc._id == result.id && doc._rev == result.rev);
+
+ var id = result.id; // save off the id for later
+
+ // Create some more documents.
+ // Notice the use of the ok member on the return result.
+ T(db.save({_id:"1",a:2,b:4}).ok);
+ T(db.save({_id:"2",a:3,b:9}).ok);
+ T(db.save({_id:"3",a:4,b:16}).ok);
+
+ // Check the database doc count
+ T(db.info().doc_count == 4);
+
+ // Check the all docs
+ var results = db.allDocs();
+ var rows = results.rows;
+
+ T(results.total_rows == results.rows.length);
+
+ for(var i=0; i < rows.length; i++) {
+ T(rows[i].id >= "0" && rows[i].id <= "4");
+ }
+
+ // Check _all_docs with descending=true
+ var desc = db.allDocs({descending:true});
+ T(desc.total_rows == desc.rows.length);
+
+ // Test a simple map functions
+
+ // create a map function that selects all documents whose "a" member
+ // has a value of 4, and then returns the document's b value.
+ var mapFunction = function(doc){
+ if (doc.a==4)
+ emit(null, doc.b);
+ };
+
+ results = db.query(mapFunction);
+
+ // verify only one document found and the result value (doc.b).
+ T(results.total_rows == 1 && results.rows[0].value == 16);
+
+ // reopen document we saved earlier
+ existingDoc = db.open(id);
+
+ T(existingDoc.a==1);
+
+ //modify and save
+ existingDoc.a=4;
+ db.save(existingDoc);
+
+ // redo the map query
+ results = db.query(mapFunction);
+
+ // the modified document should now be in the results.
+ T(results.total_rows == 2);
+
+ // write 2 more documents
+ T(db.save({a:3,b:9}).ok);
+ T(db.save({a:4,b:16}).ok);
+
+ results = db.query(mapFunction);
+
+ // 1 more document should now be in the result.
+ T(results.total_rows == 3);
+ T(db.info().doc_count == 6);
+
+ var reduceFunction = function(keys, values){
+ return sum(values);
+ };
+
+ results = db.query(mapFunction, reduceFunction);
+
+ T(results.rows[0].value == 33);
+
+ // delete a document
+ T(db.deleteDoc(existingDoc).ok);
+
+ // make sure we can't open the doc
+ T(db.open(existingDoc._id) == null);
+
+ results = db.query(mapFunction);
+
+ // 1 less document should now be in the results.
+ T(results.total_rows == 2);
+ T(db.info().doc_count == 5);
+
+ // make sure we can still open the old rev of the deleted doc
+ T(db.open(existingDoc._id, {rev: existingDoc._rev}) != null);
+ },
+
+ // Do some edit conflict detection tests
+ conflicts: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // create a doc and save
+ var doc = {_id:"foo",a:1,b:1};
+ T(db.save(doc).ok);
+
+ // reopen
+ var doc2 = db.open(doc._id);
+
+ // ensure the revisions are the same
+ T(doc._id == doc2._id && doc._rev == doc2._rev);
+
+ // edit the documents.
+ doc.a = 2;
+ doc2.a = 3;
+
+ // save one document
+ T(db.save(doc).ok);
+
+ // save the other document
+ try {
+ db.save(doc2); // this should generate a conflict exception
+ T("no save conflict 1" && false); // we shouldn't hit here
+ } catch (e) {
+ T(e.error == "conflict");
+ }
+
+ // Now clear out the _rev member and save. This indicates this document is
+ // new, not based on an existing revision.
+ doc2._rev = undefined;
+ try {
+ db.save(doc2); // this should generate a conflict exception
+ T("no save conflict 2" && false); // we shouldn't hit here
+ } catch (e) {
+ T(e.error == "conflict");
+ }
+
+ // Now delete the document from the database
+ T(db.deleteDoc(doc).ok);
+
+ T(db.save(doc2).ok); // we can save a new document over a deletion without
+ // knowing the deletion rev.
+ },
+
+ recreate_doc: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ 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};
+ T(db.save(doc).ok);
+ T(db.deleteDoc(doc).ok);
+
+ // Now create a new document with the same ID, save it, and then modify it
+ // This should work fine, but currently results in a conflict error, at
+ // least "sometimes"
+ for (var i = 0; i < 10; i++) {
+ doc = {_id: "foo"};
+ T(db.save(doc).ok);
+ doc = db.open("foo");
+ doc.a = "baz";
+ try {
+ T(db.save(doc).ok);
+ } finally {
+ // And now, we can't even delete the document anymore :/
+ T(db.deleteDoc(doc).rev != undefined);
+ }
+ }
+ },
+
+ copy_move_doc: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // copy a doc
+ T(db.save({_id:"doc_to_be_copied",v:1}).ok);
+ var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_that_was_copied"}
+ });
+
+ T(xhr.status == 201);
+ T(db.open("doc_that_was_copied").v == 1);
+
+ // move a doc
+
+ // test error condition
+ var xhr = CouchDB.request("MOVE", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_that_was_moved"}
+ });
+ T(xhr.status == 400); // bad request, MOVE requires source rev.
+
+ var rev = db.open("doc_to_be_copied")._rev;
+ var xhr = CouchDB.request("MOVE", "/test_suite_db/doc_to_be_copied?rev=" + rev, {
+ headers: {"Destination":"doc_that_was_moved"}
+ });
+
+ T(xhr.status == 201);
+ T(db.open("doc_that_was_moved").v == 1);
+ T(db.open("doc_to_be_copied") == null);
+
+ // COPY with existing target
+ T(db.save({_id:"doc_to_be_copied",v:1}).ok);
+ var doc = db.save({_id:"doc_to_be_overwritten",v:2});
+ T(doc.ok);
+
+ // error condition
+ var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_to_be_overwritten"}
+ });
+ T(xhr.status == 412); // conflict
+
+ var rev = db.open("doc_to_be_overwritten")._rev;
+ var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_to_be_overwritten?rev=" + rev}
+ });
+ T(xhr.status == 201);
+
+ var over = db.open("doc_to_be_overwritten");
+ T(rev != over._rev);
+ T(over.v == 1);
+ },
+
+ uuids: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // a single UUID without an explicit count
+ var xhr = CouchDB.request("POST", "/_uuids");
+ T(xhr.status == 200);
+ var result = JSON.parse(xhr.responseText);
+ T(result.uuids.length == 1);
+ var first = result.uuids[0];
+
+ // a single UUID with an explicit count
+ xhr = CouchDB.request("POST", "/_uuids?count=1");
+ T(xhr.status == 200);
+ result = JSON.parse(xhr.responseText);
+ T(result.uuids.length == 1);
+ var second = result.uuids[0];
+ T(first != second);
+
+ // no collisions with 1,000 UUIDs
+ xhr = CouchDB.request("POST", "/_uuids?count=1000");
+ T(xhr.status == 200);
+ result = JSON.parse(xhr.responseText);
+ T( result.uuids.length == 1000 );
+ var seen = {};
+ for(var i in result.uuids) {
+ var id = result.uuids[i];
+ T(seen[id] === undefined);
+ seen[id] = 1;
+ }
+
+ // check our library
+ },
+
+ bulk_docs: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var docs = makeDocs(5);
+
+ // Create the docs
+ var result = db.bulkSave(docs);
+ T(result.ok);
+ T(result.new_revs.length == 5);
+ for (var i = 0; i < 5; i++) {
+ T(result.new_revs[i].id == docs[i]._id);
+ T(result.new_revs[i].rev);
+ docs[i].string = docs[i].string + ".00";
+ }
+
+ // Update the docs
+ result = db.bulkSave(docs);
+ T(result.ok);
+ T(result.new_revs.length == 5);
+ for (i = 0; i < 5; i++) {
+ T(result.new_revs[i].id == i.toString());
+ docs[i]._deleted = true;
+ }
+
+ // Delete the docs
+ result = db.bulkSave(docs);
+ T(result.ok);
+ T(result.new_revs.length == 5);
+ for (i = 0; i < 5; i++) {
+ T(db.open(docs[i]._id) == null);
+ }
+
+ // verify creating a document with no id returns a new id
+ var req = CouchDB.request("POST", "/test_suite_db/_bulk_docs", {
+ body: JSON.stringify({"docs": [{"foo":"bar"}]})
+ });
+ result = JSON.parse(req.responseText);
+
+ T(result.new_revs[0].id != "");
+ T(result.new_revs[0].rev != "");
+ },
+
+ // test saving a semi-large quanitity of documents and do some view queries.
+ lots_of_docs: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // keep number lowish for now to keep tests fasts. Crank up manually to
+ // to really test.
+ var numDocsToCreate = 500;
+
+ for(var i=0; i < numDocsToCreate; i += 100) {
+ var createNow = Math.min(numDocsToCreate - i, 100);
+ var docs = makeDocs(i, i + createNow);
+ T(db.bulkSave(docs).ok);
+ }
+
+ // query all documents, and return the doc.integer member as a key.
+ results = db.query(function(doc){ emit(doc.integer, null) });
+
+ T(results.total_rows == numDocsToCreate);
+
+ // validate the keys are ordered ascending
+ for(var i=0; i<numDocsToCreate; i++) {
+ T(results.rows[i].key==i);
+ }
+
+ // do the query again, but with descending output
+ results = db.query(function(doc){ emit(doc.integer, null) }, null, {
+ descending: true
+ });
+
+ T(results.total_rows == numDocsToCreate);
+
+ // validate the keys are ordered descending
+ for(var i=0; i<numDocsToCreate; i++) {
+ T(results.rows[numDocsToCreate-1-i].key==i);
+ }
+ },
+
+ reduce: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+ var numDocs = 500
+ var docs = makeDocs(1,numDocs + 1);
+ T(db.bulkSave(docs).ok);
+ 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, count: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"]});
+ T(db.bulkSave(docs).ok);
+ 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 count params provide valid json
+ var results = db.query(map, reduce, {group: true, count: 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}));
+ }
+
+ // 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});
+ T(db.bulkSave(docs).ok);
+ }
+
+ 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);
+
+ },
+
+ reduce_false: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var numDocs = 5;
+ var docs = makeDocs(1,numDocs + 1);
+ T(db.bulkSave(docs).ok);
+ 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);
+ }
+ },
+
+ multiple_rows: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var nc = {_id:"NC", cities:["Charlotte", "Raleigh"]};
+ var ma = {_id:"MA", cities:["Boston", "Lowell", "Worcester", "Cambridge", "Springfield"]};
+ var fl = {_id:"FL", cities:["Miami", "Tampa", "Orlando", "Springfield"]};
+
+ T(db.save(nc).ok);
+ T(db.save(ma).ok);
+ T(db.save(fl).ok);
+
+ var generateListOfCitiesAndState = "function(doc) {" +
+ " for (var i = 0; i < doc.cities.length; i++)" +
+ " emit(doc.cities[i] + \", \" + doc._id, null);" +
+ "}";
+
+ var results = db.query(generateListOfCitiesAndState);
+ var rows = results.rows;
+
+ T(rows[0].key == "Boston, MA");
+ T(rows[1].key == "Cambridge, MA");
+ T(rows[2].key == "Charlotte, NC");
+ T(rows[3].key == "Lowell, MA");
+ T(rows[4].key == "Miami, FL");
+ T(rows[5].key == "Orlando, FL");
+ T(rows[6].key == "Raleigh, NC");
+ T(rows[7].key == "Springfield, FL");
+ T(rows[8].key == "Springfield, MA");
+ T(rows[9].key == "Tampa, FL");
+ T(rows[10].key == "Worcester, MA");
+
+ // add another city to NC
+ nc.cities.push("Wilmington");
+ T(db.save(nc).ok);
+
+ var results = db.query(generateListOfCitiesAndState);
+ var rows = results.rows;
+
+ T(rows[0].key == "Boston, MA");
+ T(rows[1].key == "Cambridge, MA");
+ T(rows[2].key == "Charlotte, NC");
+ T(rows[3].key == "Lowell, MA");
+ T(rows[4].key == "Miami, FL");
+ T(rows[5].key == "Orlando, FL");
+ T(rows[6].key == "Raleigh, NC");
+ T(rows[7].key == "Springfield, FL");
+ T(rows[8].key == "Springfield, MA");
+ T(rows[9].key == "Tampa, FL");
+ T(rows[10].key == "Wilmington, NC");
+ T(rows[11].key == "Worcester, MA");
+
+ // now delete MA
+ T(db.deleteDoc(ma).ok);
+
+ var results = db.query(generateListOfCitiesAndState);
+ var rows = results.rows;
+
+ T(rows[0].key == "Charlotte, NC");
+ T(rows[1].key == "Miami, FL");
+ T(rows[2].key == "Orlando, FL");
+ T(rows[3].key == "Raleigh, NC");
+ T(rows[4].key == "Springfield, FL");
+ T(rows[5].key == "Tampa, FL");
+ T(rows[6].key == "Wilmington, NC");
+ },
+
+ large_docs: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var longtext = "0123456789\n";
+
+ for (var i=0; i<10; i++) {
+ longtext = longtext + longtext
+ }
+ T(db.save({"longtest":longtext}).ok);
+ T(db.save({"longtest":longtext}).ok);
+ T(db.save({"longtest":longtext}).ok);
+ T(db.save({"longtest":longtext}).ok);
+
+ // query all documents, and return the doc.foo member as a key.
+ results = db.query(function(doc){
+ emit(null, doc.longtest);
+ });
+ },
+
+ utf8: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var texts = [];
+
+ texts[0] = "1. Ascii: hello"
+ texts[1] = "2. Russian: На берегу пустынных волн"
+ texts[2] = "3. Math: ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i),"
+ texts[3] = "4. Geek: STARGΛ̊TE SG-1"
+ texts[4] = "5. Braille: ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌"
+
+ // check that we can save a reload with full fidelity
+ for (var i=0; i<texts.length; i++) {
+ T(db.save({_id:i.toString(), text:texts[i]}).ok);
+ }
+
+ p("Interestingly, Spidermonkey seems not to treat this source code as utf8,");
+ p("but manages to treat CouchDB's output as utf8. texts[i] is the local variable,");
+ p("and doc.text is the round-tripped version.");
+ for (var i=0; i<texts.length; i++) {
+ var doc = db.open(i.toString());
+ p("\ntexts[i]: "+texts[i]);
+ p("doc.text: "+doc.text);
+ T(doc.text == texts[i]);
+ }
+
+ // check that views and key collation don't blow up
+ var rows = db.query(function(doc) { emit(null, doc.text) }).rows;
+ for (var i=0; i<texts.length; i++) {
+ T(rows[i].value == texts[i]);
+ }
+ },
+
+ attachments: function(debug) {
+ p("TODO fix attachment fetch problem. Skipping attachments test.");
+ return;
+
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var binAttDoc = {
+ _id: "bin_doc",
+ _attachments:{
+ "foo.txt": {
+ content_type:"text/plain",
+ data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ T(db.save(binAttDoc).ok);
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt");
+ T(xhr.responseText == "This is a base64 encoded text");
+ T(xhr.getResponseHeader("Content-Type") == "text/plain");
+
+ // empty attachment
+ var binAttDoc2 = {
+ _id: "bin_doc2",
+ _attachments:{
+ "foo.txt": {
+ content_type:"text/plain",
+ data: ""
+ }
+ }
+ }
+
+ T(db.save(binAttDoc2).ok);
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo.txt");
+ T(xhr.responseText.length == 0);
+ T(xhr.getResponseHeader("Content-Type") == "text/plain");
+
+ // test RESTful doc API
+
+ var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc2/foo2.txt?rev=" + binAttDoc2._rev, {
+ body:"This is no base64 encoded text",
+ headers:{"Content-Type": "text/plain;charset=utf-8"}
+ });
+ T(xhr.status == 201);
+ var rev = JSON.parse(xhr.responseText).rev;
+
+ binAttDoc2 = db.open("bin_doc2");
+
+ T(binAttDoc2._attachments["foo.txt"] !== undefined);
+ T(binAttDoc2._attachments["foo2.txt"] !== undefined);
+ T(binAttDoc2._attachments["foo2.txt"].content_type == "text/plain;charset=utf-8");
+ T(binAttDoc2._attachments["foo2.txt"].length == 30);
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo2.txt");
+ T(xhr.responseText == "This is no base64 encoded text");
+ T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
+
+ // test without rev, should fail
+ var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt");
+ T(xhr.status == 412);
+
+ // test with rev, should not fail
+ var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt?rev=" + rev);
+ T(xhr.status == 200);
+
+
+ // test binary data
+ var bin_data = "JHAPDO*AU£PN ){(3u[d 93DQ9¡€])} ææøo'∂ƒæ≤çæππ•¥∫¶®#†π¶®¥π€ª®˙π8np";
+ var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", {
+ headers:{"Content-Type":"text/plain;charset=utf-8"},
+ body:bin_data
+ });
+ T(xhr.status == 201);
+ var rev = JSON.parse(xhr.responseText).rev;
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt");
+ T(xhr.responseText == bin_data);
+ T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
+
+ var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", {
+ headers:{"Content-Type":"text/plain;charset=utf-8"},
+ body:bin_data
+ });
+ T(xhr.status == 412);
+
+ var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev, {
+ headers:{"Content-Type":"text/plain;charset=utf-8"},
+ body:bin_data
+ });
+ T(xhr.status == 201);
+ var rev = JSON.parse(xhr.responseText).rev;
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt");
+ T(xhr.responseText == bin_data);
+ T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev);
+ T(xhr.responseText == bin_data);
+ T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
+
+ var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev);
+ T(xhr.status == 200);
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev);
+ T(xhr.status == 404);
+
+ // empty attachments
+ var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt", {
+ headers:{"Content-Type":"text/plain;charset=utf-8"},
+ body:""
+ });
+ T(xhr.status == 201);
+ var rev = JSON.parse(xhr.responseText).rev;
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc4/attachment.txt");
+ T(xhr.status == 200);
+ T(xhr.responseText.length == 0);
+
+ // overwrite previsously empty attachment
+ var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt?rev=" + rev, {
+ headers:{"Content-Type":"text/plain;charset=utf-8"},
+ body:"This is a string"
+ });
+ T(xhr.status == 201);
+
+ var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc4/attachment.txt");
+ T(xhr.status == 200);
+ T(xhr.responseText == "This is a string");
+
+ },
+
+ content_negotiation: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+ var xhr;
+
+ xhr = CouchDB.request("GET", "/test_suite_db/");
+ T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/", {
+ headers: {"Accept": "text/html;text/plain;*/*"}
+ });
+ T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/", {
+ headers: {"Accept": "application/json"}
+ });
+ p("TODO: curl wrapper isn't sending Accept headers (or only sends as \"*/*\")");
+ T(xhr.getResponseHeader("Content-Type") == "application/json");
+ },
+
+ design_docs: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var numDocs = 500;
+
+ function makebigstring(power) {
+ var str = "a";
+ while(power-- > 0) {
+ str = str + str;
+ }
+ return str;
+ }
+
+ var designDoc = {
+ _id:"_design/test",
+ language: "javascript",
+ views: {
+ all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"},
+ no_docs: {map: "function(doc) {}"},
+ single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"},
+ summate: {map:"function (doc) {emit(doc.integer, doc.integer)};",
+ reduce:"function (keys, values) { return sum(values); };"},
+ summate2: {map:"function (doc) {emit(doc.integer, doc.integer)};",
+ reduce:"function (keys, values) { return sum(values); };"},
+ huge_src_and_results: {map: "function(doc) { if (doc._id == \"1\") { emit(\"" + makebigstring(16) + "\", null) }}",
+ reduce:"function (keys, values) { return \"" + makebigstring(16) + "\"; };"}
+ }
+ }
+ T(db.save(designDoc).ok);
+
+ T(db.bulkSave(makeDocs(1, numDocs + 1)).ok);
+
+ // test that the _all_docs view returns correctly with keys
+ var results = db.allDocs({startkey:"_design%2F", endkey:"_design%2FZZZ"});
+ T(results.rows.length == 1);
+
+ for (var loop = 0; loop < 2; loop++) {
+ var rows = db.view("test/all_docs_twice").rows;
+ for (var i = 0; i < numDocs; i++) {
+ T(rows[2*i].key == i+1);
+ T(rows[(2*i)+1].key == i+1);
+ }
+ T(db.view("test/no_docs").total_rows == 0)
+ T(db.view("test/single_doc").total_rows == 1)
+ restartServer();
+ };
+
+ // test when language not specified, Javascript is implied
+ var designDoc2 = {
+ _id:"_design/test2",
+ // language: "javascript",
+ views: {
+ single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}
+ }
+ };
+
+ T(db.save(designDoc2).ok);
+ T(db.view("test2/single_doc").total_rows == 1);
+
+ var summate = function(N) {return (N+1)*N/2;};
+ var result = db.view("test/summate");
+ T(result.rows[0].value == summate(numDocs));
+
+ result = db.view("test/summate", {startkey:4,endkey:4});
+ T(result.rows[0].value == 4);
+
+ result = db.view("test/summate", {startkey:4,endkey:5});
+ T(result.rows[0].value == 9);
+
+ result = db.view("test/summate", {startkey:4,endkey:6});
+ T(result.rows[0].value == 15);
+
+ // Verify that a shared index (view def is an exact copy of "summate")
+ // does not confuse the reduce stage
+ result = db.view("test/summate2", {startkey:4,endkey:6});
+ T(result.rows[0].value == 15);
+
+ for(var i=1; i<numDocs/2; i+=30) {
+ result = db.view("test/summate", {startkey:i,endkey:numDocs-i});
+ T(result.rows[0].value == summate(numDocs-i) - summate(i-1));
+ }
+
+ T(db.deleteDoc(designDoc).ok);
+ T(db.open(designDoc._id) == null);
+ T(db.view("test/no_docs") == null);
+
+ restartServer();
+ T(db.open(designDoc._id) == null);
+ T(db.view("test/no_docs") == null);
+ },
+
+ view_collation: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // NOTE, the values are already in their correct sort order. Consider this
+ // a specification of collation of json types.
+
+ var values = []
+
+ // special values sort before all other types
+ values.push(null)
+ values.push(false)
+ values.push(true)
+
+ // then numbers
+ values.push(1)
+ values.push(2)
+ values.push(3.0)
+ values.push(4)
+
+ // then text, case sensitive
+ values.push("a")
+ values.push("A")
+ values.push("aa")
+ values.push("b")
+ values.push("B")
+ values.push("ba")
+ values.push("bb")
+
+ // then arrays. compared element by element until different.
+ // Longer arrays sort after their prefixes
+ values.push(["a"])
+ values.push(["b"])
+ values.push(["b","c"])
+ values.push(["b","c", "a"])
+ values.push(["b","d"])
+ values.push(["b","d", "e"])
+
+ // then object, compares each key value in the list until different.
+ // larger objects sort after their subset objects.
+ values.push({a:1})
+ values.push({a:2})
+ values.push({b:1})
+ values.push({b:2})
+ values.push({b:2, a:1}) // Member order does matter for collation.
+ // CouchDB preserves member order
+ // but doesn't require that clients will.
+ // (this test might fail if used with a js engine
+ // that doesn't preserve order)
+ values.push({b:2, c:2})
+
+ for (var i=0; i<values.length; i++) {
+ db.save({_id:(i).toString(), foo:values[i]});
+ }
+
+ var queryFun = function(doc) { emit(doc.foo, null); }
+ var rows = db.query(queryFun).rows;
+ for (i=0; i<values.length; i++) {
+ T(equals(rows[i].key, values[i]))
+ }
+
+ // everything has collated correctly. Now to check the descending output
+ rows = db.query(queryFun, null, {descending: true}).rows
+ for (i=0; i<values.length; i++) {
+ T(equals(rows[i].key, values[values.length - 1 -i]))
+ }
+
+ // now check the key query args
+ for (i=1; i<values.length; i++) {
+ var queryOptions = {key:values[i]}
+ rows = db.query(queryFun, null, queryOptions).rows;
+ T(rows.length == 1 && equals(rows[0].key, values[i]))
+ }
+ },
+
+ view_conflicts: function(debug) {
+ var dbA = new CouchDB("test_suite_db_a");
+ dbA.deleteDb();
+ dbA.createDb();
+ var dbB = new CouchDB("test_suite_db_b");
+ dbB.deleteDb();
+ dbB.createDb();
+ if (debug) debugger;
+
+ var docA = {_id: "foo", bar: 42};
+ T(dbA.save(docA).ok);
+ CouchDB.replicate(dbA.name, dbB.name);
+
+ var docB = dbB.open("foo");
+ docB.bar = 43;
+ dbB.save(docB);
+ docA.bar = 41;
+ dbA.save(docA);
+ CouchDB.replicate(dbA.name, dbB.name);
+
+ var doc = dbB.open("foo", {conflicts: true});
+ T(doc._conflicts.length == 1);
+ var conflictRev = doc._conflicts[0];
+ if (doc.bar == 41) { // A won
+ T(conflictRev == docB._rev);
+ } else { // B won
+ T(doc.bar == 43);
+ T(conflictRev == docA._rev);
+ }
+
+ var results = dbB.query(function(doc) {
+ if (doc._conflicts) {
+ emit(doc._id, doc._conflicts);
+ }
+ });
+ T(results.rows[0].value[0] == conflictRev);
+ },
+
+ view_errors: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var doc = {integer: 1, string: "1", array: [1, 2, 3]};
+ T(db.save(doc).ok);
+
+ // emitting a key value that is undefined should result in that row not
+ // being included in the view results
+ var results = db.query(function(doc) {
+ emit(doc.undef, null);
+ });
+ T(results.total_rows == 0);
+
+ // if a view function throws an exception, its results are not included in
+ // the view index, but the view does not itself raise an error
+ var results = db.query(function(doc) {
+ doc.undef(); // throws an error
+ });
+ T(results.total_rows == 0);
+
+ // if a view function includes an undefined value in the emitted key or
+ // value, an error is logged and the result is not included in the view
+ // index, and the view itself does not raise an error
+ var results = db.query(function(doc) {
+ emit([doc._id, doc.undef], null);
+ });
+ T(results.total_rows == 0);
+ },
+
+ view_pagination: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var docs = makeDocs(0, 100);
+ T(db.bulkSave(docs).ok);
+
+ var queryFun = function(doc) { emit(doc.integer, null) };
+ var i;
+
+ // page through the view ascending and going forward
+ for (i = 0; i < docs.length; i += 10) {
+ var queryResults = db.query(queryFun, null, {
+ startkey: i,
+ startkey_docid: i,
+ count: 10
+ });
+ T(queryResults.rows.length == 10)
+ T(queryResults.total_rows == docs.length)
+ T(queryResults.offset == i)
+ var j;
+ for (j = 0; j < 10;j++) {
+ T(queryResults.rows[j].key == i + j);
+ }
+ }
+
+ // page through the view ascending and going backward
+ for (i = docs.length - 1; i >= 0; i -= 10) {
+ var queryResults = db.query(queryFun, null, {
+ startkey: i,
+ startkey_docid: i,
+ count:-10
+ });
+ T(queryResults.rows.length == 10)
+ T(queryResults.total_rows == docs.length)
+ T(queryResults.offset == i - 9)
+ var j;
+ for (j = 0; j < 10;j++) {
+ T(queryResults.rows[j].key == i - 9 + j);
+ }
+ }
+
+ // page through the view descending and going forward
+ for (i = docs.length - 1; i >= 0; i -= 10) {
+ var queryResults = db.query(queryFun, null, {
+ startkey: i,
+ startkey_docid: i,
+ descending: true,
+ count: 10
+ });
+ T(queryResults.rows.length == 10)
+ T(queryResults.total_rows == docs.length)
+ T(queryResults.offset == docs.length - i - 1)
+ var j;
+ for (j = 0; j < 10; j++) {
+ T(queryResults.rows[j].key == i - j);
+ }
+ }
+
+ // page through the view descending and going backward
+ for (i = 0; i < docs.length; i += 10) {
+ var queryResults = db.query(queryFun, null, {
+ startkey: i,
+ startkey_docid: i,
+ descending: true,
+ count:-10
+ });
+ T(queryResults.rows.length == 10)
+ T(queryResults.total_rows == docs.length)
+ T(queryResults.offset == docs.length - i - 10)
+ var j;
+ for (j = 0; j < 10; j++) {
+ T(queryResults.rows[j].key == i + 9 - j);
+ }
+ }
+
+ // ignore decending=false. CouchDB should just ignore that.
+ for (i = 0; i < docs.length; i += 10) {
+ var queryResults = db.query(queryFun, null, {
+ startkey: i,
+ startkey_docid: i,
+ descending: false,
+ count: 10
+ });
+ T(queryResults.rows.length == 10)
+ T(queryResults.total_rows == docs.length)
+ T(queryResults.offset == i)
+ var j;
+ for (j = 0; j < 10;j++) {
+ T(queryResults.rows[j].key == i + j);
+ }
+ }
+
+ // test endkey_docid
+ var queryResults = db.query(function(doc) { emit(null, null);}, null, {
+ startkey: null,
+ startkey_docid: 1,
+ endkey: null,
+ endkey_docid: 40
+ });
+
+ T(queryResults.rows.length == 35)
+ T(queryResults.total_rows == docs.length)
+ T(queryResults.offset == 1)
+ T(queryResults.rows[0].id == "1");
+ T(queryResults.rows[1].id == "10");
+ T(queryResults.rows[2].id == "11");
+ T(queryResults.rows[3].id == "12");
+ T(queryResults.rows[4].id == "13");
+ T(queryResults.rows[5].id == "14");
+ T(queryResults.rows[6].id == "15");
+ T(queryResults.rows[7].id == "16");
+ T(queryResults.rows[8].id == "17");
+ T(queryResults.rows[9].id == "18");
+ T(queryResults.rows[10].id == "19");
+ T(queryResults.rows[11].id == "2");
+ T(queryResults.rows[12].id == "20");
+ T(queryResults.rows[13].id == "21");
+ T(queryResults.rows[14].id == "22");
+ T(queryResults.rows[15].id == "23");
+ T(queryResults.rows[16].id == "24");
+ T(queryResults.rows[17].id == "25");
+ T(queryResults.rows[18].id == "26");
+ T(queryResults.rows[19].id == "27");
+ T(queryResults.rows[20].id == "28");
+ T(queryResults.rows[21].id == "29");
+ T(queryResults.rows[22].id == "3");
+ T(queryResults.rows[23].id == "30");
+ T(queryResults.rows[24].id == "31");
+ T(queryResults.rows[25].id == "32");
+ T(queryResults.rows[26].id == "33");
+ T(queryResults.rows[27].id == "34");
+ T(queryResults.rows[28].id == "35");
+ T(queryResults.rows[29].id == "36");
+ T(queryResults.rows[30].id == "37");
+ T(queryResults.rows[31].id == "38");
+ T(queryResults.rows[32].id == "39");
+ T(queryResults.rows[33].id == "4");
+ T(queryResults.rows[34].id == "40");
+
+ },
+
+ view_sandboxing: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var doc = {integer: 1, string: "1", array: [1, 2, 3]};
+ T(db.save(doc).ok);
+/*
+ // make sure that attempting to change the document throws an error
+ var results = db.query(function(doc) {
+ doc.integer = 2;
+ emit(null, doc);
+ });
+ T(results.total_rows == 0);
+
+ var results = db.query(function(doc) {
+ doc.array[0] = 0;
+ emit(null, doc);
+ });
+ T(results.total_rows == 0);
+*/
+ // make sure that a view cannot invoke interpreter internals such as the
+ // garbage collector
+ var results = db.query(function(doc) {
+ gc();
+ emit(null, doc);
+ });
+ T(results.total_rows == 0);
+
+ // make sure that a view cannot access the map_funs array defined used by
+ // the view server
+ var results = db.query(function(doc) { map_funs.push(1); emit(null, doc) });
+ T(results.total_rows == 0);
+
+ // make sure that a view cannot access the map_results array defined used by
+ // the view server
+ var results = db.query(function(doc) { map_results.push(1); emit(null, doc) });
+ T(results.total_rows == 0);
+ },
+
+ view_xml: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ db.save({content: "<doc><title id='xml'>Testing XML</title></doc>"});
+ db.save({content: "<doc><title id='e4x'>Testing E4X</title></doc>"});
+
+ var results = db.query(
+ "function(doc) {\n" +
+ " var xml = new XML(doc.content);\n" +
+ " emit(xml.title.text(), null);\n" +
+ "}");
+ T(results.total_rows == 2);
+ T(results.rows[0].key == "Testing E4X");
+ T(results.rows[1].key == "Testing XML");
+
+ var results = db.query(
+ "function(doc) {\n" +
+ " var xml = new XML(doc.content);\n" +
+ " emit(xml.title.@id, null);\n" +
+ "}");
+ T(results.total_rows == 2);
+ T(results.rows[0].key == "e4x");
+ T(results.rows[1].key == "xml");
+ },
+
+ replication: function(debug) {
+ if (debug) debugger;
+ var host = CouchDB.host;
+ var dbPairs = [
+ {source:"test_suite_db_a",
+ target:"test_suite_db_b"},
+ {source:"test_suite_db_a",
+ target:"http://" + host + "/test_suite_db_b"},
+ {source:"http://" + host + "/test_suite_db_a",
+ target:"test_suite_db_b"},
+ {source:"http://" + host + "/test_suite_db_a",
+ target:"http://" + host + "/test_suite_db_b"}
+ ]
+ var dbA = new CouchDB("test_suite_db_a");
+ var dbB = new CouchDB("test_suite_db_b");
+ var numDocs = 10;
+ var xhr;
+ for (var testPair = 0; testPair < dbPairs.length; testPair++) {
+ var A = dbPairs[testPair].source
+ var B = dbPairs[testPair].target
+
+ dbA.deleteDb();
+ dbA.createDb();
+ dbB.deleteDb();
+ dbB.createDb();
+
+ var docs = makeDocs(0, numDocs);
+ T(dbA.bulkSave(docs).ok);
+
+ T(CouchDB.replicate(A, B).ok);
+
+ for (var j = 0; j < numDocs; j++) {
+ docA = dbA.open("" + j);
+ docB = dbB.open("" + j);
+ T(docA._rev == docB._rev);
+ }
+
+ // now check binary attachments
+ var binDoc = {
+ _id:"bin_doc",
+ _attachments:{
+ "foo.txt": {
+ "type":"base64",
+ "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ dbA.save(binDoc);
+
+ T(CouchDB.replicate(A, B).ok);
+ T(CouchDB.replicate(B, A).ok);
+
+ p("TODO fix attachment fetch problem");
+ // xhr = CouchDB.request("GET", "/test_suite_db_a/bin_doc/foo.txt");
+ // T(xhr.responseText == "This is a base64 encoded text")
+ //
+ // xhr = CouchDB.request("GET", "/test_suite_db_b/bin_doc/foo.txt");
+ // T(xhr.responseText == "This is a base64 encoded text")
+
+ dbA.save({_id:"foo1",value:"a"});
+
+ T(CouchDB.replicate(A, B).ok);
+ T(CouchDB.replicate(B, A).ok);
+
+ docA = dbA.open("foo1");
+ docB = dbB.open("foo1");
+ T(docA._rev == docB._rev);
+
+ dbA.deleteDoc(docA);
+
+ T(CouchDB.replicate(A, B).ok);
+ T(CouchDB.replicate(B, A).ok);
+
+ T(dbA.open("foo1") == null);
+ T(dbB.open("foo1") == null);
+
+ dbA.save({_id:"foo",value:"a"});
+ dbB.save({_id:"foo",value:"b"});
+
+ T(CouchDB.replicate(A, B).ok);
+ T(CouchDB.replicate(B, A).ok);
+
+ // open documents and include the conflict meta data
+ docA = dbA.open("foo", {conflicts: true});
+ docB = dbB.open("foo", {conflicts: true});
+
+ // make sure the same rev is in each db
+ T(docA._rev === docB._rev);
+
+ // make sure the conflicts are the same in each db
+ T(docA._conflicts[0] === docB._conflicts[0]);
+
+ // delete a conflict.
+ dbA.deleteDoc({_id:"foo", _rev:docA._conflicts[0]});
+
+ // replicate the change
+ T(CouchDB.replicate(A, B).ok);
+
+ // open documents and include the conflict meta data
+ docA = dbA.open("foo", {conflicts: true});
+ docB = dbB.open("foo", {conflicts: true});
+
+ // We should have no conflicts this time
+ T(docA._conflicts === undefined)
+ T(docB._conflicts === undefined);
+ }
+ },
+
+ etags_head: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var xhr;
+
+ // create a new doc
+ xhr = CouchDB.request("PUT", "/test_suite_db/1", {
+ body: "{}"
+ });
+ T(xhr.status == 201);
+
+ // extract the ETag header values
+ var etag = xhr.getResponseHeader("etag")
+
+ // get the doc and verify the headers match
+ xhr = CouchDB.request("GET", "/test_suite_db/1");
+ T(etag == xhr.getResponseHeader("etag"));
+
+ // 'head' the doc and verify the headers match
+ xhr = CouchDB.request("HEAD", "/test_suite_db/1", {
+ headers: {"if-none-match": "s"}
+ });
+ T(etag == xhr.getResponseHeader("etag"));
+
+ // replace a doc
+ xhr = CouchDB.request("PUT", "/test_suite_db/1", {
+ body: "{}",
+ headers: {"if-match": etag}
+ });
+ T(xhr.status == 201);
+
+ // extract the new ETag value
+ var etagOld= etag;
+ etag = xhr.getResponseHeader("etag")
+
+ // fail to replace a doc
+ xhr = CouchDB.request("PUT", "/test_suite_db/1", {
+ body: "{}"
+ });
+ T(xhr.status == 412)
+
+ // verify get w/Etag
+ xhr = CouchDB.request("GET", "/test_suite_db/1", {
+ headers: {"if-none-match": etagOld}
+ });
+ T(xhr.status == 200);
+ xhr = CouchDB.request("GET", "/test_suite_db/1", {
+ headers: {"if-none-match": etag}
+ });
+ T(xhr.status == 304);
+
+ // fail to delete a doc
+ xhr = CouchDB.request("DELETE", "/test_suite_db/1", {
+ headers: {"if-match": etagOld}
+ });
+ T(xhr.status == 412);
+
+ //now do it for real
+ xhr = CouchDB.request("DELETE", "/test_suite_db/1", {
+ headers: {"if-match": etag}
+ });
+ T(xhr.status == 200)
+ },
+
+ compact: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+ var docs = makeDocs(0, 10);
+ var saveResult = db.bulkSave(docs);
+ T(saveResult.ok);
+
+ var binAttDoc = {
+ _id: "bin_doc",
+ _attachments:{
+ "foo.txt": {
+ content_type:"text/plain",
+ data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ T(db.save(binAttDoc).ok);
+
+ var originalsize = db.info().disk_size;
+
+ for(var i in docs) {
+ db.deleteDoc(docs[i]);
+ }
+ var deletesize = db.info().disk_size;
+ T(deletesize > originalsize);
+
+ var xhr = CouchDB.request("POST", "/test_suite_db/_compact");
+ T(xhr.status == 202);
+ // compaction isn't instantaneous, loop until done
+ while (db.info().compact_running) {};
+
+ restartServer();
+ p("TODO fix attachment fetch problem");
+ // var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt");
+ // T(xhr.responseText == "This is a base64 encoded text")
+ // T(xhr.getResponseHeader("Content-Type") == "text/plain")
+ T(db.info().doc_count == 1);
+ T(db.info().disk_size < deletesize);
+ },
+
+ purge: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ /*
+ purge is not to be confused with a document deletion. It removes the
+ document and all edit history from the local instance of the database.
+ */
+
+ var numDocs = 10;
+
+ var designDoc = {
+ _id:"_design/test",
+ language: "javascript",
+ views: {
+ all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"},
+ single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}
+ }
+ }
+
+ T(db.save(designDoc).ok);
+
+ T(db.bulkSave(makeDocs(1, numDocs + 1)).ok);
+
+ // go ahead and validate the views before purging
+ var rows = db.view("test/all_docs_twice").rows;
+ for (var i = 0; i < numDocs; i++) {
+ T(rows[2*i].key == i+1);
+ T(rows[(2*i)+1].key == i+1);
+ }
+ T(db.view("test/single_doc").total_rows == 1);
+
+ var doc1 = db.open("1");
+ var doc2 = db.open("2");
+
+ // purge the documents
+ var xhr = CouchDB.request("POST", "/test_suite_db/_purge", {
+ body: JSON.stringify({"1":[doc1._rev], "2":[doc2._rev]}),
+ });
+ T(xhr.status == 200);
+
+ var result = JSON.parse(xhr.responseText);
+ T(result.purged["1"][0] == doc1._rev);
+ T(result.purged["2"][0] == doc2._rev);
+
+ T(db.open("1") == null);
+ T(db.open("2") == null);
+
+ var rows = db.view("test/all_docs_twice").rows;
+ for (var i = 2; i < numDocs; i++) {
+ T(rows[2*(i-2)].key == i+1);
+ T(rows[(2*(i-2))+1].key == i+1);
+ }
+ T(db.view("test/single_doc").total_rows == 0);
+
+ // purge documents twice in a row without loading views
+ // (causes full view rebuilds)
+
+ var doc3 = db.open("3");
+ var doc4 = db.open("4");
+
+ xhr = CouchDB.request("POST", "/test_suite_db/_purge", {
+ body: JSON.stringify({"3":[doc3._rev]}),
+ });
+
+ T(xhr.status == 200);
+
+ xhr = CouchDB.request("POST", "/test_suite_db/_purge", {
+ body: JSON.stringify({"4":[doc4._rev]}),
+ });
+
+ T(xhr.status == 200);
+
+ var rows = db.view("test/all_docs_twice").rows;
+ for (var i = 4; i < numDocs; i++) {
+ T(rows[2*(i-4)].key == i+1);
+ T(rows[(2*(i-4))+1].key == i+1);
+ }
+ T(db.view("test/single_doc").total_rows == 0);
+ }
+};
+
+function makeDocs(start, end, templateDoc) {
+ var templateDocSrc = templateDoc ? templateDoc.toSource() : "{}"
+ if (end === undefined) {
+ end = start;
+ start = 0;
+ }
+ var docs = []
+ for (var i = start; i < end; i++) {
+ var newDoc = eval("(" + templateDocSrc + ")");
+ newDoc._id = (i).toString();
+ newDoc.integer = i
+ newDoc.string = (i).toString();
+ docs.push(newDoc)
+ }
+ return docs;
+}
+
+// *********************** Test Framework of Sorts ************************* //
+
+function patchTest(fun) {
+ var source = fun.toString();
+ var output = "";
+ var i = 0;
+ var testMarker = "T("
+ while (i < source.length) {
+ var testStart = source.indexOf(testMarker, i);
+ if (testStart == -1) {
+ output = output + source.substring(i, source.length);
+ break;
+ }
+ var testEnd = source.indexOf(");", testStart);
+ var testCode = source.substring(testStart + testMarker.length, testEnd);
+ output += source.substring(i, testStart) + "T(" + testCode + "," + JSON.stringify(testCode);
+ i = testEnd;
+ }
+ try {
+ return eval("(" + output + ")");
+ } catch (e) {
+ return null;
+ }
+}
+
+
+
+function runAllTests() {
+ var rows = $("#tests tbody.content tr");
+ $("td", rows).html("&nbsp;");
+ $("td.status", rows).removeClass("error").removeClass("failure").removeClass("success").text("not run");
+ var offset = 0;
+ function runNext() {
+ if (offset < rows.length) {
+ var row = rows.get(offset);
+ runTest($("th button", row).get(0), function() {
+ offset += 1;
+ setTimeout(runNext, 1000);
+ });
+ }
+ }
+ runNext();
+}
+
+var numFailures = 0;
+var currentRow = null;
+
+
+function runAllTestsConsole() {
+ var numTests = 0;
+ var debug = false;
+ for (var t in tests) {
+ p(t);
+ numTests += 1;
+ var testFun = tests[t];
+ var start = new Date().getTime();
+ try {
+ if (!debug) testFun = patchTest(testFun) || testFun;
+ testFun();
+ } catch(e) {
+ p("ERROR");
+ p("Exception raised: "+e.toString());
+ p("Backtrace: "+e.stack);
+ }
+ var duration = new Date().getTime() - start;
+ p(duration+"ms\n");
+ }
+ p("Results: "+numFailures.toString() + " failures in "+numTests+" tests.")
+};
+
+function runTest(button, callback, debug) {
+ if (currentRow != null) {
+ alert("Can not run multiple tests simultaneously.");
+ return;
+ }
+ var row = currentRow = $(button).parents("tr").get(0);
+ $("td.status", row).removeClass("error").removeClass("failure").removeClass("success");
+ $("td", row).html("&nbsp;");
+ var testFun = tests[row.id];
+ function run() {
+ numFailures = 0;
+ var start = new Date().getTime();
+ try {
+ if (debug == undefined || !debug) {
+ testFun = patchTest(testFun) || testFun;
+ }
+ testFun(debug);
+ var status = numFailures > 0 ? "failure" : "success";
+ } catch (e) {
+ var status = "error";
+ if ($("td.details ol", row).length == 0) {
+ $("<ol></ol>").appendTo($("td.details", row));
+ }
+ $("<li><b>Exception raised:</b> <code class='error'></code></li>")
+ .find("code").text(JSON.stringify(e)).end()
+ .appendTo($("td.details ol", row));
+ if (debug) {
+ currentRow = null;
+ throw e;
+ }
+ }
+ if ($("td.details ol", row).length) {
+ $("<a href='#'>Run with debugger</a>").click(function() {
+ runTest(this, undefined, true);
+ }).prependTo($("td.details ol", row));
+ }
+ var duration = new Date().getTime() - start;
+ $("td.status", row).removeClass("running").addClass(status).text(status);
+ $("td.duration", row).text(duration + "ms");
+ updateTestsFooter();
+ currentRow = null;
+ if (callback) callback();
+ }
+ $("td.status", row).addClass("running").text("running…");
+ setTimeout(run, 100);
+}
+
+function showSource(cell) {
+ var name = $(cell).text();
+ var win = window.open("", name, "width=700,height=500,resizable=yes,scrollbars=yes");
+ win.document.title = name;
+ $("<pre></pre>").text(tests[name].toString()).appendTo(win.document.body).fadeIn();
+}
+
+function updateTestsListing() {
+ for (var name in tests) {
+ if (!tests.hasOwnProperty(name)) continue;
+ var testFunction = tests[name];
+ var row = $("<tr><th></th><td></td><td></td><td></td></tr>")
+ .find("th").text(name).attr("title", "Show source").click(function() {
+ showSource(this);
+ }).end()
+ .find("td:nth(0)").addClass("status").text("not run").end()
+ .find("td:nth(1)").addClass("duration").html("&nbsp;").end()
+ .find("td:nth(2)").addClass("details").html("&nbsp;").end();
+ $("<button type='button' class='run' title='Run test'></button>").click(function() {
+ this.blur();
+ runTest(this);
+ return false;
+ }).prependTo(row.find("th"));
+ row.attr("id", name).appendTo("#tests tbody.content");
+ }
+ $("#tests tr").removeClass("odd").filter(":odd").addClass("odd");
+ updateTestsFooter();
+}
+
+function updateTestsFooter() {
+ var tests = $("#tests tbody.content tr td.status");
+ var testsRun = tests.not(":contains('not run'))");
+ var testsFailed = testsRun.not(".success");
+ $("#tests tbody.footer td").text(testsRun.length + " of " + tests.length +
+ " test(s) run, " + testsFailed.length + " failures");
+}
+
+// Use T to perform a test that returns false on failure and if the test fails,
+// display the line that failed.
+// Example:
+// T(MyValue==1);
+function T(arg1, arg2) {
+ if (!arg1) {
+ if (currentRow) {
+ if ($("td.details ol", currentRow).length == 0) {
+ $("<ol></ol>").appendTo($("td.details", currentRow));
+ }
+ $("<li><b>Assertion failed:</b> <code class='failure'></code></li>")
+ .find("code").text((arg2 != null ? arg2 : arg1).toString()).end()
+ .appendTo($("td.details ol", currentRow));
+ } else { // for console
+ p("Assertion failed: "+(arg2 != null ? arg2 : arg1).toString());
+ }
+ numFailures += 1
+ }
+}
+
+function equals(a,b) {
+ if (a === b) return true;
+ try {
+ return repr(a) === repr(b);
+ } catch (e) {
+ return false;
+ }
+}
+
+function repr(val) {
+ if (val === undefined) {
+ return null;
+ } else if (val === null) {
+ return "null";
+ } else {
+ return JSON.stringify(val);
+ }
+}
+
+function restartServer() {
+ var xhr = CouchDB.request("POST", "/_restart");
+ do {
+ xhr = CouchDB.request("GET", "/");
+ } while(xhr.status != 200);
+}
+
+
+p("Running CouchDB Test Suite\n");
+p("Host: "+CouchDB.host);
+p("Port: "+CouchDB.port);
+
+try {
+ p("Version: "+CouchDB.getVersion()+"\n");
+ runAllTestsConsole();
+
+} catch (e) {
+ p(e.toString());
+}
+
+p("\nFinished");
diff --git a/trunk/src/couchdb/Makefile.am b/trunk/src/couchdb/Makefile.am
index 38970b0..8245154 100644
--- a/trunk/src/couchdb/Makefile.am
+++ b/trunk/src/couchdb/Makefile.am
@@ -24,7 +24,8 @@ couch_erl_driver_la_CFLAGS = $(ICU_LOCAL_FLAGS)
couch_erl_driver_la_LIBADD = -licuuc -licudata -licui18n
locallibbin_PROGRAMS = couchjs
-couchjs_SOURCES = couch_js.c
+couchjs_SOURCES = couch_js.c curlhelper.c
+couchjs_LDADD = -lcurl -lgssapi_krb5
couchinclude_DATA = couch_db.hrl
diff --git a/trunk/src/couchdb/couch_js.c b/trunk/src/couchdb/couch_js.c
index 8481bc5..4e217bb 100644
--- a/trunk/src/couchdb/couch_js.c
+++ b/trunk/src/couchdb/couch_js.c
@@ -13,8 +13,16 @@ specific language governing permissions and limitations under the License.
*/
+#include <stdlib.h>
#include <stdio.h>
+#include <string.h>
+#include "curlhelper.h"
#include <jsapi.h>
+#include <curl/curl.h>
+
+#ifndef CURLOPT_COPYPOSTFIELDS
+ #define CURLOPT_COPYPOSTFIELDS 10165
+#endif
int gExitCode = 0;
int gStackChunkSize = 8L * 1024L;
@@ -401,6 +409,534 @@ PrintError(JSContext *context, const char *message, JSErrorReport *report) {
fprintf(stderr, "%s\n", message);
}
+JSBool ThrowError(JSContext *cx, const char *message)
+{
+ void *mark;
+ jsval *args;
+ jsval exc;
+
+ printf("%s\n",message);
+
+ args = JS_PushArguments(cx, &mark, "s", message);
+ if (args) {
+ if (JS_CallFunctionName(cx, JS_GetGlobalObject(cx),
+ "Error", 1, args, &exc))
+ JS_SetPendingException(cx, exc);
+ JS_PopArguments(cx, mark);
+ }
+
+ return JS_FALSE;
+}
+
+typedef struct buffer_counter {
+ Buffer buffer;
+ int pos;
+}* BufferCount;
+
+size_t curl_read(void *ptr, size_t size, size_t nmemb, void *stream) {
+ if( size == 0 || nmemb == 0) {
+ return 0;
+ }
+
+ char* databuffer = (char*)ptr;
+ Buffer b = ((BufferCount)stream)->buffer;
+ int* pos = &(((BufferCount)stream)->pos);
+
+ if((b->count - *pos) == 0) {
+ return 0;
+ }
+
+ int readlength = size*nmemb;
+ int spaceleft = b->count - *pos;
+ int i;
+
+ if(readlength < spaceleft) {
+ copy_Buffer(b,databuffer,*pos,readlength);
+ *(pos) += readlength;
+ return readlength;
+ } else {
+ copy_Buffer(b,databuffer,*pos,spaceleft);
+ *(pos) += spaceleft;
+ return spaceleft;
+ }
+}
+
+size_t curl_write(void *ptr, size_t size, size_t nmemb, void *stream) {
+ if( size == 0 || nmemb == 0 )
+ return 0;
+
+ char *data, *tmp;
+ Buffer b;
+
+ data = (char *)ptr;
+ b = (Buffer)stream;
+
+ append_Buffer(b,data,size*nmemb);
+
+ return size*nmemb;
+}
+
+// This uses MALLOC dont forget to free
+char* JSValToChar(JSContext* context, jsval* arg) {
+ if(!JSVAL_IS_STRING(*arg)) {
+ return NULL;
+ }
+
+ char *c, *tmp;
+ JSString *jsmsg;
+ size_t len;
+
+ jsmsg = JS_ValueToString(context,*arg);
+ len = JS_GetStringLength(jsmsg);
+ tmp = JS_GetStringBytes(jsmsg);
+
+ c = (char*)malloc(len+1);
+ c[len] = '\0';
+
+ int i;
+
+ for(i = 0;i < len;i++) {
+ c[i] = tmp[i];
+ }
+
+ return c;
+}
+
+JSBool BufferToJSVal(JSContext *context, Buffer b, jsval *rval) {
+ char* c;
+ JSString *str;
+
+ // Important for char* to be JS_malloced, otherwise js wont let you use it in the NewString method
+ c = JS_malloc(context, b->count * sizeof(char));
+ copy_Buffer(b,c,0,b->count);
+
+
+ /* Initialize a JSString object */
+ str = JS_NewString(context, c, b->count);
+
+ if (!str) {
+ JS_free(context, c);
+ return JS_FALSE;
+ }
+
+ // Set Return Value
+ *rval = STRING_TO_JSVAL(str);
+ if(rval == NULL) {
+ return JS_FALSE;
+ }
+ return JS_TRUE;
+}
+
+struct curl_slist* generateCurlHeaders(JSContext* context,jsval* arg) {
+ // If arg is an object then we go the header-hash route else return NULL
+
+ if(!JSVAL_IS_NULL(*arg)) {
+
+ struct curl_slist *slist = NULL;
+ JSObject* header_obj;
+
+ // If we fail to convert arg2 to an object. Error!
+ if(!JS_ValueToObject(context,*arg,&header_obj)) {
+ return NULL;
+ }
+
+ JSObject* iterator = JS_NewPropertyIterator(context,header_obj);
+
+ jsval *jsProperty = JS_malloc(context,sizeof(jsval));
+ jsval *jsValue = JS_malloc(context,sizeof(jsval));
+ jsid *jsId = JS_malloc(context,sizeof(jsid));
+
+ while(JS_NextProperty(context,iterator,jsId) == JS_TRUE) {
+
+ if(*jsId == JSVAL_VOID) {
+ break;
+ }
+
+ // TODO: Refactor this maybe make a JSValAppendBuffer method b/c that is what you really want to do.
+
+ Buffer bTmp = init_Buffer();
+ JS_IdToValue(context,*jsId,jsProperty);
+ char* jsPropertyName = JSValToChar(context,jsProperty);
+
+ // TODO: Remove strlen =/
+ append_Buffer(bTmp,jsPropertyName,strlen(jsPropertyName));
+ append_Buffer(bTmp,": ",2);
+
+ JS_GetProperty(context,header_obj,jsPropertyName,jsValue);
+ char* jsPropertyValue = JSValToChar(context,jsValue);
+ // TODO: Remove strlen =/
+ append_Buffer(bTmp,jsPropertyValue,strlen(jsPropertyValue));
+ append_Buffer(bTmp,"",1);
+
+ slist = curl_slist_append(slist,bTmp->data);
+
+ free_Buffer(bTmp);
+ free(jsPropertyValue);
+ free(jsPropertyName);
+ }
+
+ JS_free(context,jsProperty);
+ JS_free(context,jsValue);
+ JS_free(context,jsId);
+
+ return slist;
+
+ } else {
+ return NULL;
+ }
+}
+
+static JSBool
+GetHttp(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
+ CURL* handle;
+ Buffer b;
+ char *url;
+ size_t charslen, readlen;
+
+ // Run GC
+ JS_MaybeGC(context);
+
+ // Init Curl
+ if((handle = curl_easy_init()) == NULL) {
+ return JS_FALSE;
+ }
+
+ // Get URL
+ url = JSValToChar(context,argv);
+ if( url == NULL ) {
+ return ThrowError(context,"Unable to convert url (argument 0) to a string");
+ }
+
+ b = init_Buffer(); // Allocate buffer that will store the get resultant
+
+ // Configuration
+ curl_easy_setopt(handle,CURLOPT_WRITEFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEDATA,b);
+ curl_easy_setopt(handle,CURLOPT_HEADERFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEHEADER,b);
+ curl_easy_setopt(handle,CURLOPT_URL,url);
+ curl_easy_setopt(handle,CURLOPT_HTTPGET,1);
+ curl_easy_setopt(handle,CURLOPT_NOPROGRESS,1);
+ curl_easy_setopt(handle,CURLOPT_IPRESOLVE,CURL_IPRESOLVE_V4);
+
+ struct curl_slist *slist = generateCurlHeaders(context,argv+1);
+ if(slist != NULL) {
+ curl_easy_setopt(handle,CURLOPT_HTTPHEADER,slist);
+ }
+
+ // Perform
+ int exitcode;
+
+ if((exitcode = curl_easy_perform(handle)) != 0) {
+ if(slist != NULL) {
+ curl_slist_free_all(slist);
+ }
+ curl_easy_cleanup(handle);
+ free(url);
+ free_Buffer(b);
+ return JS_FALSE;
+ }
+
+ free(url);
+ if(slist != NULL) {
+ curl_slist_free_all(slist);
+ }
+
+ /* Treat the empty string specially */
+ if (b->count == 0) {
+ free_Buffer(b);
+ *rval = JS_GetEmptyStringValue(context);
+ curl_easy_cleanup(handle);
+ return JS_TRUE;
+ }
+
+ /* Shrink the buffer to the real size and store its value in rval */
+ shrink_Buffer(b);
+ BufferToJSVal(context,b,rval);
+
+ // Free Buffer
+ free_Buffer(b);
+
+ if(rval == NULL) {
+ curl_easy_cleanup(handle);
+ return JS_FALSE;
+ }
+
+ JS_MaybeGC(context);
+
+ curl_easy_cleanup(handle);
+
+ return JS_TRUE;
+}
+
+static JSBool
+PostHttp(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
+ CURL* handle;
+ Buffer b;
+ char *url, *body;
+ size_t charslen, readlen;
+
+ // Run GC
+ JS_MaybeGC(context);
+
+ // Init Curl
+ if((handle = curl_easy_init()) == NULL) {
+ return JS_FALSE;
+ }
+
+ // Get URL
+ if((url = JSValToChar(context,argv)) == NULL) {
+ curl_easy_cleanup(handle);
+ return JS_FALSE;
+ }
+
+ // Initialize buffer
+ b = init_Buffer();
+
+ curl_easy_setopt(handle,CURLOPT_WRITEFUNCTION,curl_write); // function that recieves data
+ curl_easy_setopt(handle,CURLOPT_WRITEDATA,b); // buffer to write the data to
+ curl_easy_setopt(handle,CURLOPT_HEADERFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEHEADER,b);
+ curl_easy_setopt(handle,CURLOPT_URL,url); // url
+ curl_easy_setopt(handle,CURLOPT_HTTPPOST,1); // Set Op. to post
+ curl_easy_setopt(handle,CURLOPT_NOPROGRESS,1); // No Progress Meter
+ curl_easy_setopt(handle,CURLOPT_IPRESOLVE,CURL_IPRESOLVE_V4); // only ipv4
+
+ if((body = JSValToChar(context,argv+1)) == NULL) { // Convert arg1 to a string
+ free(url);
+ free_Buffer(b);
+ curl_easy_cleanup(handle);
+ return JS_FALSE;
+ }
+
+ curl_easy_setopt(handle,CURLOPT_COPYPOSTFIELDS,body); // Curl wants '\0' terminated, we oblige
+ free(body);
+
+ struct curl_slist *slist = generateCurlHeaders(context,argv+2); // Initialize Headers
+ if(slist != NULL) {
+ curl_easy_setopt(handle,CURLOPT_HTTPHEADER,slist);
+ }
+
+ int exitcode;
+
+ if((exitcode = curl_easy_perform(handle)) != 0) { // Perform
+ curl_slist_free_all(slist);
+ free(url);
+ free_Buffer(b);
+ curl_easy_cleanup(handle);
+ return JS_FALSE;
+ }
+
+ free(url);
+ curl_slist_free_all(slist);
+
+ // Convert response back to javascript value and then clean
+ BufferToJSVal(context,b,rval);
+ free_Buffer(b);
+ curl_easy_cleanup(handle);
+
+ JS_MaybeGC(context);
+
+ if( rval == NULL ) {
+ return JS_FALSE;
+ }
+
+ return JS_TRUE;
+}
+
+#define CLEAN \
+ free_Buffer(b); \
+ free_Buffer(b_data->buffer); \
+ free(b_data); \
+ free(url)
+
+static JSBool
+PutHttp(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
+
+ Buffer b;
+ BufferCount b_data;
+ char *url, *data;
+ size_t charslen, readlen;
+ JSObject* header_obj;
+
+ // Run GC
+ JS_MaybeGC(context);
+
+ // Get URL
+ url = JSValToChar(context,argv);
+
+ // Allocate buffer that will store the get resultant
+ b = init_Buffer();
+
+ // Allocate data buffer and move data into them
+ b_data = (BufferCount)malloc(sizeof(Buffer) + sizeof(int));
+ b_data->buffer = init_Buffer();
+ b_data->pos = 0;
+
+ data = JSValToChar(context,(argv+1));
+ // TODO: remove strlen
+ append_Buffer(b_data->buffer,data,strlen(data));
+
+ free(data);
+
+ CURL* handle;
+
+ // Init Curl
+
+ if((handle = curl_easy_init()) == NULL) {
+ CLEAN;
+ return JS_FALSE;
+ }
+
+ // Configuration
+ curl_easy_setopt(handle,CURLOPT_WRITEFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEDATA,b);
+ curl_easy_setopt(handle,CURLOPT_HEADERFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEHEADER,b);
+ curl_easy_setopt(handle,CURLOPT_READFUNCTION,curl_read);
+ curl_easy_setopt(handle,CURLOPT_READDATA,b_data);
+ curl_easy_setopt(handle,CURLOPT_URL,url);
+ curl_easy_setopt(handle,CURLOPT_UPLOAD,1);
+
+ // Curl structure
+ struct curl_slist *slist = generateCurlHeaders(context,argv+2);
+ if(slist != NULL) {
+ curl_easy_setopt(handle,CURLOPT_HTTPHEADER,slist);
+ }
+
+ // Little Things
+ // No progress meter
+ curl_easy_setopt(handle,CURLOPT_NOPROGRESS,1);
+ // Use only ipv4
+ curl_easy_setopt(handle,CURLOPT_IPRESOLVE,CURL_IPRESOLVE_V4);
+
+ // Perform
+ int exitcode;
+
+ if((exitcode = curl_easy_perform(handle)) != 0) {
+ if(slist != NULL)
+ curl_slist_free_all(slist);
+ curl_easy_cleanup(handle);
+ CLEAN;
+ return JS_FALSE;
+ }
+
+ if(slist != NULL)
+ curl_slist_free_all(slist);
+ free_Buffer(b_data->buffer);
+ free(b_data);
+ free(url);
+
+ /* Treat the empty string specially */
+ if (b->count == 0) {
+ *rval = JS_GetEmptyStringValue(context);
+ curl_easy_cleanup(handle);
+ free_Buffer(b);
+ return JS_TRUE;
+ }
+
+ /* Shrink the buffer to the real size */
+ shrink_Buffer(b);
+
+ BufferToJSVal(context,b,rval);
+
+ free_Buffer(b);
+
+ if(rval == NULL) {
+ curl_easy_cleanup(handle);
+ return JS_FALSE;
+ }
+
+ JS_MaybeGC(context);
+
+ curl_easy_cleanup(handle);
+
+ return JS_TRUE;
+}
+
+static JSBool
+DelHttp(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
+ Buffer b;
+ char *url;
+ size_t charslen, readlen;
+ char header_name[7];
+ strcpy(header_name,"DELETE");
+
+ // Run GC
+ JS_MaybeGC(context);
+
+ // Get URL
+ url = JSValToChar(context,argv);
+
+ // Allocate buffer that will store the del resultant
+ b = init_Buffer();
+
+ CURL* handle;
+
+ // Init Curl
+ if((handle = curl_easy_init()) == NULL) {
+ free_Buffer(b);
+ return JS_FALSE;
+ }
+
+ // Configuration
+ curl_easy_setopt(handle,CURLOPT_WRITEFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEDATA,b);
+ curl_easy_setopt(handle,CURLOPT_HEADERFUNCTION,curl_write);
+ curl_easy_setopt(handle,CURLOPT_WRITEHEADER,b);
+ curl_easy_setopt(handle,CURLOPT_URL,url);
+ curl_easy_setopt(handle,CURLOPT_CUSTOMREQUEST,header_name);
+ curl_easy_setopt(handle,CURLOPT_NOPROGRESS,1);
+ curl_easy_setopt(handle,CURLOPT_IPRESOLVE,CURL_IPRESOLVE_V4);
+
+ // Curl structure
+ struct curl_slist *slist = NULL;
+ if((slist = generateCurlHeaders(context,argv+1)) != NULL) {
+ curl_easy_setopt(handle,CURLOPT_HTTPHEADER,slist);
+ }
+
+ // Perform
+ int exitcode;
+
+ if((exitcode = curl_easy_perform(handle)) != 0) {
+ if(slist != NULL)
+ curl_slist_free_all(slist);
+ curl_easy_cleanup(handle);
+ free(url);
+ free_Buffer(b);
+ return JS_FALSE;
+ }
+
+ if(slist != NULL)
+ curl_slist_free_all(slist);
+ free(url);
+
+ /* Treat the empty string specially */
+ if (b->count == 0) {
+ *rval = JS_GetEmptyStringValue(context);
+ curl_easy_cleanup(handle);
+ free_Buffer(b);
+ return JS_TRUE;
+ }
+
+ /* Shrink the buffer to the real size */
+ shrink_Buffer(b);
+
+ BufferToJSVal(context,b,rval);
+
+ if(rval == NULL) {
+ curl_easy_cleanup(handle);
+ return JS_FALSE;
+ }
+
+ JS_MaybeGC(context);
+
+ curl_easy_cleanup(handle);
+
+ return JS_TRUE;
+}
+
+
int
main(int argc, const char * argv[]) {
JSRuntime *runtime;
@@ -428,7 +964,11 @@ main(int argc, const char * argv[]) {
|| !JS_DefineFunction(context, global, "print", Print, 0, 0)
|| !JS_DefineFunction(context, global, "quit", Quit, 0, 0)
|| !JS_DefineFunction(context, global, "readline", ReadLine, 0, 0)
- || !JS_DefineFunction(context, global, "seal", Seal, 0, 0))
+ || !JS_DefineFunction(context, global, "seal", Seal, 0, 0)
+ || !JS_DefineFunction(context, global, "gethttp", GetHttp, 1, 0)
+ || !JS_DefineFunction(context, global, "posthttp", PostHttp, 2, 0)
+ || !JS_DefineFunction(context, global, "puthttp", PutHttp, 2, 0)
+ || !JS_DefineFunction(context, global, "delhttp", DelHttp, 1, 0))
return 1;
if (argc != 2) {
diff --git a/trunk/src/couchdb/curlhelper.c b/trunk/src/couchdb/curlhelper.c
new file mode 100644
index 0000000..c218887
--- /dev/null
+++ b/trunk/src/couchdb/curlhelper.c
@@ -0,0 +1,246 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include "curlhelper.h"
+
+#define TRUE 1
+#define FALSE 0
+
+Buffer init_Buffer() {
+ Buffer b;
+
+ if((b = (Buffer)malloc(sizeof(char*) + sizeof(int)*2)) == NULL) {
+ return NULL;
+ }
+
+ b->count = 0;
+ b->capacity = 50;
+
+ if(b->data = (char*)malloc(sizeof(char)*b->capacity)) {
+ return b;
+ } else {
+ return NULL;
+ }
+}
+
+void free_Buffer(Buffer b) {
+ if(b == NULL)
+ return;
+ if(b->data != NULL)
+ free(b->data);
+ free(b);
+}
+
+int append_Buffer(Buffer b, char* c, int length) {
+ int capacity_changed;
+ int new_count;
+
+ capacity_changed = FALSE;
+ new_count = b->count + length;
+
+ if(new_count > b->capacity) {
+ capacity_changed = TRUE;
+ b->capacity *= 2;
+ }
+
+ while(new_count > b->capacity) {
+ b->capacity *= 2;
+ }
+
+ if(capacity_changed) {
+ if((b->data = (char*)realloc(b->data,b->capacity)) == NULL) {
+ return FALSE;
+ }
+ }
+
+ int i;
+
+ for(i = 0;i < length;i++) {
+ *(b->data + b->count + i) = *(c + i);
+ }
+
+ b->count = new_count;
+
+ return TRUE;
+}
+
+char* toString_Buffer(Buffer b) {
+ char* result;
+
+ if((result = (char*)malloc(sizeof(char)*(b->count+1))) == NULL) {
+ return NULL;
+ }
+
+ result[b->count] = '\0';
+
+ int i;
+
+ for(i = 0;i < b->count;i++) {
+ result[i] = b->data[i];
+ }
+
+ return result;
+}
+
+int print_Buffer(Buffer b) {
+ char* c;
+
+ if((c = toString_Buffer(b)) == NULL) {
+ return FALSE;
+ }
+
+ printf("%s\n",c);
+
+ free(c);
+
+ return TRUE;
+}
+
+
+int shrink_Buffer(Buffer b) {
+ b->capacity = b->count;
+
+ if((b->data = realloc(b->data,sizeof(char)*b->capacity)) == NULL) {
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+void copy_Buffer(Buffer b, char* c, int pos, int length) {
+ if((pos + length) > b->count)
+ return;
+
+ int i;
+
+ for(i = 0; i < length;i++) {
+ *(c + i) = *(b->data + pos + i);
+ }
+}
+
+
+List init_List(int capacity) {
+ if(capacity < 5)
+ capacity = 5;
+
+ List l;
+
+ if((l = (List)malloc(sizeof(void**)+sizeof(int)*2)) == NULL) {
+ return NULL;
+ }
+
+ l->count = 0;
+ l->capacity = capacity;
+
+ if((l->elements = (void**)malloc(sizeof(void*)*l->capacity)) == NULL) {
+ return NULL;
+ }
+
+ return l;
+}
+
+void free_List(List l) {
+ if(l == NULL)
+ return;
+ if(l->elements != NULL)
+ free(l->elements);
+ free(l);
+}
+
+void* get_List(List l, int pos) {
+ if(pos > (l->count - 1)) {
+ return NULL;
+ }
+ return *(l->elements + pos);
+}
+
+void* pull_List(List l) {
+ void* r = *(l->elements);
+
+ int i;
+
+ for(i = 1; i < (l->count-1);i++) {
+ l->elements[i] = l->elements[i+1];
+ }
+ l->count -= 1;
+ return r;
+}
+
+int set_List(List l, int pos, void* ptr) {
+ if(pos > (l->count - 1)) {
+ return FALSE;
+ }
+
+ *(l->elements + pos) = ptr;
+
+ return TRUE;
+}
+
+int append_List(List l, void* ptr, int length) {
+ int capacity_changed;
+ int new_count;
+
+ capacity_changed = FALSE;
+ new_count = l->count + length;
+
+ if(new_count > l->capacity) {
+ capacity_changed = TRUE;
+ l->capacity *= 2;
+ }
+
+ while(new_count > l->capacity) {
+ l->capacity *= 2;
+ }
+
+ if(capacity_changed) {
+ if((l->elements = (void*)realloc(l->elements,l->capacity)) == NULL) {
+ return FALSE;
+ }
+ }
+
+ int i;
+
+ for(i = 0;i < length;i++) {
+ *(l->elements + l->count + i) = ptr + i;
+ }
+
+ l->count = new_count;
+
+ return TRUE;
+}
+
+int push_List(List l, void* ptr, int length) {
+ int capacity_changed;
+ int new_count;
+
+ capacity_changed = FALSE;
+ new_count = l->count + length;
+
+ if(new_count > l->capacity) {
+ capacity_changed = TRUE;
+ l->capacity *= 2;
+ }
+
+ while(new_count > l->capacity) {
+ l->capacity *= 2;
+ }
+
+ if(capacity_changed) {
+ if((l->elements = (void*)realloc(l->elements,l->capacity)) == NULL) {
+ return FALSE;
+ }
+ }
+
+ int i;
+
+ for(i = 0;i < length;i++) {
+ *(l->elements + l->count + i) = *(l->elements + i);
+ }
+
+ for(i = 0;i < length;i++) {
+ *(l->elements + i) = ptr+i;
+ }
+
+ l->count = new_count;
+
+ return TRUE;
+}
diff --git a/trunk/src/couchdb/curlhelper.h b/trunk/src/couchdb/curlhelper.h
new file mode 100644
index 0000000..baf0256
--- /dev/null
+++ b/trunk/src/couchdb/curlhelper.h
@@ -0,0 +1,34 @@
+#ifndef CURLHELPER_H
+#define CURLHELPER_H
+
+typedef struct {
+ char* data;
+ int count;
+ int capacity;
+}* Buffer;
+
+Buffer init_Buffer();
+void free_Buffer(Buffer b);
+int append_Buffer(Buffer b,char* c,int length);
+// WARNING USES MALLOC DONT FORGET TO FREE
+char* toString_Buffer(Buffer b);
+int print_Buffer(Buffer b);
+int shrink_Buffer(Buffer b);
+void copy_Buffer(Buffer b, char* c, int pos, int length);
+
+
+typedef struct {
+ void** elements;
+ int count;
+ int capacity;
+}* List;
+
+List init_List(int capacity);
+void free_List(List l);
+void* get_List(List l, int pos);
+void* pull_List(List l);
+int set_List(List l, int pos, void* ptr);
+int append_List(List l, void* ptr, int length);
+int push_List(List l, void* ptr, int length);
+
+#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment