summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorJoe <joe@ubuntu.localdomain>2010-02-22 12:19:15 -0800
committerJoe <joe@ubuntu.localdomain>2010-02-22 12:19:15 -0800
commit6fce297e9ff9f495b10281f2c5c78e6e0c2d48ad (patch)
tree42f34b519a411ce8f594a375d5be5c885ee37ed6 /test
merge attempt #1
Diffstat (limited to 'test')
-rw-r--r--test/Emakefile4
-rw-r--r--test/Makefile12
-rw-r--r--test/cluster_ops_test.erl83
-rw-r--r--test/mem2_code_change.erl12
-rw-r--r--test/mem_utils_test.erl97
-rw-r--r--test/membership2_test.erl126
-rw-r--r--test/mock.erl322
-rw-r--r--test/mock_genserver.erl209
-rw-r--r--test/partitions_test.erl121
-rw-r--r--test/replication_test.erl89
-rw-r--r--test/stub.erl168
-rw-r--r--test/test_suite.erl10
12 files changed, 1253 insertions, 0 deletions
diff --git a/test/Emakefile b/test/Emakefile
new file mode 100644
index 00000000..d05e4d94
--- /dev/null
+++ b/test/Emakefile
@@ -0,0 +1,4 @@
+{"*", [warn_obsolete_guard, warn_unused_import,
+ warn_shadow_vars, warn_export_vars, debug_info,
+ {i, "../include"},
+ {outdir, "../tests_ebin"}]}.
diff --git a/test/Makefile b/test/Makefile
new file mode 100644
index 00000000..45998c6e
--- /dev/null
+++ b/test/Makefile
@@ -0,0 +1,12 @@
+include ../support/include.mk
+
+all: $(EBIN_FILES_NO_DOCS)
+
+doc: $(EBIN_FILES)
+
+debug:
+ $(MAKE) DEBUG=-DDEBUG
+
+clean:
+ rm -rf $(EBIN_FILES)
+ rm -rf ../tests_ebin \ No newline at end of file
diff --git a/test/cluster_ops_test.erl b/test/cluster_ops_test.erl
new file mode 100644
index 00000000..1c692dcf
--- /dev/null
+++ b/test/cluster_ops_test.erl
@@ -0,0 +1,83 @@
+-module(cluster_ops_test).
+
+-include("../../couchdb/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+% read_quorum_test() ->
+% % we need to be running a cluster here...
+% % not sure how to start things up for unit tests
+
+% % but we're testing reads when a node is missing a doc, so disable internal
+% % replication - a bit harsh if anything else is here, but hey, it's a test
+% rpc:multicall(showroom, stop, []),
+% rpc:multicall(supervisor, terminate_child,
+% [couch_primary_services, couch_replication_supervisor]),
+% rpc:multicall(supervisor, delete_child,
+% [couch_primary_services, couch_replication_supervisor]),
+
+% % create db
+% DbName = <<"cluster_ops_test">>,
+% showroom_db:delete_db(DbName, []),
+% {Status, #db{name=DbName}} = showroom_db:create_db(DbName, []),
+% ?assertEqual(ok, Status),
+
+% % open db
+% {ok, Db} = showroom_db:open_db(DbName, []),
+
+% % make a test doc
+% Key = <<"a">>,
+% Json = {[{<<"_id">>,Key}]},
+% Doc = couch_doc:from_json_obj(Json),
+% Clock = vector_clock:create(node()),
+% NewDoc = Doc#doc{clock=Clock},
+
+% % insert a doc in two shards out of three
+% % TODO: we need N=3, need to fix that at db create time Options above
+% % (fb 1001)
+% {M,F,A} = {dynomite_couch_api, put,[Db, NewDoc, []]},
+% CorrectNodeParts = membership2:nodeparts_for_key(Key),
+% [{MissingNode, MissingPart} | BadNodeParts] = CorrectNodeParts,
+% MapFun = fun({Node,Part}) ->
+% rpc:call(Node, M, F, [[Part | A]])
+% end,
+% {Good, Bad} = pcall(MapFun, BadNodeParts, 2),
+% ?assertEqual(2, length(Good)),
+% ?assertEqual([], Bad),
+
+% % make sure it's notfound on the MissingNode
+% MissingNodeGet = rpc:call(MissingNode, dynomite_couch_api, get,
+% [[MissingPart, Db, Key, nil, []]]),
+% ?assertEqual({not_found, {[], [missing]}}, MissingNodeGet),
+
+% JsonDoc = {[{<<"_id">>,<<"a">>},
+% {<<"_rev">>,
+% <<"1-967a00dff5e02add41819138abb3284d">>}]},
+
+% % r=3 should fail
+% {r_quorum_not_met, {[{message, _M}, {good, G}, {bad, B}]}} =
+% showroom_doc:open_doc(Db, Key, nil, [{r, "3"}]),
+% ?assertEqual([JsonDoc,JsonDoc], G),
+% ?assertEqual([{not_found, missing}], B),
+
+% % r=2 should never fail (run it many times to make sure)
+% do_opens({Db, Key, nil, [{r, "2"}]}, 20),
+
+% ok.
+
+
+% pcall(MapFun, Servers, Const) ->
+% Replies = lib_misc:pmap(MapFun, Servers, Const),
+% lists:partition(fun valid/1, Replies).
+
+
+% valid({ok, _}) -> true;
+% valid(ok) -> true;
+% valid(_) -> false.
+
+
+% do_opens(_,0) -> ok;
+% do_opens({Db, DocId, Refs, Options} = Payload, Times) ->
+% {Status, _Doc} = showroom_doc:open_doc(Db, DocId, Refs, Options),
+% ?assertEqual(ok, Status),
+% do_opens(Payload, Times-1).
diff --git a/test/mem2_code_change.erl b/test/mem2_code_change.erl
new file mode 100644
index 00000000..3b0c73fb
--- /dev/null
+++ b/test/mem2_code_change.erl
@@ -0,0 +1,12 @@
+-module(mem2_code_change).
+
+-export([run/0]).
+
+run() ->
+ Pid = whereis(membership),
+ OldVsn = "0.7.1-cloudant",
+ Extra = "",
+
+ sys:suspend(Pid),
+ sys:change_code(Pid, membership2, OldVsn, Extra),
+ sys:resume(Pid).
diff --git a/test/mem_utils_test.erl b/test/mem_utils_test.erl
new file mode 100644
index 00000000..b884d94e
--- /dev/null
+++ b/test/mem_utils_test.erl
@@ -0,0 +1,97 @@
+-module(mem_utils_test).
+
+-include_lib("eunit/include/eunit.hrl").
+
+
+join_type_test() ->
+ Options = [{replace,node3}],
+ ?assertEqual({replace,node3}, mem_utils:join_type(dummy,dummy,Options)).
+
+
+pmap_from_full_test() ->
+ ?assertEqual([{n1,0},{n2,1},{n3,2},{n4,3}],
+ mem_utils:pmap_from_full(t_fullmap(0))).
+
+
+fix_mappings_nodedown_test() ->
+ {PMap0, Fullmap0} = mem_utils:fix_mappings(nodedown, n3, t_fullmap(0)),
+ % with n3 down, n1 takes over
+ ?assertEqual([{n1,0},{n2,1},{n1,2},{n4,3}], PMap0),
+ ?assertEqual(t_fullmap(1), lists:sort(Fullmap0)).
+
+
+fix_mappings_rejoin_test() ->
+ {PMap0, Fullmap0} = mem_utils:fix_mappings(nodedown, n3, t_fullmap(0)),
+ % with n3 down, n1 takes over
+ ?assertEqual([{n1,0},{n2,1},{n1,2},{n4,3}], PMap0),
+ ?assertEqual(t_fullmap(1), lists:sort(Fullmap0)),
+ % now have n3 rejoin
+ {PMap1, Fullmap1} = mem_utils:fix_mappings(rejoin, n3, Fullmap0),
+ ?assertEqual([{n1,0},{n2,1},{n3,2},{n4,3}], PMap1),
+ ?assertEqual(lists:sort(t_fullmap(0)), lists:sort(Fullmap1)).
+
+
+fix_mappings_replace_test() ->
+ {PMap0, Fullmap0} = mem_utils:fix_mappings(nodedown, n3, t_fullmap(0)),
+ % with n3 down, n1 takes over
+ ?assertEqual([{n1,0},{n2,1},{n1,2},{n4,3}], PMap0),
+ ?assertEqual(t_fullmap(1), lists:sort(Fullmap0)),
+ % now replace n3 with n5
+ {PMap2, Fullmap2} = mem_utils:fix_mappings(replace, {n3,n5}, Fullmap0),
+ ?assertEqual([{n1,0},{n2,1},{n5,2},{n4,3}], PMap2),
+ ?assertEqual(lists:sort(t_fullmap(2)), lists:sort(Fullmap2)).
+
+
+fix_mappings_already_down_test() ->
+ {_PMap0, Fullmap0} = mem_utils:fix_mappings(nodedown, n3, t_fullmap(1)),
+ ?assertEqual(t_fullmap(1), lists:sort(Fullmap0)).
+
+
+was_i_nodedown_test() ->
+ ?assertEqual(true, mem_utils:was_i_nodedown(n3, t_fullmap(1))),
+ ?assertEqual(false, mem_utils:was_i_nodedown(n3, t_fullmap(0))).
+
+
+%% test helper funs
+
+t_fullmap(0) -> % four node, four part fullmap (unsorted)
+ [{n1,0,primary},
+ {n2,0,partner},
+ {n3,0,partner},
+ {n2,1,primary},
+ {n3,1,partner},
+ {n4,1,partner},
+ {n3,2,primary},
+ {n4,2,partner},
+ {n1,2,partner},
+ {n4,3,primary},
+ {n1,3,partner},
+ {n2,3,partner}];
+t_fullmap(1) -> % like (0) above, but n3 is down (sorted)
+ [{n1,0,primary},
+ {n1,2,partner},
+ {n1,3,partner},
+ {n2,0,partner},
+ {n2,1,primary},
+ {n2,3,partner},
+ {n3,0,{nodedown,partner}},
+ {n3,1,{nodedown,partner}},
+ {n3,2,{nodedown,primary}},
+ {n4,1,partner},
+ {n4,2,partner},
+ {n4,3,primary}];
+t_fullmap(2) -> % like (0) above, but n3 is replaced w/ n5 (unsorted)
+ [{n1,0,primary},
+ {n2,0,partner},
+ {n5,0,partner},
+ {n2,1,primary},
+ {n5,1,partner},
+ {n4,1,partner},
+ {n5,2,primary},
+ {n4,2,partner},
+ {n1,2,partner},
+ {n4,3,primary},
+ {n1,3,partner},
+ {n2,3,partner}];
+t_fullmap(_Huh) ->
+ huh.
diff --git a/test/membership2_test.erl b/test/membership2_test.erl
new file mode 100644
index 00000000..ed804cc2
--- /dev/null
+++ b/test/membership2_test.erl
@@ -0,0 +1,126 @@
+%%% -*- erlang-indent-level:2 -*-
+-module(membership2_test).
+-author('cliff@powerset.com').
+-author('brad@cloudant.com').
+
+-include("../include/config.hrl").
+-include("../include/common.hrl").
+-include("../include/test.hrl").
+
+-include_lib("eunit/include/eunit.hrl").
+
+% singular_startup_sequence_test() ->
+% %% configuration:start_link(#config{n=1,r=1,w=1,q=6,directory=?TMP_DIR}),
+% {ok, _} = mock:mock(configuration),
+% mock:expects(configuration, get_config, fun(_Args) -> true end,
+% #config{n=1,r=1,w=1,q=6,directory=?TMP_DIR}, 3),
+% {ok, _} = mock:mock(replication),
+% mock:expects(replication, partners, fun({_, [a], _}) -> true end, []),
+% mock:expects(replication, partners_plus, fun({a, [a]}) -> true end, []),
+% {ok, M} = membership2:start_link(a, [a]),
+% State = gen_server:call(M, state),
+% ?assertEqual(a, State#membership.node),
+% ?assertEqual([a], State#membership.nodes),
+% mock:verify_and_stop(replication),
+% membership2:stop(M),
+% %% configuration:stop(),
+% mock:verify_and_stop(configuration),
+% ?assertMatch({ok, [[a]]}, file:consult(?TMP_FILE("a.world"))),
+% file:delete(?TMP_FILE("a.world")).
+
+% -define(NODEA, {a, ["d", "1", "4"]}).
+% -define(NODEB, {b, ["e", "3", "1"]}).
+% -define(NODEC, {c, ["f", "1", "2"]}).
+% -define(NODES, [?NODEA, ?NODEB, ?NODEC]).
+
+% multi_startup_sequence_test() ->
+% {ok, _} = mock:mock(configuration),
+% mock:expects(configuration, get_config, fun(_Args) -> true end,
+% (#config{n=3,r=1,w=1,q=6,directory=?TMP_DIR}), 3),
+% {ok, _} = mock:mock(replication),
+% VersionOne = vector_clock:create(make_ref()),
+% Pid1 = make_ref(),
+% VersionTwo = vector_clock:create(make_ref()),
+% Pid2 = make_ref(),
+% mock:expects(replication, partners, fun({_, ?NODES, _}) -> true end, [?NODEB, ?NODEC]),
+% {ok, _} = stub:stub(membership2, call_join, fun(?NODEB, ?NODEA) ->
+% {VersionOne, ?NODES, [{1,Pid1}]};
+% (?NODEC, ?NODEA) ->
+% {VersionTwo, ?NODES, [{2,Pid2}]}
+% end, 2),
+% ?debugMsg("proxied"),
+% ?debugFmt("check process code: ~p", [erlang:check_process_code(self(), membership2)]),
+% {ok, M} = membership2:start_link(?NODEA, ?NODES),
+% State = gen_server:call(M, state),
+% ?assertEqual(?NODEA, State#membership.node),
+% ?assertEqual(?NODES, State#membership.nodes),
+% % Servers = State#membership.servers,
+% % ?assertMatch([{1,Pid1},{2,Pid2}], membership2:servers_to_list(Servers)),
+% ?assertEqual(greater, vector_clock:compare(State#membership.version, VersionOne)),
+% ?assertEqual(greater, vector_clock:compare(State#membership.version, VersionTwo)),
+% mock:verify_and_stop(replication),
+% membership2:stop(M),
+% mock:verify_and_stop(configuration),
+% ?assertMatch({ok, [?NODES]}, file:consult(?TMP_FILE("a.world"))),
+% file:delete(?TMP_FILE("a.world")).
+
+% startup_and_first_servers_for_key_test() ->
+% configuration:start_link(#config{n=1,r=1,w=1,q=6,directory=?TMP_DIR}),
+% {ok, _} = mock:mock(replication),
+% mock:expects(replication, partners, fun({_, [a], _}) -> true end, []),
+% {ok, M} = membership2:start_link(a, [a]),
+% _State = gen_server:call(M, state),
+% ?assertEqual([], membership2:servers_for_key("blah")),
+% mock:verify_and_stop(replication),
+% membership2:stop(M),
+% configuration:stop(),
+% ?assertMatch({ok, [[a]]}, file:consult(?TMP_FILE("a.world"))),
+% file:delete(?TMP_FILE("a.world")).
+
+% startup_and_register_test() ->
+% configuration:start_link(#config{n=1,r=1,w=1,q=0,directory=?TMP_DIR}),
+% {ok, _} = mock:mock(replication),
+% mock:expects(replication, partners, fun({_, [?NODEA], _}) -> true end, [], 3),
+% {ok, M} = membership2:start_link(?NODEA, [?NODEA]),
+% SServer1 = make_server(),
+% SServer2 = make_server(),
+% membership2:register(1, SServer1),
+% membership2:register(1, SServer2),
+% ?assertEqual([SServer1, SServer2], membership2:servers_for_key("blah")),
+% mock:verify_and_stop(replication),
+% membership2:stop(M),
+% configuration:stop(),
+% SServer1 ! stop,
+% SServer2 ! stop,
+% file:delete(?TMP_FILE("a.world")).
+
+% handle_local_server_outage_test() ->
+% configuration:start_link(#config{n=1,r=1,w=1,q=0,directory=?TMP_DIR}),
+% {ok, _} = mock:mock(replication),
+% mock:expects(replication, partners, fun({_, [?NODEA], _}) -> true end, [], 4),
+% {ok, M} = membership2:start_link(?NODEA, [?NODEA]),
+% SServer1 = make_server(),
+% SServer2 = make_server(),
+% membership2:register(1, SServer1),
+% membership2:register(1, SServer2),
+% SServer1 ! stop,
+% timer:sleep(1),
+% ?assertEqual([SServer2], membership2:servers_for_key("blah")),
+% mock:verify_and_stop(replication),
+% membership2:stop(M),
+% configuration:stop(),
+% SServer2 ! stop,
+% file:delete(?TMP_FILE("a.world")).
+
+% full_gossip_test() ->
+% configuration:start_link(#config{n=1,r=1,w=1,q=2,directory=priv_dir()}),
+% {ok, _} = mock:mock(replication),
+% mock:expects(replication, partners, fun({_, ?NODES, _}) -> true end, [?NODEB, ?NODEC],4),
+
+
+% make_server() ->
+% spawn(fun() ->
+% receive
+% stop -> ok
+% end
+% end).
diff --git a/test/mock.erl b/test/mock.erl
new file mode 100644
index 00000000..2ecbf4f7
--- /dev/null
+++ b/test/mock.erl
@@ -0,0 +1,322 @@
+%%% -*- erlang-indent-level:2 -*-
+%%%-------------------------------------------------------------------
+%%% File: mock.erl
+%%% @author Cliff Moon <> []
+%%% @copyright 2009 Cliff Moon
+%%% @doc
+%%%
+%%% @end
+%%%
+%%% @since 2009-01-04 by Cliff Moon
+%%%-------------------------------------------------------------------
+-module(mock).
+-author('cliff@powerset.com').
+
+%% API
+-export([mock/1, proxy_call/2, proxy_call/3, expects/4, expects/5,
+ verify_and_stop/1, verify/1, stub_proxy_call/3, stop/1]).
+
+-include_lib("eunit/include/eunit.hrl").
+-include("../include/common.hrl").
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(mockstate, {old_code, module, expectations=[]}).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% @spec mock(Module::atom()) -> {ok,Mock::record()} | ignore | {error,Error}
+%% @doc Starts the server
+%% @end
+%%--------------------------------------------------------------------
+mock(Module) ->
+ case gen_server:start_link({local, mod_to_name(Module)}, mock, Module, []) of
+ {ok, Pid} -> {ok, Pid};
+ {error, Reason} -> {error, Reason}
+ end.
+
+%% @spec proxy_call(Module::atom(), Function::atom()) -> term()
+%% @doc Proxies a call to the mock server for Module without arguments
+%% @end
+proxy_call(Module, Function) ->
+ gen_server:call(mod_to_name(Module), {proxy_call, Function, {}}).
+
+%% @spec proxy_call(Module::atom(), Function::atom(), Args::tuple()) -> term()
+%% @doc Proxies a call to the mock server for Module with arguments
+%% @end
+proxy_call(Module, Function, Args) ->
+ gen_server:call(mod_to_name(Module), {proxy_call, Function, Args}).
+
+stub_proxy_call(Module, Function, Args) ->
+ RegName = list_to_atom(lists:concat([Module, "_", Function, "_stub"])),
+ Ref = make_ref(),
+ RegName ! {Ref, self(), Args},
+ ?debugFmt("sending {~p,~p,~p}", [Ref, self(), Args]),
+ receive
+ {Ref, Answer} -> Answer
+ end.
+
+%% @spec expects(Module::atom(),
+%% Function::atom(),
+%% Args::function(),
+%% Ret::function() | term() ) -> term()
+
+%% Times:: {at_least, integer()} | never | {no_more_than, integer()} | integer()) -> term()
+
+%% @doc Sets the expectation that Function of Module will be called during a
+%% test with Args. Args should be a fun predicate that will return true or
+%% false whether or not the argument list matches. The argument list of the
+%% function is passed in as a tuple. Ret is either a value to return or a fun
+%% of arity 2 to be evaluated in response to a proxied call. The first argument
+%% is the actual args from the call, the second is the call count starting
+%% with 1.
+expects(Module, Function, Args, Ret) ->
+ gen_server:call(mod_to_name(Module), {expects, Function, Args, Ret, 1}).
+
+expects(Module, Function, Args, Ret, Times) ->
+ gen_server:call(mod_to_name(Module), {expects, Function, Args, Ret, Times}).
+
+%% stub(Module, Function, Args, Ret) ->
+%% gen_server:call(mod_to_name(Module), {stub, Function, Args, Ret}).
+
+verify_and_stop(Module) ->
+ verify(Module),
+ stop(Module).
+
+verify(Module) ->
+ ?assertEqual(ok, gen_server:call(mod_to_name(Module), verify)).
+
+stop(Module) ->
+ gen_server:cast(mod_to_name(Module), stop),
+ timer:sleep(10).
+
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @doc Initiates the server
+%% @end
+%%--------------------------------------------------------------------
+init(Module) ->
+ case code:get_object_code(Module) of
+ {Module, Bin, Filename} ->
+ case replace_code(Module) of
+ ok -> {ok, #mockstate{module=Module,old_code={Module, Bin, Filename}}};
+ {error, Reason} -> {stop, Reason}
+ end;
+ error -> {stop, ?fmt("Could not get object code for module ~p", [Module])}
+ end.
+
+%%--------------------------------------------------------------------
+%% @spec
+%% handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% {stop, Reason, State}
+%% @doc Handling call messages
+%% @end
+%%--------------------------------------------------------------------
+handle_call({proxy_call, Function, Args}, _From,
+ State = #mockstate{module=Mod,expectations=Expects}) ->
+ case match_expectation(Function, Args, Expects) of
+ {matched, ReturnTerm, NewExpects} ->
+ {reply, ReturnTerm, State#mockstate{expectations=NewExpects}};
+ unmatched ->
+ {stop, ?fmt("got unexpected call to ~p:~p", [Mod,Function])}
+ end;
+
+handle_call({expects, Function, Args, Ret, Times}, _From,
+ State = #mockstate{expectations=Expects}) ->
+ {reply, ok, State#mockstate{
+ expectations=add_expectation(Function, Args, Ret, Times, Expects)}};
+
+handle_call(verify, _From, State = #mockstate{expectations=Expects,module=Mod}) ->
+ ?infoFmt("verifying ~p~n", [Mod]),
+ if
+ length(Expects) > 0 ->
+ {reply, {mismatch, format_missing_expectations(Expects, Mod)}, State};
+ true -> {reply, ok, State}
+ end.
+
+%%--------------------------------------------------------------------
+%% @spec handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @doc Handling cast messages
+%% @end
+%%--------------------------------------------------------------------
+handle_cast(stop, State) ->
+ timer:sleep(10),
+ {stop, normal, State}.
+
+%%--------------------------------------------------------------------
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @doc Handling all non call/cast messages
+%% @end
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @spec terminate(Reason, State) -> void()
+%% @doc This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any necessary
+%% cleaning up. When it returns, the gen_server terminates with Reason.
+%% The return value is ignored.
+%% @end
+%%--------------------------------------------------------------------
+terminate(_Reason, #mockstate{old_code={Module, Binary, Filename}}) ->
+ code:purge(Module),
+ code:delete(Module),
+ code:load_binary(Module, Filename, Binary),
+ timer:sleep(10).
+
+%%--------------------------------------------------------------------
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @doc Convert process state when code is changed
+%% @end
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+format_missing_expectations(Expects, Mod) ->
+ format_missing_expectations(Expects, Mod, []).
+
+format_missing_expectations([], _, Msgs) ->
+ lists:reverse(Msgs);
+
+format_missing_expectations([{Function, _Args, _Ret, Times, Called}|Expects], Mod, Msgs) ->
+ Msgs1 = [?fmt("expected ~p:~p to be called ~p times but was called ~p", [Mod,Function,Times,Called])|Msgs],
+ format_missing_expectations(Expects, Mod, Msgs1).
+
+add_expectation(Function, Args, Ret, Times, Expects) ->
+ Expects ++ [{Function, Args, Ret, Times, 0}].
+
+match_expectation(Function, Args, Expectations) ->
+ match_expectation(Function, Args, Expectations, []).
+
+match_expectation(_Function, _Args, [], _Rest) ->
+ unmatched;
+
+match_expectation(Function, Args, [{Function, Matcher, Ret, MaxTimes, Invoked}|Expects], Rest) ->
+ case Matcher(Args) of
+ true ->
+ ReturnTerm = prepare_return(Args, Ret, Invoked+1),
+ if
+ Invoked + 1 >= MaxTimes -> {matched, ReturnTerm, lists:reverse(Rest) ++ Expects};
+ true -> {matched, ReturnTerm, lists:reverse(Rest) ++ [{Function, Matcher, Ret, MaxTimes, Invoked+1}] ++ Expects}
+ end;
+ false -> match_expectation(Function, Args, Expects, [{Function,Matcher,Ret,MaxTimes,Invoked}|Rest])
+ end;
+
+match_expectation(Function, Args, [Expect|Expects], Rest) ->
+ match_expectation(Function, Args, Expects, [Expect|Rest]).
+
+prepare_return(Args, Ret, Invoked) when is_function(Ret) ->
+ Ret(Args, Invoked);
+
+prepare_return(_Args, Ret, _Invoked) ->
+ Ret.
+
+replace_code(Module) ->
+ Info = Module:module_info(),
+ Exports = get_exports(Info),
+ unload_code(Module),
+ NewFunctions = generate_functions(Module, Exports),
+ Forms = [
+ {attribute,1,module,Module},
+ {attribute,2,export,Exports}
+ ] ++ NewFunctions,
+ case compile:forms(Forms, [binary]) of
+ {ok, Module, Binary} -> case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Binary) of
+ {module, Module} -> ok;
+ {error, Reason} -> {error, Reason}
+ end;
+ error -> {error, "An undefined error happened when compiling."};
+ {error, Errors, Warnings} -> {error, Errors ++ Warnings}
+ end.
+
+unload_code(Module) ->
+ code:purge(Module),
+ code:delete(Module).
+
+get_exports(Info) ->
+ get_exports(Info, []).
+
+get_exports(Info, Acc) ->
+ case lists:keytake(exports, 1, Info) of
+ {value, {exports, Exports}, ModInfo} ->
+ get_exports(ModInfo, Acc ++ lists:filter(fun({module_info, _}) -> false; (_) -> true end, Exports));
+ _ -> Acc
+ end.
+
+%% stub_function_loop(Fun) ->
+%% receive
+%% {Ref, Pid, Args} ->
+%% ?debugFmt("received {~p,~p,~p}", [Ref, Pid, Args]),
+%% Ret = (catch Fun(Args) ),
+%% ?debugFmt("sending {~p,~p}", [Ref,Ret]),
+%% Pid ! {Ref, Ret},
+%% stub_function_loop(Fun)
+%% end.
+
+% Function -> {function, Lineno, Name, Arity, [Clauses]}
+% Clause -> {clause, Lineno, [Variables], [Guards], [Expressions]}
+% Variable -> {var, Line, Name}
+%
+generate_functions(Module, Exports) ->
+ generate_functions(Module, Exports, []).
+
+generate_functions(_Module, [], FunctionForms) ->
+ lists:reverse(FunctionForms);
+
+generate_functions(Module, [{Name,Arity}|Exports], FunctionForms) ->
+ generate_functions(Module, Exports, [generate_function(Module, Name, Arity)|FunctionForms]).
+
+generate_function(Module, Name, Arity) ->
+ {function, 1, Name, Arity, [{clause, 1, generate_variables(Arity), [], generate_expression(mock, proxy_call, Module, Name, Arity)}]}.
+
+generate_variables(0) -> [];
+generate_variables(Arity) ->
+ lists:map(fun(N) ->
+ {var, 1, list_to_atom(lists:concat(['Arg', N]))}
+ end, lists:seq(1, Arity)).
+
+generate_expression(M, F, Module, Name, 0) ->
+ [{call,1,{remote,1,{atom,1,M},{atom,1,F}}, [{atom,1,Module}, {atom,1,Name}]}];
+generate_expression(M, F, Module, Name, Arity) ->
+ [{call,1,{remote,1,{atom,1,M},{atom,1,F}}, [{atom,1,Module}, {atom,1,Name}, {tuple,1,lists:map(fun(N) ->
+ {var, 1, list_to_atom(lists:concat(['Arg', N]))}
+ end, lists:seq(1, Arity))}]}].
+
+mod_to_name(Module) ->
+ list_to_atom(lists:concat([mock_, Module])).
+
+%% replace_function(FF, Forms) ->
+%% replace_function(FF, Forms, []).
+
+%% replace_function(FF, [], Ret) ->
+%% [FF|lists:reverse(Ret)];
+
+%% replace_function({function,_,Name,Arity,Clauses}, [{function,Line,Name,Arity,_}|Forms], Ret) ->
+%% lists:reverse(Ret) ++ [{function,Line,Name,Arity,Clauses}|Forms];
+
+%% replace_function(FF, [FD|Forms], Ret) ->
+%% replace_function(FF, Forms, [FD|Ret]).
diff --git a/test/mock_genserver.erl b/test/mock_genserver.erl
new file mode 100644
index 00000000..cde41ff5
--- /dev/null
+++ b/test/mock_genserver.erl
@@ -0,0 +1,209 @@
+%%%-------------------------------------------------------------------
+%%% File: mock_genserver.erl
+%%% @author Cliff Moon <> []
+%%% @copyright 2009 Cliff Moon
+%%% @doc
+%%%
+%%% @end
+%%%
+%%% @since 2009-01-02 by Cliff Moon
+%%%-------------------------------------------------------------------
+-module(mock_genserver).
+-author('cliff@powerset.com').
+
+-behaviour(gen_server).
+
+-include_lib("eunit/include/eunit.hrl").
+
+%% API
+-export([start_link/1, stub_call/3, expects_call/3, expects_call/4, stop/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {call_stubs=[], call_expects=[], cast_expectations, info_expectations}).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% @spec start_link(Reference::atom()) -> {ok,Pid} | ignore | {error,Error}
+%% @doc Starts the server
+%% @end
+%%--------------------------------------------------------------------
+start_link(Reference) ->
+ gen_server:start_link(Reference, ?MODULE, [], []).
+
+stub_call(Server, Sym, Fun) when is_function(Fun) ->
+ gen_server:call(Server, {mock_stub_call, Sym, Fun}).
+
+expects_call(Server, Args, Fun) when is_function(Fun) ->
+ gen_server:call(Server, {mock_expects_call, Args, Fun}).
+
+expects_call(Server, Args, Fun, Times) when is_function(Fun) ->
+ gen_server:call(Server, {mock_expects_call, Args, Fun, Times}).
+
+stop(Server) ->
+ gen_server:call(Server, mock_stop).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @doc Initiates the server
+%% @end
+%%--------------------------------------------------------------------
+init([]) ->
+ {ok, #state{}}.
+
+%%--------------------------------------------------------------------
+%% @spec
+%% handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% {stop, Reason, State}
+%% @doc Handling call messages
+%% @end
+%%--------------------------------------------------------------------
+handle_call({mock_stub_call, Sym, Fun}, _From, State = #state{call_stubs=Stubs}) ->
+ {reply, ok, State#state{call_stubs=[{Sym, Fun}|Stubs]}};
+
+handle_call({mock_expects_call, Args, Fun}, _From, State = #state{call_expects=Expects}) ->
+ {reply, ok, State#state{call_expects=add_expectation(Args, Fun, at_least_once, Expects)}};
+
+handle_call({mock_expects_call, Args, Fun, Times}, _From, State = #state{call_expects=Expects}) ->
+ {reply, ok, State#state{call_expects=add_expectation(Args, Fun, Times, Expects)}};
+
+handle_call(mock_stop, _From, State) ->
+ {stop, normal, ok, State};
+
+handle_call(Request, _From, State = #state{call_stubs=Stubs,call_expects=Expects}) ->
+ % expectations have a higher priority
+ case find_expectation(Request, Expects) of
+ {found, {_, Fun, Time}, NewExpects} -> {reply, Fun(Request, Time), State#state{call_expects=NewExpects}};
+ not_found -> % look for a stub
+ case find_stub(Request, Stubs) of
+ {found, {_, Fun}} -> {reply, Fun(Request), State};
+ not_found ->
+ {stop, {unexpected_call, Request}, State}
+ end
+ end.
+
+%%--------------------------------------------------------------------
+%% @spec handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @doc Handling cast messages
+%% @end
+%%--------------------------------------------------------------------
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @doc Handling all non call/cast messages
+%% @end
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @spec terminate(Reason, State) -> void()
+%% @doc This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any necessary
+%% cleaning up. When it returns, the gen_server terminates with Reason.
+%% The return value is ignored.
+%% @end
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @doc Convert process state when code is changed
+%% @end
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+
+add_expectation(Args, Fun, Times, Expects) ->
+ Expects ++ [{Args, Fun, Times}].
+
+find_expectation(Request, Expects) ->
+ find_expectation(Request, Expects, []).
+
+find_expectation(_Request, [], _Rest) ->
+ not_found;
+
+find_expectation(Request, [{Args, Fun, Times}|Expects], Rest) ->
+ MatchFun = generate_match_fun(Args),
+ case MatchFun(Request) of
+ true ->
+ if
+ Times == at_least_once -> {found, {Args, Fun, Times}, lists:reverse(Rest) ++ [{Args, Fun, Times}] ++ Expects};
+ Times == 1 -> {found, {Args, Fun, Times}, lists:reverse(Rest) ++ Expects};
+ true -> {found, {Args, Fun, Times}, lists:reverse(Rest) ++ [{Args, Fun, Times-1}] ++ Expects}
+ end;
+ false -> find_expectation(Request, Expects, [{Args, Fun, Times}|Rest])
+ end.
+
+find_stub(Request, Stub) when is_tuple(Request) ->
+ Sym = element(1, Request),
+ find_stub(Sym, Stub);
+
+find_stub(_Sym, []) ->
+ not_found;
+
+find_stub(Sym, _Stubs) when not is_atom(Sym) ->
+ not_found;
+
+find_stub(Sym, [{Sym, Fun}|_Stubs]) ->
+ {found, {Sym, Fun}};
+
+find_stub(Sym, [_Stub|Stubs]) ->
+ find_stub(Sym, Stubs).
+
+generate_match_fun(Args) when is_tuple(Args) ->
+ generate_match_fun(tuple_to_list(Args));
+
+generate_match_fun(Args) when not is_list(Args) ->
+ generate_match_fun([Args]);
+
+generate_match_fun(Args) when is_list(Args) ->
+ Src = generate_match_fun("fun({", Args),
+ {ok, Tokens, _} = erl_scan:string(Src),
+ {ok, [Form]} = erl_parse:parse_exprs(Tokens),
+ {value, Fun, _} = erl_eval:expr(Form, erl_eval:new_bindings()),
+ Fun.
+
+generate_match_fun(Src, []) ->
+ Src ++ "}) -> true; (_) -> false end.";
+
+% unbound atom means you don't care about an arg
+generate_match_fun(Src, [unbound|Args]) ->
+ if
+ length(Args) > 0 -> generate_match_fun(Src ++ "_,", Args);
+ true -> generate_match_fun(Src ++ "_", Args)
+ end;
+
+generate_match_fun(Src, [Bound|Args]) ->
+ Term = lists:flatten(io_lib:format("~w", [Bound])),
+ if
+ length(Args) > 0 -> generate_match_fun(Src ++ Term ++ ",", Args);
+ true -> generate_match_fun(Src ++ Term, Args)
+ end.
diff --git a/test/partitions_test.erl b/test/partitions_test.erl
new file mode 100644
index 00000000..20effd8a
--- /dev/null
+++ b/test/partitions_test.erl
@@ -0,0 +1,121 @@
+%%% -*- erlang-indent-level:2 -*-
+-module(partitions_test).
+-author('brad@cloudant.com').
+
+-include("../include/config.hrl").
+-include("../include/common.hrl").
+-include("../include/test.hrl").
+
+
+join_test() ->
+ TableA = [{a,1},{a,2},{a,3},{a,4},{a,5},{a,6},{a,7},{a,8}],
+ TableB = [{a,1},{a,2},{a,3},{a,4},{b,5},{b,6},{b,7},{b,8}],
+ TableC = [{a,1},{a,2},{a,3},{c,4},{b,5},{b,6},{b,7},{c,8}],
+ TableD = [{a,1},{a,2},{d,3},{c,4},{b,5},{b,6},{d,7},{c,8}],
+ TableE = [{a,1},{a,2},{d,3},{c,4},{b,5},{b,6},{e,7},{c,8}],
+ TableF = [{a,1},{a,2},{d,3},{c,4},{b,5},{b,6},{e,7},{f,8}],
+ TableG = [{a,1},{a,2},{d,3},{c,4},{b,5},{g,6},{e,7},{f,8}],
+ TableH = [{a,1},{h,2},{d,3},{c,4},{b,5},{g,6},{e,7},{f,8}],
+ ?assertEqual({ok,TableA}, partitions:join(a, TableA, [])),
+ ?assertEqual({ok,TableB}, partitions:join(b, TableA, [])),
+ ?assertEqual({ok,TableC}, partitions:join(c, TableB, [])),
+ ?assertEqual({ok,TableD}, partitions:join(d, TableC, [])),
+ ?assertEqual({ok,TableE}, partitions:join(e, TableD, [])),
+ ?assertEqual({ok,TableF}, partitions:join(f, TableE, [])),
+ ?assertEqual({ok,TableG}, partitions:join(g, TableF, [])),
+ ?assertEqual({ok,TableH}, partitions:join(h, TableG, [])),
+ ?assertEqual({error, "Too many nodes vs partitions", TableH},
+ partitions:join(i, TableH, [])),
+ ok.
+
+
+hints_test() ->
+ TableA = [{a,1},{a,2},{a,3},{a,4},{a,5},{a,6},{a,7},{a,8}],
+ TableB = [{a,1},{b,2},{a,3},{a,4},{a,5},{b,6},{b,7},{b,8}],
+ TableC = [{a,1},{a,2},{a,3},{a,4},{c,5},{c,6},{c,7},{c,8}],
+ TableD = [{d,1},{d,2},{d,3},{d,4},{a,5},{a,6},{a,7},{a,8}],
+ ?assertEqual({ok, TableB}, partitions:join(b, TableA, [2])),
+ ?assertEqual({ok, TableC}, partitions:join(c, TableA, [0])),
+ ?assertEqual({ok, TableD}, partitions:join(d, TableA, [1,2,3,4])),
+ ok.
+
+
+shard_name_test() ->
+ ?assertEqual(<<"x000000/dbname_000000">>,
+ partitions:shard_name(0, <<"dbname">>)),
+ ok.
+
+
+%% note: fullmaps used here
+diff_same_length_test() ->
+ OldMap = [{a,1, type},{a,2, type},{b,3, type},{b,4, type}],
+ NewMap = [{a,1, type},{a,2, type},{b,3, type},{c,4, type}],
+ ?assertEqual([{b,c,4}], partitions:diff(OldMap, NewMap)),
+ ok.
+
+
+diff_dupes_test() ->
+ OldMap = [{'node1@node1.boorad.local',0,primary},
+ {'node2@node2.boorad.local',0,partner},
+ {'node3@node3.boorad.local',0,partner},
+ {'node1@node1.boorad.local',182687704666362864775460604089535377456991567872, primary},
+ {'node2@node2.boorad.local',182687704666362864775460604089535377456991567872, partner},
+ {'node3@node3.boorad.local',182687704666362864775460604089535377456991567872, partner},
+ {'node1@node1.boorad.local',365375409332725729550921208179070754913983135744, primary},
+ {'node2@node2.boorad.local',365375409332725729550921208179070754913983135744, partner},
+ {'node3@node3.boorad.local',365375409332725729550921208179070754913983135744, partner},
+ {'node1@node1.boorad.local',548063113999088594326381812268606132370974703616, partner},
+ {'node2@node2.boorad.local',548063113999088594326381812268606132370974703616, partner},
+ {'node3@node3.boorad.local',548063113999088594326381812268606132370974703616, primary},
+ {'node1@node1.boorad.local',730750818665451459101842416358141509827966271488, partner},
+ {'node2@node2.boorad.local',730750818665451459101842416358141509827966271488, primary},
+ {'node3@node3.boorad.local',730750818665451459101842416358141509827966271488, partner},
+ {'node1@node1.boorad.local',913438523331814323877303020447676887284957839360, partner},
+ {'node2@node2.boorad.local',913438523331814323877303020447676887284957839360, primary},
+ {'node3@node3.boorad.local',913438523331814323877303020447676887284957839360, partner},
+ {'node1@node1.boorad.local',1096126227998177188652763624537212264741949407232, partner},
+ {'node2@node2.boorad.local',1096126227998177188652763624537212264741949407232, primary},
+ {'node3@node3.boorad.local',1096126227998177188652763624537212264741949407232, partner},
+ {'node1@node1.boorad.local',1278813932664540053428224228626747642198940975104, partner},
+ {'node2@node2.boorad.local',1278813932664540053428224228626747642198940975104, partner},
+ {'node3@node3.boorad.local',1278813932664540053428224228626747642198940975104, primary}],
+ NewMap = [{'node1@node1.boorad.local',0,primary},
+ {'node2@node2.boorad.local',0,partner},
+ {'node3@node3.boorad.local',0,partner},
+ {'node1@node1.boorad.local',182687704666362864775460604089535377456991567872, primary},
+ {'node2@node2.boorad.local',182687704666362864775460604089535377456991567872, partner},
+ {'node3@node3.boorad.local',182687704666362864775460604089535377456991567872, partner},
+ {'node1@node1.boorad.local',365375409332725729550921208179070754913983135744, partner},
+ {'node2@node2.boorad.local',365375409332725729550921208179070754913983135744, partner},
+ {'node4@node4.boorad.local',365375409332725729550921208179070754913983135744, primary},
+ {'node1@node1.boorad.local',548063113999088594326381812268606132370974703616, partner},
+ {'node3@node3.boorad.local',548063113999088594326381812268606132370974703616, primary},
+ {'node4@node4.boorad.local',548063113999088594326381812268606132370974703616, partner},
+ {'node2@node2.boorad.local',730750818665451459101842416358141509827966271488, primary},
+ {'node3@node3.boorad.local',730750818665451459101842416358141509827966271488, partner},
+ {'node4@node4.boorad.local',730750818665451459101842416358141509827966271488, partner},
+ {'node2@node2.boorad.local',913438523331814323877303020447676887284957839360, primary},
+ {'node3@node3.boorad.local',913438523331814323877303020447676887284957839360, partner},
+ {'node4@node4.boorad.local',913438523331814323877303020447676887284957839360, partner},
+ {'node1@node1.boorad.local',1096126227998177188652763624537212264741949407232, partner},
+ {'node2@node2.boorad.local',1096126227998177188652763624537212264741949407232, partner},
+ {'node4@node4.boorad.local',1096126227998177188652763624537212264741949407232, primary},
+ {'node1@node1.boorad.local',1278813932664540053428224228626747642198940975104, partner},
+ {'node3@node3.boorad.local',1278813932664540053428224228626747642198940975104, primary},
+ {'node4@node4.boorad.local',1278813932664540053428224228626747642198940975104, partner}],
+
+ Diff = [{'node3@node3.boorad.local','node4@node4.boorad.local',
+ 365375409332725729550921208179070754913983135744},
+ {'node2@node2.boorad.local','node4@node4.boorad.local',
+ 548063113999088594326381812268606132370974703616},
+ {'node1@node1.boorad.local','node4@node4.boorad.local',
+ 730750818665451459101842416358141509827966271488},
+ {'node1@node1.boorad.local','node4@node4.boorad.local',
+ 913438523331814323877303020447676887284957839360},
+ {'node3@node3.boorad.local','node4@node4.boorad.local',
+ 1096126227998177188652763624537212264741949407232},
+ {'node2@node2.boorad.local','node4@node4.boorad.local',
+ 1278813932664540053428224228626747642198940975104}],
+
+ ?assertEqual(Diff, partitions:diff(OldMap, NewMap)),
+ ok.
diff --git a/test/replication_test.erl b/test/replication_test.erl
new file mode 100644
index 00000000..095e1b44
--- /dev/null
+++ b/test/replication_test.erl
@@ -0,0 +1,89 @@
+%%% -*- erlang-indent-level:2 -*-
+-module(replication_test).
+-author('brad@cloudant.com').
+
+-include("../include/config.hrl").
+-include("../include/test.hrl").
+
+-include_lib("eunit/include/eunit.hrl").
+
+-define(NODEA, {a, ["d", "1", "4"]}).
+-define(NODEB, {b, ["e", "3", "1"]}).
+-define(NODEC, {c, ["f", "1", "2"]}).
+-define(NODED, {d, ["e", "1", "2"]}).
+-define(NODEE, {e, ["e", "2", "2"]}).
+-define(NODES, [?NODEA, ?NODEB, ?NODEC, ?NODED, ?NODEE]).
+
+%% TODO: give this some effigy love, mock configuration up all of these
+%% different ways.
+
+metadata_level_1_test() ->
+ configuration:start_link(#config{n=3,r=1,w=1,q=6,
+ directory=?TMP_DIR,
+ meta=[{datacenter,roundrobin},
+ {rack, roundrobin},
+ {slot, roundrobin}
+ ]}),
+ Partners = replication:partners(?NODEA,
+ [?NODEA, ?NODEB, ?NODEC],
+ configuration:get_config()),
+ ?assertEqual([?NODEB, ?NODEC], Partners),
+ configuration:stop().
+
+
+metadata_level_2_test() ->
+ configuration:start_link(#config{n=3,r=1,w=1,q=6,
+ directory=?TMP_DIR,
+ meta=[{datacenter,roundrobin},
+ {rack, roundrobin},
+ {slot, roundrobin}
+ ]}),
+ Partners = replication:partners(?NODEA,
+ ?NODES,
+ configuration:get_config()),
+ ?assertEqual([?NODED,?NODEE], Partners),
+ configuration:stop().
+
+
+no_metadata_test() ->
+ configuration:start_link(#config{n=2,r=1,w=1,q=6,
+ directory=?TMP_DIR,
+ meta=[]}),
+ Partners = replication:partners(a,
+ [a,b,c,d],
+ configuration:get_config()),
+ ?assertEqual([b], Partners),
+ configuration:stop().
+
+
+wrap_test() ->
+ configuration:start_link(#config{n=3,r=1,w=1,q=6,
+ directory=?TMP_DIR,
+ meta=[]}),
+ Wrap1Partners = replication:partners(c,
+ [a,b,c,d],
+ configuration:get_config()),
+ ?assertEqual([a,d], Wrap1Partners),
+ Wrap2Partners = replication:partners(d,
+ [a,b,c,d],
+ configuration:get_config()),
+ ?assertEqual([a,b], Wrap2Partners),
+ configuration:stop().
+
+
+self_test() ->
+ configuration:start_link(#config{n=3,r=1,w=1,q=6,
+ directory=?TMP_DIR,
+ meta=[]}),
+ Partners = replication:partners(a, [a],
+ configuration:get_config()),
+ ?assertEqual([], Partners),
+ configuration:stop().
+
+
+remove_self_test() ->
+ configuration:start_link(
+ #config{n=4,r=1,w=1,q=6, directory=?TMP_DIR, meta=[]}),
+ Partners = replication:partners(a, [a,b], configuration:get_config()),
+ ?assertEqual([b], Partners),
+ configuration:stop().
diff --git a/test/stub.erl b/test/stub.erl
new file mode 100644
index 00000000..2a6173b5
--- /dev/null
+++ b/test/stub.erl
@@ -0,0 +1,168 @@
+%%%-------------------------------------------------------------------
+%%% File: stub.erl
+%%% @author Cliff Moon <> []
+%%% @copyright 2009 Cliff Moon
+%%% @doc
+%%%
+%%% @end
+%%%
+%%% @since 2009-05-10 by Cliff Moon
+%%%-------------------------------------------------------------------
+-module(stub).
+-author('cliff@powerset.com').
+
+-behaviour(gen_server).
+
+%% API
+-export([stub/3, stub/4, proxy_call/3]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include_lib("eunit/include/eunit.hrl").
+-include("../include/common.hrl").
+
+-record(state, {old_code, module, stub, times}).
+
+%%====================================================================
+%% API
+%%====================================================================
+
+stub(Module, Function, Fun) ->
+ stub(Module, Function, Fun, 1).
+
+stub(Module, Function, Fun, Times) when is_function(Fun) ->
+ gen_server:start({local, name(Module, Function)}, ?MODULE, [Module, Function, Fun, Times], []).
+
+proxy_call(_, Name, Args) ->
+ {Times, Reply} = gen_server:call(Name, {proxy_call, Args}),
+ if
+ Times =< 0 -> gen_server:cast(Name, stop);
+ true -> ok
+ end,
+ Reply.
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @doc Initiates the server
+%% @end
+%%--------------------------------------------------------------------
+init([Module, Function, Fun, Times]) ->
+ case code:get_object_code(Module) of
+ {Module, Bin, Filename} ->
+ ?debugMsg("stubbing"),
+ stub_function(Module, Function, arity(Fun)),
+ {ok, #state{module=Module,old_code={Module,Bin,Filename},times=Times,stub=Fun}};
+ error -> {stop, ?fmt("Could not get object code for module ~p", [Module])}
+ end.
+
+%%--------------------------------------------------------------------
+%% @spec
+%% handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% {stop, Reason, State}
+%% @doc Handling call messages
+%% @end
+%%--------------------------------------------------------------------
+handle_call({proxy_call, Args}, _From, State = #state{stub=Fun, times=Times}) ->
+ Reply = apply(Fun, tuple_to_list(Args)),
+ {reply, {Times-1, Reply}, State#state{times=Times-1}}.
+
+%%--------------------------------------------------------------------
+%% @spec handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @doc Handling cast messages
+%% @end
+%%--------------------------------------------------------------------
+handle_cast(stop, State) ->
+ sleep:timer(10),
+ {stop, normal, State}.
+
+%%--------------------------------------------------------------------
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @doc Handling all non call/cast messages
+%% @end
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @spec terminate(Reason, State) -> void()
+%% @doc This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any necessary
+%% cleaning up. When it returns, the gen_server terminates with Reason.
+%% The return value is ignored.
+%% @end
+%%--------------------------------------------------------------------
+terminate(_Reason, #state{old_code={_Module,_Bin,_Filename}}) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @doc Convert process state when code is changed
+%% @end
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+name(Module, Function) ->
+ list_to_atom(lists:concat([Module, Function, "stub"])).
+
+stub_function(Module, Function, Arity) ->
+ {_, Bin, _} = code:get_object_code(Module),
+ {ok, {Module,[{abstract_code,{raw_abstract_v1,Forms}}]}} = beam_lib:chunks(Bin, [abstract_code]),
+ ?debugMsg("replacing function"),
+ StubbedForms = replace_function(Module, Function, Arity, Forms),
+ case compile:forms(StubbedForms, [binary]) of
+ {ok, Module, Binary} -> code:load_binary(Module, atom_to_list(Module) ++ ".erl", Binary);
+ Other -> Other
+ end.
+
+arity(Fun) when is_function(Fun) ->
+ Props = erlang:fun_info(Fun),
+ proplists:get_value(arity, Props).
+
+replace_function(Module, Function, Arity, Forms) ->
+ replace_function(Module, Function, Arity, Forms, []).
+
+replace_function(_Module, _Function, _Arity, [], Acc) ->
+ lists:reverse(Acc);
+replace_function(Module, Function, Arity, [{function, Line, Function, Arity, _Clauses}|Forms], Acc) ->
+ lists:reverse(Acc) ++ [{function, Line, Function, Arity, [
+ {clause,
+ Line,
+ generate_variables(Arity),
+ [],
+ generate_expression(stub,proxy_call,Module,name(Module,Function),Arity)}]}] ++ Forms;
+replace_function(Module, Function, Arity, [Form|Forms], Acc) ->
+ replace_function(Module, Function, Arity, Forms, [Form|Acc]).
+
+generate_variables(0) -> [];
+generate_variables(Arity) ->
+ lists:map(fun(N) ->
+ {var, 1, list_to_atom(lists:concat(['Arg', N]))}
+ end, lists:seq(1, Arity)).
+
+generate_expression(M, F, Module, Name, 0) ->
+ [{call,1,{remote,1,{atom,1,M},{atom,1,F}}, [{atom,1,Module}, {atom,1,Name}]}];
+generate_expression(M, F, Module, Name, Arity) ->
+ [{call,1,{remote,1,{atom,1,M},{atom,1,F}}, [{atom,1,Module}, {atom,1,Name}, {tuple,1,lists:map(fun(N) ->
+ {var, 1, list_to_atom(lists:concat(['Arg', N]))}
+ end, lists:seq(1, Arity))}]}].
diff --git a/test/test_suite.erl b/test/test_suite.erl
new file mode 100644
index 00000000..255ed5a9
--- /dev/null
+++ b/test/test_suite.erl
@@ -0,0 +1,10 @@
+-module(test_suite).
+
+-include_lib("eunit/include/eunit.hrl").
+
+all_test_() ->
+ [{module, mem_utils_test},
+ {module, membership2_test},
+ {module, partitions_test},
+ {module, replication_test}
+ ].