aboutsummaryrefslogtreecommitdiff
path: root/learn-you-some-erlang/reminder
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2024-02-23 07:08:18 +0100
committerTrygve Laugstøl <trygvis@inamo.no>2024-02-23 07:08:18 +0100
commit5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e (patch)
tree982ca2e7f9ac4e8c350dfb5c4f60bcfdfff5afaf /learn-you-some-erlang/reminder
parent05ae56e5e89abf2993f84e6d52b250131f247c35 (diff)
downloaderlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.gz
erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.bz2
erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.xz
erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.zip
wip
Diffstat (limited to 'learn-you-some-erlang/reminder')
-rw-r--r--learn-you-some-erlang/reminder/Emakefile4
-rw-r--r--learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/src/dev_event.erl82
-rw-r--r--learn-you-some-erlang/reminder/src/event.erl61
-rw-r--r--learn-you-some-erlang/reminder/src/evserv.erl168
-rw-r--r--learn-you-some-erlang/reminder/src/sup.erl22
-rw-r--r--learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl41
-rw-r--r--learn-you-some-erlang/reminder/src/tests/event_tests.erl43
-rw-r--r--learn-you-some-erlang/reminder/src/tests/evserv_tests.erl203
-rw-r--r--learn-you-some-erlang/reminder/src/tests/sup_tests.erl25
13 files changed, 653 insertions, 0 deletions
diff --git a/learn-you-some-erlang/reminder/Emakefile b/learn-you-some-erlang/reminder/Emakefile
new file mode 100644
index 0000000..fbde9cb
--- /dev/null
+++ b/learn-you-some-erlang/reminder/Emakefile
@@ -0,0 +1,4 @@
+{'src/*', [debug_info,
+ {i, "src"},
+ {i, "include"},
+ {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/src/dev_event.erl b/learn-you-some-erlang/reminder/src/dev_event.erl
new file mode 100644
index 0000000..5b752ac
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/dev_event.erl
@@ -0,0 +1,82 @@
+%%% This module is there to test the incomplete loops and constructs
+%%% that are presented in the text, but are not the final result.
+-module(dev_event).
+-compile(export_all).
+-record(state, {server,
+ name="",
+ to_go=0}).
+
+start1(EventName, Delay) ->
+ spawn(?MODULE, init1, [self(), EventName, Delay]).
+
+start_link1(EventName, Delay) ->
+ spawn_link(?MODULE, init1, [self(), EventName, Delay]).
+
+start2(EventName, Delay) ->
+ spawn(?MODULE, init2, [self(), EventName, Delay]).
+
+start_link2(EventName, Delay) ->
+ spawn_link(?MODULE, init2, [self(), EventName, Delay]).
+
+cancel(Pid) ->
+ %% Monitor in case the process is already dead
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {self(), Ref, cancel},
+ receive
+ {Ref, ok} ->
+ erlang:demonitor(Ref, [flush]),
+ ok;
+ {'DOWN', Ref, process, Pid, _Reason} ->
+ ok
+ end.
+
+
+
+%%% Event's innards
+init1(Server, EventName, Delay) ->
+ loop2(#state{server=Server,
+ name=EventName,
+ to_go=normalize(Delay)}).
+
+init2(Server, EventName, DateTime) ->
+ loop2(#state{server=Server,
+ name=EventName,
+ to_go=time_to_go(DateTime)}).
+
+loop1(S = #state{server=Server}) ->
+ receive
+ {Server, Ref, cancel} ->
+ Server ! {Ref, ok}
+ after S#state.to_go * 1000 ->
+ Server ! {done, S#state.name}
+ end.
+
+%% Loop uses a list for times in order to go around the ~49 days limit
+%% on timeouts.
+loop2(S = #state{server=Server, to_go=[T|Next]}) ->
+ receive
+ {Server, Ref, cancel} ->
+ Server ! {Ref, ok}
+ after T*1000 ->
+ if Next =:= [] ->
+ Server ! {done, S#state.name};
+ Next =/= [] ->
+ loop2(S#state{to_go=Next})
+ end
+ end.
+
+
+time_to_go(TimeOut={{_,_,_}, {_,_,_}}) ->
+ Now = calendar:local_time(),
+ ToGo = calendar:datetime_to_gregorian_seconds(TimeOut) -
+ calendar:datetime_to_gregorian_seconds(Now),
+ Secs = if ToGo > 0 -> ToGo;
+ ToGo =< 0 -> 0
+ end,
+ normalize(Secs).
+
+%% Because Erlang is limited to about 49 days (49*24*60*60*1000) in
+%% milliseconds, the following function is used
+normalize(N) ->
+ Limit = 49*24*60*60,
+ [N rem Limit | lists:duplicate(N div Limit, Limit)].
diff --git a/learn-you-some-erlang/reminder/src/event.erl b/learn-you-some-erlang/reminder/src/event.erl
new file mode 100644
index 0000000..58b5b57
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/event.erl
@@ -0,0 +1,61 @@
+-module(event).
+-export([start/2, start_link/2, cancel/1]).
+-export([init/3, loop/1]).
+-record(state, {server,
+ name="",
+ to_go=0}).
+
+%%% Public interface
+start(EventName, DateTime) ->
+ spawn(?MODULE, init, [self(), EventName, DateTime]).
+
+start_link(EventName, DateTime) ->
+ spawn_link(?MODULE, init, [self(), EventName, DateTime]).
+
+cancel(Pid) ->
+ %% Monitor in case the process is already dead
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {self(), Ref, cancel},
+ receive
+ {Ref, ok} ->
+ erlang:demonitor(Ref, [flush]),
+ ok;
+ {'DOWN', Ref, process, Pid, _Reason} ->
+ ok
+ end.
+
+%%% Event's innards
+init(Server, EventName, DateTime) ->
+ loop(#state{server=Server,
+ name=EventName,
+ to_go=time_to_go(DateTime)}).
+
+%% Loop uses a list for times in order to go around the ~49 days limit
+%% on timeouts.
+loop(S = #state{server=Server, to_go=[T|Next]}) ->
+ receive
+ {Server, Ref, cancel} ->
+ Server ! {Ref, ok}
+ after T*1000 ->
+ if Next =:= [] ->
+ Server ! {done, S#state.name};
+ Next =/= [] ->
+ loop(S#state{to_go=Next})
+ end
+ end.
+
+%%% private functions
+time_to_go(TimeOut={{_,_,_}, {_,_,_}}) ->
+ Now = calendar:local_time(),
+ ToGo = calendar:datetime_to_gregorian_seconds(TimeOut) -
+ calendar:datetime_to_gregorian_seconds(Now),
+ Secs = if ToGo > 0 -> ToGo;
+ ToGo =< 0 -> 0
+ end,
+ normalize(Secs).
+
+%% Because Erlang is limited to about 49 days (49*24*60*60*1000) in
+%% milliseconds, the following function is used
+normalize(N) ->
+ Limit = 49*24*60*60,
+ [N rem Limit | lists:duplicate(N div Limit, Limit)].
diff --git a/learn-you-some-erlang/reminder/src/evserv.erl b/learn-you-some-erlang/reminder/src/evserv.erl
new file mode 100644
index 0000000..1087376
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/evserv.erl
@@ -0,0 +1,168 @@
+%% Event server
+-module(evserv).
+-compile(export_all).
+
+-record(state, {events, %% list of #event{} records
+ clients}). %% list of Pids
+
+-record(event, {name="",
+ description="",
+ pid,
+ timeout={{1970,1,1},{0,0,0}}}).
+
+%%% User Interface
+
+start() ->
+ register(?MODULE, Pid=spawn(?MODULE, init, [])),
+ Pid.
+
+start_link() ->
+ register(?MODULE, Pid=spawn_link(?MODULE, init, [])),
+ Pid.
+
+terminate() ->
+ ?MODULE ! shutdown.
+
+init() ->
+ %% Loading events from a static file could be done here.
+ %% You would need to pass an argument to init (maybe change the functions
+ %% start/0 and start_link/0 to start/1 and start_link/1) telling where the
+ %% resource to find the events is. Then load it from here.
+ %% Another option is to just pass the event straight to the server
+ %% through this function.
+ loop(#state{events=orddict:new(),
+ clients=orddict:new()}).
+
+subscribe(Pid) ->
+ Ref = erlang:monitor(process, whereis(?MODULE)),
+ ?MODULE ! {self(), Ref, {subscribe, Pid}},
+ receive
+ {Ref, ok} ->
+ {ok, Ref};
+ {'DOWN', Ref, process, _Pid, Reason} ->
+ {error, Reason}
+ after 5000 ->
+ {error, timeout}
+ end.
+
+add_event(Name, Description, TimeOut) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
+ receive
+ {Ref, Msg} -> Msg
+ after 5000 ->
+ {error, timeout}
+ end.
+
+add_event2(Name, Description, TimeOut) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
+ receive
+ {Ref, {error, Reason}} -> erlang:error(Reason);
+ {Ref, Msg} -> Msg
+ after 5000 ->
+ {error, timeout}
+ end.
+
+cancel(Name) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {cancel, Name}},
+ receive
+ {Ref, ok} -> ok
+ after 5000 ->
+ {error, timeout}
+ end.
+
+listen(Delay) ->
+ receive
+ M = {done, _Name, _Description} ->
+ [M | listen(0)]
+ after Delay*1000 ->
+ []
+ end.
+
+%%% The Server itself
+
+loop(S=#state{}) ->
+ receive
+ {Pid, MsgRef, {subscribe, Client}} ->
+ Ref = erlang:monitor(process, Client),
+ NewClients = orddict:store(Ref, Client, S#state.clients),
+ Pid ! {MsgRef, ok},
+ loop(S#state{clients=NewClients});
+ {Pid, MsgRef, {add, Name, Description, TimeOut}} ->
+ case valid_datetime(TimeOut) of
+ true ->
+ EventPid = event:start_link(Name, TimeOut),
+ NewEvents = orddict:store(Name,
+ #event{name=Name,
+ description=Description,
+ pid=EventPid,
+ timeout=TimeOut},
+ S#state.events),
+ Pid ! {MsgRef, ok},
+ loop(S#state{events=NewEvents});
+ false ->
+ Pid ! {MsgRef, {error, bad_timeout}},
+ loop(S)
+ end;
+ {Pid, MsgRef, {cancel, Name}} ->
+ Events = case orddict:find(Name, S#state.events) of
+ {ok, E} ->
+ event:cancel(E#event.pid),
+ orddict:erase(Name, S#state.events);
+ error ->
+ S#state.events
+ end,
+ Pid ! {MsgRef, ok},
+ loop(S#state{events=Events});
+ {done, Name} ->
+ case orddict:find(Name, S#state.events) of
+ {ok, E} ->
+ send_to_clients({done, E#event.name, E#event.description},
+ S#state.clients),
+ NewEvents = orddict:erase(Name, S#state.events),
+ loop(S#state{events=NewEvents});
+ error ->
+ %% This may happen if we cancel an event and
+ %% it fires at the same time
+ loop(S)
+ end;
+ shutdown ->
+ exit(shutdown);
+ {'DOWN', Ref, process, _Pid, _Reason} ->
+ loop(S#state{clients=orddict:erase(Ref, S#state.clients)});
+ code_change ->
+ ?MODULE:loop(S);
+ {Pid, debug} -> %% used as a hack to let me do some unit testing
+ Pid ! S,
+ loop(S);
+ Unknown ->
+ io:format("Unknown message: ~p~n",[Unknown]),
+ loop(S)
+ end.
+
+
+%%% Internal Functions
+send_to_clients(Msg, ClientDict) ->
+ orddict:map(fun(_Ref, Pid) -> Pid ! Msg end, ClientDict).
+
+valid_datetime({Date,Time}) ->
+ try
+ calendar:valid_date(Date) andalso valid_time(Time)
+ catch
+ error:function_clause -> %% not in {{Y,M,D},{H,Min,S}} format
+ false
+ end;
+valid_datetime(_) ->
+ false.
+
+%% calendar has valid_date, but nothing for days.
+%% This function is based on its interface.
+%% Ugly, but ugh.
+valid_time({H,M,S}) -> valid_time(H,M,S).
+
+valid_time(H,M,S) when H >= 0, H < 24,
+ M >= 0, M < 60,
+ S >= 0, S < 60 -> true;
+valid_time(_,_,_) -> false.
diff --git a/learn-you-some-erlang/reminder/src/sup.erl b/learn-you-some-erlang/reminder/src/sup.erl
new file mode 100644
index 0000000..71a463b
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/sup.erl
@@ -0,0 +1,22 @@
+-module(sup).
+-export([start/2, start_link/2, init/1, loop/1]).
+
+start(Mod, Args) ->
+ spawn(?MODULE, init, [{Mod, Args}]).
+
+start_link(Mod,Args) ->
+ spawn_link(?MODULE, init, [{Mod, Args}]).
+
+init({Mod,Args}) ->
+ process_flag(trap_exit, true),
+ loop({Mod,start_link,Args}).
+
+loop({M,F,A}) ->
+ Pid = apply(M,F,A),
+ receive
+ {'EXIT', _From, shutdown} ->
+ exit(shutdown); % will kill the child too
+ {'EXIT', Pid, Reason} ->
+ io:format("Process ~p exited for reason ~p~n",[Pid,Reason]),
+ loop({M,F,A})
+ end.
diff --git a/learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl b/learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl
new file mode 100644
index 0000000..5de18d1
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl
@@ -0,0 +1,41 @@
+-module(dev_event_tests).
+-include_lib("eunit/include/eunit.hrl").
+%%% Very minimal tests, only verifying that the two implementations of
+%%% timeouts work the same in their final and primitive versions
+%%% as in the final module. The rest is rather cruft to emulate the normal
+%%% event.erl module. Little is tested, because little needs to be tested.
+
+
+-record(state, {server,
+ name="",
+ to_go=[0]}).
+
+timeout_test_() ->
+ {inorder,
+ [fun() -> timeout(loop1, 2) end,
+ fun() -> timeout(loop2, [2]) end]}.
+
+timeout(AtomFun, T) ->
+ S = self(),
+ spawn_link(dev_event, AtomFun, [#state{server=S, name="test", to_go=T}]),
+ timer:sleep(1000),
+ M1 = receive A -> A after 0 -> timeout end,
+ timer:sleep(1500),
+ M2 = receive B -> B after 0 -> timeout end,
+ M3 = receive C -> C after 0 -> timeout end,
+ ?assertEqual(timeout, M1),
+ ?assertEqual({done, "test"}, M2),
+ ?assertEqual(timeout, M3).
+
+cancel_msg_test_() ->
+ {inorder,
+ [fun() -> cancel_msg(loop1, 2) end,
+ fun() -> cancel_msg(loop2, [2]) end]}.
+
+cancel_msg(AtomFun, T) ->
+ S = self(),
+ R = make_ref(),
+ Pid = spawn_link(dev_event, AtomFun, [#state{server=S, name="test", to_go=T}]),
+ Pid ! {S, R, cancel},
+ M = receive A -> A after 500 -> timeout end,
+ ?assertEqual({R, ok}, M).
diff --git a/learn-you-some-erlang/reminder/src/tests/event_tests.erl b/learn-you-some-erlang/reminder/src/tests/event_tests.erl
new file mode 100644
index 0000000..80b3a44
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/event_tests.erl
@@ -0,0 +1,43 @@
+-module(event_tests).
+-include_lib("eunit/include/eunit.hrl").
+-test_warnings([start/0, start_link/1, init/0, time_to_go/1]).
+%% defined in event.erl
+-record(state, {server,
+ name="",
+ to_go=[0]}).
+
+timeout_test_() ->
+ S = self(),
+ spawn_link(event, loop, [#state{server=S, name="test", to_go=[2]}]),
+ timer:sleep(1000),
+ M1 = receive A -> A after 0 -> timeout end,
+ timer:sleep(1500),
+ M2 = receive B -> B after 0 -> timeout end,
+ M3 = receive C -> C after 0 -> timeout end,
+ [?_assertEqual(timeout, M1),
+ ?_assertEqual({done, "test"}, M2),
+ ?_assertEqual(timeout, M3)].
+
+cancel_msg_test_() ->
+ S = self(),
+ R = make_ref(),
+ Pid = spawn_link(event, loop, [#state{server=S, name="test", to_go=[2]}]),
+ Pid ! {S, R, cancel},
+ M = receive A -> A after 500 -> timeout end,
+ [?_assertEqual({R, ok}, M)].
+
+cancel_fn_test_() ->
+ S = self(),
+ Pid = spawn_link(event, loop, [#state{server=S, name="test", to_go=[2]}]),
+ [?_assertEqual(ok, event:cancel(Pid)),
+ %% calling cancel again should fail, but still return ok.
+ ?_assertEqual(ok, event:cancel(Pid))].
+
+normalize_test_() ->
+ [?_assertEqual([0], event:normalize(0)),
+ ?_assertEqual([2], event:normalize(2)),
+ %% special cases w/ remainders
+ ?_assertEqual(1, length(event:normalize(49*24*60*59))),
+ ?_assertEqual(2, length(event:normalize(49*24*60*60))),
+ ?_assertEqual(2, length(event:normalize(49*24*60*60+1))),
+ ?_assertEqual(1000*24*60*60, lists:sum(event:normalize(1000*24*60*60)))].
diff --git a/learn-you-some-erlang/reminder/src/tests/evserv_tests.erl b/learn-you-some-erlang/reminder/src/tests/evserv_tests.erl
new file mode 100644
index 0000000..b093a58
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/evserv_tests.erl
@@ -0,0 +1,203 @@
+-module(evserv_tests).
+-include_lib("eunit/include/eunit.hrl").
+-define(FUTURE_DATE, {{2100,1,1},{0,0,0}}).
+%% all the tests in this module would be much easier with property-based testing.
+%% see Triq, Quvic Quickcheck or Proper for libraries to download that let you
+%% do this.
+-test_warnings([start/0]).
+
+valid_time_test_() ->
+ [?_assert(evserv:valid_time({0,0,0})),
+ ?_assert(evserv:valid_time({23,59,59})),
+ ?_assert(not evserv:valid_time({23,59,60})),
+ ?_assert(not evserv:valid_time({23,60,59})),
+ ?_assert(not evserv:valid_time({24,59,59})),
+ ?_assert(not evserv:valid_time({-1,0,0})),
+ ?_assert(not evserv:valid_time({0,-1,0})),
+ ?_assert(not evserv:valid_time({0,0,-1}))].
+
+valid_datetime_test_() ->
+ [?_assert(evserv:valid_datetime({{0,1,1},{0,0,0}})),
+ ?_assert(evserv:valid_datetime({{2004,2,29},{23,59,59}})),
+ ?_assert(evserv:valid_datetime({{2004,12,31},{23,59,59}})),
+ ?_assert(not evserv:valid_datetime({{2004,12,31},{23,60,60}})),
+ ?_assert(not evserv:valid_datetime({{2003,2,29},{23,59,59}})),
+ ?_assert(not evserv:valid_datetime({{0,0,0},{0,0,0}})),
+ ?_assert(not evserv:valid_datetime(1209312303))].
+
+loop_test_() ->
+ {"Testing all server events on a protocol level",
+ [{"Subscribe Tests",
+ [{spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun subscribe/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun subscribe2/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun down/1}}]},
+ {"Adding event integration tests",
+ [{spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun add/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun addremove/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun done/1}}]}]}.
+
+interface_test_() ->
+ {"Testing all server events via the interface functions",
+ [{spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun interface/1}}]}.
+
+subscribe(Pid) ->
+ Ref = make_ref(),
+ S = self(),
+ Pid ! {S, Ref, {subscribe, S}},
+ {ok, M1} = read(),
+ Pid ! {S, debug},
+ {ok, M2} = read(),
+ [?_assertEqual({Ref, ok}, M1),
+ ?_assertMatch({state,[], [{_, S}]}, M2)].
+
+subscribe2(Pid) ->
+ Ref1 = make_ref(),
+ Ref2 = make_ref(),
+ S = self(),
+ Pid ! {S, Ref1, {subscribe, S}},
+ {ok, M1} = read(),
+ Pid ! {S, Ref2, {subscribe, S}},
+ {ok, M2} = read(),
+ Pid ! {S, debug},
+ {ok, M3} = read(),
+ [?_assertEqual({Ref1, ok}, M1),
+ ?_assertEqual({Ref2, ok}, M2),
+ ?_assertMatch({state,[], [{_,S},{_,S}]}, M3)].
+
+down(Pid) ->
+ Ref = make_ref(),
+ ClientPid = spawn(fun() -> timer:sleep(50000) end),
+ Pid ! {self(), debug},
+ {ok, S1} = read(),
+ Pid ! {self(), Ref, {subscribe, ClientPid}},
+ {ok, M1} = read(),
+ Pid ! {self(), debug},
+ {ok, S2} = read(),
+ exit(ClientPid, testkill),
+ timer:sleep(100),
+ Pid ! {self(), debug},
+ {ok, S3} = read(),
+ [?_assertMatch({state, _, []}, S1),
+ ?_assertEqual({Ref, ok}, M1),
+ ?_assertMatch({state, _, [{_,ClientPid}]}, S2),
+ ?_assertEqual(S1, S3)].
+
+add(Pid) ->
+ Ref1 = make_ref(),
+ Ref2 = make_ref(),
+ Pid ! {self(), Ref1, {add, "test", "a test event", ?FUTURE_DATE}},
+ {ok, M1} = read(),
+ Pid ! {self(), Ref2, {add, "test", "a test event2", wrong_date}},
+ {ok, M2} = read(),
+ [?_assertEqual({Ref1, ok}, M1),
+ ?_assertEqual({Ref2, {error, bad_timeout}}, M2)].
+
+addremove(Pid) ->
+ Ref1 = make_ref(),
+ Ref2 = make_ref(),
+ Ref3 = make_ref(),
+ %% ask for useless deletion, check the state
+ Pid ! {self(), Ref1, {cancel, "nonexist"}},
+ {ok, M1} = read(),
+ Pid ! {self(), debug},
+ {ok, State1} = read(),
+ %% add an event, check the state
+ Pid ! {self(), Ref2, {add, "test", "a test event", ?FUTURE_DATE}},
+ {ok, M2} = read(),
+ Pid ! {self(), debug},
+ {ok, State2} = read(),
+ %% remove the event, check the state
+ Pid ! {self(), Ref3, {cancel, "test"}},
+ {ok, M3} = read(),
+ Pid ! {self(), debug},
+ {ok, State3} = read(),
+ [?_assertEqual({Ref1, ok}, M1),
+ ?_assertEqual({Ref2, ok}, M2),
+ ?_assertEqual({Ref3, ok}, M3),
+ ?_assertEqual(State1, State3),
+ ?_assert(State2 =/= State3),
+ ?_assertMatch({state,[{"test", {event,"test",_,_,?FUTURE_DATE}}],_}, State2)].
+
+done(Pid) ->
+ Ref = make_ref(),
+ {ok, _} = evserv:subscribe(self()),
+ Pid ! {self(), debug},
+ {ok, S1} = read(),
+ {Date,{H,Min,S}} = calendar:local_time(),
+ DateTime = {Date,{H,Min,S+1}},
+ Pid ! {self(), Ref, {add, "test", "a test event", DateTime}},
+ {ok, M1} = read(),
+ Pid ! {self(), debug},
+ {ok, S2} = read(),
+ X = read(),
+ timer:sleep(750),
+ {ok, M2} = read(),
+ Pid ! {self(), debug},
+ {ok, S3} = read(),
+ [?_assertMatch({state, [], _}, S1),
+ ?_assertEqual({Ref, ok}, M1),
+ ?_assertMatch({state, [{"test",_}], _}, S2),
+ ?_assertEqual(timeout, X),
+ ?_assertEqual({done, "test", "a test event"}, M2),
+ ?_assertEqual(S1,S3)].
+
+interface(_Pid) ->
+ Ref = evserv:subscribe(self()),
+ {Date,{H,Min,S}} = calendar:local_time(),
+ M1 = evserv:add_event("test", "desc", {Date,{H,Min,S+1}}),
+ M2 = evserv:cancel("test"),
+ M3 = evserv:add_event("test1", "desc1", calendar:local_time()),
+ M4 = evserv:add_event2("test2", "desc2", calendar:local_time()),
+ timer:sleep(100),
+ M5 = evserv:listen(2),
+ M6 = evserv:add_event("test3", "desc3", some_atom),
+ M7 = (catch evserv:add_event2("test4", "desc4", some_atom)),
+ [?_assertMatch({ok, _}, Ref),
+ ?_assert(is_reference(element(2, Ref))),
+ ?_assertEqual(ok, M1),
+ ?_assertEqual(ok, M2),
+ ?_assertEqual(ok, M3),
+ ?_assertEqual(ok, M4),
+ ?_assertEqual([{done,"test1","desc1"},{done,"test2","desc2"}], M5),
+ ?_assertEqual({error, bad_timeout}, M6),
+ ?_assertMatch({'EXIT', {bad_timeout,_}}, M7)].
+
+
+%% helpers
+terminate(Pid) -> Pid ! shutdown.
+
+read() ->
+ receive
+ M -> {ok, M}
+ after 500 ->
+ timeout
+ end.
+
diff --git a/learn-you-some-erlang/reminder/src/tests/sup_tests.erl b/learn-you-some-erlang/reminder/src/tests/sup_tests.erl
new file mode 100644
index 0000000..28d7fa9
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/sup_tests.erl
@@ -0,0 +1,25 @@
+-module(sup_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+restart_test_() ->
+ {"Test that everything restarts until a kill",
+ {setup,
+ fun() -> sup:start(evserv, []) end,
+ fun(_) -> ok end,
+ fun restart/1}}.
+
+restart(_SupPid) ->
+ timer:sleep(100),
+ A = is_pid(whereis(evserv)),
+ catch exit(whereis(evserv), die),
+ timer:sleep(100),
+ B = is_pid(whereis(evserv)),
+ catch exit(whereis(evserv), die),
+ timer:sleep(100),
+ C = is_pid(whereis(evserv)),
+ catch exit(whereis(evserv), shutdown),
+ timer:sleep(500),
+ D = is_pid(whereis(evserv)),
+ ?_assertEqual([true,true,true,false],
+ [A,B,C,D]).
+