summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCaleb Case <calebcase@gmail.com>2012-11-05 05:29:20 -0500
committerCaleb Case <calebcase@gmail.com>2012-11-05 05:29:20 -0500
commit5b02fdbd2316adda39803a324a46cf182f25c65b (patch)
tree6b2eb31f7d5dc76d0ec1fc78d707c70f95918164
parentac2240bcebeee92acdb343f55e58435bce235194 (diff)
parentb9af7ea506ac95034a91951f29e1f4249ef4317c (diff)
downloadcouchdb-430-fix-content-type-for-lists-after-get-row.tar.gz
Merge git://git.apache.org/couchdb430-fix-content-type-for-lists-after-get-row
-rw-r--r--.gitignore1
-rw-r--r--.mailmap13
-rw-r--r--.travis.yml6
-rw-r--r--CHANGES34
-rw-r--r--Makefile.am14
-rw-r--r--NEWS8
-rw-r--r--README6
-rw-r--r--THANKS10
-rw-r--r--etc/couchdb/Makefile.am7
-rw-r--r--etc/couchdb/default.ini.tpl.in5
-rw-r--r--etc/init/couchdb.tpl.in3
-rw-r--r--share/server/util.js2
-rw-r--r--share/www/database.html4
-rw-r--r--share/www/script/couch.js11
-rw-r--r--share/www/script/couch_test_runner.js1
-rw-r--r--share/www/script/futon.browse.js13
-rw-r--r--share/www/script/futon.js30
-rw-r--r--share/www/script/jquery.couch.js5
-rw-r--r--share/www/script/test/changes.js144
-rw-r--r--share/www/script/test/content_negotiation.js2
-rw-r--r--share/www/script/test/cookie_auth.js22
-rw-r--r--share/www/script/test/reader_acl.js7
-rw-r--r--share/www/script/test/replication.js1
-rw-r--r--share/www/script/test/replicator_db.js4
-rw-r--r--share/www/script/test/rewrite.js201
-rw-r--r--share/www/script/test/update_documents.js15
-rw-r--r--share/www/script/test/users_db.js10
-rw-r--r--share/www/script/test/uuids.js27
-rw-r--r--share/www/script/test/view_multi_key_all_docs.js4
-rw-r--r--share/www/script/test/view_multi_key_design.js4
-rw-r--r--share/www/script/test/view_multi_key_temp.js3
-rw-r--r--share/www/spec/run.html1
-rw-r--r--share/www/style/layout.css2
-rw-r--r--share/www/verify_install.html1
-rw-r--r--src/couch_index/src/couch_index.erl14
-rw-r--r--src/couch_index/src/couch_index_server.erl2
-rw-r--r--src/couch_index/src/couch_index_updater.erl9
-rw-r--r--src/couch_mrview/src/couch_mrview_compactor.erl4
-rw-r--r--src/couch_mrview/src/couch_mrview_index.erl7
-rw-r--r--src/couch_mrview/src/couch_mrview_show.erl20
-rw-r--r--src/couch_mrview/src/couch_mrview_util.erl8
-rw-r--r--src/couch_mrview/test/01-load.t2
-rw-r--r--src/couch_replicator/src/couch_replicator.erl9
-rw-r--r--src/couch_replicator/src/couch_replicator_httpc.erl25
-rw-r--r--src/couch_replicator/src/couch_replicator_manager.erl9
-rw-r--r--src/couch_replicator/test/01-load.t2
-rw-r--r--src/couchdb/couch_changes.erl15
-rw-r--r--src/couchdb/couch_config.erl13
-rw-r--r--src/couchdb/couch_config_writer.erl8
-rw-r--r--src/couchdb/couch_db_update_notifier.erl13
-rw-r--r--src/couchdb/couch_doc.erl2
-rw-r--r--src/couchdb/couch_file.erl14
-rw-r--r--src/couchdb/couch_httpd.erl116
-rw-r--r--src/couchdb/couch_httpd_db.erl33
-rw-r--r--src/couchdb/couch_httpd_misc_handlers.erl16
-rw-r--r--src/couchdb/couch_httpd_rewrite.erl20
-rw-r--r--src/couchdb/couch_httpd_vhost.erl5
-rw-r--r--src/couchdb/couch_js_functions.hrl6
-rw-r--r--src/couchdb/couch_log.erl10
-rw-r--r--src/couchdb/couch_passwords.erl25
-rw-r--r--src/couchdb/couch_server.erl23
-rw-r--r--src/couchdb/couch_server_sup.erl14
-rw-r--r--src/couchdb/couch_users_db.erl6
-rw-r--r--src/couchdb/couch_util.erl42
-rw-r--r--src/couchdb/couch_uuids.erl10
-rw-r--r--src/etap/Makefile.am20
-rw-r--r--src/etap/etap.erl330
-rw-r--r--src/etap/etap_application.erl72
-rw-r--r--src/etap/etap_can.erl79
-rw-r--r--src/etap/etap_exception.erl66
-rw-r--r--src/etap/etap_process.erl42
-rw-r--r--src/etap/etap_report.erl343
-rw-r--r--src/etap/etap_request.erl89
-rw-r--r--src/etap/etap_string.erl47
-rw-r--r--src/etap/etap_web.erl65
-rw-r--r--src/ibrowse/ibrowse_http_client.erl62
-rw-r--r--src/mochiweb/mochiweb_acceptor.erl7
-rw-r--r--src/mochiweb/mochiweb_cookies.erl12
-rw-r--r--src/mochiweb/mochiweb_request.erl25
-rw-r--r--src/mochiweb/mochiweb_socket.erl15
-rw-r--r--src/snappy/Makefile.am2
-rwxr-xr-xtest/etap/001-load.t2
-rwxr-xr-xtest/etap/020-btree-basics.t4
-rwxr-xr-xtest/etap/040-util.t4
-rw-r--r--test/etap/041-uuid-gen-id.ini20
-rwxr-xr-xtest/etap/041-uuid-gen.t33
-rwxr-xr-xtest/etap/050-stream.t2
-rwxr-xr-xtest/etap/072-cleanup.t2
-rwxr-xr-xtest/etap/073-changes.t2
-rwxr-xr-xtest/etap/083-config-no-files.t2
-rwxr-xr-xtest/etap/090-task-status.t3
-rw-r--r--test/etap/Makefile.am1
-rw-r--r--test/javascript/cli_runner.js36
-rw-r--r--test/javascript/run.tpl22
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
diff --git a/CHANGES b/CHANGES
index c3a05e4eb..82f26d0cb 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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
diff --git a/NEWS b/NEWS
index 8844d92dd..62db51fc0 100644
--- a/NEWS
+++ b/NEWS
@@ -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
-------------
diff --git a/README b/README
index b3358f665..0d125fbba 100644
--- a/README
+++ b/README
@@ -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
diff --git a/THANKS b/THANKS
index 42997c6bc..6217c947e 100644
--- a/THANKS
+++ b/THANKS
@@ -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 &amp; 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>&nbsp;</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>&nbsp;</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>&nbsp;</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