diff options
94 files changed, 1241 insertions, 1291 deletions
diff --git a/.gitignore b/.gitignore index f3bdc52ca..e60aeddca 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ build-aux *.diff !.gitignore .* +!.mailmap # ./configure diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..31f59e183 --- /dev/null +++ b/.mailmap @@ -0,0 +1,13 @@ +Benoit Chesneau <benoitc@apache.org> <bchesneau@gmail.com> +Benoit Chesneau <benoitc@apache.org> benoitc <benoitc@apache.org> + +Jason Smith <jhs@apache.org> Jason Smith (air) <jhs@iriscouch.com> +Jason Smith <jhs@apache.org> Jason Smith (air) <jhs@apache.org> + +Filipe David Borba Manana <fdmanana@apache.org> + +Randall Leeds <randall@apache.org> <randall.leeds@gmail.com> + +Paul Joseph Davis <davisp@apache.org> Paul J. Davis <davisp@apache.org> + +Bob Dionne <bitdiddle@apache.org> bitdiddle <bitdiddle@apache.org>
\ No newline at end of file diff --git a/.travis.yml b/.travis.yml index bb80cb006..29d14a383 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,11 @@ before_install: - sudo apt-get update - sudo apt-get install libicu-dev libmozjs-dev before_script: ./bootstrap && ./configure -script: make check +script: make distcheck language: erlang otp_release: + - R15B02 + - R15B01 + - R15B - R14B04 + - R14B03 @@ -10,6 +10,18 @@ Source Repository: * The source repository was migrated from SVN to Git. +HTTP Interface: + + * No longer rewrites the X-CouchDB-Requested-Path during recursive + calls to the rewriter. + * Limit recursion depth in the URL rewriter. Defaults to a maximum + of 100 invocations but is configurable. + * Fix _session for IE7. + * Added Server-Sent Events protocol to db changes API. + See http://www.w3.org/TR/eventsource/ for details. + * Make password hashing synchronous when using the /_config/admins API. + * Include user name in show/list ETags. + Storage System: * Fixed unnecessary conflict when deleting and creating a @@ -22,12 +34,27 @@ View Server: Futon: * Added view request duration to Futon. + * Disable buttons for actions that the user doesn't have permissions to. Security: * Passwords are now hashed using the PBKDF2 algorithm with a configurable work factor. +Test Suite: + + * Moved the JS test suite to the CLI + * Improved tracebacks printed by the JS CLI tests + * Improved the reliability of a number of tests + +UUID Algorithms: + + * Added the utc_id algorithm. + +URL Rewriter & Vhosts: + + * database name is encoded during rewriting (allowing embedded /'s, etc) + Version 1.2.1 ------------- @@ -118,6 +145,8 @@ View Server: configuration is matched. * Fixed incorrect reduce query results when using pagination parameters. * Made icu_driver work with Erlang R15B and later. + * Avoid invalidating view indexes when running out of file descriptors + (COUCHDB-1445). OAuth: @@ -130,6 +159,11 @@ Futon: * Futon remembers view code every time it is saved, allowing to save an edit that amounts to a revert. +Log System: + + * Log correct stacktrace in all cases. + * Improvements to log messages for file-related errors. + Version 1.1.2 ------------- diff --git a/Makefile.am b/Makefile.am index 02b4bd11e..d6836d2b7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,8 +78,17 @@ NOTICE.gz: $(top_srcdir)/NOTICE README.gz: $(top_srcdir)/README -gzip -9 < $< > $@ +.PHONY: THANKS.gz THANKS.gz: $(top_srcdir)/THANKS - -gzip -9 < $< > $@ + @sed -e '/^#.*/d' $< > $(top_builddir)/THANKS.tmp + @git shortlog -se 6c976bd..HEAD \ + | grep -v @apache.org \ + | sed -E 's/^[[:blank:]]{5}[[:digit:]]+[[:blank:]]/ * /' \ + >> $(top_builddir)/THANKS.tmp + @echo '\nFor a list of authors see the `AUTHORS` file.\n' \ + >> $(top_builddir)/THANKS.tmp + -gzip -9 < $(top_builddir)/THANKS.tmp > $@ + @rm $(top_builddir)/THANKS.tmp check: dev check-js $(top_builddir)/test/etap/run $(top_srcdir)/test/etap @@ -89,6 +98,9 @@ if USE_CURL $(top_builddir)/test/javascript/run endif +check-etap: dev + $(top_builddir)/test/etap/run $(top_srcdir)/test/etap + cover: dev rm -f cover/*.coverdata COVER=1 COVER_BIN=./src/couchdb/ $(top_builddir)/test/etap/run @@ -18,6 +18,13 @@ This version has not been released yet. * Fixed unnecessary conflict when deleting and creating a document in the same batch. * New and updated passwords are hashed using PBKDF2. + * Fix various bugs in the URL rewriter when recursion is involved. + * Added Server-Sent Events protocol to db changes API. + * Moved the JS test suite to the CLI + * Make password hashing synchronous when using the /_config/admins API. + * Added utc_id UUID algorithm. + * encode database name during URL rewriting. + * Include user name in show/list ETags. Version 1.2.1 ------------- @@ -67,6 +74,7 @@ This release contains backwards incompatible changes. * Fixed incorrect reduce query results when using pagination parameters. * Made icu_driver work with Erlang R15B and later. * Improvements to the build system and etap test suite. + * Improvements to log messages for file-related errors. Version 1.1.2 ------------- @@ -33,7 +33,7 @@ If you're getting a cryptic error message, see: For general help, see: - http://couchdb.apache.org/community/lists.html + http://couchdb.apache.org/#mailing-list The mailing lists provide a wealth of support and knowledge for you to tap into. Feel free to drop by with your questions or discussion. See the official CouchDB @@ -50,10 +50,10 @@ It should work in at least Firefox >= 3.6 with Private Browsing mode enabled. Read more about JSpec here: - http://jspec.info/ + https://github.com/liblime/jspec When you change the specs, but your changes have no effect, manually reload -the changed spec file in the browser. When the spec that tests erlang views +the changed spec file in the browser. When the spec that tests Erlang views fails, make sure you have enabled Erlang views as described here: http://wiki.apache.org/couchdb/EnableErlangViews @@ -66,7 +66,6 @@ suggesting improvements or submitting changes. Some of these people are: * Lim Yue Chuan <shasderias@gmail.com> * David Davis <xantus@xantus.org> * Klaus Trainer <klaus.trainer@web.de> - * Dale Harvey <dale@arandomurl.com> * Juuso Väänänen <juuso@vaananen.org> * Jeff Zellner <jeff.zellner@gmail.com> * Benjamin Young <byoung@bigbluehat.com> @@ -92,5 +91,10 @@ suggesting improvements or submitting changes. Some of these people are: * Simon Leblanc <sim.leblanc+apache@gmail.com> * Rogutės Sparnuotos <rogutes@googlemail.com> * Gavin McDonald <gmcdonald@apache.org> - -For a list of authors see the `AUTHORS` file. + * Fedor Indutny <fedor@indutny.com> + * Tim Blair +# Dear committer who merges a commit from a non-committer: +# You don't have to manually maintain the THANKS file anymore (yay!). +# Non-committer authors get automatically appended to THANKS and +# moved into THANKS.gz by `make`. This note will be stripped as well. +# Authors from commit 6c976bd and onwards are auto-inserted. diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index dd1054b6b..8d996f4be 100644 --- a/etc/couchdb/Makefile.am +++ b/etc/couchdb/Makefile.am @@ -29,7 +29,7 @@ default.ini: default.ini.tpl sed -e "s|%bindir%|.|g" \ -e "s|%localconfdir%|$(localconfdir)|g" \ -e "s|%localdatadir%|../share/couchdb|g" \ - -e "s|%localbuilddatadir%|../share/couchdb|g" \ + -e "s|%localbuilddatadir%|../share/couchdb|g" \ -e "s|%localstatelibdir%|../var/lib/couchdb|g" \ -e "s|%localstatelogdir%|../var/log/couchdb|g" \ -e "s|%localstaterundir%|../var/run/couchdb|g" \ @@ -43,7 +43,7 @@ default.ini: default.ini.tpl sed -e "s|%bindir%|$(bindir)|g" \ -e "s|%localconfdir%|$(localconfdir)|g" \ -e "s|%localdatadir%|$(localdatadir)|g" \ - -e "s|%localbuilddatadir%|$(localdatadir)|g" \ + -e "s|%localbuilddatadir%|$(localdatadir)|g" \ -e "s|%localstatelibdir%|$(localstatelibdir)|g" \ -e "s|%localstatelogdir%|$(localstatelogdir)|g" \ -e "s|%localstaterundir%|$(localstaterundir)|g" \ @@ -58,7 +58,7 @@ default_dev.ini: default.ini.tpl sed -e "s|%bindir%|$(abs_top_builddir)/bin|g" \ -e "s|%localconfdir%|$(abs_top_builddir)/etc/couchdb|g" \ -e "s|%localdatadir%|$(abs_top_srcdir)/share|g" \ - -e "s|%localbuilddatadir%|$(abs_top_builddir)/share|g" \ + -e "s|%localbuilddatadir%|$(abs_top_builddir)/share|g" \ -e "s|%localstatelibdir%|$(abs_top_builddir)/tmp/lib|g" \ -e "s|%localstatelogdir%|$(abs_top_builddir)/tmp/log|g" \ -e "s|%localstaterundir%|$(abs_top_builddir)/tmp/run|g" \ @@ -73,6 +73,7 @@ default_dev.ini: default.ini.tpl local_dev.ini: local.ini if test ! -f "$@"; then \ cp $< $@; \ + chmod +w $@; \ fi install-data-hook: diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index ce849057f..79ece5cd3 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -178,7 +178,12 @@ _view = {couch_mrview_http, handle_view_req} ; random prefix is regenerated and the process starts over. ; utc_random - Time since Jan 1, 1970 UTC with microseconds ; First 14 characters are the time in hex. Last 18 are random. +; utc_id - Time since Jan 1, 1970 UTC with microseconds, plus utc_id_suffix string +; First 14 characters are the time in hex. uuids/utc_id_suffix string value is appended to these. algorithm = sequential +; The utc_id_suffix value will be appended to uuids generated by the utc_id algorithm. +; Replicating instances should have unique utc_id_suffix values to ensure uniqueness of utc_id ids. +utc_id_suffix = [stats] ; rate is in milliseconds diff --git a/etc/init/couchdb.tpl.in b/etc/init/couchdb.tpl.in index e4930e005..39b62500d 100644 --- a/etc/init/couchdb.tpl.in +++ b/etc/init/couchdb.tpl.in @@ -84,6 +84,9 @@ start_couchdb () { # Start Apache CouchDB as a background process. mkdir -p "$RUN_DIR" + if test -n "$COUCHDB_USER"; then + chown $COUCHDB_USER "$RUN_DIR" + fi command="$COUCHDB -b" if test -n "$COUCHDB_STDOUT_FILE"; then command="$command -o $COUCHDB_STDOUT_FILE" diff --git a/share/server/util.js b/share/server/util.js index 5b3a63b06..b7a62d810 100644 --- a/share/server/util.js +++ b/share/server/util.js @@ -87,7 +87,7 @@ var Couch = { // create empty exports object before executing the module, // stops circular requires from filling the stack ddoc._module_cache[newModule.id] = {}; - var s = "function (module, exports, require) { " + newModule.current + " }"; + var s = "function (module, exports, require) { " + newModule.current + "\n }"; try { var func = sandbox ? evalcx(s, sandbox, newModule.id) : eval(s); func.apply(sandbox, [newModule, newModule.exports, function(name) { diff --git a/share/www/database.html b/share/www/database.html index 23945cb1d..c64f749c2 100644 --- a/share/www/database.html +++ b/share/www/database.html @@ -177,9 +177,9 @@ specific language governing permissions and limitations under the License. </div> <ul id="toolbar"> <li><button class="add">New Document</button></li> - <li><button class="security">Security…</button></li> + <li><button class="security userAdmin serverAdmin">Security…</button></li> <li><button class="compact">Compact & Cleanup…</button></li> - <li><button class="delete">Delete Database…</button></li> + <li><button class="delete serverAdmin">Delete Database…</button></li> </ul> <div id="viewcode" class="collapsed" style="display: none"> diff --git a/share/www/script/couch.js b/share/www/script/couch.js index f7099e3f0..3deb44102 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -400,15 +400,20 @@ CouchDB.xhrheader = function(xhr, header) { } } +CouchDB.proxyUrl = function(uri) { + if(uri.substr(0, CouchDB.protocol.length) != CouchDB.protocol) { + uri = CouchDB.urlPrefix + uri; + } + return uri; +} + CouchDB.request = function(method, uri, options) { options = typeof(options) == 'object' ? options : {}; options.headers = typeof(options.headers) == 'object' ? options.headers : {}; options.headers["Content-Type"] = options.headers["Content-Type"] || options.headers["content-type"] || "application/json"; options.headers["Accept"] = options.headers["Accept"] || options.headers["accept"] || "application/json"; var req = CouchDB.newXhr(); - if(uri.substr(0, CouchDB.protocol.length) != CouchDB.protocol) { - uri = CouchDB.urlPrefix + uri; - } + uri = CouchDB.proxyUrl(uri); req.open(method, uri, false); if (options.headers) { var headers = options.headers; diff --git a/share/www/script/couch_test_runner.js b/share/www/script/couch_test_runner.js index d1a53e91e..b09aeab62 100644 --- a/share/www/script/couch_test_runner.js +++ b/share/www/script/couch_test_runner.js @@ -16,6 +16,7 @@ function loadScript(url) { // disallow loading remote URLs if((url.substr(0, 7) == "http://") + || (url.substr(0, 8) == "https://") || (url.substr(0, 2) == "//") || (url.substr(0, 5) == "data:") || (url.substr(0, 11) == "javascript:")) { diff --git a/share/www/script/futon.browse.js b/share/www/script/futon.browse.js index e2919600f..bbb29b3b6 100644 --- a/share/www/script/futon.browse.js +++ b/share/www/script/futon.browse.js @@ -570,15 +570,15 @@ "/" + $.couch.encodeDocId(doc._id) + "/_view/" + encodeURIComponent(data.name); }, - error: function(status, e, reason) { - alert(reason); + error: function(status, error, reason) { + alert("Error: " + error + "\n\n" + reason); } }); } db.openDoc(docId, { error: function(status, error, reason) { if (status == 404) save(null); - else alert(reason); + else alert("Error: " + error + "\n\n" + reason); }, success: function(doc) { save(doc); @@ -616,8 +616,8 @@ $("#viewcode button.revert, #viewcode button.save") .attr("disabled", "disabled"); }, - error: function(status, e, reason) { - alert(reason); + error: function(status, error, reason) { + alert("Error: " + error + "\n\n" + reason); } }); } @@ -1009,6 +1009,9 @@ if (!page.isNew) { db.openDoc(docId, {revs_info: true, + error: function(status, error, reason) { + alert("Error: " + error + "\n\n" + reason); + }, success: function(doc) { var revs = doc._revs_info || []; delete doc._revs_info; diff --git a/share/www/script/futon.js b/share/www/script/futon.js index 5e0fb78b2..e2e0aaf36 100644 --- a/share/www/script/futon.js +++ b/share/www/script/futon.js @@ -225,20 +225,50 @@ function $$(node) { this.sidebar = function() { // get users db info? $("#userCtx span").hide(); + $(".serverAdmin").attr('disabled', 'disabled'); + $.couch.session({ success : function(r) { var userCtx = r.userCtx; + + var urlParts = location.search.substr(1).split("/"); + var dbName = decodeURIComponent(urlParts.shift()); + var dbNameRegExp = new RegExp("[^a-z0-9\_\$\(\)\+\/\-]", "g"); + dbName = dbName.replace(dbNameRegExp, ""); + $$("#userCtx").userCtx = userCtx; if (userCtx.name) { $("#userCtx .name").text(userCtx.name).attr({href : $.couch.urlPrefix + "/_utils/document.html?"+encodeURIComponent(r.info.authentication_db)+"/org.couchdb.user%3A"+encodeURIComponent(userCtx.name)}); + if (userCtx.roles.indexOf("_admin") != -1) { $("#userCtx .loggedin").show(); $("#userCtx .loggedinadmin").show(); + $(".serverAdmin").removeAttr('disabled'); // user is a server admin } else { $("#userCtx .loggedin").show(); + + if (dbName != "") { + $.couch.db(dbName).getDbProperty("_security", { // check security roles for user admins + success: function(resp) { + var adminRoles = resp.admins.roles; + + if ($.inArray(userCtx.name, resp.admins.names)>=0) { // user is admin + $(".userAdmin").removeAttr('disabled'); + } + else { + for (var i=0; i<userCtx.roles.length; i++) { + if ($.inArray(userCtx.roles[i], resp.admins.roles)>=0) { // user has role that is an admin + $(".userAdmin").removeAttr('disabled'); + } + } + } + } + }); + } } } else if (userCtx.roles.indexOf("_admin") != -1) { $("#userCtx .adminparty").show(); + $(".serverAdmin").removeAttr('disabled'); } else { $("#userCtx .loggedout").show(); }; diff --git a/share/www/script/jquery.couch.js b/share/www/script/jquery.couch.js index 62831a1a7..6abac2c6d 100644 --- a/share/www/script/jquery.couch.js +++ b/share/www/script/jquery.couch.js @@ -139,7 +139,7 @@ */ session: function(options) { options = options || {}; - $.ajax({ + ajax({ type: "GET", url: this.urlPrefix + "/_session", beforeSend: function(xhr) { xhr.setRequestHeader('Accept', 'application/json'); @@ -839,7 +839,8 @@ * uploads/all/documentation/couchbase-api-design.html#couchbase-api- * design_db-design-designdoc-view-viewname_get">docs for /db/ * _design/design-doc/_list/l1/v1</a> - * @param {String} name View to run list against + * @param {String} name View to run list against (string should have + * the design-doc name followed by a slash and the view name) * @param {ajaxSettings} options <a href="http://api.jquery.com/ * jQuery.ajax/#jQuery-ajax-settings">jQuery ajax settings</a> */ diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 19e22fd00..d76c19378 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -30,7 +30,7 @@ couchTests.changes = function(debug) { T(db.save(docFoo).ok); T(db.ensureFullCommit().ok); T(db.open(docFoo._id)._id == docFoo._id); - + req = CouchDB.request("GET", "/test_suite_db/_changes"); var resp = JSON.parse(req.responseText); @@ -39,7 +39,7 @@ couchTests.changes = function(debug) { T(resp.results[0].changes[0].rev == docFoo._rev); // test with callback - + run_on_modified_server( [{section: "httpd", key: "allow_jsonp", @@ -78,12 +78,12 @@ couchTests.changes = function(debug) { // WebKit (last checked on nightly #47686) does fail on processing // the async-request properly while javascript is executed. - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&timeout=500", true); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&timeout=500"), true); xhr.send(""); var docBar = {_id:"bar", bar:1}; db.save(docBar); - + var lines, change1, change2; waitForSuccess(function() { lines = xhr.responseText.split("\n"); @@ -96,12 +96,12 @@ couchTests.changes = function(debug) { T(change1.seq == 1); T(change1.id == "foo"); - + T(change2.seq == 2); T(change2.id == "bar"); T(change2.changes[0].rev == docBar._rev); - - + + var docBaz = {_id:"baz", baz:1}; db.save(docBaz); @@ -113,7 +113,7 @@ couchTests.changes = function(debug) { throw "bad seq, try again"; } }); - + T(change3.seq == 3); T(change3.id == "baz"); T(change3.changes[0].rev == docBaz._rev); @@ -122,9 +122,9 @@ couchTests.changes = function(debug) { xhr = CouchDB.newXhr(); //verify the hearbeat newlines are sent - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&heartbeat=10&timeout=500", true); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&heartbeat=10&timeout=500"), true); xhr.send(""); - + var str; waitForSuccess(function() { str = xhr.responseText; @@ -139,27 +139,54 @@ couchTests.changes = function(debug) { // otherwise we'll continue to receive heartbeats forever xhr.abort(); + // test Server Sent Event (eventsource) + if (!!window.EventSource) { + var source = new EventSource( + "/test_suite_db/_changes?feed=eventsource"); + var results = []; + var sourceListener = function(e) { + var data = JSON.parse(e.data); + results.push(data); + }; + + source.addEventListener('message', sourceListener , false); + + waitForSuccess(function() { + if (results.length != 3) + throw "bad seq, try again"; + }); + + source.removeEventListener('message', sourceListener, false); + + T(results[0].seq == 1); + T(results[0].id == "foo"); + + T(results[1].seq == 2); + T(results[1].id == "bar"); + T(results[1].changes[0].rev == docBar._rev); + } + // test longpolling xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=longpoll", true); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll"), true); xhr.send(""); - + waitForSuccess(function() { lines = xhr.responseText.split("\n"); if (lines[5] != '"last_seq":3}') { throw("still waiting"); } }, "last_seq"); - + xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=3", true); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll&since=3"), true); xhr.send(""); var docBarz = {_id:"barz", bar:1}; db.save(docBarz); - + var parse_changes_line = function(line) { if (line.charAt(line.length-1) == ",") { var linetrimmed = line.substring(0, line.length-1); @@ -168,14 +195,14 @@ couchTests.changes = function(debug) { } return JSON.parse(linetrimmed); }; - + waitForSuccess(function() { lines = xhr.responseText.split("\n"); if (lines[3] != '"last_seq":4}') { throw("still waiting"); } }, "change_lines"); - + var change = parse_changes_line(lines[1]); T(change.seq == 4); T(change.id == "barz"); @@ -183,8 +210,40 @@ couchTests.changes = function(debug) { T(lines[3]=='"last_seq":4}'); + // test since=now + xhr = CouchDB.newXhr(); + + xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=now", true); + xhr.send(""); + + var docBarz = {_id:"barzzzz", bar:1}; + db.save(docBarz); + + var parse_changes_line = function(line) { + if (line.charAt(line.length-1) == ",") { + var linetrimmed = line.substring(0, line.length-1); + } else { + var linetrimmed = line; + } + return JSON.parse(linetrimmed); + }; + + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + if (lines[3] != '"last_seq":5}') { + throw("still waiting"); + } + }, "change_lines"); + + var change = parse_changes_line(lines[1]); + T(change.seq == 5); + T(change.id == "barzzzz"); + T(change.changes[0].rev == docBarz._rev); + T(lines[3]=='"last_seq":5}'); + + } - + // test the filtered changes var ddoc = { _id : "_design/changes_filter", @@ -224,15 +283,15 @@ couchTests.changes = function(debug) { db.save({"bop" : "foom"}); db.save({"bop" : false}); - + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop"); var resp = JSON.parse(req.responseText); T(resp.results.length == 1, "filtered/bop"); - + req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=woox"); resp = JSON.parse(req.responseText); T(resp.results.length == 0); - + req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=bop"); resp = JSON.parse(req.responseText); T(resp.results.length == 1, "changes_filter/dynamic&field=bop"); @@ -241,32 +300,32 @@ couchTests.changes = function(debug) { // filter with longpoll // longpoll filters full history when run without a since seq xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop", false); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop"), false); xhr.send(""); var resp = JSON.parse(xhr.responseText); - T(resp.last_seq == 7); + T(resp.last_seq == 8); // longpoll waits until a matching change before returning xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=7&filter=changes_filter/bop", true); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll&since=7&filter=changes_filter/bop"), true); xhr.send(""); db.save({"_id":"falsy", "bop" : ""}); // empty string is falsy db.save({"_id":"bingo","bop" : "bingo"}); - + waitForSuccess(function() { resp = JSON.parse(xhr.responseText); }, "longpoll-since"); - - T(resp.last_seq == 9); + + T(resp.last_seq == 10); T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update"); xhr.abort(); - + var timeout = 500; - var last_seq = 10; + var last_seq = 11; while (true) { // filter with continuous xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout="+timeout, true); + xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout="+timeout), true); xhr.send(""); db.save({"_id":"rusty", "bop" : "plankton"}); @@ -302,31 +361,31 @@ couchTests.changes = function(debug) { // error conditions // non-existing design doc - var req = CouchDB.request("GET", + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=nothingtosee/bop"); TEquals(404, req.status, "should return 404 for non existant design doc"); - // non-existing filter - var req = CouchDB.request("GET", + // non-existing filter + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/movealong"); TEquals(404, req.status, "should return 404 for non existant filter fun"); // both - var req = CouchDB.request("GET", + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=nothingtosee/movealong"); - TEquals(404, req.status, + TEquals(404, req.status, "should return 404 for non existant design doc and filter fun"); // changes get all_docs style with deleted docs var doc = {a:1}; db.save(doc); db.deleteDoc(doc); - var req = CouchDB.request("GET", + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop&style=all_docs"); var resp = JSON.parse(req.responseText); var expect = (!is_safari && xhr) ? 3: 1; TEquals(expect, resp.results.length, "should return matching rows"); - + // test filter on view function (map) // T(db.save({"_id":"blah", "bop" : "plankton"}).ok); @@ -350,7 +409,7 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/_session", authOpts); var resp = JSON.parse(req.responseText); - + T(db.save({"user" : "Noah Slater"}).ok); var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/userCtx", authOpts); var resp = JSON.parse(req.responseText); @@ -442,7 +501,7 @@ couchTests.changes = function(debug) { T(resp.results.length === 2); T(resp.results[0].id === "something"); T(resp.results[1].id === "anotherthing"); - + var docids = JSON.stringify(["something", "anotherthing", "andmore"]), req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_doc_ids&doc_ids="+docids, options); var resp = JSON.parse(req.responseText); @@ -459,11 +518,11 @@ couchTests.changes = function(debug) { if (!is_safari && xhr) { // filter docids with continuous xhr = CouchDB.newXhr(); - xhr.open("POST", "/test_suite_db/_changes?feed=continuous&timeout=500&since=7&filter=_doc_ids", true); + xhr.open("POST", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&timeout=500&since=7&filter=_doc_ids"), true); xhr.setRequestHeader("Content-Type", "application/json"); - + xhr.send(options.body); - + T(db.save({"_id":"andmore", "bop" : "plankton"}).ok); waitForSuccess(function() { @@ -553,4 +612,3 @@ couchTests.changes = function(debug) { // cleanup db.deleteDb(); }; - diff --git a/share/www/script/test/content_negotiation.js b/share/www/script/test/content_negotiation.js index 5778fad2e..36e7dfbd3 100644 --- a/share/www/script/test/content_negotiation.js +++ b/share/www/script/test/content_negotiation.js @@ -19,7 +19,7 @@ couchTests.content_negotiation = function(debug) { // with no accept header var req = CouchDB.newXhr(); - req.open("GET", "/test_suite_db/", false); + req.open("GET", CouchDB.proxyUrl("/test_suite_db/"), false); req.send(""); TEquals("text/plain; charset=utf-8", req.getResponseHeader("Content-Type")); diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js index 066af85d5..40b633b35 100644 --- a/share/www/script/test/cookie_auth.js +++ b/share/www/script/test/cookie_auth.js @@ -80,7 +80,7 @@ couchTests.cookie_auth = function(debug) { T(usersDb.save(jasonUserDoc).ok); var checkDoc = open_as(usersDb, jasonUserDoc._id, "jan"); - T(checkDoc.name == "Jason Davies"); + TEquals("Jason Davies", checkDoc.name); var jchrisUserDoc = CouchDB.prepareUserDoc({ name: "jchris@apache.org" @@ -96,8 +96,8 @@ couchTests.cookie_auth = function(debug) { usersDb.save(duplicateJchrisDoc); T(false && "Can't create duplicate user names. Should have thrown an error."); } catch (e) { - T(e.error == "conflict"); - T(usersDb.last_req.status == 409); + TEquals("conflict", e.error); + TEquals(409, usersDb.last_req.status); } // we can't create _names @@ -109,8 +109,8 @@ couchTests.cookie_auth = function(debug) { usersDb.save(underscoreUserDoc); T(false && "Can't create underscore user names. Should have thrown an error."); } catch (e) { - T(e.error == "forbidden"); - T(usersDb.last_req.status == 403); + TEquals("forbidden", e.error); + TEquals(403, usersDb.last_req.status); } // we can't create docs with malformed ids @@ -124,13 +124,13 @@ couchTests.cookie_auth = function(debug) { usersDb.save(badIdDoc); T(false && "Can't create malformed docids. Should have thrown an error."); } catch (e) { - T(e.error == "forbidden"); - T(usersDb.last_req.status == 403); + TEquals("forbidden", e.error); + TEquals(403, usersDb.last_req.status); } // login works T(CouchDB.login('Jason Davies', password).ok); - T(CouchDB.session().userCtx.name == 'Jason Davies'); + TEquals('Jason Davies', CouchDB.session().userCtx.name); // JSON login works var xhr = CouchDB.request("POST", "/_session", { @@ -142,7 +142,7 @@ couchTests.cookie_auth = function(debug) { }); T(JSON.parse(xhr.responseText).ok); - T(CouchDB.session().userCtx.name == 'Jason Davies'); + TEquals('Jason Davies', CouchDB.session().userCtx.name); // update one's own credentials document jasonUserDoc.foo=2; @@ -153,8 +153,8 @@ couchTests.cookie_auth = function(debug) { usersDb.deleteDoc(jchrisUserDoc); T(false && "Can't delete other users docs. Should have thrown an error."); } catch (e) { - T(e.error == "forbidden"); - T(usersDb.last_req.status == 403); + TEquals("forbidden", e.error); + TEquals(403, usersDb.last_req.status); } // TODO should login() throw an exception here? diff --git a/share/www/script/test/reader_acl.js b/share/www/script/test/reader_acl.js index e0dde2c8c..ff770c728 100644 --- a/share/www/script/test/reader_acl.js +++ b/share/www/script/test/reader_acl.js @@ -18,6 +18,13 @@ couchTests.reader_acl = function(debug) { function testFun() { try { usersDb.deleteDb(); + try { + usersDb.createDb(); + } catch(e) { + if(usersDb.last_req.status != 412) { + throw e; + } + } secretDb.deleteDb(); secretDb.createDb(); diff --git a/share/www/script/test/replication.js b/share/www/script/test/replication.js index 224f58914..f54ffff21 100644 --- a/share/www/script/test/replication.js +++ b/share/www/script/test/replication.js @@ -1280,6 +1280,7 @@ couchTests.replication = function(debug) { // delete docs from source TEquals(true, sourceDb.deleteDoc(newDocs[0]).ok); + wait(1000); TEquals(true, sourceDb.deleteDoc(newDocs[6]).ok); waitForSeq(sourceDb, targetDb); diff --git a/share/www/script/test/replicator_db.js b/share/www/script/test/replicator_db.js index 48ca341b2..edc85f49f 100644 --- a/share/www/script/test/replicator_db.js +++ b/share/www/script/test/replicator_db.js @@ -1076,6 +1076,10 @@ couchTests.replicator_db = function(debug) { }); TEquals(200, xhr.status); + // Temporary band-aid, give the replicator db some + // time to make the switch + wait(500); + new_doc = { _id: "foo666", value: 666 diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js index 845429221..ed7d26cb0 100644 --- a/share/www/script/test/rewrite.js +++ b/share/www/script/test/rewrite.js @@ -13,33 +13,33 @@ couchTests.rewrite = function(debug) { - // this test _rewrite handler - - - var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); - db.deleteDb(); - db.createDb(); - - if (debug) debugger; - run_on_modified_server( - [{section: "httpd", - key: "authentication_handlers", - value: "{couch_httpd_auth, special_test_authentication_handler}"}, - {section:"httpd", - key: "WWW-Authenticate", - value: "X-Couch-Test-Auth"}], + var dbNames = ["test_suite_db", "test_suite_db/with_slashes"]; + for (var i=0; i < dbNames.length; i++) { + var db = new CouchDB(dbNames[i]); + var dbName = encodeURIComponent(dbNames[i]); + db.deleteDb(); + db.createDb(); + + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, + {section:"httpd", + key: "WWW-Authenticate", + value: "X-Couch-Test-Auth"}], function(){ var designDoc = { _id:"_design/test", language: "javascript", - _attachments:{ - "foo.txt": { - content_type:"text/plain", - data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" - } - }, + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, rewrites: [ { "from": "foo", @@ -84,18 +84,18 @@ couchTests.rewrite = function(debug) { "method": "GET" }, { - "from": "/welcome4/*", - "to" : "_show/welcome3", - "query": { - "name": "*" - } + "from": "/welcome4/*", + "to" : "_show/welcome3", + "query": { + "name": "*" + } }, { - "from": "/welcome5/*", - "to" : "_show/*", - "query": { - "name": "*" - } + "from": "/welcome5/*", + "to" : "_show/*", + "query": { + "name": "*" + } }, { "from": "basicView", @@ -194,8 +194,8 @@ couchTests.rewrite = function(debug) { if (!firstKey) firstKey = row.key; prevKey = row.key; send('\n<li>Key: '+row.key - +' Value: '+row.value - +' LineNo: '+row_number+'</li>'); + +' Value: '+row.value + +' LineNo: '+row_number+'</li>'); } return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>'; }), @@ -270,7 +270,7 @@ couchTests.rewrite = function(debug) { } } } - + db.save(designDoc); var docs = makeDocs(0, 10); @@ -287,16 +287,16 @@ couchTests.rewrite = function(debug) { db.bulkSave(docs2); // test simple rewriting - - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/foo"); T(req.responseText == "This is a base64 encoded text"); T(req.getResponseHeader("Content-Type") == "text/plain"); - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo2"); + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/foo2"); T(req.responseText == "This is a base64 encoded text"); T(req.getResponseHeader("Content-Type") == "text/plain"); - + // test POST // hello update world @@ -305,111 +305,111 @@ couchTests.rewrite = function(debug) { T(resp.ok); var docid = resp.id; - xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/hello/"+docid); + xhr = CouchDB.request("PUT", "/"+dbName+"/_design/test/_rewrite/hello/"+docid); T(xhr.status == 201); T(xhr.responseText == "hello doc"); T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) - + doc = db.open(docid); T(doc.world == "hello"); - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome?name=user"); + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome?name=user"); T(req.responseText == "Welcome user"); - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome/user"); + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome/user"); T(req.responseText == "Welcome user"); - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome2"); + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome2"); T(req.responseText == "Welcome user"); - xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/welcome3/test"); + xhr = CouchDB.request("PUT", "/"+dbName+"/_design/test/_rewrite/welcome3/test"); T(xhr.status == 201); T(xhr.responseText == "New World"); T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome3/test"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome3/test"); T(xhr.responseText == "Welcome test"); - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome4/user"); + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome4/user"); T(req.responseText == "Welcome user"); - req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome5/welcome3"); + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome5/welcome3"); T(req.responseText == "Welcome welcome3"); - - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/basicView"); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/basicView"); T(xhr.status == 200, "view call"); T(/{"total_rows":9/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/"); T(xhr.status == 200, "view call"); T(/{"total_rows":9/.test(xhr.responseText)); // get with query params - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8"); T(xhr.status == 200, "with query params"); T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 8/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/basicViewFixed"); T(xhr.status == 200, "with query params"); T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 8/.test(xhr.responseText)); // get with query params - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4"); T(xhr.status == 200, "with query params"); T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 8/.test(xhr.responseText)); - + // get with query params - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewPath/3/8"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/basicViewPath/3/8"); T(xhr.status == 200, "with query params"); T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 8/.test(xhr.responseText)); // get with query params - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView"); T(xhr.status == 200, "with query params"); T(/FirstKey: [1, 2]/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView2"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView2"); T(xhr.status == 200, "with query params"); T(/Value: doc 3/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView3"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView3"); T(xhr.status == 200, "with query params"); T(/Value: doc 4/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView4"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView4"); T(xhr.status == 200, "with query params"); T(/Value: doc 5/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView5/test/essai"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView5/test/essai"); T(xhr.status == 200, "with query params"); T(/Value: doc 4/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView6?a=test&b=essai"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView6?a=test&b=essai"); T(xhr.status == 200, "with query params"); T(/Value: doc 4/.test(xhr.responseText)); - xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView7/test/essai?doc=true"); + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView7/test/essai?doc=true"); T(xhr.status == 200, "with query params"); var result = JSON.parse(xhr.responseText); T(typeof(result.rows[0].doc) === "object"); // test path relative to server designDoc.rewrites.push({ - "from": "uuids", - "to": "../../../_uuids" + "from": "uuids", + "to": "../../../_uuids" }); T(db.save(designDoc).ok); - var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/uuids"); + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/uuids"); T(xhr.status == 500); var result = JSON.parse(xhr.responseText); T(result.error == "insecure_rewrite_rule"); @@ -418,23 +418,60 @@ couchTests.rewrite = function(debug) { [{section: "httpd", key: "secure_rewrites", value: "false"}], - function() { - var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/uuids?cache=bust"); - T(xhr.status == 200); - var result = JSON.parse(xhr.responseText); - T(result.uuids.length == 1); - var first = result.uuids[0]; - }); - }); + function() { + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/uuids?cache=bust"); + T(xhr.status == 200); + var result = JSON.parse(xhr.responseText); + T(result.uuids.length == 1); + var first = result.uuids[0]; + }); + }); - // test invalid rewrites - // string - var ddoc = { - _id: "_design/invalid", - rewrites: "[{\"from\":\"foo\",\"to\":\"bar\"}]" - } - db.save(ddoc); - var res = CouchDB.request("GET", "/test_suite_db/_design/invalid/_rewrite/foo"); - TEquals(400, res.status, "should return 400"); + // test invalid rewrites + // string + var ddoc = { + _id: "_design/invalid", + rewrites: "[{\"from\":\"foo\",\"to\":\"bar\"}]" + } + db.save(ddoc); + var res = CouchDB.request("GET", "/"+dbName+"/_design/invalid/_rewrite/foo"); + TEquals(400, res.status, "should return 400"); + + var ddoc_requested_path = { + _id: "_design/requested_path", + rewrites:[ + {"from": "show", "to": "_show/origin/0"}, + {"from": "show_rewritten", "to": "_rewrite/show"} + ], + shows: { + origin: stringFun(function(doc, req) { + return req.headers["x-couchdb-requested-path"]; + })} + }; + + db.save(ddoc_requested_path); + var url = "/"+dbName+"/_design/requested_path/_rewrite/show"; + var res = CouchDB.request("GET", url); + TEquals(url, res.responseText, "should return the original url"); + + var url = "/"+dbName+"/_design/requested_path/_rewrite/show_rewritten"; + var res = CouchDB.request("GET", url); + TEquals(url, res.responseText, "returned the original url"); + var ddoc_loop = { + _id: "_design/loop", + rewrites: [{ "from": "loop", "to": "_rewrite/loop"}] + }; + db.save(ddoc_loop); + + run_on_modified_server( + [{section: "httpd", + key: "rewrite_limit", + value: "2"}], + function(){ + var url = "/"+dbName+"/_design/loop/_rewrite/loop"; + var xhr = CouchDB.request("GET", url); + T(xhr.status = 400); + }); + } } diff --git a/share/www/script/test/update_documents.js b/share/www/script/test/update_documents.js index b352bb403..1dc1b4e9c 100644 --- a/share/www/script/test/update_documents.js +++ b/share/www/script/test/update_documents.js @@ -95,7 +95,10 @@ couchTests.update_documents = function(debug) { "base64" : "aGVsbG8gd29ybGQh" // "hello world!" encoded }; return [doc, resp]; - }) + }), + "empty" : stringFun(function(doc, req) { + return [{}, 'oops']; + }) } }; T(db.save(designDoc).ok); @@ -104,6 +107,7 @@ couchTests.update_documents = function(debug) { var resp = db.save(doc); T(resp.ok); var docid = resp.id; + T(equals(docid, db.last_req.getResponseHeader("X-Couch-Id"))); // update error var xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/"); @@ -114,7 +118,8 @@ couchTests.update_documents = function(debug) { xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/hello/"+docid); T(xhr.status == 201); T(xhr.responseText == "<p>hello doc</p>"); - T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))); + T(equals(docid, xhr.getResponseHeader("X-Couch-Id"))); doc = db.open(docid); T(doc.world == "hello"); @@ -221,4 +226,10 @@ couchTests.update_documents = function(debug) { T(xhr.status == 201); T(xhr.responseText == "hello world!"); T(/application\/octet-stream/.test(xhr.getResponseHeader("Content-Type"))); + + // Insert doc with empty id + xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/empty/foo"); + TEquals(400, xhr.status); + TEquals("Document id must not be empty", JSON.parse(xhr.responseText).reason); + }; diff --git a/share/www/script/test/users_db.js b/share/www/script/test/users_db.js index 7648523b1..44e6c8878 100644 --- a/share/www/script/test/users_db.js +++ b/share/www/script/test/users_db.js @@ -112,6 +112,16 @@ couchTests.users_db = function(debug) { } jchrisUserDoc.roles = []; + // "roles" must exist + delete jchrisUserDoc.roles; + try { + usersDb.save(jchrisUserDoc); + T(false && "should only allow us to save doc when roles exists"); + } catch(e) { + T(e.reason == "doc.roles must exist"); + } + jchrisUserDoc.roles = []; + // character : is not allowed in usernames var joeUserDoc = CouchDB.prepareUserDoc({ name: "joe:erlang" diff --git a/share/www/script/test/uuids.js b/share/www/script/test/uuids.js index fc33a1059..6f5d223a6 100644 --- a/share/www/script/test/uuids.js +++ b/share/www/script/test/uuids.js @@ -117,4 +117,29 @@ couchTests.uuids = function(debug) { utc_testfun ); -}; + // Test utc_id uuids + var utc_id_suffix = "frog"; + var suffix_testfun = function() { + xhr = CouchDB.request("GET", "/_uuids?count=10"); + T(xhr.status == 200); + result = JSON.parse(xhr.responseText); + for(var i = 1; i < result.uuids.length; i++) { + T(result.uuids[i].length == 14 + utc_id_suffix.length); + T(result.uuids[i].substring(14) == utc_id_suffix); + T(result.uuids[i-1] < result.uuids[i], "utc_id_suffix uuids are ordered."); + } + }; + + run_on_modified_server([{ + "section": "uuids", + "key": "algorithm", + "value": "utc_id" + }, { + "section": "uuids", + "key": "utc_id_suffix", + "value": utc_id_suffix + }], + suffix_testfun + ); + + }; diff --git a/share/www/script/test/view_multi_key_all_docs.js b/share/www/script/test/view_multi_key_all_docs.js index 1113be4d9..7c7f6f894 100644 --- a/share/www/script/test/view_multi_key_all_docs.js +++ b/share/www/script/test/view_multi_key_all_docs.js @@ -88,4 +88,8 @@ couchTests.view_multi_key_all_docs = function(debug) { T(rows[1].error == "not_found"); T(!rows[1].id); T(rows[2].id == rows[2].key && rows[2].key == "0"); + + // empty keys + rows = db.allDocs({keys: []}, null).rows; + T(rows.length == 0); }; diff --git a/share/www/script/test/view_multi_key_design.js b/share/www/script/test/view_multi_key_design.js index 383969555..a84d07a49 100644 --- a/share/www/script/test/view_multi_key_design.js +++ b/share/www/script/test/view_multi_key_design.js @@ -61,6 +61,10 @@ couchTests.view_multi_key_design = function(debug) { T(rows[i].key == rows[i].value); } + // with empty keys + rows = db.view("test/all_docs",{keys:[]},null).rows; + T(rows.length == 0); + var reduce = db.view("test/summate",{group:true},keys).rows; T(reduce.length == keys.length); for(var i=0; i<reduce.length; i++) { diff --git a/share/www/script/test/view_multi_key_temp.js b/share/www/script/test/view_multi_key_temp.js index 55eefda5d..3c0540958 100644 --- a/share/www/script/test/view_multi_key_temp.js +++ b/share/www/script/test/view_multi_key_temp.js @@ -34,4 +34,7 @@ couchTests.view_multi_key_temp = function(debug) { T(keys.indexOf(reduce[i].key) != -1); T(reduce[i].key == reduce[i].value); } + + rows = db.query(queryFun, null, {}, []).rows; + T(rows.length == 0); }; diff --git a/share/www/spec/run.html b/share/www/spec/run.html index e438333dd..9a248cb3c 100644 --- a/share/www/spec/run.html +++ b/share/www/spec/run.html @@ -22,6 +22,7 @@ the License. <script src="./custom_helpers.js"></script> <script src="../script/couch.js"></script> <script src="../script/jquery.couch.js"></script> + <script src="../script/couch_test_runner.js"></script> <script> function runSuites() { JSpec diff --git a/share/www/style/layout.css b/share/www/style/layout.css index 814eecd77..54a183ac2 100644 --- a/share/www/style/layout.css +++ b/share/www/style/layout.css @@ -234,6 +234,8 @@ body.fullwidth #wrap { margin-right: 0; } color: #666; margin: 0; padding: 2px 1em 2px 22px; cursor: pointer; font-size: 95%; line-height: 16px; } +#toolbar button[disabled] { opacity: .50; } +#toolbar button[disabled]:hover { background-position: 2px 2px; cursor: default; color: #666 } #toolbar button:hover { background-position: 2px -30px; color: #000; } #toolbar button:active { background-position: 2px -62px; color: #000; } #toolbar button.add { background-image: url(../image/add.png); } diff --git a/share/www/verify_install.html b/share/www/verify_install.html index 1806adfe8..388833bd9 100644 --- a/share/www/verify_install.html +++ b/share/www/verify_install.html @@ -45,6 +45,7 @@ specific language governing permissions and limitations under the License. } function tests() { + CouchDB.urlPrefix = ".."; var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); // cleanup, ignore the 404 diff --git a/src/couch_index/src/couch_index.erl b/src/couch_index/src/couch_index.erl index 508604829..5bf322e9f 100644 --- a/src/couch_index/src/couch_index.erl +++ b/src/couch_index/src/couch_index.erl @@ -171,6 +171,7 @@ handle_call({compacted, NewIdxState}, _From, State) -> updater=Updater, commit_delay=Delay } = State, + assert_signature_match(Mod, OldIdxState, NewIdxState), NewSeq = Mod:get(update_seq, NewIdxState), OldSeq = Mod:get(update_seq, OldIdxState), % For indices that require swapping files, we have to make sure we're @@ -210,7 +211,12 @@ handle_cast({updated, NewIdxState}, State) -> {noreply, NewState} end; handle_cast({new_state, NewIdxState}, State) -> - #st{mod=Mod, commit_delay=Delay} = State, + #st{ + mod=Mod, + idx_state=OldIdxState, + commit_delay=Delay + } = State, + assert_signature_match(Mod, OldIdxState, NewIdxState), CurrSeq = Mod:get(update_seq, NewIdxState), Args = [ Mod:get(db_name, NewIdxState), @@ -323,3 +329,9 @@ send_replies(Waiters, UpdateSeq, IdxState) -> {ToSend, Remaining} = lists:partition(Pred, Waiters), [gen_server:reply(From, {ok, IdxState}) || {From, _} <- ToSend], Remaining. + +assert_signature_match(Mod, OldIdxState, NewIdxState) -> + case {Mod:get(signature, OldIdxState), Mod:get(signature, NewIdxState)} of + {Sig, Sig} -> ok; + _ -> erlang:error(signature_mismatch) + end. diff --git a/src/couch_index/src/couch_index_server.erl b/src/couch_index/src/couch_index_server.erl index 3a0b43627..48fa8e42e 100644 --- a/src/couch_index/src/couch_index_server.erl +++ b/src/couch_index/src/couch_index_server.erl @@ -127,7 +127,7 @@ handle_cast({reset_indexes, DbName}, State) -> handle_info({'EXIT', Pid, Reason}, Server) -> case ets:lookup(?BY_PID, Pid) of - [{Pid, DbName, Sig}] -> + [{Pid, {DbName, Sig}}] -> [{DbName, {DDocId, Sig}}] = ets:match_object(?BY_DB, {DbName, {'$1', Sig}}), rem_from_ets(DbName, Sig, DDocId, Pid); diff --git a/src/couch_index/src/couch_index_updater.erl b/src/couch_index/src/couch_index_updater.erl index 853f3d111..c6d3059f9 100644 --- a/src/couch_index/src/couch_index_updater.erl +++ b/src/couch_index/src/couch_index_updater.erl @@ -86,12 +86,13 @@ handle_cast(_Mesg, State) -> {stop, unknown_cast, State}. -handle_info({'EXIT', Pid, {updated, IdxState}}, #st{mod=Mod, pid=Pid}=State) -> +handle_info({'EXIT', _, {updated, Pid, IdxState}}, #st{pid=Pid}=State) -> + Mod = State#st.mod, Args = [Mod:get(db_name, IdxState), Mod:get(idx_name, IdxState)], ?LOG_INFO("Index update finished for db: ~s idx: ~s", Args), ok = gen_server:cast(State#st.idx, {updated, IdxState}), {noreply, State#st{pid=undefined}}; -handle_info({'EXIT', Pid, reset}, #st{idx=Idx, pid=Pid}=State) -> +handle_info({'EXIT', _, {reset, Pid}}, #st{idx=Idx, pid=Pid}=State) -> {ok, NewIdxState} = gen_server:call(State#st.idx, reset), Pid2 = spawn_link(fun() -> update(Idx, State#st.mod, NewIdxState) end), {noreply, State#st{pid=Pid2}}; @@ -131,7 +132,7 @@ update(Idx, Mod, IdxState) -> PurgedIdxState = case purge_index(Db, Mod, IdxState) of {ok, IdxState0} -> IdxState0; - reset -> exit(reset) + reset -> exit({reset, self()}) end, NumChanges = couch_db:count_changes_since(Db, CurrSeq), @@ -181,7 +182,7 @@ update(Idx, Mod, IdxState) -> end, {ok, FinalIdxState} = Mod:finish_update(LastIdxSt), - exit({updated, FinalIdxState}) + exit({updated, self(), FinalIdxState}) end). diff --git a/src/couch_mrview/src/couch_mrview_compactor.erl b/src/couch_mrview/src/couch_mrview_compactor.erl index cf3cf22f2..b500ce39d 100644 --- a/src/couch_mrview/src/couch_mrview_compactor.erl +++ b/src/couch_mrview/src/couch_mrview_compactor.erl @@ -116,11 +116,11 @@ compact(State) -> recompact(State) -> link(State#mrst.fd), - {_Pid, Ref} = erlang:spawn_monitor(fun() -> + {Pid, Ref} = erlang:spawn_monitor(fun() -> couch_index_updater:update(couch_mrview_index, State) end), receive - {'DOWN', Ref, _, _, {updated, State2}} -> + {'DOWN', Ref, _, _, {updated, Pid, State2}} -> unlink(State#mrst.fd), {ok, State2} end. diff --git a/src/couch_mrview/src/couch_mrview_index.erl b/src/couch_mrview/src/couch_mrview_index.erl index a60465143..6bcb63f0e 100644 --- a/src/couch_mrview/src/couch_mrview_index.erl +++ b/src/couch_mrview/src/couch_mrview_index.erl @@ -18,7 +18,7 @@ -export([start_update/3, purge/4, process_doc/3, finish_update/1, commit/1]). -export([compact/3, swap_compacted/2]). - +-include("couch_db.hrl"). -include_lib("couch_mrview/include/couch_mrview.hrl"). @@ -88,8 +88,9 @@ open(Db, State) -> {ok, RefCounter} = couch_ref_counter:start([Fd]), {ok, NewSt#mrst{refc=RefCounter}} end; - Error -> - (catch couch_mrview_util:delete_files(DbName, Sig)), + {error, Reason} = Error -> + ?LOG_ERROR("Failed to open view file '~s': ~s", + [IndexFName, file:format_error(Reason)]), Error end. diff --git a/src/couch_mrview/src/couch_mrview_show.erl b/src/couch_mrview/src/couch_mrview_show.erl index 5ba7b9167..39c570dcf 100644 --- a/src/couch_mrview/src/couch_mrview_show.erl +++ b/src/couch_mrview/src/couch_mrview_show.erl @@ -101,7 +101,7 @@ show_etag(#httpd{user_ctx=UserCtx}=Req, Doc, DDoc, More) -> Doc -> couch_httpd:doc_etag(Doc) end, couch_httpd:make_etag({couch_httpd:doc_etag(DDoc), DocPart, Accept, - UserCtx#user_ctx.roles, More}). + {UserCtx#user_ctx.name, UserCtx#user_ctx.roles}, More}). % updates a doc based on a request % handle_doc_update_req(#httpd{method = 'GET'}=Req, _Db, _DDoc) -> @@ -143,11 +143,24 @@ send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) -> Options = [{user_ctx, Req#httpd.user_ctx}] end, NewDoc = couch_doc:from_json_obj({NewJsonDoc}), + couch_doc:validate_docid(NewDoc#doc.id), {ok, NewRev} = couch_db:update_doc(Db, NewDoc, Options), NewRevStr = couch_doc:rev_to_str(NewRev), + DocIdHeader = case DocId of + null -> + [{<<"json">>, {Props}}] = JsonResp0, + case lists:keyfind(<<"id">>, 1, Props) of + {_, NewDocId} -> + [{<<"X-Couch-Id">>, NewDocId}]; + false -> + [] + end; + DocId -> + [{<<"X-Couch-Id">>, DocId}] + end, {[ {<<"code">>, 201}, - {<<"headers">>, {[{<<"X-Couch-Update-NewRev">>, NewRevStr}]}} + {<<"headers">>, {[{<<"X-Couch-Update-NewRev">>, NewRevStr}] ++ DocIdHeader}} | JsonResp0]}; [<<"up">>, _Other, {JsonResp0}] -> {[{<<"code">>, 200} | JsonResp0]} @@ -192,9 +205,10 @@ handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, Keys) -> Args0 = couch_mrview_http:parse_qs(Req, Keys), ETagFun = fun(BaseSig, Acc0) -> UserCtx = Req#httpd.user_ctx, + Name = UserCtx#user_ctx.name, Roles = UserCtx#user_ctx.roles, Accept = couch_httpd:header_value(Req, "Accept"), - Parts = {couch_httpd:doc_etag(DDoc), Accept, Roles}, + Parts = {couch_httpd:doc_etag(DDoc), Accept, {Name, Roles}}, ETag = couch_httpd:make_etag({BaseSig, Parts}), case couch_httpd:etag_match(Req, ETag) of true -> throw({etag_match, ETag}); diff --git a/src/couch_mrview/src/couch_mrview_util.erl b/src/couch_mrview/src/couch_mrview_util.erl index 2d75df963..ba4de2d64 100644 --- a/src/couch_mrview/src/couch_mrview_util.erl +++ b/src/couch_mrview/src/couch_mrview_util.erl @@ -260,7 +260,8 @@ all_docs_reduce_to_count(Reductions) -> {Count, _, _} = couch_btree:final_reduce(Reduce, Reductions), Count. - +reduce_to_count(nil) -> + 0; reduce_to_count(Reductions) -> Reduce = fun (reduce, KVs) -> @@ -592,10 +593,7 @@ ad_ekey_opts(#mrargs{end_key_docid=EKeyDocId}=Args) -> key_opts(Args) -> key_opts(Args, []). - -key_opts(#mrargs{keys=undefined}=Args, Extra) -> - key_opts(Args#mrargs{keys=[]}, Extra); -key_opts(#mrargs{keys=[], direction=Dir}=Args, Extra) -> +key_opts(#mrargs{keys=undefined, direction=Dir}=Args, Extra) -> [[{dir, Dir}] ++ skey_opts(Args) ++ ekey_opts(Args) ++ Extra]; key_opts(#mrargs{keys=Keys, direction=Dir}=Args, Extra) -> lists:map(fun(K) -> diff --git a/src/couch_mrview/test/01-load.t b/src/couch_mrview/test/01-load.t index 4613f4989..a57c1a775 100644 --- a/src/couch_mrview/test/01-load.t +++ b/src/couch_mrview/test/01-load.t @@ -29,6 +29,6 @@ main(_) -> etap:plan(length(Modules)), lists:foreach( fun(Module) -> - etap_can:loaded_ok(Module, lists:concat(["Loaded: ", Module])) + etap:loaded_ok(Module, lists:concat(["Loaded: ", Module])) end, Modules), etap:end_tests(). diff --git a/src/couch_replicator/src/couch_replicator.erl b/src/couch_replicator/src/couch_replicator.erl index 1f7c08a76..1ce2cf545 100644 --- a/src/couch_replicator/src/couch_replicator.erl +++ b/src/couch_replicator/src/couch_replicator.erl @@ -112,7 +112,7 @@ async_replicate(#rep{id = {BaseId, Ext}, source = Src, target = Tgt} = Rep) -> RepChildId, {gen_server, start_link, [?MODULE, Rep, [{timeout, Timeout}]]}, temporary, - 1, + 250, worker, [?MODULE] }, @@ -333,6 +333,9 @@ do_init(#rep{options = Options, id = {BaseId, Ext}} = Rep) -> }. +handle_info(shutdown, St) -> + {stop, shutdown, St}; + handle_info({'DOWN', Ref, _, _, Why}, #rep_state{source_monitor = Ref} = St) -> ?LOG_ERROR("Source database is down. Reason: ~p", [Why]), {stop, source_db_down, St}; @@ -550,7 +553,7 @@ init_state(Rep) -> committed_seq = StartSeq, source_log = SourceLog, target_log = TargetLog, - rep_starttime = httpd_util:rfc1123_date(), + rep_starttime = couch_util:rfc1123_date(), src_starttime = get_value(<<"instance_start_time">>, SourceInfo), tgt_starttime = get_value(<<"instance_start_time">>, TargetInfo), session_id = couch_uuids:random(), @@ -703,7 +706,7 @@ do_checkpoint(State) -> ?LOG_INFO("recording a checkpoint for `~s` -> `~s` at source update_seq ~p", [SourceName, TargetName, NewSeq]), StartTime = ?l2b(ReplicationStartTime), - EndTime = ?l2b(httpd_util:rfc1123_date()), + EndTime = ?l2b(couch_util:rfc1123_date()), NewHistoryEntry = {[ {<<"session_id">>, SessionId}, {<<"start_time">>, StartTime}, diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl index 6804448e2..8773383c3 100644 --- a/src/couch_replicator/src/couch_replicator_httpc.erl +++ b/src/couch_replicator/src/couch_replicator_httpc.erl @@ -185,22 +185,33 @@ error_cause(Cause) -> stream_data_self(#httpdb{timeout = T} = HttpDb, Params, Worker, ReqId, Cb) -> + case accumulate_messages(ReqId, [], T + 500) of + {Data, ibrowse_async_response} -> + ibrowse:stream_next(ReqId), + {Data, fun() -> stream_data_self(HttpDb, Params, Worker, ReqId, Cb) end}; + {Data, ibrowse_async_response_end} -> + {Data, fun() -> throw({maybe_retry_req, more_data_expected}) end} + end. + +accumulate_messages(ReqId, Acc, Timeout) -> receive {ibrowse_async_response, ReqId, {error, Error}} -> throw({maybe_retry_req, Error}); {ibrowse_async_response, ReqId, <<>>} -> - ibrowse:stream_next(ReqId), - stream_data_self(HttpDb, Params, Worker, ReqId, Cb); + accumulate_messages(ReqId, Acc, Timeout); {ibrowse_async_response, ReqId, Data} -> - ibrowse:stream_next(ReqId), - {Data, fun() -> stream_data_self(HttpDb, Params, Worker, ReqId, Cb) end}; + accumulate_messages(ReqId, [Data | Acc], 0); {ibrowse_async_response_end, ReqId} -> - {<<>>, fun() -> throw({maybe_retry_req, more_data_expected}) end} - after T + 500 -> + {iolist_to_binary(lists:reverse(Acc)), ibrowse_async_response_end} + after Timeout -> % Note: ibrowse should always reply with timeouts, but this doesn't % seem to be always true when there's a very high rate of requests % and many open connections. - throw({maybe_retry_req, timeout}) + if Acc =:= [] -> + throw({maybe_retry_req, timeout}); + true -> + {iolist_to_binary(lists:reverse(Acc)), ibrowse_async_response} + end end. diff --git a/src/couch_replicator/src/couch_replicator_manager.erl b/src/couch_replicator/src/couch_replicator_manager.erl index 499e3bf36..15d625545 100644 --- a/src/couch_replicator/src/couch_replicator_manager.erl +++ b/src/couch_replicator/src/couch_replicator_manager.erl @@ -431,7 +431,14 @@ replication_complete(DocId) -> % We want to be able to start the same replication but with % eventually different values for parameters that don't % contribute to its ID calculation. - _ = supervisor:delete_child(couch_replicator_job_sup, BaseId ++ Ext); + case erlang:system_info(otp_release) < "R14B02" of + true -> + spawn(fun() -> + _ = supervisor:delete_child(couch_replicator_job_sup, BaseId ++ Ext) + end); + false -> + ok + end; #rep_state{} -> ok end, diff --git a/src/couch_replicator/test/01-load.t b/src/couch_replicator/test/01-load.t index 07561a792..8bd82ddc7 100644 --- a/src/couch_replicator/test/01-load.t +++ b/src/couch_replicator/test/01-load.t @@ -32,6 +32,6 @@ main(_) -> etap:plan(length(Modules)), lists:foreach( fun(Module) -> - etap_can:loaded_ok(Module, lists:concat(["Loaded: ", Module])) + etap:loaded_ok(Module, lists:concat(["Loaded: ", Module])) end, Modules), etap:end_tests(). diff --git a/src/couchdb/couch_changes.erl b/src/couchdb/couch_changes.erl index aec7873d8..85c9e54f9 100644 --- a/src/couchdb/couch_changes.erl +++ b/src/couchdb/couch_changes.erl @@ -63,7 +63,8 @@ handle_changes(Args1, Req, Db0) -> put(last_changes_heartbeat, now()) end, - if Feed == "continuous" orelse Feed == "longpoll" -> + case lists:member(Feed, ["continuous", "longpoll", "eventsource"]) of + true -> fun(CallbackAcc) -> {Callback, UserAcc} = get_callback_acc(CallbackAcc), Self = self(), @@ -89,7 +90,7 @@ handle_changes(Args1, Req, Db0) -> get_rest_db_updated(ok) % clean out any remaining update messages end end; - true -> + false -> fun(CallbackAcc) -> {Callback, UserAcc} = get_callback_acc(CallbackAcc), UserAcc2 = start_sending_changes(Callback, UserAcc, Feed), @@ -261,7 +262,9 @@ get_changes_timeout(Args, Callback) -> fun(UserAcc) -> {ok, Callback(timeout, ResponseType, UserAcc)} end} end. -start_sending_changes(_Callback, UserAcc, "continuous") -> +start_sending_changes(_Callback, UserAcc, ResponseType) + when ResponseType =:= "continuous" + orelse ResponseType =:= "eventsource" -> UserAcc; start_sending_changes(Callback, UserAcc, ResponseType) -> Callback(start, ResponseType, UserAcc). @@ -434,7 +437,9 @@ keep_sending_changes(Args, Acc0, FirstRound) -> end_sending_changes(Callback, UserAcc, EndSeq, ResponseType) -> Callback({stop, EndSeq}, ResponseType, UserAcc). -changes_enumerator(DocInfo, #changes_acc{resp_type = "continuous"} = Acc) -> +changes_enumerator(DocInfo, #changes_acc{resp_type = ResponseType} = Acc) + when ResponseType =:= "continuous" + orelse ResponseType =:= "eventsource" -> #changes_acc{ filter = FilterFun, callback = Callback, user_acc = UserAcc, limit = Limit, db = Db, @@ -456,7 +461,7 @@ changes_enumerator(DocInfo, #changes_acc{resp_type = "continuous"} = Acc) -> end; _ -> ChangesRow = changes_row(Results, DocInfo, Acc), - UserAcc2 = Callback({change, ChangesRow, <<>>}, "continuous", UserAcc), + UserAcc2 = Callback({change, ChangesRow, <<>>}, ResponseType, UserAcc), reset_heartbeat(), {Go, Acc#changes_acc{seq = Seq, user_acc = UserAcc2, limit = Limit - 1}} end; diff --git a/src/couchdb/couch_config.erl b/src/couchdb/couch_config.erl index f66985387..96fabbad8 100644 --- a/src/couchdb/couch_config.erl +++ b/src/couchdb/couch_config.erl @@ -12,7 +12,7 @@ % Reads CouchDB's ini file and gets queried for configuration parameters. % This module is initialized with a list of ini files that it consecutively -% reads Key/Value pairs from and saves them in an ets table. If more an one +% reads Key/Value pairs from and saves them in an ets table. If more than one % ini file is specified, the last one is used to write changes that are made % with store/2 back to that ini file. @@ -187,13 +187,10 @@ parse_ini_file(IniFile) -> case file:read_file(IniFilename) of {ok, IniBin0} -> IniBin0; - {error, eacces} -> - throw({file_permission_error, IniFile}); - {error, enoent} -> - Fmt = "Couldn't find server configuration file ~s.", - Msg = ?l2b(io_lib:format(Fmt, [IniFilename])), - ?LOG_ERROR("~s~n", [Msg]), - throw({startup_error, Msg}) + {error, Reason} = Error -> + ?LOG_ERROR("Could not read server configuration file ~s: ~s", + [IniFilename, file:format_error(Reason)]), + throw(Error) end, Lines = re:split(IniBin, "\r\n|\n|\r|\032", [{return, list}]), diff --git a/src/couchdb/couch_config_writer.erl b/src/couchdb/couch_config_writer.erl index decd269a7..21f1c3f9d 100644 --- a/src/couchdb/couch_config_writer.erl +++ b/src/couchdb/couch_config_writer.erl @@ -22,6 +22,8 @@ -export([save_to_file/2]). +-include("couch_db.hrl"). + %% @spec save_to_file( %% Config::{{Section::string(), Option::string()}, Value::string()}, %% File::filename()) -> ok @@ -38,9 +40,9 @@ save_to_file({{Section, Key}, Value}, File) -> case file:write_file(File, NewFileContents) of ok -> ok; - {error, eacces} -> - {file_permission_error, File}; - Error -> + {error, Reason} = Error -> + ?LOG_ERROR("Could not write config file ~s: ~s", + [File, file:format_error(Reason)]), Error end. diff --git a/src/couchdb/couch_db_update_notifier.erl b/src/couchdb/couch_db_update_notifier.erl index 150eb31b5..bfa770acc 100644 --- a/src/couchdb/couch_db_update_notifier.erl +++ b/src/couchdb/couch_db_update_notifier.erl @@ -53,8 +53,8 @@ handle_event(Event, Fun) when is_function(Fun, 1) -> handle_event(Event, {Fun, FunAcc}) -> FunAcc2 = Fun(Event, FunAcc), {ok, {Fun, FunAcc2}}; -handle_event({EventAtom, DbName}, Pid) -> - Obj = {[{type, list_to_binary(atom_to_list(EventAtom))}, {db, DbName}]}, +handle_event({EventType, EventDesc}, Pid) -> + Obj = encode_event(EventType, EventDesc), ok = couch_os_process:send(Pid, Obj), {ok, Pid}. @@ -71,3 +71,12 @@ handle_info({'EXIT', _, _}, Pid) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. + +encode_event(EventType, EventDesc) when is_atom(EventType) -> + encode_event(atom_to_list(EventType), EventDesc); +encode_event(EventType, EventDesc) when is_list(EventType) -> + encode_event(?l2b(EventType), EventDesc); +encode_event(EventType, {DbName, DocId}) -> + {[{type, EventType}, {db, DbName}, {id, DocId}]}; +encode_event(EventType, DbName) -> + {[{type, EventType}, {db, DbName}]}. diff --git a/src/couchdb/couch_doc.erl b/src/couchdb/couch_doc.erl index b565a91ac..349df4a1f 100644 --- a/src/couchdb/couch_doc.erl +++ b/src/couchdb/couch_doc.erl @@ -188,6 +188,8 @@ parse_revs([Rev | Rest]) -> [parse_rev(Rev) | parse_revs(Rest)]. +validate_docid(<<"">>) -> + throw({bad_request, <<"Document id must not be empty">>}); validate_docid(Id) when is_binary(Id) -> case couch_util:validate_utf8(Id) of false -> throw({bad_request, <<"Document id must be valid UTF-8">>}); diff --git a/src/couchdb/couch_file.erl b/src/couchdb/couch_file.erl index 2c2f11ac3..e00b0f002 100644 --- a/src/couchdb/couch_file.erl +++ b/src/couchdb/couch_file.erl @@ -53,17 +53,19 @@ open(Filepath, Options) -> ignore -> % get the error receive - {Ref, Pid, Error} -> + {Ref, Pid, {error, Reason} = Error} -> case process_info(self(), trap_exit) of {trap_exit, true} -> receive {'EXIT', Pid, _} -> ok end; {trap_exit, false} -> ok end, - case Error of - {error, eacces} -> {file_permission_error, Filepath}; - _ -> Error - end + ?LOG_DEBUG("Could not open file ~s: ~s", + [Filepath, file:format_error(Reason)]), + Error end; Error -> + % We can't say much here, because it could be any kind of error. + % Just let it bubble and an encapsulating subcomponent can perhaps + % be more informative. It will likely appear in the SASL log, anyway. Error end. @@ -290,7 +292,7 @@ init({Filepath, Options, ReturnPid, Ref}) -> {ok, #file{fd=Fd}}; false -> ok = file:close(Fd), - init_status_error(ReturnPid, Ref, file_exists) + init_status_error(ReturnPid, Ref, {error, eexist}) end; false -> maybe_track_open_os_files(Options), diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 8b0507683..eb35ff968 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -97,6 +97,7 @@ start_link(Name, Options) -> % will restart us and then we will pick up the new settings. BindAddress = couch_config:get("httpd", "bind_address", any), + validate_bind_address(BindAddress), DefaultSpec = "{couch_httpd_db, handle_request}", DefaultFun = make_arity_1_fun( couch_config:get("httpd", "default_handler", DefaultSpec) @@ -339,6 +340,8 @@ handle_request_int(MochiReq, DefaultFun, " must be built with Erlang OTP R13B04 or higher.", ?LOG_ERROR("~s", [ErrorReason]), send_error(HttpReq, {bad_otp_release, ErrorReason}); + exit:{body_too_large, _} -> + send_error(HttpReq, request_entity_too_large); throw:Error -> Stack = erlang:get_stacktrace(), ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]), @@ -525,33 +528,13 @@ recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) -> % called with Length == 0 on the last time. MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState). -body_length(Req) -> - case header_value(Req, "Transfer-Encoding") of - undefined -> - case header_value(Req, "Content-Length") of - undefined -> undefined; - Length -> list_to_integer(Length) - end; - "chunked" -> chunked; - Unknown -> {unknown_transfer_encoding, Unknown} - end. +body_length(#httpd{mochi_req=MochiReq}) -> + MochiReq:get(body_length). -body(#httpd{mochi_req=MochiReq, req_body=undefined} = Req) -> - case body_length(Req) of - undefined -> - MaxSize = list_to_integer( - couch_config:get("couchdb", "max_document_size", "4294967296")), - MochiReq:recv_body(MaxSize); - chunked -> - ChunkFun = fun({0, _Footers}, Acc) -> - lists:reverse(Acc); - ({_Len, Chunk}, Acc) -> - [Chunk | Acc] - end, - recv_chunked(Req, 8192, ChunkFun, []); - Len -> - MochiReq:recv_body(Len) - end; +body(#httpd{mochi_req=MochiReq, req_body=undefined}) -> + MaxSize = list_to_integer( + couch_config:get("couchdb", "max_document_size", "4294967296")), + MochiReq:recv_body(MaxSize); body(#httpd{req_body=ReqBody}) -> ReqBody. @@ -629,7 +612,7 @@ start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), - couch_stats_collector:increment({httpd_status_cdes, Code}), + couch_stats_collector:increment({httpd_status_codes, Code}), CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), Headers2 = Headers ++ server_header() ++ CookieHeader, Resp = MochiReq:start_response({Code, Headers2}), @@ -686,7 +669,9 @@ send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), Headers2 = http_1_0_keep_alive(MochiReq, Headers), - if Code >= 400 -> + if Code >= 500 -> + ?LOG_ERROR("httpd ~p error response:~n ~s", [Code, Body]); + Code >= 400 -> ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); true -> ok end, @@ -707,8 +692,19 @@ send_json(Req, Code, Headers, Value) -> {"Content-Type", negotiate_content_type(Req)}, {"Cache-Control", "must-revalidate"} ], + IdHeader = case Value of + {Props} when is_list(Props) -> + case lists:keyfind(id, 1, Props) of + {_, Id} -> + [{"X-Couch-Id", Id}]; + _ -> + [] + end; + _ -> + [] + end, Body = [start_jsonp(), ?JSON_ENCODE(Value), end_jsonp(), $\n], - send_response(Req, Code, DefaultHeaders ++ Headers, Body). + send_response(Req, Code, DefaultHeaders ++ IdHeader ++ Headers, Body). start_json_response(Req, Code) -> start_json_response(Req, Code, []). @@ -813,14 +809,17 @@ error_info({unauthorized, Msg}) -> error_info(file_exists) -> {412, <<"file_exists">>, <<"The database could not be " "created, the file already exists.">>}; +error_info(request_entity_too_large) -> + {413, <<"too_large">>, <<"the request entity is too large">>}; error_info({bad_ctype, Reason}) -> {415, <<"bad_content_type">>, Reason}; error_info(requested_range_not_satisfiable) -> {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>}; -error_info({error, illegal_database_name}) -> - {400, <<"illegal_database_name">>, <<"Only lowercase characters (a-z), " - "digits (0-9), and any of the characters _, $, (, ), +, -, and / " - "are allowed. Must begin with a letter.">>}; +error_info({error, illegal_database_name, Name}) -> + Message = "Name: '" ++ Name ++ "'. Only lowercase characters (a-z), " + ++ "digits (0-9), and any of the characters _, $, (, ), +, -, and / " + ++ "are allowed. Must begin with a letter.", + {400, <<"illegal_database_name">>, couch_util:to_binary(Message)}; error_info({missing_stub, Reason}) -> {412, <<"missing_stub">>, Reason}; error_info({Error, Reason}) -> @@ -1059,33 +1058,36 @@ check_for_last(#mp{buffer=Buffer, data_fun=DataFun}=Mp) -> data_fun = DataFun2}) end. -find_in_binary(B, Data) when size(B) > 0 -> - case size(Data) - size(B) of - Last when Last < 0 -> - partial_find(B, Data, 0, size(Data)); - Last -> - find_in_binary(B, size(B), Data, 0, Last) +find_in_binary(_B, <<>>) -> + not_found; + +find_in_binary(B, Data) -> + case binary:match(Data, [B], []) of + nomatch -> + partial_find(binary:part(B, {0, byte_size(B) - 1}), + binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), 1); + {Pos, _Len} -> + {exact, Pos} end. -find_in_binary(B, BS, D, N, Last) when N =< Last-> - case D of - <<_:N/binary, B:BS/binary, _/binary>> -> - {exact, N}; - _ -> - find_in_binary(B, BS, D, 1 + N, Last) +partial_find(<<>>, _Data, _Pos) -> + not_found; + +partial_find(B, Data, N) when byte_size(Data) > 0 -> + case binary:match(Data, [B], []) of + nomatch -> + partial_find(binary:part(B, {0, byte_size(B) - 1}), + binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), N + 1); + {Pos, _Len} -> + {partial, N + Pos} end; -find_in_binary(B, BS, D, N, Last) when N =:= 1 + Last -> - partial_find(B, D, N, BS - 1). -partial_find(_B, _D, _N, 0) -> - not_found; -partial_find(B, D, N, K) -> - <<B1:K/binary, _/binary>> = B, - case D of - <<_Skip:N/binary, B1/binary>> -> - {partial, N}; - _ -> - partial_find(B, D, 1 + N, K - 1) - end. +partial_find(_B, _Data, _N) -> + not_found. +validate_bind_address(Address) -> + case inet_parse:address(Address) of + {ok, _} -> ok; + _ -> throw({error, invalid_bind_address}) + end. diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index de39b9efb..4b345da6c 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -76,14 +76,23 @@ handle_changes_req1(Req, #db{name=DbName}=Db) -> handle_changes_req2(Req, Db) -> MakeCallback = fun(Resp) -> - fun({change, Change, _}, "continuous") -> + fun({change, {ChangeProp}=Change, _}, "eventsource") -> + Seq = proplists:get_value(<<"seq">>, ChangeProp), + send_chunk(Resp, ["data: ", ?JSON_ENCODE(Change), + "\n", "id: ", ?JSON_ENCODE(Seq), + "\n\n"]); + ({change, Change, _}, "continuous") -> send_chunk(Resp, [?JSON_ENCODE(Change) | "\n"]); ({change, Change, Prepend}, _) -> send_chunk(Resp, [Prepend, ?JSON_ENCODE(Change)]); + (start, "eventsource") -> + ok; (start, "continuous") -> ok; (start, _) -> send_chunk(Resp, "{\"results\":[\n"); + ({stop, _EndSeq}, "eventsource") -> + end_json_response(Resp); ({stop, EndSeq}, "continuous") -> send_chunk( Resp, @@ -100,7 +109,7 @@ handle_changes_req2(Req, Db) -> send_chunk(Resp, "\n") end end, - ChangesArgs = parse_changes_query(Req), + ChangesArgs = parse_changes_query(Req, Db), ChangesFun = couch_changes:handle_changes(ChangesArgs, Req, Db), WrapperFun = case ChangesArgs#changes_args.feed of "normal" -> @@ -118,6 +127,15 @@ handle_changes_req2(Req, Db) -> end ) end; + "eventsource" -> + Headers = [ + {"Content-Type", "text/event-stream"}, + {"Cache-Control", "no-cache"} + ], + {ok, Resp} = couch_httpd:start_chunked_response(Req, 200, Headers), + fun(FeedChangesFun) -> + FeedChangesFun(MakeCallback(Resp)) + end; _ -> % "longpoll" or "continuous" {ok, Resp} = couch_httpd:start_json_response(Req, 200), @@ -1095,15 +1113,22 @@ parse_doc_query(Req) -> end end, #doc_query_args{}, couch_httpd:qs(Req)). -parse_changes_query(Req) -> +parse_changes_query(Req, Db) -> lists:foldl(fun({Key, Value}, Args) -> - case {Key, Value} of + case {string:to_lower(Key), Value} of {"feed", _} -> Args#changes_args{feed=Value}; {"descending", "true"} -> Args#changes_args{dir=rev}; + {"since", "now"} -> + UpdateSeq = couch_util:with_db(Db#db.name, fun(WDb) -> + couch_db:get_update_seq(WDb) + end), + Args#changes_args{since=UpdateSeq}; {"since", _} -> Args#changes_args{since=list_to_integer(Value)}; + {"last-event-id", _} -> + Args#changes_args{since=list_to_integer(Value)}; {"limit", _} -> Args#changes_args{limit=list_to_integer(Value)}; {"style", _} -> diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl index 38dd98ec1..f7a4d7533 100644 --- a/src/couchdb/couch_httpd_misc_handlers.erl +++ b/src/couchdb/couch_httpd_misc_handlers.erl @@ -44,12 +44,12 @@ handle_welcome_req(Req, _) -> send_method_not_allowed(Req, "GET,HEAD"). handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) -> - {{Year,Month,Day},Time} = erlang:localtime(), + {{Year,Month,Day},Time} = erlang:universaltime(), OneYearFromNow = {{Year+1,Month,Day},Time}, CachingHeaders = [ %favicon should expire a year from now {"Cache-Control", "public, max-age=31536000"}, - {"Expires", httpd_util:rfc1123_date(OneYearFromNow)} + {"Expires", couch_util:rfc1123_date(OneYearFromNow)} ], couch_httpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders); @@ -90,8 +90,9 @@ handle_task_status_req(Req) -> handle_restart_req(#httpd{method='POST'}=Req) -> couch_httpd:validate_ctype(Req, "application/json"), ok = couch_httpd:verify_is_server_admin(Req), + Result = send_json(Req, 202, {[{ok, true}]}), couch_server_sup:restart_core_server(), - send_json(Req, 200, {[{ok, true}]}); + Result; handle_restart_req(Req) -> send_method_not_allowed(Req, "POST"). @@ -102,7 +103,7 @@ handle_uuids_req(#httpd{method='GET'}=Req) -> Etag = couch_httpd:make_etag(UUIDs), couch_httpd:etag_respond(Req, Etag, fun() -> CacheBustingHeaders = [ - {"Date", httpd_util:rfc1123_date()}, + {"Date", couch_util:rfc1123_date()}, {"Cache-Control", "no-cache"}, % Past date, ON PURPOSE! {"Expires", "Fri, 01 Jan 1990 00:00:00 GMT"}, @@ -212,7 +213,12 @@ handle_config_req(Req) -> % PUT /_config/Section/Key % "value" handle_approved_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req, Persist) -> - Value = couch_httpd:json_body(Req), + Value = case Section of + <<"admins">> -> + couch_passwords:hash_admin_password(couch_httpd:json_body(Req)); + _ -> + couch_httpd:json_body(Req) + end, OldValue = couch_config:get(Section, Key, ""), case couch_config:set(Section, Key, ?b2l(Value), Persist) of ok -> diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl index c8cab85d7..756cdefb2 100644 --- a/src/couchdb/couch_httpd_rewrite.erl +++ b/src/couchdb/couch_httpd_rewrite.erl @@ -116,9 +116,20 @@ handle_rewrite_req(#httpd{ % we are in a design handler DesignId = <<"_design/", DesignName/binary>>, - Prefix = <<"/", DbName/binary, "/", DesignId/binary>>, + Prefix = <<"/", (?l2b(couch_util:url_encode(DbName)))/binary, "/", DesignId/binary>>, QueryList = lists:map(fun decode_query_value/1, couch_httpd:qs(Req)), + MaxRewritesList = couch_config:get("httpd", "rewrite_limit", "100"), + MaxRewrites = list_to_integer(MaxRewritesList), + case get(couch_rewrite_count) of + undefined -> + put(couch_rewrite_count, 1); + NumRewrites when NumRewrites < MaxRewrites -> + put(couch_rewrite_count, NumRewrites + 1); + _ -> + throw({bad_request, <<"Exceeded rewrite recursion limit">>}) + end, + #doc{body={Props}} = DDoc, % get rules from ddoc @@ -165,9 +176,10 @@ handle_rewrite_req(#httpd{ % normalize final path (fix levels "." and "..") RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))), - % in order to do OAuth correctly, - % we have to save the requested path - Headers = mochiweb_headers:enter("x-couchdb-requested-path", + % In order to do OAuth correctly, we have to save the + % requested path. We use default so chained rewriting + % wont replace the original header. + Headers = mochiweb_headers:default("x-couchdb-requested-path", MochiReq:get(raw_path), MochiReq:get(headers)), diff --git a/src/couchdb/couch_httpd_vhost.erl b/src/couchdb/couch_httpd_vhost.erl index b63565b74..59f05ce79 100644 --- a/src/couchdb/couch_httpd_vhost.erl +++ b/src/couchdb/couch_httpd_vhost.erl @@ -244,7 +244,10 @@ bind_path(_, _) -> %% create vhost list from ini make_vhosts() -> - Vhosts = lists:foldl(fun({Vhost, Path}, Acc) -> + Vhosts = lists:foldl(fun + ({_, ""}, Acc) -> + Acc; + ({Vhost, Path}, Acc) -> [{parse_vhost(Vhost), split_path(Path)}|Acc] end, [], couch_config:get("vhosts")), diff --git a/src/couchdb/couch_js_functions.hrl b/src/couchdb/couch_js_functions.hrl index 36e15129c..2ecd85142 100644 --- a/src/couchdb/couch_js_functions.hrl +++ b/src/couchdb/couch_js_functions.hrl @@ -31,7 +31,11 @@ throw({forbidden: 'doc.name is required'}); } - if (newDoc.roles && !isArray(newDoc.roles)) { + if (!newDoc.roles) { + throw({forbidden: 'doc.roles must exist'}); + } + + if (!isArray(newDoc.roles)) { throw({forbidden: 'doc.roles must be an array'}); } diff --git a/src/couchdb/couch_log.erl b/src/couchdb/couch_log.erl index 8e24cab5e..fc7b39364 100644 --- a/src/couchdb/couch_log.erl +++ b/src/couchdb/couch_log.erl @@ -89,10 +89,10 @@ init([]) -> case file:open(Filename, [append]) of {ok, Fd} -> {ok, #state{fd = Fd, level = Level, sasl = Sasl}}; - {error, eacces} -> - {stop, {file_permission_error, Filename}}; - Error -> - {stop, Error} + {error, Reason} -> + ReasonStr = file:format_error(Reason), + io:format("Error opening log file ~s: ~s", [Filename, ReasonStr]), + {stop, {error, ReasonStr, Filename}} end. debug_on() -> @@ -159,7 +159,7 @@ log(#state{fd = Fd}, ConsoleMsg, FileMsg) -> get_log_messages(Pid, Level, Format, Args) -> ConsoleMsg = unicode:characters_to_binary(io_lib:format( "[~s] [~p] " ++ Format ++ "~n", [Level, Pid | Args])), - FileMsg = ["[", httpd_util:rfc1123_date(), "] ", ConsoleMsg], + FileMsg = ["[", couch_util:rfc1123_date(), "] ", ConsoleMsg], {ConsoleMsg, iolist_to_binary(FileMsg)}. diff --git a/src/couchdb/couch_passwords.erl b/src/couchdb/couch_passwords.erl index e5de87885..57e51930e 100644 --- a/src/couchdb/couch_passwords.erl +++ b/src/couchdb/couch_passwords.erl @@ -13,6 +13,8 @@ -module(couch_passwords). -export([simple/2, pbkdf2/3, pbkdf2/4, verify/2]). +-export([hash_admin_password/1, get_unhashed_admins/0]). + -include("couch_db.hrl"). -define(MAX_DERIVED_KEY_LENGTH, (1 bsl 32 - 1)). @@ -23,6 +25,29 @@ simple(Password, Salt) -> ?l2b(couch_util:to_hex(crypto:sha(<<Password/binary, Salt/binary>>))). +%% CouchDB utility functions +-spec hash_admin_password(binary()) -> binary(). +hash_admin_password(ClearPassword) -> + Iterations = couch_config:get("couch_httpd_auth", "iterations", "10000"), + Salt = couch_uuids:random(), + DerivedKey = couch_passwords:pbkdf2(couch_util:to_binary(ClearPassword), + Salt ,list_to_integer(Iterations)), + ?l2b("-pbkdf2-" ++ ?b2l(DerivedKey) ++ "," + ++ ?b2l(Salt) ++ "," + ++ Iterations). + +-spec get_unhashed_admins() -> list(). +get_unhashed_admins() -> + lists:filter( + fun({_User, "-hashed-" ++ _}) -> + false; % already hashed + ({_User, "-pbkdf2-" ++ _}) -> + false; % already hashed + ({_User, _ClearPassword}) -> + true + end, + couch_config:get("admins")). + %% Current scheme, much stronger. -spec pbkdf2(binary(), binary(), integer()) -> string(). pbkdf2(Password, Salt, Iterations) -> diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl index cf66b86fc..694daee43 100644 --- a/src/couchdb/couch_server.erl +++ b/src/couchdb/couch_server.erl @@ -104,7 +104,7 @@ check_dbname(#server{dbname_regexp=RegExp}, DbName) -> "_users" -> ok; "_replicator" -> ok; _Else -> - {error, illegal_database_name} + {error, illegal_database_name, DbName} end; match -> ok @@ -129,20 +129,11 @@ hash_admin_passwords() -> hash_admin_passwords(true). hash_admin_passwords(Persist) -> - Iterations = couch_config:get("couch_httpd_auth", "iterations", "10000"), lists:foreach( - fun({_User, "-hashed-" ++ _}) -> - ok; % already hashed - ({_User, "-pbkdf2-" ++ _}) -> - ok; % already hashed - ({User, ClearPassword}) -> - Salt = couch_uuids:random(), - DerivedKey = couch_passwords:pbkdf2(ClearPassword, Salt, - list_to_integer(Iterations)), - couch_config:set("admins", - User, "-pbkdf2-" ++ ?b2l(DerivedKey) ++ "," ++ ?b2l(Salt) ++ - "," ++ Iterations, Persist) - end, couch_config:get("admins")). + fun({User, ClearPassword}) -> + HashedPassword = couch_passwords:hash_admin_password(ClearPassword), + couch_config:set("admins", User, ?b2l(HashedPassword), Persist) + end, couch_passwords:get_unhashed_admins()). init([]) -> % read config and register for configuration changes @@ -179,7 +170,7 @@ init([]) -> {ok, #server{root_dir=RootDir, dbname_regexp=RegExp, max_dbs_open=MaxDbsOpen, - start_time=httpd_util:rfc1123_date()}}. + start_time=couch_util:rfc1123_date()}}. terminate(_Reason, _Srv) -> lists:foreach( @@ -328,6 +319,8 @@ handle_call({open_result, DbName, {ok, OpenedDbPid}, Options}, _From, Server) -> ok end, {reply, ok, Server}; +handle_call({open_result, DbName, {error, eexist}, Options}, From, Server) -> + handle_call({open_result, DbName, file_exists, Options}, From, Server); handle_call({open_result, DbName, Error, Options}, _From, Server) -> [{DbName, {opening,Opener,Froms}}] = ets:lookup(couch_dbs_by_name, DbName), lists:foreach(fun(From) -> diff --git a/src/couchdb/couch_server_sup.erl b/src/couchdb/couch_server_sup.erl index 7baede338..be3c3a3e4 100644 --- a/src/couchdb/couch_server_sup.erl +++ b/src/couchdb/couch_server_sup.erl @@ -46,7 +46,9 @@ start_server(IniFiles) -> {ok, [PidFile]} -> case file:write_file(PidFile, os:getpid()) of ok -> ok; - Error -> io:format("Failed to write PID file ~s, error: ~p", [PidFile, Error]) + {error, Reason} -> + io:format("Failed to write PID file ~s: ~s", + [PidFile, file:format_error(Reason)]) end; _ -> ok end, @@ -121,12 +123,10 @@ start_server(IniFiles) -> end end || Uri <- Uris], case file:write_file(UriFile, Lines) of ok -> ok; - {error, eacces} -> - ?LOG_ERROR("Permission error when writing to URI file ~s", [UriFile]), - throw({file_permission_error, UriFile}); - Error2 -> - ?LOG_ERROR("Failed to write to URI file ~s: ~p~n", [UriFile, Error2]), - throw(Error2) + {error, Reason2} = Error -> + ?LOG_ERROR("Failed to write to URI file ~s: ~s", + [UriFile, file:format_error(Reason2)]), + throw(Error) end end, diff --git a/src/couchdb/couch_users_db.erl b/src/couchdb/couch_users_db.erl index 6735fb695..de76142b1 100644 --- a/src/couchdb/couch_users_db.erl +++ b/src/couchdb/couch_users_db.erl @@ -104,5 +104,7 @@ after_doc_read(Doc, #db{user_ctx = UserCtx} = Db) -> throw(not_found) end. -get_doc_name(#doc{body={Body}}) -> - couch_util:get_value(?NAME, Body). +get_doc_name(#doc{id= <<"org.couchdb.user:", Name/binary>>}) -> + Name; +get_doc_name(_) -> + undefined. diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl index d023bb69a..afe3528a6 100644 --- a/src/couchdb/couch_util.erl +++ b/src/couchdb/couch_util.erl @@ -28,6 +28,7 @@ -export([url_strip_password/1]). -export([encode_doc_id/1]). -export([with_db/2]). +-export([rfc1123_date/0, rfc1123_date/1]). -include("couch_db.hrl"). @@ -445,3 +446,44 @@ with_db(DbName, Fun) -> Else -> throw(Else) end. + +rfc1123_date() -> + {{YYYY,MM,DD},{Hour,Min,Sec}} = calendar:universal_time(), + DayNumber = calendar:day_of_the_week({YYYY,MM,DD}), + lists:flatten( + io_lib:format("~s, ~2.2.0w ~3.s ~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT", + [day(DayNumber),DD,month(MM),YYYY,Hour,Min,Sec])). + +rfc1123_date(undefined) -> + undefined; +rfc1123_date(UniversalTime) -> + {{YYYY,MM,DD},{Hour,Min,Sec}} = UniversalTime, + DayNumber = calendar:day_of_the_week({YYYY,MM,DD}), + lists:flatten( + io_lib:format("~s, ~2.2.0w ~3.s ~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT", + [day(DayNumber),DD,month(MM),YYYY,Hour,Min,Sec])). + +%% day + +day(1) -> "Mon"; +day(2) -> "Tue"; +day(3) -> "Wed"; +day(4) -> "Thu"; +day(5) -> "Fri"; +day(6) -> "Sat"; +day(7) -> "Sun". + +%% month + +month(1) -> "Jan"; +month(2) -> "Feb"; +month(3) -> "Mar"; +month(4) -> "Apr"; +month(5) -> "May"; +month(6) -> "Jun"; +month(7) -> "Jul"; +month(8) -> "Aug"; +month(9) -> "Sep"; +month(10) -> "Oct"; +month(11) -> "Nov"; +month(12) -> "Dec". diff --git a/src/couchdb/couch_uuids.erl b/src/couchdb/couch_uuids.erl index e1851e1d4..6ed75a1f4 100644 --- a/src/couchdb/couch_uuids.erl +++ b/src/couchdb/couch_uuids.erl @@ -33,12 +33,15 @@ random() -> list_to_binary(couch_util:to_hex(crypto:rand_bytes(16))). utc_random() -> + utc_suffix(couch_util:to_hex(crypto:rand_bytes(9))). + +utc_suffix(Suffix) -> Now = {_, _, Micro} = now(), Nowish = calendar:now_to_universal_time(Now), Nowsecs = calendar:datetime_to_gregorian_seconds(Nowish), Then = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), Prefix = io_lib:format("~14.16.0b", [(Nowsecs - Then) * 1000000 + Micro]), - list_to_binary(Prefix ++ couch_util:to_hex(crypto:rand_bytes(9))). + list_to_binary(Prefix ++ Suffix). init([]) -> ok = couch_config:register( @@ -53,6 +56,8 @@ handle_call(create, _From, random) -> {reply, random(), random}; handle_call(create, _From, utc_random) -> {reply, utc_random(), utc_random}; +handle_call(create, _From, {utc_id, UtcIdSuffix}) -> + {reply, utc_suffix(UtcIdSuffix), {utc_id, UtcIdSuffix}}; handle_call(create, _From, {sequential, Pref, Seq}) -> Result = ?l2b(Pref ++ io_lib:format("~6.16.0b", [Seq])), case Seq >= 16#fff000 of @@ -88,6 +93,9 @@ state() -> random; utc_random -> utc_random; + utc_id -> + UtcIdSuffix = couch_config:get("uuids", "utc_id_suffix", ""), + {utc_id, UtcIdSuffix}; sequential -> {sequential, new_prefix(), inc()}; Unknown -> diff --git a/src/etap/Makefile.am b/src/etap/Makefile.am index 732347bf1..beaf65c3b 100644 --- a/src/etap/Makefile.am +++ b/src/etap/Makefile.am @@ -13,26 +13,10 @@ etapebindir = $(localerlanglibdir)/etap/ebin etap_file_collection = \ - etap.erl \ - etap_application.erl \ - etap_can.erl \ - etap_exception.erl \ - etap_process.erl \ - etap_report.erl \ - etap_request.erl \ - etap_string.erl \ - etap_web.erl + etap.erl etapebin_make_generated_file_list = \ - etap.beam \ - etap_application.beam \ - etap_can.beam \ - etap_exception.beam \ - etap_process.beam \ - etap_report.beam \ - etap_request.beam \ - etap_string.beam \ - etap_web.beam + etap.beam etapebin_DATA = $(etapebin_make_generated_file_list) diff --git a/src/etap/etap.erl b/src/etap/etap.erl index c76b980b9..7380013f0 100644 --- a/src/etap/etap.erl +++ b/src/etap/etap.erl @@ -44,22 +44,66 @@ %% a number of etap tests and then calling eta:end_tests/0. Please refer to %% the Erlang modules in the t directory of this project for example tests. -module(etap). +-vsn("0.3.4"). + -export([ - ensure_test_server/0, start_etap_server/0, test_server/1, - diag/1, diag/2, plan/1, end_tests/0, not_ok/2, ok/2, is/3, isnt/3, - any/3, none/3, fun_is/3, is_greater/3, skip/1, skip/2, - ensure_coverage_starts/0, ensure_coverage_ends/0, coverage_report/0, - datetime/1, skip/3, bail/0, bail/1 + ensure_test_server/0, + start_etap_server/0, + test_server/1, + msg/1, msg/2, + diag/1, diag/2, + expectation_mismatch_message/3, + plan/1, + end_tests/0, + not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, + fun_is/3, expect_fun/3, expect_fun/4, + is_greater/3, + skip/1, skip/2, + datetime/1, + skip/3, + bail/0, bail/1, + test_state/0, failure_count/0 +]). + +-export([ + contains_ok/3, + is_before/4 +]). + +-export([ + is_pid/2, + is_alive/2, + is_mfa/3 +]). + +-export([ + loaded_ok/2, + can_ok/2, can_ok/3, + has_attrib/2, is_attrib/3, + is_behaviour/2 +]). + +-export([ + dies_ok/2, + lives_ok/2, + throws_ok/3 ]). --record(test_state, {planned = 0, count = 0, pass = 0, fail = 0, skip = 0, skip_reason = ""}). --vsn("0.3.4"). + + +-record(test_state, { + planned = 0, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" +}). %% @spec plan(N) -> Result %% N = unknown | skip | {skip, string()} | integer() %% Result = ok %% @doc Create a test plan and boot strap the test server. plan(unknown) -> - ensure_coverage_starts(), ensure_test_server(), etap_server ! {self(), plan, unknown}, ok; @@ -68,7 +112,6 @@ plan(skip) -> plan({skip, Reason}) -> io:format("1..0 # skip ~s~n", [Reason]); plan(N) when is_integer(N), N > 0 -> - ensure_coverage_starts(), ensure_test_server(), etap_server ! {self(), plan, N}, ok. @@ -78,8 +121,10 @@ plan(N) when is_integer(N), N > 0 -> %% @todo This should probably be done in the test_server process. end_tests() -> timer:sleep(100), - ensure_coverage_ends(), - etap_server ! {self(), state}, + case whereis(etap_server) of + undefined -> self() ! true; + _ -> etap_server ! {self(), state} + end, State = receive X -> X end, if State#test_state.planned == -1 -> @@ -92,58 +137,52 @@ end_tests() -> _ -> etap_server ! done, ok end. -%% @private -ensure_coverage_starts() -> - case os:getenv("COVER") of - false -> ok; - _ -> - BeamDir = case os:getenv("COVER_BIN") of false -> "ebin"; X -> X end, - cover:compile_beam_directory(BeamDir) - end. - -%% @private -%% @doc Attempts to write out any collected coverage data to the cover/ -%% directory. This function should not be called externally, but it could be. -ensure_coverage_ends() -> - case os:getenv("COVER") of - false -> ok; - _ -> - filelib:ensure_dir("cover/"), - Name = lists:flatten([ - io_lib:format("~.16b", [X]) || X <- binary_to_list(erlang:md5( - term_to_binary({make_ref(), now()}) - )) - ]), - cover:export("cover/" ++ Name ++ ".coverdata") - end. - -%% @spec coverage_report() -> ok -%% @doc Use the cover module's covreage report builder to create code coverage -%% reports from recently created coverdata files. -coverage_report() -> - [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")], - lists:foreach( - fun(Mod) -> - cover:analyse_to_file(Mod, atom_to_list(Mod) ++ "_coverage.txt", []) - end, - cover:imported_modules() - ), - ok. - bail() -> bail(""). bail(Reason) -> etap_server ! {self(), diag, "Bail out! " ++ Reason}, - ensure_coverage_ends(), etap_server ! done, ok, ok. +%% @spec test_state() -> Return +%% Return = test_state_record() | {error, string()} +%% @doc Return the current test state +test_state() -> + etap_server ! {self(), state}, + receive + X when is_record(X, test_state) -> X + after + 1000 -> {error, "Timed out waiting for etap server reply.~n"} + end. + +%% @spec failure_count() -> Return +%% Return = integer() | {error, string()} +%% @doc Return the current failure count +failure_count() -> + case test_state() of + #test_state{fail=FailureCount} -> FailureCount; + X -> X + end. + +%% @spec msg(S) -> ok +%% S = string() +%% @doc Print a message in the test output. +msg(S) -> etap_server ! {self(), diag, S}, ok. + +%% @spec msg(Format, Data) -> ok +%% Format = atom() | string() | binary() +%% Data = [term()] +%% UnicodeList = [Unicode] +%% Unicode = int() +%% @doc Print a message in the test output. +%% Function arguments are passed through io_lib:format/2. +msg(Format, Data) -> msg(io_lib:format(Format, Data)). %% @spec diag(S) -> ok %% S = string() %% @doc Print a debug/status message related to the test suite. -diag(S) -> etap_server ! {self(), diag, "# " ++ S}, ok. +diag(S) -> msg("# " ++ S). %% @spec diag(Format, Data) -> ok %% Format = atom() | string() | binary() @@ -154,19 +193,56 @@ diag(S) -> etap_server ! {self(), diag, "# " ++ S}, ok. %% Function arguments are passed through io_lib:format/2. diag(Format, Data) -> diag(io_lib:format(Format, Data)). +%% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok +%% Got = any() +%% Expected = any() +%% Desc = string() +%% @doc Print an expectation mismatch message in the test output. +expectation_mismatch_message(Got, Expected, Desc) -> + msg(" ---"), + msg(" description: ~p", [Desc]), + msg(" found: ~p", [Got]), + msg(" wanted: ~p", [Expected]), + msg(" ..."), + ok. + +% @spec evaluate(Pass, Got, Expected, Desc) -> Result +%% Pass = true | false +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Evaluate a test statement, printing an expectation mismatch message +%% if the test failed. +evaluate(Pass, Got, Expected, Desc) -> + case mk_tap(Pass, Desc) of + false -> + expectation_mismatch_message(Got, Expected, Desc), + false; + true -> + true + end. + %% @spec ok(Expr, Desc) -> Result %% Expr = true | false %% Desc = string() %% Result = true | false %% @doc Assert that a statement is true. -ok(Expr, Desc) -> mk_tap(Expr == true, Desc). +ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). %% @spec not_ok(Expr, Desc) -> Result %% Expr = true | false %% Desc = string() %% Result = true | false %% @doc Assert that a statement is false. -not_ok(Expr, Desc) -> mk_tap(Expr == false, Desc). +not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). + +%% @spec is_ok(Expr, Desc) -> Result +%% Expr = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are the same. +is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). %% @spec is(Got, Expected, Desc) -> Result %% Got = any() @@ -174,17 +250,7 @@ not_ok(Expr, Desc) -> mk_tap(Expr == false, Desc). %% Desc = string() %% Result = true | false %% @doc Assert that two values are the same. -is(Got, Expected, Desc) -> - case mk_tap(Got == Expected, Desc) of - false -> - etap_server ! {self(), diag, " ---"}, - etap_server ! {self(), diag, io_lib:format(" description: ~p", [Desc])}, - etap_server ! {self(), diag, io_lib:format(" found: ~p", [Got])}, - etap_server ! {self(), diag, io_lib:format(" wanted: ~p", [Expected])}, - etap_server ! {self(), diag, " ..."}, - false; - true -> true - end. +is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). %% @spec isnt(Got, Expected, Desc) -> Result %% Got = any() @@ -192,7 +258,7 @@ is(Got, Expected, Desc) -> %% Desc = string() %% Result = true | false %% @doc Assert that two values are not the same. -isnt(Got, Expected, Desc) -> mk_tap(Got /= Expected, Desc). +isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). %% @spec is_greater(ValueA, ValueB, Desc) -> Result %% ValueA = number() @@ -209,6 +275,8 @@ is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> %% Desc = string() %% Result = true | false %% @doc Assert that an item is in a list. +any(Got, Items, Desc) when is_function(Got) -> + is(lists:any(Got, Items), true, Desc); any(Got, Items, Desc) -> is(lists:member(Got, Items), true, Desc). @@ -218,6 +286,8 @@ any(Got, Items, Desc) -> %% Desc = string() %% Result = true | false %% @doc Assert that an item is not in a list. +none(Got, Items, Desc) when is_function(Got) -> + is(lists:any(Got, Items), false, Desc); none(Got, Items, Desc) -> is(lists:member(Got, Items), false, Desc). @@ -230,6 +300,27 @@ none(Got, Items, Desc) -> fun_is(Fun, Expected, Desc) when is_function(Fun) -> is(Fun(Expected), true, Desc). +%% @spec expect_fun(ExpectFun, Got, Desc) -> Result +%% ExpectFun = function() +%% Got = any() +%% Desc = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match, using actual +%% value as the argument to the function. +expect_fun(ExpectFun, Got, Desc) -> + evaluate(ExpectFun(Got), Got, ExpectFun, Desc). + +%% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result +%% ExpectFun = function() +%% Got = any() +%% Desc = string() +%% ExpectStr = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match, using actual +%% value as the argument to the function. +expect_fun(ExpectFun, Got, Desc, ExpectStr) -> + evaluate(ExpectFun(Got), Got, ExpectStr, Desc). + %% @equiv skip(TestFun, "") skip(TestFun) when is_function(TestFun) -> skip(TestFun, ""). @@ -276,8 +367,113 @@ begin_skip(Reason) -> end_skip() -> etap_server ! {self(), end_skip}. -% --- -% Internal / Private functions +%% @spec contains_ok(string(), string(), string()) -> true | false +%% @doc Assert that a string is contained in another string. +contains_ok(Source, String, Desc) -> + etap:isnt( + string:str(Source, String), + 0, + Desc + ). + +%% @spec is_before(string(), string(), string(), string()) -> true | false +%% @doc Assert that a string comes before another string within a larger body. +is_before(Source, StringA, StringB, Desc) -> + etap:is_greater( + string:str(Source, StringB), + string:str(Source, StringA), + Desc + ). + +%% @doc Assert that a given variable is a pid. +is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); +is_pid(_, Desc) -> etap:ok(false, Desc). + +%% @doc Assert that a given process/pid is alive. +is_alive(Pid, Desc) -> + etap:ok(erlang:is_process_alive(Pid), Desc). + +%% @doc Assert that the current function of a pid is a given {M, F, A} tuple. +is_mfa(Pid, MFA, Desc) -> + etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). + +%% @spec loaded_ok(atom(), string()) -> true | false +%% @doc Assert that a module has been loaded successfully. +loaded_ok(M, Desc) when is_atom(M) -> + etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). + +%% @spec can_ok(atom(), atom()) -> true | false +%% @doc Assert that a module exports a given function. +can_ok(M, F) when is_atom(M), is_atom(F) -> + Matches = [X || {X, _} <- M:module_info(exports), X == F], + etap:ok(Matches > 0, lists:concat([M, " can ", F])). + +%% @spec can_ok(atom(), atom(), integer()) -> true | false +%% @doc Assert that a module exports a given function with a given arity. +can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> + Matches = [X || X <- M:module_info(exports), X == {F, A}], + etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). + +%% @spec has_attrib(M, A) -> true | false +%% M = atom() +%% A = atom() +%% @doc Asserts that a module has a given attribute. +has_attrib(M, A) when is_atom(M), is_atom(A) -> + etap:isnt( + proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), + 'asdlkjasdlkads', + lists:concat([M, " has attribute ", A]) + ). + +%% @spec has_attrib(M, A. V) -> true | false +%% M = atom() +%% A = atom() +%% V = any() +%% @doc Asserts that a module has a given attribute with a given value. +is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> + etap:is( + proplists:get_value(A, M:module_info(attributes)), + [V], + lists:concat([M, "'s ", A, " is ", V]) + ). + +%% @spec is_behavior(M, B) -> true | false +%% M = atom() +%% B = atom() +%% @doc Asserts that a given module has a specific behavior. +is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> + is_attrib(M, behaviour, B). + +%% @doc Assert that an exception is raised when running a given function. +dies_ok(F, Desc) -> + case (catch F()) of + {'EXIT', _} -> etap:ok(true, Desc); + _ -> etap:ok(false, Desc) + end. + +%% @doc Assert that an exception is not raised when running a given function. +lives_ok(F, Desc) -> + etap:is(try_this(F), success, Desc). + +%% @doc Assert that the exception thrown by a function matches the given exception. +throws_ok(F, Exception, Desc) -> + try F() of + _ -> etap:ok(nok, Desc) + catch + _:E -> + etap:is(E, Exception, Desc) + end. + +%% @private +%% @doc Run a function and catch any exceptions. +try_this(F) when is_function(F, 0) -> + try F() of + _ -> success + catch + throw:E -> {throw, E}; + error:E -> {error, E}; + exit:E -> {exit, E} + end. %% @private %% @doc Start the etap_server process if it is not running already. diff --git a/src/etap/etap_application.erl b/src/etap/etap_application.erl deleted file mode 100644 index 98b527513..000000000 --- a/src/etap/etap_application.erl +++ /dev/null @@ -1,72 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] -%% @copyright 2008 Nick Gerakines -%% @reference http://testanything.org/wiki/index.php/Main_Page -%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol -%% @todo Explain in documentation why we use a process to handle test input. -%% @todo Add test to verify the number of members in a pg2 group. -%% @doc Provide test functionality to the application and related behaviors. --module(etap_application). --export([ - start_ok/2, ensure_loaded/3, load_ok/2, - pg2_group_exists/2, pg2_group_doesntexist/2 -]). - -%% @spec load_ok(string(), string()) -> true | false -%% @doc Assert that an application can be loaded successfully. -load_ok(AppName, Desc) -> - etap:ok(application:load(AppName) == ok, Desc). - -%% @spec start_ok(string(), string()) -> true | false -%% @doc Assert that an application can be started successfully. -start_ok(AppName, Desc) -> - etap:ok(application:start(AppName) == ok, Desc). - -%% @spec ensure_loaded(string(), string(), string()) -> true | false -%% @doc Assert that an application has been loaded successfully. -ensure_loaded(AppName, AppVsn, Desc) -> - etap:any( - fun(Match) -> case Match of {AppName, _, AppVsn} -> true; _ -> false end end, - application:loaded_applications(), - Desc - ). - -%% @spec pg2_group_exists(string(), string()) -> true | false -%% @doc Assert that a pg2 group exists. -pg2_group_exists(GroupName, Desc) -> - etap:any( - fun(Match) -> Match == GroupName end, - pg2:which_groups(), - Desc - ). - -%% @spec pg2_group_doesntexist(string(), string()) -> true | false -%% @doc Assert that a pg2 group does not exists. -pg2_group_doesntexist(GroupName, Desc) -> - etap:none( - fun(Match) -> Match == GroupName end, - pg2:which_groups(), - Desc - ). diff --git a/src/etap/etap_can.erl b/src/etap/etap_can.erl deleted file mode 100644 index 552b7174b..000000000 --- a/src/etap/etap_can.erl +++ /dev/null @@ -1,79 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @reference http://testanything.org/wiki/index.php/Main_Page -%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol -%% @doc Provide test functionality modules --module(etap_can). - --export([ - loaded_ok/2, can_ok/2, can_ok/3, - has_attrib/2, is_attrib/3, is_behaviour/2 -]). - -%% @spec loaded_ok(atom(), string()) -> true | false -%% @doc Assert that a module has been loaded successfully. -loaded_ok(M, Desc) when is_atom(M) -> - etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). - -%% @spec can_ok(atom(), atom()) -> true | false -%% @doc Assert that a module exports a given function. -can_ok(M, F) when is_atom(M), is_atom(F) -> - Matches = [X || {X, _} <- M:module_info(exports), X == F], - etap:ok(Matches > 0, lists:concat([M, " can ", F])). - -%% @spec can_ok(atom(), atom(), integer()) -> true | false -%% @doc Assert that a module exports a given function with a given arity. -can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> - Matches = [X || X <- M:module_info(exports), X == {F, A}], - etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). - -%% @spec has_attrib(M, A) -> true | false -%% M = atom() -%% A = atom() -%% @doc Asserts that a module has a given attribute. -has_attrib(M, A) when is_atom(M), is_atom(A) -> - etap:isnt( - proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), - 'asdlkjasdlkads', - lists:concat([M, " has attribute ", A]) - ). - -%% @spec has_attrib(M, A. V) -> true | false -%% M = atom() -%% A = atom() -%% V = any() -%% @doc Asserts that a module has a given attribute with a given value. -is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> - etap:is( - proplists:get_value(A, M:module_info(attributes)), - [V], - lists:concat([M, "'s ", A, " is ", V]) - ). - -%% @spec is_behavior(M, B) -> true | false -%% M = atom() -%% B = atom() -%% @doc Asserts that a given module has a specific behavior. -is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> - is_attrib(M, behaviour, B). diff --git a/src/etap/etap_exception.erl b/src/etap/etap_exception.erl deleted file mode 100644 index ba6607270..000000000 --- a/src/etap/etap_exception.erl +++ /dev/null @@ -1,66 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @reference http://testanything.org/wiki/index.php/Main_Page -%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol -%% @doc Adds exception based testing to the etap suite. --module(etap_exception). - --export([dies_ok/2, lives_ok/2, throws_ok/3]). - -% --- -% External / Public functions - -%% @doc Assert that an exception is raised when running a given function. -dies_ok(F, Desc) -> - case (catch F()) of - {'EXIT', _} -> etap:ok(true, Desc); - _ -> etap:ok(false, Desc) - end. - -%% @doc Assert that an exception is not raised when running a given function. -lives_ok(F, Desc) -> - etap:is(try_this(F), success, Desc). - -%% @doc Assert that the exception thrown by a function matches the given exception. -throws_ok(F, Exception, Desc) -> - try F() of - _ -> etap:ok(nok, Desc) - catch - _:E -> - etap:is(E, Exception, Desc) - end. - -% --- -% Internal / Private functions - -%% @private -%% @doc Run a function and catch any exceptions. -try_this(F) when is_function(F, 0) -> - try F() of - _ -> success - catch - throw:E -> {throw, E}; - error:E -> {error, E}; - exit:E -> {exit, E} - end. diff --git a/src/etap/etap_process.erl b/src/etap/etap_process.erl deleted file mode 100644 index 69f5ba001..000000000 --- a/src/etap/etap_process.erl +++ /dev/null @@ -1,42 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @doc Adds process/pid testing to the etap suite. --module(etap_process). - --export([is_pid/2, is_alive/2, is_mfa/3]). - -% --- -% External / Public functions - -%% @doc Assert that a given variable is a pid. -is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); -is_pid(_, Desc) -> etap:ok(false, Desc). - -%% @doc Assert that a given process/pid is alive. -is_alive(Pid, Desc) -> - etap:ok(erlang:is_process_alive(Pid), Desc). - -%% @doc Assert that the current function of a pid is a given {M, F, A} tuple. -is_mfa(Pid, MFA, Desc) -> - etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). diff --git a/src/etap/etap_report.erl b/src/etap/etap_report.erl deleted file mode 100644 index 6d692fb61..000000000 --- a/src/etap/etap_report.erl +++ /dev/null @@ -1,343 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @doc A module for creating nice looking code coverage reports. --module(etap_report). --export([create/0]). - -%% @spec create() -> ok -%% @doc Create html code coverage reports for each module that code coverage -%% data exists for. -create() -> - [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")], - Modules = lists:foldl( - fun(Module, Acc) -> - [{Module, file_report(Module)} | Acc] - end, - [], - cover:imported_modules() - ), - index(Modules). - -%% @private -index(Modules) -> - {ok, IndexFD} = file:open("cover/index.html", [write]), - io:format(IndexFD, "<html><head><style> - table.percent_graph { height: 12px; border:1px solid #E2E6EF; empty-cells: show; } - table.percent_graph td.covered { height: 10px; background: #00f000; } - table.percent_graph td.uncovered { height: 10px; background: #e00000; } - .odd { background-color: #ddd; } - .even { background-color: #fff; } - </style></head>", []), - io:format(IndexFD, "<body>", []), - lists:foldl( - fun({Module, {Good, Bad, Source}}, LastRow) -> - case {Good + Bad, Source} of - {0, _} -> LastRow; - {_, none} -> LastRow; - _ -> - CovPer = round((Good / (Good + Bad)) * 100), - UnCovPer = round((Bad / (Good + Bad)) * 100), - RowClass = case LastRow of 1 -> "odd"; _ -> "even" end, - io:format(IndexFD, "<div class=\"~s\">", [RowClass]), - io:format(IndexFD, "<a href=\"~s\">~s</a>", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]), - io:format(IndexFD, " - <table cellspacing='0' cellpadding='0' align='right'> - <tr> - <td><tt>~p%</tt> </td><td> - <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'> - <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr> - </table> - </td> - </tr> - </table> - ", [CovPer, CovPer, UnCovPer]), - io:format(IndexFD, "</div>", []), - case LastRow of - 1 -> 0; - 0 -> 1 - end - end - end, - 0, - lists:sort(Modules) - ), - {TotalGood, TotalBad} = lists:foldl( - fun({_, {Good, Bad, Source}}, {TGood, TBad}) -> - case Source of none -> {TGood, TBad}; _ -> {TGood + Good, TBad + Bad} end - end, - {0, 0}, - Modules - ), - io:format(IndexFD, "<p>Generated on ~s.</p>~n", [etap:datetime({date(), time()})]), - case TotalGood + TotalBad of - 0 -> ok; - _ -> - TotalCovPer = round((TotalGood / (TotalGood + TotalBad)) * 100), - TotalUnCovPer = round((TotalBad / (TotalGood + TotalBad)) * 100), - io:format(IndexFD, "<div>", []), - io:format(IndexFD, "Total - <table cellspacing='0' cellpadding='0' align='right'> - <tr> - <td><tt>~p%</tt> </td><td> - <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'> - <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr> - </table> - </td> - </tr> - </table> - ", [TotalCovPer, TotalCovPer, TotalUnCovPer]), - io:format(IndexFD, "</div>", []) - end, - io:format(IndexFD, "</body></html>", []), - file:close(IndexFD), - ok. - -%% @private -file_report(Module) -> - {ok, Data} = cover:analyse(Module, calls, line), - Source = find_source(Module), - {Good, Bad} = collect_coverage(Data, {0, 0}), - case {Source, Good + Bad} of - {none, _} -> ok; - {_, 0} -> ok; - _ -> - {ok, SourceFD} = file:open(Source, [read]), - {ok, WriteFD} = file:open("cover/" ++ atom_to_list(Module) ++ "_report.html", [write]), - io:format(WriteFD, "~s", [header(Module, Good, Bad)]), - output_lines(Data, WriteFD, SourceFD, 1), - io:format(WriteFD, "~s", [footer()]), - file:close(WriteFD), - file:close(SourceFD), - ok - end, - {Good, Bad, Source}. - -%% @private -collect_coverage([], Acc) -> Acc; -collect_coverage([{{_, _}, 0} | Data], {Good, Bad}) -> - collect_coverage(Data, {Good, Bad + 1}); -collect_coverage([_ | Data], {Good, Bad}) -> - collect_coverage(Data, {Good + 1, Bad}). - -%% @private -output_lines(Data, WriteFD, SourceFD, LineNumber) -> - {Match, NextData} = datas_match(Data, LineNumber), - case io:get_line(SourceFD, '') of - eof -> ok; - Line = "%% @todo" ++ _ -> - io:format(WriteFD, "~s", [out_line(LineNumber, highlight, Line)]), - output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); - Line = "% " ++ _ -> - io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]), - output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); - Line -> - case Match of - {true, CC} -> - io:format(WriteFD, "~s", [out_line(LineNumber, CC, Line)]), - output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); - false -> - io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]), - output_lines(NextData, WriteFD, SourceFD, LineNumber + 1) - end - end. - -%% @private -out_line(Number, none, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("<span class=\"marked\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]); -out_line(Number, highlight, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("<span class=\"highlight\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]); -out_line(Number, 0, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("<span class=\"uncovered\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]); -out_line(Number, _, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("<span class=\"covered\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]). - -%% @private -datas_match([], _) -> {false, []}; -datas_match([{{_, Line}, CC} | Datas], LineNumber) when Line == LineNumber -> {{true, CC}, Datas}; -datas_match(Data, _) -> {false, Data}. - -%% @private -find_source(Module) when is_atom(Module) -> - Root = filename:rootname(Module), - Dir = filename:dirname(Root), - XDir = case os:getenv("SRC") of false -> "src"; X -> X end, - find_source([ - filename:join([Dir, Root ++ ".erl"]), - filename:join([Dir, "..", "src", Root ++ ".erl"]), - filename:join([Dir, "src", Root ++ ".erl"]), - filename:join([Dir, "elibs", Root ++ ".erl"]), - filename:join([Dir, "..", "elibs", Root ++ ".erl"]), - filename:join([Dir, XDir, Root ++ ".erl"]) - ]); -find_source([]) -> none; -find_source([Test | Tests]) -> - case filelib:is_file(Test) of - true -> Test; - false -> find_source(Tests) - end. - -%% @private -header(Module, Good, Bad) -> - io:format("Good ~p~n", [Good]), - io:format("Bad ~p~n", [Bad]), - CovPer = round((Good / (Good + Bad)) * 100), - UnCovPer = round((Bad / (Good + Bad)) * 100), - io:format("CovPer ~p~n", [CovPer]), - io_lib:format("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"> - <html lang='en' xml:lang='en' xmlns='http://www.w3.org/1999/xhtml'> - <head> - <title>~s - C0 code coverage information</title> - <style type='text/css'>body { background-color: rgb(240, 240, 245); }</style> - <style type='text/css'>span.marked0 { - background-color: rgb(185, 210, 200); - display: block; - } - span.marked { display: block; background-color: #ffffff; } - span.highlight { display: block; background-color: #fff9d7; } - span.covered { display: block; background-color: #f7f7f7 ; } - span.uncovered { display: block; background-color: #ffebe8 ; } - span.overview { - border-bottom: 1px solid #E2E6EF; - } - div.overview { - border-bottom: 1px solid #E2E6EF; - } - body { - font-family: verdana, arial, helvetica; - } - div.footer { - font-size: 68%; - margin-top: 1.5em; - } - h1, h2, h3, h4, h5, h6 { - margin-bottom: 0.5em; - } - h5 { - margin-top: 0.5em; - } - .hidden { - display: none; - } - div.separator { - height: 10px; - } - table.percent_graph { - height: 12px; - border: 1px solid #E2E6EF; - empty-cells: show; - } - table.percent_graph td.covered { - height: 10px; - background: #00f000; - } - table.percent_graph td.uncovered { - height: 10px; - background: #e00000; - } - table.percent_graph td.NA { - height: 10px; - background: #eaeaea; - } - table.report { - border-collapse: collapse; - width: 100%; - } - table.report td.heading { - background: #dcecff; - border: 1px solid #E2E6EF; - font-weight: bold; - text-align: center; - } - table.report td.heading:hover { - background: #c0ffc0; - } - table.report td.text { - border: 1px solid #E2E6EF; - } - table.report td.value { - text-align: right; - border: 1px solid #E2E6EF; - } - table.report tr.light { - background-color: rgb(240, 240, 245); - } - table.report tr.dark { - background-color: rgb(230, 230, 235); - } - </style> - </head> - <body> - <h3>C0 code coverage information</h3> - <p>Generated on ~s with <a href='http://github.com/ngerakines/etap'>etap 0.3.4</a>. - </p> - <table class='report'> - <thead> - <tr> - <td class='heading'>Name</td> - <td class='heading'>Total lines</td> - <td class='heading'>Lines of code</td> - <td class='heading'>Total coverage</td> - <td class='heading'>Code coverage</td> - </tr> - </thead> - <tbody> - <tr class='light'> - - <td> - <a href='~s'>~s</a> - </td> - <td class='value'> - <tt>??</tt> - </td> - <td class='value'> - <tt>??</tt> - </td> - <td class='value'> - <tt>??</tt> - </td> - <td> - <table cellspacing='0' cellpadding='0' align='right'> - <tr> - <td><tt>~p%</tt> </td><td> - <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'> - <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr> - </table> - </td> - </tr> - </table> - </td> - </tr> - </tbody> - </table><pre>", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]). - -%% @private -footer() -> - "</pre><hr /><p>Generated using <a href='http://github.com/ngerakines/etap'>etap 0.3.4</a>.</p> - </body> - </html> - ". diff --git a/src/etap/etap_request.erl b/src/etap/etap_request.erl deleted file mode 100644 index 9fd23acab..000000000 --- a/src/etap/etap_request.erl +++ /dev/null @@ -1,89 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @doc Provides test functionality against a specific web request. Many of -%% the exported methods can be used to build your own more complex tests. --module(etap_request, [Method, Url, InHeaders, InBody, Status, OutHeaders, OutBody]). - --export([status_is/2]). - --export([ - method/0, url/0, status/0, status_code/0, status_line/0, rheaders/0, - has_rheader/1, rheader/1, rbody/0, header_is/3, body_is/2, - body_has_string/2 -]). - -% --- -% Tests - -%% @doc Assert that response status code is the given status code. -status_is(Code, Desc) -> - etap:is(status_code(), Code, Desc). - -header_is(Name, Value, Desc) -> - etap:is(rheader(Name), Value, Desc). - -body_is(Value, Desc) -> - etap:is(rbody(), Value, Desc). - -body_has_string(String, Desc) when is_list(OutBody), is_list(String) -> - etap_string:contains_ok(OutBody, String, Desc). - -% --- -% Accessor functions - -%% @doc Access a request's method. -method() -> Method. - -%% @doc Access a request's URL. -url() -> Url. - -%% @doc Access a request's status. -status() -> Status. - -%% @doc Access a request's status code. -status_code() -> - {_, Code, _} = Status, - Code. - -%% @doc Access a request's status line. -status_line() -> - {_, _, Line} = Status, - Line. - -%% @doc Access a request's headers. -rheaders() -> OutHeaders. - -%% @doc Dertermine if a specific request header exists. -has_rheader(Key) -> - lists:keymember(Key, 1, OutHeaders). - -%% @doc Return a specific request header. -rheader(Key) -> - case lists:keysearch(Key, 1, OutHeaders) of - false -> undefined; - {value, {Key, Value}} -> Value - end. - -%% @doc Access the request's body. -rbody() -> OutBody. diff --git a/src/etap/etap_string.erl b/src/etap/etap_string.erl deleted file mode 100644 index 67aa3d541..000000000 --- a/src/etap/etap_string.erl +++ /dev/null @@ -1,47 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] -%% @copyright 2008 Nick Gerakines -%% @doc Provide testing functionality for strings. --module(etap_string). - --export([contains_ok/3, is_before/4]). - -%% @spec contains_ok(string(), string(), string()) -> true | false -%% @doc Assert that a string is contained in another string. -contains_ok(Source, String, Desc) -> - etap:isnt( - string:str(Source, String), - 0, - Desc - ). - -%% @spec is_before(string(), string(), string(), string()) -> true | false -%% @doc Assert that a string comes before another string within a larger body. -is_before(Source, StringA, StringB, Desc) -> - etap:is_greater( - string:str(Source, StringB), - string:str(Source, StringA), - Desc - ). diff --git a/src/etap/etap_web.erl b/src/etap/etap_web.erl deleted file mode 100644 index fb7aee162..000000000 --- a/src/etap/etap_web.erl +++ /dev/null @@ -1,65 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> -%% -%% Permission is hereby granted, free of charge, to any person -%% obtaining a copy of this software and associated documentation -%% files (the "Software"), to deal in the Software without -%% restriction, including without limitation the rights to use, -%% copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following -%% conditions: -%% -%% The above copyright notice and this permission notice shall be -%% included in all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -%% OTHER DEALINGS IN THE SOFTWARE. -%% -%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] -%% @copyright 2008 Nick Gerakines -%% @todo Support cookies. -%% @doc Provide testing functionality for web requests. --module(etap_web). - --export([simple_200/2, simple_404/2, build_request/4]). - -%% @doc Fetch a url and verify that it returned a 200 status. -simple_200(Url, Desc) -> - Request = build_request(get, Url, [], []), - Request:status_is(200, Desc). - -%% @doc Fetch a url and verify that it returned a 404 status. -simple_404(Url, Desc) -> - Request = build_request(get, Url, [], []), - Request:status_is(404, Desc). - -%% @doc Create and return a request structure. -build_request(Method, Url, Headers, Body) - when Method==options;Method==get;Method==head;Method==delete;Method==trace -> - try http:request(Method, {Url, Headers}, [{autoredirect, false}], []) of - {ok, {OutStatus, OutHeaders, OutBody}} -> - etap_request:new(Method, Url, Headers, Body, OutStatus, OutHeaders, OutBody); - _ -> error - catch - _:_ -> error - end; - -%% @doc Create and return a request structure. -build_request(Method, Url, Headers, Body) when Method == post; Method == put -> - ContentType = case lists:keysearch("Content-Type", 1, Headers) of - {value, {"Content-Type", X}} -> X; - _ -> [] - end, - try http:request(Method, {Url, Headers, ContentType, Body}, [{autoredirect, false}], []) of - {ok, {OutStatus, OutHeaders, OutBody}} -> - etap_request:new(Method, Url, Headers, Body, OutStatus, OutHeaders, OutBody); - _ -> error - catch - _:_ -> error - end. diff --git a/src/ibrowse/ibrowse_http_client.erl b/src/ibrowse/ibrowse_http_client.erl index eb2bf3153..00e8ed3c5 100644 --- a/src/ibrowse/ibrowse_http_client.erl +++ b/src/ibrowse/ibrowse_http_client.erl @@ -46,7 +46,7 @@ reqs=queue:new(), cur_req, status=idle, http_status_code, reply_buffer = <<>>, rep_buf_size=0, streamed_size = 0, recvd_headers=[], - status_line, raw_headers, + status_line, raw_headers, is_closing, send_timer, content_length, deleted_crlf = false, transfer_encoding, chunk_size, chunk_size_buffer = <<>>, @@ -55,11 +55,11 @@ }). -record(request, {url, method, options, from, - stream_to, caller_controls_socket = false, + stream_to, caller_controls_socket = false, caller_socket_options = [], req_id, stream_chunk_size, - save_response_to_file = false, + save_response_to_file = false, tmp_file_name, tmp_file_fd, preserve_chunked_encoding, response_format}). @@ -208,7 +208,7 @@ handle_info({stream_close, _Req_id}, State) -> do_error_reply(State, closing_on_request), {stop, normal, State}; -handle_info({tcp_closed, _Sock}, State) -> +handle_info({tcp_closed, _Sock}, State) -> do_trace("TCP connection closed by peer!~n", []), handle_sock_closed(State), {stop, normal, State}; @@ -405,7 +405,7 @@ accumulate_response(Data, #state{reply_buffer = RepBuf, State#state{reply_buffer = RepBuf_1}; _ when Caller_controls_socket == true -> do_interim_reply(StreamTo, Response_format, ReqId, RepBuf_1), - State#state{reply_buffer = <<>>, + State#state{reply_buffer = <<>>, interim_reply_sent = true, streamed_size = Streamed_size + size(RepBuf_1)}; _ when New_data_size >= Stream_chunk_size -> @@ -703,7 +703,7 @@ send_req_1(From, {stop, normal, State_1} end; -send_req_1(From, Url, Headers, Method, Body, Options, Timeout, +send_req_1(From, Url, Headers, Method, Body, Options, Timeout, #state{proxy_tunnel_setup = in_progress, tunnel_setup_queue = Q} = State) -> do_trace("Queued SSL request awaiting tunnel setup: ~n" @@ -727,7 +727,7 @@ send_req_1(From, {Caller, once} when is_pid(Caller) or is_atom(Caller) -> Async_pid_rec = {{req_id_pid, ReqId}, self()}, - true = ets:insert(ibrowse_stream, Async_pid_rec), + true = ets:insert(ibrowse_stream, Async_pid_rec), {Caller, true}; undefined -> {undefined, false}; @@ -916,7 +916,7 @@ is_chunked_encoding_specified(Options) -> case get_value(transfer_encoding, Options, false) of false -> false; - {chunked, _} -> + {chunked, _} -> true; chunked -> true @@ -1027,7 +1027,7 @@ parse_response(Data, #state{reply_buffer = Acc, reqs = Reqs, put(conn_close, ConnClose), TransferEncoding = to_lower(get_value("transfer-encoding", LCHeaders, "false")), case get_value("content-length", LCHeaders, undefined) of - _ when Method == connect, + _ when Method == connect, hd(StatCode) == $2 -> cancel_timer(State#state.send_timer), {_, Reqs_1} = queue:out(Reqs), @@ -1125,7 +1125,7 @@ parse_response(Data, #state{reply_buffer = Acc, reqs = Reqs, {error, max_headers_size_exceeded} end. -upgrade_to_ssl(#state{socket = Socket, +upgrade_to_ssl(#state{socket = Socket, connect_timeout = Conn_timeout, ssl_options = Ssl_options, tunnel_setup_queue = Q} = State) -> @@ -1165,7 +1165,7 @@ is_connection_closing(_, _) -> false. %% This clause determines the chunk size when given data from the beginning of the chunk parse_11_response(DataRecvd, - #state{transfer_encoding = chunked, + #state{transfer_encoding = chunked, chunk_size = chunk_start, chunk_size_buffer = Chunk_sz_buf } = State) -> @@ -1193,7 +1193,7 @@ parse_11_response(DataRecvd, %% This clause is to remove the CRLF between two chunks %% parse_11_response(DataRecvd, - #state{transfer_encoding = chunked, + #state{transfer_encoding = chunked, chunk_size = tbd, chunk_size_buffer = Buf } = State) -> @@ -1212,7 +1212,7 @@ parse_11_response(DataRecvd, %% not support Trailers in the Chunked Transfer encoding. Any trailer %% received is silently discarded. parse_11_response(DataRecvd, - #state{transfer_encoding = chunked, chunk_size = 0, + #state{transfer_encoding = chunked, chunk_size = 0, cur_req = CurReq, deleted_crlf = DelCrlf, chunk_size_buffer = Trailer, @@ -1301,9 +1301,9 @@ handle_response(#request{from=From, stream_to=StreamTo, req_id=ReqId, recvd_headers = RespHeaders}=State) when SaveResponseToFile /= false -> Body = RepBuf, case Fd of - undefined -> + undefined -> ok; - _ -> + _ -> ok = file:close(Fd) end, ResponseBody = case TmpFilename of @@ -1458,7 +1458,7 @@ parse_header([], _) -> invalid. scan_header(Bin) -> - case get_crlf_crlf_pos(Bin, 0) of + case get_crlf_crlf_pos(Bin) of {yes, Pos} -> {Headers, <<_:4/binary, Body/binary>>} = split_binary(Bin, Pos), {yes, Headers, Body}; @@ -1474,7 +1474,7 @@ scan_header(Bin1, Bin2) -> Bin1_already_scanned_size = size(Bin1) - 4, <<Headers_prefix:Bin1_already_scanned_size/binary, Rest/binary>> = Bin1, Bin_to_scan = <<Rest/binary, Bin2/binary>>, - case get_crlf_crlf_pos(Bin_to_scan, 0) of + case get_crlf_crlf_pos(Bin_to_scan) of {yes, Pos} -> {Headers_suffix, <<_:4/binary, Body/binary>>} = split_binary(Bin_to_scan, Pos), {yes, <<Headers_prefix/binary, Headers_suffix/binary>>, Body}; @@ -1482,9 +1482,16 @@ scan_header(Bin1, Bin2) -> {no, <<Bin1/binary, Bin2/binary>>} end. -get_crlf_crlf_pos(<<$\r, $\n, $\r, $\n, _/binary>>, Pos) -> {yes, Pos}; -get_crlf_crlf_pos(<<_, Rest/binary>>, Pos) -> get_crlf_crlf_pos(Rest, Pos + 1); -get_crlf_crlf_pos(<<>>, _) -> no. +get_crlf_crlf_pos(Data) -> + binary_bif_match(Data, <<$\r, $\n, $\r, $\n>>). + +binary_bif_match(Data, Binary) -> + case binary:match(Data, Binary) of + {Pos, _Len} -> + {yes, Pos}; + _ -> no + end. + scan_crlf(Bin) -> case get_crlf_pos(Bin) of @@ -1513,12 +1520,9 @@ scan_crlf_1(Bin1_head_size, Bin1, Bin2) -> {no, list_to_binary([Bin1, Bin2])} end. -get_crlf_pos(Bin) -> - get_crlf_pos(Bin, 0). +get_crlf_pos(Data) -> + binary_bif_match(Data, <<$\r, $\n>>). -get_crlf_pos(<<$\r, $\n, _/binary>>, Pos) -> {yes, Pos}; -get_crlf_pos(<<_, Rest/binary>>, Pos) -> get_crlf_pos(Rest, Pos + 1); -get_crlf_pos(<<>>, _) -> no. fmt_val(L) when is_list(L) -> L; fmt_val(I) when is_integer(I) -> integer_to_list(I); @@ -1595,8 +1599,8 @@ is_whitespace(_) -> false. send_async_headers(_ReqId, undefined, _, _State) -> ok; -send_async_headers(ReqId, StreamTo, Give_raw_headers, - #state{status_line = Status_line, raw_headers = Raw_headers, +send_async_headers(ReqId, StreamTo, Give_raw_headers, + #state{status_line = Status_line, raw_headers = Raw_headers, recvd_headers = Headers, http_status_code = StatCode, cur_req = #request{options = Opts} }) -> @@ -1808,7 +1812,7 @@ set_inac_timer(State, Timeout) when is_integer(Timeout) -> set_inac_timer(State, _) -> State. -get_inac_timeout(#state{cur_req = #request{options = Opts}}) -> +get_inac_timeout(#state{cur_req = #request{options = Opts}}) -> get_value(inactivity_timeout, Opts, infinity); get_inac_timeout(#state{cur_req = undefined}) -> case ibrowse:get_config_value(inactivity_timeout, undefined) of @@ -1851,5 +1855,5 @@ trace_request_body(Body) -> ok end. -to_binary(X) when is_list(X) -> list_to_binary(X); +to_binary(X) when is_list(X) -> list_to_binary(X); to_binary(X) when is_binary(X) -> X. diff --git a/src/mochiweb/mochiweb_acceptor.erl b/src/mochiweb/mochiweb_acceptor.erl index 20a9b4b94..893f99b11 100644 --- a/src/mochiweb/mochiweb_acceptor.erl +++ b/src/mochiweb/mochiweb_acceptor.erl @@ -18,13 +18,14 @@ init(Server, Listen, Loop) -> case catch mochiweb_socket:accept(Listen) of {ok, Socket} -> gen_server:cast(Server, {accepted, self(), timer:now_diff(now(), T1)}), - call_loop(Loop, Socket); + case mochiweb_socket:after_accept(Socket) of + ok -> call_loop(Loop, Socket); + {error, _} -> exit(normal) + end; {error, closed} -> exit(normal); {error, timeout} -> init(Server, Listen, Loop); - {error, esslaccept} -> - exit(normal); Other -> error_logger:error_report( [{application, mochiweb}, diff --git a/src/mochiweb/mochiweb_cookies.erl b/src/mochiweb/mochiweb_cookies.erl index c090b714f..ee91d0c1d 100644 --- a/src/mochiweb/mochiweb_cookies.erl +++ b/src/mochiweb/mochiweb_cookies.erl @@ -49,9 +49,9 @@ cookie(Key, Value, Options) -> RawAge -> When = case proplists:get_value(local_time, Options) of undefined -> - calendar:local_time(); + calendar:universal_time(); LocalTime -> - LocalTime + erlang:localtime_to_universaltime(LocalTime) end, Age = case RawAge < 0 of true -> @@ -115,12 +115,12 @@ quote(V0) -> orelse erlang:error({cookie_quoting_required, V}), V. -add_seconds(Secs, LocalTime) -> - Greg = calendar:datetime_to_gregorian_seconds(LocalTime), +add_seconds(Secs, UniversalTime) -> + Greg = calendar:datetime_to_gregorian_seconds(UniversalTime), calendar:gregorian_seconds_to_datetime(Greg + Secs). -age_to_cookie_date(Age, LocalTime) -> - httpd_util:rfc1123_date(add_seconds(Age, LocalTime)). +age_to_cookie_date(Age, UniversalTime) -> + couch_util:rfc1123_date(add_seconds(Age, UniversalTime)). %% @spec parse_cookie(string()) -> [{K::string(), V::string()}] %% @doc Parse the contents of a Cookie header field, ignoring cookie diff --git a/src/mochiweb/mochiweb_request.erl b/src/mochiweb/mochiweb_request.erl index 8225778cb..980f5ad01 100644 --- a/src/mochiweb/mochiweb_request.erl +++ b/src/mochiweb/mochiweb_request.erl @@ -600,9 +600,9 @@ maybe_redirect(RelPath, FullPath, ExtraHeaders) -> end. maybe_serve_file(File, ExtraHeaders) -> - case file:read_file_info(File) of + case read_file_info(File) of {ok, FileInfo} -> - LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime), + LastModified = couch_util:rfc1123_date(FileInfo#file_info.mtime), case get_header_value("if-modified-since") of LastModified -> respond({304, ExtraHeaders, ""}); @@ -624,9 +624,28 @@ maybe_serve_file(File, ExtraHeaders) -> not_found(ExtraHeaders) end. +read_file_info(File) -> + try + file:read_file_info(File, [{time, universal}]) + catch error:undef -> + case file:read_file_info(File) of + {ok, FileInfo} -> + {ok, FileInfo#file_info{ + atime=to_universal(FileInfo#file_info.atime), + mtime=to_universal(FileInfo#file_info.mtime), + ctime=to_universal(FileInfo#file_info.ctime) + }}; + Else -> + Else + end + end. + +to_universal(LocalTime) -> + erlang:localtime_to_universaltime(LocalTime). + server_headers() -> [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"}, - {"Date", httpd_util:rfc1123_date()}]. + {"Date", couch_util:rfc1123_date()}]. make_code(X) when is_integer(X) -> [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]]; diff --git a/src/mochiweb/mochiweb_socket.erl b/src/mochiweb/mochiweb_socket.erl index 76b018c82..ad272048c 100644 --- a/src/mochiweb/mochiweb_socket.erl +++ b/src/mochiweb/mochiweb_socket.erl @@ -4,10 +4,11 @@ -module(mochiweb_socket). --export([listen/4, accept/1, recv/3, send/2, close/1, port/1, peername/1, +-export([listen/4, accept/1, after_accept/1, recv/3, send/2, close/1, port/1, peername/1, setopts/2, type/1]). -define(ACCEPT_TIMEOUT, 2000). +-define(SSL_ACCEPT_TIMEOUT, 30000). listen(Ssl, Port, Opts, SslOpts) -> case Ssl of @@ -25,14 +26,9 @@ listen(Ssl, Port, Opts, SslOpts) -> accept({ssl, ListenSocket}) -> % There's a bug in ssl:transport_accept/2 at the moment, which is the % reason for the try...catch block. Should be fixed in OTP R14. - try ssl:transport_accept(ListenSocket) of + try ssl:transport_accept(ListenSocket, ?ACCEPT_TIMEOUT) of {ok, Socket} -> - case ssl:ssl_accept(Socket) of - ok -> - {ok, {ssl, Socket}}; - {error, _} = Err -> - Err - end; + {ok, {ssl, Socket}}; {error, _} = Err -> Err catch @@ -42,6 +38,9 @@ accept({ssl, ListenSocket}) -> accept(ListenSocket) -> gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT). +after_accept({ssl, Socket}) -> ssl:ssl_accept(Socket, ?SSL_ACCEPT_TIMEOUT); +after_accept(_Socket) -> ok. + recv({ssl, Socket}, Length, Timeout) -> ssl:recv(Socket, Length, Timeout); recv(Socket, Length, Timeout) -> diff --git a/src/snappy/Makefile.am b/src/snappy/Makefile.am index bca103bef..23dbf1472 100644 --- a/src/snappy/Makefile.am +++ b/src/snappy/Makefile.am @@ -56,7 +56,7 @@ snappy_nif_la_LDFLAGS = -module -avoid-version if WINDOWS snappy_nif_la_LDFLAGS += -no-undefined -snappy_nif_la_CXXFLAGS += -EHsc +snappy_nif_la_CXXFLAGS += -EHsc -Ox SNAPPY_SO_NAME = snappy_nif.dll else SNAPPY_SO_NAME = snappy_nif.so diff --git a/test/etap/001-load.t b/test/etap/001-load.t index 8bcfca403..5ce0d9391 100755 --- a/test/etap/001-load.t +++ b/test/etap/001-load.t @@ -60,7 +60,7 @@ main(_) -> etap:plan(length(Modules)), lists:foreach( fun(Module) -> - etap_can:loaded_ok( + etap:loaded_ok( Module, lists:concat(["Loaded: ", Module]) ) diff --git a/test/etap/020-btree-basics.t b/test/etap/020-btree-basics.t index 6886ee1b4..b0fb2d28c 100755 --- a/test/etap/020-btree-basics.t +++ b/test/etap/020-btree-basics.t @@ -235,10 +235,10 @@ test_final_reductions(Btree, KeyValues) -> KVLen = FoldLRed + FoldRRed, ok. -test_traversal_callbacks(Btree, KeyValues) -> +test_traversal_callbacks(Btree, _KeyValues) -> FoldFun = fun - (visit, GroupedKey, Unreduced, Acc) -> + (visit, _GroupedKey, _Unreduced, Acc) -> {ok, Acc andalso false}; (traverse, _LK, _Red, Acc) -> {skip, Acc andalso true} diff --git a/test/etap/040-util.t b/test/etap/040-util.t index 8f80db875..d57a32ed2 100755 --- a/test/etap/040-util.t +++ b/test/etap/040-util.t @@ -53,8 +53,8 @@ test() -> etap:ok(not couch_util:should_flush(), "Not using enough memory to flush."), AcquireMem = fun() -> - IntsToAGazillion = lists:seq(1, 200000), - LotsOfData = lists:map( + _IntsToAGazillion = lists:seq(1, 200000), + _LotsOfData = lists:map( fun(Int) -> {Int, <<"foobar">>} end, lists:seq(1, 500000)), etap:ok(couch_util:should_flush(), diff --git a/test/etap/041-uuid-gen-id.ini b/test/etap/041-uuid-gen-id.ini new file mode 100644 index 000000000..6886efdb7 --- /dev/null +++ b/test/etap/041-uuid-gen-id.ini @@ -0,0 +1,20 @@ +; Licensed to the Apache Software Foundation (ASF) under one +; or more contributor license agreements. See the NOTICE file +; distributed with this work for additional information +; regarding copyright ownership. The ASF licenses this file +; to you 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. + +[uuids] +algorithm = utc_id +utc_id_suffix = bozo diff --git a/test/etap/041-uuid-gen.t b/test/etap/041-uuid-gen.t index 1e6aa9eee..72349698e 100755 --- a/test/etap/041-uuid-gen.t +++ b/test/etap/041-uuid-gen.t @@ -22,6 +22,9 @@ seq_alg_config() -> utc_alg_config() -> test_util:source_file("test/etap/041-uuid-gen-utc.ini"). +utc_id_alg_config() -> + test_util:source_file("test/etap/041-uuid-gen-id.ini"). + % Run tests and wait for the gen_servers to shutdown run_test(IniFiles, Test) -> {ok, Pid} = couch_config:start_link(IniFiles), @@ -40,7 +43,7 @@ run_test(IniFiles, Test) -> main(_) -> test_util:init_code_path(), application:start(crypto), - etap:plan(6), + etap:plan(9), case (catch test()) of ok -> @@ -63,6 +66,7 @@ test() -> run_test([default_config()], TestUnique), run_test([default_config(), seq_alg_config()], TestUnique), run_test([default_config(), utc_alg_config()], TestUnique), + run_test([default_config(), utc_id_alg_config()], TestUnique), TestMonotonic = fun () -> etap:is( @@ -73,6 +77,7 @@ test() -> end, run_test([default_config(), seq_alg_config()], TestMonotonic), run_test([default_config(), utc_alg_config()], TestMonotonic), + run_test([default_config(), utc_id_alg_config()], TestMonotonic), % Pretty sure that the average of a uniform distribution is the % midpoint of the range. Thus, to exceed a threshold, we need @@ -94,7 +99,18 @@ test() -> "should roll over every so often." ) end, - run_test([default_config(), seq_alg_config()], TestRollOver). + run_test([default_config(), seq_alg_config()], TestRollOver), + + TestSuffix = fun() -> + UUID = binary_to_list(couch_uuids:new()), + Suffix = get_suffix(UUID), + etap:is( + test_same_suffix(100, Suffix), + true, + "utc_id ids should have the same suffix." + ) + end, + run_test([default_config(), utc_id_alg_config()], TestSuffix). test_unique(0, _) -> true; @@ -116,3 +132,16 @@ gen_until_pref_change(Prefix, N) -> Prefix -> gen_until_pref_change(Prefix, N+1); _ -> N end. + +get_suffix(UUID) when is_binary(UUID)-> + get_suffix(binary_to_list(UUID)); +get_suffix(UUID) -> + element(2, lists:split(14, UUID)). + +test_same_suffix(0, _) -> + true; +test_same_suffix(N, Suffix) -> + case get_suffix(couch_uuids:new()) of + Suffix -> test_same_suffix(N-1, Suffix); + _ -> false + end. diff --git a/test/etap/050-stream.t b/test/etap/050-stream.t index de0dfadb4..676f1e42e 100755 --- a/test/etap/050-stream.t +++ b/test/etap/050-stream.t @@ -68,7 +68,7 @@ test() -> % Stream more the 4K chunk size. {ok, ExpPtr2} = couch_file:bytes(Fd), {ok, Stream3} = couch_stream:open(Fd, [{buffer_size, 4096}]), - Acc2 = lists:foldl(fun(_, Acc) -> + lists:foldl(fun(_, Acc) -> Data = <<"a1b2c">>, couch_stream:write(Stream3, Data), [Data | Acc] diff --git a/test/etap/072-cleanup.t b/test/etap/072-cleanup.t index bd420f4d4..6721090ec 100755 --- a/test/etap/072-cleanup.t +++ b/test/etap/072-cleanup.t @@ -55,7 +55,7 @@ test() -> BoozRev = create_design_doc(<<"_design/booz">>, <<"baz">>), query_view("booz", "baz"), - {ok, Db} = couch_db:open(?TEST_DB, [{user_ctx, ?ADMIN_USER}]), + {ok, _Db} = couch_db:open(?TEST_DB, [{user_ctx, ?ADMIN_USER}]), view_cleanup(), etap:is(count_index_files(), 2, "Two index files before any deletions."), diff --git a/test/etap/073-changes.t b/test/etap/073-changes.t index 97f686060..845cd79fa 100755 --- a/test/etap/073-changes.t +++ b/test/etap/073-changes.t @@ -318,7 +318,7 @@ test_design_docs_only() -> test_heartbeat() -> {ok, Db} = create_db(test_db_name()), - {ok, Rev3} = save_doc(Db, {[ + {ok, _} = save_doc(Db, {[ {<<"_id">>, <<"_design/foo">>}, {<<"language">>, <<"javascript">>}, {<<"filters">>, {[ diff --git a/test/etap/083-config-no-files.t b/test/etap/083-config-no-files.t index 675feb59d..0ce38e667 100755 --- a/test/etap/083-config-no-files.t +++ b/test/etap/083-config-no-files.t @@ -13,8 +13,6 @@ % License for the specific language governing permissions and limitations under % the License. -default_config() -> - test_util:build_file("etc/couchdb/default_dev.ini"). main(_) -> test_util:init_code_path(), diff --git a/test/etap/090-task-status.t b/test/etap/090-task-status.t index 34855834a..23115bdaa 100755 --- a/test/etap/090-task-status.t +++ b/test/etap/090-task-status.t @@ -46,9 +46,6 @@ get_task_prop(Pid, Prop) -> Value end. -now_ts() -> - {Mega, Secs, _} = erlang:now(), - Mega * 1000000 + Secs. loop() -> receive diff --git a/test/etap/Makefile.am b/test/etap/Makefile.am index b72b982bf..c969758e8 100644 --- a/test/etap/Makefile.am +++ b/test/etap/Makefile.am @@ -41,6 +41,7 @@ EXTRA_DIST = \ 030-doc-from-json.t \ 031-doc-to-json.t \ 040-util.t \ + 041-uuid-gen-id.ini \ 041-uuid-gen-seq.ini \ 041-uuid-gen-utc.ini \ 041-uuid-gen.t \ diff --git a/test/javascript/cli_runner.js b/test/javascript/cli_runner.js index f53ffe8c0..fcb4633b1 100644 --- a/test/javascript/cli_runner.js +++ b/test/javascript/cli_runner.js @@ -17,6 +17,29 @@ var console = { } }; +var fmtStack = function(stack) { + if(!stack) { + console.log("No stack information"); + return; + } + console.log("Trace back (most recent call first):\n"); + var re = new RegExp("(.*?)@([^:]*):(.*)$"); + var lines = stack.split("\n"); + for(var i = 0; i < lines.length; i++) { + var line = lines[i]; + if(!line.length) continue; + var match = re.exec(line); + if(!match) continue + var source = match[1].substr(0, 70); + var file = match[2]; + var lnum = match[3]; + while(lnum.length < 3) lnum = " " + lnum; + console.log(" " + lnum + ": " + file); + console.log(" " + source); + } +} + + function T(arg1, arg2) { if(!arg1) { var result = (arg2 ? arg2 : arg1); @@ -32,10 +55,8 @@ function runTestConsole(num, name, func) { print("ok " + num + " " + name); } catch(e) { print("not ok " + num + " " + name); - console.log(e.toSource()); - if (e.stack) { - console.log("Stacktrace:\n" + e.stack.replace(/^/gm, "\t")); - } + console.log("Reason: " + e.message); + fmtStack(e.stack); } return passed; } @@ -52,7 +73,12 @@ function runAllTestsConsole() { numPassed++; } } - T(numPassed == numTests, "All JS CLI tests should pass."); + if(numPassed != numTests) { + console.log("Test failures: " + (numTests - numPassed)); + quit(1); + } else { + console.log("All tests passed"); + } }; waitForSuccess(CouchDB.getVersion); diff --git a/test/javascript/run.tpl b/test/javascript/run.tpl index ac78b5005..267b6d0b3 100644 --- a/test/javascript/run.tpl +++ b/test/javascript/run.tpl @@ -13,6 +13,7 @@ # the License. SRC_DIR=%abs_top_srcdir% +BUILD_DIR=%abs_top_builddir% SCRIPT_DIR=$SRC_DIR/share/www/script JS_TEST_DIR=$SRC_DIR/test/javascript @@ -36,23 +37,21 @@ else fi fi - - # stop CouchDB on exit from various signals abort() { - trap - 0 - ./utils/run -d - exit 2 + trap - 0 + ./utils/run -d + exit 2 } # start CouchDB if [ -z $COUCHDB_NO_START ]; then make dev - trap 'abort' 0 1 2 3 4 6 8 15 + trap 'abort' EXIT ./utils/run -b -r 1 -n \ - -a $SRC_DIR/etc/couchdb/default_dev.ini \ + -a $BUILD_DIR/etc/couchdb/default_dev.ini \ -a $SRC_DIR/test/random_port.ini \ - -a $SRC_DIR/etc/couchdb/local_dev.ini + -a $BUILD_DIR/etc/couchdb/local_dev.ini sleep 1 # give it a sec fi @@ -67,12 +66,13 @@ $COUCHJS -H -u $COUCH_URI_FILE \ $TEST_SRC \ $JS_TEST_DIR/couch_http.js \ $JS_TEST_DIR/cli_runner.js + RESULT=$? if [ -z $COUCHDB_NO_START ]; then - # stop CouchDB - ./utils/run -d - trap - 0 + # stop CouchDB + ./utils/run -d + trap - 0 fi exit $RESULT |