Created
October 2, 2008 06:58
-
-
Save jchris/14299 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(" "); | |
+ $("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(" "); | |
+ 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(" ").end() | |
+ .find("td:nth(2)").addClass("details").html(" ").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