Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 15B8111D33 for ; Fri, 29 Aug 2014 20:43:08 +0000 (UTC) Received: (qmail 83124 invoked by uid 500); 29 Aug 2014 20:43:05 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 82992 invoked by uid 500); 29 Aug 2014 20:43:05 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 81144 invoked by uid 99); 29 Aug 2014 20:43:04 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 29 Aug 2014 20:43:04 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 2151E9A8D5F; Fri, 29 Aug 2014 20:43:04 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: chewbranca@apache.org To: commits@couchdb.apache.org Date: Fri, 29 Aug 2014 20:43:39 -0000 Message-Id: In-Reply-To: <455b2bf437ec4190a337b5785b95de93@git.apache.org> References: <455b2bf437ec4190a337b5785b95de93@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [37/50] [abbrv] couch commit: updated refs/heads/1963-eunit-bigcouch to 08c6f0b Move files out of test/couchdb into top level test/ folder Project: http://git-wip-us.apache.org/repos/asf/couchdb-couch/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-couch/commit/b3a05dcc Tree: http://git-wip-us.apache.org/repos/asf/couchdb-couch/tree/b3a05dcc Diff: http://git-wip-us.apache.org/repos/asf/couchdb-couch/diff/b3a05dcc Branch: refs/heads/1963-eunit-bigcouch Commit: b3a05dcc4a6dc4a825aaefadb15d954c143cba78 Parents: 6bc707b Author: Russell Branca Authored: Mon Aug 11 13:19:06 2014 -0700 Committer: Russell Branca Committed: Fri Aug 29 13:42:25 2014 -0700 ---------------------------------------------------------------------- test/couch_auth_cache_tests.erl | 238 +++++++ test/couch_btree_tests.erl | 551 +++++++++++++++ test/couch_changes_tests.erl | 612 +++++++++++++++++ test/couch_db_tests.erl | 114 ++++ test/couch_doc_json_tests.erl | 391 +++++++++++ test/couch_file_tests.erl | 265 ++++++++ test/couch_key_tree_tests.erl | 380 +++++++++++ test/couch_passwords_tests.erl | 54 ++ test/couch_ref_counter_tests.erl | 107 +++ test/couch_stats_tests.erl | 412 ++++++++++++ test/couch_stream_tests.erl | 100 +++ test/couch_task_status_tests.erl | 225 +++++++ test/couch_util_tests.erl | 136 ++++ test/couch_uuids_tests.erl | 161 +++++ test/couch_work_queue_tests.erl | 393 +++++++++++ test/couchdb/couch_auth_cache_tests.erl | 238 ------- test/couchdb/couch_btree_tests.erl | 551 --------------- test/couchdb/couch_changes_tests.erl | 612 ----------------- test/couchdb/couch_db_tests.erl | 114 ---- test/couchdb/couch_doc_json_tests.erl | 391 ----------- test/couchdb/couch_file_tests.erl | 265 -------- test/couchdb/couch_key_tree_tests.erl | 380 ----------- test/couchdb/couch_passwords_tests.erl | 54 -- test/couchdb/couch_ref_counter_tests.erl | 107 --- test/couchdb/couch_stats_tests.erl | 412 ------------ test/couchdb/couch_stream_tests.erl | 100 --- test/couchdb/couch_task_status_tests.erl | 225 ------- test/couchdb/couch_util_tests.erl | 136 ---- test/couchdb/couch_uuids_tests.erl | 161 ----- test/couchdb/couch_work_queue_tests.erl | 393 ----------- test/couchdb/couchdb_attachments_tests.erl | 638 ------------------ test/couchdb/couchdb_compaction_daemon.erl | 231 ------- test/couchdb/couchdb_cors_tests.erl | 344 ---------- test/couchdb/couchdb_csp_tests.erl | 96 --- test/couchdb/couchdb_file_compression_tests.erl | 239 ------- test/couchdb/couchdb_http_proxy_tests.erl | 462 ------------- test/couchdb/couchdb_modules_load_tests.erl | 68 -- test/couchdb/couchdb_os_daemons_tests.erl | 228 ------- test/couchdb/couchdb_os_proc_pool.erl | 179 ----- test/couchdb/couchdb_update_conflicts_tests.erl | 243 ------- test/couchdb/couchdb_vhosts_tests.erl | 441 ------------ test/couchdb/couchdb_views_tests.erl | 669 ------------------- .../3b835456c235b1827e012e25666152f3.view | Bin 4192 -> 0 bytes .../couchdb/fixtures/couch_stats_aggregates.cfg | 19 - .../couchdb/fixtures/couch_stats_aggregates.ini | 20 - test/couchdb/fixtures/logo.png | Bin 3010 -> 0 bytes test/couchdb/fixtures/os_daemon_bad_perm.sh | 17 - test/couchdb/fixtures/os_daemon_can_reboot.sh | 15 - .../couchdb/fixtures/os_daemon_configer.escript | 101 --- test/couchdb/fixtures/os_daemon_die_on_boot.sh | 15 - test/couchdb/fixtures/os_daemon_die_quickly.sh | 15 - test/couchdb/fixtures/os_daemon_looper.escript | 26 - test/couchdb/fixtures/test.couch | Bin 16482 -> 0 bytes test/couchdb/json_stream_parse_tests.erl | 151 ----- test/couchdb/test_request.erl | 75 --- test/couchdb/test_web.erl | 112 ---- test/couchdb_attachments_tests.erl | 638 ++++++++++++++++++ test/couchdb_compaction_daemon.erl | 231 +++++++ test/couchdb_cors_tests.erl | 344 ++++++++++ test/couchdb_csp_tests.erl | 96 +++ test/couchdb_file_compression_tests.erl | 239 +++++++ test/couchdb_http_proxy_tests.erl | 462 +++++++++++++ test/couchdb_modules_load_tests.erl | 68 ++ test/couchdb_os_daemons_tests.erl | 228 +++++++ test/couchdb_os_proc_pool.erl | 179 +++++ test/couchdb_update_conflicts_tests.erl | 243 +++++++ test/couchdb_vhosts_tests.erl | 441 ++++++++++++ test/couchdb_views_tests.erl | 669 +++++++++++++++++++ .../3b835456c235b1827e012e25666152f3.view | Bin 0 -> 4192 bytes test/fixtures/couch_stats_aggregates.cfg | 19 + test/fixtures/couch_stats_aggregates.ini | 20 + test/fixtures/logo.png | Bin 0 -> 3010 bytes test/fixtures/os_daemon_bad_perm.sh | 17 + test/fixtures/os_daemon_can_reboot.sh | 15 + test/fixtures/os_daemon_configer.escript | 101 +++ test/fixtures/os_daemon_die_on_boot.sh | 15 + test/fixtures/os_daemon_die_quickly.sh | 15 + test/fixtures/os_daemon_looper.escript | 26 + test/fixtures/test.couch | Bin 0 -> 16482 bytes test/json_stream_parse_tests.erl | 151 +++++ test/test_request.erl | 75 +++ test/test_web.erl | 112 ++++ 82 files changed, 8543 insertions(+), 8543 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/b3a05dcc/test/couch_auth_cache_tests.erl ---------------------------------------------------------------------- diff --git a/test/couch_auth_cache_tests.erl b/test/couch_auth_cache_tests.erl new file mode 100644 index 0000000..3b2321c --- /dev/null +++ b/test/couch_auth_cache_tests.erl @@ -0,0 +1,238 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_auth_cache_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(SALT, <<"SALT">>). +-define(TIMEOUT, 1000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + Pid. + +stop(Pid) -> + erlang:monitor(process, Pid), + couch_server_sup:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + DbName = ?tempdb(), + couch_config:set("couch_httpd_auth", "authentication_db", + ?b2l(DbName), false), + DbName. + +teardown(DbName) -> + ok = couch_server:delete(DbName, [?ADMIN_USER]), + ok. + + +couch_auth_cache_test_() -> + { + "CouchDB auth cache tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_get_nil_on_missed_cache/1, + fun should_get_right_password_hash/1, + fun should_ensure_doc_hash_equals_cached_one/1, + fun should_update_password/1, + fun should_cleanup_cache_after_userdoc_deletion/1, + fun should_restore_cache_after_userdoc_recreation/1, + fun should_drop_cache_on_auth_db_change/1, + fun should_restore_cache_on_auth_db_change/1, + fun should_recover_cache_after_shutdown/1 + ] + } + } + }. + + +should_get_nil_on_missed_cache(_) -> + ?_assertEqual(nil, couch_auth_cache:get_user_creds("joe")). + +should_get_right_password_hash(DbName) -> + ?_test(begin + PasswordHash = hash_password("pass1"), + {ok, _} = update_user_doc(DbName, "joe", "pass1"), + Creds = couch_auth_cache:get_user_creds("joe"), + ?assertEqual(PasswordHash, + couch_util:get_value(<<"password_sha">>, Creds)) + end). + +should_ensure_doc_hash_equals_cached_one(DbName) -> + ?_test(begin + {ok, _} = update_user_doc(DbName, "joe", "pass1"), + Creds = couch_auth_cache:get_user_creds("joe"), + + CachedHash = couch_util:get_value(<<"password_sha">>, Creds), + StoredHash = get_user_doc_password_sha(DbName, "joe"), + ?assertEqual(StoredHash, CachedHash) + end). + +should_update_password(DbName) -> + ?_test(begin + PasswordHash = hash_password("pass2"), + {ok, Rev} = update_user_doc(DbName, "joe", "pass1"), + {ok, _} = update_user_doc(DbName, "joe", "pass2", Rev), + Creds = couch_auth_cache:get_user_creds("joe"), + ?assertEqual(PasswordHash, + couch_util:get_value(<<"password_sha">>, Creds)) + end). + +should_cleanup_cache_after_userdoc_deletion(DbName) -> + ?_test(begin + {ok, _} = update_user_doc(DbName, "joe", "pass1"), + delete_user_doc(DbName, "joe"), + ?assertEqual(nil, couch_auth_cache:get_user_creds("joe")) + end). + +should_restore_cache_after_userdoc_recreation(DbName) -> + ?_test(begin + PasswordHash = hash_password("pass5"), + {ok, _} = update_user_doc(DbName, "joe", "pass1"), + delete_user_doc(DbName, "joe"), + ?assertEqual(nil, couch_auth_cache:get_user_creds("joe")), + + {ok, _} = update_user_doc(DbName, "joe", "pass5"), + Creds = couch_auth_cache:get_user_creds("joe"), + + ?assertEqual(PasswordHash, + couch_util:get_value(<<"password_sha">>, Creds)) + end). + +should_drop_cache_on_auth_db_change(DbName) -> + ?_test(begin + {ok, _} = update_user_doc(DbName, "joe", "pass1"), + full_commit(DbName), + couch_config:set("couch_httpd_auth", "authentication_db", + ?b2l(?tempdb()), false), + ?assertEqual(nil, couch_auth_cache:get_user_creds("joe")) + end). + +should_restore_cache_on_auth_db_change(DbName) -> + ?_test(begin + PasswordHash = hash_password("pass1"), + {ok, _} = update_user_doc(DbName, "joe", "pass1"), + Creds = couch_auth_cache:get_user_creds("joe"), + full_commit(DbName), + + DbName1 = ?tempdb(), + couch_config:set("couch_httpd_auth", "authentication_db", + ?b2l(DbName1), false), + + {ok, _} = update_user_doc(DbName1, "joe", "pass5"), + full_commit(DbName1), + + couch_config:set("couch_httpd_auth", "authentication_db", + ?b2l(DbName), false), + + Creds = couch_auth_cache:get_user_creds("joe"), + ?assertEqual(PasswordHash, + couch_util:get_value(<<"password_sha">>, Creds)) + end). + +should_recover_cache_after_shutdown(DbName) -> + ?_test(begin + PasswordHash = hash_password("pass2"), + {ok, Rev0} = update_user_doc(DbName, "joe", "pass1"), + {ok, Rev1} = update_user_doc(DbName, "joe", "pass2", Rev0), + full_commit(DbName), + shutdown_db(DbName), + {ok, Rev1} = get_doc_rev(DbName, "joe"), + ?assertEqual(PasswordHash, get_user_doc_password_sha(DbName, "joe")) + end). + + +update_user_doc(DbName, UserName, Password) -> + update_user_doc(DbName, UserName, Password, nil). + +update_user_doc(DbName, UserName, Password, Rev) -> + User = iolist_to_binary(UserName), + Doc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"org.couchdb.user:", User/binary>>}, + {<<"name">>, User}, + {<<"type">>, <<"user">>}, + {<<"salt">>, ?SALT}, + {<<"password_sha">>, hash_password(Password)}, + {<<"roles">>, []} + ] ++ case Rev of + nil -> []; + _ -> [{<<"_rev">>, Rev}] + end + }), + {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), + {ok, NewRev} = couch_db:update_doc(AuthDb, Doc, []), + ok = couch_db:close(AuthDb), + {ok, couch_doc:rev_to_str(NewRev)}. + +hash_password(Password) -> + ?l2b(couch_util:to_hex(crypto:sha(iolist_to_binary([Password, ?SALT])))). + +shutdown_db(DbName) -> + {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), + ok = couch_db:close(AuthDb), + couch_util:shutdown_sync(AuthDb#db.main_pid), + ok = timer:sleep(1000). + +get_doc_rev(DbName, UserName) -> + DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]), + {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), + UpdateRev = + case couch_db:open_doc(AuthDb, DocId, []) of + {ok, Doc} -> + {Props} = couch_doc:to_json_obj(Doc, []), + couch_util:get_value(<<"_rev">>, Props); + {not_found, missing} -> + nil + end, + ok = couch_db:close(AuthDb), + {ok, UpdateRev}. + +get_user_doc_password_sha(DbName, UserName) -> + DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]), + {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), + {ok, Doc} = couch_db:open_doc(AuthDb, DocId, []), + ok = couch_db:close(AuthDb), + {Props} = couch_doc:to_json_obj(Doc, []), + couch_util:get_value(<<"password_sha">>, Props). + +delete_user_doc(DbName, UserName) -> + DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]), + {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), + {ok, Doc} = couch_db:open_doc(AuthDb, DocId, []), + {Props} = couch_doc:to_json_obj(Doc, []), + DeletedDoc = couch_doc:from_json_obj({[ + {<<"_id">>, DocId}, + {<<"_rev">>, couch_util:get_value(<<"_rev">>, Props)}, + {<<"_deleted">>, true} + ]}), + {ok, _} = couch_db:update_doc(AuthDb, DeletedDoc, []), + ok = couch_db:close(AuthDb). + +full_commit(DbName) -> + {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), + {ok, _} = couch_db:ensure_full_commit(AuthDb), + ok = couch_db:close(AuthDb). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/b3a05dcc/test/couch_btree_tests.erl ---------------------------------------------------------------------- diff --git a/test/couch_btree_tests.erl b/test/couch_btree_tests.erl new file mode 100644 index 0000000..911640f --- /dev/null +++ b/test/couch_btree_tests.erl @@ -0,0 +1,551 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_btree_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(ROWS, 1000). + + +setup() -> + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + {ok, Btree} = couch_btree:open(nil, Fd, [{compression, none}, + {reduce, fun reduce_fun/2}]), + {Fd, Btree}. + +setup_kvs(_) -> + setup(). + +setup_red() -> + {_, EvenOddKVs} = lists:foldl( + fun(Idx, {Key, Acc}) -> + case Key of + "even" -> {"odd", [{{Key, Idx}, 1} | Acc]}; + _ -> {"even", [{{Key, Idx}, 1} | Acc]} + end + end, {"odd", []}, lists:seq(1, ?ROWS)), + {Fd, Btree} = setup(), + {ok, Btree1} = couch_btree:add_remove(Btree, EvenOddKVs, []), + {Fd, Btree1}. +setup_red(_) -> + setup_red(). + +teardown(Fd) when is_pid(Fd) -> + ok = couch_file:close(Fd); +teardown({Fd, _}) -> + teardown(Fd). +teardown(_, {Fd, _}) -> + teardown(Fd). + + +kvs_test_funs() -> + [ + fun should_set_fd_correctly/2, + fun should_set_root_correctly/2, + fun should_create_zero_sized_btree/2, + fun should_set_reduce_option/2, + fun should_fold_over_empty_btree/2, + fun should_add_all_keys/2, + fun should_continuously_add_new_kv/2, + fun should_continuously_remove_keys/2, + fun should_insert_keys_in_reversed_order/2, + fun should_add_every_odd_key_remove_every_even/2, + fun should_add_every_even_key_remove_every_old/2 + ]. + +red_test_funs() -> + [ + fun should_reduce_whole_range/2, + fun should_reduce_first_half/2, + fun should_reduce_second_half/2 + ]. + + +btree_open_test_() -> + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + {ok, Btree} = couch_btree:open(nil, Fd, [{compression, none}]), + { + "Ensure that created btree is really a btree record", + ?_assert(is_record(Btree, btree)) + }. + +sorted_kvs_test_() -> + Funs = kvs_test_funs(), + Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, ?ROWS)], + { + "BTree with sorted keys", + { + foreachx, + fun setup_kvs/1, fun teardown/2, + [{Sorted, Fun} || Fun <- Funs] + } + }. + +rsorted_kvs_test_() -> + Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, ?ROWS)], + Funs = kvs_test_funs(), + Reversed = Sorted, + { + "BTree with backward sorted keys", + { + foreachx, + fun setup_kvs/1, fun teardown/2, + [{Reversed, Fun} || Fun <- Funs] + } + }. + +shuffled_kvs_test_() -> + Funs = kvs_test_funs(), + Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, ?ROWS)], + Shuffled = shuffle(Sorted), + { + "BTree with shuffled keys", + { + foreachx, + fun setup_kvs/1, fun teardown/2, + [{Shuffled, Fun} || Fun <- Funs] + } + }. + +reductions_test_() -> + { + "BTree reductions", + [ + { + "Common tests", + { + foreach, + fun setup_red/0, fun teardown/1, + [ + fun should_reduce_without_specified_direction/1, + fun should_reduce_forward/1, + fun should_reduce_backward/1 + ] + } + }, + { + "Range requests", + [ + { + "Forward direction", + { + foreachx, + fun setup_red/1, fun teardown/2, + [{fwd, F} || F <- red_test_funs()] + } + }, + { + "Backward direction", + { + foreachx, + fun setup_red/1, fun teardown/2, + [{rev, F} || F <- red_test_funs()] + } + } + ] + } + ] + }. + + +should_set_fd_correctly(_, {Fd, Btree}) -> + ?_assertMatch(Fd, Btree#btree.fd). + +should_set_root_correctly(_, {_, Btree}) -> + ?_assertMatch(nil, Btree#btree.root). + +should_create_zero_sized_btree(_, {_, Btree}) -> + ?_assertMatch(0, couch_btree:size(Btree)). + +should_set_reduce_option(_, {_, Btree}) -> + ReduceFun = fun reduce_fun/2, + Btree1 = couch_btree:set_options(Btree, [{reduce, ReduceFun}]), + ?_assertMatch(ReduceFun, Btree1#btree.reduce). + +should_fold_over_empty_btree(_, {_, Btree}) -> + {ok, _, EmptyRes} = couch_btree:foldl(Btree, fun(_, X) -> {ok, X+1} end, 0), + ?_assertEqual(EmptyRes, 0). + +should_add_all_keys(KeyValues, {Fd, Btree}) -> + {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), + [ + should_return_complete_btree_on_adding_all_keys(KeyValues, Btree1), + should_have_non_zero_size(Btree1), + should_have_lesser_size_than_file(Fd, Btree1), + should_keep_root_pointer_to_kp_node(Fd, Btree1), + should_remove_all_keys(KeyValues, Btree1) + ]. + +should_return_complete_btree_on_adding_all_keys(KeyValues, Btree) -> + ?_assert(test_btree(Btree, KeyValues)). + +should_have_non_zero_size(Btree) -> + ?_assert(couch_btree:size(Btree) > 0). + +should_have_lesser_size_than_file(Fd, Btree) -> + ?_assert((couch_btree:size(Btree) =< couch_file:bytes(Fd))). + +should_keep_root_pointer_to_kp_node(Fd, Btree) -> + ?_assertMatch({ok, {kp_node, _}}, + couch_file:pread_term(Fd, element(1, Btree#btree.root))). + +should_remove_all_keys(KeyValues, Btree) -> + Keys = keys(KeyValues), + {ok, Btree1} = couch_btree:add_remove(Btree, [], Keys), + { + "Should remove all the keys", + [ + should_produce_valid_btree(Btree1, []), + should_be_empty(Btree1) + ] + }. + +should_continuously_add_new_kv(KeyValues, {_, Btree}) -> + {Btree1, _} = lists:foldl( + fun(KV, {BtAcc, PrevSize}) -> + {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []), + ?assert(couch_btree:size(BtAcc2) > PrevSize), + {BtAcc2, couch_btree:size(BtAcc2)} + end, {Btree, couch_btree:size(Btree)}, KeyValues), + { + "Should continuously add key-values to btree", + [ + should_produce_valid_btree(Btree1, KeyValues), + should_not_be_empty(Btree1) + ] + }. + +should_continuously_remove_keys(KeyValues, {_, Btree}) -> + {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), + {Btree2, _} = lists:foldl( + fun({K, _}, {BtAcc, PrevSize}) -> + {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [], [K]), + ?assert(couch_btree:size(BtAcc2) < PrevSize), + {BtAcc2, couch_btree:size(BtAcc2)} + end, {Btree1, couch_btree:size(Btree1)}, KeyValues), + { + "Should continuously remove keys from btree", + [ + should_produce_valid_btree(Btree2, []), + should_be_empty(Btree2) + ] + }. + +should_insert_keys_in_reversed_order(KeyValues, {_, Btree}) -> + KeyValuesRev = lists:reverse(KeyValues), + {Btree1, _} = lists:foldl( + fun(KV, {BtAcc, PrevSize}) -> + {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []), + ?assert(couch_btree:size(BtAcc2) > PrevSize), + {BtAcc2, couch_btree:size(BtAcc2)} + end, {Btree, couch_btree:size(Btree)}, KeyValuesRev), + should_produce_valid_btree(Btree1, KeyValues). + +should_add_every_odd_key_remove_every_even(KeyValues, {_, Btree}) -> + {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), + {_, Rem2Keys0, Rem2Keys1} = lists:foldl(fun(X, {Count, Left, Right}) -> + case Count rem 2 == 0 of + true -> {Count + 1, [X | Left], Right}; + false -> {Count + 1, Left, [X | Right]} + end + end, {0, [], []}, KeyValues), + ?_assert(test_add_remove(Btree1, Rem2Keys0, Rem2Keys1)). + +should_add_every_even_key_remove_every_old(KeyValues, {_, Btree}) -> + {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), + {_, Rem2Keys0, Rem2Keys1} = lists:foldl(fun(X, {Count, Left, Right}) -> + case Count rem 2 == 0 of + true -> {Count + 1, [X | Left], Right}; + false -> {Count + 1, Left, [X | Right]} + end + end, {0, [], []}, KeyValues), + ?_assert(test_add_remove(Btree1, Rem2Keys1, Rem2Keys0)). + + +should_reduce_without_specified_direction({_, Btree}) -> + ?_assertMatch( + {ok, [{{"odd", _}, ?ROWS div 2}, {{"even", _}, ?ROWS div 2}]}, + fold_reduce(Btree, [])). + +should_reduce_forward({_, Btree}) -> + ?_assertMatch( + {ok, [{{"odd", _}, ?ROWS div 2}, {{"even", _}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, fwd}])). + +should_reduce_backward({_, Btree}) -> + ?_assertMatch( + {ok, [{{"even", _}, ?ROWS div 2}, {{"odd", _}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, rev}])). + +should_reduce_whole_range(fwd, {_, Btree}) -> + {SK, EK} = {{"even", 0}, {"odd", ?ROWS - 1}}, + [ + { + "include endkey", + ?_assertMatch( + {ok, [{{"odd", 1}, ?ROWS div 2}, + {{"even", 2}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, fwd}, + {start_key, SK}, + {end_key, EK}])) + }, + { + "exclude endkey", + ?_assertMatch( + {ok, [{{"odd", 1}, (?ROWS div 2) - 1}, + {{"even", 2}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, fwd}, + {start_key, SK}, + {end_key_gt, EK}])) + } + ]; +should_reduce_whole_range(rev, {_, Btree}) -> + {SK, EK} = {{"odd", ?ROWS - 1}, {"even", 2}}, + [ + { + "include endkey", + ?_assertMatch( + {ok, [{{"even", ?ROWS}, ?ROWS div 2}, + {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, rev}, + {start_key, SK}, + {end_key, EK}])) + }, + { + "exclude endkey", + ?_assertMatch( + {ok, [{{"even", ?ROWS}, (?ROWS div 2) - 1}, + {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, rev}, + {start_key, SK}, + {end_key_gt, EK}])) + } + ]. + +should_reduce_first_half(fwd, {_, Btree}) -> + {SK, EK} = {{"even", 0}, {"odd", (?ROWS div 2) - 1}}, + [ + { + "include endkey", + ?_assertMatch( + {ok, [{{"odd", 1}, ?ROWS div 4}, + {{"even", 2}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, fwd}, + {start_key, SK}, {end_key, EK}])) + }, + { + "exclude endkey", + ?_assertMatch( + {ok, [{{"odd", 1}, (?ROWS div 4) - 1}, + {{"even", 2}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, fwd}, + {start_key, SK}, + {end_key_gt, EK}])) + } + ]; +should_reduce_first_half(rev, {_, Btree}) -> + {SK, EK} = {{"odd", ?ROWS - 1}, {"even", ?ROWS div 2}}, + [ + { + "include endkey", + ?_assertMatch( + {ok, [{{"even", ?ROWS}, (?ROWS div 4) + 1}, + {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, rev}, + {start_key, SK}, + {end_key, EK}])) + }, + { + "exclude endkey", + ?_assertMatch( + {ok, [{{"even", ?ROWS}, ?ROWS div 4}, + {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, + fold_reduce(Btree, [{dir, rev}, + {start_key, SK}, + {end_key_gt, EK}])) + } + ]. + +should_reduce_second_half(fwd, {_, Btree}) -> + {SK, EK} = {{"even", ?ROWS div 2}, {"odd", ?ROWS - 1}}, + [ + { + "include endkey", + ?_assertMatch( + {ok, [{{"odd", 1}, ?ROWS div 2}, + {{"even", ?ROWS div 2}, (?ROWS div 4) + 1}]}, + fold_reduce(Btree, [{dir, fwd}, + {start_key, SK}, + {end_key, EK}])) + }, + { + "exclude endkey", + ?_assertMatch( + {ok, [{{"odd", 1}, (?ROWS div 2) - 1}, + {{"even", ?ROWS div 2}, (?ROWS div 4) + 1}]}, + fold_reduce(Btree, [{dir, fwd}, + {start_key, SK}, + {end_key_gt, EK}])) + } + ]; +should_reduce_second_half(rev, {_, Btree}) -> + {SK, EK} = {{"odd", (?ROWS div 2) + 1}, {"even", 2}}, + [ + { + "include endkey", + ?_assertMatch( + {ok, [{{"even", ?ROWS}, ?ROWS div 2}, + {{"odd", (?ROWS div 2) + 1}, (?ROWS div 4) + 1}]}, + fold_reduce(Btree, [{dir, rev}, + {start_key, SK}, + {end_key, EK}])) + }, + { + "exclude endkey", + ?_assertMatch( + {ok, [{{"even", ?ROWS}, (?ROWS div 2) - 1}, + {{"odd", (?ROWS div 2) + 1}, (?ROWS div 4) + 1}]}, + fold_reduce(Btree, [{dir, rev}, + {start_key, SK}, + {end_key_gt, EK}])) + } + ]. + +should_produce_valid_btree(Btree, KeyValues) -> + ?_assert(test_btree(Btree, KeyValues)). + +should_be_empty(Btree) -> + ?_assertEqual(couch_btree:size(Btree), 0). + +should_not_be_empty(Btree) -> + ?_assert(couch_btree:size(Btree) > 0). + +fold_reduce(Btree, Opts) -> + GroupFun = fun({K1, _}, {K2, _}) -> + K1 == K2 + end, + FoldFun = fun(GroupedKey, Unreduced, Acc) -> + {ok, [{GroupedKey, couch_btree:final_reduce(Btree, Unreduced)} | Acc]} + end, + couch_btree:fold_reduce(Btree, FoldFun, [], + [{key_group_fun, GroupFun}] ++ Opts). + + +keys(KVs) -> + [K || {K, _} <- KVs]. + +reduce_fun(reduce, KVs) -> + length(KVs); +reduce_fun(rereduce, Reds) -> + lists:sum(Reds). + + +shuffle(List) -> + randomize(round(math:log(length(List)) + 0.5), List). + +randomize(1, List) -> + randomize(List); +randomize(T, List) -> + lists:foldl( + fun(_E, Acc) -> + randomize(Acc) + end, randomize(List), lists:seq(1, (T - 1))). + +randomize(List) -> + D = lists:map(fun(A) -> {random:uniform(), A} end, List), + {_, D1} = lists:unzip(lists:keysort(1, D)), + D1. + +test_btree(Btree, KeyValues) -> + ok = test_key_access(Btree, KeyValues), + ok = test_lookup_access(Btree, KeyValues), + ok = test_final_reductions(Btree, KeyValues), + ok = test_traversal_callbacks(Btree, KeyValues), + true. + +test_add_remove(Btree, OutKeyValues, RemainingKeyValues) -> + Btree2 = lists:foldl( + fun({K, _}, BtAcc) -> + {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [], [K]), + BtAcc2 + end, Btree, OutKeyValues), + true = test_btree(Btree2, RemainingKeyValues), + + Btree3 = lists:foldl( + fun(KV, BtAcc) -> + {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []), + BtAcc2 + end, Btree2, OutKeyValues), + true = test_btree(Btree3, OutKeyValues ++ RemainingKeyValues). + +test_key_access(Btree, List) -> + FoldFun = fun(Element, {[HAcc|TAcc], Count}) -> + case Element == HAcc of + true -> {ok, {TAcc, Count + 1}}; + _ -> {ok, {TAcc, Count + 1}} + end + end, + Length = length(List), + Sorted = lists:sort(List), + {ok, _, {[], Length}} = couch_btree:foldl(Btree, FoldFun, {Sorted, 0}), + {ok, _, {[], Length}} = couch_btree:fold(Btree, FoldFun, + {Sorted, 0}, [{dir, rev}]), + ok. + +test_lookup_access(Btree, KeyValues) -> + FoldFun = fun({Key, Value}, {Key, Value}) -> {stop, true} end, + lists:foreach( + fun({Key, Value}) -> + [{ok, {Key, Value}}] = couch_btree:lookup(Btree, [Key]), + {ok, _, true} = couch_btree:foldl(Btree, FoldFun, + {Key, Value}, [{start_key, Key}]) + end, KeyValues). + +test_final_reductions(Btree, KeyValues) -> + KVLen = length(KeyValues), + FoldLFun = fun(_X, LeadingReds, Acc) -> + CountToStart = KVLen div 3 + Acc, + CountToStart = couch_btree:final_reduce(Btree, LeadingReds), + {ok, Acc + 1} + end, + FoldRFun = fun(_X, LeadingReds, Acc) -> + CountToEnd = KVLen - KVLen div 3 + Acc, + CountToEnd = couch_btree:final_reduce(Btree, LeadingReds), + {ok, Acc + 1} + end, + {LStartKey, _} = case KVLen of + 0 -> {nil, nil}; + _ -> lists:nth(KVLen div 3 + 1, lists:sort(KeyValues)) + end, + {RStartKey, _} = case KVLen of + 0 -> {nil, nil}; + _ -> lists:nth(KVLen div 3, lists:sort(KeyValues)) + end, + {ok, _, FoldLRed} = couch_btree:foldl(Btree, FoldLFun, 0, + [{start_key, LStartKey}]), + {ok, _, FoldRRed} = couch_btree:fold(Btree, FoldRFun, 0, + [{dir, rev}, {start_key, RStartKey}]), + KVLen = FoldLRed + FoldRRed, + ok. + +test_traversal_callbacks(Btree, _KeyValues) -> + FoldFun = fun + (visit, _GroupedKey, _Unreduced, Acc) -> + {ok, Acc andalso false}; + (traverse, _LK, _Red, Acc) -> + {skip, Acc andalso true} + end, + % With 250 items the root is a kp. Always skipping should reduce to true. + {ok, _, true} = couch_btree:fold(Btree, FoldFun, true, [{dir, fwd}]), + ok. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/b3a05dcc/test/couch_changes_tests.erl ---------------------------------------------------------------------- diff --git a/test/couch_changes_tests.erl b/test/couch_changes_tests.erl new file mode 100644 index 0000000..a129ba2 --- /dev/null +++ b/test/couch_changes_tests.erl @@ -0,0 +1,612 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_changes_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles = [<<"_admin">>]}}). +-define(TIMEOUT, 3000). +-define(TEST_TIMEOUT, 10000). + +-record(row, { + id, + seq, + deleted = false +}). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + Pid. + +stop(Pid) -> + erlang:monitor(process, Pid), + couch_server_sup:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + DbName = ?tempdb(), + {ok, Db} = create_db(DbName), + Revs = [R || {ok, R} <- [ + save_doc(Db, {[{<<"_id">>, <<"doc1">>}]}), + save_doc(Db, {[{<<"_id">>, <<"doc2">>}]}), + save_doc(Db, {[{<<"_id">>, <<"doc3">>}]}), + save_doc(Db, {[{<<"_id">>, <<"doc4">>}]}), + save_doc(Db, {[{<<"_id">>, <<"doc5">>}]}) + ]], + Rev = lists:nth(3, Revs), + {ok, Rev1} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}, {<<"_rev">>, Rev}]}), + Revs1 = Revs ++ [Rev1], + Revs2 = Revs1 ++ [R || {ok, R} <- [ + save_doc(Db, {[{<<"_id">>, <<"doc6">>}]}), + save_doc(Db, {[{<<"_id">>, <<"_design/foo">>}]}), + save_doc(Db, {[{<<"_id">>, <<"doc7">>}]}), + save_doc(Db, {[{<<"_id">>, <<"doc8">>}]}) + ]], + {DbName, list_to_tuple(Revs2)}. + +teardown({DbName, _}) -> + delete_db(DbName), + ok. + + +changes_test_() -> + { + "Changes feeed", + { + setup, + fun start/0, fun stop/1, + [ + filter_by_doc_id(), + filter_by_design(), + continuous_feed(), + filter_by_custom_function() + ] + } + }. + +filter_by_doc_id() -> + { + "Filter _doc_id", + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_filter_by_specific_doc_ids/1, + fun should_filter_by_specific_doc_ids_descending/1, + fun should_filter_by_specific_doc_ids_with_since/1, + fun should_filter_by_specific_doc_ids_no_result/1, + fun should_handle_deleted_docs/1 + ] + } + }. + +filter_by_design() -> + { + "Filter _design", + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_emit_only_design_documents/1 + ] + } + }. + +filter_by_custom_function() -> + { + "Filter function", + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_receive_heartbeats/1 + ] + } + }. + +continuous_feed() -> + { + "Continuous Feed", + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_filter_continuous_feed_by_specific_doc_ids/1 + ] + } + }. + + +should_filter_by_specific_doc_ids({DbName, _}) -> + ?_test( + begin + ChangesArgs = #changes_args{ + filter = "_doc_ids" + }, + DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + Consumer = spawn_consumer(DbName, ChangesArgs, Req), + + {Rows, LastSeq} = wait_finished(Consumer), + {ok, Db} = couch_db:open_int(DbName, []), + UpSeq = couch_db:get_update_seq(Db), + couch_db:close(Db), + stop_consumer(Consumer), + + ?assertEqual(2, length(Rows)), + [#row{seq = Seq1, id = Id1}, #row{seq = Seq2, id = Id2}] = Rows, + ?assertEqual(<<"doc4">>, Id1), + ?assertEqual(4, Seq1), + ?assertEqual(<<"doc3">>, Id2), + ?assertEqual(6, Seq2), + ?assertEqual(UpSeq, LastSeq) + end). + +should_filter_by_specific_doc_ids_descending({DbName, _}) -> + ?_test( + begin + ChangesArgs = #changes_args{ + filter = "_doc_ids", + dir = rev + }, + DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + Consumer = spawn_consumer(DbName, ChangesArgs, Req), + + {Rows, LastSeq} = wait_finished(Consumer), + {ok, Db} = couch_db:open_int(DbName, []), + couch_db:close(Db), + stop_consumer(Consumer), + + ?assertEqual(2, length(Rows)), + [#row{seq = Seq1, id = Id1}, #row{seq = Seq2, id = Id2}] = Rows, + ?assertEqual(<<"doc3">>, Id1), + ?assertEqual(6, Seq1), + ?assertEqual(<<"doc4">>, Id2), + ?assertEqual(4, Seq2), + ?assertEqual(4, LastSeq) + end). + +should_filter_by_specific_doc_ids_with_since({DbName, _}) -> + ?_test( + begin + ChangesArgs = #changes_args{ + filter = "_doc_ids", + since = 5 + }, + DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + Consumer = spawn_consumer(DbName, ChangesArgs, Req), + + {Rows, LastSeq} = wait_finished(Consumer), + {ok, Db} = couch_db:open_int(DbName, []), + UpSeq = couch_db:get_update_seq(Db), + couch_db:close(Db), + stop_consumer(Consumer), + + ?assertEqual(1, length(Rows)), + [#row{seq = Seq1, id = Id1}] = Rows, + ?assertEqual(<<"doc3">>, Id1), + ?assertEqual(6, Seq1), + ?assertEqual(UpSeq, LastSeq) + end). + +should_filter_by_specific_doc_ids_no_result({DbName, _}) -> + ?_test( + begin + ChangesArgs = #changes_args{ + filter = "_doc_ids", + since = 6 + }, + DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + Consumer = spawn_consumer(DbName, ChangesArgs, Req), + + {Rows, LastSeq} = wait_finished(Consumer), + {ok, Db} = couch_db:open_int(DbName, []), + UpSeq = couch_db:get_update_seq(Db), + couch_db:close(Db), + stop_consumer(Consumer), + + ?assertEqual(0, length(Rows)), + ?assertEqual(UpSeq, LastSeq) + end). + +should_handle_deleted_docs({DbName, Revs}) -> + ?_test( + begin + Rev3_2 = element(6, Revs), + {ok, Db} = couch_db:open_int(DbName, []), + {ok, _} = save_doc( + Db, + {[{<<"_id">>, <<"doc3">>}, + {<<"_deleted">>, true}, + {<<"_rev">>, Rev3_2}]}), + + ChangesArgs = #changes_args{ + filter = "_doc_ids", + since = 9 + }, + DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + Consumer = spawn_consumer(DbName, ChangesArgs, Req), + + {Rows, LastSeq} = wait_finished(Consumer), + couch_db:close(Db), + stop_consumer(Consumer), + + ?assertEqual(1, length(Rows)), + ?assertMatch( + [#row{seq = LastSeq, id = <<"doc3">>, deleted = true}], + Rows + ), + ?assertEqual(11, LastSeq) + end). + +should_filter_continuous_feed_by_specific_doc_ids({DbName, Revs}) -> + ?_test( + begin + {ok, Db} = couch_db:open_int(DbName, []), + ChangesArgs = #changes_args{ + filter = "_doc_ids", + feed = "continuous" + }, + DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + Consumer = spawn_consumer(DbName, ChangesArgs, Req), + pause(Consumer), + + Rows = get_rows(Consumer), + ?assertEqual(2, length(Rows)), + [#row{seq = Seq1, id = Id1}, #row{seq = Seq2, id = Id2}] = Rows, + ?assertEqual(<<"doc4">>, Id1), + ?assertEqual(4, Seq1), + ?assertEqual(<<"doc3">>, Id2), + ?assertEqual(6, Seq2), + + clear_rows(Consumer), + {ok, _Rev9} = save_doc(Db, {[{<<"_id">>, <<"doc9">>}]}), + {ok, _Rev10} = save_doc(Db, {[{<<"_id">>, <<"doc10">>}]}), + unpause(Consumer), + pause(Consumer), + ?assertEqual([], get_rows(Consumer)), + + Rev4 = element(4, Revs), + Rev3_2 = element(6, Revs), + {ok, Rev4_2} = save_doc(Db, {[{<<"_id">>, <<"doc4">>}, + {<<"_rev">>, Rev4}]}), + {ok, _} = save_doc(Db, {[{<<"_id">>, <<"doc11">>}]}), + {ok, _} = save_doc(Db, {[{<<"_id">>, <<"doc4">>}, + {<<"_rev">>, Rev4_2}]}), + {ok, _} = save_doc(Db, {[{<<"_id">>, <<"doc12">>}]}), + {ok, Rev3_3} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}, + {<<"_rev">>, Rev3_2}]}), + unpause(Consumer), + pause(Consumer), + + NewRows = get_rows(Consumer), + ?assertEqual(2, length(NewRows)), + [Row14, Row16] = NewRows, + ?assertEqual(<<"doc4">>, Row14#row.id), + ?assertEqual(15, Row14#row.seq), + ?assertEqual(<<"doc3">>, Row16#row.id), + ?assertEqual(17, Row16#row.seq), + + clear_rows(Consumer), + {ok, _Rev3_4} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}, + {<<"_rev">>, Rev3_3}]}), + unpause(Consumer), + pause(Consumer), + + FinalRows = get_rows(Consumer), + + unpause(Consumer), + stop_consumer(Consumer), + + ?assertMatch([#row{seq = 18, id = <<"doc3">>}], FinalRows) + end). + +should_emit_only_design_documents({DbName, Revs}) -> + ?_test( + begin + ChangesArgs = #changes_args{ + filter = "_design" + }, + Consumer = spawn_consumer(DbName, ChangesArgs, {json_req, null}), + + {Rows, LastSeq} = wait_finished(Consumer), + {ok, Db} = couch_db:open_int(DbName, []), + UpSeq = couch_db:get_update_seq(Db), + couch_db:close(Db), + + ?assertEqual(1, length(Rows)), + ?assertEqual(UpSeq, LastSeq), + ?assertEqual([#row{seq = 8, id = <<"_design/foo">>}], Rows), + + stop_consumer(Consumer), + + {ok, Db2} = couch_db:open_int(DbName, [?ADMIN_USER]), + {ok, _} = save_doc(Db2, {[{<<"_id">>, <<"_design/foo">>}, + {<<"_rev">>, element(8, Revs)}, + {<<"_deleted">>, true}]}), + + Consumer2 = spawn_consumer(DbName, ChangesArgs, {json_req, null}), + + {Rows2, LastSeq2} = wait_finished(Consumer2), + UpSeq2 = UpSeq + 1, + couch_db:close(Db2), + + ?assertEqual(1, length(Rows2)), + ?assertEqual(UpSeq2, LastSeq2), + ?assertEqual([#row{seq = 11, + id = <<"_design/foo">>, + deleted = true}], + Rows2) + end). + +should_receive_heartbeats(_) -> + {timeout, ?TEST_TIMEOUT div 1000, + ?_test( + begin + DbName = ?tempdb(), + Timeout = 100, + {ok, Db} = create_db(DbName), + + {ok, _} = save_doc(Db, {[ + {<<"_id">>, <<"_design/filtered">>}, + {<<"language">>, <<"javascript">>}, + {<<"filters">>, {[ + {<<"foo">>, <<"function(doc) { + return ['doc10', 'doc11', 'doc12'].indexOf(doc._id) != -1;}">> + }]}} + ]}), + + ChangesArgs = #changes_args{ + filter = "filtered/foo", + feed = "continuous", + timeout = 10000, + heartbeat = 1000 + }, + Consumer = spawn_consumer(DbName, ChangesArgs, {json_req, null}), + + {ok, _Rev1} = save_doc(Db, {[{<<"_id">>, <<"doc1">>}]}), + timer:sleep(Timeout), + {ok, _Rev2} = save_doc(Db, {[{<<"_id">>, <<"doc2">>}]}), + timer:sleep(Timeout), + {ok, _Rev3} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}]}), + timer:sleep(Timeout), + {ok, _Rev4} = save_doc(Db, {[{<<"_id">>, <<"doc4">>}]}), + timer:sleep(Timeout), + {ok, _Rev5} = save_doc(Db, {[{<<"_id">>, <<"doc5">>}]}), + timer:sleep(Timeout), + {ok, _Rev6} = save_doc(Db, {[{<<"_id">>, <<"doc6">>}]}), + timer:sleep(Timeout), + {ok, _Rev7} = save_doc(Db, {[{<<"_id">>, <<"doc7">>}]}), + timer:sleep(Timeout), + {ok, _Rev8} = save_doc(Db, {[{<<"_id">>, <<"doc8">>}]}), + timer:sleep(Timeout), + {ok, _Rev9} = save_doc(Db, {[{<<"_id">>, <<"doc9">>}]}), + + Heartbeats = get_heartbeats(Consumer), + ?assert(Heartbeats > 0), + + {ok, _Rev10} = save_doc(Db, {[{<<"_id">>, <<"doc10">>}]}), + timer:sleep(Timeout), + {ok, _Rev11} = save_doc(Db, {[{<<"_id">>, <<"doc11">>}]}), + timer:sleep(Timeout), + {ok, _Rev12} = save_doc(Db, {[{<<"_id">>, <<"doc12">>}]}), + + Heartbeats2 = get_heartbeats(Consumer), + ?assert(Heartbeats2 > Heartbeats), + + Rows = get_rows(Consumer), + ?assertEqual(3, length(Rows)), + + {ok, _Rev13} = save_doc(Db, {[{<<"_id">>, <<"doc13">>}]}), + timer:sleep(Timeout), + {ok, _Rev14} = save_doc(Db, {[{<<"_id">>, <<"doc14">>}]}), + timer:sleep(Timeout), + + Heartbeats3 = get_heartbeats(Consumer), + ?assert(Heartbeats3 > Heartbeats2) + end)}. + + +save_doc(Db, Json) -> + Doc = couch_doc:from_json_obj(Json), + {ok, Rev} = couch_db:update_doc(Db, Doc, []), + {ok, couch_doc:rev_to_str(Rev)}. + +get_rows(Consumer) -> + Ref = make_ref(), + Consumer ! {get_rows, Ref}, + Resp = receive + {rows, Ref, Rows} -> + Rows + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +get_heartbeats(Consumer) -> + Ref = make_ref(), + Consumer ! {get_heartbeats, Ref}, + Resp = receive + {hearthbeats, Ref, HeartBeats} -> + HeartBeats + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +clear_rows(Consumer) -> + Ref = make_ref(), + Consumer ! {reset, Ref}, + Resp = receive + {ok, Ref} -> + ok + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +stop_consumer(Consumer) -> + Ref = make_ref(), + Consumer ! {stop, Ref}, + Resp = receive + {ok, Ref} -> + ok + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +pause(Consumer) -> + Ref = make_ref(), + Consumer ! {pause, Ref}, + Resp = receive + {paused, Ref} -> + ok + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +unpause(Consumer) -> + Ref = make_ref(), + Consumer ! {continue, Ref}, + Resp = receive + {ok, Ref} -> + ok + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +wait_finished(_Consumer) -> + Resp = receive + {consumer_finished, Rows, LastSeq} -> + {Rows, LastSeq} + after ?TIMEOUT -> + timeout + end, + ?assertNotEqual(timeout, Resp), + Resp. + +spawn_consumer(DbName, ChangesArgs0, Req) -> + Parent = self(), + spawn(fun() -> + put(heartbeat_count, 0), + Callback = fun + ({change, {Change}, _}, _, Acc) -> + Id = couch_util:get_value(<<"id">>, Change), + Seq = couch_util:get_value(<<"seq">>, Change), + Del = couch_util:get_value(<<"deleted">>, Change, false), + [#row{id = Id, seq = Seq, deleted = Del} | Acc]; + ({stop, LastSeq}, _, Acc) -> + Parent ! {consumer_finished, lists:reverse(Acc), LastSeq}, + stop_loop(Parent, Acc); + (timeout, _, Acc) -> + put(heartbeat_count, get(heartbeat_count) + 1), + maybe_pause(Parent, Acc); + (_, _, Acc) -> + maybe_pause(Parent, Acc) + end, + {ok, Db} = couch_db:open_int(DbName, []), + ChangesArgs = case (ChangesArgs0#changes_args.timeout =:= undefined) + andalso (ChangesArgs0#changes_args.heartbeat =:= undefined) of + true -> + ChangesArgs0#changes_args{timeout = 10, heartbeat = 10}; + false -> + ChangesArgs0 + end, + FeedFun = couch_changes:handle_changes(ChangesArgs, Req, Db), + try + FeedFun({Callback, []}) + catch throw:{stop, _} -> + ok + end, + catch couch_db:close(Db) + end). + +maybe_pause(Parent, Acc) -> + receive + {get_rows, Ref} -> + Parent ! {rows, Ref, lists:reverse(Acc)}, + maybe_pause(Parent, Acc); + {get_heartbeats, Ref} -> + Parent ! {hearthbeats, Ref, get(heartbeat_count)}, + maybe_pause(Parent, Acc); + {reset, Ref} -> + Parent ! {ok, Ref}, + maybe_pause(Parent, []); + {pause, Ref} -> + Parent ! {paused, Ref}, + pause_loop(Parent, Acc); + {stop, Ref} -> + Parent ! {ok, Ref}, + throw({stop, Acc}); + V -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {value, V}, + {reason, "Received unexpected message"}]}) + after 0 -> + Acc + end. + +pause_loop(Parent, Acc) -> + receive + {stop, Ref} -> + Parent ! {ok, Ref}, + throw({stop, Acc}); + {reset, Ref} -> + Parent ! {ok, Ref}, + pause_loop(Parent, []); + {continue, Ref} -> + Parent ! {ok, Ref}, + Acc; + {get_rows, Ref} -> + Parent ! {rows, Ref, lists:reverse(Acc)}, + pause_loop(Parent, Acc) + end. + +stop_loop(Parent, Acc) -> + receive + {get_rows, Ref} -> + Parent ! {rows, Ref, lists:reverse(Acc)}, + stop_loop(Parent, Acc); + {stop, Ref} -> + Parent ! {ok, Ref}, + Acc + end. + +create_db(DbName) -> + couch_db:create(DbName, [?ADMIN_USER, overwrite]). + +delete_db(DbName) -> + ok = couch_server:delete(DbName, [?ADMIN_USER]). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/b3a05dcc/test/couch_db_tests.erl ---------------------------------------------------------------------- diff --git a/test/couch_db_tests.erl b/test/couch_db_tests.erl new file mode 100644 index 0000000..3089714 --- /dev/null +++ b/test/couch_db_tests.erl @@ -0,0 +1,114 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_db_tests). + +-include("couch_eunit.hrl"). + +-define(TIMEOUT, 120). + + +setup() -> + {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), + couch_config:set("log", "include_sasl", "false", false), + ok. + +teardown(_) -> + couch_server_sup:stop(). + + +create_delete_db_test_()-> + { + "Database create/delete tests", + { + setup, + fun setup/0, fun teardown/1, + fun(_) -> + [should_create_db(), + should_delete_db(), + should_create_multiple_dbs(), + should_delete_multiple_dbs(), + should_create_delete_database_continuously()] + end + } + }. + + +should_create_db() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, []), + ok = couch_db:close(Db), + {ok, AllDbs} = couch_server:all_databases(), + ?_assert(lists:member(DbName, AllDbs)). + +should_delete_db() -> + DbName = ?tempdb(), + couch_db:create(DbName, []), + couch_server:delete(DbName, []), + {ok, AllDbs} = couch_server:all_databases(), + ?_assertNot(lists:member(DbName, AllDbs)). + +should_create_multiple_dbs() -> + gen_server:call(couch_server, {set_max_dbs_open, 3}), + + DbNames = [?tempdb() || _ <- lists:seq(1, 6)], + lists:foreach(fun(DbName) -> + {ok, Db} = couch_db:create(DbName, []), + ok = couch_db:close(Db) + end, DbNames), + + {ok, AllDbs} = couch_server:all_databases(), + NumCreated = lists:foldl(fun(DbName, Acc) -> + ?assert(lists:member(DbName, AllDbs)), + Acc+1 + end, 0, DbNames), + + ?_assertEqual(NumCreated, 6). + +should_delete_multiple_dbs() -> + DbNames = [?tempdb() || _ <- lists:seq(1, 6)], + lists:foreach(fun(DbName) -> + {ok, Db} = couch_db:create(DbName, []), + ok = couch_db:close(Db) + end, DbNames), + + lists:foreach(fun(DbName) -> + ok = couch_server:delete(DbName, []) + end, DbNames), + + {ok, AllDbs} = couch_server:all_databases(), + NumDeleted = lists:foldl(fun(DbName, Acc) -> + ?assertNot(lists:member(DbName, AllDbs)), + Acc + 1 + end, 0, DbNames), + + ?_assertEqual(NumDeleted, 6). + +should_create_delete_database_continuously() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, []), + couch_db:close(Db), + [{timeout, ?TIMEOUT, {integer_to_list(N) ++ " times", + ?_assert(loop(DbName, N))}} + || N <- [10, 100, 1000]]. + +loop(_, 0) -> + true; +loop(DbName, N) -> + ok = cycle(DbName), + loop(DbName, N - 1). + +cycle(DbName) -> + ok = couch_server:delete(DbName, []), + {ok, Db} = couch_db:create(DbName, []), + couch_db:close(Db), + ok. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/b3a05dcc/test/couch_doc_json_tests.erl ---------------------------------------------------------------------- diff --git a/test/couch_doc_json_tests.erl b/test/couch_doc_json_tests.erl new file mode 100644 index 0000000..1592b6b --- /dev/null +++ b/test/couch_doc_json_tests.erl @@ -0,0 +1,391 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_doc_json_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + + +setup() -> + couch_config:start_link(?CONFIG_CHAIN), + couch_config:set("attachments", "compression_level", "0", false), + ok. + +teardown(_) -> + couch_config:stop(). + + +json_doc_test_() -> + { + setup, + fun setup/0, fun teardown/1, + [ + { + "Document from JSON", + [ + from_json_success_cases(), + from_json_error_cases() + ] + }, + { + "Document to JSON", + [ + to_json_success_cases() + ] + } + ] + }. + +from_json_success_cases() -> + Cases = [ + { + {[]}, + #doc{}, + "Return an empty document for an empty JSON object." + }, + { + {[{<<"_id">>, <<"zing!">>}]}, + #doc{id = <<"zing!">>}, + "Parses document ids." + }, + { + {[{<<"_id">>, <<"_design/foo">>}]}, + #doc{id = <<"_design/foo">>}, + "_design/document ids." + }, + { + {[{<<"_id">>, <<"_local/bam">>}]}, + #doc{id = <<"_local/bam">>}, + "_local/document ids." + }, + { + {[{<<"_rev">>, <<"4-230234">>}]}, + #doc{revs = {4, [<<"230234">>]}}, + "_rev stored in revs." + }, + { + {[{<<"soap">>, 35}]}, + #doc{body = {[{<<"soap">>, 35}]}}, + "Non underscore prefixed fields stored in body." + }, + { + {[{<<"_attachments">>, {[ + {<<"my_attachment.fu">>, {[ + {<<"stub">>, true}, + {<<"content_type">>, <<"application/awesome">>}, + {<<"length">>, 45} + ]}}, + {<<"noahs_private_key.gpg">>, {[ + {<<"data">>, <<"SSBoYXZlIGEgcGV0IGZpc2gh">>}, + {<<"content_type">>, <<"application/pgp-signature">>} + ]}} + ]}}]}, + #doc{atts = [ + #att{ + name = <<"my_attachment.fu">>, + data = stub, + type = <<"application/awesome">>, + att_len = 45, + disk_len = 45, + revpos = nil + }, + #att{ + name = <<"noahs_private_key.gpg">>, + data = <<"I have a pet fish!">>, + type = <<"application/pgp-signature">>, + att_len = 18, + disk_len = 18, + revpos = 0 + } + ]}, + "Attachments are parsed correctly." + }, + { + {[{<<"_deleted">>, true}]}, + #doc{deleted = true}, + "_deleted controls the deleted field." + }, + { + {[{<<"_deleted">>, false}]}, + #doc{}, + "{\"_deleted\": false} is ok." + }, + { + {[ + {<<"_revisions">>, + {[{<<"start">>, 4}, + {<<"ids">>, [<<"foo1">>, <<"phi3">>, <<"omega">>]}]}}, + {<<"_rev">>, <<"6-something">>} + ]}, + #doc{revs = {4, [<<"foo1">>, <<"phi3">>, <<"omega">>]}}, + "_revisions attribute are preferred to _rev." + }, + { + {[{<<"_revs_info">>, dropping}]}, + #doc{}, + "Drops _revs_info." + }, + { + {[{<<"_local_seq">>, dropping}]}, + #doc{}, + "Drops _local_seq." + }, + { + {[{<<"_conflicts">>, dropping}]}, + #doc{}, + "Drops _conflicts." + }, + { + {[{<<"_deleted_conflicts">>, dropping}]}, + #doc{}, + "Drops _deleted_conflicts." + } + ], + lists:map( + fun({EJson, Expect, Msg}) -> + {Msg, ?_assertMatch(Expect, couch_doc:from_json_obj(EJson))} + end, + Cases). + +from_json_error_cases() -> + Cases = [ + { + [], + {bad_request, "Document must be a JSON object"}, + "arrays are invalid" + }, + { + 4, + {bad_request, "Document must be a JSON object"}, + "integers are invalid" + }, + { + true, + {bad_request, "Document must be a JSON object"}, + "literals are invalid" + }, + { + {[{<<"_id">>, {[{<<"foo">>, 5}]}}]}, + {bad_request, <<"Document id must be a string">>}, + "Document id must be a string." + }, + { + {[{<<"_id">>, <<"_random">>}]}, + {bad_request, + <<"Only reserved document ids may start with underscore.">>}, + "Disallow arbitrary underscore prefixed docids." + }, + { + {[{<<"_rev">>, 5}]}, + {bad_request, <<"Invalid rev format">>}, + "_rev must be a string" + }, + { + {[{<<"_rev">>, "foobar"}]}, + {bad_request, <<"Invalid rev format">>}, + "_rev must be %d-%s" + }, + { + {[{<<"_rev">>, "foo-bar"}]}, + "Error if _rev's integer expection is broken." + }, + { + {[{<<"_revisions">>, {[{<<"start">>, true}]}}]}, + {doc_validation, "_revisions.start isn't an integer."}, + "_revisions.start must be an integer." + }, + { + {[{<<"_revisions">>, {[{<<"start">>, 0}, {<<"ids">>, 5}]}}]}, + {doc_validation, "_revisions.ids isn't a array."}, + "_revions.ids must be a list." + }, + { + {[{<<"_revisions">>, {[{<<"start">>, 0}, {<<"ids">>, [5]}]}}]}, + {doc_validation, "RevId isn't a string"}, + "Revision ids must be strings." + }, + { + {[{<<"_something">>, 5}]}, + {doc_validation, <<"Bad special document member: _something">>}, + "Underscore prefix fields are reserved." + } + ], + + lists:map(fun + ({EJson, Expect, Msg}) -> + Error = (catch couch_doc:from_json_obj(EJson)), + {Msg, ?_assertMatch(Expect, Error)}; + ({EJson, Msg}) -> + try + couch_doc:from_json_obj(EJson), + {"Conversion failed to raise an exception", ?_assert(false)} + catch + _:_ -> {Msg, ?_assert(true)} + end + end, Cases). + +to_json_success_cases() -> + Cases = [ + { + #doc{}, + {[{<<"_id">>, <<"">>}]}, + "Empty docs are {\"_id\": \"\"}" + }, + { + #doc{id = <<"foo">>}, + {[{<<"_id">>, <<"foo">>}]}, + "_id is added." + }, + { + #doc{revs = {5, ["foo"]}}, + {[{<<"_id">>, <<>>}, {<<"_rev">>, <<"5-foo">>}]}, + "_rev is added." + }, + { + [revs], + #doc{revs = {5, [<<"first">>, <<"second">>]}}, + {[ + {<<"_id">>, <<>>}, + {<<"_rev">>, <<"5-first">>}, + {<<"_revisions">>, {[ + {<<"start">>, 5}, + {<<"ids">>, [<<"first">>, <<"second">>]} + ]}} + ]}, + "_revisions include with revs option" + }, + { + #doc{body = {[{<<"foo">>, <<"bar">>}]}}, + {[{<<"_id">>, <<>>}, {<<"foo">>, <<"bar">>}]}, + "Arbitrary fields are added." + }, + { + #doc{deleted = true, body = {[{<<"foo">>, <<"bar">>}]}}, + {[{<<"_id">>, <<>>}, {<<"foo">>, <<"bar">>}, {<<"_deleted">>, true}]}, + "Deleted docs no longer drop body members." + }, + { + #doc{meta = [ + {revs_info, 4, [{<<"fin">>, deleted}, {<<"zim">>, missing}]} + ]}, + {[ + {<<"_id">>, <<>>}, + {<<"_revs_info">>, [ + {[{<<"rev">>, <<"4-fin">>}, {<<"status">>, <<"deleted">>}]}, + {[{<<"rev">>, <<"3-zim">>}, {<<"status">>, <<"missing">>}]} + ]} + ]}, + "_revs_info field is added correctly." + }, + { + #doc{meta = [{local_seq, 5}]}, + {[{<<"_id">>, <<>>}, {<<"_local_seq">>, 5}]}, + "_local_seq is added as an integer." + }, + { + #doc{meta = [{conflicts, [{3, <<"yep">>}, {1, <<"snow">>}]}]}, + {[ + {<<"_id">>, <<>>}, + {<<"_conflicts">>, [<<"3-yep">>, <<"1-snow">>]} + ]}, + "_conflicts is added as an array of strings." + }, + { + #doc{meta = [{deleted_conflicts, [{10923, <<"big_cowboy_hat">>}]}]}, + {[ + {<<"_id">>, <<>>}, + {<<"_deleted_conflicts">>, [<<"10923-big_cowboy_hat">>]} + ]}, + "_deleted_conflicsts is added as an array of strings." + }, + { + #doc{atts = [ + #att{ + name = <<"big.xml">>, + type = <<"xml/sucks">>, + data = fun() -> ok end, + revpos = 1, + att_len = 400, + disk_len = 400 + }, + #att{ + name = <<"fast.json">>, + type = <<"json/ftw">>, + data = <<"{\"so\": \"there!\"}">>, + revpos = 1, + att_len = 16, + disk_len = 16 + } + ]}, + {[ + {<<"_id">>, <<>>}, + {<<"_attachments">>, {[ + {<<"big.xml">>, {[ + {<<"content_type">>, <<"xml/sucks">>}, + {<<"revpos">>, 1}, + {<<"length">>, 400}, + {<<"stub">>, true} + ]}}, + {<<"fast.json">>, {[ + {<<"content_type">>, <<"json/ftw">>}, + {<<"revpos">>, 1}, + {<<"length">>, 16}, + {<<"stub">>, true} + ]}} + ]}} + ]}, + "Attachments attached as stubs only include a length." + }, + { + [attachments], + #doc{atts = [ + #att{ + name = <<"stuff.txt">>, + type = <<"text/plain">>, + data = fun() -> <<"diet pepsi">> end, + revpos = 1, + att_len = 10, + disk_len = 10 + }, + #att{ + name = <<"food.now">>, + type = <<"application/food">>, + revpos = 1, + data = <<"sammich">> + } + ]}, + {[ + {<<"_id">>, <<>>}, + {<<"_attachments">>, {[ + {<<"stuff.txt">>, {[ + {<<"content_type">>, <<"text/plain">>}, + {<<"revpos">>, 1}, + {<<"data">>, <<"ZGlldCBwZXBzaQ==">>} + ]}}, + {<<"food.now">>, {[ + {<<"content_type">>, <<"application/food">>}, + {<<"revpos">>, 1}, + {<<"data">>, <<"c2FtbWljaA==">>} + ]}} + ]}} + ]}, + "Attachments included inline with attachments option." + } + ], + + lists:map(fun + ({Doc, EJson, Msg}) -> + {Msg, ?_assertMatch(EJson, couch_doc:to_json_obj(Doc, []))}; + ({Options, Doc, EJson, Msg}) -> + {Msg, ?_assertMatch(EJson, couch_doc:to_json_obj(Doc, Options))} + end, Cases). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/b3a05dcc/test/couch_file_tests.erl ---------------------------------------------------------------------- diff --git a/test/couch_file_tests.erl b/test/couch_file_tests.erl new file mode 100644 index 0000000..ad13383 --- /dev/null +++ b/test/couch_file_tests.erl @@ -0,0 +1,265 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_file_tests). + +-include("couch_eunit.hrl"). + +-define(BLOCK_SIZE, 4096). +-define(setup(F), {setup, fun setup/0, fun teardown/1, F}). +-define(foreach(Fs), {foreach, fun setup/0, fun teardown/1, Fs}). + + +setup() -> + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + Fd. + +teardown(Fd) -> + ok = couch_file:close(Fd). + + +open_close_test_() -> + { + "Test for proper file open and close", + [ + should_return_enoent_if_missed(), + should_ignore_invalid_flags_with_open(), + ?setup(fun should_return_pid_on_file_open/1), + should_close_file_properly(), + ?setup(fun should_create_empty_new_files/1) + ] + }. + +should_return_enoent_if_missed() -> + ?_assertEqual({error, enoent}, couch_file:open("not a real file")). + +should_ignore_invalid_flags_with_open() -> + ?_assertMatch({ok, _}, + couch_file:open(?tempfile(), [create, invalid_option])). + +should_return_pid_on_file_open(Fd) -> + ?_assert(is_pid(Fd)). + +should_close_file_properly() -> + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + ok = couch_file:close(Fd), + ?_assert(true). + +should_create_empty_new_files(Fd) -> + ?_assertMatch({ok, 0}, couch_file:bytes(Fd)). + + +read_write_test_() -> + { + "Common file read/write tests", + ?foreach([ + fun should_increase_file_size_on_write/1, + fun should_return_current_file_size_on_write/1, + fun should_write_and_read_term/1, + fun should_write_and_read_binary/1, + fun should_write_and_read_large_binary/1, + fun should_return_term_as_binary_for_reading_binary/1, + fun should_read_term_written_as_binary/1, + fun should_read_iolist/1, + fun should_fsync/1, + fun should_not_read_beyond_eof/1, + fun should_truncate/1 + ]) + }. + + +should_increase_file_size_on_write(Fd) -> + {ok, 0, _} = couch_file:append_term(Fd, foo), + {ok, Size} = couch_file:bytes(Fd), + ?_assert(Size > 0). + +should_return_current_file_size_on_write(Fd) -> + {ok, 0, _} = couch_file:append_term(Fd, foo), + {ok, Size} = couch_file:bytes(Fd), + ?_assertMatch({ok, Size, _}, couch_file:append_term(Fd, bar)). + +should_write_and_read_term(Fd) -> + {ok, Pos, _} = couch_file:append_term(Fd, foo), + ?_assertMatch({ok, foo}, couch_file:pread_term(Fd, Pos)). + +should_write_and_read_binary(Fd) -> + {ok, Pos, _} = couch_file:append_binary(Fd, <<"fancy!">>), + ?_assertMatch({ok, <<"fancy!">>}, couch_file:pread_binary(Fd, Pos)). + +should_return_term_as_binary_for_reading_binary(Fd) -> + {ok, Pos, _} = couch_file:append_term(Fd, foo), + Foo = couch_compress:compress(foo, snappy), + ?_assertMatch({ok, Foo}, couch_file:pread_binary(Fd, Pos)). + +should_read_term_written_as_binary(Fd) -> + {ok, Pos, _} = couch_file:append_binary(Fd, <<131,100,0,3,102,111,111>>), + ?_assertMatch({ok, foo}, couch_file:pread_term(Fd, Pos)). + +should_write_and_read_large_binary(Fd) -> + BigBin = list_to_binary(lists:duplicate(100000, 0)), + {ok, Pos, _} = couch_file:append_binary(Fd, BigBin), + ?_assertMatch({ok, BigBin}, couch_file:pread_binary(Fd, Pos)). + +should_read_iolist(Fd) -> + %% append_binary == append_iolist? + %% Possible bug in pread_iolist or iolist() -> append_binary + {ok, Pos, _} = couch_file:append_binary(Fd, ["foo", $m, <<"bam">>]), + {ok, IoList} = couch_file:pread_iolist(Fd, Pos), + ?_assertMatch(<<"foombam">>, iolist_to_binary(IoList)). + +should_fsync(Fd) -> + {"How does on test fsync?", ?_assertMatch(ok, couch_file:sync(Fd))}. + +should_not_read_beyond_eof(_) -> + {"No idea how to test reading beyond EOF", ?_assert(true)}. + +should_truncate(Fd) -> + {ok, 0, _} = couch_file:append_term(Fd, foo), + {ok, Size} = couch_file:bytes(Fd), + BigBin = list_to_binary(lists:duplicate(100000, 0)), + {ok, _, _} = couch_file:append_binary(Fd, BigBin), + ok = couch_file:truncate(Fd, Size), + ?_assertMatch({ok, foo}, couch_file:pread_term(Fd, 0)). + + +header_test_() -> + { + "File header read/write tests", + [ + ?foreach([ + fun should_write_and_read_atom_header/1, + fun should_write_and_read_tuple_header/1, + fun should_write_and_read_second_header/1, + fun should_truncate_second_header/1, + fun should_produce_same_file_size_on_rewrite/1, + fun should_save_headers_larger_than_block_size/1 + ]), + should_recover_header_marker_corruption(), + should_recover_header_size_corruption(), + should_recover_header_md5sig_corruption(), + should_recover_header_data_corruption() + ] + }. + + +should_write_and_read_atom_header(Fd) -> + ok = couch_file:write_header(Fd, hello), + ?_assertMatch({ok, hello}, couch_file:read_header(Fd)). + +should_write_and_read_tuple_header(Fd) -> + ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), + ?_assertMatch({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd)). + +should_write_and_read_second_header(Fd) -> + ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), + ok = couch_file:write_header(Fd, [foo, <<"more">>]), + ?_assertMatch({ok, [foo, <<"more">>]}, couch_file:read_header(Fd)). + +should_truncate_second_header(Fd) -> + ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), + {ok, Size} = couch_file:bytes(Fd), + ok = couch_file:write_header(Fd, [foo, <<"more">>]), + ok = couch_file:truncate(Fd, Size), + ?_assertMatch({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd)). + +should_produce_same_file_size_on_rewrite(Fd) -> + ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), + {ok, Size1} = couch_file:bytes(Fd), + ok = couch_file:write_header(Fd, [foo, <<"more">>]), + {ok, Size2} = couch_file:bytes(Fd), + ok = couch_file:truncate(Fd, Size1), + ok = couch_file:write_header(Fd, [foo, <<"more">>]), + ?_assertMatch({ok, Size2}, couch_file:bytes(Fd)). + +should_save_headers_larger_than_block_size(Fd) -> + Header = erlang:make_tuple(5000, <<"CouchDB">>), + couch_file:write_header(Fd, Header), + {"COUCHDB-1319", ?_assertMatch({ok, Header}, couch_file:read_header(Fd))}. + + +should_recover_header_marker_corruption() -> + ?_assertMatch( + ok, + check_header_recovery( + fun(CouchFd, RawFd, Expect, HeaderPos) -> + ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), + file:pwrite(RawFd, HeaderPos, <<0>>), + ?assertMatch(Expect, couch_file:read_header(CouchFd)) + end) + ). + +should_recover_header_size_corruption() -> + ?_assertMatch( + ok, + check_header_recovery( + fun(CouchFd, RawFd, Expect, HeaderPos) -> + ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), + % +1 for 0x1 byte marker + file:pwrite(RawFd, HeaderPos + 1, <<10/integer>>), + ?assertMatch(Expect, couch_file:read_header(CouchFd)) + end) + ). + +should_recover_header_md5sig_corruption() -> + ?_assertMatch( + ok, + check_header_recovery( + fun(CouchFd, RawFd, Expect, HeaderPos) -> + ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), + % +5 = +1 for 0x1 byte and +4 for term size. + file:pwrite(RawFd, HeaderPos + 5, <<"F01034F88D320B22">>), + ?assertMatch(Expect, couch_file:read_header(CouchFd)) + end) + ). + +should_recover_header_data_corruption() -> + ?_assertMatch( + ok, + check_header_recovery( + fun(CouchFd, RawFd, Expect, HeaderPos) -> + ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), + % +21 = +1 for 0x1 byte, +4 for term size and +16 for MD5 sig + file:pwrite(RawFd, HeaderPos + 21, <<"some data goes here!">>), + ?assertMatch(Expect, couch_file:read_header(CouchFd)) + end) + ). + + +check_header_recovery(CheckFun) -> + Path = ?tempfile(), + {ok, Fd} = couch_file:open(Path, [create, overwrite]), + {ok, RawFd} = file:open(Path, [read, write, raw, binary]), + + {ok, _} = write_random_data(Fd), + ExpectHeader = {some_atom, <<"a binary">>, 756}, + ok = couch_file:write_header(Fd, ExpectHeader), + + {ok, HeaderPos} = write_random_data(Fd), + ok = couch_file:write_header(Fd, {2342, <<"corruption! greed!">>}), + + CheckFun(Fd, RawFd, {ok, ExpectHeader}, HeaderPos), + + ok = file:close(RawFd), + ok = couch_file:close(Fd), + ok. + +write_random_data(Fd) -> + write_random_data(Fd, 100 + random:uniform(1000)). + +write_random_data(Fd, 0) -> + {ok, Bytes} = couch_file:bytes(Fd), + {ok, (1 + Bytes div ?BLOCK_SIZE) * ?BLOCK_SIZE}; +write_random_data(Fd, N) -> + Choices = [foo, bar, <<"bizzingle">>, "bank", ["rough", stuff]], + Term = lists:nth(random:uniform(4) + 1, Choices), + {ok, _, _} = couch_file:append_term(Fd, Term), + write_random_data(Fd, N - 1).