diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | share/server/loop.js | 25 | ||||
-rw-r--r-- | share/server/render.js | 183 | ||||
-rw-r--r-- | share/server/util.js | 5 | ||||
-rw-r--r-- | share/www/script/test/list_views.js | 311 | ||||
-rw-r--r-- | share/www/script/test/show_documents.js | 14 | ||||
-rw-r--r-- | src/couchdb/couch_httpd.erl | 8 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_show.erl | 181 | ||||
-rw-r--r-- | src/couchdb/couch_js.c | 9 | ||||
-rw-r--r-- | src/couchdb/couch_os_process.erl | 8 | ||||
-rw-r--r-- | src/couchdb/couch_query_servers.erl | 37 | ||||
-rw-r--r-- | test/query_server_spec.rb | 450 |
12 files changed, 740 insertions, 492 deletions
@@ -10,6 +10,7 @@ erl_crash.dump configure autom4te.cache build-aux +*.diff # ./configure diff --git a/share/server/loop.js b/share/server/loop.js index db6a9702..188692ba 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -21,6 +21,9 @@ try { sandbox.toJSON = toJSON; sandbox.respondWith = respondWith; sandbox.registerType = registerType; + sandbox.start = start; + sandbox.send = send; + sandbox.getRow = getRow; } catch (e) {} // Commands are in the form of json arrays: @@ -31,21 +34,19 @@ try { var line, cmd, cmdkey; var dispatch = { - "reset" : State.reset, - "add_fun" : State.addFun, - "map_doc" : Views.mapDoc, - "reduce" : Views.reduce, - "rereduce" : Views.rereduce, - "validate" : Validate.validate, - "show_doc" : Render.showDoc, - "list_begin" : Render.listBegin, - "list_row" : Render.listRow, - "list_tail" : Render.listTail + "reset" : State.reset, + "add_fun" : State.addFun, + "map_doc" : Views.mapDoc, + "reduce" : Views.reduce, + "rereduce" : Views.rereduce, + "validate" : Validate.validate, + "show" : Render.show, + "list" : Render.list }; while (line = eval(readline())) { - cmd = eval(line) - line_length = line.length + cmd = eval(line); + line_length = line.length; try { cmdkey = cmd.shift(); if (dispatch[cmdkey]) { diff --git a/share/server/render.js b/share/server/render.js index 13ef1322..99541eab 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -12,9 +12,10 @@ // mimeparse.js // http://code.google.com/p/mimeparse/ +// MIT Licensed http://www.opensource.org/licenses/mit-license.php // Code with comments: http://mimeparse.googlecode.com/svn/trunk/mimeparse.js // Tests: http://mimeparse.googlecode.com/svn/trunk/mimeparse-js-test.html -// Ported from version 0.1.2 +// Ported by Chris Anderson from version 0.1.2 var Mimeparse = (function() { function strip(string) { @@ -111,6 +112,8 @@ var Mimeparse = (function() { return publicMethods; })(); +var respCT; +var respTail; // this function provides a shortcut for managing responses by Accept header respondWith = function(req, responders) { var bestKey = null, accept = req.headers["Accept"]; @@ -127,11 +130,16 @@ respondWith = function(req, responders) { bestKey = req.query.format; } var rFunc = responders[bestKey || responders.fallback || "html"]; - if (rFunc) { - var resp = maybeWrapResponse(rFunc()); - resp["headers"] = resp["headers"] || {}; - resp["headers"]["Content-Type"] = bestMime; - respond(resp); + if (rFunc) { + if (isShow) { + var resp = maybeWrapResponse(rFunc()); + resp["headers"] = resp["headers"] || {}; + resp["headers"]["Content-Type"] = bestMime; + respond(["resp", resp]); + } else { + respCT = bestMime; + respTail = rFunc(); + } } else { throw({code:406, body:"Not Acceptable: "+accept}); } @@ -162,8 +170,6 @@ registerType("text", "text/plain", "txt"); registerType("html", "text/html"); registerType("xhtml", "application/xhtml+xml", "xhtml"); registerType("xml", "application/xml", "text/xml", "application/x-xml"); -// http://www.ietf.org/rfc/rfc4627.txt -registerType("json", "application/json", "text/x-json"); registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); registerType("css", "text/css"); registerType("ics", "text/calendar"); @@ -171,57 +177,148 @@ registerType("csv", "text/csv"); registerType("rss", "application/rss+xml"); registerType("atom", "application/atom+xml"); registerType("yaml", "application/x-yaml", "text/yaml"); + // just like Rails registerType("multipart_form", "multipart/form-data"); registerType("url_encoded_form", "application/x-www-form-urlencoded"); +// http://www.ietf.org/rfc/rfc4627.txt +registerType("json", "application/json", "text/x-json"); + + + +// Start chunks +var startResp = {}; +function start(resp) { + startResp = resp || {}; +}; + +function sendStart(label) { + startResp = startResp || {}; + startResp["headers"] = startResp["headers"] || {}; + startResp["headers"]["Content-Type"] = startResp["headers"]["Content-Type"] || respCT; + + respond(["start", chunks, startResp]); + chunks = []; + startResp = {}; +} +// Send chunk +var chunks = []; +function send(chunk) { + chunks.push(chunk.toString()); +}; + +function blowChunks(label) { + respond([label||"chunks", chunks]); + chunks = []; +}; + +var gotRow = false, lastRow = false; +function getRow() { + if (lastRow) return null; + if (!gotRow) { + gotRow = true; + sendStart(); + } else { + blowChunks() + } + var line = readline(); + var json = eval(line); + if (json[0] == "list_end") { + lastRow = true + return null; + } + if (json[0] != "list_row") { + respond({ + error: "query_server_error", + reason: "not a row '" + json[0] + "'"}); + quit(); + } + return json[1]; +}; + +//// +//// Render dispatcher +//// +//// +//// +//// +var isShow = false; var Render = (function() { var row_info; + return { - showDoc : function(funSrc, doc, req) { + show : function(funSrc, doc, req) { + isShow = true; var formFun = compileFunction(funSrc); - runRenderFunction(formFun, [doc, req], funSrc); - }, - listBegin : function(head, req) { - row_info = { first_key: null, row_number: 0, prev_key: null }; - runRenderFunction(funs[0], [head, null, req, null], funsrc[0]); - }, - listRow : function(row, req) { - if (row_info.first_key == null) { - row_info.first_key = row.key; - } - runRenderFunction(funs[0], [null, row, req, row_info], funsrc[0], true); - row_info.prev_key = row.key; - row_info.row_number++; + runShowRenderFunction(formFun, [doc, req], funSrc, true); }, - listTail : function(req) { - runRenderFunction(funs[0], [null, null, req, row_info], funsrc[0]); + list : function(head, req) { + isShow = false; + runListRenderFunction(funs[0], [head, req], funsrc[0], false); } } })(); -function runRenderFunction(renderFun, args, funSrc, htmlErrors) { - responseSent = false; +function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; + } else { + return resp; + } +}; + +function runShowRenderFunction(renderFun, args, funSrc, htmlErrors) { try { var resp = renderFun.apply(null, args); - if (!responseSent) { - if (resp) { - respond(maybeWrapResponse(resp)); - } else { - respond({error:"render_error",reason:"undefined response from render function"}); - } + if (resp) { + respond(["resp", maybeWrapResponse(resp)]); + } else { + renderError("undefined response from render function"); } } catch(e) { - var logMessage = "function raised error: "+e.toString(); - log(logMessage); - // log("stacktrace: "+e.stack); - var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; - respond({ - error:"render_error", - reason:errorMessage}); + respondError(e, funSrc, htmlErrors); } }; +function runListRenderFunction(renderFun, args, funSrc, htmlErrors) { + try { + gotRow = false; + lastRow = false; + respTail = ""; + if (renderFun.arity > 2) { + throw("the list API has changed for CouchDB 0.10, please upgrade your code"); + } + var resp = renderFun.apply(null, args); + if (!gotRow) { + getRow(); + } + if (typeof resp != "undefined") { + chunks.push(resp); + } else if (respTail) { + chunks.push(respTail); + } + blowChunks("end"); + } catch(e) { + respondError(e, funSrc, htmlErrors); + } +}; + +function renderError(m) { + respond({error : "render_error", reason : m}); +} + + +function respondError(e, funSrc, htmlErrors) { + var logMessage = "function raised error: "+e.toString(); + log(logMessage); + log("stacktrace: "+e.stack); + var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; + respond({ + error:"render_error", + reason:errorMessage}); +} function escapeHTML(string) { return string.replace(/&/g, "&") @@ -241,11 +338,3 @@ function htmlRenderError(e, funSrc) { return {body:msg}; }; -function maybeWrapResponse(resp) { - var type = typeof resp; - if ((type == "string") || (type == "xml")) { - return {body:resp}; - } else { - return resp; - } -}; diff --git a/share/server/util.js b/share/server/util.js index 7faf2f0b..13b8a779 100644 --- a/share/server/util.js +++ b/share/server/util.js @@ -91,10 +91,8 @@ function recursivelySeal(obj) { } } -var responseSent; // prints the object as JSON, and rescues and logs any toJSON() related errors function respond(obj) { - responseSent = true; try { print(toJSON(obj)); } catch(e) { @@ -103,10 +101,11 @@ function respond(obj) { }; log = function(message) { + // return; if (typeof message == "undefined") { message = "Error: attempting to log message of 'undefined'."; } else if (typeof message != "string") { message = toJSON(message); } - print(toJSON({log: message})); + respond(["log", message]); }; diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index 663a5930..2b7e4cb3 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -39,106 +39,127 @@ couchTests.list_views = function(debug) { } }, lists: { - simpleForm: stringFun(function(head, row, req, row_info) { - if (row) { - // we ignore headers on rows and tail - return { - body : '\n<li>Key: '+row.key - +' Value: '+row.value - +' LineNo: '+row_info.row_number+'</li>' - }; - } else if (head) { - // we return an object (like those used by external and show) - // so that we can specify headers - return { - body : '<h1>Total Rows: ' - + head.total_rows - + ' Offset: ' + head.offset - + '</h1><ul>' - }; - } else { - // tail - return {body : '</ul>'+ - '<p>FirstKey: '+(row_info ? row_info.first_key : '')+ - ' LastKey: '+(row_info ? row_info.prev_key : '')+'</p>'}; + basicBasic : stringFun(function(head, req) { + send("head"); + var row; + while(row = getRow()) { + log("row: "+toJSON(row)); + send(row.key); + }; + return "tail"; + }), + basicJSON : stringFun(function(head, req) { + start({"headers":{"Content-Type" : "application/json"}}); + send('{"head":'+toJSON(head)+', '); + send('"req":'+toJSON(req)+', '); + send('"rows":['); + var row, sep = ''; + while (row = getRow()) { + send(sep + toJSON(row)); + sep = ', '; } + return "]}"; }), - acceptSwitch: stringFun(function(head, row, req, row_info) { - return respondWith(req, { + simpleForm: stringFun(function(head, req) { + log("simpleForm"); + send('<h1>Total Rows: ' + // + head.total_rows + // + ' Offset: ' + head.offset + + '</h1><ul>'); + + // rows + var row, row_number = 0, prevKey, firstKey = null; + while (row = getRow()) { + row_number += 1; + if (!firstKey) firstKey = row.key; + prevKey = row.key; + send('\n<li>Key: '+row.key + +' Value: '+row.value + +' LineNo: '+row_number+'</li>'); + } + + // tail + return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>'; + }), + acceptSwitch: stringFun(function(head, req) { + // respondWith takes care of setting the proper headers + respondWith(req, { html : function() { - // If you're outputting text and you're not setting - // any headers, you can just return a string. - if (head) { - return "HTML <ul>"; - } else if (row) { - return '\n<li>Key: ' - +row.key+' Value: '+row.value - +' LineNo: '+row_info.row_number+'</li>'; - } else { // tail - return '</ul>'; + send("HTML <ul>"); + var row, num = 0; + while (row = getRow()) { + num ++; + send('\n<li>Key: ' + +row.key+' Value: '+row.value + +' LineNo: '+num+'</li>'); } + + // tail + return '</ul>'; }, xml : function() { - if (head) { - return '<feed xmlns="http://www.w3.org/2005/Atom">' - +'<title>Test XML Feed</title>'; - } else if (row) { - // Becase Safari can't stand to see that dastardly - // E4X outside of a string. Outside of tests you - // can just use E4X literals. + send('<feed xmlns="http://www.w3.org/2005/Atom">' + +'<title>Test XML Feed</title>'); + + while (row = getRow()) { var entry = new XML('<entry/>'); entry.id = row.id; entry.title = row.key; entry.content = row.value; - // We'll also let you return just an E4X object - // if you aren't setting headers. - return entry; - } else { - return "</feed>"; + send(entry); } + return "</feed>"; } - }) + }); }), - qsParams: stringFun(function(head, row, req, row_info) { - if(head) return {body: req.query.foo}; - else return {body: "\n"}; + qsParams: stringFun(function(head, req) { + return toJSON(req.query) + "\n"; }), - stopIter: stringFun(function(head, row, req, row_info) { - if(head) { - return {body: "head"}; - } else if(row) { - if(row_info.row_number > 2) return {stop: true}; - return {body: " " + row_info.row_number}; - } else { - return {body: " tail"}; - } + stopIter: stringFun(function(req) { + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; }), - stopIter2: stringFun(function(head, row, req, row_info) { - return respondWith(req, { + stopIter2: stringFun(function(head, req) { + respondWith(req, { html: function() { - if(head) { - return "head"; - } else if(row) { - if(row_info.row_number > 2) return {stop: true}; - return " " + row_info.row_number; - } else { - return " tail"; - } + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; } }); }), - emptyList: stringFun(function(head, row, req, row_info) { - return { body: "" }; + tooManyGetRows : stringFun(function() { + send("head"); + var row; + while(row = getRow()) { + send(row.key); + }; + getRow(); + getRow(); + getRow(); + row = getRow(); + return "after row: "+toJSON(row); }), - rowError : stringFun(function(head, row, req, row_info) { - if (head) { - return "head"; - } else if(row) { - return missingValue; - } else { - return "tail" - } + emptyList: stringFun(function() { + return " "; + }), + rowError : stringFun(function(head, req) { + send("head"); + var row = getRow(); + send(fooBarBam); // intentional error + return "tail"; }) } }; @@ -152,26 +173,40 @@ couchTests.list_views = function(debug) { T(view.total_rows == 10); // standard get - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView"); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView"); T(xhr.status == 200, "standard get should be 200"); - T(/Total Rows/.test(xhr.responseText)); - T(/Key: 1/.test(xhr.responseText)); - T(/LineNo: 0/.test(xhr.responseText)); - T(/LineNo: 5/.test(xhr.responseText)); - T(/FirstKey: 0/.test(xhr.responseText)); - T(/LastKey: 9/.test(xhr.responseText)); - - - var lines = xhr.responseText.split('\n'); - T(/LineNo: 5/.test(lines[6])); + T(/head0123456789tail/.test(xhr.responseText)); // test that etags are available var etag = xhr.getResponseHeader("etag"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView", { + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView", { headers: {"if-none-match": etag} }); T(xhr.status == 304); + // test the richness of the arguments + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView"); + T(xhr.status == 200, "standard get should be 200"); + var resp = JSON.parse(xhr.responseText); + TEquals(resp.head, {total_rows:10, offset:0}); + T(resp.rows.length == 10); + TEquals(resp.rows[0], {"id": "0","key": 0,"value": "0"}); + + TEquals(resp.req.info.db_name, "test_suite_db"); + TEquals(resp.req.verb, "GET"); + TEquals(resp.req.path, [ + "test_suite_db", + "_design", + "lists", + "_list", + "basicJSON", + "basicView" + ]); + T(resp.req.headers.Accept); + T(resp.req.headers.Host); + T(resp.req.headers["User-Agent"]); + T(resp.req.cookie); + // get with query params var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=3"); T(xhr.status == 200, "with query params"); @@ -179,26 +214,30 @@ couchTests.list_views = function(debug) { T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 9/.test(xhr.responseText)); - // with 0 rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=30"); T(xhr.status == 200, "0 rows"); T(/Total Rows/.test(xhr.responseText)); - T(/Offset: null/.test(xhr.responseText)); + + //too many Get Rows + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/tooManyGetRows/basicView"); + T(xhr.status == 200, "tooManyGetRows"); + T(/9after row: null/.test(xhr.responseText)); + // reduce with 0 rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?startkey=30"); T(xhr.status == 200, "reduce 0 rows"); T(/Total Rows/.test(xhr.responseText)); - T(/Offset: undefined/.test(xhr.responseText)); - + T(/LastKey: undefined/.test(xhr.responseText)); // when there is a reduce present, but not used var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?reduce=false"); T(xhr.status == 200, "reduce false"); T(/Total Rows/.test(xhr.responseText)); T(/Key: 1/.test(xhr.responseText)); + // when there is a reduce present, and used xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?group=true"); @@ -221,48 +260,12 @@ couchTests.list_views = function(debug) { headers: {"if-none-match": etag} }); T(xhr.status == 200, "reduce etag"); - - // with accept headers for HTML - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { - headers: { - "Accept": 'text/html' - } - }); - T(xhr.getResponseHeader("Content-Type") == "text/html"); - T(xhr.responseText.match(/HTML/)); - T(xhr.responseText.match(/Value/)); - - // now with xml - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { - headers: { - "Accept": 'application/xml' - } - }); - T(xhr.getResponseHeader("Content-Type") == "application/xml"); - T(xhr.responseText.match(/XML/)); - T(xhr.responseText.match(/entry/)); - - // now with extra qs params - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/qsParams/basicView?foo=blam"); - T(xhr.responseText.match(/blam/)); - - // aborting iteration - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/basicView"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "basic stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "stop 2"); - - // aborting iteration with reduce - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/withReduce?group=true"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop 2"); // empty list var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/emptyList/basicView"); - T(xhr.responseText.match(/^$/)); + T(xhr.responseText.match(/^ $/)); xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/emptyList/withReduce?group=true"); - T(xhr.responseText.match(/^$/)); + T(xhr.responseText.match(/^ $/)); // multi-key fetch var xhr = CouchDB.request("POST", "/test_suite_db/_design/lists/_list/simpleForm/basicView", { @@ -281,7 +284,45 @@ couchTests.list_views = function(debug) { }); T(xhr.status == 400); T(/query_parse_error/.test(xhr.responseText)); - + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/rowError/basicView"); - T(/<h1>Render Error<\/h1>/.test(xhr.responseText)); + T(/ReferenceError/.test(xhr.responseText)); + + + // now with extra qs params + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/qsParams/basicView?foo=blam"); + T(xhr.responseText.match(/blam/)); + + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/basicView"); + // T(xhr.getResponseHeader("Content-Type") == "text/plain"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "basic stop"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "stop 2"); + + // aborting iteration with reduce + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/withReduce?group=true"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop 2"); + + // with accept headers for HTML + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { + headers: { + "Accept": 'text/html' + } + }); + T(xhr.getResponseHeader("Content-Type") == "text/html"); + T(xhr.responseText.match(/HTML/)); + T(xhr.responseText.match(/Value/)); + + // now with xml + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { + headers: { + "Accept": 'application/xml' + } + }); + T(xhr.getResponseHeader("Content-Type") == "application/xml"); + T(xhr.responseText.match(/XML/)); + T(xhr.responseText.match(/entry/)); }; diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index bf12e4c6..945bd1da 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -106,9 +106,7 @@ couchTests.show_documents = function(debug) { registerType("foo", "application/foo","application/x-foo"); return respondWith(req, { html : function() { - return { - body:"Ha ha, you said \"" + doc.word + "\"." - }; + return "Ha ha, you said \"" + doc.word + "\"."; }, xml : function() { var xml = new XML('<xml><node/></xml>'); @@ -145,10 +143,14 @@ couchTests.show_documents = function(debug) { // hello template world xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/"+docid); T(xhr.responseText == "Hello World"); +// +// }; +// +// function foo() { - // error stacktraces - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); - T(JSON.parse(xhr.responseText).error == "render_error"); + // // error stacktraces + // xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); + // T(JSON.parse(xhr.responseText).error == "render_error"); // hello template world (no docid) xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello"); diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 457ab519..3417b850 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -177,6 +177,14 @@ handle_request(MochiReq, DefaultFun, % ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]), % ?LOG_DEBUG("Stacktrace: ~p",[erlang:get_stacktrace()]), send_error(HttpReq, Error); + error:badarg -> + ?LOG_ERROR("Badarg error in HTTP request",[]), + ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]), + send_error(HttpReq, badarg); + error:function_clause -> + ?LOG_ERROR("function_clause error in HTTP request",[]), + ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]), + send_error(HttpReq, function_clause); Tag:Error -> ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]), ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]), diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl index 49dd88bc..ef5d41d1 100644 --- a/src/couchdb/couch_httpd_show.erl +++ b/src/couchdb/couch_httpd_show.erl @@ -117,47 +117,6 @@ send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Keys) -> end end. -make_map_start_resp_fun(QueryServer, Db) -> - fun(Req, CurrentEtag, TotalViewCount, Offset, _Acc) -> - ExternalResp = couch_query_servers:render_list_head(QueryServer, - Req, Db, TotalViewCount, Offset), - JsonResp = apply_etag(ExternalResp, CurrentEtag), - #extern_resp_args{ - code = Code, - data = BeginBody, - ctype = CType, - headers = ExtHeaders - } = couch_httpd_external:parse_external_response(JsonResp), - JsonHeaders = couch_httpd_external:default_or_content_type(CType, ExtHeaders), - {ok, Resp} = start_chunked_response(Req, Code, JsonHeaders), - {ok, Resp, binary_to_list(BeginBody)} - end. - -make_map_send_row_fun(QueryServer, Req) -> - fun(Resp, Db2, {{Key, DocId}, Value}, _IncludeDocs, RowFront) -> - try - JsonResp = couch_query_servers:render_list_row(QueryServer, - Req, Db2, {{Key, DocId}, Value}), - #extern_resp_args{ - stop = StopIter, - data = RowBody - } = couch_httpd_external:parse_external_response(JsonResp), - case StopIter of - true -> {stop, ""}; - _ -> - Chunk = RowFront ++ binary_to_list(RowBody), - case Chunk of - [] -> ok; - _ -> send_chunk(Resp, Chunk) - end, - {ok, ""} - end - catch - throw:Error -> - send_chunked_error(Resp, Error), - throw({already_sent, Resp, Error}) - end - end. output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) -> #view_query_args{ @@ -179,7 +138,7 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), StartListRespFun = make_map_start_resp_fun(QueryServer, Db), - SendListRowFun = make_map_send_row_fun(QueryServer, Req), + SendListRowFun = make_map_send_row_fun(QueryServer), FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, RowCount, #view_fold_helper_funs{ @@ -189,7 +148,7 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer }), FoldAccInit = {Limit, SkipCount, undefined, []}, {ok, FoldResult} = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit), - finish_list(Req, Db, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) + finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) end); output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) -> @@ -210,7 +169,7 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), StartListRespFun = make_map_start_resp_fun(QueryServer, Db), - SendListRowFun = make_map_send_row_fun(QueryServer, Req), + SendListRowFun = make_map_send_row_fun(QueryServer), FoldAccInit = {Limit, SkipCount, undefined, []}, {ok, FoldResult} = lists:foldl( @@ -226,49 +185,64 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer }), couch_view:fold(View, {Key, StartDocId}, Dir, FoldlFun, FoldAcc) end, {ok, FoldAccInit}, Keys), - finish_list(Req, Db, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) + finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) end). -make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag) -> - fun(Req2, _Etag, _Acc) -> - JsonResp = couch_query_servers:render_reduce_head(QueryServer, - Req2, Db), - JsonResp2 = apply_etag(JsonResp, CurrentEtag), - #extern_resp_args{ - code = Code, - data = BeginBody, - ctype = CType, - headers = ExtHeaders - } = couch_httpd_external:parse_external_response(JsonResp2), - JsonHeaders = couch_httpd_external:default_or_content_type(CType, ExtHeaders), - {ok, Resp} = start_chunked_response(Req, Code, JsonHeaders), - {ok, Resp, binary_to_list(BeginBody)} +make_map_start_resp_fun(QueryServer, Db) -> + fun(Req, Etag, TotalRows, Offset, _Acc) -> + Head = {[{<<"total_rows">>, TotalRows}, {<<"offset">>, Offset}]}, + start_list_resp(QueryServer, Req, Db, Head, Etag) end. -make_reduce_send_row_fun(QueryServer, Req, Db) -> - fun(Resp, {Key, Value}, RowFront) -> - try - JsonResp = couch_query_servers:render_reduce_row(QueryServer, - Req, Db, {Key, Value}), - #extern_resp_args{ - stop = StopIter, - data = RowBody - } = couch_httpd_external:parse_external_response(JsonResp), - case StopIter of - true -> {stop, ""}; - _ -> - Chunk = RowFront ++ binary_to_list(RowBody), - case Chunk of - [] -> ok; - _ -> send_chunk(Resp, Chunk) - end, - {ok, ""} - end - catch - throw:Error -> - send_chunked_error(Resp, Error), - throw({already_sent, Resp, Error}) +make_reduce_start_resp_fun(QueryServer, _Req, Db, _CurrentEtag) -> + fun(Req2, Etag, _Acc) -> + start_list_resp(QueryServer, Req2, Db, {[]}, Etag) + end. + +start_list_resp(QueryServer, Req, Db, Head, Etag) -> + [<<"start">>,Chunks,JsonResp] = couch_query_servers:render_list_head(QueryServer, + Req, Db, Head), + JsonResp2 = apply_etag(JsonResp, Etag), + #extern_resp_args{ + code = Code, + ctype = CType, + headers = ExtHeaders + } = couch_httpd_external:parse_external_response(JsonResp2), + JsonHeaders = couch_httpd_external:default_or_content_type(CType, ExtHeaders), + {ok, Resp} = start_chunked_response(Req, Code, JsonHeaders), + {ok, Resp, ?b2l(?l2b(Chunks))}. + +make_map_send_row_fun(QueryServer) -> + fun(Resp, Db, Row, _IncludeDocs, RowFront) -> + send_list_row(Resp, QueryServer, Db, Row, RowFront) + end. + +make_reduce_send_row_fun(QueryServer, Db) -> + fun(Resp, Row, RowFront) -> + send_list_row(Resp, QueryServer, Db, Row, RowFront) + end. + +send_list_row(Resp, QueryServer, Db, Row, RowFront) -> + try + [Go,Chunks] = couch_query_servers:render_list_row(QueryServer, Db, Row), + Chunk = RowFront ++ ?b2l(?l2b(Chunks)), + send_non_empty_chunk(Resp, Chunk), + case Go of + <<"chunks">> -> + {ok, ""}; + <<"end">> -> + {stop, stop} end + catch + throw:Error -> + send_chunked_error(Resp, Error), + throw({already_sent, Resp, Error}) + end. + +send_non_empty_chunk(Resp, Chunk) -> + case Chunk of + [] -> ok; + _ -> send_chunk(Resp, Chunk) end. output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) -> @@ -291,7 +265,7 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}), couch_httpd:etag_respond(Req, CurrentEtag, fun() -> StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag), - SendListRowFun = make_reduce_send_row_fun(QueryServer, Req, Db), + SendListRowFun = make_reduce_send_row_fun(QueryServer, Db), {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, GroupLevel, QueryArgs, CurrentEtag, @@ -303,7 +277,7 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q {ok, FoldResult} = couch_view:fold_reduce(View, Dir, {StartKey, StartDocId}, {EndKey, EndDocId}, GroupRowsFun, RespFun, FoldAccInit), - finish_list(Req, Db, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) + finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) end); output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) -> @@ -325,7 +299,7 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q couch_httpd:etag_respond(Req, CurrentEtag, fun() -> StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag), - SendListRowFun = make_reduce_send_row_fun(QueryServer, Req, Db), + SendListRowFun = make_reduce_send_row_fun(QueryServer, Db), {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, GroupLevel, QueryArgs, CurrentEtag, @@ -339,33 +313,30 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q couch_view:fold_reduce(View, Dir, {Key, StartDocId}, {Key, EndDocId}, GroupRowsFun, RespFun, FoldAcc) end, {ok, FoldAccInit}, Keys), - finish_list(Req, Db, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) + finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) end). -finish_list(Req, Db, QueryServer, Etag, FoldResult, StartListRespFun, TotalRows) -> - {Resp, BeginBody} = case FoldResult of +finish_list(Req, QueryServer, Etag, FoldResult, StartFun, TotalRows) -> + case FoldResult of {_, _, undefined, _} -> - {ok, Resp2, BeginBody2} = render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows), - {Resp2, BeginBody2}; - {_, _, Resp0, _} -> - {Resp0, ""} - end, - JsonTail = couch_query_servers:render_list_tail(QueryServer, Req, Db), - #extern_resp_args{ - data = Tail - } = couch_httpd_external:parse_external_response(JsonTail), - Chunk = BeginBody ++ binary_to_list(Tail), - case Chunk of - [] -> ok; - _ -> send_chunk(Resp, Chunk) + {ok, Resp, BeginBody} = + render_head_for_empty_list(StartFun, Req, Etag, TotalRows), + [<<"end">>, Chunks] = couch_query_servers:render_list_tail(QueryServer), + Chunk = BeginBody ++ ?b2l(?l2b(Chunks)), + send_non_empty_chunk(Resp, Chunk); + {_, _, Resp, stop} -> + ok; + {_, _, Resp, _} -> + [<<"end">>, Chunks] = couch_query_servers:render_list_tail(QueryServer), + send_non_empty_chunk(Resp, ?b2l(?l2b(Chunks))) end, send_chunk(Resp, []). render_head_for_empty_list(StartListRespFun, Req, Etag, null) -> - StartListRespFun(Req, Etag, []); + StartListRespFun(Req, Etag, []); % for reduce render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows) -> StartListRespFun(Req, Etag, TotalRows, null, []). - + send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq}=Req, Db) -> % compute etag with no doc Headers = MReq:get(headers), @@ -373,7 +344,7 @@ send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq}=Req, Db Accept = proplists:get_value('Accept', Hlist), CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept}), couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - ExternalResp = couch_query_servers:render_doc_show(Lang, ShowSrc, + [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, DocId, nil, Req, Db), JsonResp = apply_etag(ExternalResp, CurrentEtag), couch_httpd_external:send_external_response(Req, JsonResp) @@ -387,7 +358,7 @@ send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_r CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept}), % We know our etag now couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - ExternalResp = couch_query_servers:render_doc_show(Lang, ShowSrc, + [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, DocId, Doc, Req, Db), JsonResp = apply_etag(ExternalResp, CurrentEtag), couch_httpd_external:send_external_response(Req, JsonResp) diff --git a/src/couchdb/couch_js.c b/src/couchdb/couch_js.c index 045e6c7f..d95b9db0 100644 --- a/src/couchdb/couch_js.c +++ b/src/couchdb/couch_js.c @@ -247,13 +247,13 @@ GC(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { static JSBool Print(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { - uintN i, n; + uintN i; size_t cl, bl; JSString *str; jschar *chars; char *bytes; - for (i = n = 0; i < argc; i++) { + for (i = 0; i < argc; i++) { str = JS_ValueToString(context, argv[i]); if (!str) return JS_FALSE; @@ -270,9 +270,8 @@ Print(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { fprintf(stdout, "%s%s", i ? " " : "", bytes); JS_free(context, bytes); } - n++; - if (n) - fputc('\n', stdout); + + fputc('\n', stdout); fflush(stdout); return JS_TRUE; } diff --git a/src/couchdb/couch_os_process.erl b/src/couchdb/couch_os_process.erl index 66853b65..ef2e8bfc 100644 --- a/src/couchdb/couch_os_process.erl +++ b/src/couchdb/couch_os_process.erl @@ -53,7 +53,7 @@ prompt(Pid, Data) -> {ok, Result} -> Result; Error -> - ?LOG_DEBUG("OS Process Error ~p",[Error]), + ?LOG_ERROR("OS Process Error :: ~p",[Error]), throw(Error) end. @@ -81,20 +81,22 @@ readline(OsProc, Acc) when is_record(OsProc, os_proc) -> % Standard JSON functions writejson(OsProc, Data) when is_record(OsProc, os_proc) -> + % ?LOG_DEBUG("OS Process Input :: ~p", [Data]), true = writeline(OsProc, ?JSON_ENCODE(Data)). readjson(OsProc) when is_record(OsProc, os_proc) -> Line = readline(OsProc), case ?JSON_DECODE(Line) of - {[{<<"log">>,Msg}]} when is_binary(Msg) -> + [<<"log">>, Msg] when is_binary(Msg) -> % we got a message to log. Log it and continue - ?LOG_INFO("OS Process Log Message: ~s", [Msg]), + ?LOG_INFO("OS Process :: ~s", [Msg]), readjson(OsProc); {[{<<"error">>, Id}, {<<"reason">>, Reason}]} -> throw({list_to_atom(binary_to_list(Id)),Reason}); {[{<<"reason">>, Reason}, {<<"error">>, Id}]} -> throw({list_to_atom(binary_to_list(Id)),Reason}); Result -> + % ?LOG_DEBUG("OS Process Output :: ~p", [Result]), Result end. diff --git a/src/couchdb/couch_query_servers.erl b/src/couchdb/couch_query_servers.erl index ef2bde3b..5a1dc90a 100644 --- a/src/couchdb/couch_query_servers.erl +++ b/src/couchdb/couch_query_servers.erl @@ -18,9 +18,8 @@ -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2,code_change/3,stop/0]). -export([start_doc_map/2, map_docs/2, stop_doc_map/1]). -export([reduce/3, rereduce/3,validate_doc_update/5]). --export([render_doc_show/6,start_view_list/2,render_list_head/5, - render_list_row/4, render_list_tail/3, render_reduce_head/3, - render_reduce_row/4]). +-export([render_doc_show/6, start_view_list/2, + render_list_head/4, render_list_row/3, render_list_tail/1]). % -export([test/0]). -include("couch_db.hrl"). @@ -183,7 +182,7 @@ render_doc_show(Lang, ShowSrc, DocId, Doc, Req, Db) -> _ -> {{append_docid(DocId, JsonReqIn)}, couch_doc:to_json_obj(Doc, [revs])} end, try couch_os_process:prompt(Pid, - [<<"show_doc">>, ShowSrc, JsonDoc, JsonReq]) of + [<<"show">>, ShowSrc, JsonDoc, JsonReq]) of FormResp -> FormResp after @@ -195,32 +194,24 @@ start_view_list(Lang, ListSrc) -> true = couch_os_process:prompt(Pid, [<<"add_fun">>, ListSrc]), {ok, {Lang, Pid}}. -render_list_head({_Lang, Pid}, Req, Db, TotalRows, Offset) -> - Head = {[{<<"total_rows">>, TotalRows}, {<<"offset">>, Offset}]}, +render_list_head({_Lang, Pid}, Req, Db, Head) -> JsonReq = couch_httpd_external:json_req_obj(Req, Db), - couch_os_process:prompt(Pid, [<<"list_begin">>, Head, JsonReq]). + couch_os_process:prompt(Pid, [<<"list">>, Head, JsonReq]). -render_list_row({_Lang, Pid}, Req, Db, {{Key, DocId}, Value}) -> +render_list_row({_Lang, Pid}, Db, {{Key, DocId}, Value}) -> JsonRow = couch_httpd_view:view_row_obj(Db, {{Key, DocId}, Value}, false), - JsonReq = couch_httpd_external:json_req_obj(Req, Db), - couch_os_process:prompt(Pid, [<<"list_row">>, JsonRow, JsonReq]). + couch_os_process:prompt(Pid, [<<"list_row">>, JsonRow]); -render_list_tail({Lang, Pid}, Req, Db) -> - JsonReq = couch_httpd_external:json_req_obj(Req, Db), - JsonResp = couch_os_process:prompt(Pid, [<<"list_tail">>, JsonReq]), +render_list_row({_Lang, Pid}, _, {Key, Value}) -> + JsonRow = {[{key, Key}, {value, Value}]}, + couch_os_process:prompt(Pid, [<<"list_row">>, JsonRow]). + +render_list_tail({Lang, Pid}) -> + JsonResp = couch_os_process:prompt(Pid, [<<"list_end">>]), ok = ret_os_process(Lang, Pid), - JsonResp. + JsonResp. - -render_reduce_head({_Lang, Pid}, Req, Db) -> - Head = {[]}, - JsonReq = couch_httpd_external:json_req_obj(Req, Db), - couch_os_process:prompt(Pid, [<<"list_begin">>, Head, JsonReq]). -render_reduce_row({_Lang, Pid}, Req, Db, {Key, Value}) -> - JsonRow = {[{key, Key}, {value, Value}]}, - JsonReq = couch_httpd_external:json_req_obj(Req, Db), - couch_os_process:prompt(Pid, [<<"list_row">>, JsonRow, JsonReq]). init([]) -> diff --git a/test/query_server_spec.rb b/test/query_server_spec.rb index ac519cca..51eb3526 100644 --- a/test/query_server_spec.rb +++ b/test/query_server_spec.rb @@ -22,31 +22,6 @@ require 'spec' require 'json' -JSON_REQ = { - "body"=>"undefined", - "verb"=>"GET", - "info"=>{ - "disk_format_version"=>2, - "purge_seq"=>0, - "doc_count"=>9082, - "instance_start_time"=>"1243713611467271", - "update_seq"=>9512, - "disk_size"=>27541604, - "compact_running"=>false, - "db_name"=>"toast", - "doc_del_count"=>1 - }, - "cookie"=>{}, - "form"=>{}, - "query"=>{"q"=>"stuff"}, - "path"=>["toast", "_ext"], - "headers"=>{ - "User-Agent"=>"curl/7.18.1 (i386-apple-darwin9.2.2) libcurl/7.18.1 zlib/1.2.3", - "Host"=>"localhost:5984", - "Accept"=>"*/*" - } -} - class OSProcessRunner def self.run trace = false @@ -78,9 +53,9 @@ class OSProcessRunner def add_fun(fun) run(["add_fun", fun]) end - def get_chunk + def get_chunks resp = jsgets - raise "not a chunk" unless resp.first == "chunk" + raise "not a chunk" unless resp.first == "chunks" return resp[1] end def run json @@ -103,10 +78,10 @@ class OSProcessRunner # puts "err: #{err}" if err if resp rj = JSON.parse("[#{resp.chomp}]")[0] - if rj.respond_to?(:[]) && !rj.is_a?(Array) && - if rj["log"] - log = rj["log"] - puts "log: #{log}" #if @trace + if rj.respond_to?(:[]) && rj.is_a?(Array) + if rj[0] == "log" + log = rj[1] + puts "log: #{log}" if @trace rj = jsgets end end @@ -177,131 +152,300 @@ describe "query server normal case" do @qs.run(["rereduce", [@fun], vs]).should == [true, [45]] end end + # it "should validate" + describe "validation" do + before(:all) do + @fun = <<-JS + function(newDoc, oldDoc, userCtx) { + if (newDoc.bad) throw({forbidden:"bad doc"}); + "foo bar"; + } + JS + @qs.reset! + end + it "should allow good updates" do + @qs.run(["validate", @fun, {"good" => true}, {}, {}]).should == 1 + end + it "should reject invalid updates" do + @qs.run(["validate", @fun, {"bad" => true}, {}, {}]).should == {"forbidden"=>"bad doc"} + end + end describe "show" do - before(:all) do - @fun = <<-JS - function(doc, req) { - return [doc.title, doc.body].join(' - ') - } - JS - @qs.reset! - end - it "should show" do - @qs.rrun(["show_doc", @fun, - {:title => "Best ever", :body => "Doc body"}, JSON_REQ]) - @qs.jsgets.should == {"body"=>"Best ever - Doc body"} - end - end - - describe "show with headers" do - before(:all) do - @fun = <<-JS - function(doc, req) { - return { - headers : {"X-Plankton":"Rusty"}, - body : [doc.title, doc.body].join(' - ') - } - - } - JS - @qs.reset! - end - it "should show" do - @qs.rrun(["show_doc", @fun, - {:title => "Best ever", :body => "Doc body"}]) - @qs.jsgets.should == {"headers"=>{"X-Plankton"=>"Rusty"}, "body"=>"Best ever - Doc body"} - end - end - - describe "list with headers" do - before(:each) do - @fun = <<-JS - function(head, row, req) { - if (head) return {headers : {"Content-Type" : "text/plain"}, code : 200, "body" : "foo"}; - if (row) return 'some "text" here'; - return "tail"; - }; - JS - @qs.reset! - @qs.add_fun(@fun).should == true - end - it "should send head, row, and tail" do - @qs.rrun(["list_begin", {"total_rows"=>1000}, {"q" => "ok"}]) - @qs.jsgets.should == {"headers"=>{"Content-Type"=>"text/plain"}, "code"=>200, "body"=>"foo"} - @qs.run(["list_row", {"foo"=>"bar"}, {"q" => "ok"}]).should == {"body"=>"some \"text\" here"} - @qs.run(["list_tail", {"foo"=>"bar"}, {"q" => "ok"}]).should == {"body"=>"tail"} - end - end - - describe "list with headers and rows" do - before(:each) do - @fun = <<-JS - function(head, row, req) { - if (head) return {headers : {"Content-Type" : "text/plain"}, code : 200, "body" : "foo"}; - if (row) return 'row value '+row.value; - return "tail "+req.q; - }; - JS - @qs.reset! - @qs.add_fun(@fun).should == true - end - it "should render rows" do - @qs.rrun(["list_begin", {"total_rows"=>1000}, {"q" => "ok"}]) - @qs.jsgets.should == {"headers"=>{"Content-Type"=>"text/plain"}, "code"=>200, "body"=>"foo"} - @qs.run(["list_row", {"value"=>"bar"}, {"q" => "ok"}]).should == {"body"=>"row value bar"} - @qs.run(["list_row", {"value"=>"baz"}, {"q" => "ok"}]).should == {"body"=>"row value baz"} - @qs.run(["list_row", {"value"=>"bam"}, {"q" => "ok"}]).should == {"body"=>"row value bam"} - @qs.run(["list_tail", {"q" => "ok"}]).should == {"body"=>"tail ok"} - end - end - end # query server normal case + before(:all) do + @fun = <<-JS + function(doc, req) { + log("ok"); + return [doc.title, doc.body].join(' - '); + } + JS + @qs.reset! + end + it "should show" do + @qs.rrun(["show", @fun, + {:title => "Best ever", :body => "Doc body"}]) + @qs.jsgets.should == ["resp", {"body" => "Best ever - Doc body"}] + end + end + + describe "show with headers" do + before(:all) do + @fun = <<-JS + function(doc, req) { + var resp = {"code":200, "headers":{"X-Plankton":"Rusty"}}; + resp.body = [doc.title, doc.body].join(' - '); + return resp; + } + JS + @qs.reset! + end + it "should show headers" do + @qs.rrun(["show", @fun, + {:title => "Best ever", :body => "Doc body"}]) + @qs.jsgets.should == ["resp", {"code"=>200,"headers" => {"X-Plankton"=>"Rusty"}, "body" => "Best ever - Doc body"}] + end + end + +# end +# LIST TESTS +# __END__ + + describe "raw list with headers" do + before(:each) do + @fun = <<-JS + function(head, req) { + start({headers:{"Content-Type" : "text/plain"}}); + send("first chunk"); + send('second "chunk"'); + return "tail"; + }; + JS + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should do headers proper" do + @qs.rrun(["list", {"total_rows"=>1000}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["first chunk", 'second "chunk"'], {"headers"=>{"Content-Type"=>"text/plain"}}] + @qs.rrun(["list_end"]) + @qs.jsgets.should == ["end", ["tail"]] + end + end + + describe "list with rows" do + before(:each) do + @fun = <<-JS + function(head, req) { + send("first chunk"); + send(req.q); + var row; + log("about to getRow " + typeof(getRow)); + while(row = getRow()) { + send(row.key); + }; + return "tail"; + }; + JS + @qs.run(["reset"]).should == true + @qs.add_fun(@fun).should == true + end + it "should should list em" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["first chunk", "ok"], {"headers"=>{}}] + @qs.rrun(["list_row", {"key"=>"baz"}]) + @qs.get_chunks.should == ["baz"] + @qs.rrun(["list_row", {"key"=>"bam"}]) + @qs.get_chunks.should == ["bam"] + @qs.rrun(["list_end"]) + @qs.jsgets.should == ["end", ["tail"]] + end + it "should work with zero rows" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["first chunk", "ok"], {"headers"=>{}}] + @qs.rrun(["list_end"]) + @qs.jsgets.should == ["end", ["tail"]] + end + end + + describe "should buffer multiple chunks sent for a single row." do + before(:all) do + @fun = <<-JS + function(head, req) { + send("bacon"); + var row; + log("about to getRow " + typeof(getRow)); + while(row = getRow()) { + send(row.key); + send("eggs"); + }; + return "tail"; + }; + JS + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should should buffer em" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["bacon"], {"headers"=>{}}] + @qs.rrun(["list_row", {"key"=>"baz"}]) + @qs.get_chunks.should == ["baz", "eggs"] + @qs.rrun(["list_row", {"key"=>"bam"}]) + @qs.get_chunks.should == ["bam", "eggs"] + @qs.rrun(["list_end"]) + @qs.jsgets.should == ["end", ["tail"]] + end + end + + describe "example list" do + before(:all) do + @fun = <<-JS + function(head, req) { + send("first chunk"); + send(req.q); + var row; + while(row = getRow()) { + send(row.key); + }; + return "early"; + }; + JS + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should run normal" do + @qs.run(["list", {"foo"=>"bar"}, {"q" => "ok"}]).should == ["start", ["first chunk", "ok"], {"headers"=>{}}] + @qs.run(["list_row", {"key"=>"baz"}]).should == ["chunks", ["baz"]] + @qs.run(["list_row", {"key"=>"bam"}]).should == ["chunks", ["bam"]] + @qs.run(["list_row", {"key"=>"foom"}]).should == ["chunks", ["foom"]] + @qs.run(["list_row", {"key"=>"fooz"}]).should == ["chunks", ["fooz"]] + @qs.run(["list_row", {"key"=>"foox"}]).should == ["chunks", ["foox"]] + @qs.run(["list_end"]).should == ["end" , ["early"]] + end + end + + describe "only goes to 2 list" do + before(:all) do + @fun = <<-JS + function(head, req) { + send("first chunk"); + send(req.q); + var row, i=0; + while(row = getRow()) { + send(row.key); + i += 1; + if (i > 2) { + return('early tail'); + } + }; + }; + JS + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should end early" do + @qs.run(["list", {"foo"=>"bar"}, {"q" => "ok"}]). + should == ["start", ["first chunk", "ok"], {"headers"=>{}}] + @qs.run(["list_row", {"key"=>"baz"}]). + should == ["chunks", ["baz"]] - describe "query server errors" do - before(:each) do - @qs = QueryServerRunner.run - end - after(:each) do - @qs.close - end - - describe "list" do - before(:each) do - @fun = <<-JS - function(head, row, req) { - if (head) return {headers : {"Content-Type" : "text/plain"}, code : 200, "body" : "foo"}; - if (row) return 'row value '+row.value; - return "tail "+req.q; - }; - JS - @qs.run(["reset"]).should == true - @qs.add_fun(@fun).should == true - end - it "should reset in the middle" do - @qs.rrun(["list_begin", {"total_rows"=>1000}, {"q" => "ok"}]) - @qs.jsgets.should == {"headers"=>{"Content-Type"=>"text/plain"}, "code"=>200, "body"=>"foo"} - @qs.run(["list_row", {"value"=>"bar"}, {"q" => "ok"}]).should == {"body"=>"row value bar"} - @qs.run(["reset"]).should == true - end - end - end #query server that errors + @qs.run(["list_row", {"key"=>"bam"}]). + should == ["chunks", ["bam"]] + + @qs.run(["list_row", {"key"=>"foom"}]). + should == ["end", ["foom", "early tail"]] + # here's where js has to discard quit properly + @qs.run(["reset"]). + should == true + end + end +end -## tests for the generic "echo" external - -# describe "running an external" do -# before(:all) do -# @ext = ExternalRunner.run -# -# end -# it "should respond to 'info'" do -# @ext.rrun(['info']) -# @ext.jsgets.should == ["info", "echo", "external server that prints its arguments as JSON"] -# end -# it "should echo the request" do +def should_have_exited qs + begin + qs.run(["reset"]) + "raise before this".should == true + rescue RuntimeError => e + e.message.should == "no response" + rescue Errno::EPIPE + true.should == true + end +end -# @ext.rrun(['req', req_obj]) -# @ext.jsgets.should == ["x"] -# end -# end -# +describe "query server that exits" do + before(:each) do + @qs = QueryServerRunner.run + end + after(:each) do + @qs.close + end + + describe "old style list" do + before(:each) do + @fun = <<-JS + function(head, req, foo, bar) { + return "stuff"; + } + JS + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should get a warning" do + resp = @qs.run(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + resp["error"].should == "render_error" + resp["reason"].should include("the list API has changed") + end + end + + describe "only goes to 2 list" do + before(:each) do + @fun = <<-JS + function(head, req) { + send("bacon") + var row, i = 0; + while(row = getRow()) { + send(row.key); + i += 1; + if (i > 2) { + return('early'); + } + }; + } + JS + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should exit if erlang sends too many rows" do + @qs.run(["list", {"foo"=>"bar"}, {"q" => "ok"}]).should == ["start", ["bacon"], {"headers"=>{}}] + @qs.run(["list_row", {"key"=>"baz"}]).should == ["chunks", ["baz"]] + @qs.run(["list_row", {"key"=>"foom"}]).should == ["chunks", ["foom"]] + @qs.run(["list_row", {"key"=>"fooz"}]).should == ["end", ["fooz", "early"]] + @qs.rrun(["list_row", {"key"=>"foox"}]) + @qs.jsgets["error"].should == "query_server_error" + should_have_exited @qs + end + end + + describe "raw list" do + before(:each) do + @fun = <<-JS + function(head, req) { + send("first chunk"); + send(req.q); + var row; + while(row = getRow()) { + send(row.key); + }; + return "tail"; + }; + JS + @qs.run(["reset"]).should == true + @qs.add_fun(@fun).should == true + end + it "should exit if it gets a non-row in the middle" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["first chunk", "ok"], {"headers"=>{}}] + @qs.run(["reset"])["error"].should == "query_server_error" + should_have_exited @qs + end + end +end |