From 627b7d2d90c7cfcec2c8bb6e5b5e2b53ea60d217 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Thu, 26 Nov 2009 19:32:08 +0000 Subject: Setup JavaScript command line runner. All JS tests can now be run from the command line using the cURL adapter. In the future I would like to rework this and provide better TAP output. For now, each test is a single TAP assert. git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@884675 13f79535-47bb-0310-9956-ffa450edef68 --- .gitignore | 4 + Makefile.am | 4 +- license.skip | 2 + share/www/script/couch.js | 2 +- share/www/script/test/changes.js | 7 +- share/www/script/test/cookie_auth.js | 6 +- src/couchdb/priv/couch_js/http.c | 58 +++++--- src/couchdb/priv/couch_js/main.c | 12 +- test/javascript/Makefile.am | 11 +- test/javascript/cli_runner.js | 52 ++++++++ test/javascript/couch_http.js | 57 ++++++++ test/javascript/run.tpl | 30 +++++ test/javascript/runner.sh | 19 --- test/javascript/test.js | 249 ----------------------------------- 14 files changed, 217 insertions(+), 296 deletions(-) create mode 100644 test/javascript/cli_runner.js create mode 100644 test/javascript/couch_http.js create mode 100644 test/javascript/run.tpl delete mode 100755 test/javascript/runner.sh delete mode 100644 test/javascript/test.js diff --git a/.gitignore b/.gitignore index be37e00a..19085e70 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ src/mochiweb/Makefile stamp-h1 test/.deps/ test/Makefile +test/javascript/run_js_tests.sh var/Makefile # for make @@ -65,6 +66,7 @@ src/mochiweb/mochiweb.app test/local.ini test/etap/run test/etap/test_util.erl +test/javascript/run share/server/main.js # for make dev @@ -85,6 +87,8 @@ test/etap/run # for make check test/etap/temp.* +couchdb.stderr +couchdb.stdout # for make cover diff --git a/Makefile.am b/Makefile.am index ae6ab9fd..fff5648b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -53,9 +53,11 @@ dev: all @echo "it creates development ini files as well as a" @echo "$(top_builddir)/tmp structure for development runtime files." @echo "Use ./utils/run to launch CouchDB from the source tree." + mkdir -p $(top_builddir)/etc/couchdb/default.d + mkdir -p $(top_builddir)/etc/couchdb/local.d mkdir -p $(top_builddir)/tmp/lib mkdir -p $(top_builddir)/tmp/log - mkdir -p $(top_builddir)/tmp/run + mkdir -p $(top_builddir)/tmp/run/couchdb distclean-local: rm -fr $(top_builddir)/tmp diff --git a/license.skip b/license.skip index f33902ee..3c7d83a5 100644 --- a/license.skip +++ b/license.skip @@ -12,6 +12,8 @@ ^CHANGES ^config.* ^configure +^couchdb.stderr +^couchdb.stdout ^cover/.*\.coverdata ^cover/.*\.html ^erl_crash.dump diff --git a/share/www/script/couch.js b/share/www/script/couch.js index 9f0995ff..f7d7d43d 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -496,4 +496,4 @@ CouchDB.params = function(options) { returnArray.push(key + "=" + value); } return returnArray.join("&"); -} +}; diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index f348aed8..bf4a1617 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -57,7 +57,12 @@ couchTests.changes = function(debug) { } // poor man's browser detection - var is_safari = navigator.userAgent.match(/AppleWebKit/); + var is_safari = false; + if(typeof(navigator) == "undefined") { + is_safari = true; // For CouchHTTP based runners + } else if(navigator.userAgent.match(/AppleWebKit/)) { + is_safari = true; + }; if (!is_safari && xhr) { // Only test the continuous stuff if we have a real XHR object // with real async support. diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js index d82d302a..0a42b4a9 100644 --- a/share/www/script/test/cookie_auth.js +++ b/share/www/script/test/cookie_auth.js @@ -94,7 +94,11 @@ couchTests.cookie_auth = function(debug) { body: "username=Jason%20Davies&password="+encodeURIComponent(password) }); // should this be a redirect code instead of 200? - T(xhr.status == 200); + // The cURL adapter is returning the expected 302 here. + // I imagine this has to do with whether the client is willing + // to follow the redirect, ie, the browser follows and does a + // GET on the returned Location + T(xhr.status == 200 || xhr.status == 302); usersDb.deleteDb(); // test user creation diff --git a/src/couchdb/priv/couch_js/http.c b/src/couchdb/priv/couch_js/http.c index 6ee337af..703ab97b 100644 --- a/src/couchdb/priv/couch_js/http.c +++ b/src/couchdb/priv/couch_js/http.c @@ -38,6 +38,9 @@ char* METHODS[] = {"GET", "HEAD", "POST", "PUT", "DELETE", "COPY", NULL}; static JSBool go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t blen); +static JSString* +str_from_binary(JSContext* cx, char* data, size_t length); + static JSBool constructor(JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval) { @@ -406,13 +409,13 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen) if(!HANDLE) { JS_ReportError(cx, "Failed to initialize cURL handle."); - goto error; + goto done; } if(http->method < 0 || http->method > COPY) { JS_ReportError(cx, "INTERNAL: Unknown method."); - goto error; + goto done; } curl_easy_setopt(HANDLE, CURLOPT_CUSTOMREQUEST, METHODS[http->method]); @@ -452,13 +455,13 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen) if(curl_easy_perform(HANDLE) != 0) { JS_ReportError(cx, "Failed to execute HTTP request: %s", ERRBUF); - goto error; + goto done; } if(!state.resp_headers) { JS_ReportError(cx, "Failed to recieve HTTP headers."); - goto error; + goto done; } tmp = OBJECT_TO_JSVAL(state.resp_headers); @@ -473,7 +476,7 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen) )) { JS_ReportError(cx, "INTERNAL: Failed to set response headers."); - goto error; + goto done; } if(state.recvbuf) // Is good enough? @@ -482,16 +485,15 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen) jsbody = dec_string(cx, state.recvbuf, state.read+1); if(!jsbody) { - // This is so dirty its not even almost funny. I'm ignoring - // all sorts of content-types and character sets and just falling - // back to doing a chop job when something doesn't decode as UTF-8 - // which is pretty sad. But, if you hate me for it, then feel free - // to write a patch that does the proper content-type parsing and - // actually respects charsets as returned in headers. - jsbody = JS_NewString(cx, state.recvbuf, state.read); + // If we can't decode the body as UTF-8 we forcefully + // convert it to a string by just forcing each byte + // to a jschar. + jsbody = str_from_binary(cx, state.recvbuf, state.read); if(!jsbody) { - JS_ReportError(cx, "INTERNAL: Failed to decode body."); - goto error; + if(!JS_IsExceptionPending(cx)) { + JS_ReportError(cx, "INTERNAL: Failed to decode body."); + } + goto done; } } tmp = STRING_TO_JSVAL(jsbody); @@ -512,16 +514,13 @@ go(JSContext* cx, JSObject* obj, HTTPData* http, char* body, size_t bodylen) )) { JS_ReportError(cx, "INTERNAL: Failed to set responseText."); - goto error; + goto done; } ret = JS_TRUE; - goto success; -error: +done: if(state.recvbuf) JS_free(cx, state.recvbuf); - -success: return ret; } @@ -644,4 +643,23 @@ recv_body(void *ptr, size_t size, size_t nmem, void *data) state->read += length; return length; } - + +JSString* +str_from_binary(JSContext* cx, char* data, size_t length) +{ + jschar* conv = (jschar*) JS_malloc(cx, length * sizeof(jschar)); + JSString* ret = NULL; + size_t i; + + if(!conv) return NULL; + + for(i = 0; i < length; i++) + { + conv[i] = (jschar) data[i]; + } + + ret = JS_NewUCString(cx, conv, length); + if(!ret) JS_free(cx, conv); + + return ret; +} diff --git a/src/couchdb/priv/couch_js/main.c b/src/couchdb/priv/couch_js/main.c index ee0e42c9..f8578375 100644 --- a/src/couchdb/priv/couch_js/main.c +++ b/src/couchdb/priv/couch_js/main.c @@ -306,13 +306,21 @@ main(int argc, const char * argv[]) JS_SetGlobalObject(cx, global); - if(argc != 2) { + if(argc > 2) + { fprintf(stderr, "incorrect number of arguments\n\n"); fprintf(stderr, "usage: %s \n", argv[0]); return 2; } - execute_script(cx, global, argv[1]); + if(argc == 0) + { + execute_script(cx, global, NULL); + } + else + { + execute_script(cx, global, argv[1]); + } FINISH_REQUEST(cx); diff --git a/test/javascript/Makefile.am b/test/javascript/Makefile.am index faaca829..81aec36b 100644 --- a/test/javascript/Makefile.am +++ b/test/javascript/Makefile.am @@ -11,5 +11,12 @@ ## the License. EXTRA_DIST = \ - runner.sh \ - test.js + cli_runner.js \ + couch_http.js \ + run.tpl + +run: run.tpl + sed -e "s|%abs_top_srcdir%|$(abs_top_srcdir)|" \ + -e "s|%abs_top_builddir%|$(abs_top_builddir)|" \ + < $< > $@ + chmod +x $@ diff --git a/test/javascript/cli_runner.js b/test/javascript/cli_runner.js new file mode 100644 index 00000000..cdbe2e73 --- /dev/null +++ b/test/javascript/cli_runner.js @@ -0,0 +1,52 @@ +// 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. + +var console = { + log: function(arg) { + var msg = (arg.toString()).replace(/\n/g, "\n "); + print("# " + msg); + } +}; + +function T(arg1, arg2) { + if(!arg1) { + throw((arg2 ? arg2 : arg1).toString()); + } +} + +function runTestConsole(num, name, func) { + try { + func(); + print("ok " + num + " " + name); + } catch(e) { + msg = e.toString(); + msg = msg.replace(/\n/g, "\n "); + print("not ok " + num + " " + name + " " + msg); + } +} + +function runAllTestsConsole() { + var numTests = 0; + for(var t in couchTests) { numTests += 1; } + print("1.." + numTests); + var testId = 0; + for(var t in couchTests) { + testId += 1; + runTestConsole(testId, t, couchTests[t]); + } +}; + +try { + runAllTestsConsole(); +} catch (e) { + p("# " + e.toString()); +} diff --git a/test/javascript/couch_http.js b/test/javascript/couch_http.js new file mode 100644 index 00000000..f92cf119 --- /dev/null +++ b/test/javascript/couch_http.js @@ -0,0 +1,57 @@ +// 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. + +(function() { + CouchHTTP.prototype.base_url = "http://127.0.0.1:5984" + + if(typeof(CouchHTTP) != "undefined") { + CouchHTTP.prototype.open = function(method, url, async) { + if(/^\s*http:\/\//.test(url)) { + return this._open(method, url, async); + } else { + return this._open(method, this.base_url + url, async); + } + }; + + CouchHTTP.prototype.setRequestHeader = function(name, value) { + // Drop content-length headers because cURL will set it for us + // based on body length + if(name.toLowerCase().replace(/^\s+|\s+$/g, '') != "content-length") { + this._setRequestHeader(name, value); + } + } + + CouchHTTP.prototype.send = function(body) { + this._send(body || ""); + var headers = {}; + this._headers.forEach(function(hdr) { + var pair = hdr.split(":"); + var name = pair.shift(); + headers[name] = pair.join(":").replace(/^\s+|\s+$/g, ""); + }); + this.headers = headers; + }; + + CouchHTTP.prototype.getResponseHeader = function(name) { + for(var hdr in this.headers) { + if(hdr.toLowerCase() == name.toLowerCase()) { + return this.headers[hdr]; + } + } + return null; + }; + } +})(); + +CouchDB.newXhr = function() { + return new CouchHTTP(); +}; diff --git a/test/javascript/run.tpl b/test/javascript/run.tpl new file mode 100644 index 00000000..c5abe6e7 --- /dev/null +++ b/test/javascript/run.tpl @@ -0,0 +1,30 @@ +#!/bin/sh -e + +# 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. + +SRC_DIR=%abs_top_srcdir% +SCRIPT_DIR=$SRC_DIR/share/www/script +JS_TEST_DIR=$SRC_DIR/test/javascript + +COUCHJS=%abs_top_builddir%/src/couchdb/priv/couchjs + +cat $SCRIPT_DIR/json2.js \ + $SCRIPT_DIR/sha1.js \ + $SCRIPT_DIR/oauth.js \ + $SCRIPT_DIR/couch.js \ + $SCRIPT_DIR/couch_test_runner.js \ + $SCRIPT_DIR/couch_tests.js \ + $SCRIPT_DIR/test/*.js \ + $JS_TEST_DIR/couch_http.js \ + $JS_TEST_DIR/cli_runner.js \ + | $COUCHJS - diff --git a/test/javascript/runner.sh b/test/javascript/runner.sh deleted file mode 100755 index 1f48c390..00000000 --- a/test/javascript/runner.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -e - -# 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. - -cat ../share/www/script/couch.js \ - ../share/www/script/couch_test_runner.js \ - ../share/www/script/couch_tests.js \ - ../share/www/script/test/*.js test.js \ - | ../src/couchdb/couchjs - diff --git a/test/javascript/test.js b/test/javascript/test.js deleted file mode 100644 index 7f8a0787..00000000 --- a/test/javascript/test.js +++ /dev/null @@ -1,249 +0,0 @@ -// 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); - } -}; - -RegExp.escape = function(text) { - if (!arguments.callee.sRE) { - var specials = [ - '/', '.', '*', '+', '?', '|', - '(', ')', '[', ']', '{', '}', '\\' - ]; - arguments.callee.sRE = new RegExp( - '(\\' + specials.join('|\\') + ')', 'g' - ); - } - return text.replace(arguments.callee.sRE, '\\$1'); -} - -// This is a JS wrapper for the curl function made available in couch_js.c, -// it should be used in other JavaScripts that would like to make HTTP calls. - -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) { - var keymatcher = new RegExp(RegExp.escape(key), "i"); - for (var i in headers) { - var h = headers[i]; - if (keymatcher.test(h)) { - var value = h.substr(key.length+2); - return value.replace(/^\s+|\s+$/g,""); - } - } - return ""; - } - } - }; - return { - GET : function(url, body, headers) { - var st, urx = url, hx = (headers || null); - st = gethttp(urx, hx); - return parseCurl(st); - }, - HEAD : function(url, body, headers) { - var st, urx = url, hx = (headers || null); - st = headhttp(urx, hx); - return parseCurl(st); - }, - DELETE : function(url, body, headers) { - var st, urx = url, hx = (headers || null); - st = delhttp(urx, hx); - return parseCurl(st); - }, - MOVE : function(url, body, headers) { - var st, urx = url, hx = (headers || null); - st = movehttp(urx, hx); - return parseCurl(st); - }, - COPY : function(url, body, headers) { - var st, urx = url, hx = (headers || null); - st = copyhttp(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); - } - }; -})(); - -// Monkeypatches to CouchDB client for use of curl. - -CouchDB.host = (typeof window == 'undefined' || !window) ? "127.0.0.1:5984" : window; - -CouchDB.request = function(method, uri, options) { - var full_uri = "http://" + CouchDB.host + 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); -} - - - -// *************** Test Framework Console Adapter ****************** // - -var p = print; -var numFailures = 0; - -function runAllTestsConsole() { - var numTests = 0; - var debug = false; - for (var t in couchTests) { - p(t); - if (t == "utf8") { - p("We skip the utf8 test because it fails due to problems in couch_js.c"); - p("Run the in-browser tests to verify utf8.\n"); - } else { - numTests += 1; - var testFun = couchTests[t]; - runTestConsole(testFun, debug); - } - } - p("Results: "+numFailures.toString() + " failures in "+numTests+" tests.") -}; - -function runTestConsole(testFun, debug) { - var start = new Date().getTime(); - try { - if (!debug) testFun = patchTest(testFun) || testFun; - testFun(); - p("PASS"); - } catch(e) { - p("ERROR"); - p("Exception raised: "+e.toString()); - p("Backtrace: "+e.stack); - } - var duration = new Date().getTime() - start; - p(duration+"ms\n"); -}; - - -// 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) { - p("Assertion failed: "+(arg2 != null ? arg2 : arg1).toString()); - numFailures += 1 - } -} - -p("Running CouchDB Test Suite\n"); -p("Host: "+CouchDB.host); - -try { - p("Version: "+CouchDB.getVersion()+"\n"); - runAllTestsConsole(); - // runTestConsole(tests.attachments); -} catch (e) { - p(e.toString()); -} - -p("\nFinished"); -- cgit v1.2.3