http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_update_conflicts_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_update_conflicts_tests.erl b/test/couchdb_update_conflicts_tests.erl new file mode 100644 index 0000000..7226860 --- /dev/null +++ b/test/couchdb_update_conflicts_tests.erl @@ -0,0 +1,243 @@ +% 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(couchdb_update_conflicts_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(i2l(I), integer_to_list(I)). +-define(ADMIN_USER, {userctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(DOC_ID, <<"foobar">>). +-define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]). +-define(TIMEOUT, 10000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + couch_config:set("couchdb", "delayed_commits", "true", false), + 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} = couch_db:create(DbName, [?ADMIN_USER, overwrite]), + Doc = couch_doc:from_json_obj({[{<<"_id">>, ?DOC_ID}, + {<<"value">>, 0}]}), + {ok, Rev} = couch_db:update_doc(Db, Doc, []), + ok = couch_db:close(Db), + RevStr = couch_doc:rev_to_str(Rev), + {DbName, RevStr}. +setup(_) -> + setup(). + +teardown({DbName, _}) -> + ok = couch_server:delete(DbName, []), + ok. +teardown(_, {DbName, _RevStr}) -> + teardown({DbName, _RevStr}). + + +view_indexes_cleanup_test_() -> + { + "Update conflicts", + { + setup, + fun start/0, fun stop/1, + [ + concurrent_updates(), + couchdb_188() + ] + } + }. + +concurrent_updates()-> + { + "Concurrent updates", + { + foreachx, + fun setup/1, fun teardown/2, + [{NumClients, fun should_concurrently_update_doc/2} + || NumClients <- ?NUM_CLIENTS] + } + }. + +couchdb_188()-> + { + "COUCHDB-188", + { + foreach, + fun setup/0, fun teardown/1, + [fun should_bulk_create_delete_doc/1] + } + }. + + +should_concurrently_update_doc(NumClients, {DbName, InitRev})-> + {?i2l(NumClients) ++ " clients", + {inorder, + [{"update doc", + {timeout, ?TIMEOUT div 1000, + ?_test(concurrent_doc_update(NumClients, DbName, InitRev))}}, + {"ensure in single leaf", + ?_test(ensure_in_single_revision_leaf(DbName))}]}}. + +should_bulk_create_delete_doc({DbName, InitRev})-> + ?_test(bulk_delete_create(DbName, InitRev)). + + +concurrent_doc_update(NumClients, DbName, InitRev) -> + Clients = lists:map( + fun(Value) -> + ClientDoc = couch_doc:from_json_obj({[ + {<<"_id">>, ?DOC_ID}, + {<<"_rev">>, InitRev}, + {<<"value">>, Value} + ]}), + Pid = spawn_client(DbName, ClientDoc), + {Value, Pid, erlang:monitor(process, Pid)} + end, + lists:seq(1, NumClients)), + + lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients), + + {NumConflicts, SavedValue} = lists:foldl( + fun({Value, Pid, MonRef}, {AccConflicts, AccValue}) -> + receive + {'DOWN', MonRef, process, Pid, {ok, _NewRev}} -> + {AccConflicts, Value}; + {'DOWN', MonRef, process, Pid, conflict} -> + {AccConflicts + 1, AccValue}; + {'DOWN', MonRef, process, Pid, Error} -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Client " ++ ?i2l(Value) + ++ " got update error: " + ++ couch_util:to_list(Error)}]}) + after ?TIMEOUT div 2 -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Timeout waiting for client " + ++ ?i2l(Value) ++ " to die"}]}) + end + end, {0, nil}, Clients), + ?assertEqual(NumClients - 1, NumConflicts), + + {ok, Db} = couch_db:open_int(DbName, []), + {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []), + ok = couch_db:close(Db), + ?assertEqual(1, length(Leaves)), + + [{ok, Doc2}] = Leaves, + {JsonDoc} = couch_doc:to_json_obj(Doc2, []), + ?assertEqual(SavedValue, couch_util:get_value(<<"value">>, JsonDoc)). + +ensure_in_single_revision_leaf(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []), + ok = couch_db:close(Db), + [{ok, Doc}] = Leaves, + + %% FIXME: server restart won't work from test side + %% stop(ok), + %% start(), + + {ok, Db2} = couch_db:open_int(DbName, []), + {ok, Leaves2} = couch_db:open_doc_revs(Db2, ?DOC_ID, all, []), + ok = couch_db:close(Db2), + ?assertEqual(1, length(Leaves2)), + + [{ok, Doc2}] = Leaves, + ?assertEqual(Doc, Doc2). + +bulk_delete_create(DbName, InitRev) -> + {ok, Db} = couch_db:open_int(DbName, []), + + DeletedDoc = couch_doc:from_json_obj({[ + {<<"_id">>, ?DOC_ID}, + {<<"_rev">>, InitRev}, + {<<"_deleted">>, true} + ]}), + NewDoc = couch_doc:from_json_obj({[ + {<<"_id">>, ?DOC_ID}, + {<<"value">>, 666} + ]}), + + {ok, Results} = couch_db:update_docs(Db, [DeletedDoc, NewDoc], []), + ok = couch_db:close(Db), + + ?assertEqual(2, length([ok || {ok, _} <- Results])), + [{ok, Rev1}, {ok, Rev2}] = Results, + + {ok, Db2} = couch_db:open_int(DbName, []), + {ok, [{ok, Doc1}]} = couch_db:open_doc_revs( + Db2, ?DOC_ID, [Rev1], [conflicts, deleted_conflicts]), + {ok, [{ok, Doc2}]} = couch_db:open_doc_revs( + Db2, ?DOC_ID, [Rev2], [conflicts, deleted_conflicts]), + ok = couch_db:close(Db2), + + {Doc1Props} = couch_doc:to_json_obj(Doc1, []), + {Doc2Props} = couch_doc:to_json_obj(Doc2, []), + + %% Document was deleted + ?assert(couch_util:get_value(<<"_deleted">>, Doc1Props)), + %% New document not flagged as deleted + ?assertEqual(undefined, couch_util:get_value(<<"_deleted">>, + Doc2Props)), + %% New leaf revision has the right value + ?assertEqual(666, couch_util:get_value(<<"value">>, + Doc2Props)), + %% Deleted document has no conflicts + ?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>, + Doc1Props)), + %% Deleted document has no deleted conflicts + ?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>, + Doc1Props)), + %% New leaf revision doesn't have conflicts + ?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>, + Doc1Props)), + %% New leaf revision doesn't have deleted conflicts + ?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>, + Doc1Props)), + + %% Deleted revision has position 2 + ?assertEqual(2, element(1, Rev1)), + %% New leaf revision has position 1 + ?assertEqual(1, element(1, Rev2)). + + +spawn_client(DbName, Doc) -> + spawn(fun() -> + {ok, Db} = couch_db:open_int(DbName, []), + receive + go -> ok + end, + erlang:yield(), + Result = try + couch_db:update_doc(Db, Doc, []) + catch _:Error -> + Error + end, + ok = couch_db:close(Db), + exit(Result) + end).
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_vhosts_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_vhosts_tests.erl b/test/couchdb_vhosts_tests.erl new file mode 100644 index 0000000..94b1957 --- /dev/null +++ b/test/couchdb_vhosts_tests.erl @@ -0,0 +1,441 @@ +% 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(couchdb_vhosts_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(TIMEOUT, 1000). +-define(iofmt(S, A), lists:flatten(io_lib:format(S, A))). + + +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} = couch_db:create(DbName, [?ADMIN_USER]), + Doc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"doc1">>}, + {<<"value">>, 666} + ]}), + + Doc1 = couch_doc:from_json_obj({[ + {<<"_id">>, <<"_design/doc1">>}, + {<<"shows">>, {[ + {<<"test">>, <<"function(doc, req) { + return { json: { + requested_path: '/' + req.requested_path.join('/'), + path: '/' + req.path.join('/')}};}">>} + ]}}, + {<<"rewrites">>, [ + {[ + {<<"from">>, <<"/">>}, + {<<"to">>, <<"_show/test">>} + ]} + ]} + ]}), + {ok, _} = couch_db:update_docs(Db, [Doc, Doc1]), + couch_db:ensure_full_commit(Db), + couch_db:close(Db), + + Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + Url = "http://" ++ Addr ++ ":" ++ Port, + {Url, ?b2l(DbName)}. + +setup_oauth() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), + + couch_config:set("couch_httpd_auth", "authentication_db", + ?b2l(?tempdb()), false), + couch_config:set("oauth_token_users", "otoksec1", "joe", false), + couch_config:set("oauth_consumer_secrets", "consec1", "foo", false), + couch_config:set("oauth_token_secrets", "otoksec1", "foobar", false), + couch_config:set("couch_httpd_auth", "require_valid_user", "true", false), + + ok = couch_config:set( + "vhosts", "oauth-example.com", + "/" ++ ?b2l(DbName) ++ "/_design/test/_rewrite/foobar", false), + + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"_design/test">>}, + {<<"language">>, <<"javascript">>}, + {<<"rewrites">>, [ + {[ + {<<"from">>, <<"foobar">>}, + {<<"to">>, <<"_info">>} + ]} + ]} + ]}), + {ok, _} = couch_db:update_doc(Db, DDoc, []), + + couch_db:ensure_full_commit(Db), + couch_db:close(Db), + + Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + Url = "http://" ++ Addr ++ ":" ++ Port, + {Url, ?b2l(DbName)}. + +teardown({_, DbName}) -> + ok = couch_server:delete(?l2b(DbName), []), + ok. + + +vhosts_test_() -> + { + "Virtual Hosts rewrite tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_return_database_info/1, + fun should_return_revs_info/1, + fun should_serve_utils_for_vhost/1, + fun should_return_virtual_request_path_field_in_request/1, + fun should_return_real_request_path_field_in_request/1, + fun should_match_wildcard_vhost/1, + fun should_return_db_info_for_wildcard_vhost_for_custom_db/1, + fun should_replace_rewrite_variables_for_db_and_doc/1, + fun should_return_db_info_for_vhost_with_resource/1, + fun should_return_revs_info_for_vhost_with_resource/1, + fun should_return_db_info_for_vhost_with_wildcard_resource/1, + fun should_return_path_for_vhost_with_wildcard_host/1 + ] + } + } + }. + +oauth_test_() -> + { + "Virtual Hosts OAuth tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup_oauth/0, fun teardown/1, + [ + fun should_require_auth/1, + fun should_succeed_oauth/1, + fun should_fail_oauth_with_wrong_credentials/1 + ] + } + } + }. + + +should_return_database_info({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false), + case test_request:get(Url, [], [{host_header, "example.com"}]) of + {ok, _, _, Body} -> + {JsonBody} = ejson:decode(Body), + ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_revs_info({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false), + case test_request:get(Url ++ "/doc1?revs_info=true", [], + [{host_header, "example.com"}]) of + {ok, _, _, Body} -> + {JsonBody} = ejson:decode(Body), + ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_serve_utils_for_vhost({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false), + case test_request:get(Url ++ "/_utils/index.html", [], + [{host_header, "example.com"}]) of + {ok, _, _, Body} -> + ?assertMatch(<<"<!DOCTYPE html>", _/binary>>, Body); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_virtual_request_path_field_in_request({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "example1.com", + "/" ++ DbName ++ "/_design/doc1/_rewrite/", + false), + case test_request:get(Url, [], [{host_header, "example1.com"}]) of + {ok, _, _, Body} -> + {Json} = ejson:decode(Body), + ?assertEqual(<<"/">>, + proplists:get_value(<<"requested_path">>, Json)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_real_request_path_field_in_request({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "example1.com", + "/" ++ DbName ++ "/_design/doc1/_rewrite/", + false), + case test_request:get(Url, [], [{host_header, "example1.com"}]) of + {ok, _, _, Body} -> + {Json} = ejson:decode(Body), + Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), + ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_match_wildcard_vhost({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "*.example.com", + "/" ++ DbName ++ "/_design/doc1/_rewrite", false), + case test_request:get(Url, [], [{host_header, "test.example.com"}]) of + {ok, _, _, Body} -> + {Json} = ejson:decode(Body), + Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), + ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_db_info_for_wildcard_vhost_for_custom_db({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", ":dbname.example1.com", + "/:dbname", false), + Host = DbName ++ ".example1.com", + case test_request:get(Url, [], [{host_header, Host}]) of + {ok, _, _, Body} -> + {JsonBody} = ejson:decode(Body), + ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_replace_rewrite_variables_for_db_and_doc({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts",":appname.:dbname.example1.com", + "/:dbname/_design/:appname/_rewrite/", false), + Host = "doc1." ++ DbName ++ ".example1.com", + case test_request:get(Url, [], [{host_header, Host}]) of + {ok, _, _, Body} -> + {Json} = ejson:decode(Body), + Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), + ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_db_info_for_vhost_with_resource({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", + "example.com/test", "/" ++ DbName, false), + ReqUrl = Url ++ "/test", + case test_request:get(ReqUrl, [], [{host_header, "example.com"}]) of + {ok, _, _, Body} -> + {JsonBody} = ejson:decode(Body), + ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + + +should_return_revs_info_for_vhost_with_resource({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", + "example.com/test", "/" ++ DbName, false), + ReqUrl = Url ++ "/test/doc1?revs_info=true", + case test_request:get(ReqUrl, [], [{host_header, "example.com"}]) of + {ok, _, _, Body} -> + {JsonBody} = ejson:decode(Body), + ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_db_info_for_vhost_with_wildcard_resource({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "*.example2.com/test", "/*", false), + ReqUrl = Url ++ "/test", + Host = DbName ++ ".example2.com", + case test_request:get(ReqUrl, [], [{host_header, Host}]) of + {ok, _, _, Body} -> + {JsonBody} = ejson:decode(Body), + ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_return_path_for_vhost_with_wildcard_host({Url, DbName}) -> + ?_test(begin + ok = couch_config:set("vhosts", "*/test1", + "/" ++ DbName ++ "/_design/doc1/_show/test", + false), + case test_request:get(Url ++ "/test1") of + {ok, _, _, Body} -> + {Json} = ejson:decode(Body), + Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), + ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_require_auth({Url, _}) -> + ?_test(begin + case test_request:get(Url, [], [{host_header, "oauth-example.com"}]) of + {ok, Code, _, Body} -> + ?assertEqual(401, Code), + {JsonBody} = ejson:decode(Body), + ?assertEqual(<<"unauthorized">>, + couch_util:get_value(<<"error">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_succeed_oauth({Url, _}) -> + ?_test(begin + AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"), + JoeDoc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"org.couchdb.user:joe">>}, + {<<"type">>, <<"user">>}, + {<<"name">>, <<"joe">>}, + {<<"roles">>, []}, + {<<"password_sha">>, <<"fe95df1ca59a9b567bdca5cbaf8412abd6e06121">>}, + {<<"salt">>, <<"4e170ffeb6f34daecfd814dfb4001a73">>} + ]}), + {ok, AuthDb} = couch_db:open_int(?l2b(AuthDbName), [?ADMIN_USER]), + {ok, _} = couch_db:update_doc(AuthDb, JoeDoc, [?ADMIN_USER]), + + Host = "oauth-example.com", + Consumer = {"consec1", "foo", hmac_sha1}, + SignedParams = oauth:sign( + "GET", "http://" ++ Host ++ "/", [], Consumer, "otoksec1", "foobar"), + OAuthUrl = oauth:uri(Url, SignedParams), + + case test_request:get(OAuthUrl, [], [{host_header, Host}]) of + {ok, Code, _, Body} -> + ?assertEqual(200, Code), + {JsonBody} = ejson:decode(Body), + ?assertEqual(<<"test">>, + couch_util:get_value(<<"name">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). + +should_fail_oauth_with_wrong_credentials({Url, _}) -> + ?_test(begin + AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"), + JoeDoc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"org.couchdb.user:joe">>}, + {<<"type">>, <<"user">>}, + {<<"name">>, <<"joe">>}, + {<<"roles">>, []}, + {<<"password_sha">>, <<"fe95df1ca59a9b567bdca5cbaf8412abd6e06121">>}, + {<<"salt">>, <<"4e170ffeb6f34daecfd814dfb4001a73">>} + ]}), + {ok, AuthDb} = couch_db:open_int(?l2b(AuthDbName), [?ADMIN_USER]), + {ok, _} = couch_db:update_doc(AuthDb, JoeDoc, [?ADMIN_USER]), + + Host = "oauth-example.com", + Consumer = {"consec1", "bad_secret", hmac_sha1}, + SignedParams = oauth:sign( + "GET", "http://" ++ Host ++ "/", [], Consumer, "otoksec1", "foobar"), + OAuthUrl = oauth:uri(Url, SignedParams), + + case test_request:get(OAuthUrl, [], [{host_header, Host}]) of + {ok, Code, _, Body} -> + ?assertEqual(401, Code), + {JsonBody} = ejson:decode(Body), + ?assertEqual(<<"unauthorized">>, + couch_util:get_value(<<"error">>, JsonBody)); + Else -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, ?iofmt("Request failed: ~p", [Else])}]}) + end + end). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_views_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_views_tests.erl b/test/couchdb_views_tests.erl new file mode 100644 index 0000000..6d81f32 --- /dev/null +++ b/test/couchdb_views_tests.erl @@ -0,0 +1,669 @@ +% 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(couchdb_views_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). +-include_lib("couch_mrview/include/couch_mrview.hrl"). + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(DELAY, 100). +-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(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), + ok = couch_db:close(Db), + FooRev = create_design_doc(DbName, <<"_design/foo">>, <<"bar">>), + query_view(DbName, "foo", "bar"), + BooRev = create_design_doc(DbName, <<"_design/boo">>, <<"baz">>), + query_view(DbName, "boo", "baz"), + {DbName, {FooRev, BooRev}}. + +setup_with_docs() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), + ok = couch_db:close(Db), + create_docs(DbName), + create_design_doc(DbName, <<"_design/foo">>, <<"bar">>), + DbName. + +teardown({DbName, _}) -> + teardown(DbName); +teardown(DbName) when is_binary(DbName) -> + couch_server:delete(DbName, [?ADMIN_USER]), + ok. + + +view_indexes_cleanup_test_() -> + { + "View indexes cleanup", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_have_two_indexes_alive_before_deletion/1, + fun should_cleanup_index_file_after_ddoc_deletion/1, + fun should_cleanup_all_index_files/1 + ] + } + } + }. + +view_group_db_leaks_test_() -> + { + "View group db leaks", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup_with_docs/0, fun teardown/1, + [ + fun couchdb_1138/1, + fun couchdb_1309/1 + ] + } + } + }. + +view_group_shutdown_test_() -> + { + "View group shutdown", + { + setup, + fun start/0, fun stop/1, + [couchdb_1283()] + } + }. + + +should_not_remember_docs_in_index_after_backup_restore_test() -> + %% COUCHDB-640 + start(), + DbName = setup_with_docs(), + + ok = backup_db_file(DbName), + create_doc(DbName, "doc666"), + + Rows0 = query_view(DbName, "foo", "bar"), + ?assert(has_doc("doc1", Rows0)), + ?assert(has_doc("doc2", Rows0)), + ?assert(has_doc("doc3", Rows0)), + ?assert(has_doc("doc666", Rows0)), + + restore_backup_db_file(DbName), + + Rows1 = query_view(DbName, "foo", "bar"), + ?assert(has_doc("doc1", Rows1)), + ?assert(has_doc("doc2", Rows1)), + ?assert(has_doc("doc3", Rows1)), + ?assertNot(has_doc("doc666", Rows1)), + + teardown(DbName), + stop(whereis(couch_server_sup)). + + +should_upgrade_legacy_view_files_test() -> + start(), + + ok = couch_config:set("query_server_config", "commit_freq", "0", false), + + DbName = <<"test">>, + DbFileName = "test.couch", + DbFilePath = filename:join([?FIXTURESDIR, DbFileName]), + OldViewName = "3b835456c235b1827e012e25666152f3.view", + FixtureViewFilePath = filename:join([?FIXTURESDIR, OldViewName]), + NewViewName = "a1c5929f912aca32f13446122cc6ce50.view", + + DbDir = couch_config:get("couchdb", "database_dir"), + ViewDir = couch_config:get("couchdb", "view_index_dir"), + OldViewFilePath = filename:join([ViewDir, ".test_design", OldViewName]), + NewViewFilePath = filename:join([ViewDir, ".test_design", "mrview", + NewViewName]), + + % cleanup + Files = [ + filename:join([DbDir, DbFileName]), + OldViewFilePath, + NewViewFilePath + ], + lists:foreach(fun(File) -> file:delete(File) end, Files), + + % copy old db file into db dir + {ok, _} = file:copy(DbFilePath, filename:join([DbDir, DbFileName])), + + % copy old view file into view dir + ok = filelib:ensure_dir(filename:join([ViewDir, ".test_design"])), + {ok, _} = file:copy(FixtureViewFilePath, OldViewFilePath), + + % ensure old header + OldHeader = read_header(OldViewFilePath), + ?assertMatch(#index_header{}, OldHeader), + + % query view for expected results + Rows0 = query_view(DbName, "test", "test"), + ?assertEqual(2, length(Rows0)), + + % ensure old file gone + ?assertNot(filelib:is_regular(OldViewFilePath)), + + % add doc to trigger update + DocUrl = db_url(DbName) ++ "/boo", + {ok, _, _, _} = test_request:put( + DocUrl, [{"Content-Type", "application/json"}], <<"{\"a\":3}">>), + + % query view for expected results + Rows1 = query_view(DbName, "test", "test"), + ?assertEqual(3, length(Rows1)), + + % ensure new header + timer:sleep(2000), % have to wait for awhile to upgrade the index + NewHeader = read_header(NewViewFilePath), + ?assertMatch(#mrheader{}, NewHeader), + + teardown(DbName), + stop(whereis(couch_server_sup)). + + +should_have_two_indexes_alive_before_deletion({DbName, _}) -> + view_cleanup(DbName), + ?_assertEqual(2, count_index_files(DbName)). + +should_cleanup_index_file_after_ddoc_deletion({DbName, {FooRev, _}}) -> + delete_design_doc(DbName, <<"_design/foo">>, FooRev), + view_cleanup(DbName), + ?_assertEqual(1, count_index_files(DbName)). + +should_cleanup_all_index_files({DbName, {FooRev, BooRev}})-> + delete_design_doc(DbName, <<"_design/foo">>, FooRev), + delete_design_doc(DbName, <<"_design/boo">>, BooRev), + view_cleanup(DbName), + ?_assertEqual(0, count_index_files(DbName)). + +couchdb_1138(DbName) -> + ?_test(begin + {ok, IndexerPid} = couch_index_server:get_index( + couch_mrview_index, DbName, <<"_design/foo">>), + ?assert(is_pid(IndexerPid)), + ?assert(is_process_alive(IndexerPid)), + ?assertEqual(2, count_db_refs(DbName)), + + Rows0 = query_view(DbName, "foo", "bar"), + ?assertEqual(3, length(Rows0)), + ?assertEqual(2, count_db_refs(DbName)), + ?assert(is_process_alive(IndexerPid)), + + create_doc(DbName, "doc1000"), + Rows1 = query_view(DbName, "foo", "bar"), + ?assertEqual(4, length(Rows1)), + ?assertEqual(2, count_db_refs(DbName)), + ?assert(is_process_alive(IndexerPid)), + + Ref1 = get_db_ref_counter(DbName), + compact_db(DbName), + Ref2 = get_db_ref_counter(DbName), + ?assertEqual(2, couch_ref_counter:count(Ref2)), + ?assertNotEqual(Ref2, Ref1), + ?assertNot(is_process_alive(Ref1)), + ?assert(is_process_alive(IndexerPid)), + + compact_view_group(DbName, "foo"), + ?assertEqual(2, count_db_refs(DbName)), + Ref3 = get_db_ref_counter(DbName), + ?assertEqual(Ref3, Ref2), + ?assert(is_process_alive(IndexerPid)), + + create_doc(DbName, "doc1001"), + Rows2 = query_view(DbName, "foo", "bar"), + ?assertEqual(5, length(Rows2)), + ?assertEqual(2, count_db_refs(DbName)), + ?assert(is_process_alive(IndexerPid)) + end). + +couchdb_1309(DbName) -> + ?_test(begin + {ok, IndexerPid} = couch_index_server:get_index( + couch_mrview_index, DbName, <<"_design/foo">>), + ?assert(is_pid(IndexerPid)), + ?assert(is_process_alive(IndexerPid)), + ?assertEqual(2, count_db_refs(DbName)), + + create_doc(DbName, "doc1001"), + Rows0 = query_view(DbName, "foo", "bar"), + check_rows_value(Rows0, null), + ?assertEqual(4, length(Rows0)), + ?assertEqual(2, count_db_refs(DbName)), + ?assert(is_process_alive(IndexerPid)), + + update_design_doc(DbName, <<"_design/foo">>, <<"bar">>), + {ok, NewIndexerPid} = couch_index_server:get_index( + couch_mrview_index, DbName, <<"_design/foo">>), + ?assert(is_pid(NewIndexerPid)), + ?assert(is_process_alive(NewIndexerPid)), + ?assertNotEqual(IndexerPid, NewIndexerPid), + ?assertEqual(2, count_db_refs(DbName)), + + Rows1 = query_view(DbName, "foo", "bar", ok), + ?assertEqual(0, length(Rows1)), + Rows2 = query_view(DbName, "foo", "bar"), + check_rows_value(Rows2, 1), + ?assertEqual(4, length(Rows2)), + + MonRef0 = erlang:monitor(process, IndexerPid), + receive + {'DOWN', MonRef0, _, _, _} -> + ok + after ?TIMEOUT -> + erlang:error( + {assertion_failed, + [{module, ?MODULE}, {line, ?LINE}, + {reason, "old view group is not dead after ddoc update"}]}) + end, + + MonRef1 = erlang:monitor(process, NewIndexerPid), + ok = couch_server:delete(DbName, [?ADMIN_USER]), + receive + {'DOWN', MonRef1, _, _, _} -> + ok + after ?TIMEOUT -> + erlang:error( + {assertion_failed, + [{module, ?MODULE}, {line, ?LINE}, + {reason, "new view group did not die after DB deletion"}]}) + end + end). + +couchdb_1283() -> + ?_test(begin + ok = couch_config:set("couchdb", "max_dbs_open", "3", false), + ok = couch_config:set("couchdb", "delayed_commits", "false", false), + + {ok, MDb1} = couch_db:create(?tempdb(), [?ADMIN_USER]), + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"_design/foo">>}, + {<<"language">>, <<"javascript">>}, + {<<"views">>, {[ + {<<"foo">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} + ]}}, + {<<"foo2">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} + ]}}, + {<<"foo3">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} + ]}}, + {<<"foo4">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} + ]}}, + {<<"foo5">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} + ]}} + ]}} + ]}), + {ok, _} = couch_db:update_doc(MDb1, DDoc, []), + ok = populate_db(MDb1, 100, 100), + query_view(MDb1#db.name, "foo", "foo"), + ok = couch_db:close(MDb1), + + {ok, Db1} = couch_db:create(?tempdb(), [?ADMIN_USER]), + ok = couch_db:close(Db1), + {ok, Db2} = couch_db:create(?tempdb(), [?ADMIN_USER]), + ok = couch_db:close(Db2), + {ok, Db3} = couch_db:create(?tempdb(), [?ADMIN_USER]), + ok = couch_db:close(Db3), + + Writer1 = spawn_writer(Db1#db.name), + Writer2 = spawn_writer(Db2#db.name), + + ?assert(is_process_alive(Writer1)), + ?assert(is_process_alive(Writer2)), + + ?assertEqual(ok, get_writer_status(Writer1)), + ?assertEqual(ok, get_writer_status(Writer2)), + + {ok, MonRef} = couch_mrview:compact(MDb1#db.name, <<"_design/foo">>, + [monitor]), + + Writer3 = spawn_writer(Db3#db.name), + ?assert(is_process_alive(Writer3)), + ?assertEqual({error, all_dbs_active}, get_writer_status(Writer3)), + + ?assert(is_process_alive(Writer1)), + ?assert(is_process_alive(Writer2)), + ?assert(is_process_alive(Writer3)), + + receive + {'DOWN', MonRef, process, _, Reason} -> + ?assertEqual(normal, Reason) + after ?TIMEOUT -> + erlang:error( + {assertion_failed, + [{module, ?MODULE}, {line, ?LINE}, + {reason, "Failure compacting view group"}]}) + end, + + ?assertEqual(ok, writer_try_again(Writer3)), + ?assertEqual(ok, get_writer_status(Writer3)), + + ?assert(is_process_alive(Writer1)), + ?assert(is_process_alive(Writer2)), + ?assert(is_process_alive(Writer3)), + + ?assertEqual(ok, stop_writer(Writer1)), + ?assertEqual(ok, stop_writer(Writer2)), + ?assertEqual(ok, stop_writer(Writer3)) + end). + +create_doc(DbName, DocId) when is_list(DocId) -> + create_doc(DbName, ?l2b(DocId)); +create_doc(DbName, DocId) when is_binary(DocId) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), + Doc666 = couch_doc:from_json_obj({[ + {<<"_id">>, DocId}, + {<<"value">>, 999} + ]}), + {ok, _} = couch_db:update_docs(Db, [Doc666]), + couch_db:ensure_full_commit(Db), + couch_db:close(Db). + +create_docs(DbName) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), + Doc1 = couch_doc:from_json_obj({[ + {<<"_id">>, <<"doc1">>}, + {<<"value">>, 1} + + ]}), + Doc2 = couch_doc:from_json_obj({[ + {<<"_id">>, <<"doc2">>}, + {<<"value">>, 2} + + ]}), + Doc3 = couch_doc:from_json_obj({[ + {<<"_id">>, <<"doc3">>}, + {<<"value">>, 3} + + ]}), + {ok, _} = couch_db:update_docs(Db, [Doc1, Doc2, Doc3]), + couch_db:ensure_full_commit(Db), + couch_db:close(Db). + +populate_db(Db, BatchSize, N) when N > 0 -> + Docs = lists:map( + fun(_) -> + couch_doc:from_json_obj({[ + {<<"_id">>, couch_uuids:new()}, + {<<"value">>, base64:encode(crypto:rand_bytes(1000))} + ]}) + end, + lists:seq(1, BatchSize)), + {ok, _} = couch_db:update_docs(Db, Docs, []), + populate_db(Db, BatchSize, N - length(Docs)); +populate_db(_Db, _, _) -> + ok. + +create_design_doc(DbName, DDName, ViewName) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, DDName}, + {<<"language">>, <<"javascript">>}, + {<<"views">>, {[ + {ViewName, {[ + {<<"map">>, <<"function(doc) { emit(doc.value, null); }">>} + ]}} + ]}} + ]}), + {ok, Rev} = couch_db:update_doc(Db, DDoc, []), + couch_db:ensure_full_commit(Db), + couch_db:close(Db), + Rev. + +update_design_doc(DbName, DDName, ViewName) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), + {ok, Doc} = couch_db:open_doc(Db, DDName, [?ADMIN_USER]), + {Props} = couch_doc:to_json_obj(Doc, []), + Rev = couch_util:get_value(<<"_rev">>, Props), + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, DDName}, + {<<"_rev">>, Rev}, + {<<"language">>, <<"javascript">>}, + {<<"views">>, {[ + {ViewName, {[ + {<<"map">>, <<"function(doc) { emit(doc.value, 1); }">>} + ]}} + ]}} + ]}), + {ok, NewRev} = couch_db:update_doc(Db, DDoc, [?ADMIN_USER]), + couch_db:ensure_full_commit(Db), + couch_db:close(Db), + NewRev. + +delete_design_doc(DbName, DDName, Rev) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, DDName}, + {<<"_rev">>, couch_doc:rev_to_str(Rev)}, + {<<"_deleted">>, true} + ]}), + {ok, _} = couch_db:update_doc(Db, DDoc, [Rev]), + couch_db:close(Db). + +db_url(DbName) -> + Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(DbName). + +query_view(DbName, DDoc, View) -> + query_view(DbName, DDoc, View, false). + +query_view(DbName, DDoc, View, Stale) -> + {ok, Code, _Headers, Body} = test_request:get( + db_url(DbName) ++ "/_design/" ++ DDoc ++ "/_view/" ++ View + ++ case Stale of + false -> []; + _ -> "?stale=" ++ atom_to_list(Stale) + end), + ?assertEqual(200, Code), + {Props} = ejson:decode(Body), + couch_util:get_value(<<"rows">>, Props, []). + +check_rows_value(Rows, Value) -> + lists:foreach( + fun({Row}) -> + ?assertEqual(Value, couch_util:get_value(<<"value">>, Row)) + end, Rows). + +view_cleanup(DbName) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), + couch_mrview:cleanup(Db), + couch_db:close(Db). + +get_db_ref_counter(DbName) -> + {ok, #db{fd_ref_counter = Ref} = Db} = couch_db:open_int(DbName, []), + ok = couch_db:close(Db), + Ref. + +count_db_refs(DbName) -> + Ref = get_db_ref_counter(DbName), + % have to sleep a bit to let couchdb cleanup all refs and leave only + % active ones. otherwise the related tests will randomly fail due to + % count number mismatch + timer:sleep(200), + couch_ref_counter:count(Ref). + +count_index_files(DbName) -> + % call server to fetch the index files + RootDir = couch_config:get("couchdb", "view_index_dir"), + length(filelib:wildcard(RootDir ++ "/." ++ + binary_to_list(DbName) ++ "_design"++"/mrview/*")). + +has_doc(DocId1, Rows) -> + DocId = iolist_to_binary(DocId1), + lists:any(fun({R}) -> lists:member({<<"id">>, DocId}, R) end, Rows). + +backup_db_file(DbName) -> + DbDir = couch_config:get("couchdb", "database_dir"), + DbFile = filename:join([DbDir, ?b2l(DbName) ++ ".couch"]), + {ok, _} = file:copy(DbFile, DbFile ++ ".backup"), + ok. + +restore_backup_db_file(DbName) -> + DbDir = couch_config:get("couchdb", "database_dir"), + stop(whereis(couch_server_sup)), + DbFile = filename:join([DbDir, ?b2l(DbName) ++ ".couch"]), + ok = file:delete(DbFile), + ok = file:rename(DbFile ++ ".backup", DbFile), + start(), + ok. + +compact_db(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, _} = couch_db:start_compact(Db), + ok = couch_db:close(Db), + wait_db_compact_done(DbName, 10). + +wait_db_compact_done(_DbName, 0) -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "DB compaction failed to finish"}]}); +wait_db_compact_done(DbName, N) -> + {ok, Db} = couch_db:open_int(DbName, []), + ok = couch_db:close(Db), + case is_pid(Db#db.compactor_pid) of + false -> + ok; + true -> + ok = timer:sleep(?DELAY), + wait_db_compact_done(DbName, N - 1) + end. + +compact_view_group(DbName, DDocId) when is_list(DDocId) -> + compact_view_group(DbName, ?l2b("_design/" ++ DDocId)); +compact_view_group(DbName, DDocId) when is_binary(DDocId) -> + ok = couch_mrview:compact(DbName, DDocId), + wait_view_compact_done(DbName, DDocId, 10). + +wait_view_compact_done(_DbName, _DDocId, 0) -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "DB compaction failed to finish"}]}); +wait_view_compact_done(DbName, DDocId, N) -> + {ok, Code, _Headers, Body} = test_request:get( + db_url(DbName) ++ "/" ++ ?b2l(DDocId) ++ "/_info"), + ?assertEqual(200, Code), + {Info} = ejson:decode(Body), + {IndexInfo} = couch_util:get_value(<<"view_index">>, Info), + CompactRunning = couch_util:get_value(<<"compact_running">>, IndexInfo), + case CompactRunning of + false -> + ok; + true -> + ok = timer:sleep(?DELAY), + wait_view_compact_done(DbName, DDocId, N - 1) + end. + +spawn_writer(DbName) -> + Parent = self(), + spawn(fun() -> + process_flag(priority, high), + writer_loop(DbName, Parent) + end). + +get_writer_status(Writer) -> + Ref = make_ref(), + Writer ! {get_status, Ref}, + receive + {db_open, Ref} -> + ok; + {db_open_error, Error, Ref} -> + Error + after ?TIMEOUT -> + timeout + end. + +writer_try_again(Writer) -> + Ref = make_ref(), + Writer ! {try_again, Ref}, + receive + {ok, Ref} -> + ok + after ?TIMEOUT -> + timeout + end. + +stop_writer(Writer) -> + Ref = make_ref(), + Writer ! {stop, Ref}, + receive + {ok, Ref} -> + ok + after ?TIMEOUT -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Timeout on stopping process"}]}) + end. + +writer_loop(DbName, Parent) -> + case couch_db:open_int(DbName, []) of + {ok, Db} -> + writer_loop_1(Db, Parent); + Error -> + writer_loop_2(DbName, Parent, Error) + end. + +writer_loop_1(Db, Parent) -> + receive + {get_status, Ref} -> + Parent ! {db_open, Ref}, + writer_loop_1(Db, Parent); + {stop, Ref} -> + ok = couch_db:close(Db), + Parent ! {ok, Ref} + end. + +writer_loop_2(DbName, Parent, Error) -> + receive + {get_status, Ref} -> + Parent ! {db_open_error, Error, Ref}, + writer_loop_2(DbName, Parent, Error); + {try_again, Ref} -> + Parent ! {ok, Ref}, + writer_loop(DbName, Parent) + end. + +read_header(File) -> + {ok, Fd} = couch_file:open(File), + {ok, {_Sig, Header}} = couch_file:read_header(Fd), + couch_file:close(Fd), + Header. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/3b835456c235b1827e012e25666152f3.view ---------------------------------------------------------------------- diff --git a/test/fixtures/3b835456c235b1827e012e25666152f3.view b/test/fixtures/3b835456c235b1827e012e25666152f3.view new file mode 100644 index 0000000..9c67648 Binary files /dev/null and b/test/fixtures/3b835456c235b1827e012e25666152f3.view differ http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/couch_config_tests_1.ini ---------------------------------------------------------------------- diff --git a/test/fixtures/couch_config_tests_1.ini b/test/fixtures/couch_config_tests_1.ini new file mode 100644 index 0000000..55451da --- /dev/null +++ b/test/fixtures/couch_config_tests_1.ini @@ -0,0 +1,22 @@ +; Licensed to the Apache Software Foundation (ASF) under one +; or more contributor license agreements. See the NOTICE file +; distributed with this work for additional information +; regarding copyright ownership. The ASF licenses this file +; to you under the Apache License, Version 2.0 (the +; "License"); you may not use this file except in compliance +; with the License. You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, +; software distributed under the License is distributed on an +; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +; KIND, either express or implied. See the License for the +; specific language governing permissions and limitations +; under the License. + +[couchdb] +max_dbs_open=10 + +[httpd] +port=4895 http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/couch_config_tests_2.ini ---------------------------------------------------------------------- diff --git a/test/fixtures/couch_config_tests_2.ini b/test/fixtures/couch_config_tests_2.ini new file mode 100644 index 0000000..5f46357 --- /dev/null +++ b/test/fixtures/couch_config_tests_2.ini @@ -0,0 +1,22 @@ +; Licensed to the Apache Software Foundation (ASF) under one +; or more contributor license agreements. See the NOTICE file +; distributed with this work for additional information +; regarding copyright ownership. The ASF licenses this file +; to you under the Apache License, Version 2.0 (the +; "License"); you may not use this file except in compliance +; with the License. You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, +; software distributed under the License is distributed on an +; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +; KIND, either express or implied. See the License for the +; specific language governing permissions and limitations +; under the License. + +[httpd] +port = 80 + +[fizbang] +unicode = normalized http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/couch_stats_aggregates.cfg ---------------------------------------------------------------------- diff --git a/test/fixtures/couch_stats_aggregates.cfg b/test/fixtures/couch_stats_aggregates.cfg new file mode 100644 index 0000000..30e475d --- /dev/null +++ b/test/fixtures/couch_stats_aggregates.cfg @@ -0,0 +1,19 @@ +% Licensed to the Apache Software Foundation (ASF) under one +% or more contributor license agreements. See the NOTICE file +% distributed with this work for additional information +% regarding copyright ownership. The ASF licenses this file +% to you under the Apache License, Version 2.0 (the +% "License"); you may not use this file except in compliance +% with the License. You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, +% software distributed under the License is distributed on an +% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +% KIND, either express or implied. See the License for the +% specific language governing permissions and limitations +% under the License. + +{testing, stuff, "yay description"}. +{number, '11', "randomosity"}. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/couch_stats_aggregates.ini ---------------------------------------------------------------------- diff --git a/test/fixtures/couch_stats_aggregates.ini b/test/fixtures/couch_stats_aggregates.ini new file mode 100644 index 0000000..cc5cd21 --- /dev/null +++ b/test/fixtures/couch_stats_aggregates.ini @@ -0,0 +1,20 @@ +; Licensed to the Apache Software Foundation (ASF) under one +; or more contributor license agreements. See the NOTICE file +; distributed with this work for additional information +; regarding copyright ownership. The ASF licenses this file +; to you under the Apache License, Version 2.0 (the +; "License"); you may not use this file except in compliance +; with the License. You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, +; software distributed under the License is distributed on an +; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +; KIND, either express or implied. See the License for the +; specific language governing permissions and limitations +; under the License. + +[stats] +rate = 10000000 ; We call collect_sample in testing +samples = [0, 1] http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/logo.png ---------------------------------------------------------------------- diff --git a/test/fixtures/logo.png b/test/fixtures/logo.png new file mode 100644 index 0000000..d21ac02 Binary files /dev/null and b/test/fixtures/logo.png differ http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/os_daemon_bad_perm.sh ---------------------------------------------------------------------- diff --git a/test/fixtures/os_daemon_bad_perm.sh b/test/fixtures/os_daemon_bad_perm.sh new file mode 100644 index 0000000..345c8b4 --- /dev/null +++ b/test/fixtures/os_daemon_bad_perm.sh @@ -0,0 +1,17 @@ +#!/bin/sh -e +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# Please do not make this file executable as that's the error being tested. + +sleep 5 http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/os_daemon_can_reboot.sh ---------------------------------------------------------------------- diff --git a/test/fixtures/os_daemon_can_reboot.sh b/test/fixtures/os_daemon_can_reboot.sh new file mode 100755 index 0000000..5bc10e8 --- /dev/null +++ b/test/fixtures/os_daemon_can_reboot.sh @@ -0,0 +1,15 @@ +#!/bin/sh -e +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +sleep 2 http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/os_daemon_configer.escript ---------------------------------------------------------------------- diff --git a/test/fixtures/os_daemon_configer.escript b/test/fixtures/os_daemon_configer.escript new file mode 100755 index 0000000..d437423 --- /dev/null +++ b/test/fixtures/os_daemon_configer.escript @@ -0,0 +1,101 @@ +#! /usr/bin/env escript + +% 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. + +-include("../couch_eunit.hrl"). + + +read() -> + case io:get_line('') of + eof -> + stop; + Data -> + ejson:decode(Data) + end. + +write(Mesg) -> + Data = iolist_to_binary(ejson:encode(Mesg)), + io:format(binary_to_list(Data) ++ "\n", []). + +get_cfg(Section) -> + write([<<"get">>, Section]), + read(). + +get_cfg(Section, Name) -> + write([<<"get">>, Section, Name]), + read(). + +log(Mesg) -> + write([<<"log">>, Mesg]). + +log(Mesg, Level) -> + write([<<"log">>, Mesg, {[{<<"level">>, Level}]}]). + +test_get_cfg1() -> + Path = list_to_binary(?FILE), + FileName = list_to_binary(filename:basename(?FILE)), + {[{FileName, Path}]} = get_cfg(<<"os_daemons">>). + +test_get_cfg2() -> + Path = list_to_binary(?FILE), + FileName = list_to_binary(filename:basename(?FILE)), + Path = get_cfg(<<"os_daemons">>, FileName), + <<"sequential">> = get_cfg(<<"uuids">>, <<"algorithm">>). + + +test_get_unknown_cfg() -> + {[]} = get_cfg(<<"aal;3p4">>), + null = get_cfg(<<"aal;3p4">>, <<"313234kjhsdfl">>). + +test_log() -> + log(<<"foobar!">>), + log(<<"some stuff!">>, <<"debug">>), + log(2), + log(true), + write([<<"log">>, <<"stuff">>, 2]), + write([<<"log">>, 3, null]), + write([<<"log">>, [1, 2], {[{<<"level">>, <<"debug">>}]}]), + write([<<"log">>, <<"true">>, {[]}]). + +do_tests() -> + test_get_cfg1(), + test_get_cfg2(), + test_get_unknown_cfg(), + test_log(), + loop(io:read("")). + +loop({ok, _}) -> + loop(io:read("")); +loop(eof) -> + init:stop(); +loop({error, _Reason}) -> + init:stop(). + +main([]) -> + init_code_path(), + couch_config:start_link(?CONFIG_CHAIN), + couch_drv:start_link(), + do_tests(). + +init_code_path() -> + Paths = [ + "couchdb", + "ejson", + "erlang-oauth", + "ibrowse", + "mochiweb", + "snappy" + ], + lists:foreach(fun(Name) -> + code:add_patha(filename:join([?BUILDDIR, "src", Name])) + end, Paths). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/os_daemon_die_on_boot.sh ---------------------------------------------------------------------- diff --git a/test/fixtures/os_daemon_die_on_boot.sh b/test/fixtures/os_daemon_die_on_boot.sh new file mode 100755 index 0000000..256ee79 --- /dev/null +++ b/test/fixtures/os_daemon_die_on_boot.sh @@ -0,0 +1,15 @@ +#!/bin/sh -e +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +exit 1 http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/os_daemon_die_quickly.sh ---------------------------------------------------------------------- diff --git a/test/fixtures/os_daemon_die_quickly.sh b/test/fixtures/os_daemon_die_quickly.sh new file mode 100755 index 0000000..f5a1368 --- /dev/null +++ b/test/fixtures/os_daemon_die_quickly.sh @@ -0,0 +1,15 @@ +#!/bin/sh -e +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +sleep 1 http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/os_daemon_looper.escript ---------------------------------------------------------------------- diff --git a/test/fixtures/os_daemon_looper.escript b/test/fixtures/os_daemon_looper.escript new file mode 100755 index 0000000..73974e9 --- /dev/null +++ b/test/fixtures/os_daemon_looper.escript @@ -0,0 +1,26 @@ +#! /usr/bin/env escript + +% 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. + +loop() -> + loop(io:read("")). + +loop({ok, _}) -> + loop(io:read("")); +loop(eof) -> + stop; +loop({error, Reason}) -> + throw({error, Reason}). + +main([]) -> + loop(). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/fixtures/test.couch ---------------------------------------------------------------------- diff --git a/test/fixtures/test.couch b/test/fixtures/test.couch new file mode 100644 index 0000000..32c79af Binary files /dev/null and b/test/fixtures/test.couch differ http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/json_stream_parse_tests.erl ---------------------------------------------------------------------- diff --git a/test/json_stream_parse_tests.erl b/test/json_stream_parse_tests.erl new file mode 100644 index 0000000..92303b6 --- /dev/null +++ b/test/json_stream_parse_tests.erl @@ -0,0 +1,151 @@ +% 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(json_stream_parse_tests). + +-include("couch_eunit.hrl"). + +-define(CASES, + [ + {1, "1", "integer numeric literial"}, + {3.1416, "3.14160", "float numeric literal"}, % text representation may truncate, trail zeroes + {-1, "-1", "negative integer numeric literal"}, + {-3.1416, "-3.14160", "negative float numeric literal"}, + {12.0e10, "1.20000e+11", "float literal in scientific notation"}, + {1.234E+10, "1.23400e+10", "another float literal in scientific notation"}, + {-1.234E-10, "-1.23400e-10", "negative float literal in scientific notation"}, + {10.0, "1.0e+01", "yet another float literal in scientific notation"}, + {123.456, "1.23456E+2", "yet another float literal in scientific notation"}, + {10.0, "1e1", "yet another float literal in scientific notation"}, + {<<"foo">>, "\"foo\"", "string literal"}, + {<<"foo", 5, "bar">>, "\"foo\\u0005bar\"", "string literal with \\u0005"}, + {<<"">>, "\"\"", "empty string literal"}, + {<<"\n\n\n">>, "\"\\n\\n\\n\"", "only new lines literal"}, + {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\"", + "only white spaces string literal"}, + {null, "null", "null literal"}, + {true, "true", "true literal"}, + {false, "false", "false literal"}, + {<<"null">>, "\"null\"", "null string literal"}, + {<<"true">>, "\"true\"", "true string literal"}, + {<<"false">>, "\"false\"", "false string literal"}, + {{[]}, "{}", "empty object literal"}, + {{[{<<"foo">>, <<"bar">>}]}, "{\"foo\":\"bar\"}", + "simple object literal"}, + {{[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, + "{\"foo\":\"bar\",\"baz\":123}", "another simple object literal"}, + {[], "[]", "empty array literal"}, + {[[]], "[[]]", "empty array literal inside a single element array literal"}, + {[1, <<"foo">>], "[1,\"foo\"]", "simple non-empty array literal"}, + {[1199344435545.0, 1], "[1199344435545.0,1]", + "another simple non-empty array literal"}, + {[false, true, 321, null], "[false, true, 321, null]", "array of literals"}, + {{[{<<"foo">>, [123]}]}, "{\"foo\":[123]}", + "object literal with an array valued property"}, + {{[{<<"foo">>, {[{<<"bar">>, true}]}}]}, + "{\"foo\":{\"bar\":true}}", "nested object literal"}, + {{[{<<"foo">>, []}, {<<"bar">>, {[{<<"baz">>, true}]}}, + {<<"alice">>, <<"bob">>}]}, + "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}", + "complex object literal"}, + {[-123, <<"foo">>, {[{<<"bar">>, []}]}, null], + "[-123,\"foo\",{\"bar\":[]},null]", + "complex array literal"} + ] +). + + +raw_json_input_test_() -> + Tests = lists:map( + fun({EJson, JsonString, Desc}) -> + {Desc, + ?_assert(equiv(EJson, json_stream_parse:to_ejson(JsonString)))} + end, ?CASES), + {"Tests with raw JSON string as the input", Tests}. + +one_byte_data_fun_test_() -> + Tests = lists:map( + fun({EJson, JsonString, Desc}) -> + DataFun = fun() -> single_byte_data_fun(JsonString) end, + {Desc, + ?_assert(equiv(EJson, json_stream_parse:to_ejson(DataFun)))} + end, ?CASES), + {"Tests with a 1 byte output data function as the input", Tests}. + +test_multiple_bytes_data_fun_test_() -> + Tests = lists:map( + fun({EJson, JsonString, Desc}) -> + DataFun = fun() -> multiple_bytes_data_fun(JsonString) end, + {Desc, + ?_assert(equiv(EJson, json_stream_parse:to_ejson(DataFun)))} + end, ?CASES), + {"Tests with a multiple bytes output data function as the input", Tests}. + + +%% Test for equivalence of Erlang terms. +%% Due to arbitrary order of construction, equivalent objects might +%% compare unequal as erlang terms, so we need to carefully recurse +%% through aggregates (tuples and objects). +equiv({Props1}, {Props2}) -> + equiv_object(Props1, Props2); +equiv(L1, L2) when is_list(L1), is_list(L2) -> + equiv_list(L1, L2); +equiv(N1, N2) when is_number(N1), is_number(N2) -> + N1 == N2; +equiv(B1, B2) when is_binary(B1), is_binary(B2) -> + B1 == B2; +equiv(true, true) -> + true; +equiv(false, false) -> + true; +equiv(null, null) -> + true. + +%% Object representation and traversal order is unknown. +%% Use the sledgehammer and sort property lists. +equiv_object(Props1, Props2) -> + L1 = lists:keysort(1, Props1), + L2 = lists:keysort(1, Props2), + Pairs = lists:zip(L1, L2), + true = lists:all( + fun({{K1, V1}, {K2, V2}}) -> + equiv(K1, K2) andalso equiv(V1, V2) + end, + Pairs). + +%% Recursively compare tuple elements for equivalence. +equiv_list([], []) -> + true; +equiv_list([V1 | L1], [V2 | L2]) -> + equiv(V1, V2) andalso equiv_list(L1, L2). + +single_byte_data_fun([]) -> + done; +single_byte_data_fun([H | T]) -> + {<<H>>, fun() -> single_byte_data_fun(T) end}. + +multiple_bytes_data_fun([]) -> + done; +multiple_bytes_data_fun(L) -> + N = crypto:rand_uniform(0, 7), + {Part, Rest} = split(L, N), + {list_to_binary(Part), fun() -> multiple_bytes_data_fun(Rest) end}. + +split(L, N) when length(L) =< N -> + {L, []}; +split(L, N) -> + take(N, L, []). + +take(0, L, Acc) -> + {lists:reverse(Acc), L}; +take(N, [H|L], Acc) -> + take(N - 1, L, [H | Acc]). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/test_request.erl ---------------------------------------------------------------------- diff --git a/test/test_request.erl b/test/test_request.erl new file mode 100644 index 0000000..68e4956 --- /dev/null +++ b/test/test_request.erl @@ -0,0 +1,75 @@ +% 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(test_request). + +-export([get/1, get/2, get/3]). +-export([put/2, put/3]). +-export([options/1, options/2, options/3]). +-export([request/3, request/4]). + +get(Url) -> + request(get, Url, []). + +get(Url, Headers) -> + request(get, Url, Headers). +get(Url, Headers, Opts) -> + request(get, Url, Headers, [], Opts). + + +put(Url, Body) -> + request(put, Url, [], Body). + +put(Url, Headers, Body) -> + request(put, Url, Headers, Body). + + +options(Url) -> + request(options, Url, []). + +options(Url, Headers) -> + request(options, Url, Headers). + +options(Url, Headers, Opts) -> + request(options, Url, Headers, [], Opts). + + +request(Method, Url, Headers) -> + request(Method, Url, Headers, []). + +request(Method, Url, Headers, Body) -> + request(Method, Url, Headers, Body, [], 3). + +request(Method, Url, Headers, Body, Opts) -> + request(Method, Url, Headers, Body, Opts, 3). + +request(_Method, _Url, _Headers, _Body, _Opts, 0) -> + {error, request_failed}; +request(Method, Url, Headers, Body, Opts, N) -> + case code:is_loaded(ibrowse) of + false -> + {ok, _} = ibrowse:start(); + _ -> + ok + end, + case ibrowse:send_req(Url, Headers, Method, Body, Opts) of + {ok, Code0, RespHeaders, RespBody0} -> + Code = list_to_integer(Code0), + RespBody = iolist_to_binary(RespBody0), + {ok, Code, RespHeaders, RespBody}; + {error, {'EXIT', {normal, _}}} -> + % Connection closed right after a successful request that + % used the same connection. + request(Method, Url, Headers, Body, N - 1); + Error -> + Error + end. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/test_web.erl ---------------------------------------------------------------------- diff --git a/test/test_web.erl b/test/test_web.erl new file mode 100644 index 0000000..1de2cd1 --- /dev/null +++ b/test/test_web.erl @@ -0,0 +1,112 @@ +% 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(test_web). +-behaviour(gen_server). + +-include("couch_eunit.hrl"). + +-export([start_link/0, stop/0, loop/1, get_port/0, set_assert/1, check_last/0]). +-export([init/1, terminate/2, code_change/3]). +-export([handle_call/3, handle_cast/2, handle_info/2]). + +-define(SERVER, test_web_server). +-define(HANDLER, test_web_handler). +-define(DELAY, 500). + +start_link() -> + gen_server:start({local, ?HANDLER}, ?MODULE, [], []), + mochiweb_http:start([ + {name, ?SERVER}, + {loop, {?MODULE, loop}}, + {port, 0} + ]). + +loop(Req) -> + %?debugFmt("Handling request: ~p", [Req]), + case gen_server:call(?HANDLER, {check_request, Req}) of + {ok, RespInfo} -> + {ok, Req:respond(RespInfo)}; + {raw, {Status, Headers, BodyChunks}} -> + Resp = Req:start_response({Status, Headers}), + lists:foreach(fun(C) -> Resp:send(C) end, BodyChunks), + erlang:put(mochiweb_request_force_close, true), + {ok, Resp}; + {chunked, {Status, Headers, BodyChunks}} -> + Resp = Req:respond({Status, Headers, chunked}), + timer:sleep(?DELAY), + lists:foreach(fun(C) -> Resp:write_chunk(C) end, BodyChunks), + Resp:write_chunk([]), + {ok, Resp}; + {error, Reason} -> + ?debugFmt("Error: ~p", [Reason]), + Body = lists:flatten(io_lib:format("Error: ~p", [Reason])), + {ok, Req:respond({200, [], Body})} + end. + +get_port() -> + mochiweb_socket_server:get(?SERVER, port). + +set_assert(Fun) -> + ?assertEqual(ok, gen_server:call(?HANDLER, {set_assert, Fun})). + +check_last() -> + gen_server:call(?HANDLER, last_status). + +init(_) -> + {ok, nil}. + +terminate(_Reason, _State) -> + ok. + +stop() -> + gen_server:cast(?SERVER, stop). + + +handle_call({check_request, Req}, _From, State) when is_function(State, 1) -> + Resp2 = case (catch State(Req)) of + {ok, Resp} -> + {reply, {ok, Resp}, was_ok}; + {raw, Resp} -> + {reply, {raw, Resp}, was_ok}; + {chunked, Resp} -> + {reply, {chunked, Resp}, was_ok}; + Error -> + {reply, {error, Error}, not_ok} + end, + Req:cleanup(), + Resp2; +handle_call({check_request, _Req}, _From, _State) -> + {reply, {error, no_assert_function}, not_ok}; +handle_call(last_status, _From, State) when is_atom(State) -> + {reply, State, nil}; +handle_call(last_status, _From, State) -> + {reply, {error, not_checked}, State}; +handle_call({set_assert, Fun}, _From, nil) -> + {reply, ok, Fun}; +handle_call({set_assert, _}, _From, State) -> + {reply, {error, assert_function_set}, State}; +handle_call(Msg, _From, State) -> + {reply, {ignored, Msg}, State}. + +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(Msg, State) -> + ?debugFmt("Ignoring cast message: ~p", [Msg]), + {noreply, State}. + +handle_info(Msg, State) -> + ?debugFmt("Ignoring info message: ~p", [Msg]), + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}.
