From 29af20374cee5f7c75efc22b8e653febd78670a9 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Sun, 22 Apr 2012 23:15:20 +0100 Subject: Expand erlang releases tested by Travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index bb80cb006..aeabca9c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,7 @@ before_script: ./bootstrap && ./configure script: make check language: erlang otp_release: + - R15B01 + - R15B - R14B04 + - R14B03 -- cgit v1.2.1 From 1ffff829cb6a6618c3dc6cccd915c71d67ce69d8 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Mon, 23 Apr 2012 14:00:19 +0100 Subject: .travis.yml: make distcheck --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aeabca9c2..9566c411f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ 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: - R15B01 -- cgit v1.2.1 From 08071a80e0dc78b744cf56179c0aaac0a4390fde Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Mon, 23 Apr 2012 20:04:49 +0200 Subject: Make password hashing synchronous when using the /_config/admins API. This should account for many intermittent JavaScript test suite errors. The patch retains hashing admins on couch_server start to account for users editing their .ini files directly. Knowledge about password hash prefixes and the password hashing itself has been moved to couch_passwords. Thanks to Dale Harvey and Robert Newson for helping me to hunt this down and shaping and reviewing the patch. --- src/couchdb/couch_httpd_misc_handlers.erl | 7 ++++++- src/couchdb/couch_passwords.erl | 25 +++++++++++++++++++++++++ src/couchdb/couch_server.erl | 17 ++++------------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl index 38dd98ec1..6ee49e0b2 100644 --- a/src/couchdb/couch_httpd_misc_handlers.erl +++ b/src/couchdb/couch_httpd_misc_handlers.erl @@ -212,7 +212,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_passwords.erl b/src/couchdb/couch_passwords.erl index e5de87885..ce87f2c50 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(<>))). +%% 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(?b2l(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..8ebb7d623 100644 --- a/src/couchdb/couch_server.erl +++ b/src/couchdb/couch_server.erl @@ -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 -- cgit v1.2.1 From 3beac4e7d6e247af09d087fa53bac85c4b2f7c38 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Sat, 24 Mar 2012 20:17:28 -0500 Subject: Fix random failures in replication.js test This should fix the random failures for the 'expected "25" got "31" error message that pops up occasionally. If my hunch is correct when we remove the two docs from the source db its possible to get the writes on the target out of order depending on a race condition. This just sleeps a bit in between the deletions to try and reduce that possibility. I haven't spotted this error after applying this patch. --- share/www/script/test/replication.js | 1 + 1 file changed, 1 insertion(+) 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); -- cgit v1.2.1 From ba6c5745adc67b3f24837aa6d34250205d2e18e4 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Sun, 25 Mar 2012 05:46:15 -0500 Subject: Fix race condition in the auth db creation This test deletes a database and then tries to go and write to it without ensuring that it exists. This is just a race with the auth cache code trying to recreate its own database. This addition just makes sure it exists by trying to create it and ignoring any 412 errors if the auth cache won the race. --- share/www/script/test/reader_acl.js | 7 +++++++ 1 file changed, 7 insertions(+) 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(); -- cgit v1.2.1 From 7d2fe95802e032b824f732f3b819483f71b6b443 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Wed, 21 Mar 2012 22:42:15 -0500 Subject: Improve the CLI JavaScript stack formating This dresses up the output of the JavaScript tests by making sure the output is sane. Most of the issue was that the tracebak prints function arguments which can include the entire source of the test. This also tries to print the source file and line number nicely so that we can find right where errors have occurred. Its proven quite useful while fixing JavaScript tests. --- test/javascript/cli_runner.js | 36 +++++++++++++++++++++++++++++++----- test/javascript/run.tpl | 21 +++------------------ 2 files changed, 34 insertions(+), 23 deletions(-) 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..3cf7e6904 100644 --- a/test/javascript/run.tpl +++ b/test/javascript/run.tpl @@ -36,23 +36,15 @@ else fi fi - - -# stop CouchDB on exit from various signals -abort() { - 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 ./utils/run -b -r 1 -n \ -a $SRC_DIR/etc/couchdb/default_dev.ini \ -a $SRC_DIR/test/random_port.ini \ -a $SRC_DIR/etc/couchdb/local_dev.ini + RUN_PID=$! + trap "./utils/run -d || kill $RUN_PID || exit 2" EXIT sleep 1 # give it a sec fi @@ -67,12 +59,5 @@ $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 -fi -exit $RESULT +exit $? -- cgit v1.2.1 From 6c976bd948565305c006746f449dc8cd21a749e1 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Mon, 26 Mar 2012 15:17:02 -0500 Subject: Fixing the replicator_db JS test --- share/www/script/test/replicator_db.js | 4 ++++ src/couch_replicator/src/couch_replicator.erl | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) 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/src/couch_replicator/src/couch_replicator.erl b/src/couch_replicator/src/couch_replicator.erl index 1f7c08a76..e91e1ae49 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}; -- cgit v1.2.1 From 40a4e3343179936cc35d586f78ca99f4c0b65a42 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 23 Mar 2012 20:52:17 +0100 Subject: Do not overwrite X-CouchDB-Requested-Path Repeated rewrites would replace the initial value of X-CouchDB-Requested-Path. Fixes: COUCHDB-1442 --- CHANGES | 5 +++++ NEWS | 1 + share/www/script/test/rewrite.js | 20 ++++++++++++++++++++ src/couchdb/couch_httpd_rewrite.erl | 7 ++++--- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index c3a05e4eb..aa9af12aa 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,11 @@ 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. + Storage System: * Fixed unnecessary conflict when deleting and creating a diff --git a/NEWS b/NEWS index 8844d92dd..bcd9c4011 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,7 @@ 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. Version 1.2.1 ------------- diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js index 845429221..aaf8b69e9 100644 --- a/share/www/script/test/rewrite.js +++ b/share/www/script/test/rewrite.js @@ -437,4 +437,24 @@ couchTests.rewrite = function(debug) { var res = CouchDB.request("GET", "/test_suite_db/_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 = "/test_suite_db/_design/requested_path/_rewrite/show"; + var res = CouchDB.request("GET", url); + TEquals(url, res.responseText, "should return the original url"); + + var url = "/test_suite_db/_design/requested_path/_rewrite/show_rewritten"; + var res = CouchDB.request("GET", url); + TEquals(url, res.responseText, "returned the original url"); } diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl index c8cab85d7..cb164cd1f 100644 --- a/src/couchdb/couch_httpd_rewrite.erl +++ b/src/couchdb/couch_httpd_rewrite.erl @@ -165,9 +165,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)), -- cgit v1.2.1 From d076976cafc7ae01555f69808a7bdf0e84ec5702 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 28 Mar 2012 16:58:20 +0200 Subject: Limit rewrite recursion depth Loops in the rewriter would end up pegging the CPU until memory was exhausted. Max recursion is now configurable and limited to 100 iterations. Fixes: COUCHDB-1441 --- CHANGES | 2 ++ share/www/script/test/rewrite.js | 16 ++++++++++++++++ src/couchdb/couch_httpd_rewrite.erl | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGES b/CHANGES index aa9af12aa..c1bc9ba33 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,8 @@ 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. Storage System: diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js index aaf8b69e9..352e6b99b 100644 --- a/share/www/script/test/rewrite.js +++ b/share/www/script/test/rewrite.js @@ -457,4 +457,20 @@ couchTests.rewrite = function(debug) { var url = "/test_suite_db/_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 = "/test_suite_db/_design/loop/_rewrite/loop"; + var xhr = CouchDB.request("GET", url); + T(xhr.status = 400); + }); } diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl index cb164cd1f..207891ab9 100644 --- a/src/couchdb/couch_httpd_rewrite.erl +++ b/src/couchdb/couch_httpd_rewrite.erl @@ -119,6 +119,17 @@ handle_rewrite_req(#httpd{ Prefix = <<"/", 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), + NRewrites = 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 -- cgit v1.2.1 From 87f5dfd47c336373258721b95786cfc75999263b Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Sat, 7 Apr 2012 15:41:21 -0400 Subject: Added Ronny Pfannschmidt to the THANKS --- THANKS | 1 + 1 file changed, 1 insertion(+) diff --git a/THANKS b/THANKS index 42997c6bc..9033efe9c 100644 --- a/THANKS +++ b/THANKS @@ -92,5 +92,6 @@ suggesting improvements or submitting changes. Some of these people are: * Simon Leblanc * Rogutės Sparnuotos * Gavin McDonald + * Ronny Pfannschmidt For a list of authors see the `AUTHORS` file. -- cgit v1.2.1 From c83860c6269684e22a2984b791bc1e7231309f96 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Wed, 25 Apr 2012 17:17:05 -0500 Subject: Update changes with JS CLI test changes --- CHANGES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES b/CHANGES index c1bc9ba33..9242138ff 100644 --- a/CHANGES +++ b/CHANGES @@ -35,6 +35,11 @@ Security: * Passwords are now hashed using the PBKDF2 algorithm with a configurable work factor. +Test Suite: + + * Improved tracebacks printed by the JS CLI tests + * Improved the reliability of a number of tests + Version 1.2.1 ------------- -- cgit v1.2.1 From bdb50bef5de0d78e3f256e2aadda1c6013d15325 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Wed, 25 Apr 2012 18:03:16 -0500 Subject: Silence compiler warning --- src/couchdb/couch_httpd_rewrite.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl index 207891ab9..3e97258df 100644 --- a/src/couchdb/couch_httpd_rewrite.erl +++ b/src/couchdb/couch_httpd_rewrite.erl @@ -121,7 +121,7 @@ handle_rewrite_req(#httpd{ MaxRewritesList = couch_config:get("httpd", "rewrite_limit", "100"), MaxRewrites = list_to_integer(MaxRewritesList), - NRewrites = case get(couch_rewrite_count) of + case get(couch_rewrite_count) of undefined -> put(couch_rewrite_count, 1); NumRewrites when NumRewrites < MaxRewrites -> -- cgit v1.2.1 From 0250310257b2f280b063f748c06c0dd9f5eaacb1 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Wed, 25 Apr 2012 18:10:54 -0500 Subject: Fix JS CLI tests for make distcheck The default_dev.ini and local_dev.ini files are build artefacts and thus need to be found in the build directory instead of the source directory. --- test/javascript/run.tpl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/javascript/run.tpl b/test/javascript/run.tpl index 3cf7e6904..d163f9fd0 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 @@ -40,9 +41,9 @@ fi if [ -z $COUCHDB_NO_START ]; then make dev ./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 RUN_PID=$! trap "./utils/run -d || kill $RUN_PID || exit 2" EXIT sleep 1 # give it a sec -- cgit v1.2.1 From 6214d848815e91997aab70e1d8193103a21926a4 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Thu, 26 Apr 2012 13:33:50 +0100 Subject: s/http/httpc --- src/etap/etap_web.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/etap/etap_web.erl b/src/etap/etap_web.erl index fb7aee162..f97edfd77 100644 --- a/src/etap/etap_web.erl +++ b/src/etap/etap_web.erl @@ -42,7 +42,7 @@ simple_404(Url, 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 + try httpc:request(Method, {Url, Headers}, [{autoredirect, false}], []) of {ok, {OutStatus, OutHeaders, OutBody}} -> etap_request:new(Method, Url, Headers, Body, OutStatus, OutHeaders, OutBody); _ -> error @@ -56,7 +56,7 @@ build_request(Method, Url, Headers, Body) when Method == post; Method == put -> {value, {"Content-Type", X}} -> X; _ -> [] end, - try http:request(Method, {Url, Headers, ContentType, Body}, [{autoredirect, false}], []) of + try httpc:request(Method, {Url, Headers, ContentType, Body}, [{autoredirect, false}], []) of {ok, {OutStatus, OutHeaders, OutBody}} -> etap_request:new(Method, Url, Headers, Body, OutStatus, OutHeaders, OutBody); _ -> error -- cgit v1.2.1 From f6b97ee0c452233785dcd9b38699fc3935f9b107 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Thu, 26 Apr 2012 14:49:55 +0100 Subject: Use TEquals for great good --- share/www/script/test/cookie_auth.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) 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? -- cgit v1.2.1 From c2b12491a1ee2f6e0d396d0a875489b98ec8a6cd Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Thu, 26 Apr 2012 12:15:37 -0500 Subject: Revert an errant hunk from test/javascript/run.tpl Accidentally included half an idea on trying to make sure that the CouchDB process spawned by the JS CLI tests exited. --- test/javascript/run.tpl | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/javascript/run.tpl b/test/javascript/run.tpl index d163f9fd0..267b6d0b3 100644 --- a/test/javascript/run.tpl +++ b/test/javascript/run.tpl @@ -37,15 +37,21 @@ else fi fi +# stop CouchDB on exit from various signals +abort() { + trap - 0 + ./utils/run -d + exit 2 +} + # start CouchDB if [ -z $COUCHDB_NO_START ]; then make dev + trap 'abort' EXIT ./utils/run -b -r 1 -n \ -a $BUILD_DIR/etc/couchdb/default_dev.ini \ -a $SRC_DIR/test/random_port.ini \ -a $BUILD_DIR/etc/couchdb/local_dev.ini - RUN_PID=$! - trap "./utils/run -d || kill $RUN_PID || exit 2" EXIT sleep 1 # give it a sec fi @@ -61,4 +67,12 @@ $COUCHJS -H -u $COUCH_URI_FILE \ $JS_TEST_DIR/couch_http.js \ $JS_TEST_DIR/cli_runner.js -exit $? +RESULT=$? + +if [ -z $COUCHDB_NO_START ]; then + # stop CouchDB + ./utils/run -d + trap - 0 +fi + +exit $RESULT -- cgit v1.2.1 From eb3d5d8b4e973b8766a17055b1cf7258a6b8df89 Mon Sep 17 00:00:00 2001 From: bitdiddle Date: Thu, 26 Apr 2012 12:28:53 -0400 Subject: Update etap and remove obsolete files Update tests to use new etap, removed unused variable warnings, and added a check-etap to the Makefile as a convenience --- Makefile.am | 3 + src/couch_mrview/test/01-load.t | 2 +- src/couch_replicator/test/01-load.t | 2 +- src/etap/Makefile.am | 20 +-- src/etap/etap.erl | 330 +++++++++++++++++++++++++++------- src/etap/etap_application.erl | 72 -------- src/etap/etap_can.erl | 79 --------- src/etap/etap_exception.erl | 66 ------- src/etap/etap_process.erl | 42 ----- src/etap/etap_report.erl | 343 ------------------------------------ src/etap/etap_request.erl | 89 ---------- src/etap/etap_string.erl | 47 ----- src/etap/etap_web.erl | 65 ------- test/etap/001-load.t | 2 +- test/etap/020-btree-basics.t | 4 +- test/etap/040-util.t | 4 +- test/etap/050-stream.t | 2 +- test/etap/072-cleanup.t | 2 +- test/etap/073-changes.t | 2 +- test/etap/083-config-no-files.t | 2 - test/etap/090-task-status.t | 3 - 21 files changed, 278 insertions(+), 903 deletions(-) delete mode 100644 src/etap/etap_application.erl delete mode 100644 src/etap/etap_can.erl delete mode 100644 src/etap/etap_exception.erl delete mode 100644 src/etap/etap_process.erl delete mode 100644 src/etap/etap_report.erl delete mode 100644 src/etap/etap_request.erl delete mode 100644 src/etap/etap_string.erl delete mode 100644 src/etap/etap_web.erl diff --git a/Makefile.am b/Makefile.am index 02b4bd11e..9dd22b86a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -89,6 +89,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/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/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/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 -%% -%% 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 [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 -%% -%% 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 -%% -%% 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 -%% -%% 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 -%% -%% 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, "", []), - io:format(IndexFD, "", []), - 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, "
", [RowClass]), - io:format(IndexFD, "~s", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]), - io:format(IndexFD, " - - - - -
~p%  - - -
-
- ", [CovPer, CovPer, UnCovPer]), - io:format(IndexFD, "
", []), - 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, "

Generated on ~s.

~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, "
", []), - io:format(IndexFD, "Total - - - - -
~p%  - - -
-
- ", [TotalCovPer, TotalCovPer, TotalUnCovPer]), - io:format(IndexFD, "
", []) - end, - io:format(IndexFD, "", []), - 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("~s ~s", [Number, PadNu, Line]); -out_line(Number, highlight, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("~s ~s", [Number, PadNu, Line]); -out_line(Number, 0, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("~s ~s", [Number, PadNu, Line]); -out_line(Number, _, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("~s ~s", [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(" - - - ~s - C0 code coverage information - - - - -

C0 code coverage information

-

Generated on ~s with etap 0.3.4. -

- - - - - - - - - - - - - - - - - - - - -
NameTotal linesLines of codeTotal coverageCode coverage
- ~s - - ?? - - ?? - - ?? - - - - - -
~p%  - - -
-
-
", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]).
-
-%% @private
-footer() ->
-    "

Generated using etap 0.3.4.

- - - ". 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 -%% -%% 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 -%% -%% 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 [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 f97edfd77..000000000 --- a/src/etap/etap_web.erl +++ /dev/null @@ -1,65 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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 [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 httpc: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 httpc: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/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/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 -- cgit v1.2.1 From 591c973b3ec1df2c5294e5fdaea66bc66e21f9b7 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Thu, 26 Apr 2012 19:34:34 -0500 Subject: Make sure that local_dev.ini is writable --- etc/couchdb/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index dd1054b6b..fe045f952 100644 --- a/etc/couchdb/Makefile.am +++ b/etc/couchdb/Makefile.am @@ -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: -- cgit v1.2.1 From b1799cdcd554260eafe0e8d29bddd7e664dc7a03 Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Sun, 29 Apr 2012 13:33:10 +0100 Subject: Revert "Make sure that local_dev.ini is writable" This reverts commit 591c973b3ec1df2c5294e5fdaea66bc66e21f9b7. This breaks installing form non-writable media (think CD/DVD installs). --- etc/couchdb/Makefile.am | 1 - 1 file changed, 1 deletion(-) diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index fe045f952..dd1054b6b 100644 --- a/etc/couchdb/Makefile.am +++ b/etc/couchdb/Makefile.am @@ -73,7 +73,6 @@ default_dev.ini: default.ini.tpl local_dev.ini: local.ini if test ! -f "$@"; then \ cp $< $@; \ - chmod +w $@; \ fi install-data-hook: -- cgit v1.2.1 From bdcfcca1ec92f379e5376741e5ab103630573ac4 Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Mon, 30 Apr 2012 12:16:36 +0100 Subject: Revert "Revert "Make sure that local_dev.ini is writable"" This breaks builds that expect a read-only install media like CD/DVDs. This reverts commit b1799cdcd554260eafe0e8d29bddd7e664dc7a03. --- etc/couchdb/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index dd1054b6b..fe045f952 100644 --- a/etc/couchdb/Makefile.am +++ b/etc/couchdb/Makefile.am @@ -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: -- cgit v1.2.1 From 9acc34ed9ed4315f7cd6f5b8ac45e47f2c0b8cb6 Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Mon, 30 Apr 2012 12:23:00 +0100 Subject: Revert "Revert "Revert "Make sure that local_dev.ini is writable""" Revert. Git <3. This reverts commit bdcfcca1ec92f379e5376741e5ab103630573ac4. --- etc/couchdb/Makefile.am | 1 - 1 file changed, 1 deletion(-) diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index fe045f952..dd1054b6b 100644 --- a/etc/couchdb/Makefile.am +++ b/etc/couchdb/Makefile.am @@ -73,7 +73,6 @@ default_dev.ini: default.ini.tpl local_dev.ini: local.ini if test ! -f "$@"; then \ cp $< $@; \ - chmod +w $@; \ fi install-data-hook: -- cgit v1.2.1 From 36dd7c1929fb6df9c287df4771b6afbb1cd93dd5 Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Mon, 30 Apr 2012 17:19:55 +0100 Subject: whitespace --- etc/couchdb/Makefile.am | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index dd1054b6b..a65ffb9d2 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" \ -- cgit v1.2.1 From 69906467c03266e3f3f49e9a3620435fcc3550d8 Mon Sep 17 00:00:00 2001 From: Paul Joseph Davis Date: Mon, 30 Apr 2012 14:02:46 -0500 Subject: Make local_dev.ini writable again I nearly did the fourth revert but I changed whitespace to match the previous line as well. --- etc/couchdb/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/etc/couchdb/Makefile.am b/etc/couchdb/Makefile.am index a65ffb9d2..8d996f4be 100644 --- a/etc/couchdb/Makefile.am +++ b/etc/couchdb/Makefile.am @@ -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: -- cgit v1.2.1 From eb0e377e5b40bdc1122aaf54c2f98b8ba7629096 Mon Sep 17 00:00:00 2001 From: Magnus Hoff Date: Sat, 28 Apr 2012 20:24:43 +0200 Subject: Merged pull request #19 from @maghoff with thanks Fix failed loading of CommonJS modules that end in a line comment. --- share/server/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { -- cgit v1.2.1 From bd7509ab0152b78abf5aa8ca30ead6ba2e8392da Mon Sep 17 00:00:00 2001 From: Dave Cottlehuber Date: Mon, 30 Apr 2012 23:44:49 +0200 Subject: Added Magnus Hoff to THANKS --- THANKS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/THANKS b/THANKS index 9033efe9c..ddd9ab9ca 100644 --- a/THANKS +++ b/THANKS @@ -93,5 +93,7 @@ suggesting improvements or submitting changes. Some of these people are: * Rogutės Sparnuotos * Gavin McDonald * Ronny Pfannschmidt + * Magnus Hoff For a list of authors see the `AUTHORS` file. + -- cgit v1.2.1 From a1d46acba95b08c96cff5b59770f84588e5e9c6c Mon Sep 17 00:00:00 2001 From: Alexander Dorofeev Date: Sun, 29 Apr 2012 09:54:30 +0600 Subject: Directory creation in line 86 breaks permissions. Closes PR #20. Thanks to Alexander Dorofeev. --- THANKS | 1 + etc/init/couchdb.tpl.in | 1 + 2 files changed, 2 insertions(+) diff --git a/THANKS b/THANKS index ddd9ab9ca..0c0a8661c 100644 --- a/THANKS +++ b/THANKS @@ -94,6 +94,7 @@ suggesting improvements or submitting changes. Some of these people are: * Gavin McDonald * Ronny Pfannschmidt * Magnus Hoff + * Alexander Dorofeev For a list of authors see the `AUTHORS` file. diff --git a/etc/init/couchdb.tpl.in b/etc/init/couchdb.tpl.in index e4930e005..173efa362 100644 --- a/etc/init/couchdb.tpl.in +++ b/etc/init/couchdb.tpl.in @@ -84,6 +84,7 @@ start_couchdb () { # Start Apache CouchDB as a background process. mkdir -p "$RUN_DIR" + chown $COUCHDB_USER "$RUN_DIR" command="$COUCHDB -b" if test -n "$COUCHDB_STDOUT_FILE"; then command="$command -o $COUCHDB_STDOUT_FILE" -- cgit v1.2.1 From 7d540c6e61a6aa3c44580041a25edf4ea4574b40 Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Sun, 6 May 2012 14:09:13 +0200 Subject: init.d: Only chown when COUCHDB_USER is not empty. --- etc/init/couchdb.tpl.in | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/etc/init/couchdb.tpl.in b/etc/init/couchdb.tpl.in index 173efa362..39b62500d 100644 --- a/etc/init/couchdb.tpl.in +++ b/etc/init/couchdb.tpl.in @@ -84,7 +84,9 @@ start_couchdb () { # Start Apache CouchDB as a background process. mkdir -p "$RUN_DIR" - chown $COUCHDB_USER "$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" -- cgit v1.2.1 From af7441d8dc48ecaee4d66322fbc1e0ce5426cf19 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Mon, 7 May 2012 14:56:35 +0200 Subject: make sure that ClearPassword string is handled. When you set a clear password in the config, the `couch_passwords:hash_admin_password/1` get a string instead of a binary. --- src/couchdb/couch_passwords.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/couchdb/couch_passwords.erl b/src/couchdb/couch_passwords.erl index ce87f2c50..57e51930e 100644 --- a/src/couchdb/couch_passwords.erl +++ b/src/couchdb/couch_passwords.erl @@ -30,8 +30,8 @@ simple(Password, Salt) -> hash_admin_password(ClearPassword) -> Iterations = couch_config:get("couch_httpd_auth", "iterations", "10000"), Salt = couch_uuids:random(), - DerivedKey = couch_passwords:pbkdf2(?b2l(ClearPassword), Salt, - list_to_integer(Iterations)), + DerivedKey = couch_passwords:pbkdf2(couch_util:to_binary(ClearPassword), + Salt ,list_to_integer(Iterations)), ?l2b("-pbkdf2-" ++ ?b2l(DerivedKey) ++ "," ++ ?b2l(Salt) ++ "," ++ Iterations). -- cgit v1.2.1 From 093d2aa6544546a95f6133f1db3c4f4179793f3c Mon Sep 17 00:00:00 2001 From: benoitc Date: Wed, 16 May 2012 07:30:19 +0200 Subject: add Server-Sent Events protocol to db changes API. close #COUCHDB-986 This patch add support for the new specification of w3c by adding a new feed type named `eventsource`: http://www.w3.org/TR/2009/WD-eventsource-20090423/ This patch is based on @indutny patch with edits. --- share/www/script/test/changes.js | 28 ++++++++++++++++++++++++++++ src/couchdb/couch_changes.erl | 15 ++++++++++----- src/couchdb/couch_httpd_db.erl | 24 ++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 19e22fd00..c529b21cb 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -139,6 +139,34 @@ 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(); 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_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index de39b9efb..092001442 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, @@ -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_json_response(Req, 200, Headers), + fun(FeedChangesFun) -> + FeedChangesFun(MakeCallback(Resp)) + end; _ -> % "longpoll" or "continuous" {ok, Resp} = couch_httpd:start_json_response(Req, 200), @@ -1097,13 +1115,15 @@ parse_doc_query(Req) -> parse_changes_query(Req) -> 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", _} -> 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", _} -> -- cgit v1.2.1 From ca1ba7385ed486ddbaa76010307eee2f46ae446c Mon Sep 17 00:00:00 2001 From: benoitc Date: Wed, 16 May 2012 07:48:14 +0200 Subject: fix whitespaces --- share/www/script/test/changes.js | 71 ++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index c529b21cb..7c0af0f20 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", @@ -83,7 +83,7 @@ couchTests.changes = function(debug) { 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); @@ -124,7 +124,7 @@ couchTests.changes = function(debug) { //verify the hearbeat newlines are sent xhr.open("GET", "/test_suite_db/_changes?feed=continuous&heartbeat=10&timeout=500", true); xhr.send(""); - + var str; waitForSuccess(function() { str = xhr.responseText; @@ -147,21 +147,20 @@ couchTests.changes = function(debug) { var sourceListener = function(e) { var data = JSON.parse(e.data); results.push(data); - }; source.addEventListener('message', sourceListener , false); - + waitForSuccess(function() { - if (results.length != 3) + 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); @@ -172,14 +171,14 @@ couchTests.changes = function(debug) { xhr.open("GET", "/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); @@ -187,7 +186,7 @@ couchTests.changes = function(debug) { 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); @@ -196,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"); @@ -212,7 +211,7 @@ couchTests.changes = function(debug) { } - + // test the filtered changes var ddoc = { _id : "_design/changes_filter", @@ -252,15 +251,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"); @@ -279,15 +278,15 @@ couchTests.changes = function(debug) { 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.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update"); xhr.abort(); - + var timeout = 500; var last_seq = 10; while (true) { @@ -330,31 +329,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); @@ -378,7 +377,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); @@ -470,7 +469,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); @@ -489,9 +488,9 @@ couchTests.changes = function(debug) { xhr = CouchDB.newXhr(); xhr.open("POST", "/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() { -- cgit v1.2.1 From 0e5c44d86537715629012a96de31ac1d0d55d902 Mon Sep 17 00:00:00 2001 From: benoitc Date: Wed, 16 May 2012 07:48:27 +0200 Subject: fix changes test to make it wokr on all browsers --- share/www/script/test/changes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 7c0af0f20..91fde7e98 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -140,7 +140,7 @@ couchTests.changes = function(debug) { xhr.abort(); // test Server Sent Event (eventsource) - if (window.EventSource) { + if (!!window.EventSource) { var source = new EventSource( "/test_suite_db/_changes?feed=eventsource"); var results = []; -- cgit v1.2.1 From 6c1ac8b3aabb24c69c043c522d67fc15141df358 Mon Sep 17 00:00:00 2001 From: benoitc Date: Wed, 16 May 2012 08:52:08 +0200 Subject: update news --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index bcd9c4011..fa8cdc7ff 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,7 @@ This version has not been released yet. 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. Version 1.2.1 ------------- -- cgit v1.2.1 From 46d3ce1b1265dffc8fb502f62f55c469f7785d93 Mon Sep 17 00:00:00 2001 From: benoitc Date: Wed, 16 May 2012 18:12:16 +0200 Subject: make sure default headers are defaults. --- src/couchdb/couch_httpd_db.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index 092001442..e9e410905 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -132,7 +132,7 @@ handle_changes_req2(Req, Db) -> {"Content-Type", "text/event-stream"}, {"Cache-Control", "no-cache"} ], - {ok, Resp} = couch_httpd:start_json_response(Req, 200, Headers), + {ok, Resp} = couch_httpd:start_chunked_response(Req, 200, Headers), fun(FeedChangesFun) -> FeedChangesFun(MakeCallback(Resp)) end; -- cgit v1.2.1 From 325cee6f48c9e1627a9ce9ac0cc90ce0d726bd6e Mon Sep 17 00:00:00 2001 From: benoitc Date: Thu, 17 May 2012 11:03:07 +0200 Subject: vhosts values should not be empty. A vhost value can be empty when removed with the couch_config module. --- src/couchdb/couch_httpd_vhost.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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")), -- cgit v1.2.1 From 7156254d09bcee4580fa1340edfc5d616ff2213d Mon Sep 17 00:00:00 2001 From: Anthony S Baker Date: Wed, 9 May 2012 13:56:25 -0400 Subject: COUCHDB-1473 & COUCHDB-1472 - Futon: disable buttons if user has insufficient rights - Disabled the delete database button if it is not in adminparty, or if the current user is not admin. - Security button is also disabled if user is not a database admin. --- share/www/database.html | 4 ++-- share/www/script/futon.js | 30 ++++++++++++++++++++++++++++++ share/www/style/layout.css | 2 ++ 3 files changed, 34 insertions(+), 2 deletions(-) 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.
  • -
  • +
  • -
  • +