%% @doc A module to be used where gen_tcp is expected. %% %% A "fake socket" is used in test where we need to mock socket communication. %% It is a pid maintaining a list of expected send and recv events. -module(mock_tcp). %% gen_tcp interface functions. -export([send/2, recv/2, recv/3]). %% Functions to setup the mock_tcp. -export([create/1, close/1]). %% @doc Creates a mock_tcp process with a buffer of expected recv/2,3 and send/2 %% calls. The pid of the mock_tcp process is returned. -spec create([{recv, binary()} | {send, binary()}]) -> pid(). create(ExpectedEvents) -> spawn_link(fun () -> loop(ExpectedEvents) end). %% @doc Receives NumBytes bytes from mock_tcp Pid. This function can be used %% as a replacement for gen_tcp:recv/2 in unit tests. If there not enough data %% in the mock_tcp's buffer, an error is raised. recv(Pid, NumBytes) -> Pid ! {recv, NumBytes, self()}, receive {ok, Data} -> {ok, Data}; error -> error({unexpected_recv, NumBytes}) after 100 -> error(noreply) end. recv(Pid, NumBytes, _Timeout) -> recv(Pid, NumBytes). %% @doc Sends data to a mock_tcp. This can be used as replacement for %% gen_tcp:send/2 in unit tests. If the data sent is not what the mock_tcp %% expected, an error is raised. send(Pid, Data) -> Pid ! {send, iolist_to_binary(Data), self()}, receive ok -> ok; error -> error({unexpected_send, Data}) after 100 -> error(noreply) end. %% Stops the mock_tcp process. If the mock_tcp's buffer is not empty, %% an error is raised. close(Pid) -> Pid ! {done, self()}, receive ok -> ok; {remains, Remains} -> error({unexpected_close, Remains}) after 100 -> error(noreply) end. %% Used by create/1. loop(AllEvents = [{Func, Data} | Events]) -> receive {recv, NumBytes, FromPid} when Func == recv, NumBytes == size(Data) -> FromPid ! {ok, Data}, loop(Events); {recv, NumBytes, FromPid} when Func == recv, NumBytes < size(Data) -> <> = Data, FromPid ! {ok, Data1}, loop([{recv, Rest} | Events]); {send, Bytes, FromPid} when Func == send, Bytes == Data -> FromPid ! ok, loop(Events); {send, Bytes, FromPid} when Func == send, size(Bytes) < size(Data) -> Size = size(Bytes), case Data of <> -> FromPid ! ok, loop([{send, Rest} | Events]); _ -> FromPid ! error end; {_, _, FromPid} -> FromPid ! error; {done, FromPid} -> FromPid ! {remains, AllEvents} end; loop([]) -> receive {done, FromPid} -> FromPid ! ok; {_, _, FromPid} -> FromPid ! error end. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). %% Tests for the mock_tcp functions. bad_recv_test() -> Pid = create([{recv, <<"foobar">>}]), ?assertError(_, recv(Pid, 10)). success_test() -> Pid = create([{recv, <<"foobar">>}, {send, <<"baz">>}]), %?assertError({unexpected_close, _}, close(Pid)), ?assertEqual({ok, <<"foo">>}, recv(Pid, 3)), ?assertEqual({ok, <<"bar">>}, recv(Pid, 3)), ?assertEqual(ok, send(Pid, <<"baz">>)), ?assertEqual(ok, close(Pid)), %% The process will exit after close. Another recv will raise noreply. ?assertError(noreply, recv(Pid, 3)). -endif.