You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ch...@apache.org on 2014/08/26 23:00:17 UTC
[26/50] [abbrv] Move files out of test/couchdb into top level test/
folder
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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/516a7c2d/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}.