diff options
author | Joe <joe@ubuntu.localdomain> | 2010-02-22 12:19:15 -0800 |
---|---|---|
committer | Joe <joe@ubuntu.localdomain> | 2010-02-22 12:19:15 -0800 |
commit | 6fce297e9ff9f495b10281f2c5c78e6e0c2d48ad (patch) | |
tree | 42f34b519a411ce8f594a375d5be5c885ee37ed6 /test |
merge attempt #1
Diffstat (limited to 'test')
-rw-r--r-- | test/Emakefile | 4 | ||||
-rw-r--r-- | test/Makefile | 12 | ||||
-rw-r--r-- | test/cluster_ops_test.erl | 83 | ||||
-rw-r--r-- | test/mem2_code_change.erl | 12 | ||||
-rw-r--r-- | test/mem_utils_test.erl | 97 | ||||
-rw-r--r-- | test/membership2_test.erl | 126 | ||||
-rw-r--r-- | test/mock.erl | 322 | ||||
-rw-r--r-- | test/mock_genserver.erl | 209 | ||||
-rw-r--r-- | test/partitions_test.erl | 121 | ||||
-rw-r--r-- | test/replication_test.erl | 89 | ||||
-rw-r--r-- | test/stub.erl | 168 | ||||
-rw-r--r-- | test/test_suite.erl | 10 |
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} + ]. |