blob: 530b7efd03af3d2521ebd9e9f9239fa6f818d63d [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couch_server_tests).
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
-include("../src/couch_server_int.hrl").
start() ->
Ctx = test_util:start_couch(),
config:set("log", "include_sasl", "false", false),
Ctx.
setup() ->
DbName = ?tempdb(),
{ok, Db} = couch_db:create(DbName, []),
Db.
setup(rename) ->
config:set("couchdb", "enable_database_recovery", "true", false),
setup();
setup(_) ->
setup().
teardown(Db) ->
FilePath = couch_db:get_filepath(Db),
(catch couch_db:close(Db)),
(catch file:delete(FilePath)).
teardown(rename, Db) ->
config:set("couchdb", "enable_database_recovery", "false", false),
teardown(Db);
teardown(_, Db) ->
teardown(Db).
delete_db_test_() ->
{
"Test for proper deletion of db file",
{
setup,
fun start/0, fun test_util:stop/1,
[
make_test_case(rename, [fun should_rename_on_delete/2]),
make_test_case(delete, [fun should_delete/2])
]
}
}.
make_test_case(Mod, Funs) ->
{
lists:flatten(io_lib:format("~s", [Mod])),
{foreachx, fun setup/1, fun teardown/2, [{Mod, Fun} || Fun <- Funs]}
}.
should_rename_on_delete(_, Db) ->
DbName = couch_db:name(Db),
Origin = couch_db:get_filepath(Db),
?_test(begin
?assert(filelib:is_regular(Origin)),
?assertMatch(ok, couch_server:delete(DbName, [])),
?assertNot(filelib:is_regular(Origin)),
DeletedFiles = deleted_files(Origin),
?assertMatch([_], DeletedFiles),
[Renamed] = DeletedFiles,
?assertEqual(
filename:extension(Origin), filename:extension(Renamed)),
?assert(filelib:is_regular(Renamed))
end).
should_delete(_, Db) ->
DbName = couch_db:name(Db),
Origin = couch_db:get_filepath(Db),
?_test(begin
?assert(filelib:is_regular(Origin)),
?assertMatch(ok, couch_server:delete(DbName, [])),
?assertNot(filelib:is_regular(Origin)),
?assertMatch([], deleted_files(Origin))
end).
deleted_files(ViewFile) ->
filelib:wildcard(filename:rootname(ViewFile) ++ "*.deleted.*").
bad_engine_option_test_() ->
{
setup,
fun start/0,
fun test_util:stop/1,
[
fun t_bad_engine_option/0
]
}.
t_bad_engine_option() ->
Resp = couch_server:create(?tempdb(), [{engine, <<"cowabunga!">>}]),
?assertEqual(Resp, {error, {invalid_engine_extension, <<"cowabunga!">>}}).
get_engine_path_test_() ->
{
setup,
fun start/0, fun test_util:stop/1,
{
foreach,
fun setup/0, fun teardown/1,
[
fun should_return_engine_path/1,
fun should_return_invalid_engine_error/1
]
}
}.
should_return_engine_path(Db) ->
DbName = couch_db:name(Db),
Engine = couch_db_engine:get_engine(Db),
Resp = couch_server:get_engine_path(DbName, Engine),
FilePath = couch_db:get_filepath(Db),
?_assertMatch({ok, FilePath}, Resp).
should_return_invalid_engine_error(Db) ->
DbName = couch_db:name(Db),
Engine = fake_engine,
Resp = couch_server:get_engine_path(DbName, Engine),
?_assertMatch({error, {invalid_engine, Engine}}, Resp).
interleaved_requests_test_() ->
{
setup,
fun start_interleaved/0,
fun stop_interleaved/1,
fun make_interleaved_requests/1
}.
start_interleaved() ->
TestDbName = ?tempdb(),
meck:new(couch_db, [passthrough]),
meck:expect(couch_db, start_link, fun(Engine, DbName, Filename, Options) ->
case DbName of
TestDbName ->
receive
go -> ok
end,
Res = meck:passthrough([Engine, DbName, Filename, Options]),
% We're unlinking and sending a delayed
% EXIT signal so that we can mimic a specific
% message order in couch_server. On a test machine
% this is a big race condition which affects the
% ability to induce the bug.
case Res of
{ok, Db} ->
DbPid = couch_db:get_pid(Db),
unlink(DbPid),
Msg = {'EXIT', DbPid, killed},
erlang:send_after(2000, whereis(couch_server), Msg);
_ ->
ok
end,
Res;
_ ->
meck:passthrough([Engine, DbName, Filename, Options])
end
end),
{test_util:start_couch(), TestDbName}.
stop_interleaved({Ctx, TestDbName}) ->
couch_server:delete(TestDbName, [?ADMIN_CTX]),
meck:unload(),
test_util:stop_couch(Ctx).
make_interleaved_requests({_, TestDbName}) ->
[
fun() -> t_interleaved_create_delete_open(TestDbName) end
].
t_interleaved_create_delete_open(DbName) ->
{CrtRef, DelRef, OpenRef} = {make_ref(), make_ref(), make_ref()},
CrtMsg = {'$gen_call', {self(), CrtRef}, {create, DbName, [?ADMIN_CTX]}},
DelMsg = {'$gen_call', {self(), DelRef}, {delete, DbName, [?ADMIN_CTX]}},
OpenMsg = {'$gen_call', {self(), OpenRef}, {open, DbName, [?ADMIN_CTX]}},
% Get the current couch_server pid so we're sure
% to not end up messaging two different pids
CouchServer = whereis(couch_server),
% Start our first instance that will succeed in
% an invalid state. Notice that the opener pid
% spawned by couch_server:open_async/5 will halt
% in our meck expect function waiting for a message.
%
% We're using raw message passing here so that we don't
% have to coordinate multiple processes for this test.
CouchServer ! CrtMsg,
{ok, Opener} = get_opener_pid(DbName),
% We have to suspend couch_server so that we can enqueue
% our next requests and let the opener finish processing.
erlang:suspend_process(CouchServer),
% Since couch_server is suspend, this delete request won't
% be processed until after the opener has sent its
% successful open response via gen_server:call/3
CouchServer ! DelMsg,
% This open request will be in the queue after the
% delete request but before the gen_server:call/3
% message which will establish the mixed up state
% in the couch_dbs ets table
CouchServer ! OpenMsg,
% First release the opener pid so it can continue
% working while we tweak meck
Opener ! go,
% Replace our expect call to meck so that the OpenMsg
% isn't blocked on the receive
meck:expect(couch_db, start_link, fun(Engine, DbName1, Filename, Options) ->
meck:passthrough([Engine, DbName1, Filename, Options])
end),
% Wait for the '$gen_call' message from OpenerPid to arrive
% in couch_server's mailbox
ok = wait_for_open_async_result(CouchServer, Opener),
% Now monitor and resume the couch_server and assert that
% couch_server does not crash while processing OpenMsg
CSRef = erlang:monitor(process, CouchServer),
erlang:resume_process(CouchServer),
check_monitor_not_triggered(CSRef),
% The create response is expected to return not_found
% due to the delete request canceling the async opener
% pid and sending not_found to all waiters unconditionally
?assertEqual({CrtRef, not_found}, get_next_message()),
% Our delete request was processed normally
?assertEqual({DelRef, ok}, get_next_message()),
% The db was deleted thus it should be not found
% when we try and open it.
?assertMatch({OpenRef, {not_found, no_db_file}}, get_next_message()),
% And finally assert that couch_server is still
% alive.
?assert(is_process_alive(CouchServer)),
check_monitor_not_triggered(CSRef).
get_opener_pid(DbName) ->
WaitFun = fun() ->
case ets:lookup(couch_dbs, DbName) of
[#entry{pid = Pid}] ->
{ok, Pid};
[] ->
wait
end
end,
test_util:wait(WaitFun).
wait_for_open_async_result(CouchServer, Opener) ->
WaitFun = fun() ->
{_, Messages} = erlang:process_info(CouchServer, messages),
Found = lists:foldl(fun(Msg, Acc) ->
case Msg of
{'$gen_call', {Opener, _}, {open_result, _, _, {ok, _}}} ->
true;
_ ->
Acc
end
end, false, Messages),
if Found -> ok; true -> wait end
end,
test_util:wait(WaitFun).
check_monitor_not_triggered(Ref) ->
receive
{'DOWN', Ref, _, _, Reason0} ->
erlang:error({monitor_triggered, Reason0})
after 100 ->
ok
end.
get_next_message() ->
receive
Msg ->
Msg
after 5000 ->
erlang:error(timeout)
end.