diff options
author | Trygve Laugstøl <trygvis@inamo.no> | 2024-02-23 07:08:18 +0100 |
---|---|---|
committer | Trygve Laugstøl <trygvis@inamo.no> | 2024-02-23 07:08:18 +0100 |
commit | 5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e (patch) | |
tree | 982ca2e7f9ac4e8c350dfb5c4f60bcfdfff5afaf /learn-you-some-erlang | |
parent | 05ae56e5e89abf2993f84e6d52b250131f247c35 (diff) | |
download | erlang-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')
236 files changed, 10088 insertions, 0 deletions
diff --git a/learn-you-some-erlang/LICENSE.txt b/learn-you-some-erlang/LICENSE.txt new file mode 100644 index 0000000..5be26d9 --- /dev/null +++ b/learn-you-some-erlang/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2009 Frederic Trottier-Hebert + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/learn-you-some-erlang/README.txt b/learn-you-some-erlang/README.txt new file mode 100644 index 0000000..d46e598 --- /dev/null +++ b/learn-you-some-erlang/README.txt @@ -0,0 +1,17 @@ +Man there's not a lot to write in a readme for such a book. + +You can get the license info in LICENSE.TXT (hint: it's the MIT license). +I probably won't copy it in every module. Just pretend it's there, please. + +The code in these files has been tested with Erlang releases R13B+. In case you're not sure it will run on another version, you can run the tests yourself by compiling tester.erl and then calling: + +1> tester:dir(). +or +2> tester:dir("/modules/path","/unit/tests/modules/path/"). + +Which should run all the tests. If you're using the release R13B, there is a patch you need to apply in order to have unit tests running (see http://erlang.org/download.html). + +Note that even if the tests run fine, you'd benefit in using releases R13B+. + +If you need any more help, don't hesitate contacting me: mononcqc@ferd.ca. + diff --git a/learn-you-some-erlang/band_supervisor.erl b/learn-you-some-erlang/band_supervisor.erl new file mode 100644 index 0000000..dd17ba8 --- /dev/null +++ b/learn-you-some-erlang/band_supervisor.erl @@ -0,0 +1,41 @@ +-module(band_supervisor). +-behaviour(supervisor). + +-export([start_link/1]). +-export([init/1]). + +start_link(Type) -> + supervisor:start_link({local,?MODULE}, ?MODULE, Type). + +%% The band supervisor will allow its band members to make a few +%% mistakes before shutting down all operations, based on what +%% mood he's in. A lenient supervisor will tolerate more mistakes +%% than an angry supervisor, who'll tolerate more than a +%% complete jerk supervisor +init(lenient) -> + init({one_for_one, 3, 60}); +init(angry) -> + init({rest_for_one, 2, 60}); +init(jerk) -> + init({one_for_all, 1, 60}); +init(jamband) -> + {ok, {{simple_one_for_one, 3, 60}, + [{jam_musician, + {musicians, start_link, []}, + temporary, 1000, worker, [musicians]} + ]}}; +init({RestartStrategy, MaxRestart, MaxTime}) -> + {ok, {{RestartStrategy, MaxRestart, MaxTime}, + [{singer, + {musicians, start_link, [singer, good]}, + permanent, 1000, worker, [musicians]}, + {bass, + {musicians, start_link, [bass, good]}, + temporary, 1000, worker, [musicians]}, + {drum, + {musicians, start_link, [drum, bad]}, + transient, 1000, worker, [musicians]}, + {keytar, + {musicians, start_link, [keytar, good]}, + transient, 1000, worker, [musicians]} + ]}}. diff --git a/learn-you-some-erlang/calc.erl b/learn-you-some-erlang/calc.erl new file mode 100644 index 0000000..c41ffb6 --- /dev/null +++ b/learn-you-some-erlang/calc.erl @@ -0,0 +1,51 @@ +-module(calc). +-export([rpn/1, rpn_test/0]). + +%% rpn(List()) -> Int() | Float() +%% parses an RPN string and outputs the results. +rpn(L) when is_list(L) -> + [Res] = lists:foldl(fun rpn/2, [], string:tokens(L, " ")), + Res. + +%% rpn(Str(), List()) -> List() +%% Returns the new stack after an operation has been done. +%% If no operator is found, we assume a number. +rpn("+", [N1,N2|S]) -> [N2+N1|S]; +rpn("-", [N1,N2|S]) -> [N2-N1|S]; +rpn("*", [N1,N2|S]) -> [N2*N1|S]; +rpn("/", [N1,N2|S]) -> [N2/N1|S]; +rpn("^", [N1,N2|S]) -> [math:pow(N2,N1)|S]; +rpn("ln", [N|S]) -> [math:log(N)|S]; +rpn("log10", [N|S]) -> [math:log10(N)|S]; +rpn("sum", Stack) -> [lists:sum(Stack)]; +rpn("prod", Stack) -> [lists:foldl(fun erlang:'*'/2, 1, Stack)]; +rpn(X, Stack) -> [read(X)|Stack]. + +%% read(String()) -> Int() | Float() +read(N) -> + case string:to_float(N) of + {error,no_float} -> list_to_integer(N); + {F,_} -> F + end. + +%% returns 'ok' iff successful +rpn_test() -> + 5 = rpn("2 3 +"), + 87 = rpn("90 3 -"), + -4 = rpn("10 4 3 + 2 * -"), + -2.0 = rpn("10 4 3 + 2 * - 2 /"), + ok = try + rpn("90 34 12 33 55 66 + * - +") + catch + error:{badmatch,[_|_]} -> ok + end, + 4037 = rpn("90 34 12 33 55 66 + * - + -"), + 8.0 = rpn("2 3 ^"), + true = math:sqrt(2) == rpn("2 0.5 ^"), + true = math:log(2.7) == rpn("2.7 ln"), + true = math:log10(2.7) == rpn("2.7 log10"), + 50 = rpn("10 10 10 20 sum"), + 10.0 = rpn("10 10 10 20 sum 5 /"), + 1000.0 = rpn("10 10 20 0.5 prod"), + ok. + diff --git a/learn-you-some-erlang/cards.erl b/learn-you-some-erlang/cards.erl new file mode 100644 index 0000000..ee8dcd2 --- /dev/null +++ b/learn-you-some-erlang/cards.erl @@ -0,0 +1,16 @@ +-module(cards). +-export([kind/1, main/0]). + +-type suit() :: spades | clubs | hearts | diamonds. +-type value() :: 1..10 | j | q | k. +-type card() :: {suit(), value()}. + +-spec kind(card()) -> face | number. +kind({_, A}) when A >= 1, A =< 10 -> number; +kind(_) -> face. + +main() -> + number = kind({spades, 7}), + face = kind({hearts, k}), + number = kind({rubies, 4}), + face = kind({clubs, q}). diff --git a/learn-you-some-erlang/cases.erl b/learn-you-some-erlang/cases.erl new file mode 100644 index 0000000..9b4f931 --- /dev/null +++ b/learn-you-some-erlang/cases.erl @@ -0,0 +1,22 @@ +-module(cases). +-export([insert/2,beach/1]). + +insert(X,[]) -> + [X]; +insert(X,Set) -> + case lists:member(X,Set) of + true -> Set; + false -> [X|Set] + end. + +beach(Temperature) -> + case Temperature of + {celsius, N} when N >= 20, N =< 45 -> + 'favorable'; + {kelvin, N} when N >= 293, N =< 318 -> + 'scientifically favorable'; + {fahrenheit, N} when N >= 68, N =< 113 -> + 'favorable in the US'; + _ -> + 'avoid beach' + end. diff --git a/learn-you-some-erlang/cat_fsm.erl b/learn-you-some-erlang/cat_fsm.erl new file mode 100644 index 0000000..3fa5039 --- /dev/null +++ b/learn-you-some-erlang/cat_fsm.erl @@ -0,0 +1,23 @@ +-module(cat_fsm). +-export([start/0, event/2]). + +start() -> + spawn(fun() -> dont_give_crap() end). + +event(Pid, Event) -> + Ref = make_ref(), % won't care for monitors here + Pid ! {self(), Ref, Event}, + receive + {Ref, Msg} -> {ok, Msg} + after 5000 -> + {error, timeout} + end. + +dont_give_crap() -> + receive + {Pid, Ref, _Msg} -> Pid ! {Ref, meh}; + _ -> ok + end, + io:format("Switching to 'dont_give_crap' state~n"), + dont_give_crap(). + diff --git a/learn-you-some-erlang/convert.erl b/learn-you-some-erlang/convert.erl new file mode 100644 index 0000000..86c3c2b --- /dev/null +++ b/learn-you-some-erlang/convert.erl @@ -0,0 +1,14 @@ +-module(convert). +-export([main/0, convert/1]). + +main() -> + [_,_] = convert({a,b}), + {_,_} = convert([a,b]), + [_,_] = convert([a,b]), + {_,_} = convert({a,b}). + +-spec convert(tuple()) -> list() + ; (list()) -> tuple(). +convert(Tup) when is_tuple(Tup) -> tuple_to_list(Tup); +convert(L = [_|_]) -> list_to_tuple(L). + diff --git a/learn-you-some-erlang/ct/demo/basic_SUITE.erl b/learn-you-some-erlang/ct/demo/basic_SUITE.erl new file mode 100644 index 0000000..566ff54 --- /dev/null +++ b/learn-you-some-erlang/ct/demo/basic_SUITE.erl @@ -0,0 +1,13 @@ +-module(basic_SUITE). +-include_lib("common_test/include/ct.hrl"). +-export([all/0]). +-export([test1/1, test2/1]). + +all() -> [test1,test2]. + +test1(_Config) -> + 1 = 1. + +test2(_Config) -> + A = 0, + 1/A. diff --git a/learn-you-some-erlang/ct/demo/state_SUITE.erl b/learn-you-some-erlang/ct/demo/state_SUITE.erl new file mode 100644 index 0000000..3ee0e5b --- /dev/null +++ b/learn-you-some-erlang/ct/demo/state_SUITE.erl @@ -0,0 +1,24 @@ +-module(state_SUITE). +-include_lib("common_test/include/ct.hrl"). + +-export([all/0, init_per_testcase/2, end_per_testcase/2]). +-export([ets_tests/1]). + +all() -> [ets_tests]. + +init_per_testcase(ets_tests, Config) -> + TabId = ets:new(account, [ordered_set, public]), + ets:insert(TabId, {andy, 2131}), + ets:insert(TabId, {david, 12}), + ets:insert(TabId, {steve, 12943752}), + [{table,TabId} | Config]. + +end_per_testcase(ets_tests, Config) -> + ets:delete(?config(table, Config)). + +ets_tests(Config) -> + TabId = ?config(table, Config), + [{david, 12}] = ets:lookup(TabId, david), + steve = ets:last(TabId), + true = ets:insert(TabId, {zachary, 99}), + zachary = ets:last(TabId). diff --git a/learn-you-some-erlang/ct/dist.spec b/learn-you-some-erlang/ct/dist.spec new file mode 100644 index 0000000..e25cdfb --- /dev/null +++ b/learn-you-some-erlang/ct/dist.spec @@ -0,0 +1,14 @@ +{node, a, 'a@ferdmbp.local'}. +{node, b, 'b@ferdmbp.local'}. + +{init, [a,b], [{node_start, [{monitor_master, true}]}]}. + +{alias, demo, "/Users/ferd/code/self/learn-you-some-erlang/ct/demo/"}. +{alias, meeting, "/Users/ferd/code/self/learn-you-some-erlang/ct/meeting/"}. + +{logdir, all_nodes, "/Users/ferd/code/self/learn-you-some-erlang/ct/logs/"}. +{logdir, master, "/Users/ferd/code/self/learn-you-some-erlang/ct/logs/"}. + +{suites, [b], meeting, all}. +{suites, [a], demo, all}. +{skip_cases, [a], demo, basic_SUITE, test2, "This test fails on purpose"}. diff --git a/learn-you-some-erlang/ct/meeting/meeting.erl b/learn-you-some-erlang/ct/meeting/meeting.erl new file mode 100644 index 0000000..0de9453 --- /dev/null +++ b/learn-you-some-erlang/ct/meeting/meeting.erl @@ -0,0 +1,45 @@ +-module(meeting). +-export([rent_projector/1, use_chairs/1, book_room/1, + get_all_bookings/0, start/0, stop/0]). +-record(bookings, {projector, room, chairs}). + +start() -> + Pid = spawn(fun() -> loop(#bookings{}) end), + register(?MODULE, Pid). + +stop() -> + ?MODULE ! stop. + +rent_projector(Group) -> + ?MODULE ! {projector, Group}. + +book_room(Group) -> + ?MODULE ! {room, Group}. + +use_chairs(Group) -> + ?MODULE ! {chairs, Group}. + +get_all_bookings() -> + Ref = make_ref(), + ?MODULE ! {self(), Ref, get_bookings}, + receive + {Ref, Reply} -> + Reply + end. + +loop(B = #bookings{}) -> + receive + stop -> ok; + {From, Ref, get_bookings} -> + From ! {Ref, [{room, B#bookings.room}, + {chairs, B#bookings.chairs}, + {projector, B#bookings.projector}]}, + loop(B); + {room, Group} -> + loop(B#bookings{room=Group}); + {chairs, Group} -> + loop(B#bookings{chairs=Group}); + {projector, Group} -> + loop(B#bookings{projector=Group}) + end. + diff --git a/learn-you-some-erlang/ct/meeting/meeting_SUITE.erl b/learn-you-some-erlang/ct/meeting/meeting_SUITE.erl new file mode 100755 index 0000000..f699569 --- /dev/null +++ b/learn-you-some-erlang/ct/meeting/meeting_SUITE.erl @@ -0,0 +1,51 @@ +-module(meeting_SUITE). +-include_lib("common_test/include/ct.hrl"). +-export([all/0, groups/0, init_per_group/2, end_per_group/2]). +-export([carla/1, mark/1, dog/1, all_same_owner/1]). + +%% Regroup the tests as groups that will let you try to see if the 'meeting.erl' +%% modules contains concurrency errors or not. + +all() -> [{group, session}]. + +groups() -> [{session, + [], + [{group, clients}, all_same_owner]}, + {clients, + [parallel, {repeat, 10}], + [carla, mark, dog]}]. + +init_per_group(session, Config) -> + meeting:start(), + Config; +init_per_group(_, Config) -> + Config. + +end_per_group(session, _Config) -> + meeting:stop(); +end_per_group(_, _Config) -> + ok. + +carla(_Config) -> + meeting:book_room(women), + timer:sleep(10), + meeting:rent_projector(women), + timer:sleep(10), + meeting:use_chairs(women). + +mark(_Config) -> + meeting:rent_projector(men), + timer:sleep(10), + meeting:use_chairs(men), + timer:sleep(10), + meeting:book_room(men). + +dog(_Config) -> + meeting:rent_projector(animals), + timer:sleep(10), + meeting:use_chairs(animals), + timer:sleep(10), + meeting:book_room(animals). + +all_same_owner(_Config) -> + [{_,Owner}, {_, Owner}, {_, Owner}] = meeting:get_all_bookings(). diff --git a/learn-you-some-erlang/ct/spec.spec b/learn-you-some-erlang/ct/spec.spec new file mode 100644 index 0000000..1752664 --- /dev/null +++ b/learn-you-some-erlang/ct/spec.spec @@ -0,0 +1,7 @@ +{alias, demo, "./demo/"}. +{alias, meeting, "./meeting/"}. +{logdir, "./logs/"}. + +{suites, meeting, all}. +{suites, demo, all}. +{skip_cases, demo, basic_SUITE, test2, "This test fails on purpose"}. diff --git a/learn-you-some-erlang/curling.erl b/learn-you-some-erlang/curling.erl new file mode 100644 index 0000000..2c50074 --- /dev/null +++ b/learn-you-some-erlang/curling.erl @@ -0,0 +1,37 @@ +-module(curling). +-export([start_link/2, set_teams/3, add_points/3, next_round/1]). +-export([join_feed/2, leave_feed/2]). +-export([game_info/1]). + +start_link(TeamA, TeamB) -> + {ok, Pid} = gen_event:start_link(), + %% The scoreboard will always be there + gen_event:add_handler(Pid, curling_scoreboard, []), + %% Start the stats accumulator + gen_event:add_handler(Pid, curling_accumulator, []), + set_teams(Pid, TeamA, TeamB), + {ok, Pid}. + +set_teams(Pid, TeamA, TeamB) -> + gen_event:notify(Pid, {set_teams, TeamA, TeamB}). + +add_points(Pid, Team, N) -> + gen_event:notify(Pid, {add_points, Team, N}). + +next_round(Pid) -> + gen_event:notify(Pid, next_round). + +%% Subscribes the pid ToPid to the event feed. +%% The specific event handler for the newsfeed is +%% returned in case someone wants to leave +join_feed(Pid, ToPid) -> + HandlerId = {curling_feed, make_ref()}, + gen_event:add_sup_handler(Pid, HandlerId, [ToPid]), + HandlerId. + +leave_feed(Pid, HandlerId) -> + gen_event:delete_handler(Pid, HandlerId, leave_feed). + +%% Returns the current game state. +game_info(Pid) -> + gen_event:call(Pid, curling_accumulator, game_data). diff --git a/learn-you-some-erlang/curling_accumulator.erl b/learn-you-some-erlang/curling_accumulator.erl new file mode 100644 index 0000000..f6cacfd --- /dev/null +++ b/learn-you-some-erlang/curling_accumulator.erl @@ -0,0 +1,35 @@ +-module(curling_accumulator). +-behaviour(gen_event). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, + terminate/2]). + +-record(state, {teams=orddict:new(), round=0}). + +init([]) -> + {ok, #state{}}. + +handle_event({set_teams, TeamA, TeamB}, S=#state{teams=T}) -> + Teams = orddict:store(TeamA, 0, orddict:store(TeamB, 0, T)), + {ok, S#state{teams=Teams}}; +handle_event({add_points, Team, N}, S=#state{teams=T}) -> + Teams = orddict:update_counter(Team, N, T), + {ok, S#state{teams=Teams}}; +handle_event(next_round, S=#state{}) -> + {ok, S#state{round = S#state.round+1}}; +handle_event(_Event, Pid) -> + {ok, Pid}. + +handle_call(game_data, S=#state{teams=T, round=R}) -> + {ok, {orddict:to_list(T), {round, R}}, S}; +handle_call(_, State) -> + {ok, ok, State}. + +handle_info(_, State) -> + {ok, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. diff --git a/learn-you-some-erlang/curling_feed.erl b/learn-you-some-erlang/curling_feed.erl new file mode 100644 index 0000000..b1b789a --- /dev/null +++ b/learn-you-some-erlang/curling_feed.erl @@ -0,0 +1,24 @@ +-module(curling_feed). +-behaviour(gen_event). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, + terminate/2]). + +init([Pid]) -> + {ok, Pid}. + +handle_event(Event, Pid) -> + Pid ! {curling_feed, Event}, + {ok, Pid}. + +handle_call(_, State) -> + {ok, ok, State}. + +handle_info(_, State) -> + {ok, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. diff --git a/learn-you-some-erlang/curling_scoreboard.erl b/learn-you-some-erlang/curling_scoreboard.erl new file mode 100644 index 0000000..34ff48d --- /dev/null +++ b/learn-you-some-erlang/curling_scoreboard.erl @@ -0,0 +1,32 @@ +-module(curling_scoreboard). +-behaviour(gen_event). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, + terminate/2]). + +init([]) -> + {ok, []}. + +handle_event({set_teams, TeamA, TeamB}, State) -> + curling_scoreboard_hw:set_teams(TeamA, TeamB), + {ok, State}; +handle_event({add_points, Team, N}, State) -> + [curling_scoreboard_hw:add_point(Team) || _ <- lists:seq(1,N)], + {ok, State}; +handle_event(next_round, State) -> + curling_scoreboard_hw:next_round(), + {ok, State}; +handle_event(_, State) -> + {ok, State}. + +handle_call(_, State) -> + {ok, ok, State}. + +handle_info(_, State) -> + {ok, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. diff --git a/learn-you-some-erlang/curling_scoreboard_hw.erl b/learn-you-some-erlang/curling_scoreboard_hw.erl new file mode 100644 index 0000000..adfe4ab --- /dev/null +++ b/learn-you-some-erlang/curling_scoreboard_hw.erl @@ -0,0 +1,19 @@ +-module(curling_scoreboard_hw). +-export([add_point/1, next_round/0, set_teams/2, reset_board/0]). + +%% This is a 'dumb' module that's only there to replace what a real hardware +%% controller would likely do. The real hardware controller would likely hold +%% some state and make sure everything works right, but this one doesn't mind. + +%% Shows the teams on the scoreboard. +set_teams(TeamA, TeamB) -> + io:format("Scoreboard: Team ~s vs. Team ~s~n", [TeamA, TeamB]). + +next_round() -> + io:format("Scoreboard: round over~n"). + +add_point(Team) -> + io:format("Scoreboard: increased score of team ~s by 1~n", [Team]). + +reset_board() -> + io:format("Scoreboard: All teams are undefined and all scores are 0~n"). diff --git a/learn-you-some-erlang/discrep1.erl b/learn-you-some-erlang/discrep1.erl new file mode 100644 index 0000000..188e938 --- /dev/null +++ b/learn-you-some-erlang/discrep1.erl @@ -0,0 +1,6 @@ +-module(discrep1). +-export([run/0]). + +run() -> some_op(5, you). + +some_op(A, B) -> A + B. diff --git a/learn-you-some-erlang/discrep2.erl b/learn-you-some-erlang/discrep2.erl new file mode 100644 index 0000000..7462e5f --- /dev/null +++ b/learn-you-some-erlang/discrep2.erl @@ -0,0 +1,12 @@ +-module(discrep2). +-export([run/0]). + +run() -> + Tup = money(5, you), + some_op(count(Tup), account(Tup)). + +money(Num, Name) -> {give, Num, Name}. +count({give, Num, _}) -> Num. +account({give, _, X}) -> X. + +some_op(A, B) -> A + B. diff --git a/learn-you-some-erlang/discrep3.erl b/learn-you-some-erlang/discrep3.erl new file mode 100644 index 0000000..aaec5d6 --- /dev/null +++ b/learn-you-some-erlang/discrep3.erl @@ -0,0 +1,14 @@ +-module(discrep3). +-export([run/0]). + +run() -> + Tup = money(5, you), + some_op(item(count, Tup), item(account, Tup)). + +money(Num, Name) -> {give, Num, Name}. + +item(count, {give, X, _}) -> X; +item(account, {give, _, X}) -> X. + +some_op(A, B) -> A + B. + diff --git a/learn-you-some-erlang/discrep4.erl b/learn-you-some-erlang/discrep4.erl new file mode 100644 index 0000000..949a843 --- /dev/null +++ b/learn-you-some-erlang/discrep4.erl @@ -0,0 +1,21 @@ +-module(discrep4). +-export([run/0]). +-type cents() :: integer(). +-type account() :: atom(). +-type transaction() :: {'give', cents(), account()}. + +run() -> + Tup = money(5, you), + some_op(item(count,Tup), item(account,Tup)). + +-spec money(cents(), account()) -> transaction(). +money(Num, Name) -> {give, Num, Name}. + +-spec item('count', transaction()) -> cents(); + ('account', transaction()) -> account(). +item(count, {give, X, _}) -> X; +item(account, {give, _, X}) -> X. + +some_op(A,B) -> A + B. + + diff --git a/learn-you-some-erlang/dog_fsm.erl b/learn-you-some-erlang/dog_fsm.erl new file mode 100644 index 0000000..f784933 --- /dev/null +++ b/learn-you-some-erlang/dog_fsm.erl @@ -0,0 +1,42 @@ +-module(dog_fsm). +-export([start/0, squirrel/1, pet/1]). + +start() -> spawn(fun() -> bark() end). + +squirrel(Pid) -> Pid ! squirrel. + +pet(Pid) -> Pid ! pet. + +bark() -> + io:format("Dog says: BARK! BARK!~n"), + receive + pet -> + wag_tail(); + _ -> + io:format("Dog is confused~n"), + bark() + after 2000 -> + bark() + end. + +wag_tail() -> + io:format("Dog wags its tail~n"), + receive + pet -> + sit(); + _ -> + io:format("Dog is confused~n"), + wag_tail() + after 30000 -> + bark() + end. + +sit() -> + io:format("Dog is sitting. Gooooood boy!~n"), + receive + squirrel -> + bark(); + _ -> + io:format("Dog is confused~n"), + sit() + end. diff --git a/learn-you-some-erlang/dolphins.erl b/learn-you-some-erlang/dolphins.erl new file mode 100644 index 0000000..1e84b77 --- /dev/null +++ b/learn-you-some-erlang/dolphins.erl @@ -0,0 +1,34 @@ +-module(dolphins). +-compile(export_all). + +dolphin1() -> + receive + do_a_flip -> + io:format("How about no?~n"); + fish -> + io:format("So long and thanks for all the fish!~n"); + _ -> + io:format("Heh, we're smarter than you humans.~n") + end. + +dolphin2() -> + receive + {From, do_a_flip} -> + From ! "How about no?"; + {From, fish} -> + From ! "So long and thanks for all the fish!"; + _ -> + io:format("Heh, we're smarter than you humans.~n") + end. + +dolphin3() -> + receive + {From, do_a_flip} -> + From ! "How about no?", + dolphin3(); + {From, fish} -> + From ! "So long and thanks for all the fish!"; + _ -> + io:format("Heh, we're smarter than you humans.~n"), + dolphin3() + end. diff --git a/learn-you-some-erlang/erlcount-1.0/Emakefile b/learn-you-some-erlang/erlcount-1.0/Emakefile new file mode 100644 index 0000000..76e98fc --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/Emakefile @@ -0,0 +1,4 @@ +{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}. +%% The TESTDIR macro assumes the file is running from the 'erlcount-1.0' +%% directory, sitting within 'learn-you-some-erlang/'. +{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}, {d, 'TESTDIR', ".."}]}. diff --git a/learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app b/learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app new file mode 100644 index 0000000..1d40c4b --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app @@ -0,0 +1,13 @@ +{application, erlcount, + [{vsn, "1.0.0"}, + {modules, [erlcount, erlcount_sup, erlcount_lib, + erlcount_dispatch, erlcount_counter]}, + {applications, [ppool]}, + {registered, [erlcount]}, + {mod, {erlcount, []}}, + {env, + [{directory, "."}, + {regex, ["if\\s.+->", "case\\s.+\\sof"]}, + {max_files, 10}]} + ]}. + diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount.erl new file mode 100644 index 0000000..16a9f23 --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount.erl @@ -0,0 +1,9 @@ +-module(erlcount). +-behaviour(application). +-export([start/2, stop/1]). + +start(normal, _Args) -> + erlcount_sup:start_link(). + +stop(_State) -> + ok. diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl new file mode 100644 index 0000000..c42fd4d --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl @@ -0,0 +1,35 @@ +-module(erlcount_counter). +-behaviour(gen_server). +-export([start_link/4]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {dispatcher, ref, file, re}). + +start_link(DispatcherPid, Ref, FileName, Regex) -> + gen_server:start_link(?MODULE, [DispatcherPid, Ref, FileName, Regex], []). + +init([DispatcherPid, Ref, FileName, Regex]) -> + self() ! start, + {ok, #state{dispatcher=DispatcherPid, + ref = Ref, + file = FileName, + re = Regex}}. + +handle_call(_Msg, _From, State) -> + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(start, S = #state{re=Re, ref=Ref}) -> + {ok, Bin} = file:read_file(S#state.file), + Count = erlcount_lib:regex_count(Re, Bin), + erlcount_dispatch:complete(S#state.dispatcher, Re, Ref, Count), + {stop, normal, S}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl new file mode 100644 index 0000000..0bc5e45 --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl @@ -0,0 +1,86 @@ +-module(erlcount_dispatch). +-behaviour(gen_fsm). +-export([start_link/0, complete/4]). +-export([init/1, dispatching/2, listening/2, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). + +-define(POOL, erlcount). +-record(data, {regex=[], refs=[]}). + +%%% PUBLIC API +start_link() -> + gen_fsm:start_link(?MODULE, [], []). + +complete(Pid, Regex, Ref, Count) -> + gen_fsm:send_all_state_event(Pid, {complete, Regex, Ref, Count}). + +%%% GEN_FSM +%% Two states: dispatching and listening +init([]) -> + %% Move the get_env stuff to the supervisor's init. + {ok, Re} = application:get_env(regex), + {ok, Dir} = application:get_env(directory), + {ok, MaxFiles} = application:get_env(max_files), + ppool:start_pool(?POOL, MaxFiles, {erlcount_counter, start_link, []}), + case lists:all(fun valid_regex/1, Re) of + true -> + %% creates a regex entry of the form [{Re, Count}] + self() ! {start, Dir}, + {ok, dispatching, #data{regex=[{R,0} || R <- Re]}}; + false -> + {stop, invalid_regex} + end. + +dispatching({continue, File, Continuation}, Data = #data{regex=Re, refs=Refs}) -> + F = fun({Regex,_Count}, NewRefs) -> + Ref = make_ref(), + ppool:async_queue(?POOL, [self(), Ref, File, Regex]), + [Ref|NewRefs] + end, + NewRefs = lists:foldl(F, Refs, Re), + gen_fsm:send_event(self(), Continuation()), + {next_state, dispatching, Data#data{refs = NewRefs}}; +dispatching(done, Data) -> + %% This is a special case. We can not assume that all messages have NOT + %% been received by the time we hit 'done'. As such, we directly move to + %% listening/2 without waiting for an external event. + listening(done, Data). + +listening(done, #data{regex=Re, refs=[]}) -> % all received! + [io:format("Regex ~s has ~p results~n", [R,C]) || {R, C} <- Re], + {stop, normal, done}; +listening(done, Data) -> % entries still missing + {next_state, listening, Data}. + +handle_event({complete, Regex, Ref, Count}, State, Data = #data{regex=Re, refs=Refs}) -> + {Regex, OldCount} = lists:keyfind(Regex, 1, Re), + NewRe = lists:keyreplace(Regex, 1, Re, {Regex, OldCount+Count}), + NewData = Data#data{regex=NewRe, refs=Refs--[Ref]}, + case State of + dispatching -> + {next_state, dispatching, NewData}; + listening -> + listening(done, NewData) + end. + +handle_sync_event(Event, _From, State, Data) -> + io:format("Unexpected event: ~p~n", [Event]), + {next_state, State, Data}. + +handle_info({start, Dir}, State, Data) -> + gen_fsm:send_event(self(), erlcount_lib:find_erl(Dir)), + {next_state, State, Data}. + +terminate(_Reason, _State, _Data) -> + ok. + +code_change(_OldVsn, State, Data, _Extra) -> + {ok, State, Data}. + +%%% PRIVATE +valid_regex(Re) -> + try re:run("", Re) of + _ -> true + catch + error:badarg -> false + end. diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl new file mode 100644 index 0000000..70c062f --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl @@ -0,0 +1,55 @@ +-module(erlcount_lib). +-export([find_erl/1, regex_count/2]). +-include_lib("kernel/include/file.hrl"). + +%% Finds all files ending in .erl +find_erl(Directory) -> + find_erl(Directory, queue:new()). + +regex_count(Re, Str) -> + case re:run(Str, Re, [global]) of + nomatch -> 0; + {match, List} -> length(List) + end. + +%%% Private +%% Dispatches based on file type +find_erl(Name, Queue) -> + {ok, F = #file_info{}} = file:read_file_info(Name), + case F#file_info.type of + directory -> handle_directory(Name, Queue); + regular -> handle_regular_file(Name, Queue); + _Other -> dequeue_and_run(Queue) + end. + +%% Opens directories and enqueues files in there +handle_directory(Dir, Queue) -> + case file:list_dir(Dir) of + {ok, []} -> + dequeue_and_run(Queue); + {ok, Files} -> + dequeue_and_run(enqueue_many(Dir, Files, Queue)) + end. + +%% Checks if the file finishes in .erl +handle_regular_file(Name, Queue) -> + case filename:extension(Name) of + ".erl" -> + {continue, Name, fun() -> dequeue_and_run(Queue) end}; + _NonErl -> + dequeue_and_run(Queue) + end. + +%% Pops an item from the queue and runs it. +dequeue_and_run(Queue) -> + case queue:out(Queue) of + {empty, _} -> done; + {{value, File}, NewQueue} -> find_erl(File, NewQueue) + end. + +%% Adds a bunch of items to the queue. +enqueue_many(Path, Files, Queue) -> + F = fun(File, Q) -> queue:in(filename:join(Path,File), Q) end, + lists:foldl(F, Queue, Files). + + diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl new file mode 100644 index 0000000..b8633a3 --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl @@ -0,0 +1,17 @@ +-module(erlcount_sup). +-behaviour(supervisor). +-export([start_link/0, init/1]). + +start_link() -> + supervisor:start_link(?MODULE, []). + +init([]) -> + MaxRestart = 5, + MaxTime = 100, + {ok, {{one_for_one, MaxRestart, MaxTime}, + [{dispatch, + {erlcount_dispatch, start_link, []}, + transient, + 60000, + worker, + [erlcount_dispatch]}]}}. diff --git a/learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl b/learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl new file mode 100644 index 0000000..7459a39 --- /dev/null +++ b/learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl @@ -0,0 +1,35 @@ +-module(erlcount_tests). +-include_lib("eunit/include/eunit.hrl"). +-ifndef(TESTDIR). +%% Assumes we're running from the app's directory. We want to target the +%% 'learn-you-some-erlang' directory. +-define(TESTDIR, ".."). +-endif. + +%% NOTE: +%% Because we do not want the tests to be bound to a specific snapshot in time +%% of our app, we will instead compare it to an oracle built with unix +%% commands. Users running windows sadly won't be able to run these tests. + +%% We'll be forcing the design to be continuation-based when it comes to +%% reading files. This will require some explaining to the user, but will +%% allow to show how we can read files and schedule them at the same time, +%% but without breaking functional principles of referential transparency +%% and while allowing specialised functions to be written in a testable manner. +find_erl_test_() -> + ?_assertEqual(lists:sort(string:tokens(os:cmd("find "++?TESTDIR++" -name *.erl"), "\n")), + lists:sort(build_list(erlcount_lib:find_erl(?TESTDIR)))). + +build_list(Term) -> build_list(Term, []). + +build_list(done, List) -> List; +build_list({continue, Entry, Fun}, List) -> + build_list(Fun(), [Entry|List]). + +regex_count_test_() -> + [?_assertEqual(5, erlcount_lib:regex_count("a", "a a a a a")), + ?_assertEqual(0, erlcount_lib:regex_count("o", "a a a a a")), + ?_assertEqual(2, erlcount_lib:regex_count("a.*", "a a a\na a a")), + ?_assertEqual(3, erlcount_lib:regex_count("if", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")), + ?_assertEqual(1, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")), + ?_assertEqual(2, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end,\n if true -> ok end.\n"))]. diff --git a/learn-you-some-erlang/exceptions.erl b/learn-you-some-erlang/exceptions.erl new file mode 100644 index 0000000..0ed6742 --- /dev/null +++ b/learn-you-some-erlang/exceptions.erl @@ -0,0 +1,76 @@ +-module(exceptions). +-compile(export_all). + +throws(F) -> + try F() of + _ -> ok + catch + Throw -> {throw, caught, Throw} + end. + +errors(F) -> + try F() of + _ -> ok + catch + error:Error -> {error, caught, Error} + end. + +exits(F) -> + try F() of + _ -> ok + catch + exit:Exit -> {exit, caught, Exit} + end. + +sword(1) -> throw(slice); +sword(2) -> erlang:error(cut_arm); +sword(3) -> exit(cut_leg); +sword(4) -> throw(punch); +sword(5) -> exit(cross_bridge). + +%%"I must cross this bridge" +black_knight(Attack) when is_function(Attack, 0) -> + try Attack() of + _ -> "None shall pass." + catch + throw:slice -> "It is but a scratch."; + error:cut_arm -> "I've had worse."; + exit:cut_leg -> "Come on you pansy!"; + _:_ -> "Just a flesh wound." + end. +%"We'll call it a draw..." + +talk() -> "blah blah". + +whoa() -> + try + talk(), + _Knight = "None shall Pass!", + _Doubles = [N*2 || N <- lists:seq(1,100)], + throw(up), + _WillReturnThis = tequila + of + tequila -> "hey this worked!" + catch + Exception:Reason -> {caught, Exception, Reason} + end. + +im_impressed() -> + try + talk(), + _Knight = "None shall Pass!", + _Doubles = [N*2 || N <- lists:seq(1,100)], + throw(up), + _WillReturnThis = tequila + catch + Exception:Reason -> {caught, Exception, Reason} + end. + +catcher(X,Y) -> + case catch X/Y of + {'EXIT', {badarith,_}} -> "uh oh"; + N -> N + end. + +one_or_two(1) -> return; +one_or_two(2) -> throw(return). diff --git a/learn-you-some-erlang/fifo.erl b/learn-you-some-erlang/fifo.erl new file mode 100644 index 0000000..9687456 --- /dev/null +++ b/learn-you-some-erlang/fifo.erl @@ -0,0 +1,15 @@ +-module(fifo). +-export([new/0, push/2, pop/1, empty/1]). + +%% implemented as two stacks; push on the first, pop on the second. +%% when the second is empty, reverse the first and make it the second. +new() -> {fifo, [], []}. + +push({fifo, In, Out}, X) -> {fifo, [X|In], Out}. + +pop({fifo, [], []}) -> erlang:error('empty fifo'); +pop({fifo, In, []}) -> pop({fifo, [], lists:reverse(In)}); +pop({fifo, In, [H|T]}) -> {H, {fifo, In, T}}. + +empty({fifo, [], []}) -> true; +empty({fifo, _, _}) -> false. diff --git a/learn-you-some-erlang/fifo_types.erl b/learn-you-some-erlang/fifo_types.erl new file mode 100644 index 0000000..d1bf4ed --- /dev/null +++ b/learn-you-some-erlang/fifo_types.erl @@ -0,0 +1,35 @@ +-module(fifo_types). +-export([new/0, push/2, pop/1, empty/1]). +-export([test/0]). + +-spec new() -> {fifo, [], []}. +new() -> {fifo, [], []}. + +-spec push({fifo, In::list(), Out::list()}, term()) -> {fifo, list(), list()}. +push({fifo, In, Out}, X) -> {fifo, [X|In], Out}. + +-spec pop({fifo, In::list(), Out::list()}) -> {term(), {fifo, list(), list()}}. +pop({fifo, [], []}) -> erlang:error('empty fifo'); +pop({fifo, In, []}) -> pop({fifo, [], lists:reverse(In)}); +pop({fifo, In, [H|T]}) -> {H, {fifo, In, T}}. + +%-spec empty({fifo, [], []}) -> true; +% ({fifo, list(), list()}) -> false. +%-spec empty({fifo, [], []}) -> true; +% ({fifo, [any(), ...], [any(), ...]}) -> false. +-spec empty({fifo, [], []}) -> true; + ({fifo, [any(), ...], []}) -> false; + ({fifo, [], [any(), ...]}) -> false; + ({fifo, [any(), ...], [any(), ...]}) -> false. +empty({fifo, [], []}) -> true; +empty({fifo, _, _}) -> false. + +test() -> + N = new(), + {2, N2} = pop(push(push(new(), 2), 5)), + {5, N3} = pop(N2), + N = N3, + true = empty(N3), + false = empty(N2), + %pop({fifo, [a|b], [e]}). + pop({fifo, [a, b], [e]}). diff --git a/learn-you-some-erlang/functions.erl b/learn-you-some-erlang/functions.erl new file mode 100644 index 0000000..730a80d --- /dev/null +++ b/learn-you-some-erlang/functions.erl @@ -0,0 +1,17 @@ +-module(functions). +-export([head/1, second/1, same/2, valid_time/1]). + +head([H|_]) -> H. + +second([_,X|_]) -> X. + +same(X,X) -> + true; +same(_,_) -> + false. + +valid_time({Date = {Y,M,D}, Time = {H,Min,S}}) -> + io:format("The Date tuple (~p) says today is: ~p/~p/~p,~n",[Date,Y,M,D]), + io:format("The time tuple (~p) indicates: ~p:~p:~p.~n", [Time,H,Min,S]); +valid_time(_) -> + io:format("Stop feeding me wrong data!~n"). diff --git a/learn-you-some-erlang/guards.erl b/learn-you-some-erlang/guards.erl new file mode 100644 index 0000000..3b1a2bc --- /dev/null +++ b/learn-you-some-erlang/guards.erl @@ -0,0 +1,15 @@ +-module(guards). +-export([old_enough/1, right_age/1, wrong_age/1]). + +old_enough(X) when X >= 16 -> true; +old_enough(_) -> false. + +right_age(X) when X >= 16, X =< 104 -> + true; +right_age(_) -> + false. + +wrong_age(X) when X < 16; X > 104 -> + true; +wrong_age(_) -> + false. diff --git a/learn-you-some-erlang/hhfuns.erl b/learn-you-some-erlang/hhfuns.erl new file mode 100644 index 0000000..ebe07a4 --- /dev/null +++ b/learn-you-some-erlang/hhfuns.erl @@ -0,0 +1,111 @@ +-module(hhfuns). +-compile(export_all). + +one() -> 1. +two() -> 2. + +add(X,Y) -> X() + Y(). + +increment([]) -> []; +increment([H|T]) -> [H+1|increment(T)]. + +decrement([]) -> []; +decrement([H|T]) -> [H-1|decrement(T)]. + + +map(_, []) -> []; +map(F, [H|T]) -> [F(H)|map(F,T)]. + +incr(X) -> X + 1. +decr(X) -> X - 1. + +%% bases/1. Refered as the same function refactored in the book +base1(A) -> + B = A + 1, + F = fun() -> A * B end, + F(). + +%%% can't compile this one +%% base(A) -> +%% B = A + 1, +%% F = fun() -> C = A * B end, +%% F(), +%% C. + +base2() -> + A = 1, + (fun() -> A = 2 end)(). + +base3() -> + A = 1, + (fun(A) -> A = 2 end)(2). + +a() -> + Secret = "pony", + fun() -> Secret end. + +b(F) -> + "a/0's password is "++F(). + +even(L) -> lists:reverse(even(L,[])). + +even([], Acc) -> Acc; +even([H|T], Acc) when H rem 2 == 0 -> + even(T, [H|Acc]); +even([_|T], Acc) -> + even(T, Acc). + +old_men(L) -> lists:reverse(old_men(L,[])). + +old_men([], Acc) -> Acc; +old_men([Person = {male, Age}|People], Acc) when Age > 60 -> + old_men(People, [Person|Acc]); +old_men([_|People], Acc) -> + old_men(People, Acc). + +filter(Pred, L) -> lists:reverse(filter(Pred, L,[])). + +filter(_, [], Acc) -> Acc; +filter(Pred, [H|T], Acc) -> + case Pred(H) of + true -> filter(Pred, T, [H|Acc]); + false -> filter(Pred, T, Acc) + end. + +%% find the maximum of a list +max([H|T]) -> max2(T, H). + +max2([], Max) -> Max; +max2([H|T], Max) when H > Max -> max2(T, H); +max2([_|T], Max) -> max2(T, Max). + +%% find the minimum of a list +min([H|T]) -> min2(T,H). + +min2([], Min) -> Min; +min2([H|T], Min) when H < Min -> min2(T,H); +min2([_|T], Min) -> min2(T, Min). + +%% sum of all the elements of a list +sum(L) -> sum(L,0). + +sum([], Sum) -> Sum; +sum([H|T], Sum) -> sum(T, H+Sum). + +fold(_, Start, []) -> Start; +fold(F, Start, [H|T]) -> fold(F, F(H,Start), T). + +reverse(L) -> + fold(fun(X,Acc) -> [X|Acc] end, [], L). + +map2(F,L) -> + reverse(fold(fun(X,Acc) -> [F(X)|Acc] end, [], L)). + +filter2(Pred, L) -> + F = fun(X,Acc) -> + case Pred(X) of + true -> [X|Acc]; + false -> Acc + end + end, + reverse(fold(F, [], L)). diff --git a/learn-you-some-erlang/keyval_benchmark.erl b/learn-you-some-erlang/keyval_benchmark.erl new file mode 100644 index 0000000..45c83be --- /dev/null +++ b/learn-you-some-erlang/keyval_benchmark.erl @@ -0,0 +1,192 @@ +-module(keyval_benchmark). +-compile(export_all). + +%% Runs all benchmarks with Reps number of elements. +bench(Reps) -> + io:format("Base Case:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(base_case(Reps)), + io:format("~nNaive Orddict:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(naive_orddict(Reps)), + io:format("~nSmart Orddict:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(smart_orddict(Reps)), + io:format("~nNaive Dict:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(naive_dict(Reps)), + io:format("~nSmart Dict:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(smart_dict(Reps)), + io:format("~nNaive gb_trees:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(naive_gb_trees(Reps)), + io:format("~nSmart gb_trees:~n"), + io:format("Operation\tTotal (µs)\tAverage (µs)~n"), + print(smart_gb_trees(Reps)). + +%% formats the benchmark results cleanly. +print([]) -> ok; +print([{Op, Total, Avg} | Rest]) -> + io:format("~8s\t~10B\t~.6f~n", [Op, Total, Avg]), + print(Rest). + +%% Example of a base benchmark function. This one actually does +%% nothing and can be used as a control for all the benchmark - it +%% will measure how much time it takes just to loop over data and +%% apply functions. +base_case(Reps) -> + benchmark(Reps, % N repetitions + [], % Empty data structure + fun ?MODULE:null/3, % Create + fun ?MODULE:null/2, % Read + fun ?MODULE:null/3, % Update + fun ?MODULE:null/2). % Delete + +%% Ordered dictionaries. Assumes that the value is present on reads. +smart_orddict(Reps) -> + benchmark(Reps, + orddict:new(), + fun orddict:store/3, + fun orddict:fetch/2, + fun orddict:store/3, + fun orddict:erase/2). + +%% Ordered dictionaries. Doesn't know whether a key is there or not on +%% reads. +naive_orddict(Reps) -> + benchmark(Reps, + orddict:new(), + fun orddict:store/3, + fun orddict:find/2, + fun orddict:store/3, + fun orddict:erase/2). + +%% Dictionary benchmark. Assumes that the value is present on reads. +smart_dict(Reps) -> + benchmark(Reps, + dict:new(), + fun dict:store/3, + fun dict:fetch/2, + fun dict:store/3, + fun dict:erase/2). + +%% Dictionary benchmark. Doesn't know if the value exisst at read time. +naive_dict(Reps) -> + benchmark(Reps, + dict:new(), + fun dict:store/3, + fun dict:find/2, + fun dict:store/3, + fun dict:erase/2). + +%% gb_trees benchmark. Uses the most general functions -- i.e.: it never +%% assumes that the value is not in a tree when inserting and never assumes +%% it is there when updating or deleting. +naive_gb_trees(Reps) -> + benchmark(Reps, + gb_trees:empty(), + fun gb_trees:enter/3, + fun gb_trees:lookup/2, + fun gb_trees:enter/3, + fun gb_trees:delete_any/2). + +%% gb_trees benchmark. Uses specific function: it assumes that the values +%% are not there when inserting and assumes it exists when updating or +%% deleting. +smart_gb_trees(Reps) -> + benchmark(Reps, + gb_trees:empty(), + fun gb_trees:insert/3, + fun gb_trees:get/2, + fun gb_trees:update/3, + fun gb_trees:delete/2). + +%% Empty functions used for the 'base_case/1' benchmark. They must do +%% nothing interesting. +null(_, _) -> ok. +null(_, _, _) -> ok. + +%% Runs all the functions of 4 formats: Create, Read, Update, Delete. +%% Create: it's a regular insertion, but it goes from an empty structure +%% to a filled one. Requires an empty data structure, +%% Read: reads every key from a complete data structure. +%% Update: usually, this is the same as the insertion from 'create', +%% except that it's run on full data structures. In some cases, +%% like gb_trees, there also exist operations for updates when +%% the keys are known that act differently from regular inserts. +%% Delete: removes a key from a tree. Because we want to test the +%% efficiency of it all, we will always delete from a complete +%% structure. +%% The function returns a list of all times averaged over the number +%% of repetitions (Reps) needed. +benchmark(Reps, Empty, CreateFun, ReadFun, UpdateFun, DeleteFun) -> + Keys = make_keys(Reps), + {TimeC, Struct} = timer:tc(?MODULE, create, [Keys, CreateFun, Empty]), + {TimeR, _} = timer:tc(?MODULE, read, [Keys, Struct, ReadFun]), + {TimeU, _} = timer:tc(?MODULE, update, [Keys, Struct, UpdateFun]), + {TimeD, _} = timer:tc(?MODULE, delete, [Keys, Struct, DeleteFun]), + [{create, TimeC, TimeC/Reps}, + {read, TimeR, TimeR/Reps}, + {update, TimeU, TimeU/Reps}, + {delete, TimeD, TimeD/Reps}]. + +%% Generate unique random numbers. No repetition allowed +make_keys(N) -> + %% The trick is to generate all numbers as usual, but match them + %% with a random value in a tuple of the form {Random, Number}. + %% The idea is to then sort the list generated that way; done in + %% this manner, we know all values will be unique and the randomness + %% will be done by the sorting. + Random = lists:sort([{random:uniform(N), X} || X <- lists:seq(1, N)]), + %% it's a good idea to then filter out the index (the random index) + %% to only return the real numbers we want. This is simple to do + %% with a list comprehension where '_' removes the extraneous data. + %% The keys are then fit into a tuple to make the test a bit more + %% realistic for comparison. + [{some, key, X} || {_, X} <- Random]. + +%% Loop function to apply the construction of a data structure. +%% The parameters passed are a list of all keys to use and then the +%% higher order function responsible of the creation of a data +%% structure. This is usually a function of the form +%% F(Key, Value, Structure). +%% In the first call, the structure has to be the empty data structure +%% that will progressively be filled. +%% The only value inserted for each key is 'some_data'; we only care +%% about the keys when dealing with key/value stuff. +create([], _, Acc) -> Acc; +create([Key|Rest], Fun, Acc) -> + create(Rest, Fun, Fun(Key, some_data, Acc)). + +%% Loop function to apply successive readings to a data structure. +%% The parameters passed are a list of all keys, the complete data +%% structure and then a higher order function responsible for +%% fetching the data. Such functions are usually of the form +%% F(Key, Structure). +read([], _, _) -> ok; +read([Key|Rest], Struct, Fun) -> + Fun(Key, Struct), + read(Rest, Struct, Fun). + +%% Loop function to apply updates to a data structure. +%% Takes a list of keys, a full data structure and a higher order +%% function responsible for the updating, usually of the form +%% F(Key, NewValue, Structure). +%% All values for a given key are replaced by 'newval', again because +%% we don't care about the values, but merely the operations with +%% the keys. +update([], _, _) -> ok; +update([Key|Rest], Struct, Fun) -> + Fun(Key, newval, Struct), + update(Rest, Struct, Fun). + +%% Loop function to apply deletions to a data structure. +%% Each deletion is made on a full data structure. +%% Takes a list of keys, a data structure and a higher order function +%% to do the deletion. Usually of the form +%% F(Key, Structure). +delete([], _, _) -> ok; +delete([Key|Rest], Struct, Fun) -> + Fun(Key, Struct), + delete(Rest, Struct, Fun). diff --git a/learn-you-some-erlang/kitchen.erl b/learn-you-some-erlang/kitchen.erl new file mode 100644 index 0000000..bbe0271 --- /dev/null +++ b/learn-you-some-erlang/kitchen.erl @@ -0,0 +1,64 @@ +-module(kitchen). +-compile(export_all). + +start(FoodList) -> + spawn(?MODULE, fridge2, [FoodList]). + +store(Pid, Food) -> + Pid ! {self(), {store, Food}}, + receive + {Pid, Msg} -> Msg + end. + +take(Pid, Food) -> + Pid ! {self(), {take, Food}}, + receive + {Pid, Msg} -> Msg + end. + +store2(Pid, Food) -> + Pid ! {self(), {store, Food}}, + receive + {Pid, Msg} -> Msg + after 3000 -> + timeout + end. + +take2(Pid, Food) -> + Pid ! {self(), {take, Food}}, + receive + {Pid, Msg} -> Msg + after 3000 -> + timeout + end. + +fridge1() -> + receive + {From, {store, _Food}} -> + From ! {self(), ok}, + fridge1(); + {From, {take, _Food}} -> + %% uh.... + From ! {self(), not_found}, + fridge1(); + terminate -> + ok + end. + +fridge2(FoodList) -> + receive + {From, {store, Food}} -> + From ! {self(), ok}, + fridge2([Food|FoodList]); + {From, {take, Food}} -> + case lists:member(Food, FoodList) of + true -> + From ! {self(), {ok, Food}}, + fridge2(lists:delete(Food, FoodList)); + false -> + From ! {self(), not_found}, + fridge2(FoodList) + end; + terminate -> + ok + end. diff --git a/learn-you-some-erlang/kitty_gen_server.erl b/learn-you-some-erlang/kitty_gen_server.erl new file mode 100644 index 0000000..c906a6a --- /dev/null +++ b/learn-you-some-erlang/kitty_gen_server.erl @@ -0,0 +1,57 @@ +-module(kitty_gen_server). +-behaviour(gen_server). + +-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(cat, {name, color=green, description}). + +%%% Client API +start_link() -> + gen_server:start_link(?MODULE, [], []). + +%% Synchronous call +order_cat(Pid, Name, Color, Description) -> + gen_server:call(Pid, {order, Name, Color, Description}). + +%% This call is asynchronous +return_cat(Pid, Cat = #cat{}) -> + gen_server:cast(Pid, {return, Cat}). + +%% Synchronous call +close_shop(Pid) -> + gen_server:call(Pid, terminate). + +%%% Server functions +init([]) -> {ok, []}. %% no treatment of info here! + +handle_call({order, Name, Color, Description}, _From, Cats) -> + if Cats =:= [] -> + {reply, make_cat(Name, Color, Description), Cats}; + Cats =/= [] -> + {reply, hd(Cats), tl(Cats)} + end; +handle_call(terminate, _From, Cats) -> + {stop, normal, ok, Cats}. + +handle_cast({return, Cat = #cat{}}, Cats) -> + {noreply, [Cat|Cats]}. + +handle_info(Msg, Cats) -> + io:format("Unexpected message: ~p~n",[Msg]), + {noreply, Cats}. + +terminate(normal, Cats) -> + [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats], + ok. + +code_change(_OldVsn, State, _Extra) -> + %% No change planned. The function is there for the behaviour, + %% but will not be used. Only a version on the next + {ok, State}. + +%%% Private functions +make_cat(Name, Col, Desc) -> + #cat{name=Name, color=Col, description=Desc}. + diff --git a/learn-you-some-erlang/kitty_server.erl b/learn-you-some-erlang/kitty_server.erl new file mode 100644 index 0000000..a40e20a --- /dev/null +++ b/learn-you-some-erlang/kitty_server.erl @@ -0,0 +1,74 @@ +%%%%% Naive version +-module(kitty_server). + +-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). + +-record(cat, {name, color=green, description}). + +%%% Client API +start_link() -> spawn_link(fun init/0). + +%% Synchronous call +order_cat(Pid, Name, Color, Description) -> + Ref = erlang:monitor(process, Pid), + Pid ! {self(), Ref, {order, Name, Color, Description}}, + receive + {Ref, Cat = #cat{}} -> + erlang:demonitor(Ref, [flush]), + Cat; + {'DOWN', Ref, process, Pid, Reason} -> + erlang:error(Reason) + after 5000 -> + erlang:error(timeout) + end. + +%% This call is asynchronous +return_cat(Pid, Cat = #cat{}) -> + Pid ! {return, Cat}, + ok. + +%% Synchronous call +close_shop(Pid) -> + Ref = erlang:monitor(process, Pid), + Pid ! {self(), Ref, terminate}, + receive + {Ref, ok} -> + erlang:demonitor(Ref, [flush]), + ok; + {'DOWN', Ref, process, Pid, Reason} -> + erlang:error(Reason) + after 5000 -> + erlang:error(timeout) + end. + +%%% Server functions +init() -> loop([]). + +loop(Cats) -> + receive + {Pid, Ref, {order, Name, Color, Description}} -> + if Cats =:= [] -> + Pid ! {Ref, make_cat(Name, Color, Description)}, + loop(Cats); + Cats =/= [] -> % got to empty the stock + Pid ! {Ref, hd(Cats)}, + loop(tl(Cats)) + end; + {return, Cat = #cat{}} -> + loop([Cat|Cats]); + {Pid, Ref, terminate} -> + Pid ! {Ref, ok}, + terminate(Cats); + Unknown -> + %% do some logging here too + io:format("Unknown message: ~p~n", [Unknown]), + loop(Cats) + end. + +%%% Private functions +make_cat(Name, Col, Desc) -> + #cat{name=Name, color=Col, description=Desc}. + +terminate(Cats) -> + [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats], + ok. diff --git a/learn-you-some-erlang/kitty_server2.erl b/learn-you-some-erlang/kitty_server2.erl new file mode 100644 index 0000000..b2acbb2 --- /dev/null +++ b/learn-you-some-erlang/kitty_server2.erl @@ -0,0 +1,49 @@ +%%%%% Abstracted version +-module(kitty_server2). + +-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). +-export([init/1, handle_call/3, handle_cast/2]). + +-record(cat, {name, color=green, description}). + +%%% Client API +start_link() -> my_server:start_link(?MODULE, []). + +%% Synchronous call +order_cat(Pid, Name, Color, Description) -> + my_server:call(Pid, {order, Name, Color, Description}). + +%% This call is asynchronous +return_cat(Pid, Cat = #cat{}) -> + my_server:cast(Pid, {return, Cat}). + +%% Synchronous call +close_shop(Pid) -> + my_server:call(Pid, terminate). + +%%% Server functions +init([]) -> []. %% no treatment of info here! + +handle_call({order, Name, Color, Description}, From, Cats) -> + if Cats =:= [] -> + my_server:reply(From, make_cat(Name, Color, Description)), + Cats; + Cats =/= [] -> + my_server:reply(From, hd(Cats)), + tl(Cats) + end; + +handle_call(terminate, From, Cats) -> + my_server:reply(From, ok), + terminate(Cats). + +handle_cast({return, Cat = #cat{}}, Cats) -> + [Cat|Cats]. + +%%% Private functions +make_cat(Name, Col, Desc) -> + #cat{name=Name, color=Col, description=Desc}. + +terminate(Cats) -> + [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats], + exit(normal). diff --git a/learn-you-some-erlang/linkmon.erl b/learn-you-some-erlang/linkmon.erl new file mode 100644 index 0000000..f93e9e5 --- /dev/null +++ b/learn-you-some-erlang/linkmon.erl @@ -0,0 +1,83 @@ +-module(linkmon). +-compile([export_all]). + +myproc() -> + timer:sleep(5000), + exit(reason). + +chain(0) -> + receive + _ -> ok + after 2000 -> + exit("chain dies here") + end; +chain(N) -> + Pid = spawn(fun() -> chain(N-1) end), + link(Pid), + receive + _ -> ok + end. + +start_critic() -> + spawn(?MODULE, critic, []). + +judge(Pid, Band, Album) -> + Pid ! {self(), {Band, Album}}, + receive + {Pid, Criticism} -> Criticism + after 2000 -> + timeout + end. + +critic() -> + receive + {From, {"Rage Against the Turing Machine", "Unit Testify"}} -> + From ! {self(), "They are great!"}; + {From, {"System of a Downtime", "Memoize"}} -> + From ! {self(), "They're not Johnny Crash but they're good."}; + {From, {"Johnny Crash", "The Token Ring of Fire"}} -> + From ! {self(), "Simply incredible."}; + {From, {_Band, _Album}} -> + From ! {self(), "They are terrible!"} + end, + critic(). + + +start_critic2() -> + spawn(?MODULE, restarter, []). + +restarter() -> + process_flag(trap_exit, true), + Pid = spawn_link(?MODULE, critic2, []), + register(critic, Pid), + receive + {'EXIT', Pid, normal} -> % not a crash + ok; + {'EXIT', Pid, shutdown} -> % manual shutdown, not a crash + ok; + {'EXIT', Pid, _} -> + restarter() + end. + +judge2(Band, Album) -> + Ref = make_ref(), + critic ! {self(), Ref, {Band, Album}}, + receive + {Ref, Criticism} -> Criticism + after 2000 -> + timeout + end. + +critic2() -> + receive + {From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} -> + From ! {Ref, "They are great!"}; + {From, Ref, {"System of a Downtime", "Memoize"}} -> + From ! {Ref, "They're not Johnny Crash but they're good."}; + {From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} -> + From ! {Ref, "Simply incredible."}; + {From, Ref, {_Band, _Album}} -> + From ! {Ref, "They are terrible!"} + end, + critic2(). + diff --git a/learn-you-some-erlang/m8ball/Emakefile b/learn-you-some-erlang/m8ball/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/m8ball/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/m8ball/config/a.config b/learn-you-some-erlang/m8ball/config/a.config new file mode 100644 index 0000000..983d5e1 --- /dev/null +++ b/learn-you-some-erlang/m8ball/config/a.config @@ -0,0 +1,9 @@ +[{kernel, + [{distributed, [{m8ball, + 5000, + [a@ferdmbp, {b@ferdmbp, c@ferdmbp}]}]}, + {sync_nodes_mandatory, [b@ferdmbp, c@ferdmbp]}, + {sync_nodes_timeout, 30000} + ] + } +]. diff --git a/learn-you-some-erlang/m8ball/config/b.config b/learn-you-some-erlang/m8ball/config/b.config new file mode 100644 index 0000000..2d0ccfc --- /dev/null +++ b/learn-you-some-erlang/m8ball/config/b.config @@ -0,0 +1,9 @@ +[{kernel, + [{distributed, [{m8ball, + 5000, + [a@ferdmbp, {b@ferdmbp, c@ferdmbp}]}]}, + {sync_nodes_mandatory, [a@ferdmbp, c@ferdmbp]}, + {sync_nodes_timeout, 30000} + ] + } +]. diff --git a/learn-you-some-erlang/m8ball/config/c.config b/learn-you-some-erlang/m8ball/config/c.config new file mode 100644 index 0000000..3dd3609 --- /dev/null +++ b/learn-you-some-erlang/m8ball/config/c.config @@ -0,0 +1,9 @@ +[{kernel, + [{distributed, [{m8ball, + 5000, + [a@ferdmbp, {b@ferdmbp, c@ferdmbp}]}]}, + {sync_nodes_mandatory, [a@ferdmbp, b@ferdmbp]}, + {sync_nodes_timeout, 30000} + ] + } +]. diff --git a/learn-you-some-erlang/m8ball/ebin/m8ball.app b/learn-you-some-erlang/m8ball/ebin/m8ball.app new file mode 100644 index 0000000..352c1d4 --- /dev/null +++ b/learn-you-some-erlang/m8ball/ebin/m8ball.app @@ -0,0 +1,13 @@ +{application, m8ball, + [{vsn, "1.0.0"}, + {description, "Answer vital questions"}, + {modules, [m8ball, m8ball_sup, m8ball_server]}, + {applications, [stdlib, kernel, crypto]}, + {registered, [m8ball, m8ball_sup, m8ball_server]}, + {mod, {m8ball, []}}, + {env, [ + {answers, {<<"Yes">>, <<"No">>, <<"Doubtful">>, + <<"I don't like your tone">>, <<"Of course">>, + <<"Of course not">>, <<"*backs away slowly and runs away*">>}} + ]} + ]}. diff --git a/learn-you-some-erlang/m8ball/logs/.track-this b/learn-you-some-erlang/m8ball/logs/.track-this new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/m8ball/logs/.track-this diff --git a/learn-you-some-erlang/m8ball/src/m8ball.erl b/learn-you-some-erlang/m8ball/src/m8ball.erl new file mode 100644 index 0000000..041601b --- /dev/null +++ b/learn-you-some-erlang/m8ball/src/m8ball.erl @@ -0,0 +1,24 @@ +-module(m8ball). +-behaviour(application). +-export([start/2, stop/1]). +-export([ask/1]). + +%%%%%%%%%%%%%%%%% +%%% CALLBACKS %%% +%%%%%%%%%%%%%%%%% + +%% start({failover, Node}, Args) is only called +%% when a start_phase key is defined. +start(normal, []) -> + m8ball_sup:start_link(); +start({takeover, _OtherNode}, []) -> + m8ball_sup:start_link(). + +stop(_State) -> + ok. + +%%%%%%%%%%%%%%%%% +%%% INTERFACE %%% +%%%%%%%%%%%%%%%%% +ask(Question) -> + m8ball_server:ask(Question). diff --git a/learn-you-some-erlang/m8ball/src/m8ball_server.erl b/learn-you-some-erlang/m8ball/src/m8ball_server.erl new file mode 100644 index 0000000..4e821ad --- /dev/null +++ b/learn-you-some-erlang/m8ball/src/m8ball_server.erl @@ -0,0 +1,46 @@ +-module(m8ball_server). +-behaviour(gen_server). +-export([start_link/0, stop/0, ask/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +%%%%%%%%%%%%%%%%% +%%% INTERFACE %%% +%%%%%%%%%%%%%%%%% +start_link() -> + gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:call({global, ?MODULE}, stop). + +ask(_Question) -> % the question doesn't matter! + gen_server:call({global, ?MODULE}, question). + +%%%%%%%%%%%%%%%%% +%%% CALLBACKS %%% +%%%%%%%%%%%%%%%%% +init([]) -> + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed(A,B,C), + {ok, []}. + +handle_call(question, _From, State) -> + {ok, Answers} = application:get_env(m8ball, answers), + Answer = element(random:uniform(tuple_size(Answers)), Answers), + {reply, Answer, State}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Call, _From, State) -> + {noreply, State}. + +handle_cast(_Cast, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. diff --git a/learn-you-some-erlang/m8ball/src/m8ball_sup.erl b/learn-you-some-erlang/m8ball/src/m8ball_sup.erl new file mode 100644 index 0000000..ca5e426 --- /dev/null +++ b/learn-you-some-erlang/m8ball/src/m8ball_sup.erl @@ -0,0 +1,16 @@ +-module(m8ball_sup). +-behaviour(supervisor). +-export([start_link/0, init/1]). + +start_link() -> + supervisor:start_link({global,?MODULE}, ?MODULE, []). + +init([]) -> + {ok, {{one_for_one, 1, 10}, + [{m8ball, + {m8ball_server, start_link, []}, + permanent, + 5000, + worker, + [m8ball_server] + }]}}. diff --git a/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl new file mode 100644 index 0000000..3afba6e --- /dev/null +++ b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl @@ -0,0 +1,32 @@ +-module(dist_m8ball_SUITE). +-include_lib("common_test/include/ct.hrl"). +-export([all/0, groups/0, init_per_group/2, end_per_group/2]). +-export([can_contact/1]). + +all() -> [{group, main}, {group, backup}]. + +groups() -> [{main, + [], + [can_contact]}, + {backup, + [], + [can_contact]}]. + +init_per_group(main, Config) -> + application:start(crypto), + application:start(m8ball), + Config; +init_per_group(backup, Config) -> + application:start(crypto), + application:start(m8ball), + Config. + +end_per_group(main, _Config) -> + %application:stop(m8ball), + ok; +end_per_group(backup, _Config) -> + %application:stop(m8ball), + ok. + +can_contact(_Config) -> + <<_/binary>> = m8ball:ask(<<"Some Question">>). diff --git a/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config new file mode 100644 index 0000000..7fbe260 --- /dev/null +++ b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config @@ -0,0 +1,9 @@ +[{kernel, + [{distributed, [{m8ball, + 5000, + ['a@li101-172.members.linode.com', 'b@li101-172.members.linode.com']}]}, + {sync_nodes_mandatory, ['a@li101-172.members.linode.com']}, + {sync_nodes_timeout, 5000} + ] + } +]. diff --git a/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config new file mode 100644 index 0000000..bcac447 --- /dev/null +++ b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config @@ -0,0 +1,9 @@ +[{kernel, + [{distributed, [{m8ball, + 5000, + ['a@li101-172.members.linode.com', 'b@li101-172.members.linode.com']}]}, + {sync_nodes_mandatory, ['b@li101-172.members.linode.com']}, + {sync_nodes_timeout, 5000} + ] + } +]. diff --git a/learn-you-some-erlang/m8ball/test/m8ball.spec b/learn-you-some-erlang/m8ball/test/m8ball.spec new file mode 100644 index 0000000..51d9cf3 --- /dev/null +++ b/learn-you-some-erlang/m8ball/test/m8ball.spec @@ -0,0 +1,5 @@ +{include, "../include/"}. +{alias, root, "/home/ferd/code/learn-you-some-erlang/m8ball/test"}. +{logdir, "/home/ferd/code/learn-you-some-erlang/m8ball/logs"}. + +{suites, root, all}. diff --git a/learn-you-some-erlang/m8ball/test/m8ball_dist.spec b/learn-you-some-erlang/m8ball/test/m8ball_dist.spec new file mode 100644 index 0000000..c49a6ca --- /dev/null +++ b/learn-you-some-erlang/m8ball/test/m8ball_dist.spec @@ -0,0 +1,20 @@ +{node, master, 'ct@li101-172.members.linode.com'}. +{node, a, 'a@li101-172.members.linode.com'}. +{node, b, 'b@li101-172.members.linode.com'}. + +{init, a, [{node_start, [{monitor_master, true}, + {boot_timeout, 10000}, + {erl_flags, "-pa /home/ferd/code/learn-you-some-erlang/m8ball/ebin/ " + "-config /home/ferd/code/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config"}]}]}. +{init, b, [{node_start, [{monitor_master, true}, + {boot_timeout, 10000}, + {erl_flags, "-pa /home/ferd/code/learn-you-some-erlang/m8ball/ebin/ " + "-config /home/ferd/code/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config"}]}]}. + +{include, "../include/"}. +{alias, root, "/home/ferd/code/learn-you-some-erlang/m8ball/test"}. +{logdir, "/home/ferd/code/learn-you-some-erlang/m8ball/logs"}. +{logdir, master, "/home/ferd/code/learn-you-some-erlang/m8ball/logs"}. + +{groups, a, root, dist_m8ball_SUITE, main}. +{groups, b, root, dist_m8ball_SUITE, backup}. diff --git a/learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl b/learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl new file mode 100644 index 0000000..6bc4156 --- /dev/null +++ b/learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl @@ -0,0 +1,45 @@ +-module(m8ball_server_SUITE). +-include_lib("common_test/include/ct.hrl"). +-export([all/0, init_per_testcase/2, end_per_testcase/2]). +-export([random_answer/1, binary_answer/1, determined_answers/1, + reload_answers/1]). + +all() -> [random_answer, binary_answer, determined_answers, + reload_answers]. + +init_per_testcase(_Test, Config) -> + Answers = [<<"Outlook not so good">>, + <<"I don't think so">>, + <<"Yes, definitely">>, + <<"STOP SHAKING ME">>], + application:set_env(m8ball, answers, list_to_tuple(Answers)), + m8ball_server:start_link(), + [{answers, Answers} | Config]. + +end_per_testcase(_Test, _Config) -> + m8ball_server:stop(). + +%% The answer should come from the config +random_answer(_Config) -> + Answers1 = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)], + Answers2 = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)], + true = 1 < length(sets:to_list(sets:from_list(Answers1))), + true = Answers1 =/= Answers2. + +binary_answer(_Config) -> + Answers = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)], + L = length(Answers), + L = length(lists:filter(fun erlang:is_binary/1, Answers)). + +determined_answers(Config) -> + Answers = ?config(answers, Config), + Res = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)], + true = lists:all(fun(X) -> lists:member(X, Answers) end, Res). + +reload_answers(_Config) -> + Ref = make_ref(), + application:set_env(m8ball, answers, {Ref}), + [Ref,Ref,Ref] = [m8ball_server:ask("Question") || _ <- lists:seq(1,3)]. + + +%% NOTE: do distributed testing in m8ball_SUITE or something diff --git a/learn-you-some-erlang/mafiapp-1.0.0/Emakefile b/learn-you-some-erlang/mafiapp-1.0.0/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app b/learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app new file mode 100644 index 0000000..972432f --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app @@ -0,0 +1,5 @@ +{application, mafiapp, + [{description, "Help the boss keep track of his friends"}, + {vsn, "1.0.0"}, + {modules, [mafiapp, mafiapp_sup]}, + {applications, [stdlib, kernel, mnesia]}]}. diff --git a/learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this b/learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this diff --git a/learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec new file mode 100644 index 0000000..4953e67 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec @@ -0,0 +1,3 @@ +{alias, root, "./test/"}. +{logdir, "./logs/"}. +{suites, root, all}. diff --git a/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl new file mode 100644 index 0000000..0e6a2b0 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl @@ -0,0 +1,172 @@ +-module(mafiapp). +-behaviour(application). +-include_lib("stdlib/include/ms_transform.hrl"). +-export([start/2, stop/1]). +-export([install/1]). +-export([add_friend/4, friend_by_name/1, friend_by_expertise/1, + add_service/4, debts/1]). +-export([add_enemy/2, find_enemy/1, enemy_killed/1]). + + +-record(mafiapp_friends, {name, + contact=[], + info=[], + expertise}). +-record(mafiapp_services, {from, + to, + date, + description}). +-record(mafiapp_enemies, {name, + info=[]}). + +start(normal, []) -> + mnesia:wait_for_tables([mafiapp_friends, + mafiapp_services, + mafiapp_enemies], 5000), + mafiapp_sup:start_link(). + + +stop(_) -> ok. + +install(Nodes) -> + ok = mnesia:create_schema(Nodes), + rpc:multicall(Nodes, application, start, [mnesia]), + mnesia:create_table(mafiapp_friends, + [{attributes, record_info(fields, mafiapp_friends)}, + {index, [#mafiapp_friends.expertise]}, + {disc_copies, Nodes}]), + mnesia:create_table(mafiapp_services, + [{attributes, record_info(fields, mafiapp_services)}, + {index, [#mafiapp_services.to]}, + {disc_copies, Nodes}, + {type, bag}]), + mnesia:create_table(mafiapp_enemies, + [{attributes, record_info(fields, mafiapp_enemies)}, + {disc_copies, Nodes}, + {local_content, true}]), + rpc:multicall(Nodes, application, stop, [mnesia]). + +add_friend(Name, Contact, Info, Expertise) -> + F = fun() -> + mnesia:write(#mafiapp_friends{name=Name, + contact=Contact, + info=Info, + expertise=Expertise}) + end, + mnesia:activity(transaction, F). + +friend_by_name(Name) -> + F = fun() -> + case mnesia:read({mafiapp_friends, Name}) of + [#mafiapp_friends{contact=C, info=I, expertise=E}] -> + {Name,C,I,E,find_services(Name)}; + [] -> + undefined + end + end, + mnesia:activity(transaction, F). + +friend_by_expertise(Expertise) -> + %% Alternative form: + %% MatchSpec = [{#mafiapp_friends{name = '$1', + %% contact = '$2', + %% info = '$3', + %% expertise = Expertise}, + %% [], + %% [{{'$1','$2','$3',Expertise}}]}], + %% F = fun() -> + %% [{Name,C,I,E,find_services(Name)} || + %% {Name,C,I,E} <- mnesia:select(mafiapp_friends, MatchSpec)] + %% end, + Pattern = #mafiapp_friends{_ = '_', + expertise = Expertise}, + F = fun() -> + Res = mnesia:match_object(Pattern), + [{Name,C,I,Expertise,find_services(Name)} || + #mafiapp_friends{name=Name, + contact=C, + info=I} <- Res] + end, + mnesia:activity(transaction, F). + +%% Adding validation is left to the reader +add_service(From, To, Date, Description) -> + F = fun() -> + case mnesia:read({mafiapp_friends, From}) =:= [] orelse + mnesia:read({mafiapp_friends, To}) =:= [] of + true -> + {error, unknown_friend}; + false -> + mnesia:write(#mafiapp_services{from=From, + to=To, + date=Date, + description=Description}) + end + end, + mnesia:activity(transaction,F). + +debts(Name) -> + Match = ets:fun2ms( + fun(#mafiapp_services{from=From, to=To}) when From =:= Name -> + {To,-1}; + (#mafiapp_services{from=From, to=To}) when To =:= Name -> + {From,1} + end), + F = fun() -> mnesia:select(mafiapp_services, Match) end, + Dict = lists:foldl(fun({Person,N}, Dict) -> + dict:update(Person, fun(X) -> X + N end, N, Dict) + end, + dict:new(), + mnesia:activity(transaction, F)), + lists:sort([{V,K} || {K,V} <- dict:to_list(Dict)]). +%% The following implementation is a somewhat more efficient +%% alternative on large databases, given it will use indexes +%% rather than traversing the entire table. +% F = fun() -> +% Given = mnesia:read({mafiapp_services, Name}), +% Received = mnesia:match_object(#mafiapp_services{to=Name, _='_'}), +% {Given, Received} +% end, +% {Given, Received} = mnesia:activity(transaction, F), +% Dict1 = lists:foldl(fun(#mafiapp_services{to=N}, Dict) -> +% dict:update(N, fun(X) -> X - 1 end, -1, Dict) +% end, +% dict:new(), +% Given), +% Dict2 = lists:foldl(fun(#mafiapp_services{from=N}, Dict) -> +% dict:update(N, fun(X) -> X + 1 end, 1, Dict) +% end, +% Dict1, +% Received), +% lists:sort([{V,K} || {K,V} <- dict:to_list(Dict2)]). + +add_enemy(Name, Info) -> + F = fun() -> mnesia:write(#mafiapp_enemies{name=Name, info=Info}) end, + mnesia:activity(transaction, F). + +find_enemy(Name) -> + F = fun() -> mnesia:read({mafiapp_enemies, Name}) end, + case mnesia:activity(transaction, F) of + [] -> undefined; + [#mafiapp_enemies{name=N, info=I}] -> {N,I} + end. + +enemy_killed(Name) -> + F = fun() -> mnesia:delete({mafiapp_enemies, Name}) end, + mnesia:activity(transaction, F). + +%%%%%%%%%%%%%%% +%%% PRIVATE %%% +%%%%%%%%%%%%%%% + +find_services(Name) -> + Match = ets:fun2ms( + fun(#mafiapp_services{from=From, to=To, date=D, description=Desc}) + when From =:= Name -> + {to, To, D, Desc}; + (#mafiapp_services{from=From, to=To, date=D, description=Desc}) + when To =:= Name -> + {from, From, D, Desc} + end + ), + mnesia:select(mafiapp_services, Match). diff --git a/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl new file mode 100644 index 0000000..e31b221 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl @@ -0,0 +1,12 @@ +-module(mafiapp_sup). +-behaviour(supervisor). +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link(?MODULE, []). + +%% This does absolutely nothing, only there to +%% allow to wait for tables. +init([]) -> + {ok, {{one_for_one, 1, 1}, []}}. diff --git a/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec new file mode 100644 index 0000000..4953e67 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec @@ -0,0 +1,3 @@ +{alias, root, "./test/"}. +{logdir, "./logs/"}. +{suites, root, all}. diff --git a/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl new file mode 100644 index 0000000..6005d29 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl @@ -0,0 +1,128 @@ +-module(mafiapp_SUITE). +-include_lib("common_test/include/ct.hrl"). +-export([init_per_suite/1, end_per_suite/1, + init_per_testcase/2, end_per_testcase/2, + all/0]). +-export([add_service/1, friend_by_name/1, friend_by_expertise/1, + friend_with_services/1, accounts/1, enemies/1]). + +all() -> [add_service, friend_by_name, friend_by_expertise, + friend_with_services, accounts, enemies]. + +init_per_suite(Config) -> + Priv = ?config(priv_dir, Config), + application:load(mnesia), + application:set_env(mnesia, dir, Priv), + application:load(mafiapp), + mafiapp:install([node()]), + application:start(mnesia), + application:start(mafiapp), + Config. + +end_per_suite(_Config) -> + application:stop(mnesia), + ok. + +init_per_testcase(add_service, Config) -> + Config; +init_per_testcase(accounts, Config) -> + ok = mafiapp:add_friend("Consigliere", [], [you], consigliere), + Config; +init_per_testcase(_, Config) -> + ok = mafiapp:add_friend("Don Corleone", [], [boss], boss), + Config. + +end_per_testcase(_, _Config) -> + ok. + +%% services can go both way: from a friend to the boss, or +%% from the boss to a friend! A boss friend is required! +add_service(_Config) -> + {error, unknown_friend} = mafiapp:add_service("from name", + "to name", + {1946,5,23}, + "a fake service"), + ok = mafiapp:add_friend("Don Corleone", [], [boss], boss), + ok = mafiapp:add_friend("Alan Parsons", + [{twitter,"@ArtScienceSound"}], + [{born, {1948,12,20}}, + musician, 'audio engineer', + producer, "has projects"], + mixing), + ok = mafiapp:add_service("Alan Parsons", "Don Corleone", + {1973,3,1}, + "Helped release a Pink Floyd album"). + +friend_by_name(_Config) -> + ok = mafiapp:add_friend("Pete Cityshend", + [{phone, "418-542-3000"}, + {email, "quadrophonia@example.org"}, + {other, "yell real loud"}], + [{born, {1945,5,19}}, + musician, popular], + music), + {"Pete Cityshend", + _Contact, _Info, music, + _Services} = mafiapp:friend_by_name("Pete Cityshend"), + undefined = mafiapp:friend_by_name(make_ref()). + +friend_by_expertise(_Config) -> + ok = mafiapp:add_friend("A Red Panda", + [{location, "in a zoo"}], + [animal,cute], + climbing), + [{"A Red Panda", + _Contact, _Info, climbing, + _Services}] = mafiapp:friend_by_expertise(climbing), + [] = mafiapp:friend_by_expertise(make_ref()). + +friend_with_services(_Config) -> + ok = mafiapp:add_friend("Someone", [{other, "at the fruit stand"}], + [weird, mysterious], shadiness), + ok = mafiapp:add_service("Don Corleone", "Someone", + {1949,2,14}, "Increased business"), + ok = mafiapp:add_service("Someone", "Don Corleone", + {1949,12,25}, "Gave a Christmas gift"), + %% We don't care about the order. The test was made to fit + %% whatever the functions returned. + {"Someone", + _Contact, _Info, shadiness, + [{to, "Don Corleone", {1949,12,25}, "Gave a Christmas gift"}, + {from, "Don Corleone", {1949,2,14}, "Increased business"}]} = + mafiapp:friend_by_name("Someone"). + +%% It should be possible to find all people who owe us things. +accounts(_Config) -> + ok = mafiapp:add_friend("Gill Bates", [{email, "ceo@macrohard.com"}], + [clever,rich], computers), + ok = mafiapp:add_service("Consigliere", "Gill Bates", + {1985,11,20}, "Bought 15 copies of software"), + ok = mafiapp:add_service("Gill Bates", "Consigliere", + {1986,8,17}, "Made computer faster"), + ok = mafiapp:add_friend("Pierre Gauthier", [{other, "city arena"}], + [{job, "sports team GM"}], sports), + ok = mafiapp:add_service("Pierre Gauthier", "Consigliere", {2009,6,30}, + "Took on a huge, bad contract"), + ok = mafiapp:add_friend("Wayne Gretzky", [{other, "Canada"}], + [{born, {1961,1,26}}, "hockey legend"], + hockey), + ok = mafiapp:add_service("Consigliere", "Wayne Gretzky", {1964,1,26}, + "Gave first pair of ice skates"), + %% Wayne Gretzky owes us something so the debt is negative + %% Gill Bates are equal + %% Gauthier is owed a service. + [{-1,"Wayne Gretzky"}, + {0,"Gill Bates"}, + {1,"Pierre Gauthier"}] = mafiapp:debts("Consigliere"), + [{1, "Consigliere"}] = mafiapp:debts("Wayne Gretzky"). + +enemies(_Config) -> + undefined = mafiapp:find_enemy("Edward"), + ok = mafiapp:add_enemy("Edward", [{bio, "Vampire"}, + {comment, "He sucks (blood)"}]), + {"Edward", [{bio, "Vampire"}, + {comment, "He sucks (blood)"}]} = + mafiapp:find_enemy("Edward"), + ok = mafiapp:enemy_killed("Edward"), + undefined = mafiapp:find_enemy("Edward"). + diff --git a/learn-you-some-erlang/mafiapp-1.0.1/Emakefile b/learn-you-some-erlang/mafiapp-1.0.1/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app b/learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app new file mode 100644 index 0000000..f4268ae --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app @@ -0,0 +1,5 @@ +{application, mafiapp, + [{description, "Help the boss keep track of his friends"}, + {vsn, "1.0.1"}, + {modules, [mafiapp, mafiapp_sup]}, + {applications, [stdlib, kernel, mnesia]}]}. diff --git a/learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this b/learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this diff --git a/learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec new file mode 100644 index 0000000..4953e67 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec @@ -0,0 +1,3 @@ +{alias, root, "./test/"}. +{logdir, "./logs/"}. +{suites, root, all}. diff --git a/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl new file mode 100644 index 0000000..f40ebce --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl @@ -0,0 +1,142 @@ +-module(mafiapp). +-behaviour(application). +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("stdlib/include/qlc.hrl"). +-export([start/2, stop/1]). +-export([install/1]). +-export([add_friend/4, friend_by_name/1, friend_by_expertise/1, + add_service/4, debts/1]). +-export([add_enemy/2, find_enemy/1, enemy_killed/1]). + + +-record(mafiapp_friends, {name, + contact=[], + info=[], + expertise}). +-record(mafiapp_services, {from, + to, + date, + description}). +-record(mafiapp_enemies, {name, + info=[]}). + +start(normal, []) -> + mnesia:wait_for_tables([mafiapp_friends, + mafiapp_services, + mafiapp_enemies], 5000), + mafiapp_sup:start_link(). + +stop(_) -> ok. + +install(Nodes) -> + ok = mnesia:create_schema(Nodes), + rpc:multicall(Nodes, application, start, [mnesia]), + mnesia:create_table(mafiapp_friends, + [{attributes, record_info(fields, mafiapp_friends)}, + {index, [#mafiapp_friends.expertise]}, + {disc_copies, Nodes}]), + mnesia:create_table(mafiapp_services, + [{attributes, record_info(fields, mafiapp_services)}, + {index, [#mafiapp_services.to]}, + {disc_copies, Nodes}, + {type, bag}]), + mnesia:create_table(mafiapp_enemies, + [{attributes, record_info(fields, mafiapp_enemies)}, + {disc_copies, Nodes}, + {local_content, true}]), + rpc:multicall(Nodes, application, stop, [mnesia]). + +add_friend(Name, Contact, Info, Expertise) -> + F = fun() -> + mnesia:write(#mafiapp_friends{name=Name, + contact=Contact, + info=Info, + expertise=Expertise}) + end, + mnesia:activity(transaction, F). + +friend_by_name(Name) -> + F = fun() -> + case mnesia:read({mafiapp_friends, Name}) of + [#mafiapp_friends{contact=C, info=I, expertise=E}] -> + {Name,C,I,E,find_services(Name)}; + [] -> + undefined + end + end, + mnesia:activity(transaction, F). + +friend_by_expertise(Expertise) -> + F = fun() -> + qlc:eval(qlc:q( + [{Name,C,I,E,find_services(Name)} || + #mafiapp_friends{name=Name, + contact=C, + info=I, + expertise=E} <- mnesia:table(mafiapp_friends), + E =:= Expertise])) + end, + mnesia:activity(transaction, F). + +%% Adding validation is left to the reader +add_service(From, To, Date, Description) -> + F = fun() -> + case mnesia:read({mafiapp_friends, From}) =:= [] orelse + mnesia:read({mafiapp_friends, To}) =:= [] of + true -> + {error, unknown_friend}; + false -> + mnesia:write(#mafiapp_services{from=From, + to=To, + date=Date, + description=Description}) + end + end, + mnesia:activity(transaction,F). + +debts(Name) -> + F = fun() -> + QH = qlc:q( + [if Name =:= To -> {From,1}; + Name =:= From -> {To,-1} + end || #mafiapp_services{from=From, to=To} <- + mnesia:table(mafiapp_services), + Name =:= To orelse Name =:= From]), + qlc:fold(fun({Person,N}, Dict) -> + dict:update(Person, fun(X) -> X + N end, N, Dict) + end, + dict:new(), + QH) + end, + lists:sort([{V,K} || {K,V} <- dict:to_list(mnesia:activity(transaction, F))]). + +add_enemy(Name, Info) -> + F = fun() -> mnesia:write(#mafiapp_enemies{name=Name, info=Info}) end, + mnesia:activity(transaction, F). + +find_enemy(Name) -> + F = fun() -> mnesia:read({mafiapp_enemies, Name}) end, + case mnesia:activity(transaction, F) of + [] -> undefined; + [#mafiapp_enemies{name=N, info=I}] -> {N,I} + end. + +enemy_killed(Name) -> + F = fun() -> mnesia:delete({mafiapp_enemies, Name}) end, + mnesia:activity(transaction, F). + +%%%%%%%%%%%%%%% +%%% PRIVATE %%% +%%%%%%%%%%%%%%% + +find_services(Name) -> + Match = ets:fun2ms( + fun(#mafiapp_services{from=From, to=To, date=D, description=Desc}) + when From =:= Name -> + {to, To, D, Desc}; + (#mafiapp_services{from=From, to=To, date=D, description=Desc}) + when To =:= Name -> + {from, From, D, Desc} + end + ), + mnesia:select(mafiapp_services, Match). diff --git a/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl new file mode 100644 index 0000000..e31b221 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl @@ -0,0 +1,12 @@ +-module(mafiapp_sup). +-behaviour(supervisor). +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link(?MODULE, []). + +%% This does absolutely nothing, only there to +%% allow to wait for tables. +init([]) -> + {ok, {{one_for_one, 1, 1}, []}}. diff --git a/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec new file mode 100644 index 0000000..4953e67 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec @@ -0,0 +1,3 @@ +{alias, root, "./test/"}. +{logdir, "./logs/"}. +{suites, root, all}. diff --git a/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl new file mode 100644 index 0000000..6005d29 --- /dev/null +++ b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl @@ -0,0 +1,128 @@ +-module(mafiapp_SUITE). +-include_lib("common_test/include/ct.hrl"). +-export([init_per_suite/1, end_per_suite/1, + init_per_testcase/2, end_per_testcase/2, + all/0]). +-export([add_service/1, friend_by_name/1, friend_by_expertise/1, + friend_with_services/1, accounts/1, enemies/1]). + +all() -> [add_service, friend_by_name, friend_by_expertise, + friend_with_services, accounts, enemies]. + +init_per_suite(Config) -> + Priv = ?config(priv_dir, Config), + application:load(mnesia), + application:set_env(mnesia, dir, Priv), + application:load(mafiapp), + mafiapp:install([node()]), + application:start(mnesia), + application:start(mafiapp), + Config. + +end_per_suite(_Config) -> + application:stop(mnesia), + ok. + +init_per_testcase(add_service, Config) -> + Config; +init_per_testcase(accounts, Config) -> + ok = mafiapp:add_friend("Consigliere", [], [you], consigliere), + Config; +init_per_testcase(_, Config) -> + ok = mafiapp:add_friend("Don Corleone", [], [boss], boss), + Config. + +end_per_testcase(_, _Config) -> + ok. + +%% services can go both way: from a friend to the boss, or +%% from the boss to a friend! A boss friend is required! +add_service(_Config) -> + {error, unknown_friend} = mafiapp:add_service("from name", + "to name", + {1946,5,23}, + "a fake service"), + ok = mafiapp:add_friend("Don Corleone", [], [boss], boss), + ok = mafiapp:add_friend("Alan Parsons", + [{twitter,"@ArtScienceSound"}], + [{born, {1948,12,20}}, + musician, 'audio engineer', + producer, "has projects"], + mixing), + ok = mafiapp:add_service("Alan Parsons", "Don Corleone", + {1973,3,1}, + "Helped release a Pink Floyd album"). + +friend_by_name(_Config) -> + ok = mafiapp:add_friend("Pete Cityshend", + [{phone, "418-542-3000"}, + {email, "quadrophonia@example.org"}, + {other, "yell real loud"}], + [{born, {1945,5,19}}, + musician, popular], + music), + {"Pete Cityshend", + _Contact, _Info, music, + _Services} = mafiapp:friend_by_name("Pete Cityshend"), + undefined = mafiapp:friend_by_name(make_ref()). + +friend_by_expertise(_Config) -> + ok = mafiapp:add_friend("A Red Panda", + [{location, "in a zoo"}], + [animal,cute], + climbing), + [{"A Red Panda", + _Contact, _Info, climbing, + _Services}] = mafiapp:friend_by_expertise(climbing), + [] = mafiapp:friend_by_expertise(make_ref()). + +friend_with_services(_Config) -> + ok = mafiapp:add_friend("Someone", [{other, "at the fruit stand"}], + [weird, mysterious], shadiness), + ok = mafiapp:add_service("Don Corleone", "Someone", + {1949,2,14}, "Increased business"), + ok = mafiapp:add_service("Someone", "Don Corleone", + {1949,12,25}, "Gave a Christmas gift"), + %% We don't care about the order. The test was made to fit + %% whatever the functions returned. + {"Someone", + _Contact, _Info, shadiness, + [{to, "Don Corleone", {1949,12,25}, "Gave a Christmas gift"}, + {from, "Don Corleone", {1949,2,14}, "Increased business"}]} = + mafiapp:friend_by_name("Someone"). + +%% It should be possible to find all people who owe us things. +accounts(_Config) -> + ok = mafiapp:add_friend("Gill Bates", [{email, "ceo@macrohard.com"}], + [clever,rich], computers), + ok = mafiapp:add_service("Consigliere", "Gill Bates", + {1985,11,20}, "Bought 15 copies of software"), + ok = mafiapp:add_service("Gill Bates", "Consigliere", + {1986,8,17}, "Made computer faster"), + ok = mafiapp:add_friend("Pierre Gauthier", [{other, "city arena"}], + [{job, "sports team GM"}], sports), + ok = mafiapp:add_service("Pierre Gauthier", "Consigliere", {2009,6,30}, + "Took on a huge, bad contract"), + ok = mafiapp:add_friend("Wayne Gretzky", [{other, "Canada"}], + [{born, {1961,1,26}}, "hockey legend"], + hockey), + ok = mafiapp:add_service("Consigliere", "Wayne Gretzky", {1964,1,26}, + "Gave first pair of ice skates"), + %% Wayne Gretzky owes us something so the debt is negative + %% Gill Bates are equal + %% Gauthier is owed a service. + [{-1,"Wayne Gretzky"}, + {0,"Gill Bates"}, + {1,"Pierre Gauthier"}] = mafiapp:debts("Consigliere"), + [{1, "Consigliere"}] = mafiapp:debts("Wayne Gretzky"). + +enemies(_Config) -> + undefined = mafiapp:find_enemy("Edward"), + ok = mafiapp:add_enemy("Edward", [{bio, "Vampire"}, + {comment, "He sucks (blood)"}]), + {"Edward", [{bio, "Vampire"}, + {comment, "He sucks (blood)"}]} = + mafiapp:find_enemy("Edward"), + ok = mafiapp:enemy_killed("Edward"), + undefined = mafiapp:find_enemy("Edward"). + diff --git a/learn-you-some-erlang/multiproc.erl b/learn-you-some-erlang/multiproc.erl new file mode 100644 index 0000000..abddaf9 --- /dev/null +++ b/learn-you-some-erlang/multiproc.erl @@ -0,0 +1,39 @@ +-module(multiproc). +-compile([export_all]). + +sleep(T) -> + receive + after T -> ok + end. + +flush() -> + receive + _ -> flush() + after 0 -> + ok + end. + +important() -> + receive + {Priority, Message} when Priority > 10 -> + [Message | important()] + after 0 -> + normal() + end. + +normal() -> + receive + {_, Message} -> + [Message | normal()] + after 0 -> + [] + end. + +%% optimized in R14A +optimized(Pid) -> + Ref = make_ref(), + Pid ! {self(), Ref, hello}, + receive + {Pid, Ref, Msg} -> + io:format("~p~n", [Msg]) + end. diff --git a/learn-you-some-erlang/musicians.erl b/learn-you-some-erlang/musicians.erl new file mode 100644 index 0000000..e369d44 --- /dev/null +++ b/learn-you-some-erlang/musicians.erl @@ -0,0 +1,84 @@ +-module(musicians). +-behaviour(gen_server). + +-export([start_link/2, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, code_change/3, terminate/2]). + +-record(state, {name="", role, skill=good}). +-define(DELAY, 750). + +start_link(Role, Skill) -> + gen_server:start_link({local, Role}, ?MODULE, [Role, Skill], []). + +stop(Role) -> gen_server:call(Role, stop). + +init([Role, Skill]) -> + %% To know when the parent shuts down + process_flag(trap_exit, true), + %% sets a seed for random number generation for the life of the process + %% uses the current time to do it. Unique value guaranteed by now() + random:seed(now()), + TimeToPlay = random:uniform(3000), + Name = pick_name(), + StrRole = atom_to_list(Role), + io:format("Musician ~s, playing the ~s entered the room~n", + [Name, StrRole]), + {ok, #state{name=Name, role=StrRole, skill=Skill}, TimeToPlay}. + +handle_call(stop, _From, S=#state{}) -> + {stop, normal, ok, S}; +handle_call(_Message, _From, S) -> + {noreply, S, ?DELAY}. + +handle_cast(_Message, S) -> + {noreply, S, ?DELAY}. + +handle_info(timeout, S = #state{name=N, skill=good}) -> + io:format("~s produced sound!~n",[N]), + {noreply, S, ?DELAY}; +handle_info(timeout, S = #state{name=N, skill=bad}) -> + case random:uniform(5) of + 1 -> + io:format("~s played a false note. Uh oh~n",[N]), + {stop, bad_note, S}; + _ -> + io:format("~s produced sound!~n",[N]), + {noreply, S, ?DELAY} + end; +handle_info(_Message, S) -> + {noreply, S, ?DELAY}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(normal, S) -> + io:format("~s left the room (~s)~n",[S#state.name, S#state.role]); +terminate(bad_note, S) -> + io:format("~s sucks! kicked that member out of the band! (~s)~n", + [S#state.name, S#state.role]); +terminate(shutdown, S) -> + io:format("The manager is mad and fired the whole band! " + "~s just got back to playing in the subway~n", + [S#state.name]); +terminate(_Reason, S) -> + io:format("~s has been kicked out (~s)~n", [S#state.name, S#state.role]). + +%% Yes, the names are based off the magic school bus characters +%% 10 names! +pick_name() -> + %% the seed must be set for the random functions. Use within the + %% process that started with init/1 + lists:nth(random:uniform(10), firstnames()) + ++ " " ++ + lists:nth(random:uniform(10), lastnames()). + +firstnames() -> + ["Valerie", "Arnold", "Carlos", "Dorothy", "Keesha", + "Phoebe", "Ralphie", "Tim", "Wanda", "Janet"]. + +lastnames() -> + ["Frizzle", "Perlstein", "Ramon", "Ann", "Franklin", + "Terese", "Tennelli", "Jamal", "Li", "Perlstein"]. + + diff --git a/learn-you-some-erlang/my_server.erl b/learn-you-some-erlang/my_server.erl new file mode 100644 index 0000000..ed83a7a --- /dev/null +++ b/learn-you-some-erlang/my_server.erl @@ -0,0 +1,42 @@ +-module(my_server). +-export([start/2, start_link/2, call/2, cast/2, reply/2]). + +%%% Public API +start(Module, InitialState) -> + spawn(fun() -> init(Module, InitialState) end). + +start_link(Module, InitialState) -> + spawn_link(fun() -> init(Module, InitialState) end). + +call(Pid, Msg) -> + Ref = erlang:monitor(process, Pid), + Pid ! {sync, self(), Ref, Msg}, + receive + {Ref, Reply} -> + erlang:demonitor(Ref, [flush]), + Reply; + {'DOWN', Ref, process, Pid, Reason} -> + erlang:error(Reason) + after 5000 -> + erlang:error(timeout) + end. + +cast(Pid, Msg) -> + Pid ! {async, Msg}, + ok. + +reply({Pid, Ref}, Reply) -> + Pid ! {Ref, Reply}. + +%%% Private stuff +init(Module, InitialState) -> + loop(Module, Module:init(InitialState)). + +loop(Module, State) -> + receive + {async, Msg} -> + loop(Module, Module:handle_cast(Msg, State)); + {sync, Pid, Ref, Msg} -> + loop(Module, Module:handle_call(Msg, {Pid, Ref}, State)) + end. + diff --git a/learn-you-some-erlang/oop.erl b/learn-you-some-erlang/oop.erl new file mode 100644 index 0000000..19653d3 --- /dev/null +++ b/learn-you-some-erlang/oop.erl @@ -0,0 +1,28 @@ +-module(oop). +-export([animal/1, dog/1, cat/1]). + +%% all the method calls need to be in tuples when they have more than +%% one argument so we can use functions of arity 1 for every call we make. + +animal(Name) -> + fun(type) -> "living thing"; + (name) -> Name; + (move) -> Name++" moves around..."; + ({eat, Item}) -> Name++" eats "++Item; + (_) -> "I'm sorry Dave, I can't do that." + end. + +dog(Name) -> + Parent = animal(Name), + fun(talk) -> Name++" says: Woof!"; + ({chase, Animal}) when is_function(Animal) -> + Name++" chases a "++Animal(type)++" named "++Animal(name)++" around"; + (X) -> Parent(X) + end. + +cat(Name) -> + Parent = animal(Name), + fun(type) -> "cat"; + (talk) -> Name++" says: Meow!"; + (X) -> Parent(X) + end. diff --git a/learn-you-some-erlang/ops.erl b/learn-you-some-erlang/ops.erl new file mode 100644 index 0000000..ccd95cd --- /dev/null +++ b/learn-you-some-erlang/ops.erl @@ -0,0 +1,4 @@ +-module(ops). +-export([add/2]). + +add(A,B) -> A + B. diff --git a/learn-you-some-erlang/ops_tests.erl b/learn-you-some-erlang/ops_tests.erl new file mode 100644 index 0000000..53323cf --- /dev/null +++ b/learn-you-some-erlang/ops_tests.erl @@ -0,0 +1,25 @@ +-module(ops_tests). +-include_lib("eunit/include/eunit.hrl"). + +add_test() -> + 4 = ops:add(2,2). + +new_add_test() -> + ?assertEqual(4, ops:add(2,2)), + ?assertEqual(3, ops:add(1,2)), + ?assert(is_number(ops:add(1,2))), + ?assertEqual(3, ops:add(1,1)), + ?assertError(badarith, 1/0). + +add_test_() -> + [test_them_types(), + test_them_values(), + ?_assertError(badarith, 1/0)]. + +test_them_types() -> + ?_assert(is_number(ops:add(1,2))). + +test_them_values() -> + [?_assertEqual(4, ops:add(2,2)), + ?_assertEqual(3, ops:add(1,2)), + ?_assertEqual(3, ops:add(1,1))]. diff --git a/learn-you-some-erlang/pool/ppool.erl b/learn-you-some-erlang/pool/ppool.erl new file mode 100644 index 0000000..b5d34c0 --- /dev/null +++ b/learn-you-some-erlang/pool/ppool.erl @@ -0,0 +1,25 @@ +%%% API module for the pool +-module(ppool). +-export([start_link/0, stop/0, start_pool/3, + run/2, sync_queue/2, async_queue/2, stop_pool/1]). + +start_link() -> + ppool_supersup:start_link(). + +stop() -> + ppool_supersup:stop(). + +start_pool(Name, Limit, {M,F,A}) -> + ppool_supersup:start_pool(Name, Limit, {M,F,A}). + +stop_pool(Name) -> + ppool_supersup:stop_pool(Name). + +run(Name, Args) -> + ppool_serv:run(Name, Args). + +async_queue(Name, Args) -> + ppool_serv:async_queue(Name, Args). + +sync_queue(Name, Args) -> + ppool_serv:sync_queue(Name, Args). diff --git a/learn-you-some-erlang/pool/ppool_nagger.erl b/learn-you-some-erlang/pool/ppool_nagger.erl new file mode 100644 index 0000000..903f821 --- /dev/null +++ b/learn-you-some-erlang/pool/ppool_nagger.erl @@ -0,0 +1,50 @@ +%% demo module, a nagger for tasks, +%% because the previous one wasn't good enough +%% +%% Can take: +%% - a time delay for which to nag, +%% - an adress to say where the messages should be sent +%% - a message to send in the mailbox telling you what to nag, +%% with an id to be able to call: -> +%% - a command to say the task is done +-module(ppool_nagger). +-behaviour(gen_server). +-export([start_link/4, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, code_change/3, terminate/2]). + +start_link(Task, Delay, Max, SendTo) -> + gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo} , []). + +stop(Pid) -> + gen_server:call(Pid, stop). + +init({Task, Delay, Max, SendTo}) -> + process_flag(trap_exit, true), % for tests & terminate too + {ok, {Task, Delay, Max, SendTo}, Delay}. + +%%% OTP Callbacks +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Msg, _From, State) -> + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(timeout, {Task, Delay, Max, SendTo}) -> + SendTo ! {self(), Task}, + if Max =:= infinity -> + {noreply, {Task, Delay, Max, SendTo}, Delay}; + Max =< 1 -> + {stop, normal, {Task, Delay, 0, SendTo}}; + Max > 1 -> + {noreply, {Task, Delay, Max-1, SendTo}, Delay} + end; +handle_info(_Msg, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> ok. diff --git a/learn-you-some-erlang/pool/ppool_serv.erl b/learn-you-some-erlang/pool/ppool_serv.erl new file mode 100644 index 0000000..512b49f --- /dev/null +++ b/learn-you-some-erlang/pool/ppool_serv.erl @@ -0,0 +1,113 @@ +-module(ppool_serv). +-behaviour(gen_server). +-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +%% The friendly supervisor is started dynamically! +-define(SPEC(MFA), + {worker_sup, + {ppool_worker_sup, start_link, [MFA]}, + temporary, + 10000, + supervisor, + [ppool_worker_sup]}). + +-record(state, {limit=0, + sup, + refs, + queue=queue:new()}). + +start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) -> + gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []). + +start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) -> + gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []). + +run(Name, Args) -> + gen_server:call(Name, {run, Args}). + +sync_queue(Name, Args) -> + gen_server:call(Name, {sync, Args}, infinity). + +async_queue(Name, Args) -> + gen_server:cast(Name, {async, Args}). + +stop(Name) -> + gen_server:call(Name, stop). + +%% Gen server +init({Limit, MFA, Sup}) -> + %% We need to find the Pid of the worker supervisor from here, + %% but alas, this would be calling the supervisor while it waits for us! + self() ! {start_worker_supervisor, Sup, MFA}, + {ok, #state{limit=Limit, refs=gb_sets:empty()}}. + +handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_call({run, _Args}, _From, S=#state{limit=N}) when N =< 0 -> + {reply, noalloc, S}; + +handle_call({sync, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_call({sync, Args}, From, S = #state{queue=Q}) -> + {noreply, S#state{queue=queue:in({From, Args}, Q)}}; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Msg, _From, State) -> + {noreply, State}. + + +handle_cast({async, Args}, S=#state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {noreply, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_cast({async, Args}, S=#state{limit=N, queue=Q}) when N =< 0 -> + {noreply, S#state{queue=queue:in(Args,Q)}}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{refs=Refs}) -> + io:format("received down msg~n"), + case gb_sets:is_element(Ref, Refs) of + true -> + handle_down_worker(Ref, S); + false -> %% Not our responsibility + {noreply, S} + end; +handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) -> + {ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)), + link(Pid), + {noreply, S#state{sup=Pid}}; +handle_info(Msg, State) -> + io:format("Unknown msg: ~p~n", [Msg]), + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. + +handle_down_worker(Ref, S = #state{limit=L, sup=Sup, refs=Refs}) -> + case queue:out(S#state.queue) of + {{value, {From, Args}}, Q} -> + {ok, Pid} = supervisor:start_child(Sup, Args), + NewRef = erlang:monitor(process, Pid), + NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)), + gen_server:reply(From, {ok, Pid}), + {noreply, S#state{refs=NewRefs, queue=Q}}; + {{value, Args}, Q} -> + {ok, Pid} = supervisor:start_child(Sup, Args), + NewRef = erlang:monitor(process, Pid), + NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)), + {noreply, S#state{refs=NewRefs, queue=Q}}; + {empty, _} -> + {noreply, S#state{limit=L+1, refs=gb_sets:delete(Ref,Refs)}} + end. diff --git a/learn-you-some-erlang/pool/ppool_sup.erl b/learn-you-some-erlang/pool/ppool_sup.erl new file mode 100644 index 0000000..71ec31d --- /dev/null +++ b/learn-you-some-erlang/pool/ppool_sup.erl @@ -0,0 +1,17 @@ +-module(ppool_sup). +-export([start_link/3, init/1]). +-behaviour(supervisor). + +start_link(Name, Limit, MFA) -> + supervisor:start_link(?MODULE, {Name, Limit, MFA}). + +init({Name, Limit, MFA}) -> + MaxRestart = 1, + MaxTime = 3000, + {ok, {{one_for_all, MaxRestart, MaxTime}, + [{serv, + {ppool_serv, start_link, [Name, Limit, self(), MFA]}, + permanent, + 5000, + worker, + [ppool_serv]}]}}. diff --git a/learn-you-some-erlang/pool/ppool_supersup.erl b/learn-you-some-erlang/pool/ppool_supersup.erl new file mode 100644 index 0000000..5790c5f --- /dev/null +++ b/learn-you-some-erlang/pool/ppool_supersup.erl @@ -0,0 +1,31 @@ +-module(ppool_supersup). +-behaviour(supervisor). +-export([start_link/0, stop/0, start_pool/3, stop_pool/1]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ppool}, ?MODULE, []). + +%% technically, a supervisor can not be killed in an easy way. +%% Let's do it brutally! +stop() -> + case whereis(ppool) of + P when is_pid(P) -> + exit(P, kill); + _ -> ok + end. + +start_pool(Name, Limit, MFA) -> + ChildSpec = {Name, + {ppool_sup, start_link, [Name, Limit, MFA]}, + permanent, 10500, supervisor, [ppool_sup]}, + supervisor:start_child(ppool, ChildSpec). + +stop_pool(Name) -> + supervisor:terminate_child(ppool, Name), + supervisor:delete_child(ppool, Name). + +init([]) -> + MaxRestart = 6, + MaxTime = 3000, + {ok, {{one_for_one, MaxRestart, MaxTime}, []}}. diff --git a/learn-you-some-erlang/pool/ppool_tests.erl b/learn-you-some-erlang/pool/ppool_tests.erl new file mode 100644 index 0000000..7cec6d0 --- /dev/null +++ b/learn-you-some-erlang/pool/ppool_tests.erl @@ -0,0 +1,200 @@ +-module(ppool_tests). +-include_lib("eunit/include/eunit.hrl"). +-export([test_mfa/1, wait_mfa/1]). + +%%% All Test Fixtures +start_test_() -> + {"It should be possible to start a pool server and give it a name", + {setup, + fun find_unique_name/0, + fun(Name) -> + [start_and_test_name(Name)] + end}}. + +mfa_test_() -> + {"A pool process can be allocated which will be ordered " + "to run an MFA call determined at start time, with arguments " + "provided at call time", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_mfa(Name)] + end} + }. + +alloc_test_() -> + {"A pool process can be allocated which will be ordered " + "to run a worker, only if there are enough which " + "haven't been ordered to run yet.", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_alloc(Name), + pool_run_noalloc(Name)] + end} + }. + +realloc_test_() -> + {"When an allocated process dies, " + "A new one can be allocated to replace it.", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_realloc(Name)] + end} + }. + +queue_test_() -> + {"The queue function can be used to run the function as soon as possible. " + "If no space is available, the worker call is added to the queue.", + {foreach, + fun start_ppool/0, + fun kill_ppool/1, + [fun(Name) -> test_async_queue(Name) end, + fun(Name) -> test_sync_queue(Name) end]} + }. + +supervision_test_() -> + {"The ppool will never restart a dead child, but all children (OTP " + "compliant) will be shut down when closing the pool, even if they " + "are trapping exits", + {setup, + fun find_unique_name/0, + fun test_supervision/1}}. + +auth_test_() -> + {"The ppool should only dequeue tasks after receiving a down signal " + "from a worker and nobody else", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun test_auth_dealloc/1}}. + +%%% Setups/teardowns +find_unique_name() -> + ppool:start_link(), + Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))), + ?assertEqual(undefined, whereis(Name)), + Name. + +start_ppool() -> + Name = find_unique_name(), + ppool:start_pool(Name, 2, {ppool_nagger, start_link, []}), + Name. + +kill_ppool(Name) -> + ppool:stop_pool(Name). + +%%% Actual tests +start_and_test_name(Name) -> + ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}), + A = whereis(Name), + ppool:stop_pool(Name), + timer:sleep(100), + B = whereis(Name), + [?_assert(undefined =/= A), + ?_assertEqual(undefined, B)]. + +pool_run_mfa(Name) -> + ppool:run(Name, [i_am_running, 1, 1, self()]), + X = receive + {_Pid, i_am_running} -> ok + after 3000 -> + timeout + end, + ?_assertEqual(ok, X). + +pool_run_alloc(Name) -> + {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]), + X = receive + {Pid, i_am_running} -> ok + after 3000 -> + timeout + end, + [?_assert(is_pid(Pid)), + ?_assertEqual(ok, X)]. + +pool_run_noalloc(Name) -> + %% Init function should have set the limit to 2 + ppool:run(Name, [i_am_running, 300, 1, self()]), + ppool:run(Name, [i_am_running, 300, 1, self()]), + X = ppool:run(Name, [i_am_running, 1, 1, self()]), + ?_assertEqual(noalloc, X). + +pool_run_realloc(Name) -> + %% Init function should have set the limit to 2 + {ok, A} = ppool:run(Name, [i_am_running, 500, 1, self()]), + timer:sleep(100), + {ok, B} = ppool:run(Name, [i_am_running, 500, 1, self()]), + timer:sleep(600), + {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]), + timer:sleep(100), + L = flush(), + [?_assert(is_pid(Pid)), + ?_assertEqual([{A,i_am_running}, {B,i_am_running}, {Pid,i_am_running}], + L)]. + +test_async_queue(Name) -> + %% Still two elements max! + ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]), + noalloc = ppool:run(Name, [i_am_running, 2000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 500, 1, self()]), + timer:sleep(3500), + L = flush(), + ?_assertMatch([{_, i_am_running}, {_, i_am_running}, {_, i_am_running}], L). + +test_sync_queue(Name) -> + %% Hell yase, two max + {ok, Pid} = ppool:sync_queue(Name, [i_am_running, 200, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]), + {ok, Pid2} = ppool:sync_queue(Name, [i_am_running, 100, 1, self()]), + timer:sleep(300), + L = flush(), + [?_assert(is_pid(Pid)), + ?_assert(is_pid(Pid2)), + ?_assertMatch([{_,i_am_running}, {_,i_am_running}, + {_,i_am_running}, {_,i_am_running}], + L)]. + +test_supervision(Name) -> + ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}), + {ok, Pid} = ppool:run(Name, [sup, 10000, 100, self()]), + ppool:stop_pool(Name), + timer:sleep(100), + ?_assertEqual(undefined, process_info(Pid)). + +test_auth_dealloc(Name) -> + %% Hell yase, two max + {ok, _Pid} = ppool:sync_queue(Name, [i_am_running, 500, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 1, 1, self()]), + timer:sleep(600), + Name ! {'DOWN', make_ref(), process, self(), normal}, + Name ! {'DOWN', make_ref(), process, self(), normal}, + Name ! {'DOWN', make_ref(), process, self(), normal}, + timer:sleep(200), + L = flush(), + ?_assertMatch([{_,i_am_running}], L). + + + +flush() -> + receive + X -> [X|flush()] + after 0 -> + [] + end. + +%% Exported Helper functions +test_mfa(Pid) -> + Pid ! i_am_running. + +wait_mfa(Pid) -> + Pid ! i_am_running, + timer:sleep(3000). diff --git a/learn-you-some-erlang/pool/ppool_worker_sup.erl b/learn-you-some-erlang/pool/ppool_worker_sup.erl new file mode 100644 index 0000000..2467c47 --- /dev/null +++ b/learn-you-some-erlang/pool/ppool_worker_sup.erl @@ -0,0 +1,15 @@ +-module(ppool_worker_sup). +-export([start_link/1, init/1]). +-behaviour(supervisor). + +start_link(MFA = {_,_,_}) -> + supervisor:start_link(?MODULE, MFA). + +init({M,F,A}) -> + MaxRestart = 5, + MaxTime = 3600, + {ok, {{simple_one_for_one, MaxRestart, MaxTime}, + [{ppool_worker, + {M,F,A}, + temporary, 5000, worker, [M]}]}}. + diff --git a/learn-you-some-erlang/ppool-1.0/Emakefile b/learn-you-some-erlang/ppool-1.0/Emakefile new file mode 100644 index 0000000..8e1f951 --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/Emakefile @@ -0,0 +1,2 @@ +{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}. +{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}. diff --git a/learn-you-some-erlang/ppool-1.0/ebin/ppool.app b/learn-you-some-erlang/ppool-1.0/ebin/ppool.app new file mode 100644 index 0000000..a791f54 --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/ebin/ppool.app @@ -0,0 +1,6 @@ +{application, ppool, + [{vsn, "1.0.0"}, + {modules, [ppool, ppool_serv, ppool_sup, ppool_supersup, ppool_worker_sup]}, + {registered, [ppool]}, + {mod, {ppool, []}} + ]}. diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool.erl b/learn-you-some-erlang/ppool-1.0/src/ppool.erl new file mode 100644 index 0000000..5723f98 --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/src/ppool.erl @@ -0,0 +1,26 @@ +%%% API module for the pool +-module(ppool). +-behaviour(application). +-export([start/2, stop/1, start_pool/3, + run/2, sync_queue/2, async_queue/2, stop_pool/1]). + +start(normal, _Args) -> + ppool_supersup:start_link(). + +stop(_State) -> + ok. + +start_pool(Name, Limit, {M,F,A}) -> + ppool_supersup:start_pool(Name, Limit, {M,F,A}). + +stop_pool(Name) -> + ppool_supersup:stop_pool(Name). + +run(Name, Args) -> + ppool_serv:run(Name, Args). + +async_queue(Name, Args) -> + ppool_serv:async_queue(Name, Args). + +sync_queue(Name, Args) -> + ppool_serv:sync_queue(Name, Args). diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl new file mode 100644 index 0000000..a16130c --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl @@ -0,0 +1,112 @@ +-module(ppool_serv). +-behaviour(gen_server). +-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +%% The friendly supervisor is started dynamically! +-define(SPEC(MFA), + {worker_sup, + {ppool_worker_sup, start_link, [MFA]}, + temporary, + 10000, + supervisor, + [ppool_worker_sup]}). + +-record(state, {limit=0, + sup, + refs, + queue=queue:new()}). + +start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) -> + gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []). + +start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) -> + gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []). + +run(Name, Args) -> + gen_server:call(Name, {run, Args}). + +sync_queue(Name, Args) -> + gen_server:call(Name, {sync, Args}, infinity). + +async_queue(Name, Args) -> + gen_server:cast(Name, {async, Args}). + +stop(Name) -> + gen_server:call(Name, stop). + +%% Gen server +init({Limit, MFA, Sup}) -> + %% We need to find the Pid of the worker supervisor from here, + %% but alas, this would be calling the supervisor while it waits for us! + self() ! {start_worker_supervisor, Sup, MFA}, + {ok, #state{limit=Limit, refs=gb_sets:empty()}}. + +handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_call({run, _Args}, _From, S=#state{limit=N}) when N =< 0 -> + {reply, noalloc, S}; + +handle_call({sync, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_call({sync, Args}, From, S = #state{queue=Q}) -> + {noreply, S#state{queue=queue:in({From, Args}, Q)}}; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Msg, _From, State) -> + {noreply, State}. + + +handle_cast({async, Args}, S=#state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {noreply, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_cast({async, Args}, S=#state{limit=N, queue=Q}) when N =< 0 -> + {noreply, S#state{queue=queue:in(Args,Q)}}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{refs=Refs}) -> + case gb_sets:is_element(Ref, Refs) of + true -> + handle_down_worker(Ref, S); + false -> %% Not our responsibility + {noreply, S} + end; +handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) -> + {ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)), + link(Pid), + {noreply, S#state{sup=Pid}}; +handle_info(Msg, State) -> + io:format("Unknown msg: ~p~n", [Msg]), + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. + +handle_down_worker(Ref, S = #state{limit=L, sup=Sup, refs=Refs}) -> + case queue:out(S#state.queue) of + {{value, {From, Args}}, Q} -> + {ok, Pid} = supervisor:start_child(Sup, Args), + NewRef = erlang:monitor(process, Pid), + NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)), + gen_server:reply(From, {ok, Pid}), + {noreply, S#state{refs=NewRefs, queue=Q}}; + {{value, Args}, Q} -> + {ok, Pid} = supervisor:start_child(Sup, Args), + NewRef = erlang:monitor(process, Pid), + NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)), + {noreply, S#state{refs=NewRefs, queue=Q}}; + {empty, _} -> + {noreply, S#state{limit=L+1, refs=gb_sets:delete(Ref,Refs)}} + end. diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl new file mode 100644 index 0000000..71ec31d --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl @@ -0,0 +1,17 @@ +-module(ppool_sup). +-export([start_link/3, init/1]). +-behaviour(supervisor). + +start_link(Name, Limit, MFA) -> + supervisor:start_link(?MODULE, {Name, Limit, MFA}). + +init({Name, Limit, MFA}) -> + MaxRestart = 1, + MaxTime = 3000, + {ok, {{one_for_all, MaxRestart, MaxTime}, + [{serv, + {ppool_serv, start_link, [Name, Limit, self(), MFA]}, + permanent, + 5000, + worker, + [ppool_serv]}]}}. diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl new file mode 100644 index 0000000..06fa0af --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl @@ -0,0 +1,22 @@ +-module(ppool_supersup). +-behaviour(supervisor). +-export([start_link/0, start_pool/3, stop_pool/1]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ppool}, ?MODULE, []). + +start_pool(Name, Limit, MFA) -> + ChildSpec = {Name, + {ppool_sup, start_link, [Name, Limit, MFA]}, + permanent, 10500, supervisor, [ppool_sup]}, + supervisor:start_child(ppool, ChildSpec). + +stop_pool(Name) -> + supervisor:terminate_child(ppool, Name), + supervisor:delete_child(ppool, Name). + +init([]) -> + MaxRestart = 6, + MaxTime = 3000, + {ok, {{one_for_one, MaxRestart, MaxTime}, []}}. diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl new file mode 100644 index 0000000..2467c47 --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl @@ -0,0 +1,15 @@ +-module(ppool_worker_sup). +-export([start_link/1, init/1]). +-behaviour(supervisor). + +start_link(MFA = {_,_,_}) -> + supervisor:start_link(?MODULE, MFA). + +init({M,F,A}) -> + MaxRestart = 5, + MaxTime = 3600, + {ok, {{simple_one_for_one, MaxRestart, MaxTime}, + [{ppool_worker, + {M,F,A}, + temporary, 5000, worker, [M]}]}}. + diff --git a/learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl b/learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl new file mode 100644 index 0000000..903f821 --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl @@ -0,0 +1,50 @@ +%% demo module, a nagger for tasks, +%% because the previous one wasn't good enough +%% +%% Can take: +%% - a time delay for which to nag, +%% - an adress to say where the messages should be sent +%% - a message to send in the mailbox telling you what to nag, +%% with an id to be able to call: -> +%% - a command to say the task is done +-module(ppool_nagger). +-behaviour(gen_server). +-export([start_link/4, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, code_change/3, terminate/2]). + +start_link(Task, Delay, Max, SendTo) -> + gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo} , []). + +stop(Pid) -> + gen_server:call(Pid, stop). + +init({Task, Delay, Max, SendTo}) -> + process_flag(trap_exit, true), % for tests & terminate too + {ok, {Task, Delay, Max, SendTo}, Delay}. + +%%% OTP Callbacks +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Msg, _From, State) -> + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(timeout, {Task, Delay, Max, SendTo}) -> + SendTo ! {self(), Task}, + if Max =:= infinity -> + {noreply, {Task, Delay, Max, SendTo}, Delay}; + Max =< 1 -> + {stop, normal, {Task, Delay, 0, SendTo}}; + Max > 1 -> + {noreply, {Task, Delay, Max-1, SendTo}, Delay} + end; +handle_info(_Msg, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> ok. diff --git a/learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl b/learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl new file mode 100644 index 0000000..8f0dfe2 --- /dev/null +++ b/learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl @@ -0,0 +1,200 @@ +-module(ppool_tests). +-include_lib("eunit/include/eunit.hrl"). +-export([test_mfa/1, wait_mfa/1]). + +%%% All Test Fixtures +start_test_() -> + {"It should be possible to start a pool server and give it a name", + {setup, + fun find_unique_name/0, + fun(Name) -> + [start_and_test_name(Name)] + end}}. + +mfa_test_() -> + {"A pool process can be allocated which will be ordered " + "to run an MFA call determined at start time, with arguments " + "provided at call time", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_mfa(Name)] + end} + }. + +alloc_test_() -> + {"A pool process can be allocated which will be ordered " + "to run a worker, only if there are enough which " + "haven't been ordered to run yet.", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_alloc(Name), + pool_run_noalloc(Name)] + end} + }. + +realloc_test_() -> + {"When an allocated process dies, " + "A new one can be allocated to replace it.", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_realloc(Name)] + end} + }. + +queue_test_() -> + {"The queue function can be used to run the function as soon as possible. " + "If no space is available, the worker call is added to the queue.", + {foreach, + fun start_ppool/0, + fun kill_ppool/1, + [fun(Name) -> test_async_queue(Name) end, + fun(Name) -> test_sync_queue(Name) end]} + }. + +supervision_test_() -> + {"The ppool will never restart a dead child, but all children (OTP " + "compliant) will be shut down when closing the pool, even if they " + "are trapping exits", + {setup, + fun find_unique_name/0, + fun test_supervision/1}}. + +auth_test_() -> + {"The ppool should only dequeue tasks after receiving a down signal " + "from a worker and nobody else", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun test_auth_dealloc/1}}. + +%%% Setups/teardowns +find_unique_name() -> + application:start(ppool), + Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))), + ?assertEqual(undefined, whereis(Name)), + Name. + +start_ppool() -> + Name = find_unique_name(), + ppool:start_pool(Name, 2, {ppool_nagger, start_link, []}), + Name. + +kill_ppool(Name) -> + ppool:stop_pool(Name). + +%%% Actual tests +start_and_test_name(Name) -> + ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}), + A = whereis(Name), + ppool:stop_pool(Name), + timer:sleep(100), + B = whereis(Name), + [?_assert(undefined =/= A), + ?_assertEqual(undefined, B)]. + +pool_run_mfa(Name) -> + ppool:run(Name, [i_am_running, 1, 1, self()]), + X = receive + {_Pid, i_am_running} -> ok + after 3000 -> + timeout + end, + ?_assertEqual(ok, X). + +pool_run_alloc(Name) -> + {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]), + X = receive + {Pid, i_am_running} -> ok + after 3000 -> + timeout + end, + [?_assert(is_pid(Pid)), + ?_assertEqual(ok, X)]. + +pool_run_noalloc(Name) -> + %% Init function should have set the limit to 2 + ppool:run(Name, [i_am_running, 300, 1, self()]), + ppool:run(Name, [i_am_running, 300, 1, self()]), + X = ppool:run(Name, [i_am_running, 1, 1, self()]), + ?_assertEqual(noalloc, X). + +pool_run_realloc(Name) -> + %% Init function should have set the limit to 2 + {ok, A} = ppool:run(Name, [i_am_running, 500, 1, self()]), + timer:sleep(100), + {ok, B} = ppool:run(Name, [i_am_running, 500, 1, self()]), + timer:sleep(600), + {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]), + timer:sleep(100), + L = flush(), + [?_assert(is_pid(Pid)), + ?_assertEqual([{A,i_am_running}, {B,i_am_running}, {Pid,i_am_running}], + L)]. + +test_async_queue(Name) -> + %% Still two elements max! + ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]), + noalloc = ppool:run(Name, [i_am_running, 2000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 500, 1, self()]), + timer:sleep(3500), + L = flush(), + ?_assertMatch([{_, i_am_running}, {_, i_am_running}, {_, i_am_running}], L). + +test_sync_queue(Name) -> + %% Hell yase, two max + {ok, Pid} = ppool:sync_queue(Name, [i_am_running, 200, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]), + {ok, Pid2} = ppool:sync_queue(Name, [i_am_running, 100, 1, self()]), + timer:sleep(300), + L = flush(), + [?_assert(is_pid(Pid)), + ?_assert(is_pid(Pid2)), + ?_assertMatch([{_,i_am_running}, {_,i_am_running}, + {_,i_am_running}, {_,i_am_running}], + L)]. + +test_supervision(Name) -> + ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}), + {ok, Pid} = ppool:run(Name, [sup, 10000, 100, self()]), + ppool:stop_pool(Name), + timer:sleep(100), + ?_assertEqual(undefined, process_info(Pid)). + +test_auth_dealloc(Name) -> + %% Hell yase, two max + {ok, _Pid} = ppool:sync_queue(Name, [i_am_running, 500, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 1, 1, self()]), + timer:sleep(600), + Name ! {'DOWN', make_ref(), process, self(), normal}, + Name ! {'DOWN', make_ref(), process, self(), normal}, + Name ! {'DOWN', make_ref(), process, self(), normal}, + timer:sleep(200), + L = flush(), + ?_assertMatch([{_,i_am_running}], L). + + + +flush() -> + receive + X -> [X|flush()] + after 0 -> + [] + end. + +%% Exported Helper functions +test_mfa(Pid) -> + Pid ! i_am_running. + +wait_mfa(Pid) -> + Pid ! i_am_running, + timer:sleep(3000). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app new file mode 100644 index 0000000..abda4b2 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app @@ -0,0 +1,7 @@ +{application, processquest, + [{description, "Game inspired by the Progress Quest game (http://progressquest.com)"}, + {vsn, "1.0.0"}, + {mod, {processquest, []}}, + {registered, [pq_supersup]}, + {modules, [processquest, pq_stats, pq_enemy, pq_events, pq_player]}, + {applications, [stdlib, kernel, regis, crypto]}]}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl new file mode 100644 index 0000000..6f8e9b3 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl @@ -0,0 +1,17 @@ +%% Gives random enemies +-module(pq_enemy). +-export([fetch/0]). + +fetch() -> + L = enemies(), + lists:nth(random:uniform(length(L)), L). + +enemies() -> + [{<<"Ant">>, [{drop, {<<"Ant Egg">>, 1}}, {experience, 1}]}, + {<<"Wildcat">>, [{drop, {<<"Pelt">>, 1}}, {experience, 1}]}, + {<<"Pig">>, [{drop, {<<"Bacon">>, 1}}, {experience, 1}]}, + {<<"Wild Pig">>, [{drop, {<<"Tasty Ribs">>, 2}}, {experience, 1}]}, + {<<"Goblin">>, [{drop, {<<"Goblin hair">>, 1}}, {experience, 2}]}, + {<<"Robot">>, [{drop, {<<"Chunks of Metal">>, 3}}, {experience, 2}]}]. + + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl new file mode 100644 index 0000000..324c432 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl @@ -0,0 +1,49 @@ +%%% Wrapper module for the event manager of ProgressQuest players. +%%% It adds a few functions to wrap the events and sleep on the right +%%% scale on behalf of the pq_player process. +-module(pq_events). +-export([killed/3, location/3, lvl_up/5, buy/4, sell/3]). +-export([start_link/1, stop/1, add_handler/3, delete_handler/3, notify/2]). + +start_link(Name) -> + {ok, Pid} = gen_event:start_link(), + ok = regis:register({events, Name}, Pid), + {ok, Pid}. + +stop(Name) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:stop(ManagerPid). + +add_handler(Name, Handler, Args) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:add_handler(ManagerPid, Handler, Args). + +delete_handler(Name, Handler, Args) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:delete_handler(ManagerPid, Handler, Args). + +notify(Name, Msg) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:notify(ManagerPid, Msg). + +killed(Name, Enemy = {_EnemyName, _Props}, Time) -> + notify(Name, {Name, killed, Time*2, Enemy}), + timer:sleep(Time*2). + +location(Name, Place, Time) -> + notify(Name, {Name, heading, Time, Place}), + timer:sleep(Time). + +lvl_up(Name, NewStats, NewLvl, NewExp, _Time) -> + notify(Name, {Name, lvl_up, 0, NewStats, NewLvl, NewExp}), + ok. + +buy(Name, Slot, Item, Time) -> + T = round(Time/2), + notify(Name, {Name, buy, T, Slot, Item}), + timer:sleep(T). + +sell(Name, Item, Time) -> + T = round(Time/5), + notify(Name, {Name, sell, T, Item}), + timer:sleep(Time). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl new file mode 100644 index 0000000..241e9cd --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl @@ -0,0 +1,77 @@ +%%% Can be used to obtain weapons and pieces of equipment of various types +%%% to be equipped by the hero. The standard format is: +%%% {Name, LevelModifier, Level, Price}. +-module(pq_market). +-export([helmet/2, weapon/2, shield/2, armor/2]). + +weapon(CombinedLvl, Money) -> + L = [ + {<<"plastic knife">>, -1, 1, 2}, + {<<"plastic knife">>, 0, 1, 3}, + {<<"plastic knife">>, 1, 1, 5}, + {<<"metal spoon">>, -1, 4, 3}, + {<<"metal spoon">>, 0, 4, 4}, + {<<"butter knife">>, -1, 6, 5}, + {<<"butter knife">>, 0, 6, 7}, + {<<"butter knife">>, 1, 6, 9}, + {<<"machete">>, -1, 9, 15}, + {<<"machete">>, 0, 9, 20}, + {<<"machete">>, 1, 9, 25}, + {<<"broad sword">>, -1, 12, 23}, + {<<"broad sword">>, 0, 12, 30}, + {<<"broad sword">>, 1, 12, 38}, + {<<"lance">>, -1, 15, 32}, + {<<"lance">>, 0, 15, 44}, + {<<"lance">>, 1, 15, 57}, + {<<"pistol">>, -1, 25, 95}, + {<<"pistol">>, 0, 25, 105}, + {<<"pistol">>, 1, 25, 155}, + {<<"submachine gun">>, -1, 40, 200}, + {<<"submachine gun">>, 0, 40, 245}, + {<<"submachine gun">>, 1, 40, 365} + ], + first_match(fun(W = {_, Modifier, Lvl, Price}) -> + if Modifier+Lvl > CombinedLvl, Price =< Money -> W; + true -> continue + end + end, L). + +helmet(CombinedLvl, Money) -> pick_material(CombinedLvl, Money). +shield(CombinedLvl, Money) -> pick_material(CombinedLvl, Money). +armor(CombinedLvl, Money) -> pick_material(CombinedLvl, Money). + +pick_material(CombinedLvl, Money) -> + L = materials(), + first_match(fun(W = {_, Modifier, Lvl, Price}) -> + if Modifier+Lvl > CombinedLvl, Price =< Money -> W; + true -> continue + end + end, L). + + +first_match(_, []) -> undefined; +first_match(F, [H|T]) -> + case F(H) of + continue -> first_match(F,T); + Val -> Val + end. + +materials() -> + [{<<"wool">>, 0, 1, 25}, + {<<"pleather">>, 0, 2, 45}, + {<<"pleather">>, 1, 2, 50}, + {<<"pleather">>, 2, 2, 65}, + {<<"leather">>, -2, 7, 30}, + {<<"leather">>, -1, 7, 35}, + {<<"leather">>, 0, 7, 45}, + {<<"leather">>, 2, 7, 65}, + {<<"chain mail">>, -2, 12, 70}, + {<<"chain mail">>, 0, 12, 85}, + {<<"chain mail">>, 1, 12, 95}, + {<<"chain mail">>, 2, 12, 105}, + {<<"plate mail">>, -2, 17, 90}, + {<<"plate mail">>, -1, 17, 95}, + {<<"plate mail">>, 0, 17, 105}, + {<<"plate mail">>, 1, 17, 115}, + {<<"plate mail">>, 2, 17, 135}]. + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl new file mode 100644 index 0000000..b304f69 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl @@ -0,0 +1,175 @@ +%%% The core of ProcessQuest -- the player FSM, +%%% acting for each of the players in the game. +%%% +%%% The FSM actually depends on no external events and only sends messages +%%% to itself to prompt state changes. This is somewhat unusual as far as +%%% gen_fsm usages go, but it's pretty useful when needing to work with +%%% a standalone process. +-module(pq_player). +-behaviour(gen_fsm). +-export([start_link/2]). +-export([init/1, market/2, killing/2, handle_event/3, handle_sync_event/4, + handle_info/3, terminate/3, code_change/4]). + +-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, + equip=[], money=0, loot=[], bought=[], time=0}). + + +%%% Possible states & events +%% +% sell buy +% / | | \ +% \ ^ ^ / +% [market]<--, +% | | +% done buying | +% | bag full +% v / +% [killing fields] +% / V V | +% \ / | | +% kill lvl up + +start_link(Name, Opts) -> + gen_fsm:start_link(?MODULE, {Name, Opts}, []). + +init({Name, Opts}) -> + %% Properly seeding stuff. If not doing this, the random module will + %% seed it based on a slightly unique time value. However, when starting + %% many processes at about the same time, the seeds can be very close + %% and give barely random results. The crypto:rand_bytes/1 function + %% allows for much better seeding. + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + %% The first event, to start the FSM + gen_fsm:send_event(self(), kill), + case regis:register(Name, self()) of + {error, _} -> + {stop, name_taken}; + ok -> + %% Use proplists with default values to let the user configure + %% all parts of the FSM's state by using the Opts proplist. + S = #state{ + name=Name, + stats=proplists:get_value(stats, Opts, pq_stats:initial_roll()), + exp=proplists:get_value(exp, Opts, 0), + lvlexp=proplists:get_value(lvlexp, Opts, 1000), + lvl=proplists:get_value(lvl, Opts, 1), + equip=proplists:get_value(equip, Opts, []), + money=proplists:get_value(money, Opts, 0), + loot=proplists:get_value(loot, Opts, []), + bought=proplists:get_value(bought, Opts, []), + time=proplists:get_value(time, Opts, 0) + }, + {ok, market, S} + end. + +%% Done selling. Switch to the event where we head to the killing fields +market(sell, S = #state{loot=[]}) -> + gen_fsm:send_event(self(), buy), + {next_state, market, S}; +%% Selling an Item we have looted to the market, for whatever value it has +market(sell, S = #state{loot=[H={_X,Val}|T], money=M}) -> + pq_events:sell(S#state.name, H, S#state.time), + gen_fsm:send_event(self(), sell), + {next_state, market, S#state{loot=T, money=M+Val}}; +%% When done selling, buy items with your money +market(buy, S = #state{equip=Equip, money=Money, bought=Bought}) -> + %% we have slots of equipment. It's useless to buy the same + %% kind of item time after time, so we must select one we haven't observed yet + case next_slot(Equip, Bought) of + undefined -> + %% when no slot is found, go to the killing field + gen_fsm:send_event(self(), kill), + {next_state, market, S#state{bought=[]}}; + OldItem = {Slot, {_Name, Modifier, Lvl, _Price}} -> + %% Replace the item by a slightly better one if possible. + case pq_market:Slot(Modifier+Lvl, Money) of + undefined -> + market(buy, S#state{bought=[Slot|Bought]}); + NewItem = {_, _, _, Price} -> + pq_events:buy(S#state.name, Slot, NewItem, S#state.time), + gen_fsm:send_event(self(), buy), + NewEquip = [{Slot, NewItem} | Equip -- [OldItem]], + {next_state, market, S#state{equip=NewEquip, + money=Money-Price, + bought=[Slot|Bought]}} + end + end; +%% Heading to the killing field. State only useful as a state transition. +market(kill, S) -> + pq_events:location(S#state.name, killing, S#state.time), + gen_fsm:send_event(self(), kill), + {next_state, killing, S}. + +%% Killing an enemy on the killing field. Taking its drop and keeping it +%% in our loot. +killing(kill, S = #state{loot=Loot, stats=Stats, exp=Exp, lvlexp=LvlExp}) -> + MaxSize = proplists:get_value(strength, Stats)*2, + {EnemyName, Props} = pq_enemy:fetch(), + pq_events:killed(S#state.name, {EnemyName, Props}, S#state.time), + Drop = {_N, _V} = proplists:get_value(drop, Props), + KillExp = proplists:get_value(experience, Props), + NewLoot = [Drop|Loot], + if length(NewLoot) =:= MaxSize -> + gen_fsm:send_event(self(), market); + Exp+KillExp >= LvlExp -> + gen_fsm:send_event(self(), lvl_up); + true -> + gen_fsm:send_event(self(), kill) + end, + {next_state, killing, S#state{loot=NewLoot, exp=Exp+KillExp}}; +%% If we just leveled up, the stats get updated before we keep killing. +killing(lvl_up, S = #state{stats=Stats, lvl=Lvl, lvlexp=LvlExp}) -> + NewStats = [{charisma, proplists:get_value(charisma, Stats)+pq_stats:roll()}, + {constitution, proplists:get_value(constitution, Stats)+pq_stats:roll()}, + {dexterity, proplists:get_value(dexterity, Stats)+pq_stats:roll()}, + {intelligence, proplists:get_value(intelligence, Stats)+pq_stats:roll()}, + {strength, proplists:get_value(strength, Stats)+pq_stats:roll()}, + {wisdom, proplists:get_value(wisdom, Stats)+pq_stats:roll()}], + gen_fsm:send_event(self(), kill), + pq_events:lvl_up(S#state.name, NewStats, Lvl+1, LvlExp*2, S#state.time), + {next_state, killing, S#state{stats=NewStats, lvl=Lvl+1, lvlexp=LvlExp*2}}; +%% Heading to the market state transition +killing(market, S) -> + pq_events:location(S#state.name, market, S#state.time), + gen_fsm:send_event(self(), sell), + {next_state, market, S}. + +handle_event(_Event, StateName, State) -> + {next_state, StateName, State}. + +handle_sync_event(_Event, _From, StateName, State) -> + {next_state, StateName, State}. + +handle_info(_Event, StateName, State) -> + {next_state, StateName, State}. + +terminate(_Reason, _StateName, _State) -> + ok. + +code_change(_OldVsn, StateName, State, _Extra) -> + {next_state, StateName, State}. + +%%%%%%%%%%%%%%% +%%% PRIVATE %%% +%%%%%%%%%%%%%%% + +%% Picks a slot based on what has been seen so far, combined with the +%% current weakest item. +next_slot(Equip, Bought) -> + L = expand(Equip), + case lists:sort([{Mod+Lvl, Entry} || Entry = {Slot, {_, Mod, Lvl, _}} <- L, + not lists:member(Slot, Bought)]) of + [] -> undefined; + [{_, Entry}|_] -> Entry + end. + +expand(L) -> + [expand_field(armor, L), + expand_field(helmet, L), + expand_field(shield, L), + expand_field(weapon, L)]. + +expand_field(F, L) -> + {F, proplists:get_value(F, L, {undefined,0,0,0})}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl new file mode 100644 index 0000000..379f5e9 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl @@ -0,0 +1,19 @@ +%%% Rolls dice to generate statistics or level increases for a character +-module(pq_stats). +-export([initial_roll/0, roll/0]). + +%% First roll, when setting the stats up for the first time +initial_roll() -> + [{charisma, roll(3)}, + {constitution, roll(3)}, + {dexterity, roll(3)}, + {intelligence, roll(3)}, + {strength, roll(3)}, + {wisdom, roll(3)}]. + +%% Rolls a single die. Used when leveling up +roll() -> roll(1). + +%% Rolls Num 6-faced dice +roll(Num) -> + lists:sum([random:uniform(6) || _ <- lists:seq(1,Num)]). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl new file mode 100644 index 0000000..de89a9a --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl @@ -0,0 +1,31 @@ +%%% Supervisor for each player. Goes over a pair of a +%%% gen_fsm (pq_player) and gen_event (pq_events). +-module(pq_sup). +-behaviour(supervisor). +-export([start_link/2]). +-export([init/1]). + + +start_link(Name, Info) -> + supervisor:start_link(?MODULE, {Name,Info}). + +%% The name is passed to the events so that +%% it can register itself as {events, Name} into the +%% 'regis' regsitry app. +%% Same for pq_player, which also gets the info. +%% +%% It is important that pq_events is started before +%% pq_player, otherwise we might create race conditions +%% when starting a player and then quickly generating events to +%% an event manager that doesn't exist. +init({Name, Info}) -> + {ok, + {{one_for_all, 2, 3600}, + [{events, + {pq_events, start_link, [Name]}, + permanent, 5000, worker, [dynamic]}, + {player, + {pq_player, start_link, [Name, Info]}, + permanent, 2000, worker, [pq_player]}]}}. + + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl new file mode 100644 index 0000000..577bbed --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl @@ -0,0 +1,28 @@ +%%% pq_supersup is the ProcessQuest top-level supervisor. +%%% It sits over many pq_sup instances, allowing to have +%%% a truckload of different players running at once. +-module(pq_supersup). +-behaviour(supervisor). +-export([start_link/0, start_player/2, stop_player/1]). +-export([init/1]). + +%% We register it so that it's guaranteed to be unique +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% Using a SOFO strategy because we get to have many +%% supervisees of the same type. +init([]) -> + {ok, + {{simple_one_for_one, 1, 60000}, + [{sup, + {pq_sup, start_link, []}, + permanent, infinity, supervisor, [pq_sup]}]}}. + +%% Starts an individual player +start_player(Name, Info) -> + supervisor:start_child(?MODULE, [Name, Info]). + +%% Stops a player. +stop_player(Name) -> + supervisor:terminate_child(?MODULE, regis:whereis(Name)). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl new file mode 100644 index 0000000..469dcb0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl @@ -0,0 +1,39 @@ +%%% ProcessQuest's main wrapping module. +%%% Start ProcessQuest by calling application:start(processquest). +%%% Create a player by calling processquest:start_player(Name, Info). +%%% - Name is any term to identify the player +%%% - Info is additional information to configure the player. Consult +%%% the pq_player module for more info. +%%% +%%% You can subscribe to the player events by calling +%%% processquest:subscribe(Name, Handler, Args). +%%% The handler is a regular event handler. See test/pq_events_handler.erl. +-module(processquest). +-behaviour(application). +-export([start/2, stop/1]). +-export([start_player/2, stop_player/1, subscribe/3]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% APPLICATION CALLBACKS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +start(normal, []) -> + pq_supersup:start_link(). + +stop(_) -> ok. + +%%%%%%%%%%%%%%%%%%%%%% +%%% USER INTERFACE %%% +%%%%%%%%%%%%%%%%%%%%%% + +%% Starts a player +start_player(Name, Info) -> + pq_supersup:start_player(Name, Info). + +%% Stops a player +stop_player(Name) -> + pq_supersup:stop_player(Name). + +%% Subscribe to user events +subscribe(Name, Handler, Args) -> + pq_events:add_handler(Name, Handler, Args). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl new file mode 100644 index 0000000..14742fa --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl @@ -0,0 +1,33 @@ +-module(pq_enemy_tests). +-include_lib("eunit/include/eunit.hrl"). + +is_random_test_() -> + F = fun(Parent, Ref) -> fun() -> + <<A:32,B:32,C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + Entries = [pq_enemy:fetch() || _ <- lists:seq(1,100)], + Parent ! {Ref, Entries} + end end, + Refs = [begin + Ref = make_ref(), + spawn_link(F(self(), Ref)), + Ref + end || _ <- lists:seq(1,3)], + [A,B,C] = [receive + {Ref, X} -> X + end || Ref <- Refs], + [?_assert(A =/= B), + ?_assert(A =/= C), + ?_assert(B =/= C)]. + +format_test_() -> + [[?_assertMatch({_Name, [{drop, {_DropName, _DropVal}}, + {experience, _Exp}]}, pq_enemy:fetch()) + || _ <- lists:seq(1,10)], + begin + {Name, [{drop, {Drop, Val}}, {experience, Exp}]} = pq_enemy:fetch(), + [?_assert(is_binary(Name)), + ?_assert(is_binary(Drop)), + ?_assert(is_integer(Val)), + ?_assert(is_integer(Exp))] + end]. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl new file mode 100644 index 0000000..a02df4c --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl @@ -0,0 +1,24 @@ +%% A fake event handler used for tests +-module(pq_events_handler). +-behaviour(gen_event). +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2, code_change/3]). + +init(Parent) -> {ok, Parent}. + +handle_event(E, Pid) -> + Pid ! E, + {ok, Pid}. + +handle_call(Req, Pid) -> + Pid ! Req, + {ok, ok, Pid}. + +handle_info(E, Pid) -> + Pid ! E, + {ok, Pid}. + +terminate(_, _) -> ok. + +code_change(_OldVsn, Pid, _Extra) -> + {ok, Pid}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl new file mode 100644 index 0000000..3a327b7 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl @@ -0,0 +1,55 @@ +-module(pq_events_tests). +-include_lib("eunit/include/eunit.hrl"). + +-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}). +-define(setup(T), ?setup(make_ref(), T)). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +events_start_stop_reg_test_() -> + {"The event handler can be reached, started and stopped by using the " + "player's name", + ?setup(can_contact)}. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start(Name) -> + application:start(regis), + {ok, Pid} = pq_events:start_link(Name), + unlink(Pid), + Name. + +stop(Name) -> + pq_events:stop(Name). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% +can_contact(Name) -> + ok = pq_events:add_handler(Name, pq_events_handler, self()), + pq_events:notify(Name, hello), + L1 = flush(), + pq_events:delete_handler(Name, pq_events_handler, []), + pq_events:notify(Name, hello), + L2 = flush(), + [?_assertEqual([hello], L1), + ?_assertEqual([], L2)]. + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +flush() -> + receive + X -> [X | flush1()] + after 300 -> + [] + end. + +flush1() -> + receive + X -> [X | flush1()] + after 0 -> + [] + end. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl new file mode 100644 index 0000000..d689f67 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl @@ -0,0 +1,15 @@ +-module(pq_market_tests). +-include_lib("eunit/include/eunit.hrl"). + +best_smallest_weapon_test_() -> + [?_assertMatch({<<"plastic knife">>, 0, 1, 3}, pq_market:weapon(0, 5)), + ?_assertMatch({<<"plastic knife">>, 1, 1, 5}, pq_market:weapon(1, 100)), + ?_assertMatch(undefined, pq_market:weapon(0,0)), + ?_assertMatch(undefined, pq_market:weapon(50000,100000000000000000000))]. + +best_smallest_gear_test_() -> + [[?_assertMatch({<<"wool">>, 0, 1, 25}, pq_market:F(0, 35)), + ?_assertMatch({<<"pleather">>, 0, 2, 45}, pq_market:F(1, 100)), + ?_assertMatch(undefined, pq_market:F(0,0)), + ?_assertMatch(undefined, pq_market:F(50000,100000000000000000000))] + || F <- [helmet, shield, armor]]. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl new file mode 100644 index 0000000..bbdbf3d --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl @@ -0,0 +1,303 @@ +-module(pq_player_tests). +-include_lib("eunit/include/eunit.hrl"). +-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, % copied from pq_player.erl + equip=[], money=0, loot=[], bought=[], time=0}). + +-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}). +-define(setup(T), ?setup(make_ref(), T)). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +new_player_test_() -> + [{"A player holds its own name in its state", + ?setup(initial_name)}, + {"A new player has stats randomly rolled", + ?setup(initial_roll)}, + {"A new player becomes a registered process", + ?setup(initial_register)}, + {"A player has a counter for experience and a counter for " + "the next level (which is higher than the current exp)", + ?setup(initial_lvl)}, + {"A player has a basic equipment (empty) when first going", + ?setup(initial_equipment)}, + {"A new player has no money", + ?setup(initial_money)}, + {"A new player has no loot", + ?setup(initial_loot)}, + {"The state of a player can be overriden using the info " + "arguments to the init function", + ?setup(override_init)}]. + +market_test_() -> + [{"A player with N items will sell all of them to the market " + "and end with equivalent money, before switching to the " + "buying state", + ?setup(sell_all)}, + {"A player with nearly infinite money will buy items available " + "for his money, higher than his level", + ?setup(buy_items)}, + {"A player with no money or no items available for the price " + "range leaves for the killing fields.", + ?setup(buy_none)}, + {"Receiving the kill message just forwards to the killing state", + ?setup(to_killing)}]. + +killing_fields_test_() -> + [{"Kill enemies until the loot limit is hit. Loot is 2x Strength", + ?setup(loot_limit)}, + {"Killing enemies raises XP until someone levels up", + ?setup(kill_xp)}, + {"Leveling up boosts stats. The sum is higher by at least as " + "many points as there are fields, but not more than 6 times. " + "Moreover, the rolling is random.", + ?setup(lvl_stats)}, + {"receiving the market message just forwards to the market state", + ?setup(to_market)}]. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start(Name) -> + application:start(crypto), + application:start(regis), + Pid = spawn(fun() -> timer:sleep(infinity) end), + regis:register({events, Name}, Pid), + Name. + +stop(Name) -> + exit(regis:whereis({events, Name}), kill), + application:stop(regis), + application:stop(crypto). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% + +%% Initial State +initial_name(Name) -> + {ok, market, S} = pq_player:init({Name, []}), + M = read_event(), + [?_assertEqual(Name, S#state.name), + ?_assertEqual(M, kill)]. + +initial_roll(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertMatch([{charisma,_}, {constitution, _}, {dexterity, _}, + {intelligence, _}, {strength, _}, {wisdom, _}], + lists:sort(S#state.stats)). + +initial_register(Ref) -> + {ok, _, _} = pq_player:init({Ref, []}), + _ = read_event(), + Pid = regis:whereis(Ref), + Ret = pq_player:init({Ref, []}), + _ = read_event(), + [?_assert(undefined =/= Pid), + ?_assert(is_pid(Pid)), + ?_assertEqual({stop, name_taken}, Ret)]. + +initial_lvl(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + [?_assert(is_integer(S#state.lvlexp)), + ?_assert(S#state.lvlexp > 0), + ?_assert(S#state.exp =:= 0)]. % start at 0 exp + +initial_equipment(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + [?_assert(is_list(S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(armor, S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(helmet, S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(weapon, S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(shield, S#state.equip))]. + +initial_money(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertEqual(0, S#state.money). + +initial_loot(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertEqual([], S#state.loot). + +override_init(Name) -> + {ok, _, Partial} = pq_player:init({Name, [ + {stats, [{charisma,1}, {constitution,1}, {dexterity,1}, + {intelligence,1}, {strength,1}, {wisdom,1}]}, + {lvlexp,1}]}), + regis:unregister(Name), + _ = read_event(), + {ok, _, Complete} = pq_player:init({Name, [ + {stats, [{charisma,1}, {constitution,1}, {dexterity,1}, + {intelligence,1}, {strength,1}, {wisdom,1}]}, + {exp, 1}, {lvlexp,1}, {lvl,9}, + {equip, [{weapon,{<<"plastic knife">>, -1, 1, 2}}]}, + {money,1}, {loot, [{<<"Bacon">>, 1}]}, {bought, [helmet]} + ]}), + _ = read_event(), + [?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}], + lvlexp=1, lvl=1, exp=0}, + Partial), + ?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}], + exp=1, lvlexp=1, lvl=9, equip=[{weapon,_}], + money=1, loot=[{_,_}], bought=[_]}, + Complete)]. + +%% Market +sell_all(Name) -> + undefined = read_event(), + Loot = [proplists:get_value(drop, element(2, pq_enemy:fetch())) + || _ <- lists:seq(1,5)], + {[Sum1, Sum2, Sum3, Sum4, Sum5], _} = lists:mapfoldl( + fun(X, Sum) -> {X+Sum, X+Sum} end, + 0, + [Val || {_, Val} <- Loot] + ), + %undefined = read_event(), + S0 = #state{name=Name, loot=Loot, money=0, lvl=1}, + {next_state, market, S1} = pq_player:market(sell, S0), + M1 = read_event(), + {next_state, market, S2} = pq_player:market(sell, S1), + M2 = read_event(), + {next_state, market, S3} = pq_player:market(sell, S2), + M3 = read_event(), + {next_state, market, S4} = pq_player:market(sell, S3), + M4 = read_event(), + {next_state, market, S5} = pq_player:market(sell, S4), + M5 = read_event(), + {next_state, market, S6} = pq_player:market(sell, S5), + M6 = read_event(), + [?_assertMatch(#state{money=Sum1, loot=[_,_,_,_]}, S1), + ?_assertMatch(#state{money=Sum2, loot=[_,_,_]}, S2), + ?_assertMatch(#state{money=Sum3, loot=[_,_]}, S3), + ?_assertMatch(#state{money=Sum4, loot=[_]}, S4), + ?_assertMatch(#state{money=Sum5, loot=[]}, S5), + ?_assertMatch(#state{money=Sum5, loot=[]}, S6), + ?_assertEqual([sell, sell, sell, sell, sell, buy], + [M1,M2,M3,M4,M5,M6])]. + +buy_items(Name) -> + %% 4 different pieces of equipment to buy + S0 = #state{name=Name, equip=[], money=999999999999}, + {next_state, market, S1} = pq_player:market(buy, S0), + M1 = read_event(), + {next_state, market, S2} = pq_player:market(buy, S1), + M2 = read_event(), + {next_state, market, S3} = pq_player:market(buy, S2), + M3 = read_event(), + {next_state, market, S4} = pq_player:market(buy, S3), + M4 = read_event(), + %% All slots bought. Implicit requirement: not buying for the + %% same slot twice. + {next_state, market, S5} = pq_player:market(buy, S4), + M5 = read_event(), + [?_assertEqual([S5#state.money, S4#state.money, S3#state.money, + S2#state.money, S1#state.money, S0#state.money], + lists:sort([S5#state.money, S4#state.money, S3#state.money, + S2#state.money, S1#state.money, S0#state.money])), + ?_assertEqual([1,2,3,4,4], + [length(L) || L <- [S1#state.equip, S2#state.equip, + S3#state.equip, S4#state.equip, + S5#state.equip]]), + ?_assertEqual([buy, buy, buy, buy, kill], + [M1, M2, M3, M4, M5])]. + +buy_none(Name) -> + S0 = #state{name=Name, equip=[], money=0}, + %% one try per part of the equipment + {next_state, market, S1} = pq_player:market(buy, S0), + _ = read_event(), + {next_state, market, S2} = pq_player:market(buy, S1), + _ = read_event(), + {next_state, market, S3} = pq_player:market(buy, S2), + _ = read_event(), + {next_state, market, S4} = pq_player:market(buy, S3), + M = read_event(), + [?_assertEqual(S0, S4), + ?_assertEqual(kill, M)]. + +to_killing(Name) -> + S = #state{name=Name}, + Res = pq_player:market(kill, S), + M = read_event(), + [?_assertMatch({next_state, killing, S}, Res), + ?_assertEqual(kill, M)]. + +%% Killing fields tests +loot_limit(Name) -> + S0 = #state{name=Name, stats=[{strength, 2}], loot=[]}, + {next_state, killing, S1 = #state{loot=L1}} = pq_player:killing(kill, S0), + M1 = read_event(), + {next_state, killing, S2 = #state{loot=L2}} = pq_player:killing(kill, S1), + M2 = read_event(), + {next_state, killing, S3 = #state{loot=L3}} = pq_player:killing(kill, S2), + M3 = read_event(), + {next_state, killing, #state{loot=L4}} = pq_player:killing(kill, S3), + M4 = read_event(), + %% Group identical drops with a counter? + [?_assertEqual([1,2,3,4], [length(L) || L <- [L1, L2, L3, L4]]), + ?_assertEqual([kill, kill, kill, market], [M1, M2, M3, M4])]. + +kill_xp(Name) -> + S0 = #state{name=Name, stats=[{strength, 999}|pq_stats:initial_roll()], + lvl=1, exp=0, lvlexp=5}, + %% between 1 and 5 kills required to lvl up. + {next_state, NS1, S1} = pq_player:killing(kill, S0), + M1 = read_event(), + {next_state, NS2, S2} = pq_player:NS1(M1, S1), + M2 = read_event(), + {next_state, NS3, S3} = pq_player:NS2(M2, S2), + M3 = read_event(), + {next_state, NS4, S4} = pq_player:NS3(M3, S3), + M4 = read_event(), + {next_state, NS5, S5} = pq_player:NS4(M4, S4), + M5 = read_event(), + {next_state, NS6, S6} = pq_player:NS5(M5, S5), + M6 = read_event(), + [?_assert(lists:any(fun(#state{lvl=L}) -> L > 1 end, [S1,S2,S3,S4,S5,S6])), + ?_assert(lists:any(fun(#state{lvlexp=L}) -> L >= 10 end, [S1,S2,S3,S4,S5,S6])), + ?_assert(lists:any(fun(#state{exp=E}) -> E >= 5 end, [S1,S2,S3,S4,S5,S6])), + ?_assert(lists:any(fun(FSMState) -> FSMState =:= killing end, + [NS1, NS2, NS3, NS4, NS5, NS6])), + ?_assert(lists:any(fun(Msg) -> Msg =:= kill end, + [M1, M2, M3, M4, M5, M6])), + ?_assert(lists:any(fun(Msg) -> Msg =:= lvl_up end, + [M1, M2, M3, M4, M5, M6]))]. + +lvl_stats(Name) -> + {ok, _, S0} = pq_player:init({Name, []}), + _ = read_event(), + TotalStats = length(S0#state.stats), + {next_state, killing, S1} = pq_player:killing(lvl_up, S0), + _ = read_event(), + {next_state, killing, S2} = pq_player:killing(lvl_up, S0), + _ = read_event(), + SumInit = lists:sum([Pts || {_,Pts} <- S0#state.stats]), + SumS1 = lists:sum([Pts || {_,Pts} <- S1#state.stats]), + SumS2 = lists:sum([Pts || {_,Pts} <- S2#state.stats]), + [?_assert(SumS1 >= TotalStats+SumInit), + ?_assert(SumS2 >= TotalStats+SumInit), + ?_assert(SumS1 =< TotalStats*6 + SumInit), + ?_assert(SumS2 =< TotalStats*6 + SumInit), + ?_assert(S1#state.stats =/= S2#state.stats)]. + +to_market(Name) -> + S = #state{name=Name}, + Res = pq_player:killing(market, S), + M = read_event(), + [?_assertMatch({next_state, market, S}, Res), + ?_assertEqual(sell, M)]. + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +read_event() -> + receive + {'$gen_event', Msg} -> Msg + after 0 -> + undefined + end. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl new file mode 100644 index 0000000..da9daa5 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl @@ -0,0 +1,28 @@ +-module(pq_stats_tests). +-include_lib("eunit/include/eunit.hrl"). + +all_stats_test_() -> + Stats = pq_stats:initial_roll(), + {"Checks whether all stats are returned", + [?_assertEqual([charisma, constitution, dexterity, + intelligence, strength, wisdom], + lists:sort(proplists:get_keys(Stats)))]}. + +initial_roll_test_() -> + Rolls = [pq_stats:initial_roll() || _ <- lists:seq(1,100)], + {"All die rolls are made out of 3 d6 dice", + %% 6 == number of stats + [?_assertEqual(6, length([S || {_,S} <- Stats, S >= 3, S =< 18])) + || Stats <- Rolls]}. + +initial_random_roll_test_() -> + Stats = [pq_stats:initial_roll() || _ <- lists:seq(1,100)], + {"All die rolls are random", + ?_assertEqual(lists:sort(Stats), + lists:sort(sets:to_list(sets:from_list(Stats))))}. + +single_die_roll_test_() -> + Rolls = [pq_stats:roll() || _ <- lists:seq(1,100)], + [?_assertEqual(100, length([N || N <- Rolls, N >= 1, N =< 6])), + ?_assert(1 =/= length(sets:to_list(sets:from_list(Rolls))))]. + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl new file mode 100644 index 0000000..fa118b1 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl @@ -0,0 +1,49 @@ +-module(processquest_tests). +-include_lib("eunit/include/eunit.hrl"). + +%%% Integration tests verifying the whole app. +-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}). +-define(setup(T), ?setup(make_ref(), T)). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +integration_test_() -> + [{"A player can be started from the processquest module and monitored", + ?setup(subscribe)}]. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start(Name) -> + application:start(crypto), + application:start(regis), + application:start(processquest), + processquest:start_player(Name, [{time,1100}]), + Name. + +stop(Name) -> + processquest:stop_player(Name), + application:stop(processquest), + application:stop(regis). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% +subscribe(Name) -> + ok = processquest:subscribe(Name, pq_events_handler, self()), + timer:sleep(4000), + Msgs = flush(), + [?_assertMatch([{Name, killed, _Time1, {_EnemyName1, _Props1}}, + {Name, killed, _Time2, {_EnemyName2, _Props2}}, + {Name, killed, _Time3, {_EnemyName3, _Props3}}], + Msgs)]. + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +flush() -> + receive + X -> [X | flush()] + after 0 -> [] + end. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app new file mode 100644 index 0000000..d923479 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app @@ -0,0 +1,8 @@ +{application, processquest, + [{description, "Game inspired by the Progress Quest game (http://progressquest.com)"}, + {vsn, "1.1.0"}, + {mod, {processquest, []}}, + {registered, [pq_supersup]}, + {modules, [processquest, pq_stats, pq_enemy, pq_quest, pq_events, + pq_player]}, + {applications, [stdlib, kernel, regis, crypto]}]}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup new file mode 100644 index 0000000..41d305c --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup @@ -0,0 +1,9 @@ +{"1.1.0", + [{"1.0.0", [{add_module, pq_quest}, + {load_module, pq_enemy}, + {load_module, pq_events}, + {update, pq_player, {advanced, []}, [pq_quest, pq_events]}]}], + [{"1.0.0", [{update, pq_player, {advanced, []}}, + {delete_module, pq_quest}, + {load_module, pq_enemy}, + {load_module, pq_events}]}]}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl new file mode 100644 index 0000000..e1c2332 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl @@ -0,0 +1,32 @@ +%% Gives random enemies +-module(pq_enemy). +-export([fetch/0]). + +fetch() -> + L = enemies(), + lists:nth(random:uniform(length(L)), L). + +enemies() -> + [{<<"Spider">>, [{drop, {<<"Spider Egg">>, 1}}, {experience, 1}]}, + {<<"Wildcat">>, [{drop, {<<"Pelt">>, 1}}, {experience, 1}]}, + {<<"Pig">>, [{drop, {<<"Bacon">>, 1}}, {experience, 1}]}, + {<<"Wild Pig">>, [{drop, {<<"Tasty Ribs">>, 2}}, {experience, 1}]}, + {<<"Goblin">>, [{drop, {<<"Goblin hair">>, 1}}, {experience, 2}]}, + {<<"Robot">>, [{drop, {<<"Chunks of Metal">>, 3}}, {experience, 2}]}, + {<<"Factory Worker">>, [{drop, {<<"Wrench">>,2}}, {experience,1}]}, + {<<"Carnie">>, [{drop, {<<"Cotton Candy">>,1}}, {experience,1}]}, + {<<"Mad Beaver">>, [{drop, {<<"Wood chips">>, 2}}, {experience, 1}]}, + {<<"Silent magpie">>, [{drop, {<<"Shiny things">>, 3}}, {experience, 1}]}, + {<<"Great Lizard">>, [{drop, {<<"Lizard tail">>, 1}}, {experience, 1}]}, + {<<"Cheetah">>, [{drop, {<<"Fur">>, 3}}, {experience, 4}]}, + {<<"Radish Horse">>, [{drop, {<<"Horseradish">>,1}}, {experience, 2}]}, + {<<"Sand Worm">>, [{drop, {<<"Spices">>,10}}, {experience, 25}]}, + {<<"Mule">>, [{drop, {<<"Map">>, 2}}, {experience, 12}]}, + {<<"Man Tree">>, [{drop, {<<"branch">>,1}}, {experience, 2}]}, + {<<"Penguin Lord">>, [{drop, {<<"Penguin Egg">>,1}}, {experience, 3}]}, + {<<"Cursed Priest">>, [{drop, {<<"Grail">>, 3}}, {experience, 5}]}, + {<<"Bearded cow">>, [{drop, {<<"Hairy milk">>, 1}}, {experience, 6}]}, + {<<"Hellish crow">>, [{drop, {<<"Black feather">>, 1}}, {experience, 1}]}, + {<<"Wolverine">>, [{drop, {<<"Puddle of blood">>, 1}}, {experience, 2}]}, + {<<"Gangsta Bear">>, [{drop, {<<"Bear Grylls">>, 3}}, {experience, 4}]}]. + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl new file mode 100644 index 0000000..faea13e --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl @@ -0,0 +1,53 @@ +%%% Wrapper module for the event manager of ProgressQuest players. +%%% It adds a few functions to wrap the events and sleep on the right +%%% scale on behalf of the pq_player process. +-module(pq_events). +-export([killed/3, location/3, lvl_up/5, buy/4, sell/3, quest/4]). +-export([start_link/1, stop/1, add_handler/3, delete_handler/3, notify/2]). + +start_link(Name) -> + {ok, Pid} = gen_event:start_link(), + ok = regis:register({events, Name}, Pid), + {ok, Pid}. + +stop(Name) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:stop(ManagerPid). + +add_handler(Name, Handler, Args) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:add_handler(ManagerPid, Handler, Args). + +delete_handler(Name, Handler, Args) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:delete_handler(ManagerPid, Handler, Args). + +notify(Name, Msg) -> + ManagerPid = regis:whereis({events, Name}), + gen_event:notify(ManagerPid, Msg). + +killed(Name, Enemy = {_EnemyName, _Props}, Time) -> + notify(Name, {Name, killed, Time*2, Enemy}), + timer:sleep(Time*2). + +location(Name, Place, Time) -> + notify(Name, {Name, heading, Time, Place}), + timer:sleep(Time). + +lvl_up(Name, NewStats, NewLvl, NewExp, _Time) -> + notify(Name, {Name, lvl_up, 0, NewStats, NewLvl, NewExp}), + ok. + +buy(Name, Slot, Item, Time) -> + T = round(Time/2), + notify(Name, {Name, buy, T, Slot, Item}), + timer:sleep(T). + +sell(Name, Item, Time) -> + T = round(Time/5), + notify(Name, {Name, sell, T, Item}), + timer:sleep(Time). + +quest(Name, {Old, _}, {New, _}, _Time) -> + notify(Name, {Name, quest, 0, Old, New}), + ok. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl new file mode 100644 index 0000000..241e9cd --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl @@ -0,0 +1,77 @@ +%%% Can be used to obtain weapons and pieces of equipment of various types +%%% to be equipped by the hero. The standard format is: +%%% {Name, LevelModifier, Level, Price}. +-module(pq_market). +-export([helmet/2, weapon/2, shield/2, armor/2]). + +weapon(CombinedLvl, Money) -> + L = [ + {<<"plastic knife">>, -1, 1, 2}, + {<<"plastic knife">>, 0, 1, 3}, + {<<"plastic knife">>, 1, 1, 5}, + {<<"metal spoon">>, -1, 4, 3}, + {<<"metal spoon">>, 0, 4, 4}, + {<<"butter knife">>, -1, 6, 5}, + {<<"butter knife">>, 0, 6, 7}, + {<<"butter knife">>, 1, 6, 9}, + {<<"machete">>, -1, 9, 15}, + {<<"machete">>, 0, 9, 20}, + {<<"machete">>, 1, 9, 25}, + {<<"broad sword">>, -1, 12, 23}, + {<<"broad sword">>, 0, 12, 30}, + {<<"broad sword">>, 1, 12, 38}, + {<<"lance">>, -1, 15, 32}, + {<<"lance">>, 0, 15, 44}, + {<<"lance">>, 1, 15, 57}, + {<<"pistol">>, -1, 25, 95}, + {<<"pistol">>, 0, 25, 105}, + {<<"pistol">>, 1, 25, 155}, + {<<"submachine gun">>, -1, 40, 200}, + {<<"submachine gun">>, 0, 40, 245}, + {<<"submachine gun">>, 1, 40, 365} + ], + first_match(fun(W = {_, Modifier, Lvl, Price}) -> + if Modifier+Lvl > CombinedLvl, Price =< Money -> W; + true -> continue + end + end, L). + +helmet(CombinedLvl, Money) -> pick_material(CombinedLvl, Money). +shield(CombinedLvl, Money) -> pick_material(CombinedLvl, Money). +armor(CombinedLvl, Money) -> pick_material(CombinedLvl, Money). + +pick_material(CombinedLvl, Money) -> + L = materials(), + first_match(fun(W = {_, Modifier, Lvl, Price}) -> + if Modifier+Lvl > CombinedLvl, Price =< Money -> W; + true -> continue + end + end, L). + + +first_match(_, []) -> undefined; +first_match(F, [H|T]) -> + case F(H) of + continue -> first_match(F,T); + Val -> Val + end. + +materials() -> + [{<<"wool">>, 0, 1, 25}, + {<<"pleather">>, 0, 2, 45}, + {<<"pleather">>, 1, 2, 50}, + {<<"pleather">>, 2, 2, 65}, + {<<"leather">>, -2, 7, 30}, + {<<"leather">>, -1, 7, 35}, + {<<"leather">>, 0, 7, 45}, + {<<"leather">>, 2, 7, 65}, + {<<"chain mail">>, -2, 12, 70}, + {<<"chain mail">>, 0, 12, 85}, + {<<"chain mail">>, 1, 12, 95}, + {<<"chain mail">>, 2, 12, 105}, + {<<"plate mail">>, -2, 17, 90}, + {<<"plate mail">>, -1, 17, 95}, + {<<"plate mail">>, 0, 17, 105}, + {<<"plate mail">>, 1, 17, 115}, + {<<"plate mail">>, 2, 17, 135}]. + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl new file mode 100644 index 0000000..9cca7e0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl @@ -0,0 +1,216 @@ +%%% The core of ProcessQuest -- the player FSM, +%%% acting for each of the players in the game. +%%% +%%% The FSM actually depends on no external events and only sends messages +%%% to itself to prompt state changes. This is somewhat unusual as far as +%%% gen_fsm usages go, but it's pretty useful when needing to work with +%%% a standalone process. +-module(pq_player). +-behaviour(gen_fsm). +-export([start_link/2]). +-export([init/1, market/2, killing/2, handle_event/3, handle_sync_event/4, + handle_info/3, terminate/3, code_change/4]). + +-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, + equip=[], money=0, loot=[], bought=[], + time=0, quest}). + + +%%% Possible states & events +%% +% sell buy +% / | | \ +% \ ^ ^ / +% [market]<--, +% | | +% done buying | +% | bag full +% v / +% [killing fields] +% / V V | +% \ / | | +% kill lvl up + +start_link(Name, Opts) -> + gen_fsm:start_link(?MODULE, {Name, Opts}, []). + +init({Name, Opts}) -> + %% Properly seeding stuff. If not doing this, the random module will + %% seed it based on a slightly unique time value. However, when starting + %% many processes at about the same time, the seeds can be very close + %% and give barely random results. The crypto:rand_bytes/1 function + %% allows for much better seeding. + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + %% The first event, to start the FSM + gen_fsm:send_event(self(), kill), + case regis:register(Name, self()) of + {error, _} -> + {stop, name_taken}; + ok -> + %% Use proplists with default values to let the user configure + %% all parts of the FSM's state by using the Opts proplist. + S = #state{ + name=Name, + stats=proplists:get_value(stats, Opts, pq_stats:initial_roll()), + exp=proplists:get_value(exp, Opts, 0), + lvlexp=proplists:get_value(lvlexp, Opts, 1000), + lvl=proplists:get_value(lvl, Opts, 1), + equip=proplists:get_value(equip, Opts, []), + money=proplists:get_value(money, Opts, 0), + loot=proplists:get_value(loot, Opts, []), + bought=proplists:get_value(bought, Opts, []), + time=proplists:get_value(time, Opts, 0), + quest=proplists:get_value(quest, Opts, pq_quest:fetch()) + }, + {ok, market, S} + end. + +%% Done selling. Switch to the event where we head to the killing fields +market(sell, S = #state{loot=[]}) -> + gen_fsm:send_event(self(), buy), + {next_state, market, S}; +%% Selling an Item we have looted to the market, for whatever value it has +market(sell, S = #state{loot=[{Drop,Val}|T], money=M, lvl=Lvl}) -> + pq_events:sell(S#state.name, {Drop, Val*Lvl}, S#state.time), + gen_fsm:send_event(self(), sell), + {next_state, market, S#state{loot=T, money=M+Val*Lvl}}; +%% When done selling, buy items with your money +market(buy, S = #state{equip=Equip, money=Money, bought=Bought}) -> + %% we have slots of equipment. It's useless to buy the same + %% kind of item time after time, so we must select one we haven't observed yet + case next_slot(Equip, Bought) of + undefined -> + %% when no slot is found, go to the killing field + gen_fsm:send_event(self(), kill), + {next_state, market, S#state{bought=[]}}; + OldItem = {Slot, {_Name, Modifier, Lvl, _Price}} -> + %% Replace the item by a slightly better one if possible. + case pq_market:Slot(Modifier+Lvl, Money) of + undefined -> + market(buy, S#state{bought=[Slot|Bought]}); + NewItem = {_, _, _, Price} -> + pq_events:buy(S#state.name, Slot, NewItem, S#state.time), + gen_fsm:send_event(self(), buy), + NewEquip = [{Slot, NewItem} | Equip -- [OldItem]], + {next_state, market, S#state{equip=NewEquip, + money=Money-Price, + bought=[Slot|Bought]}} + end + end; +%% Heading to the killing field. State only useful as a state transition. +market(kill, S) -> + pq_events:location(S#state.name, killing, S#state.time), + gen_fsm:send_event(self(), kill), + {next_state, killing, S}. + +%% Killing an enemy on the killing field. Taking its drop and keeping it +%% in our loot. +killing(kill, S = #state{loot=Loot, stats=Stats, exp=Exp, lvlexp=LvlExp, + quest=Quest}) -> + MaxSize = proplists:get_value(strength, Stats)*2, + {EnemyName, Props} = pq_enemy:fetch(), + pq_events:killed(S#state.name, {EnemyName, Props}, S#state.time), + Drop = {_N, _V} = proplists:get_value(drop, Props), + KillExp = proplists:get_value(experience, Props), + NewLoot = [Drop|Loot], + {QuestExp, NewQuest} = case check_quest(Quest) of + UpdatedQuest = {0, _} -> UpdatedQuest; + QuestBeaten = {_, NewQuest0} -> + pq_events:quest(S#state.name, Quest, NewQuest0, S#state.time), + QuestBeaten + end, + if length(NewLoot) =:= MaxSize -> + gen_fsm:send_event(self(), market); + Exp+KillExp+QuestExp >= LvlExp -> + gen_fsm:send_event(self(), lvl_up); + true -> + gen_fsm:send_event(self(), kill) + end, + {next_state, killing, S#state{loot=NewLoot, + exp=Exp+KillExp+QuestExp, + quest=NewQuest}}; +%% If we just leveled up, the stats get updated before we keep killing. +killing(lvl_up, S = #state{stats=Stats, lvl=Lvl, lvlexp=LvlExp}) -> + NewStats = [{charisma, proplists:get_value(charisma, Stats)+pq_stats:roll()}, + {constitution, proplists:get_value(constitution, Stats)+pq_stats:roll()}, + {dexterity, proplists:get_value(dexterity, Stats)+pq_stats:roll()}, + {intelligence, proplists:get_value(intelligence, Stats)+pq_stats:roll()}, + {strength, proplists:get_value(strength, Stats)+pq_stats:roll()}, + {wisdom, proplists:get_value(wisdom, Stats)+pq_stats:roll()}], + gen_fsm:send_event(self(), kill), + pq_events:lvl_up(S#state.name, NewStats, Lvl+1, LvlExp*2, S#state.time), + {next_state, killing, S#state{stats=NewStats, lvl=Lvl+1, lvlexp=LvlExp*2}}; +%% Heading to the market state transition +killing(market, S) -> + pq_events:location(S#state.name, market, S#state.time), + gen_fsm:send_event(self(), sell), + {next_state, market, S}. + +handle_event(_Event, StateName, State) -> + {next_state, StateName, State}. + +handle_sync_event(_Event, _From, StateName, State) -> + {next_state, StateName, State}. + +handle_info(_Event, StateName, State) -> + {next_state, StateName, State}. + +terminate(_Reason, _StateName, _State) -> + ok. + +code_change({down, _}, + StateName, + #state{name=N, stats=S, exp=E, lvlexp=LE, lvl=L, equip=Eq, + money=M, loot=Lo, bought=B, time=T}, + _Extra) -> + Old = {state, N, S, E, LE, L, Eq, M, Lo, B, T}, + {ok, StateName, Old}; +code_change(_OldVsn, + StateName, + {state, Name, Stats, Exp, LvlExp, Lvl, Equip, Money, Loot, + Bought, Time}, + _Extra) -> + State = #state{ + name=Name, stats=Stats, exp=Exp, lvlexp=LvlExp, lvl=Lvl, equip=Equip, + money=Money, loot=Loot, bought=Bought, time=Time, quest=pq_quest:fetch() + }, + {ok, StateName, State}. + +%%%%%%%%%%%%%%% +%%% PRIVATE %%% +%%%%%%%%%%%%%%% + +%% Picks a slot based on what has been seen so far, combined with the +%% current weakest item. +next_slot(Equip, Bought) -> + L = expand(Equip), + case lists:sort([{Mod+Lvl, Entry} || Entry = {Slot, {_, Mod, Lvl, _}} <- L, + not lists:member(Slot, Bought)]) of + [] -> undefined; + [{_, Entry}|_] -> Entry + end. + +expand(L) -> + [expand_field(armor, L), + expand_field(helmet, L), + expand_field(shield, L), + expand_field(weapon, L)]. + +expand_field(F, L) -> + {F, proplists:get_value(F, L, {undefined,0,0,0})}. + +%% Checks quests, if they are ready for the next level or not +check_quest({Name, Props}) -> + case proplists:get_value(kills, Props) of + 1 -> + case pq_quest:fetch() of + %% Same name, we want new stuff! + {Name, _} -> check_quest({Name, Props}); + NewQuest -> + Exp = proplists:get_value(experience, Props), + {Exp, NewQuest} + end; + Q -> + {0, {Name, [{kills,Q-1} | Props--[{kills,Q}]]}} + end. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl new file mode 100644 index 0000000..b8294fb --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl @@ -0,0 +1,15 @@ +-module(pq_quest). +-export([fetch/0]). + +fetch() -> + L = quests(), + lists:nth(random:uniform(length(L)), L). + +quests() -> + [{<<"Fetch me a nut">>, [{experience, 150}, {kills, 20}]}, + {<<"Cancel the festival">>, [{experience, 65}, {kills, 8}]}, + {<<"Summon the dragon">>, [{experience, 1000}, {kills, 100}]}, + {<<"Meet the invisible man">>, [{experience, 200}, {kills, 25}]}, + {<<"Find quest ideas">>, [{experience, 340}, {kills, 32}]}, + {<<"Invent maple syrup">>, [{experience, 1500}, {kills, 175}]}, + {<<"Slay the Bieber">>, [{experience, 500}, {kills, 45}]}]. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl new file mode 100644 index 0000000..379f5e9 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl @@ -0,0 +1,19 @@ +%%% Rolls dice to generate statistics or level increases for a character +-module(pq_stats). +-export([initial_roll/0, roll/0]). + +%% First roll, when setting the stats up for the first time +initial_roll() -> + [{charisma, roll(3)}, + {constitution, roll(3)}, + {dexterity, roll(3)}, + {intelligence, roll(3)}, + {strength, roll(3)}, + {wisdom, roll(3)}]. + +%% Rolls a single die. Used when leveling up +roll() -> roll(1). + +%% Rolls Num 6-faced dice +roll(Num) -> + lists:sum([random:uniform(6) || _ <- lists:seq(1,Num)]). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl new file mode 100644 index 0000000..de89a9a --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl @@ -0,0 +1,31 @@ +%%% Supervisor for each player. Goes over a pair of a +%%% gen_fsm (pq_player) and gen_event (pq_events). +-module(pq_sup). +-behaviour(supervisor). +-export([start_link/2]). +-export([init/1]). + + +start_link(Name, Info) -> + supervisor:start_link(?MODULE, {Name,Info}). + +%% The name is passed to the events so that +%% it can register itself as {events, Name} into the +%% 'regis' regsitry app. +%% Same for pq_player, which also gets the info. +%% +%% It is important that pq_events is started before +%% pq_player, otherwise we might create race conditions +%% when starting a player and then quickly generating events to +%% an event manager that doesn't exist. +init({Name, Info}) -> + {ok, + {{one_for_all, 2, 3600}, + [{events, + {pq_events, start_link, [Name]}, + permanent, 5000, worker, [dynamic]}, + {player, + {pq_player, start_link, [Name, Info]}, + permanent, 2000, worker, [pq_player]}]}}. + + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl new file mode 100644 index 0000000..577bbed --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl @@ -0,0 +1,28 @@ +%%% pq_supersup is the ProcessQuest top-level supervisor. +%%% It sits over many pq_sup instances, allowing to have +%%% a truckload of different players running at once. +-module(pq_supersup). +-behaviour(supervisor). +-export([start_link/0, start_player/2, stop_player/1]). +-export([init/1]). + +%% We register it so that it's guaranteed to be unique +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% Using a SOFO strategy because we get to have many +%% supervisees of the same type. +init([]) -> + {ok, + {{simple_one_for_one, 1, 60000}, + [{sup, + {pq_sup, start_link, []}, + permanent, infinity, supervisor, [pq_sup]}]}}. + +%% Starts an individual player +start_player(Name, Info) -> + supervisor:start_child(?MODULE, [Name, Info]). + +%% Stops a player. +stop_player(Name) -> + supervisor:terminate_child(?MODULE, regis:whereis(Name)). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl new file mode 100644 index 0000000..469dcb0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl @@ -0,0 +1,39 @@ +%%% ProcessQuest's main wrapping module. +%%% Start ProcessQuest by calling application:start(processquest). +%%% Create a player by calling processquest:start_player(Name, Info). +%%% - Name is any term to identify the player +%%% - Info is additional information to configure the player. Consult +%%% the pq_player module for more info. +%%% +%%% You can subscribe to the player events by calling +%%% processquest:subscribe(Name, Handler, Args). +%%% The handler is a regular event handler. See test/pq_events_handler.erl. +-module(processquest). +-behaviour(application). +-export([start/2, stop/1]). +-export([start_player/2, stop_player/1, subscribe/3]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% APPLICATION CALLBACKS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +start(normal, []) -> + pq_supersup:start_link(). + +stop(_) -> ok. + +%%%%%%%%%%%%%%%%%%%%%% +%%% USER INTERFACE %%% +%%%%%%%%%%%%%%%%%%%%%% + +%% Starts a player +start_player(Name, Info) -> + pq_supersup:start_player(Name, Info). + +%% Stops a player +stop_player(Name) -> + pq_supersup:stop_player(Name). + +%% Subscribe to user events +subscribe(Name, Handler, Args) -> + pq_events:add_handler(Name, Handler, Args). diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl new file mode 100644 index 0000000..14742fa --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl @@ -0,0 +1,33 @@ +-module(pq_enemy_tests). +-include_lib("eunit/include/eunit.hrl"). + +is_random_test_() -> + F = fun(Parent, Ref) -> fun() -> + <<A:32,B:32,C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + Entries = [pq_enemy:fetch() || _ <- lists:seq(1,100)], + Parent ! {Ref, Entries} + end end, + Refs = [begin + Ref = make_ref(), + spawn_link(F(self(), Ref)), + Ref + end || _ <- lists:seq(1,3)], + [A,B,C] = [receive + {Ref, X} -> X + end || Ref <- Refs], + [?_assert(A =/= B), + ?_assert(A =/= C), + ?_assert(B =/= C)]. + +format_test_() -> + [[?_assertMatch({_Name, [{drop, {_DropName, _DropVal}}, + {experience, _Exp}]}, pq_enemy:fetch()) + || _ <- lists:seq(1,10)], + begin + {Name, [{drop, {Drop, Val}}, {experience, Exp}]} = pq_enemy:fetch(), + [?_assert(is_binary(Name)), + ?_assert(is_binary(Drop)), + ?_assert(is_integer(Val)), + ?_assert(is_integer(Exp))] + end]. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl new file mode 100644 index 0000000..a02df4c --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl @@ -0,0 +1,24 @@ +%% A fake event handler used for tests +-module(pq_events_handler). +-behaviour(gen_event). +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2, code_change/3]). + +init(Parent) -> {ok, Parent}. + +handle_event(E, Pid) -> + Pid ! E, + {ok, Pid}. + +handle_call(Req, Pid) -> + Pid ! Req, + {ok, ok, Pid}. + +handle_info(E, Pid) -> + Pid ! E, + {ok, Pid}. + +terminate(_, _) -> ok. + +code_change(_OldVsn, Pid, _Extra) -> + {ok, Pid}. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl new file mode 100644 index 0000000..3a327b7 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl @@ -0,0 +1,55 @@ +-module(pq_events_tests). +-include_lib("eunit/include/eunit.hrl"). + +-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}). +-define(setup(T), ?setup(make_ref(), T)). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +events_start_stop_reg_test_() -> + {"The event handler can be reached, started and stopped by using the " + "player's name", + ?setup(can_contact)}. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start(Name) -> + application:start(regis), + {ok, Pid} = pq_events:start_link(Name), + unlink(Pid), + Name. + +stop(Name) -> + pq_events:stop(Name). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% +can_contact(Name) -> + ok = pq_events:add_handler(Name, pq_events_handler, self()), + pq_events:notify(Name, hello), + L1 = flush(), + pq_events:delete_handler(Name, pq_events_handler, []), + pq_events:notify(Name, hello), + L2 = flush(), + [?_assertEqual([hello], L1), + ?_assertEqual([], L2)]. + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +flush() -> + receive + X -> [X | flush1()] + after 300 -> + [] + end. + +flush1() -> + receive + X -> [X | flush1()] + after 0 -> + [] + end. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl new file mode 100644 index 0000000..d689f67 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl @@ -0,0 +1,15 @@ +-module(pq_market_tests). +-include_lib("eunit/include/eunit.hrl"). + +best_smallest_weapon_test_() -> + [?_assertMatch({<<"plastic knife">>, 0, 1, 3}, pq_market:weapon(0, 5)), + ?_assertMatch({<<"plastic knife">>, 1, 1, 5}, pq_market:weapon(1, 100)), + ?_assertMatch(undefined, pq_market:weapon(0,0)), + ?_assertMatch(undefined, pq_market:weapon(50000,100000000000000000000))]. + +best_smallest_gear_test_() -> + [[?_assertMatch({<<"wool">>, 0, 1, 25}, pq_market:F(0, 35)), + ?_assertMatch({<<"pleather">>, 0, 2, 45}, pq_market:F(1, 100)), + ?_assertMatch(undefined, pq_market:F(0,0)), + ?_assertMatch(undefined, pq_market:F(50000,100000000000000000000))] + || F <- [helmet, shield, armor]]. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl new file mode 100644 index 0000000..d0f9c62 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl @@ -0,0 +1,395 @@ +-module(pq_player_tests). +-include_lib("eunit/include/eunit.hrl"). +-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, % copied from pq_player.erl + equip=[], money=0, loot=[], bought=[], + time=0, quest}). + +-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}). +-define(setup(T), ?setup(make_ref(), T)). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +new_player_test_() -> + [{"A player holds its own name in its state", + ?setup(initial_name)}, + {"A new player has stats randomly rolled", + ?setup(initial_roll)}, + {"A new player becomes a registered process", + ?setup(initial_register)}, + {"A player has a counter for experience and a counter for " + "the next level (which is higher than the current exp)", + ?setup(initial_lvl)}, + {"A player has a basic equipment (empty) when first going", + ?setup(initial_equipment)}, + {"A new player has no money", + ?setup(initial_money)}, + {"A new player has no loot", + ?setup(initial_loot)}, + {"A new player has a quest given to him", + ?setup(initial_quest)}, + {"The state of a player can be overriden using the info " + "arguments to the init function", + ?setup(override_init)}]. + +market_test_() -> + [{"A player with N items will sell all of them to the market " + "and end with equivalent money, before switching to the " + "buying state", + ?setup(sell_all)}, + {"A player with nearly infinite money will buy items available " + "for his money, higher than his level", + ?setup(buy_items)}, + {"A player with no money or no items available for the price " + "range leaves for the killing fields.", + ?setup(buy_none)}, + {"Receiving the kill message just forwards to the killing state", + ?setup(to_killing)}]. + +killing_fields_test_() -> + [{"Kill enemies until the loot limit is hit. Loot is 2x Strength", + ?setup(loot_limit)}, + {"Killing enemies raises XP until someone levels up", + ?setup(kill_xp)}, + {"Leveling up boosts stats. The sum is higher by at least as " + "many points as there are fields, but not more than 6 times. " + "Moreover, the rolling is random.", + ?setup(lvl_stats)}, + {"receiving the market message just forwards to the market state", + ?setup(to_market)}]. + +quest_test_() -> + [{"A player who kills enough enemies to complete a quest obtains " + "a new, different one", + ?setup(quest_change)}, + {"A player who kills an enemy sees the quest kill count change", + ?setup(quest_kill_count)}, + {"A player who completes a quest obtains XP in exchange", + ?setup(quest_exp)}]. + +code_change_test_() -> + [{"Updating", update()}, + {"Downgrading", downgrade()}]. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start(Name) -> + application:start(crypto), + application:start(regis), + Pid = spawn(fun() -> timer:sleep(infinity) end), + regis:register({events, Name}, Pid), + Name. + +stop(Name) -> + exit(regis:whereis({events, Name}), kill), + application:stop(regis), + application:stop(crypto). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% + +%% Initial State +initial_name(Name) -> + {ok, market, S} = pq_player:init({Name, []}), + M = read_event(), + [?_assertEqual(Name, S#state.name), + ?_assertEqual(M, kill)]. + +initial_roll(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertMatch([{charisma,_}, {constitution, _}, {dexterity, _}, + {intelligence, _}, {strength, _}, {wisdom, _}], + lists:sort(S#state.stats)). + +initial_register(Ref) -> + {ok, _, _} = pq_player:init({Ref, []}), + _ = read_event(), + Pid = regis:whereis(Ref), + Ret = pq_player:init({Ref, []}), + _ = read_event(), + [?_assert(undefined =/= Pid), + ?_assert(is_pid(Pid)), + ?_assertEqual({stop, name_taken}, Ret)]. + +initial_lvl(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + [?_assert(is_integer(S#state.lvlexp)), + ?_assert(S#state.lvlexp > 0), + ?_assert(S#state.exp =:= 0)]. % start at 0 exp + +initial_equipment(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + [?_assert(is_list(S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(armor, S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(helmet, S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(weapon, S#state.equip)), + ?_assertEqual(undefined, proplists:get_value(shield, S#state.equip))]. + +initial_money(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertEqual(0, S#state.money). + +initial_loot(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertEqual([], S#state.loot). + +initial_quest(Name) -> + {ok, _, S} = pq_player:init({Name, []}), + _ = read_event(), + ?_assertMatch({_QuestName, _Props}, S#state.quest). + +override_init(Name) -> + {ok, _, Partial} = pq_player:init({Name, [ + {stats, [{charisma,1}, {constitution,1}, {dexterity,1}, + {intelligence,1}, {strength,1}, {wisdom,1}]}, + {lvlexp,1}]}), + regis:unregister(Name), + _ = read_event(), + {ok, _, Complete} = pq_player:init({Name, [ + {stats, [{charisma,1}, {constitution,1}, {dexterity,1}, + {intelligence,1}, {strength,1}, {wisdom,1}]}, + {exp, 1}, {lvlexp,1}, {lvl,9}, + {equip, [{weapon,{<<"plastic knife">>, -1, 1, 2}}]}, + {money,1}, {loot, [{<<"Bacon">>, 1}]}, {bought, [helmet]}, + {time, 1}, + {quest, {<<"A">>, [{experience, 1},{kills,1}]}} + ]}), + _ = read_event(), + [?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}], + lvlexp=1, lvl=1, exp=0}, + Partial), + ?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}], + exp=1, lvlexp=1, lvl=9, equip=[{weapon,_}], + money=1, loot=[{_,_}], bought=[_], time=1, + quest={<<"A">>, [_|_]}}, + Complete)]. + +%% Market +sell_all(Name) -> + Loot = [proplists:get_value(drop, element(2, pq_enemy:fetch())) + || _ <- lists:seq(1,5)], + [{_, LV1}, {_, LV2}, {_, LV3}, {_, LV4}, {_, LV5}] = Loot, + LootVals = [LV1, LV2*2, LV3*3, LV4*4, LV5*5], + {[Sum1, Sum2, Sum3, Sum4, Sum5], _} = lists:mapfoldl( + fun(X, Sum) -> {X+Sum, X+Sum} end, + 0, + LootVals + ), + undefined = read_event(), + S0 = #state{name=Name, loot=Loot, money=0}, + {next_state, market, S1} = pq_player:market(sell, S0), + M1 = read_event(), + {next_state, market, S2} = pq_player:market(sell, S1#state{lvl=2}), + M2 = read_event(), + {next_state, market, S3} = pq_player:market(sell, S2#state{lvl=3}), + M3 = read_event(), + {next_state, market, S4} = pq_player:market(sell, S3#state{lvl=4}), + M4 = read_event(), + {next_state, market, S5} = pq_player:market(sell, S4#state{lvl=5}), + M5 = read_event(), + {next_state, market, S6} = pq_player:market(sell, S5#state{lvl=6}), + M6 = read_event(), + [?_assertMatch(#state{money=Sum1, loot=[_,_,_,_]}, S1), + ?_assertMatch(#state{money=Sum2, loot=[_,_,_]}, S2), + ?_assertMatch(#state{money=Sum3, loot=[_,_]}, S3), + ?_assertMatch(#state{money=Sum4, loot=[_]}, S4), + ?_assertMatch(#state{money=Sum5, loot=[]}, S5), + ?_assertMatch(#state{money=Sum5, loot=[]}, S6), + ?_assertEqual([sell, sell, sell, sell, sell, buy], + [M1,M2,M3,M4,M5,M6])]. + +buy_items(Name) -> + %% 4 different pieces of equipment to buy + S0 = #state{name=Name, equip=[], money=999999999999}, + {next_state, market, S1} = pq_player:market(buy, S0), + M1 = read_event(), + {next_state, market, S2} = pq_player:market(buy, S1), + M2 = read_event(), + {next_state, market, S3} = pq_player:market(buy, S2), + M3 = read_event(), + {next_state, market, S4} = pq_player:market(buy, S3), + M4 = read_event(), + %% All slots bought. Implicit requirement: not buying for the + %% same slot twice. + {next_state, market, S5} = pq_player:market(buy, S4), + M5 = read_event(), + [?_assertEqual([S5#state.money, S4#state.money, S3#state.money, + S2#state.money, S1#state.money, S0#state.money], + lists:sort([S5#state.money, S4#state.money, S3#state.money, + S2#state.money, S1#state.money, S0#state.money])), + ?_assertEqual([1,2,3,4,4], + [length(L) || L <- [S1#state.equip, S2#state.equip, + S3#state.equip, S4#state.equip, + S5#state.equip]]), + ?_assertEqual([buy, buy, buy, buy, kill], + [M1, M2, M3, M4, M5])]. + +buy_none(Name) -> + S0 = #state{name=Name, equip=[], money=0}, + %% one try per part of the equipment + {next_state, market, S1} = pq_player:market(buy, S0), + _ = read_event(), + {next_state, market, S2} = pq_player:market(buy, S1), + _ = read_event(), + {next_state, market, S3} = pq_player:market(buy, S2), + _ = read_event(), + {next_state, market, S4} = pq_player:market(buy, S3), + M = read_event(), + [?_assertEqual(S0, S4), + ?_assertEqual(kill, M)]. + +to_killing(Name) -> + S = #state{name=Name}, + Res = pq_player:market(kill, S), + M = read_event(), + [?_assertMatch({next_state, killing, S}, Res), + ?_assertEqual(kill, M)]. + +%% Killing fields tests +loot_limit(Name) -> + S0 = #state{name=Name, stats=[{strength, 2}], loot=[], + quest={<<1>>,[{experience,0},{kills,10000}]}}, + {next_state, killing, S1 = #state{loot=L1}} = pq_player:killing(kill, S0), + M1 = read_event(), + {next_state, killing, S2 = #state{loot=L2}} = pq_player:killing(kill, S1), + M2 = read_event(), + {next_state, killing, S3 = #state{loot=L3}} = pq_player:killing(kill, S2), + M3 = read_event(), + {next_state, killing, #state{loot=L4}} = pq_player:killing(kill, S3), + M4 = read_event(), + %% Group identical drops with a counter? + [?_assertEqual([1,2,3,4], [length(L) || L <- [L1, L2, L3, L4]]), + ?_assertEqual([kill, kill, kill, market], [M1, M2, M3, M4])]. + +kill_xp(Name) -> + S0 = #state{name=Name, stats=[{strength, 999}|pq_stats:initial_roll()], + lvl=1, exp=0, lvlexp=5, + quest={<<1>>,[{experience,0},{kills,10000}]}}, + %% between 1 and 5 kills required to lvl up. + {next_state, NS1, S1} = pq_player:killing(kill, S0), + M1 = read_event(), + {next_state, NS2, S2} = pq_player:NS1(M1, S1), + M2 = read_event(), + {next_state, NS3, S3} = pq_player:NS2(M2, S2), + M3 = read_event(), + {next_state, NS4, S4} = pq_player:NS3(M3, S3), + M4 = read_event(), + {next_state, NS5, S5} = pq_player:NS4(M4, S4), + M5 = read_event(), + {next_state, NS6, S6} = pq_player:NS5(M5, S5), + M6 = read_event(), + [?_assert(lists:any(fun(#state{lvl=L}) -> L > 1 end, [S1,S2,S3,S4,S5,S6])), + ?_assert(lists:any(fun(#state{lvlexp=L}) -> L >= 10 end, [S1,S2,S3,S4,S5,S6])), + ?_assert(lists:any(fun(#state{exp=E}) -> E >= 5 end, [S1,S2,S3,S4,S5,S6])), + ?_assert(lists:any(fun(FSMState) -> FSMState =:= killing end, + [NS1, NS2, NS3, NS4, NS5, NS6])), + ?_assert(lists:any(fun(Msg) -> Msg =:= kill end, + [M1, M2, M3, M4, M5, M6])), + ?_assert(lists:any(fun(Msg) -> Msg =:= lvl_up end, + [M1, M2, M3, M4, M5, M6]))]. + +lvl_stats(Name) -> + {ok, _, S0} = pq_player:init({Name, []}), + _ = read_event(), + TotalStats = length(S0#state.stats), + {next_state, killing, S1} = pq_player:killing(lvl_up, S0), + _ = read_event(), + {next_state, killing, S2} = pq_player:killing(lvl_up, S0), + _ = read_event(), + SumInit = lists:sum([Pts || {_,Pts} <- S0#state.stats]), + SumS1 = lists:sum([Pts || {_,Pts} <- S1#state.stats]), + SumS2 = lists:sum([Pts || {_,Pts} <- S2#state.stats]), + [?_assert(SumS1 >= TotalStats+SumInit), + ?_assert(SumS2 >= TotalStats+SumInit), + ?_assert(SumS1 =< TotalStats*6 + SumInit), + ?_assert(SumS2 =< TotalStats*6 + SumInit), + ?_assert(S1#state.stats =/= S2#state.stats)]. + +to_market(Name) -> + S = #state{name=Name}, + Res = pq_player:killing(market, S), + M = read_event(), + [?_assertMatch({next_state, market, S}, Res), + ?_assertEqual(sell, M)]. + +%% Quests +quest_change(Name) -> + {ok, _, S0=#state{quest={N, Props}}} = pq_player:init({Name, []}), + _ = read_event(), + S = S0#state{quest={N, [{kills,1}|Props]}}, + States = [element(3, pq_player:killing(kill, S)) || _ <- lists:seq(1,10)], + [read_event() || _ <- lists:seq(1,10)], + [?_assert(lists:all(fun(#state{quest={QName, _}}) -> QName =/= N end, + States))]. + +quest_kill_count(Name) -> + {ok, _, S0=#state{quest={N, Props}}} = pq_player:init({Name, []}), + _ = read_event(), + S1 = S0#state{quest={N, [{kills,3}|Props]}}, + {next_state, _, S2} = pq_player:killing(kill, S1), + _ = read_event(), + [?_assertEqual(2, proplists:get_value(kills,element(2,S2#state.quest)))]. + +quest_exp(Name) -> + {ok, _, S0=#state{quest={N, _Props}}} = pq_player:init({Name, [{lvl,0},{exp,0}]}), + _ = read_event(), + S1 = S0#state{quest={N, [{kills,2},{experience, 100000}]}}, + {next_state, _, S2 = #state{exp=E1}} = pq_player:killing(kill, S1), + _ = read_event(), + {next_state, _, #state{exp=E2}} = pq_player:killing(kill, S2), + _ = read_event(), + [?_assert(E1 < 100000), + ?_assert(E2 > 100000)]. + +%% Update/Downgrade + +update() -> + Stats = [{charisma,1}, {constitution,1}, {dexterity,1}, + {intelligence,1}, {strength,1}, {wisdom,1}], + OldState = {state, joe, + Stats, + 1, % exp + 2, % lvlexp + 3, % lvl + [{weapon,{<<"a">>, -1, 1, 2}}], % equip + 4, % money + [{<<"b">>, 1}], % loot + [helmet], % bought + 5 % time + }, + ?_assertMatch({ok, + market, + #state{name=joe, stats=Stats, exp=1, lvlexp=2, + lvl=3, equip=[{weapon,{<<"a">>,-1,1,2}}], + money=4, loot=[{<<"b">>,1}], bought=[helmet], + quest={_, [_|_]}}}, + pq_player:code_change("some version hash", market, OldState, none)). + +downgrade() -> + Stats = [{charisma,1}, {constitution,1}, {dexterity,1}, + {intelligence,1}, {strength,1}, {wisdom,1}], + State = #state{name=joe, stats=Stats, exp=1, lvlexp=2, + lvl=3, equip=[{weapon,{<<"a">>,-1,1,2}}], + money=4, loot=[{<<"b">>,1}], bought=[helmet], + time=5, quest={<<"this goes">>, []}}, + NewState = {state, joe, Stats, 1, 2, 3, [{weapon,{<<"a">>, -1, 1, 2}}], + 4, [{<<"b">>, 1}], [helmet], 5}, + ?_assertMatch({ok, market, NewState}, + pq_player:code_change({down, "old vsn hash"}, market, State, none)). + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +read_event() -> + receive + {'$gen_event', Msg} -> Msg + after 0 -> + undefined + end. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl new file mode 100644 index 0000000..4e4f98c --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl @@ -0,0 +1,30 @@ +-module(pq_quest_tests). +-include_lib("eunit/include/eunit.hrl"). + +format_test_() -> + Quest = pq_quest:fetch(), + [?_assertMatch({_Name, _Props}, Quest), + ?_assert(is_binary(element(1,Quest))), + ?_assert(is_integer(proplists:get_value(kills, element(2,Quest)))), + ?_assert(0 < proplists:get_value(kills, element(2,Quest))), + ?_assert(is_integer(proplists:get_value(experience, element(2,Quest)))), + ?_assert(0 < proplists:get_value(experience, element(2,Quest)))]. + +is_random_test_() -> + F = fun(Parent, Ref) -> fun() -> + <<A:32,B:32,C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + Entries = [pq_quest:fetch() || _ <- lists:seq(1,100)], + Parent ! {Ref, Entries} + end end, + Refs = [begin + Ref = make_ref(), + spawn_link(F(self(), Ref)), + Ref + end || _ <- lists:seq(1,3)], + [A,B,C] = [receive + {Ref, X} -> X + end || Ref <- Refs], + [?_assert(A =/= B), + ?_assert(A =/= C), + ?_assert(B =/= C)]. diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl new file mode 100644 index 0000000..da9daa5 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl @@ -0,0 +1,28 @@ +-module(pq_stats_tests). +-include_lib("eunit/include/eunit.hrl"). + +all_stats_test_() -> + Stats = pq_stats:initial_roll(), + {"Checks whether all stats are returned", + [?_assertEqual([charisma, constitution, dexterity, + intelligence, strength, wisdom], + lists:sort(proplists:get_keys(Stats)))]}. + +initial_roll_test_() -> + Rolls = [pq_stats:initial_roll() || _ <- lists:seq(1,100)], + {"All die rolls are made out of 3 d6 dice", + %% 6 == number of stats + [?_assertEqual(6, length([S || {_,S} <- Stats, S >= 3, S =< 18])) + || Stats <- Rolls]}. + +initial_random_roll_test_() -> + Stats = [pq_stats:initial_roll() || _ <- lists:seq(1,100)], + {"All die rolls are random", + ?_assertEqual(lists:sort(Stats), + lists:sort(sets:to_list(sets:from_list(Stats))))}. + +single_die_roll_test_() -> + Rolls = [pq_stats:roll() || _ <- lists:seq(1,100)], + [?_assertEqual(100, length([N || N <- Rolls, N >= 1, N =< 6])), + ?_assert(1 =/= length(sets:to_list(sets:from_list(Rolls))))]. + diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl new file mode 100644 index 0000000..fa118b1 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl @@ -0,0 +1,49 @@ +-module(processquest_tests). +-include_lib("eunit/include/eunit.hrl"). + +%%% Integration tests verifying the whole app. +-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}). +-define(setup(T), ?setup(make_ref(), T)). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +integration_test_() -> + [{"A player can be started from the processquest module and monitored", + ?setup(subscribe)}]. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start(Name) -> + application:start(crypto), + application:start(regis), + application:start(processquest), + processquest:start_player(Name, [{time,1100}]), + Name. + +stop(Name) -> + processquest:stop_player(Name), + application:stop(processquest), + application:stop(regis). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% +subscribe(Name) -> + ok = processquest:subscribe(Name, pq_events_handler, self()), + timer:sleep(4000), + Msgs = flush(), + [?_assertMatch([{Name, killed, _Time1, {_EnemyName1, _Props1}}, + {Name, killed, _Time2, {_EnemyName2, _Props2}}, + {Name, killed, _Time3, {_EnemyName3, _Props3}}], + Msgs)]. + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +flush() -> + receive + X -> [X | flush()] + after 0 -> [] + end. diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile new file mode 100644 index 0000000..b8be313 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], [{outdir, "ebin"}]}. + diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app b/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app new file mode 100644 index 0000000..169f6d2 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app @@ -0,0 +1,7 @@ +{application, regis, + [{description, "A non-distributed process registry"}, + {vsn, "1.0.0"}, + {mod, {regis, []}}, + {registered, [regis_server]}, + {modules, [regis, regis_sup, regis_server]}, + {applications, [stdlib, kernel]}]}. diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl new file mode 100644 index 0000000..0b37c7c --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl @@ -0,0 +1,29 @@ +%%% Application wrapper module for regis, +%%% a process registration application. +%%% +%%% This was added because the standard process registry has a precise +%%% meaning of representing VM-global, non-dynamic processes. +%%% However, for this, we needed dynamic names and so we had to write +%%% one ourselves. Of course we could have used 'global' (but we +%%% didn't see distributed Erlang yet) or 'gproc' (I don't want to +%%% depend on external libs for this guide), so checkthem out +%%% if you're writing your own app. +-module(regis). +-behaviour(application). +-export([start/2, stop/1]). +-export([register/2, unregister/1, whereis/1, get_names/0]). + + +start(normal, []) -> + regis_sup:start_link(). + +stop(_) -> + ok. + +register(Name, Pid) -> regis_server:register(Name, Pid). + +unregister(Name) -> regis_server:unregister(Name). + +whereis(Name) -> regis_server:whereis(Name). + +get_names() -> regis_server:get_names(). diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl new file mode 100644 index 0000000..a668b02 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl @@ -0,0 +1,98 @@ +%%% The core of the app: the server in charge of tracking processes. +-module(regis_server). +-behaviour(gen_server). + +-export([start_link/0, stop/0, register/2, unregister/1, whereis/1, + get_names/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +%% We have two indexes: one by name and one by pid, for +%% MAXIMUM SPEED (not actually measured). +-record(state, {pid, name}). + +%%%%%%%%%%%%%%%%% +%%% INTERFACE %%% +%%%%%%%%%%%%%%%%% +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:call(?MODULE, stop). + +%% Give a name to a process +register(Name, Pid) when is_pid(Pid) -> + gen_server:call(?MODULE, {register, Name, Pid}). + +%% Remove the name from a process +unregister(Name) -> + gen_server:call(?MODULE, {unregister, Name}). + +%% Find the pid associated with a process +whereis(Name) -> + gen_server:call(?MODULE, {whereis, Name}). + +%% Find all the names currently registered. +get_names() -> + gen_server:call(?MODULE, get_names). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% GEN_SERVER CALLBACKS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +init([]) -> + %% Using gb_trees to store items. gb_trees generally have + %% good overall performance. + {ok, #state{pid = gb_trees:empty(), + name = gb_trees:empty()}}. + +handle_call({register, Name, Pid}, _From, S = #state{pid=P, name=N}) -> + case {gb_trees:is_defined(Pid, P), gb_trees:is_defined(Name, N)} of + {true, _} -> + {reply, {error, already_named}, S}; + {_, true} -> + {reply, {error, name_taken}, S}; + {false, false} -> + Ref = erlang:monitor(process, Pid), + {reply, ok, S#state{pid=gb_trees:insert(Pid, {Name,Ref}, P), + name=gb_trees:insert(Name, {Pid,Ref}, N)}} + end; +handle_call({unregister, Name}, _From, S = #state{pid=P, name=N}) -> + case gb_trees:lookup(Name, N) of + {value, {Pid,Ref}} -> + erlang:demonitor(Ref, [flush]), + {reply, ok, S#state{pid=gb_trees:delete(Pid, P), + name=gb_trees:delete(Name, N)}}; + none -> + {reply, ok, S} + end; +handle_call({whereis, Name}, _From, S = #state{name=N}) -> + case gb_trees:lookup(Name, N) of + {value, {Pid,_}} -> + {reply, Pid, S}; + none -> + {reply, undefined, S} + end; +handle_call(get_names, _From, S = #state{name=N}) -> + {reply, gb_trees:keys(N), S}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Event, _From, State) -> + {noreply, State}. + +handle_cast(_Event, State) -> + {noreply, State}. + +handle_info({'DOWN', Ref, process, Pid, _Reason}, S = #state{pid=P,name=N}) -> + {value, {Name, Ref}} = gb_trees:lookup(Pid, P), + {noreply, S#state{pid = gb_trees:delete(Pid, P), + name = gb_trees:delete(Name, N)}}; +handle_info(_Event, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. + + diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl new file mode 100644 index 0000000..be333e6 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl @@ -0,0 +1,18 @@ +%%% The top-level supervisor of the registration +%%% server. +-module(regis_sup). +-behaviour(supervisor). +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link(?MODULE, []). + +init([]) -> + {ok, {{one_for_one, 1, 3600}, + [{server, + {regis_server, start_link, []}, + permanent, + 500, + worker, + [regis_server]}]}}. diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl new file mode 100644 index 0000000..40e1af0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl @@ -0,0 +1,120 @@ +-module(regis_server_tests). +-include_lib("eunit/include/eunit.hrl"). + +-define(setup(F), {setup, fun start/0, fun stop/1, F}). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TESTS DESCRIPTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% + +start_stop_test_() -> + {"The server can be started, stopped and has a registered name", + ?setup(fun is_registered/1)}. + +register_test_() -> + [{"A process can be registered and contacted", + ?setup(fun register_contact/1)}, + {"A list of registered processes can be obtained", + ?setup(fun registered_list/1)}, + {"An undefined name should return 'undefined' to crash calls", + ?setup(fun noregister/1)}, + {"A process can not have two names", + ?setup(fun two_names_one_pid/1)}, + {"Two processes cannot share the same name", + ?setup(fun two_pids_one_name/1)}]. + +unregister_test_() -> + [{"A process that was registered can be registered again iff it was " + "unregistered between both calls", + ?setup(fun re_un_register/1)}, + {"Unregistering never crashes", + ?setup(fun unregister_nocrash/1)}, + {"A crash unregisters a process", + ?setup(fun crash_unregisters/1)}]. + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% +start() -> + {ok, Pid} = regis_server:start_link(), + Pid. + +stop(_) -> + regis_server:stop(). + +%%%%%%%%%%%%%%%%%%%% +%%% ACTUAL TESTS %%% +%%%%%%%%%%%%%%%%%%%% +is_registered(Pid) -> + [?_assert(erlang:is_process_alive(Pid)), + ?_assertEqual(Pid, whereis(regis_server))]. + +register_contact(_) -> + Pid = proc_lib:spawn_link(fun() -> callback(regcontact) end), + timer:sleep(15), + Ref = make_ref(), + WherePid = regis_server:whereis(regcontact), + regis_server:whereis(regcontact) ! {self(), Ref, hi}, + Rec = receive + {Ref, hi} -> true + after 2000 -> false + end, + [?_assertEqual(Pid, WherePid), + ?_assert(Rec)]. + +noregister(_) -> + [?_assertError(badarg, regis_server:whereis(make_ref()) ! hi), + ?_assertEqual(undefined, regis_server:whereis(make_ref()))]. + +two_names_one_pid(_) -> + ok = regis_server:register(make_ref(), self()), + Res = regis_server:register(make_ref(), self()), + [?_assertEqual({error, already_named}, Res)]. + +two_pids_one_name(_) -> + Pid = proc_lib:spawn(fun() -> callback(myname) end), + timer:sleep(15), + Res = regis_server:register(myname, self()), + exit(Pid, kill), + [?_assertEqual({error, name_taken}, Res)]. + +registered_list(_) -> + L1 = regis_server:get_names(), + Pids = [spawn(fun() -> callback(N) end) || N <- lists:seq(1,15)], + timer:sleep(200), + L2 = regis_server:get_names(), + [exit(Pid, kill) || Pid <- Pids], + [?_assertEqual([], L1), + ?_assertEqual(lists:sort(lists:seq(1,15)), lists:sort(L2))]. + +re_un_register(_) -> + Ref = make_ref(), + L = [regis_server:register(Ref, self()), + regis_server:register(make_ref(), self()), + regis_server:unregister(Ref), + regis_server:register(make_ref(), self())], + [?_assertEqual([ok, {error, already_named}, ok, ok], L)]. + +unregister_nocrash(_) -> + ?_assertEqual(ok, regis_server:unregister(make_ref())). + +crash_unregisters(_) -> + Ref = make_ref(), + Pid = spawn(fun() -> callback(Ref) end), + timer:sleep(150), + Pid = regis_server:whereis(Ref), + exit(Pid, kill), + timer:sleep(95), + regis_server:register(Ref, self()), + S = regis_server:whereis(Ref), + Self = self(), + ?_assertEqual(Self, S). + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% HELPER FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%% +callback(Name) -> + ok = regis_server:register(Name, self()), + receive + {From, Ref, Msg} -> From ! {Ref, Msg} + end. diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl new file mode 100644 index 0000000..61f5f16 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl @@ -0,0 +1,18 @@ +-module(regis_tests). +-include_lib("eunit/include/eunit.hrl"). + +app_test_() -> + {inorder, + [?_assert(try application:start(regis) of + ok -> true; + {error, {already_started, regis}} -> true; + _ -> false + catch + _:_ -> false + end), + ?_assert(try application:stop(regis) of + ok -> true; + _ -> false + catch + _:_ -> false + end)]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app new file mode 100644 index 0000000..dd6d8c7 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app @@ -0,0 +1,11 @@ +{application, sockserv, + [{description, "Socket server to forward ProcessQuest messages to a client"}, + {vsn, "1.0.0"}, + {mod, {sockserv, []}}, + {registered, [sockserv_sup]}, + {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans, + sockserv_pq_events]}, + {applications, [stdlib, kernel, processquest]}, + {env, + [{port, 8082}]} + ]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl new file mode 100644 index 0000000..bd82b6d --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl @@ -0,0 +1,13 @@ +%%% Starting the sockserv application. +%%% The sockserv application is a lightweight +%%% Raw socket server that can be used with telnet +%%% to follow updates on the process quest game. +%%% The port is defined in the app's env +-module(sockserv). +-behaviour(application). +-export([start/2, stop/1]). + +start(normal, []) -> + sockserv_sup:start_link(). + +stop(_) -> ok. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl new file mode 100644 index 0000000..c3c6705 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl @@ -0,0 +1,25 @@ +%%% Converts events from a player's event manager into a +%%% cast sent to the sockserv socket gen_server. +-module(sockserv_pq_events). +-behaviour(gen_event). +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2, code_change/3]). + +init(Parent) -> {ok, Parent}. + +handle_event(E, Pid) -> + gen_server:cast(Pid, E), + {ok, Pid}. + +handle_call(Req, Pid) -> + Pid ! Req, + {ok, ok, Pid}. + +handle_info(E, Pid) -> + Pid ! E, + {ok, Pid}. + +terminate(_, _) -> ok. + +code_change(_OldVsn, Pid, _Extra) -> + {ok, Pid}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl new file mode 100644 index 0000000..cabc608 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl @@ -0,0 +1,153 @@ +%%% Handles socket connections, and bridges a remote server +%%% With a progressquest game. +-module(sockserv_serv). +-behaviour(gen_server). + +-record(state, {name, % player's name + next, % next step, used when initializing + socket}). % the current socket + +-export([start_link/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +-define(SOCK(Msg), {tcp, _Port, Msg}). +-define(TIME, 800). +-define(EXP, 50). + +%% The socket is passed in from sockserv_sup. +%% It's a listen socket, as started by gen_tcp:listen/2. +%% +%% In Erlang, a TCP socket must be started as a listening socket first. +%% The listening socket can then be used to listen for a connection, +%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done +%% later in this module. +%% +%% A single listen socket can be used by many processes, each accepting +%% a communication. When a communication is accepted with accept/1-2, +%% a new socket, called accept socket, is returned. This accept socket +%% is the one that may be used to communicate with a client. +start_link(Socket) -> + gen_server:start_link(?MODULE, Socket, []). + +init(Socket) -> + %% properly seeding the process + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + %% Because accepting a connection is a blocking function call, + %% we can not do it in here. Forward to the server loop! + gen_server:cast(self(), accept), + {ok, #state{socket=Socket}}. + +handle_call(_E, _From, State) -> + {noreply, State}. + +%% Accepting a connection +handle_cast(accept, S = #state{socket=ListenSocket}) -> + %% this is the socket acceptance mentioned earlier + {ok, AcceptSocket} = gen_tcp:accept(ListenSocket), + %% Remember that thou art dust, and to dust thou shalt return. + %% We want to always keep a given number of children in this app. + sockserv_sup:start_socket(), % a new acceptor is born, praise the lord + send(AcceptSocket, "What's your character's name?", []), + {noreply, S#state{socket=AcceptSocket, next=name}}; +%% The player has given us his name (in handle_info) +%% so we now roll stats that might or might not satisfy +%% said player. +handle_cast(roll_stats, S = #state{socket=Socket}) -> + Roll = pq_stats:initial_roll(), + send(Socket, + "Stats for your character:~n" + " Charisma: ~B~n" + " Constitution: ~B~n" + " Dexterity: ~B~n" + " Intelligence: ~B~n" + " Strength: ~B~n" + " Wisdom: ~B~n~n" + "Do you agree to these? y/n~n", + [Points || {_Name, Points} <- lists:sort(Roll)]), + {noreply, S#state{next={stats, Roll}}}; +%% The player has accepted the stats! Start the game! +handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) -> + processquest:start_player(Name, [{stats,Stats},{time,?TIME}, + {lvlexp, ?EXP}]), + processquest:subscribe(Name, sockserv_pq_events, self()), + {noreply, S#state{next=playing}}; +%% Events coming in from process quest +%% We know this because all these events' tuples start with the +%% name of the player. +handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N -> + [case E of + {wait, Time} -> timer:sleep(Time); + IoList -> send(Sock, IoList, []) + end || E <- sockserv_trans:to_str(Event)], % translate to a string + {noreply, S}. + +%% The TCP client sends the string "quit". We close the connection. +handle_info(?SOCK("quit"++_), S) -> + processquest:stop_player(S#state.name), + {stop, normal, S}; +%% We receive a string while looking for a name -- we assume that hte +%% string is the name. +handle_info(?SOCK(Str), S = #state{next=name}) -> + Name = line(Str), + gen_server:cast(self(), roll_stats), + {noreply, S#state{name=Name, next=stats}}; +%% The user might or might not accept the stats we rolled in handle_cast +handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) -> + case line(Str) of + "y" -> + gen_server:cast(self(), stats_accepted); + "n" -> + gen_server:cast(self(), roll_stats); + _ -> % ask again because we didn't get what we wanted + send(Socket, "Answer with y (yes) or n (no)", []) + end, + {noreply, S}; +handle_info(?SOCK(E), S = #state{socket=Socket}) -> + send(Socket, "Unexpected input: ~p~n", [E]), + {noreply, S}; +handle_info({tcp_closed, _Socket}, S) -> + {stop, normal, S}; +handle_info({tcp_error, _Socket, _}, S) -> + {stop, normal, S}; +handle_info(E, S) -> + io:format("unexpected: ~p~n", [E]), + {noreply, S}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(normal, #state{socket=S}) -> + gen_tcp:close(S); +terminate(_Reason, _State) -> + io:format("terminate reason: ~p~n", [_Reason]). + +%% Send a message through a socket, then make it active again. +%% The difference between an active and a passive socket is that +%% an active socket will send incoming data as Erlang messages, while +%% passive sockets will require to be polled with gen_tcp:recv/2-3. +%% +%% Depending on the context, you might want one or the other. I chose +%% to have active sockets because they feel somewhat easier to work +%% with. However, one problem with active sockets is that all input +%% is blindly changed into messages and makes it so the Erlang VM +%% is somewhat more subject to overload. Passive sockets push this +%% responsibility to the underlying implementation and the OS and are +%% somewhat safer. +%% +%% A middle ground exists, with sockets that are 'active once'. +%% The {active, once} option (can be set with inet:setopts or +%% when creating the listen socket) makes it so only *one* message +%% will be sent in active mode, and then the socket is automatically +%% turned back to passive mode. On each message reception, we turn +%% the socket back to {active once} as to achieve rate limiting. +send(Socket, Str, Args) -> + ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)), + ok = inet:setopts(Socket, [{active, once}]), + ok. + +%% Let's get rid of the whitespace and ignore whatever's after. +%% makes it simpler to deal with telnet. +line(Str) -> + hd(string:tokens(Str, "\r\n ")). diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl new file mode 100644 index 0000000..0e8cbdd --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl @@ -0,0 +1,32 @@ +%%% The supervisor in charge of all the socket acceptors. +-module(sockserv_sup). +-behaviour(supervisor). + +-export([start_link/0, start_socket/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + {ok, Port} = application:get_env(port), + %% Set the socket into {active_once} mode. + %% See sockserv_serv comments for more details + {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]), + spawn_link(fun empty_listeners/0), + {ok, {{simple_one_for_one, 60, 3600}, + [{socket, + {sockserv_serv, start_link, [ListenSocket]}, % pass the socket! + temporary, 1000, worker, [sockserv_serv]} + ]}}. + +start_socket() -> + supervisor:start_child(?MODULE, []). + +%% Start with 20 listeners so that many multiple connections can +%% be started at once, without serialization. In best circumstances, +%% a process would keep the count active at all times to insure nothing +%% bad happens over time when processes get killed too much. +empty_listeners() -> + [start_socket() || _ <- lists:seq(1,20)], + ok. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl new file mode 100644 index 0000000..23a70c0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl @@ -0,0 +1,58 @@ +%%% Translates the process quest events to iolists +%%% that can be sent over a socket. +%%% +%%% IO lists are lists of bytes (0..255, ASCII), +%%% binaries and other iolists. They allow to append, +%%% prepend and insert data in strings without re-writing +%%% the fragments that compose them. Erlang's drivers and +%%% IO modules accept them without an issue and are a quick, +%%% somewhat elegant solution to immutable data structures +%%% requiring many changes. +-module(sockserv_trans). +-export([to_str/1]). + +%% The player killed something +to_str({_User, killed, Time, {EnemyName, Props}}) -> + {Drop, _} = proplists:get_value(drop, Props), + [["Executing a ",EnemyName, "..."], + {wait, Time}, % take a pause between the output values + ["Obtained ", Drop, "."]]; +%% Changing locations +to_str({_Name, heading, _Time, Loc}) -> + [["Heading to ", + case Loc of + market -> "the marketplace to sell loot..."; + killing -> "the killing fields..." + end]]; +%% Leveling up +to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) -> + [["Leveled up to level ", integer_to_list(NewLvl), + " Here are your new stats:", $\n, + io_lib:format( + " Charisma: ~B~n" + " Constitution: ~B~n" + " Dexterity: ~B~n" + " Intelligence: ~B~n" + " Strength: ~B~n" + " Wisdom: ~B~n~n", + [Points || {_, Points} <- lists:sort(NewStats)])]]; +%% Bought an item +to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) -> + SlotTxt = case Slot of + armor -> " armor"; + weapon -> ""; + helmet -> " helmet"; + shield -> " shield" + end, + [["Negotiating purchase of better equipment..."], + {wait, Time}, + ["Bought a ", Item, SlotTxt]]; +%% Sold an item +to_str({_Name, sell, Time, {Item, Val}}) -> + [["Selling ", Item], + {wait, Time}, + ["Got ", integer_to_list(Val), " bucks."]]; +%% Completed a quest +to_str({_Name, quest, 0, Completed, New}) -> + [["Completed quest: ", Completed, "..."], + ["Obtained new quest: ", New, "."]]. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app new file mode 100644 index 0000000..63618d0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app @@ -0,0 +1,11 @@ +{application, sockserv, + [{description, "Socket server to forward ProcessQuest messages to a client"}, + {vsn, "1.0.1"}, + {mod, {sockserv, []}}, + {registered, [sockserv_sup]}, + {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans, + sockserv_pq_events]}, + {applications, [stdlib, kernel, processquest]}, + {env, + [{port, 8082}]} + ]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup new file mode 100644 index 0000000..5462e65 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup @@ -0,0 +1,4 @@ +{"1.0.1", + [{"1.0.0", [{load_module, sockserv_serv}]}], + [{"1.0.0", [{load_module, sockserv_serv}]}]}. + diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl new file mode 100644 index 0000000..bd82b6d --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl @@ -0,0 +1,13 @@ +%%% Starting the sockserv application. +%%% The sockserv application is a lightweight +%%% Raw socket server that can be used with telnet +%%% to follow updates on the process quest game. +%%% The port is defined in the app's env +-module(sockserv). +-behaviour(application). +-export([start/2, stop/1]). + +start(normal, []) -> + sockserv_sup:start_link(). + +stop(_) -> ok. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl new file mode 100644 index 0000000..c3c6705 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl @@ -0,0 +1,25 @@ +%%% Converts events from a player's event manager into a +%%% cast sent to the sockserv socket gen_server. +-module(sockserv_pq_events). +-behaviour(gen_event). +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2, code_change/3]). + +init(Parent) -> {ok, Parent}. + +handle_event(E, Pid) -> + gen_server:cast(Pid, E), + {ok, Pid}. + +handle_call(Req, Pid) -> + Pid ! Req, + {ok, ok, Pid}. + +handle_info(E, Pid) -> + Pid ! E, + {ok, Pid}. + +terminate(_, _) -> ok. + +code_change(_OldVsn, Pid, _Extra) -> + {ok, Pid}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl new file mode 100644 index 0000000..24aed16 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl @@ -0,0 +1,154 @@ +%%% Handles socket connections, and bridges a remote server +%%% With a progressquest game. +-module(sockserv_serv). +-behaviour(gen_server). + +-record(state, {name, % player's name + next, % next step, used when initializing + socket}). % the current socket + +-export([start_link/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +-define(SOCK(Msg), {tcp, _Port, Msg}). +-define(TIME, 800). +-define(EXP, 50). + +%% The socket is passed in from sockserv_sup. +%% It's a listen socket, as started by gen_tcp:listen/2. +%% +%% In Erlang, a TCP socket must be started as a listening socket first. +%% The listening socket can then be used to listen for a connection, +%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done +%% later in this module. +%% +%% A single listen socket can be used by many processes, each accepting +%% a communication. When a communication is accepted with accept/1-2, +%% a new socket, called accept socket, is returned. This accept socket +%% is the one that may be used to communicate with a client. +start_link(Socket) -> + gen_server:start_link(?MODULE, Socket, []). + +init(Socket) -> + %% properly seeding the process + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + %% Because accepting a connection is a blocking function call, + %% we can not do it in here. Forward to the server loop! + gen_server:cast(self(), accept), + {ok, #state{socket=Socket}}. + +handle_call(_E, _From, State) -> + {noreply, State}. + +%% Accepting a connection +handle_cast(accept, S = #state{socket=ListenSocket}) -> + %% this is the socket acceptance mentioned earlier + {ok, AcceptSocket} = gen_tcp:accept(ListenSocket), + %% Remember that thou art dust, and to dust thou shalt return. + %% We want to always keep a given number of children in this app. + sockserv_sup:start_socket(), % a new acceptor is born, praise the lord + send(AcceptSocket, "What's your character's name?", []), + {noreply, S#state{socket=AcceptSocket, next=name}}; +%% The player has given us his name (in handle_info) +%% so we now roll stats that might or might not satisfy +%% said player. +handle_cast(roll_stats, S = #state{socket=Socket}) -> + Roll = pq_stats:initial_roll(), + send(Socket, + "Stats for your character:~n" + " Charisma: ~B~n" + " Constitution: ~B~n" + " Dexterity: ~B~n" + " Intelligence: ~B~n" + " Strength: ~B~n" + " Wisdom: ~B~n~n" + "Do you agree to these? y/n~n", + [Points || {_Name, Points} <- lists:sort(Roll)]), + {noreply, S#state{next={stats, Roll}}}; +%% The player has accepted the stats! Start the game! +handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) -> + processquest:start_player(Name, [{stats,Stats},{time,?TIME}, + {lvlexp, ?EXP}]), + processquest:subscribe(Name, sockserv_pq_events, self()), + {noreply, S#state{next=playing}}; +%% Events coming in from process quest +%% We know this because all these events' tuples start with the +%% name of the player. +handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N -> + [case E of + {wait, Time} -> timer:sleep(Time); + IoList -> send(Sock, IoList, []) + end || E <- sockserv_trans:to_str(Event)], % translate to a string + {noreply, S}. + +%% The TCP client sends the string "quit". We close the connection. +handle_info(?SOCK("quit"++_), S) -> + processquest:stop_player(S#state.name), + gen_tcp:close(S#state.socket), + {stop, normal, S}; +%% We receive a string while looking for a name -- we assume that hte +%% string is the name. +handle_info(?SOCK(Str), S = #state{next=name}) -> + Name = line(Str), + gen_server:cast(self(), roll_stats), + {noreply, S#state{name=Name, next=stats}}; +%% The user might or might not accept the stats we rolled in handle_cast +handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) -> + case line(Str) of + "y" -> + gen_server:cast(self(), stats_accepted); + "n" -> + gen_server:cast(self(), roll_stats); + _ -> % ask again because we didn't get what we wanted + send(Socket, "Answer with y (yes) or n (no)", []) + end, + {noreply, S}; +handle_info(?SOCK(E), S = #state{socket=Socket}) -> + send(Socket, "Unexpected input: ~p~n", [E]), + {noreply, S}; +handle_info({tcp_closed, _Socket}, S) -> + {stop, normal, S}; +handle_info({tcp_error, _Socket, _}, S) -> + {stop, normal, S}; +handle_info(E, S) -> + io:format("unexpected: ~p~n", [E]), + {noreply, S}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(normal, _State) -> + ok; +terminate(_Reason, _State) -> + io:format("terminate reason: ~p~n", [_Reason]). + +%% Send a message through a socket, then make it active again. +%% The difference between an active and a passive socket is that +%% an active socket will send incoming data as Erlang messages, while +%% passive sockets will require to be polled with gen_tcp:recv/2-3. +%% +%% Depending on the context, you might want one or the other. I chose +%% to have active sockets because they feel somewhat easier to work +%% with. However, one problem with active sockets is that all input +%% is blindly changed into messages and makes it so the Erlang VM +%% is somewhat more subject to overload. Passive sockets push this +%% responsibility to the underlying implementation and the OS and are +%% somewhat safer. +%% +%% A middle ground exists, with sockets that are 'active once'. +%% The {active, once} option (can be set with inet:setopts or +%% when creating the listen socket) makes it so only *one* message +%% will be sent in active mode, and then the socket is automatically +%% turned back to passive mode. On each message reception, we turn +%% the socket back to {active once} as to achieve rate limiting. +send(Socket, Str, Args) -> + ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)), + ok = inet:setopts(Socket, [{active, once}]), + ok. + +%% Let's get rid of the whitespace and ignore whatever's after. +%% makes it simpler to deal with telnet. +line(Str) -> + hd(string:tokens(Str, "\r\n ")). diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl new file mode 100644 index 0000000..0e8cbdd --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl @@ -0,0 +1,32 @@ +%%% The supervisor in charge of all the socket acceptors. +-module(sockserv_sup). +-behaviour(supervisor). + +-export([start_link/0, start_socket/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + {ok, Port} = application:get_env(port), + %% Set the socket into {active_once} mode. + %% See sockserv_serv comments for more details + {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]), + spawn_link(fun empty_listeners/0), + {ok, {{simple_one_for_one, 60, 3600}, + [{socket, + {sockserv_serv, start_link, [ListenSocket]}, % pass the socket! + temporary, 1000, worker, [sockserv_serv]} + ]}}. + +start_socket() -> + supervisor:start_child(?MODULE, []). + +%% Start with 20 listeners so that many multiple connections can +%% be started at once, without serialization. In best circumstances, +%% a process would keep the count active at all times to insure nothing +%% bad happens over time when processes get killed too much. +empty_listeners() -> + [start_socket() || _ <- lists:seq(1,20)], + ok. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl new file mode 100644 index 0000000..23a70c0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl @@ -0,0 +1,58 @@ +%%% Translates the process quest events to iolists +%%% that can be sent over a socket. +%%% +%%% IO lists are lists of bytes (0..255, ASCII), +%%% binaries and other iolists. They allow to append, +%%% prepend and insert data in strings without re-writing +%%% the fragments that compose them. Erlang's drivers and +%%% IO modules accept them without an issue and are a quick, +%%% somewhat elegant solution to immutable data structures +%%% requiring many changes. +-module(sockserv_trans). +-export([to_str/1]). + +%% The player killed something +to_str({_User, killed, Time, {EnemyName, Props}}) -> + {Drop, _} = proplists:get_value(drop, Props), + [["Executing a ",EnemyName, "..."], + {wait, Time}, % take a pause between the output values + ["Obtained ", Drop, "."]]; +%% Changing locations +to_str({_Name, heading, _Time, Loc}) -> + [["Heading to ", + case Loc of + market -> "the marketplace to sell loot..."; + killing -> "the killing fields..." + end]]; +%% Leveling up +to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) -> + [["Leveled up to level ", integer_to_list(NewLvl), + " Here are your new stats:", $\n, + io_lib:format( + " Charisma: ~B~n" + " Constitution: ~B~n" + " Dexterity: ~B~n" + " Intelligence: ~B~n" + " Strength: ~B~n" + " Wisdom: ~B~n~n", + [Points || {_, Points} <- lists:sort(NewStats)])]]; +%% Bought an item +to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) -> + SlotTxt = case Slot of + armor -> " armor"; + weapon -> ""; + helmet -> " helmet"; + shield -> " shield" + end, + [["Negotiating purchase of better equipment..."], + {wait, Time}, + ["Bought a ", Item, SlotTxt]]; +%% Sold an item +to_str({_Name, sell, Time, {Item, Val}}) -> + [["Selling ", Item], + {wait, Time}, + ["Got ", integer_to_list(Val), " bucks."]]; +%% Completed a quest +to_str({_Name, quest, 0, Completed, New}) -> + [["Completed quest: ", Completed, "..."], + ["Obtained new quest: ", New, "."]]. diff --git a/learn-you-some-erlang/processquest/processquest-1.0.0.config b/learn-you-some-erlang/processquest/processquest-1.0.0.config new file mode 100644 index 0000000..2e62f5d --- /dev/null +++ b/learn-you-some-erlang/processquest/processquest-1.0.0.config @@ -0,0 +1,21 @@ +{sys, [ + {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]}, + {erts, [{mod_cond, derived}, + {app_file, strip}]}, + {rel, "processquest", "1.0.0", + [kernel, stdlib, sasl, crypto, regis, sockserv, processquest]}, + {boot_rel, "processquest"}, + {relocatable, true}, + {profile, embedded}, + {app_file, strip}, + {incl_cond, exclude}, + {excl_app_filters, ["_tests.beam"]}, + {excl_archive_filters, [".*"]}, + {app, stdlib, [{incl_cond, include}]}, + {app, kernel, [{incl_cond, include}]}, + {app, sasl, [{incl_cond, include}]}, + {app, crypto, [{incl_cond, include}]}, + {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]}, + {app, sockserv, [{vsn, "1.0.0"}, {incl_cond, include}]}, + {app, processquest, [{vsn, "1.0.0"}, {incl_cond, include}]} +]}. diff --git a/learn-you-some-erlang/processquest/processquest-1.1.0.config b/learn-you-some-erlang/processquest/processquest-1.1.0.config new file mode 100644 index 0000000..38a55f4 --- /dev/null +++ b/learn-you-some-erlang/processquest/processquest-1.1.0.config @@ -0,0 +1,21 @@ +{sys, [ + {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]}, + {erts, [{mod_cond, derived}, + {app_file, strip}]}, + {rel, "processquest", "1.1.0", + [kernel, stdlib, sasl, crypto, regis, processquest, sockserv]}, + {boot_rel, "processquest"}, + {relocatable, true}, + {profile, embedded}, + {app_file, strip}, + {incl_cond, exclude}, + {excl_app_filters, ["_tests.beam"]}, + {excl_archive_filters, [".*"]}, + {app, stdlib, [{incl_cond, include}]}, + {app, kernel, [{incl_cond, include}]}, + {app, sasl, [{incl_cond, include}]}, + {app, crypto, [{incl_cond, include}]}, + {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]}, + {app, sockserv, [{vsn, "1.0.1"}, {incl_cond, include}]}, + {app, processquest, [{vsn, "1.1.0"}, {incl_cond, include}]} +]}. diff --git a/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked b/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked diff --git a/learn-you-some-erlang/records.erl b/learn-you-some-erlang/records.erl new file mode 100644 index 0000000..f5a65b1 --- /dev/null +++ b/learn-you-some-erlang/records.erl @@ -0,0 +1,39 @@ +-module(records). +-compile(export_all). +-include("records.hrl"). + +-record(robot, {name, + type=industrial, + hobbies, + details=[]}). +-record(user, {id, name, group, age}). + +first_robot() -> + #robot{name="Mechatron", + type=handmade, + details=["Moved by a small man inside"]}. + +car_factory(CorpName) -> + #robot{name=CorpName, hobbies="building cars"}. + +%% use pattern matching to filter +admin_panel(#user{name=Name, group=admin}) -> + Name ++ " is allowed!"; +admin_panel(#user{name=Name}) -> + Name ++ " is not allowed". + +%% can extend user without problem +adult_section(U = #user{}) when U#user.age >= 18 -> + %% Show stuff that can't be written in such a text + allowed; +adult_section(_) -> + %% redirect to sesame street site + forbidden. + +repairman(Rob) -> + Details = Rob#robot.details, + NewRob = Rob#robot{details=["Repaired by repairman"|Details]}, + {repaired, NewRob}. + + +included() -> #included{some_field="Some value"}. diff --git a/learn-you-some-erlang/records.hrl b/learn-you-some-erlang/records.hrl new file mode 100644 index 0000000..c9a0eae --- /dev/null +++ b/learn-you-some-erlang/records.hrl @@ -0,0 +1,4 @@ +%% this is a .hrl (header) file. +-record(included, {some_field, + some_default = "yeah!", + unimaginative_name}). diff --git a/learn-you-some-erlang/recursive.erl b/learn-you-some-erlang/recursive.erl new file mode 100644 index 0000000..96a046c --- /dev/null +++ b/learn-you-some-erlang/recursive.erl @@ -0,0 +1,135 @@ +-module(recursive). +-export([fac/1, tail_fac/1, len/1, tail_len/1, duplicate/2, + tail_duplicate/2, reverse/1, tail_reverse/1, sublist/2, + tail_sublist/2, zip/2, lenient_zip/2, tail_zip/2, + tail_lenient_zip/2]). +-export([quicksort/1, lc_quicksort/1, bestest_qsort/1]). + +%% basic recursive factorial function +fac(0) -> 1; +fac(N) when N > 0 -> N*fac(N-1). + +%% tail recursive version of fac/1 +tail_fac(N) -> tail_fac(N,1). + +tail_fac(0,Acc) -> Acc; +tail_fac(N,Acc) when N > 0 -> tail_fac(N-1,N*Acc). + +%% finds the len of a list +len([]) -> 0; +len([_|T]) -> 1 + len(T). + +%% tail recursive version of len/1 +tail_len(L) -> tail_len(L,0). + +tail_len([], Acc) -> Acc; +tail_len([_|T], Acc) -> tail_len(T,Acc+1). + +%% duplicates Term N times +duplicate(0,_) -> + []; +duplicate(N,Term) when N > 0 -> + [Term|duplicate(N-1,Term)]. + +%% tail recursive version of duplicate/2 +tail_duplicate(N,Term) -> + tail_duplicate(N,Term,[]). + +tail_duplicate(0,_,List) -> + List; +tail_duplicate(N,Term,List) when N > 0 -> + tail_duplicate(N-1, Term, [Term|List]). + +%% reverses a list (a truly descriptive function name!) +reverse([]) -> []; +reverse([H|T]) -> reverse(T)++[H]. + + +%% tail recursive version of reverse/1 +tail_reverse(L) -> tail_reverse(L,[]). + +tail_reverse([],Acc) -> Acc; +tail_reverse([H|T],Acc) -> tail_reverse(T, [H|Acc]). + + +%% returns the N first elements of a list +sublist(_,0) -> []; +sublist([],_) -> []; +sublist([H|T],N) when N > 0 -> [H|sublist(T,N-1)]. + +%% tail recursive version of sublist/2 +tail_sublist(L, N) -> reverse(tail_sublist(L, N, [])). + +tail_sublist(_, 0, SubList) -> SubList; +tail_sublist([], _, SubList) -> SubList; +tail_sublist([H|T], N, SubList) when N > 0 -> + tail_sublist(T, N-1, [H|SubList]). + +%% Takes two lists [A] and [B] and returns a list of tuples +%% with the form [{A,B}]. Both lists need to be of same lenght. +zip([],[]) -> []; +zip([X|Xs],[Y|Ys]) -> [{X,Y}|zip(Xs,Ys)]. + +%% Same as zip/2, but lists can vary in lenght +lenient_zip([],_) -> []; +lenient_zip(_,[]) -> []; +lenient_zip([X|Xs],[Y|Ys]) -> [{X,Y}|lenient_zip(Xs,Ys)]. + +%% tail recursive version of zip/2 +tail_zip(X,Y) -> reverse(tail_zip(X,Y,[])). + +tail_zip([],[],Acc) -> Acc; +tail_zip([X|Xs],[Y|Ys], Acc) -> + tail_zip(Xs,Ys, [{X,Y}|Acc]). + +%% tail recursive version of lenient-zip/2 +tail_lenient_zip(X,Y) -> reverse(tail_lenient_zip(X,Y,[])). + +tail_lenient_zip([],_,Acc) -> Acc; +tail_lenient_zip(_,[],Acc) -> Acc; +tail_lenient_zip([X|Xs],[Y|Ys], Acc) -> + tail_lenient_zip(Xs,Ys,[{X,Y}|Acc]). + +%% basic quicksort function. +quicksort([]) -> []; +quicksort([Pivot|Rest]) -> + {Smaller, Larger} = partition(Pivot,Rest,[],[]), + quicksort(Smaller) ++ [Pivot] ++ quicksort(Larger). + +partition(_,[], Smaller, Larger) -> {Smaller, Larger}; +partition(Pivot, [H|T], Smaller, Larger) -> + if H =< Pivot -> partition(Pivot, T, [H|Smaller], Larger); + H > Pivot -> partition(Pivot, T, Smaller, [H|Larger]) + end. + +%% quicksort built with list comprehensions rather than with a +%% partition function. +lc_quicksort([]) -> []; +lc_quicksort([Pivot|Rest]) -> + lc_quicksort([Smaller || Smaller <- Rest, Smaller =< Pivot]) + ++ [Pivot] ++ + lc_quicksort([Larger || Larger <- Rest, Larger > Pivot]). + +%% BESTEST QUICKSORT, YEAH! +%% (This is not really the bestest quicksort, because we do not do +%% adequate pivot selection. It is the bestest of this book, alright? +%% Thanks to literateprograms.org for this example. Give them a visit! +%% http://en.literateprograms.org/Quicksort_(Erlang) ) +bestest_qsort([]) -> []; +bestest_qsort(L=[_|_]) -> + bestest_qsort(L, []). + +bestest_qsort([], Acc) -> Acc; +bestest_qsort([Pivot|Rest], Acc) -> + bestest_partition(Pivot, Rest, {[], [Pivot], []}, Acc). + +bestest_partition(_, [], {Smaller, Equal, Larger}, Acc) -> + bestest_qsort(Smaller, Equal ++ bestest_qsort(Larger, Acc)); +bestest_partition(Pivot, [H|T], {Smaller, Equal, Larger}, Acc) -> + if H < Pivot -> + bestest_partition(Pivot, T, {[H|Smaller], Equal, Larger}, Acc); + H > Pivot -> + bestest_partition(Pivot, T, {Smaller, Equal, [H|Larger]}, Acc); + H == Pivot -> + bestest_partition(Pivot, T, {Smaller, [H|Equal], Larger}, Acc) + end. diff --git a/learn-you-some-erlang/release/erlcount-1.0.config b/learn-you-some-erlang/release/erlcount-1.0.config new file mode 100644 index 0000000..235ab23 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0.config @@ -0,0 +1,20 @@ +{sys, [ + {lib_dirs, ["/home/ferd/code/learn-you-some-erlang/release/"]}, + {erts, [{vsn, "5.8.4"}]}, + {rel, "erlcount", "1.0.0", + [kernel, + stdlib, + {ppool, permanent}, + {erlcount, transient} + ]}, + {boot_rel, "erlcount"}, + {relocatable, true}, + {profile, embedded}, + {app, ppool, [{vsn, "1.0.0"}, + {app_file, all}, + {debug_info, keep}]}, + {app, erlcount, [{vsn, "1.0.0"}, + {incl_cond, include}, + {app_file, strip}, + {debug_info, strip}]} +]}. diff --git a/learn-you-some-erlang/release/erlcount-1.0.rel b/learn-you-some-erlang/release/erlcount-1.0.rel new file mode 100644 index 0000000..aa2ce44 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0.rel @@ -0,0 +1,7 @@ +{release, + {"erlcount", "1.0.0"}, + {erts, "5.8.4"}, + [{kernel, "2.14.4"}, + {stdlib, "1.17.4"}, + {ppool, "1.0.0", permanent}, + {erlcount, "1.0.0", transient}]}. diff --git a/learn-you-some-erlang/release/erlcount-1.0/Emakefile b/learn-you-some-erlang/release/erlcount-1.0/Emakefile new file mode 100644 index 0000000..76e98fc --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/Emakefile @@ -0,0 +1,4 @@ +{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}. +%% The TESTDIR macro assumes the file is running from the 'erlcount-1.0' +%% directory, sitting within 'learn-you-some-erlang/'. +{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}, {d, 'TESTDIR', ".."}]}. diff --git a/learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app b/learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app new file mode 100644 index 0000000..22f0f0a --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app @@ -0,0 +1,14 @@ +{application, erlcount, + [{vsn, "1.0.0"}, + {description, "Run regular expressions on Erlang source files"}, + {modules, [erlcount, erlcount_sup, erlcount_lib, + erlcount_dispatch, erlcount_counter]}, + {applications, [stdlib, kernel, ppool]}, + {registered, [erlcount]}, + {mod, {erlcount, []}}, + {env, + [{directory, "."}, + {regex, ["if\\s.+->", "case\\s.+\\sof"]}, + {max_files, 10}]} + ]}. + diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl new file mode 100644 index 0000000..16a9f23 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl @@ -0,0 +1,9 @@ +-module(erlcount). +-behaviour(application). +-export([start/2, stop/1]). + +start(normal, _Args) -> + erlcount_sup:start_link(). + +stop(_State) -> + ok. diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl new file mode 100644 index 0000000..c42fd4d --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl @@ -0,0 +1,35 @@ +-module(erlcount_counter). +-behaviour(gen_server). +-export([start_link/4]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {dispatcher, ref, file, re}). + +start_link(DispatcherPid, Ref, FileName, Regex) -> + gen_server:start_link(?MODULE, [DispatcherPid, Ref, FileName, Regex], []). + +init([DispatcherPid, Ref, FileName, Regex]) -> + self() ! start, + {ok, #state{dispatcher=DispatcherPid, + ref = Ref, + file = FileName, + re = Regex}}. + +handle_call(_Msg, _From, State) -> + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(start, S = #state{re=Re, ref=Ref}) -> + {ok, Bin} = file:read_file(S#state.file), + Count = erlcount_lib:regex_count(Re, Bin), + erlcount_dispatch:complete(S#state.dispatcher, Re, Ref, Count), + {stop, normal, S}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl new file mode 100644 index 0000000..c125da3 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl @@ -0,0 +1,86 @@ +-module(erlcount_dispatch). +-behaviour(gen_fsm). +-export([start_link/0, complete/4]). +-export([init/1, dispatching/2, listening/2, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). + +-define(POOL, erlcount). +-record(data, {regex=[], refs=[]}). + +%%% PUBLIC API +start_link() -> + gen_fsm:start_link(?MODULE, [], []). + +complete(Pid, Regex, Ref, Count) -> + gen_fsm:send_all_state_event(Pid, {complete, Regex, Ref, Count}). + +%%% GEN_FSM +%% Two states: dispatching and listening +init([]) -> + %% Move the get_env stuff to the supervisor's init. + {ok, Re} = application:get_env(regex), + {ok, Dir} = application:get_env(directory), + {ok, MaxFiles} = application:get_env(max_files), + ppool:start_pool(?POOL, MaxFiles, {erlcount_counter, start_link, []}), + case lists:all(fun valid_regex/1, Re) of + true -> + %% creates a regex entry of the form [{Re, Count}] + self() ! {start, Dir}, + {ok, dispatching, #data{regex=[{R,0} || R <- Re]}}; + false -> + {stop, invalid_regex} + end. + +dispatching({continue, File, Continuation}, Data = #data{regex=Re, refs=Refs}) -> + F = fun({Regex,_Count}, NewRefs) -> + Ref = make_ref(), + ppool:async_queue(?POOL, [self(), Ref, File, Regex]), + [Ref|NewRefs] + end, + NewRefs = lists:foldl(F, Refs, Re), + gen_fsm:send_event(self(), Continuation()), + {next_state, dispatching, Data#data{refs = NewRefs}}; +dispatching(done, Data) -> + %% This is a special case. We can not assume that all messages have NOT + %% been received by the time we hit 'done'. As such, we directly move to + %% listening/2 without waiting for an external event. + listening(done, Data). + +listening(done, #data{regex=Re, refs=[]}) -> % all received! + [io:format("Regex ~s has ~p results~n", [R,C]) || {R, C} <- Re], + {stop, normal, done}; +listening(done, Data) -> % entries still missing + {next_state, listening, Data}. + +handle_event({complete, Regex, Ref, Count}, State, Data = #data{regex=Re, refs=Refs}) -> + {Regex, OldCount} = lists:keyfind(Regex, 1, Re), + NewRe = lists:keyreplace(Regex, 1, Re, {Regex, OldCount+Count}), + NewData = Data#data{regex=NewRe, refs=Refs--[Ref]}, + case State of + dispatching -> + {next_state, dispatching, NewData}; + listening -> + listening(done, NewData) + end. + +handle_sync_event(Event, _From, State, Data) -> + io:format("Unexpected event: ~p~n", [Event]), + {next_state, State, Data}. + +handle_info({start, Dir}, State, Data) -> + gen_fsm:send_event(self(), erlcount_lib:find_erl(Dir)), + {next_state, State, Data}. + +terminate(_Reason, _State, _Data) -> + init:stop(). + +code_change(_OldVsn, State, Data, _Extra) -> + {ok, State, Data}. + +%%% PRIVATE +valid_regex(Re) -> + try re:run("", Re) of + _ -> true + catch + error:badarg -> false + end. diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl new file mode 100644 index 0000000..70c062f --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl @@ -0,0 +1,55 @@ +-module(erlcount_lib). +-export([find_erl/1, regex_count/2]). +-include_lib("kernel/include/file.hrl"). + +%% Finds all files ending in .erl +find_erl(Directory) -> + find_erl(Directory, queue:new()). + +regex_count(Re, Str) -> + case re:run(Str, Re, [global]) of + nomatch -> 0; + {match, List} -> length(List) + end. + +%%% Private +%% Dispatches based on file type +find_erl(Name, Queue) -> + {ok, F = #file_info{}} = file:read_file_info(Name), + case F#file_info.type of + directory -> handle_directory(Name, Queue); + regular -> handle_regular_file(Name, Queue); + _Other -> dequeue_and_run(Queue) + end. + +%% Opens directories and enqueues files in there +handle_directory(Dir, Queue) -> + case file:list_dir(Dir) of + {ok, []} -> + dequeue_and_run(Queue); + {ok, Files} -> + dequeue_and_run(enqueue_many(Dir, Files, Queue)) + end. + +%% Checks if the file finishes in .erl +handle_regular_file(Name, Queue) -> + case filename:extension(Name) of + ".erl" -> + {continue, Name, fun() -> dequeue_and_run(Queue) end}; + _NonErl -> + dequeue_and_run(Queue) + end. + +%% Pops an item from the queue and runs it. +dequeue_and_run(Queue) -> + case queue:out(Queue) of + {empty, _} -> done; + {{value, File}, NewQueue} -> find_erl(File, NewQueue) + end. + +%% Adds a bunch of items to the queue. +enqueue_many(Path, Files, Queue) -> + F = fun(File, Q) -> queue:in(filename:join(Path,File), Q) end, + lists:foldl(F, Queue, Files). + + diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl new file mode 100644 index 0000000..b8633a3 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl @@ -0,0 +1,17 @@ +-module(erlcount_sup). +-behaviour(supervisor). +-export([start_link/0, init/1]). + +start_link() -> + supervisor:start_link(?MODULE, []). + +init([]) -> + MaxRestart = 5, + MaxTime = 100, + {ok, {{one_for_one, MaxRestart, MaxTime}, + [{dispatch, + {erlcount_dispatch, start_link, []}, + transient, + 60000, + worker, + [erlcount_dispatch]}]}}. diff --git a/learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl b/learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl new file mode 100644 index 0000000..7459a39 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl @@ -0,0 +1,35 @@ +-module(erlcount_tests). +-include_lib("eunit/include/eunit.hrl"). +-ifndef(TESTDIR). +%% Assumes we're running from the app's directory. We want to target the +%% 'learn-you-some-erlang' directory. +-define(TESTDIR, ".."). +-endif. + +%% NOTE: +%% Because we do not want the tests to be bound to a specific snapshot in time +%% of our app, we will instead compare it to an oracle built with unix +%% commands. Users running windows sadly won't be able to run these tests. + +%% We'll be forcing the design to be continuation-based when it comes to +%% reading files. This will require some explaining to the user, but will +%% allow to show how we can read files and schedule them at the same time, +%% but without breaking functional principles of referential transparency +%% and while allowing specialised functions to be written in a testable manner. +find_erl_test_() -> + ?_assertEqual(lists:sort(string:tokens(os:cmd("find "++?TESTDIR++" -name *.erl"), "\n")), + lists:sort(build_list(erlcount_lib:find_erl(?TESTDIR)))). + +build_list(Term) -> build_list(Term, []). + +build_list(done, List) -> List; +build_list({continue, Entry, Fun}, List) -> + build_list(Fun(), [Entry|List]). + +regex_count_test_() -> + [?_assertEqual(5, erlcount_lib:regex_count("a", "a a a a a")), + ?_assertEqual(0, erlcount_lib:regex_count("o", "a a a a a")), + ?_assertEqual(2, erlcount_lib:regex_count("a.*", "a a a\na a a")), + ?_assertEqual(3, erlcount_lib:regex_count("if", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")), + ?_assertEqual(1, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")), + ?_assertEqual(2, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end,\n if true -> ok end.\n"))]. diff --git a/learn-you-some-erlang/release/erlcount-sm.config b/learn-you-some-erlang/release/erlcount-sm.config new file mode 100644 index 0000000..6e22bf1 --- /dev/null +++ b/learn-you-some-erlang/release/erlcount-sm.config @@ -0,0 +1,17 @@ +{sys, [ + {lib_dirs, ["/home/ferd/code/learn-you-some-erlang/release/"]}, + {erts, [{mod_cond, derived}, + {app_file, strip}]}, + {rel, "erlcount", "1.0.0", [kernel, stdlib, ppool, erlcount]}, + {boot_rel, "erlcount"}, + {relocatable, true}, + {profile, embedded}, + {app_file, strip}, + {debug_info, strip}, + {incl_cond, exclude}, + {excl_app_filters, ["_tests.beam"]}, + {app, stdlib, [{incl_cond, include}]}, + {app, kernel, [{incl_cond, include}]}, + {app, ppool, [{vsn, "1.0.0"}, {incl_cond, include}]}, + {app, erlcount, [{vsn, "1.0.0"}, {incl_cond, include}]} +]}. diff --git a/learn-you-some-erlang/release/ppool-1.0/Emakefile b/learn-you-some-erlang/release/ppool-1.0/Emakefile new file mode 100644 index 0000000..8e1f951 --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/Emakefile @@ -0,0 +1,2 @@ +{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}. +{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}. diff --git a/learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app b/learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app new file mode 100644 index 0000000..f806b7c --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app @@ -0,0 +1,8 @@ +{application, ppool, + [{vsn, "1.0.0"}, + {description, "Run and enqueue different concurrent tasks"}, + {modules, [ppool, ppool_serv, ppool_sup, ppool_supersup, ppool_worker_sup]}, + {applications, [stdlib, kernel]}, + {registered, [ppool]}, + {mod, {ppool, []}} + ]}. diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool.erl new file mode 100644 index 0000000..5723f98 --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool.erl @@ -0,0 +1,26 @@ +%%% API module for the pool +-module(ppool). +-behaviour(application). +-export([start/2, stop/1, start_pool/3, + run/2, sync_queue/2, async_queue/2, stop_pool/1]). + +start(normal, _Args) -> + ppool_supersup:start_link(). + +stop(_State) -> + ok. + +start_pool(Name, Limit, {M,F,A}) -> + ppool_supersup:start_pool(Name, Limit, {M,F,A}). + +stop_pool(Name) -> + ppool_supersup:stop_pool(Name). + +run(Name, Args) -> + ppool_serv:run(Name, Args). + +async_queue(Name, Args) -> + ppool_serv:async_queue(Name, Args). + +sync_queue(Name, Args) -> + ppool_serv:sync_queue(Name, Args). diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl new file mode 100644 index 0000000..bfb9b93 --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl @@ -0,0 +1,113 @@ +-module(ppool_serv). +-behaviour(gen_server). +-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +%% The friendly supervisor is started dynamically! +-define(SPEC(MFA), + {worker_sup, + {ppool_worker_sup, start_link, [MFA]}, + temporary, + 10000, + supervisor, + [ppool_worker_sup]}). + +-record(state, {limit=0, + sup, + refs, + queue=queue:new()}). + +start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) -> + gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []). + +start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) -> + gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []). + +run(Name, Args) -> + gen_server:call(Name, {run, Args}). + +sync_queue(Name, Args) -> + gen_server:call(Name, {sync, Args}, infinity). + +async_queue(Name, Args) -> + gen_server:cast(Name, {async, Args}). + +stop(Name) -> + gen_server:call(Name, stop). + +%% Gen server +init({Limit, MFA, Sup}) -> + %% We need to find the Pid of the worker supervisor from here, + %% but alas, this would be calling the supervisor while it waits for us! + self() ! {start_worker_supervisor, Sup, MFA}, + {ok, #state{limit=Limit, refs=gb_sets:empty()}}. + +handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_call({run, _Args}, _From, S=#state{limit=N}) when N =< 0 -> + {reply, noalloc, S}; + +handle_call({sync, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_call({sync, Args}, From, S = #state{queue=Q}) -> + {noreply, S#state{queue=queue:in({From, Args}, Q)}}; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Msg, _From, State) -> + {noreply, State}. + + +handle_cast({async, Args}, S=#state{limit=N, sup=Sup, refs=R}) when N > 0 -> + {ok, Pid} = supervisor:start_child(Sup, Args), + Ref = erlang:monitor(process, Pid), + {noreply, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}}; +handle_cast({async, Args}, S=#state{limit=N, queue=Q}) when N =< 0 -> + {noreply, S#state{queue=queue:in(Args,Q)}}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{refs=Refs}) -> +%% io:format("received down msg~n"), + case gb_sets:is_element(Ref, Refs) of + true -> + handle_down_worker(Ref, S); + false -> %% Not our responsibility + {noreply, S} + end; +handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) -> + {ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)), + link(Pid), + {noreply, S#state{sup=Pid}}; +handle_info(Msg, State) -> + io:format("Unknown msg: ~p~n", [Msg]), + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. + +handle_down_worker(Ref, S = #state{limit=L, sup=Sup, refs=Refs}) -> + case queue:out(S#state.queue) of + {{value, {From, Args}}, Q} -> + {ok, Pid} = supervisor:start_child(Sup, Args), + NewRef = erlang:monitor(process, Pid), + NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)), + gen_server:reply(From, {ok, Pid}), + {noreply, S#state{refs=NewRefs, queue=Q}}; + {{value, Args}, Q} -> + {ok, Pid} = supervisor:start_child(Sup, Args), + NewRef = erlang:monitor(process, Pid), + NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)), + {noreply, S#state{refs=NewRefs, queue=Q}}; + {empty, _} -> + {noreply, S#state{limit=L+1, refs=gb_sets:delete(Ref,Refs)}} + end. diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl new file mode 100644 index 0000000..71ec31d --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl @@ -0,0 +1,17 @@ +-module(ppool_sup). +-export([start_link/3, init/1]). +-behaviour(supervisor). + +start_link(Name, Limit, MFA) -> + supervisor:start_link(?MODULE, {Name, Limit, MFA}). + +init({Name, Limit, MFA}) -> + MaxRestart = 1, + MaxTime = 3000, + {ok, {{one_for_all, MaxRestart, MaxTime}, + [{serv, + {ppool_serv, start_link, [Name, Limit, self(), MFA]}, + permanent, + 5000, + worker, + [ppool_serv]}]}}. diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl new file mode 100644 index 0000000..06fa0af --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl @@ -0,0 +1,22 @@ +-module(ppool_supersup). +-behaviour(supervisor). +-export([start_link/0, start_pool/3, stop_pool/1]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ppool}, ?MODULE, []). + +start_pool(Name, Limit, MFA) -> + ChildSpec = {Name, + {ppool_sup, start_link, [Name, Limit, MFA]}, + permanent, 10500, supervisor, [ppool_sup]}, + supervisor:start_child(ppool, ChildSpec). + +stop_pool(Name) -> + supervisor:terminate_child(ppool, Name), + supervisor:delete_child(ppool, Name). + +init([]) -> + MaxRestart = 6, + MaxTime = 3000, + {ok, {{one_for_one, MaxRestart, MaxTime}, []}}. diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl new file mode 100644 index 0000000..2467c47 --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl @@ -0,0 +1,15 @@ +-module(ppool_worker_sup). +-export([start_link/1, init/1]). +-behaviour(supervisor). + +start_link(MFA = {_,_,_}) -> + supervisor:start_link(?MODULE, MFA). + +init({M,F,A}) -> + MaxRestart = 5, + MaxTime = 3600, + {ok, {{simple_one_for_one, MaxRestart, MaxTime}, + [{ppool_worker, + {M,F,A}, + temporary, 5000, worker, [M]}]}}. + diff --git a/learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl b/learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl new file mode 100644 index 0000000..903f821 --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl @@ -0,0 +1,50 @@ +%% demo module, a nagger for tasks, +%% because the previous one wasn't good enough +%% +%% Can take: +%% - a time delay for which to nag, +%% - an adress to say where the messages should be sent +%% - a message to send in the mailbox telling you what to nag, +%% with an id to be able to call: -> +%% - a command to say the task is done +-module(ppool_nagger). +-behaviour(gen_server). +-export([start_link/4, stop/1]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, code_change/3, terminate/2]). + +start_link(Task, Delay, Max, SendTo) -> + gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo} , []). + +stop(Pid) -> + gen_server:call(Pid, stop). + +init({Task, Delay, Max, SendTo}) -> + process_flag(trap_exit, true), % for tests & terminate too + {ok, {Task, Delay, Max, SendTo}, Delay}. + +%%% OTP Callbacks +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Msg, _From, State) -> + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(timeout, {Task, Delay, Max, SendTo}) -> + SendTo ! {self(), Task}, + if Max =:= infinity -> + {noreply, {Task, Delay, Max, SendTo}, Delay}; + Max =< 1 -> + {stop, normal, {Task, Delay, 0, SendTo}}; + Max > 1 -> + {noreply, {Task, Delay, Max-1, SendTo}, Delay} + end; +handle_info(_Msg, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> ok. diff --git a/learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl b/learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl new file mode 100644 index 0000000..8f0dfe2 --- /dev/null +++ b/learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl @@ -0,0 +1,200 @@ +-module(ppool_tests). +-include_lib("eunit/include/eunit.hrl"). +-export([test_mfa/1, wait_mfa/1]). + +%%% All Test Fixtures +start_test_() -> + {"It should be possible to start a pool server and give it a name", + {setup, + fun find_unique_name/0, + fun(Name) -> + [start_and_test_name(Name)] + end}}. + +mfa_test_() -> + {"A pool process can be allocated which will be ordered " + "to run an MFA call determined at start time, with arguments " + "provided at call time", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_mfa(Name)] + end} + }. + +alloc_test_() -> + {"A pool process can be allocated which will be ordered " + "to run a worker, only if there are enough which " + "haven't been ordered to run yet.", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_alloc(Name), + pool_run_noalloc(Name)] + end} + }. + +realloc_test_() -> + {"When an allocated process dies, " + "A new one can be allocated to replace it.", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun(Name) -> + [pool_run_realloc(Name)] + end} + }. + +queue_test_() -> + {"The queue function can be used to run the function as soon as possible. " + "If no space is available, the worker call is added to the queue.", + {foreach, + fun start_ppool/0, + fun kill_ppool/1, + [fun(Name) -> test_async_queue(Name) end, + fun(Name) -> test_sync_queue(Name) end]} + }. + +supervision_test_() -> + {"The ppool will never restart a dead child, but all children (OTP " + "compliant) will be shut down when closing the pool, even if they " + "are trapping exits", + {setup, + fun find_unique_name/0, + fun test_supervision/1}}. + +auth_test_() -> + {"The ppool should only dequeue tasks after receiving a down signal " + "from a worker and nobody else", + {setup, + fun start_ppool/0, + fun kill_ppool/1, + fun test_auth_dealloc/1}}. + +%%% Setups/teardowns +find_unique_name() -> + application:start(ppool), + Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))), + ?assertEqual(undefined, whereis(Name)), + Name. + +start_ppool() -> + Name = find_unique_name(), + ppool:start_pool(Name, 2, {ppool_nagger, start_link, []}), + Name. + +kill_ppool(Name) -> + ppool:stop_pool(Name). + +%%% Actual tests +start_and_test_name(Name) -> + ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}), + A = whereis(Name), + ppool:stop_pool(Name), + timer:sleep(100), + B = whereis(Name), + [?_assert(undefined =/= A), + ?_assertEqual(undefined, B)]. + +pool_run_mfa(Name) -> + ppool:run(Name, [i_am_running, 1, 1, self()]), + X = receive + {_Pid, i_am_running} -> ok + after 3000 -> + timeout + end, + ?_assertEqual(ok, X). + +pool_run_alloc(Name) -> + {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]), + X = receive + {Pid, i_am_running} -> ok + after 3000 -> + timeout + end, + [?_assert(is_pid(Pid)), + ?_assertEqual(ok, X)]. + +pool_run_noalloc(Name) -> + %% Init function should have set the limit to 2 + ppool:run(Name, [i_am_running, 300, 1, self()]), + ppool:run(Name, [i_am_running, 300, 1, self()]), + X = ppool:run(Name, [i_am_running, 1, 1, self()]), + ?_assertEqual(noalloc, X). + +pool_run_realloc(Name) -> + %% Init function should have set the limit to 2 + {ok, A} = ppool:run(Name, [i_am_running, 500, 1, self()]), + timer:sleep(100), + {ok, B} = ppool:run(Name, [i_am_running, 500, 1, self()]), + timer:sleep(600), + {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]), + timer:sleep(100), + L = flush(), + [?_assert(is_pid(Pid)), + ?_assertEqual([{A,i_am_running}, {B,i_am_running}, {Pid,i_am_running}], + L)]. + +test_async_queue(Name) -> + %% Still two elements max! + ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]), + noalloc = ppool:run(Name, [i_am_running, 2000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 500, 1, self()]), + timer:sleep(3500), + L = flush(), + ?_assertMatch([{_, i_am_running}, {_, i_am_running}, {_, i_am_running}], L). + +test_sync_queue(Name) -> + %% Hell yase, two max + {ok, Pid} = ppool:sync_queue(Name, [i_am_running, 200, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]), + {ok, Pid2} = ppool:sync_queue(Name, [i_am_running, 100, 1, self()]), + timer:sleep(300), + L = flush(), + [?_assert(is_pid(Pid)), + ?_assert(is_pid(Pid2)), + ?_assertMatch([{_,i_am_running}, {_,i_am_running}, + {_,i_am_running}, {_,i_am_running}], + L)]. + +test_supervision(Name) -> + ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}), + {ok, Pid} = ppool:run(Name, [sup, 10000, 100, self()]), + ppool:stop_pool(Name), + timer:sleep(100), + ?_assertEqual(undefined, process_info(Pid)). + +test_auth_dealloc(Name) -> + %% Hell yase, two max + {ok, _Pid} = ppool:sync_queue(Name, [i_am_running, 500, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]), + ok = ppool:async_queue(Name, [i_am_running, 1, 1, self()]), + timer:sleep(600), + Name ! {'DOWN', make_ref(), process, self(), normal}, + Name ! {'DOWN', make_ref(), process, self(), normal}, + Name ! {'DOWN', make_ref(), process, self(), normal}, + timer:sleep(200), + L = flush(), + ?_assertMatch([{_,i_am_running}], L). + + + +flush() -> + receive + X -> [X|flush()] + after 0 -> + [] + end. + +%% Exported Helper functions +test_mfa(Pid) -> + Pid ! i_am_running. + +wait_mfa(Pid) -> + Pid ! i_am_running, + timer:sleep(3000). diff --git a/learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank b/learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank 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]). + diff --git a/learn-you-some-erlang/road.erl b/learn-you-some-erlang/road.erl new file mode 100644 index 0000000..fa34f5a --- /dev/null +++ b/learn-you-some-erlang/road.erl @@ -0,0 +1,39 @@ +-module(road). +-compile(export_all). + +main([FileName]) -> + {ok, Bin} = file:read_file(FileName), + Map = parse_map(Bin), + io:format("~p~n",[optimal_path(Map)]), + erlang:halt(). + +%% Transform a string into a readable map of triples +parse_map(Bin) when is_binary(Bin) -> + parse_map(binary_to_list(Bin)); +parse_map(Str) when is_list(Str) -> + Values = [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")], + group_vals(Values, []). + +group_vals([], Acc) -> + lists:reverse(Acc); +group_vals([A,B,X|Rest], Acc) -> + group_vals(Rest, [{A,B,X} | Acc]). + +%% Picks the best of all paths, woo! +optimal_path(Map) -> + {A,B} = lists:foldl(fun shortest_step/2, {{0,[]}, {0,[]}}, Map), + {_Dist,Path} = if hd(element(2,A)) =/= {x,0} -> A; + hd(element(2,B)) =/= {x,0} -> B + end, + lists:reverse(Path). + +%% actual problem solving +%% change triples of the form {A,B,X} +%% where A,B,X are distances and a,b,x are possible paths +%% to the form {DistanceSum, PathList}. +shortest_step({A,B,X}, {{DistA,PathA}, {DistB,PathB}}) -> + OptA1 = {DistA + A, [{a,A}|PathA]}, + OptA2 = {DistB + B + X, [{x,X}, {b,B}|PathB]}, + OptB1 = {DistB + B, [{b,B}|PathB]}, + OptB2 = {DistA + A + X, [{x,X}, {a,A}|PathA]}, + {erlang:min(OptA1, OptA2), erlang:min(OptB1, OptB2)}. diff --git a/learn-you-some-erlang/road.txt b/learn-you-some-erlang/road.txt new file mode 100644 index 0000000..fa6997e --- /dev/null +++ b/learn-you-some-erlang/road.txt @@ -0,0 +1,12 @@ +50 +10 +30 +5 +90 +20 +40 +2 +25 +10 +8 +0 diff --git a/learn-you-some-erlang/tester.erl b/learn-you-some-erlang/tester.erl new file mode 100644 index 0000000..f1d1b1c --- /dev/null +++ b/learn-you-some-erlang/tester.erl @@ -0,0 +1,55 @@ +-module(tester). +-export([dir/0, dir/2]). +-define(EXT, ".erl"). % file extension to look for +-define(MODS, "./"). +-define(TESTS, "./tests/"). + +%% scans both a module directory and a test directory, compiles the +%% modules inside and then call for tests to be ran. +%% +%% usage: +%% tester:dir("./","./tests/"). +dir() -> dir(?MODS, ?TESTS). +dir(ModulePath, TestPath) -> + ModuleList = module_list(ModulePath), + TestList = module_list(TestPath), + [compile(ModulePath++X) || X <- ModuleList], + [compile(TestPath++X) || X <- TestList], + test_all(TestList), + warnings(), + cleanup(ModuleList), + cleanup(TestList), + ok. + +%% assumes pre-compiled modules +test_all(FileList) -> + Split = [lists:nth(1, string:tokens(File, ".")) || File <- FileList], + [eunit:test(list_to_existing_atom(F), [verbose]) || F <- Split]. + % [(list_to_existing_atom(F)):test() || F <- Split]. + +cleanup(Files) -> + [file:delete(lists:nth(1, string:tokens(F, "."))++".beam") || F <- Files]. + +%% get module .erl file names from a directory +module_list(Path) -> + SameExt = fun(File) -> get_ext(File) =:= ?EXT end, + {ok, Files} = file:list_dir(Path), + lists:filter(SameExt, Files). + +%% find the extension of a file (length is taken from the ?EXT macro). +get_ext(Str) -> + lists:reverse(string:sub_string(lists:reverse(Str), 1, length(?EXT))). + +compile(FileName) -> + compile:file(FileName, [report, verbose, export_all]). + +warnings() -> + Warns = [{Mod, get_warnings(Mod)} || {Mod,_Path} <- code:all_loaded(), + has_warnings(Mod)], + io:format("These need to be tested better: ~n\t~p~n", [Warns]). + +has_warnings(Mod) -> + is_list(get_warnings(Mod)). + +get_warnings(Mod) -> + proplists:get_value(test_warnings, Mod:module_info(attributes)). diff --git a/learn-you-some-erlang/tests/calc_tests.erl b/learn-you-some-erlang/tests/calc_tests.erl new file mode 100644 index 0000000..4f6f530 --- /dev/null +++ b/learn-you-some-erlang/tests/calc_tests.erl @@ -0,0 +1,6 @@ +-module(calc_tests). +-include_lib("eunit/include/eunit.hrl"). + +%% runs the unit test function defined in calc.erl +all_test() -> + ?assert(ok =:= calc:rpn_test()). diff --git a/learn-you-some-erlang/tests/cases_tests.erl b/learn-you-some-erlang/tests/cases_tests.erl new file mode 100644 index 0000000..5b7bd0b --- /dev/null +++ b/learn-you-some-erlang/tests/cases_tests.erl @@ -0,0 +1,24 @@ +-module(cases_tests). +-include_lib("eunit/include/eunit.hrl"). + +insert_test_() -> + [?_assertEqual([1], cases:insert(1,[])), + ?_assertEqual([1], cases:insert(1,[1])), + ?_assertEqual([1,2], cases:insert(1,[2]))]. + +beach_test_() -> + [?_assertEqual('favorable', cases:beach({celsius, 20})), + ?_assertEqual('favorable', cases:beach({celsius, 45})), + ?_assertEqual('avoid beach', cases:beach({celsius, 46})), + ?_assertEqual('avoid beach', cases:beach({celsius, 19})), + ?_assertEqual('scientifically favorable', cases:beach({kelvin, 293})), + ?_assertEqual('scientifically favorable', cases:beach({kelvin, 318})), + ?_assertEqual('avoid beach', cases:beach({kelvin, 292})), + ?_assertEqual('avoid beach', cases:beach({celsius, 319})), + ?_assertEqual('favorable in the US', + cases:beach({fahrenheit, 68})), + ?_assertEqual('favorable in the US', + cases:beach({fahrenheit, 113})), + ?_assertEqual('avoid beach', cases:beach({fahrenheit, 67})), + ?_assertEqual('avoid beach', cases:beach({fahrenheit, 114})), + ?_assertEqual('avoid beach', cases:beach(cat))]. diff --git a/learn-you-some-erlang/tests/cat_fsm_tests.erl b/learn-you-some-erlang/tests/cat_fsm_tests.erl new file mode 100644 index 0000000..aa4b940 --- /dev/null +++ b/learn-you-some-erlang/tests/cat_fsm_tests.erl @@ -0,0 +1,21 @@ +-module(cat_fsm_tests). +-include_lib("eunit/include/eunit.hrl"). + +cat_fsm_test_() -> + {setup, fun setup/0, fun teardown/1, fun state_test_/1}. + +setup() -> + cat_fsm:start(). + +teardown(Pid) -> + exit(Pid, end_test). + +state_test_(Pid) -> + [?_assertEqual(dont_give_crap, get_state(Pid)), + ?_assertEqual({ok, meh}, cat_fsm:event(Pid, event)), + ?_assertEqual(dont_give_crap, get_state(Pid))]. + +get_state(Pid) -> + List = erlang:process_info(Pid), + {_, {_Mod, Fn, _Arity}} = lists:keyfind(current_function, 1, List), + Fn. diff --git a/learn-you-some-erlang/tests/dog_fsm_tests.erl b/learn-you-some-erlang/tests/dog_fsm_tests.erl new file mode 100644 index 0000000..7217244 --- /dev/null +++ b/learn-you-some-erlang/tests/dog_fsm_tests.erl @@ -0,0 +1,81 @@ +-module(dog_fsm_tests). +-include_lib("eunit/include/eunit.hrl"). + +bark_test_() -> + {foreach, + fun setup_bark/0, + fun teardown/1, + [fun bark_pet_test_/1, fun bark_other_test_/1]}. + +wag_tail_test_() -> + {foreach, + fun setup_wag_tail/0, + fun teardown/1, + [fun wag_pet_test_/1, fun wag_other_test_/1]}. + +sit_test_() -> + {foreach, + fun setup_sit/0, + fun teardown/1, + [fun sit_squirrel_test_/1, fun sit_other_test_/1]}. + +setup_bark() -> + Pid = dog_fsm:start(), + timer:sleep(100), + Pid. + +setup_wag_tail() -> + Pid = dog_fsm:start(), + dog_fsm:pet(Pid), + timer:sleep(100), + Pid. + +setup_sit() -> + Pid = dog_fsm:start(), + dog_fsm:pet(Pid), + dog_fsm:pet(Pid), + timer:sleep(100), + Pid. + +teardown(Pid) -> + exit(Pid, end_test). + +init_test() -> + Pid = dog_fsm:start(), + timer:sleep(100), + ?assertEqual(bark, get_state(Pid)). + +bark_pet_test_(Pid) -> + [?_assertEqual(bark, get_state(Pid)), + ?_assertEqual(pet, dog_fsm:pet(Pid)), + begin timer:sleep(100), ?_assertEqual(wag_tail, get_state(Pid)) end]. + +bark_other_test_(Pid) -> + [?_assertEqual(bark, get_state(Pid)), + ?_assertEqual(squirrel, dog_fsm:squirrel(Pid)), + begin timer:sleep(100), ?_assertEqual(bark, get_state(Pid)) end]. + +wag_pet_test_(Pid) -> + [?_assertEqual(wag_tail, get_state(Pid)), + ?_assertEqual(pet, dog_fsm:pet(Pid)), + begin timer:sleep(100), ?_assertEqual(sit, get_state(Pid)) end]. + +wag_other_test_(Pid) -> + [?_assertEqual(wag_tail, get_state(Pid)), + ?_assertEqual(squirrel, dog_fsm:squirrel(Pid)), + begin timer:sleep(100), ?_assertEqual(wag_tail, get_state(Pid)) end]. + +sit_squirrel_test_(Pid) -> + [?_assertEqual(sit, get_state(Pid)), + ?_assertEqual(squirrel, dog_fsm:squirrel(Pid)), + begin timer:sleep(100), ?_assertEqual(bark, get_state(Pid)) end]. + +sit_other_test_(Pid) -> + [?_assertEqual(sit, get_state(Pid)), + ?_assertEqual(pet, dog_fsm:pet(Pid)), + begin timer:sleep(100), ?_assertEqual(sit, get_state(Pid)) end]. + +get_state(Pid) -> + List = erlang:process_info(Pid), + {_, {_Mod, Fn, _Arity}} = lists:keyfind(current_function, 1, List), + Fn. diff --git a/learn-you-some-erlang/tests/dolphins_tests.erl b/learn-you-some-erlang/tests/dolphins_tests.erl new file mode 100644 index 0000000..670f6a2 --- /dev/null +++ b/learn-you-some-erlang/tests/dolphins_tests.erl @@ -0,0 +1,87 @@ +-module(dolphins_tests). +-include_lib("eunit/include/eunit.hrl"). +%% sorry, this test library is a bit dirty, but it should do +%% the job. + +%% cannot test dolphin1/0 for lack of any results outside of I/O. +-test_warnings([dolphin1/0]). + +dolphin2_test_() -> + [?_assertEqual("How about no?", d2_do_a_flip()), + ?_assertEqual("So long and thanks for all the fish!", + d2_fish())]. + +d2_do_a_flip() -> + Pid = spawn(dolphins, dolphin2, []), + Pid ! Pid ! {self(), do_a_flip}, + %% this function receives a message and that's it + F = fun() -> + receive + M -> M + after 500 -> + error + end + end, + %% receive the first message + Msg1 = F(), + %% only one message should be received. If the second one + %% is anything except 'error' (no message), return an error. + %% otherwise send the message for validation. + case F() of + error -> Msg1; + _ -> error + end. + +d2_fish() -> + Pid = spawn(dolphins, dolphin2, []), + Pid ! Pid ! {self(), fish}, + %% this function receives a message and that's it + F = fun() -> + receive + M -> M + after 500 -> + error + end + end, + %% receive the first message + Msg1 = F(), + %% only one message should be received. If the second one + %% is anything except 'error' (no message), return an error. + %% otherwise send the message for validation. + case F() of + error -> Msg1; + _ -> error + end. + +dolphin3_test_() -> + [?_assertEqual(["How about no?", + "How about no?", + "So long and thanks for all the fish!"], + d3())]. + +d3() -> + Pid = spawn(dolphins, dolphin3, []), + Pid ! Pid ! {self(), do_a_flip}, % both should be received + Pid ! invalid, % should be ignored, but keep the process going + Pid ! {self(), fish}, % should terminate the process + Pid ! {self(), do_a_flip}, % should return nothing + %% this function receives a message and that's it + F = fun() -> + receive + M -> M + after 500 -> + error + end + end, + %% receive the expected messages + Msg1 = F(), + Msg2 = F(), + Msg3 = F(), + Msgs = [Msg1, Msg2, Msg3], + %% Additional messages should now fail. If a message is + %% received, add it to the list and let the test fail, + %% otherwise send the normal message list. + case F() of + error -> Msgs; + M -> Msgs ++ [M] + end. diff --git a/learn-you-some-erlang/tests/exceptions_tests.erl b/learn-you-some-erlang/tests/exceptions_tests.erl new file mode 100644 index 0000000..6adfd24 --- /dev/null +++ b/learn-you-some-erlang/tests/exceptions_tests.erl @@ -0,0 +1,61 @@ +-module(exceptions_tests). +-include_lib("eunit/include/eunit.hrl"). + +throws_test_() -> + [?_assertEqual(ok, exceptions:throws(fun() -> a end)), + ?_assertException(error, {badfun, _} , exceptions:throws(a)), + ?_assertEqual({throw, caught, a}, + exceptions:throws(fun() -> throw(a) end))]. + +errors_test_() -> + [?_assertEqual(ok, exceptions:errors(fun() -> a end)), + ?_assertException(throw, + a, + exceptions:errors(fun() -> throw(a) end)), + ?_assertEqual({error, caught, a}, + exceptions:errors(fun() -> erlang:error(a) end))]. +exits_test_() -> + [?_assertEqual(ok, exceptions:exits(fun() -> a end)), + ?_assertException(error, {badfun, _}, exceptions:exits(a)), + ?_assertEqual({exit, caught, a}, + exceptions:exits(fun() -> exit(a) end))]. + +talk_test() -> + ?assertEqual("blah blah", exceptions:talk()). + +sword_test_() -> + [?_assertException(throw, slice, exceptions:sword(1)), + ?_assertException(error, cut_arm, exceptions:sword(2)), + ?_assertException(exit, cut_leg, exceptions:sword(3)), + ?_assertException(throw, punch, exceptions:sword(4)), + ?_assertException(exit, cross_bridge, exceptions:sword(5))]. + +black_knight_test_() -> + [?_assertEqual("None shall pass.", + exceptions:black_knight(fun exceptions:talk/0)), + ?_assertEqual("It is but a scratch.", + exceptions:black_knight(fun() -> exceptions:sword(1) end)), + ?_assertEqual("I've had worse.", + exceptions:black_knight(fun() -> exceptions:sword(2) end)), + ?_assertEqual("Come on you pansy!", + exceptions:black_knight(fun() -> exceptions:sword(3) end)), + ?_assertEqual("Just a flesh wound.", + exceptions:black_knight(fun() -> exceptions:sword(4) end)), + ?_assertEqual("Just a flesh wound.", + exceptions:black_knight(fun() -> exceptions:sword(5) end))]. + +whoa_test() -> + ?assertEqual({caught, throw, up}, exceptions:whoa()). + +im_impressed_test() -> + ?assertEqual({caught, throw, up}, exceptions:im_impressed()). + +catcher_test_() -> + [?_assertEqual("uh oh", exceptions:catcher(1,0)), + ?_assertEqual(1.0, exceptions:catcher(3,3)), + ?_assertEqual(2.0, exceptions:catcher(6,3))]. + +one_or_two_test_() -> + [?_assertEqual(return, exceptions:one_or_two(1)), + ?_assertEqual(return, catch exceptions:one_or_two(2)), + ?_assertException(throw, return, exceptions:one_or_two(2))]. diff --git a/learn-you-some-erlang/tests/fifo_tests.erl b/learn-you-some-erlang/tests/fifo_tests.erl new file mode 100644 index 0000000..1252a1b --- /dev/null +++ b/learn-you-some-erlang/tests/fifo_tests.erl @@ -0,0 +1,22 @@ +-module(fifo_tests). +-include_lib("eunit/include/eunit.hrl"). + +new_test() -> ?assertEqual({fifo,[],[]}, fifo:new()). + +push_test_() -> + [?_assertEqual({fifo,[1,3],[2,4]}, fifo:push({fifo,[3],[2,4]},1)), + ?_assertEqual({fifo,[2],[]}, fifo:push({fifo,[],[]},2))]. + +pop_test_() -> + [?_assertEqual({3,{fifo,[],[2]}}, fifo:pop({fifo,[],[3,2]})), + ?_assertEqual({3,{fifo,[],[2]}}, fifo:pop({fifo,[2,3],[]})), + ?_assertEqual({3,{fifo,[2,1],[]}},fifo:pop({fifo,[2,1],[3]})), + ?_assertEqual({1,{fifo,[],[2,3]}}, + fifo:pop(fifo:push(fifo:push(fifo:push(fifo:new(),1),2),3))), + ?_assertError('empty fifo', fifo:pop({fifo,[],[]}))]. + +empty_test_() -> + [?_assertEqual(true, fifo:empty(fifo:new())), + ?_assertEqual(false, fifo:empty({fifo,[1],[]})), + ?_assertEqual(false, fifo:empty({fifo,[],[1]})), + ?_assertEqual(false, fifo:empty({fifo,[1],[2]}))]. diff --git a/learn-you-some-erlang/tests/functions_tests.erl b/learn-you-some-erlang/tests/functions_tests.erl new file mode 100644 index 0000000..7973b9e --- /dev/null +++ b/learn-you-some-erlang/tests/functions_tests.erl @@ -0,0 +1,27 @@ +-module(functions_tests). +-include_lib("eunit/include/eunit.hrl"). +-test_warnings([valid_time_test/0]). + +head_test() -> ?assertEqual(1, functions:head([1,2,3,4])). + +second_test() -> ?assertEqual(2, functions:second([1,2,3,4])). + +same_test_() -> + [?_assertEqual(true, functions:same(a,a)), + ?_assertEqual(true, functions:same(12,12)), + ?_assertEqual(false, functions:same(a,b)), + ?_assertEqual(false, functions:same(12.0, 12))]. + +%% no clean way to test valid_time's io stuff, so this one is p. much the +%% same thing as the main objective was to test pattern matching. +%% io:format should be used as least as possible to do testing :( +valid_time({_Date = {_Y,_M,_D}, _Time = {_H,_Min,_S}}) -> + matches; +valid_time(_) -> + nomatch. + +valid_time_test_() -> + [?_assertEqual(matches, valid_time({{2011,09,06},{09,04,43}})), + ?_assertEqual(nomatch, valid_time({{2011,09,06},{09,04}}))]. + + diff --git a/learn-you-some-erlang/tests/guards_tests.erl b/learn-you-some-erlang/tests/guards_tests.erl new file mode 100644 index 0000000..d908bcf --- /dev/null +++ b/learn-you-some-erlang/tests/guards_tests.erl @@ -0,0 +1,22 @@ +-module(guards_tests). +-include_lib("eunit/include/eunit.hrl"). + +old_enough_test_() -> + [?_assertEqual(true, guards:old_enough(16)), + ?_assertEqual(false, guards:old_enough(15)), + ?_assertEqual(false, guards:old_enough(-16))]. + +right_age_test_() -> + [?_assertEqual(true, guards:right_age(16)), + ?_assertEqual(true, guards:right_age(104)), + ?_assertEqual(true, guards:right_age(50)), + ?_assertEqual(false, guards:right_age(15)), + ?_assertEqual(false, guards:right_age(105))]. + +wrong_age_test_() -> + [?_assertEqual(false, guards:wrong_age(16)), + ?_assertEqual(false, guards:wrong_age(104)), + ?_assertEqual(false, guards:wrong_age(50)), + ?_assertEqual(true, guards:wrong_age(15)), + ?_assertEqual(true, guards:wrong_age(105))]. + diff --git a/learn-you-some-erlang/tests/hhfuns_tests.erl b/learn-you-some-erlang/tests/hhfuns_tests.erl new file mode 100644 index 0000000..c5c27cf --- /dev/null +++ b/learn-you-some-erlang/tests/hhfuns_tests.erl @@ -0,0 +1,95 @@ +-module(hhfuns_tests). +-include_lib("eunit/include/eunit.hrl"). + +one_test() -> + ?assertEqual(1, hhfuns:one()). + +two_test() -> + ?assertEqual(2, hhfuns:two()). + +add_test_() -> + [?_assertEqual(3, hhfuns:add(fun hhfuns:one/0, fun hhfuns:two/0)), + ?_assertError({badfun, _}, hhfuns:add(1,2)), + ?_assertEqual(2, hhfuns:add(fun hhfuns:one/0, fun hhfuns:one/0))]. + +increment_test() -> + ?assertEqual([1,2,3], hhfuns:increment([0,1,2])). + +decrement_test() -> + ?assertEqual([1,2,3], hhfuns:decrement([2,3,4])). + +map_test_() -> + [?_assertEqual([1,2,3], hhfuns:map(fun hhfuns:incr/1, [0,1,2])), + ?_assertEqual([1,2,3], hhfuns:map(fun hhfuns:decr/1, [2,3,4]))]. + +bases_test_() -> + [?_assertEqual(12, hhfuns:base1(3)), + ?_assertError({badmatch, _}, hhfuns:base2()), + ?_assertEqual(2, hhfuns:base3())]. + +closure_test() -> + ?assertEqual("a/0's password is pony", hhfuns:b(hhfuns:a())). + +even_test_() -> + [?_assertEqual([], hhfuns:even([])), + ?_assertEqual([], hhfuns:even([3,5,7])), + ?_assertEqual([2,4], hhfuns:even([1,2,3,4]))]. + +old_test_() -> + L = [{male,45},{female,67},{male,66},{female,12},{unkown,174},{male,74}], + [?_assertEqual([{male,66},{male,74}], hhfuns:old_men(L)), + ?_assertEqual([], hhfuns:old_men([{male,45}, {female, -54}])), + ?_assertEqual([], hhfuns:old_men([]))]. + +filter_test_() -> + L = [{male,45},{female,67},{male,66},{female,12},{unkown,174},{male,74}], + IsEven = fun(X) -> X rem 2 == 0 end, + IsOldMale = fun({Gender, Age}) -> Gender == male andalso Age > 60 end, + [?_assertEqual([], hhfuns:filter(IsEven, [])), + ?_assertEqual([], hhfuns:filter(IsEven, [3,5,7])), + ?_assertEqual([2,4], hhfuns:filter(IsEven, [1,2,3,4])), + ?_assertEqual([{male,66},{male,74}], hhfuns:filter(IsOldMale, L)), + ?_assertEqual([], hhfuns:filter(IsOldMale, [{male,45}, {female, -54}])), + ?_assertEqual([], hhfuns:filter(IsOldMale, []))]. + +max_test_() -> + [?_assertEqual(3, hhfuns:max([1,2,3])), + ?_assertEqual(-1, hhfuns:max([-10,-1,-5.5])), + ?_assertError(function_clause, hhfuns:max([]))]. + +min_test_() -> + [?_assertEqual(0, hhfuns:min([1,2,0,3])), + ?_assertEqual(-10, hhfuns:min([-10,-1,-5.5])), + ?_assertError(function_clause, hhfuns:min([]))]. + +sum_test_() -> + [?_assertEqual(0, hhfuns:sum([])), + ?_assertEqual(6, hhfuns:sum([1,2,3]))]. + +fold_test_() -> + [H|T] = [1,7,3,5,9,0,2,3], + [?_assertEqual(9, + hhfuns:fold(fun(A,B) when A > B -> A; (_,B) -> B end, H, T)), + ?_assertEqual(0, + hhfuns:fold(fun(A,B) when A < B -> A; (_,B) -> B end, H, T)), + ?_assertEqual(21, hhfuns:fold(fun(A,B) -> A + B end, 0, lists:seq(1,6)))]. + +reverse_test_() -> + [?_assertEqual([3,2,1], hhfuns:reverse([1,2,3])), + ?_assertEqual([], hhfuns:reverse([]))]. + +map2_test_() -> + [?_assertEqual([1,2,3], hhfuns:map2(fun hhfuns:incr/1, [0,1,2])), + ?_assertEqual([1,2,3], hhfuns:map2(fun hhfuns:decr/1, [2,3,4]))]. + + +filter2_test_() -> + L = [{male,45},{female,67},{male,66},{female,12},{unkown,174},{male,74}], + IsEven = fun(X) -> X rem 2 == 0 end, + IsOldMale = fun({Gender, Age}) -> Gender == male andalso Age > 60 end, + [?_assertEqual([], hhfuns:filter2(IsEven, [])), + ?_assertEqual([], hhfuns:filter2(IsEven, [3,5,7])), + ?_assertEqual([2,4], hhfuns:filter2(IsEven, [1,2,3,4])), + ?_assertEqual([{male,66},{male,74}], hhfuns:filter2(IsOldMale, L)), + ?_assertEqual([], hhfuns:filter2(IsOldMale, [{male,45}, {female, -54}])), + ?_assertEqual([], hhfuns:filter2(IsOldMale, []))]. diff --git a/learn-you-some-erlang/tests/kitchen_tests.erl b/learn-you-some-erlang/tests/kitchen_tests.erl new file mode 100644 index 0000000..821f60c --- /dev/null +++ b/learn-you-some-erlang/tests/kitchen_tests.erl @@ -0,0 +1,118 @@ +-module(kitchen_tests). +-include_lib("eunit/include/eunit.hrl"). + +fridge1_test_() -> + {"Tests fridge1 although the function is never run in the text", + {foreach, + fun() -> spawn(kitchen, fridge1, []) end, + fun(Pid) -> exit(Pid, kill) end, + [fun fridge1_store/1, + fun fridge1_take/1] + } + }. + +fridge1_store(Pid) -> + Pid ! {self(), {store, item}}, + Reply = receive_or_timeout(), + [?_assertEqual({Pid, ok}, Reply)]. + +fridge1_take(Pid) -> + Pid ! {self(), {take, item}}, + Reply = receive_or_timeout(), + [?_assertEqual({Pid, not_found}, Reply)]. + + +fridge2_test_() -> + {"Tests fridge2", + {foreach, + fun() -> spawn(kitchen, fridge2, [[]]) end, + fun(Pid) -> exit(Pid, kill) end, + [fun fridge2_store/1, + fun fridge2_take_nostore/1, + fun fridge2_take_stored/1] + } + }. + +fridge2_store(Pid) -> + Pid ! {self(), {store, item}}, + Pid ! {self(), {store, item2}}, + Reply1 = receive_or_timeout(), + Reply2 = receive_or_timeout(), + [?_assertEqual({Pid, ok}, Reply1), + ?_assertEqual({Pid, ok}, Reply2)]. + +fridge2_take_nostore(Pid) -> + Pid ! {self(), {take, item}}, + Reply = receive_or_timeout(), + [?_assertEqual({Pid, not_found}, Reply)]. + +fridge2_take_stored(Pid) -> + Pid ! {self(), {store, item}}, + _ = receive_or_timeout(), % flush the 'ok' msg + Pid ! {self(), {take, item}}, + Pid ! {self(), {take, item}}, % check if the food is removed + R1 = receive_or_timeout(), + R2 = receive_or_timeout(), + [?_assertEqual({Pid, {ok, item}}, R1), + ?_assertEqual({Pid, not_found}, R2)]. + + +abstractions_test_() -> + {"Tests the abstraction function we added to the script", + [{"Basic take/2 and store/2 abstractions. Can't test the hanging.", + {foreach, + fun() -> spawn(kitchen, fridge2, [[]]) end, + fun(Pid) -> exit(Pid, kill) end, + [fun store/1, + fun take/1] + }}, + {"Start/1 abstraction tests. Reuses the take/2 and store/2 tests.", + {foreach, + fun() -> kitchen:start([]) end, + fun(Pid) -> exit(Pid, kill) end, + [fun store/1, + fun take/1] + }}, + {"take2/2 and store2/2 tests.", + {timeout, 10, + {foreach, + fun() -> kitchen:start([]) end, + fun(Pid) -> exit(Pid, kill) end, + [fun store2/1, + fun take2/1] + } + }} + ] + }. + +store(Pid) -> + [?_assertEqual(ok, kitchen:store(Pid, item))]. + +take(Pid) -> + kitchen:store(Pid, item), + R1 = kitchen:take(Pid, item), + R2 = kitchen:take(Pid, item), + [?_assertEqual({ok, item}, R1), + ?_assertEqual(not_found, R2)]. + +store2(Pid) -> + R1 = kitchen:store2(c:pid(0,7,0), item), + R2 = kitchen:store2(Pid, item), + [?_assertEqual(timeout, R1), + ?_assertEqual(ok, R2)]. + +take2(Pid) -> + kitchen:store2(Pid, item), + R1 = kitchen:take2(c:pid(0,7,0), item), + R2 = kitchen:take2(Pid, item), + R3 = kitchen:take2(Pid, item), + [?_assertEqual(timeout, R1), + ?_assertEqual({ok, item}, R2), + ?_assertEqual(not_found, R3)]. + +receive_or_timeout() -> + receive + M -> M + after 1000 -> + timeout + end. diff --git a/learn-you-some-erlang/tests/kitty_gen_server_tests.erl b/learn-you-some-erlang/tests/kitty_gen_server_tests.erl new file mode 100644 index 0000000..e167158 --- /dev/null +++ b/learn-you-some-erlang/tests/kitty_gen_server_tests.erl @@ -0,0 +1,19 @@ +-module(kitty_gen_server_tests). +-record(cat, {name, color=green, description}). % stolen from kitty_gen_server.erl +-include_lib("eunit/include/eunit.hrl"). +-define(CAT1, #cat{name=a, color=b, description=c}). +-define(CAT2, #cat{name=d, color=e, description=f}). + +order_test() -> + {ok, Pid} = kitty_gen_server:start_link(), + ?assertEqual(?CAT1, kitty_gen_server:order_cat(Pid, a, b, c)), + ?assertEqual(?CAT2, kitty_gen_server:order_cat(Pid, d, e, f)), + ?assertEqual(ok, kitty_gen_server:close_shop(Pid)). + +return_test() -> + {ok, Pid} = kitty_gen_server:start_link(), + ?assertEqual(ok, kitty_gen_server:return_cat(Pid, ?CAT1)), + ?assertEqual(?CAT1, kitty_gen_server:order_cat(Pid, d, e, f)), + ?assertEqual(?CAT2, kitty_gen_server:order_cat(Pid, d, e, f)), + ?assertEqual(ok, kitty_gen_server:close_shop(Pid)). + diff --git a/learn-you-some-erlang/tests/kitty_server2_tests.erl b/learn-you-some-erlang/tests/kitty_server2_tests.erl new file mode 100644 index 0000000..a41594a --- /dev/null +++ b/learn-you-some-erlang/tests/kitty_server2_tests.erl @@ -0,0 +1,23 @@ +-module(kitty_server2_tests). +-record(cat, {name, color=green, description}). % stolen from kitty_server2.erl +-include_lib("eunit/include/eunit.hrl"). +-define(CAT1, #cat{name=a, color=b, description=c}). +-define(CAT2, #cat{name=d, color=e, description=f}). + +order_test() -> + Pid = kitty_server2:start_link(), + ?assertEqual(?CAT1, kitty_server2:order_cat(Pid, a, b, c)), + ?assertEqual(?CAT2, kitty_server2:order_cat(Pid, d, e, f)), + ?assertEqual(ok, kitty_server2:close_shop(Pid)). + +return_test() -> + Pid = kitty_server2:start_link(), + ?assertEqual(ok, kitty_server2:return_cat(Pid, ?CAT1)), + ?assertEqual(?CAT1, kitty_server2:order_cat(Pid, d, e, f)), + ?assertEqual(?CAT2, kitty_server2:order_cat(Pid, d, e, f)), + ?assertEqual(ok, kitty_server2:close_shop(Pid)). + +close_noproc_test() -> + DeadPid = spawn_link(fun() -> ok end), + timer:sleep(100), + ?assertError(noproc, kitty_server2:close_shop(DeadPid)). diff --git a/learn-you-some-erlang/tests/kitty_server_tests.erl b/learn-you-some-erlang/tests/kitty_server_tests.erl new file mode 100644 index 0000000..47b2561 --- /dev/null +++ b/learn-you-some-erlang/tests/kitty_server_tests.erl @@ -0,0 +1,23 @@ +-module(kitty_server_tests). +-record(cat, {name, color=green, description}). % stolen from kitty_server.erl +-include_lib("eunit/include/eunit.hrl"). +-define(CAT1, #cat{name=a, color=b, description=c}). +-define(CAT2, #cat{name=d, color=e, description=f}). + +order_test() -> + Pid = kitty_server:start_link(), + ?assertEqual(?CAT1, kitty_server:order_cat(Pid, a, b, c)), + ?assertEqual(?CAT2, kitty_server:order_cat(Pid, d, e, f)), + ?assertEqual(ok, kitty_server:close_shop(Pid)). + +return_test() -> + Pid = kitty_server:start_link(), + ?assertEqual(ok, kitty_server:return_cat(Pid, ?CAT1)), + ?assertEqual(?CAT1, kitty_server:order_cat(Pid, d, e, f)), + ?assertEqual(?CAT2, kitty_server:order_cat(Pid, d, e, f)), + ?assertEqual(ok, kitty_server:close_shop(Pid)). + +close_noproc_test() -> + DeadPid = spawn_link(fun() -> ok end), + timer:sleep(100), + ?assertError(noproc, kitty_server:close_shop(DeadPid)). diff --git a/learn-you-some-erlang/tests/linkmon_tests.erl b/learn-you-some-erlang/tests/linkmon_tests.erl new file mode 100644 index 0000000..a04ee27 --- /dev/null +++ b/learn-you-some-erlang/tests/linkmon_tests.erl @@ -0,0 +1,43 @@ +-module(linkmon_tests). +-include_lib("eunit/include/eunit.hrl"). + +myproc_test_() -> + {timeout, + 7, + ?_assertEqual({'EXIT', reason}, + catch linkmon:myproc())}. + +chain_test_() -> + {timeout, + 3, + ?_assertEqual(ok, chain_proc())}. + +chain_proc() -> + process_flag(trap_exit, true), + link(spawn(linkmon, chain, [3])), + receive + {'EXIT', _, "chain dies here"} -> ok + end. + +critic1_test_() -> + Critic = linkmon:start_critic(), + A = linkmon:judge(Critic, "Genesis", "The Lambda Lies Down on Broadway"), + exit(Critic, solar_storm), + B = linkmon:judge(Critic, "Genesis", "A trick of the Tail Recursion"), + [?_assertEqual("They are terrible!", A), + ?_assertEqual(timeout, B)]. + +critic2_test_() -> + catch unregister(critic), + linkmon:start_critic2(), + timer:sleep(200), + A = linkmon:judge2("The Doors", "Light my Firewall"), + exit(whereis(critic), kill), + timer:sleep(200), + B = linkmon:judge2("Rage Against the Turing Machine", "Unit Testify"), + exit(whereis(critic), shutdown), + timer:sleep(200), + C = (catch linkmon:judge2("a", "b")), + [?_assertEqual("They are terrible!", A), + ?_assertEqual("They are great!", B), + ?_assertMatch({'EXIT', {badarg, _}}, C)]. diff --git a/learn-you-some-erlang/tests/multiproc_tests.erl b/learn-you-some-erlang/tests/multiproc_tests.erl new file mode 100644 index 0000000..1020324 --- /dev/null +++ b/learn-you-some-erlang/tests/multiproc_tests.erl @@ -0,0 +1,28 @@ +-module(multiproc_tests). +-include_lib("eunit/include/eunit.hrl"). + +%% sleep's implementation is copy/pasted from the timer module. +%% not much to test to be safe. +sleep_test_() -> + [?_assertEqual(ok, multiproc:sleep(10))]. + +flush_test_() -> + {spawn, + [fun() -> + self() ! a, + self() ! b, + ok = multiproc:flush(), + self() ! c, + [?assertEqual(receive M -> M end, c)] + end]}. + +priority_test_() -> + {spawn, + [fun() -> + self() ! {15, high}, + self() ! {7, low}, + self() ! {1, low}, + self() ! {17, high}, + [?assertEqual([high, high, low, low], + multiproc:important())] + end]}. diff --git a/learn-you-some-erlang/tests/musicians_tests.erl b/learn-you-some-erlang/tests/musicians_tests.erl new file mode 100644 index 0000000..b1b4254 --- /dev/null +++ b/learn-you-some-erlang/tests/musicians_tests.erl @@ -0,0 +1,56 @@ +%% WARNING: THESE TESTS TAKE A LONG TIME TO RUN +-module(musicians_tests). +-include_lib("eunit/include/eunit.hrl"). +-define(INSTRUMENTS, [a,b,c,d,e,f,g,h]). + +rand_name_test_() -> + {"Make sure that random names are generated", + {setup, + fun setup_many_good/0, + fun teardown_many/1, + fun test_names/1}}. + +eventual_crash_test_() -> + {"Checks that bad musicians die at some point, while" + "good ones don't", + {inparallel, + [{timeout, 20000, crash()}, + {timeout, 20000, nocrash()}]}}. + +crash() -> + {ok, Pid} = musicians:start_link(drum, bad), + Ref = erlang:monitor(process, Pid), + unlink(Pid), + Rec = receive + {'DOWN', Ref, process, Pid, _R} -> ok + after 19000 -> timeout + end, + ?_assertEqual(ok, Rec). + +nocrash() -> + {ok, Pid} = musicians:start_link(carhorn, good), + Ref = erlang:monitor(process, Pid), + unlink(Pid), + Rec = receive + {'DOWN', Ref, process, Pid, _R} -> ok + after 19000 -> musicians:stop(carhorn), timeout + end, + ?_assertEqual(timeout, Rec). + +setup_many_good() -> + [element(2,musicians:start_link(X, good)) || + X <- ?INSTRUMENTS]. + +teardown_many(_) -> + [musicians:stop(X) || X <- ?INSTRUMENTS]. + +test_names(Musicians) -> + Names = [find_name(M) || M <- Musicians], + SetNames = ordsets:to_list(ordsets:from_list(Names)), + ?_assert(2 < length(SetNames)). % totally arbitrary ratio + +find_name(Inst) -> + {status, _Pid, _, [_Dict, _Status, _Ancestor, _, + [_Header, _, + {data, [{"State", {state, Name, _, _}}]}]]} = sys:get_status(Inst), + Name. diff --git a/learn-you-some-erlang/tests/oop_tests.erl b/learn-you-some-erlang/tests/oop_tests.erl new file mode 100644 index 0000000..6ea444a --- /dev/null +++ b/learn-you-some-erlang/tests/oop_tests.erl @@ -0,0 +1,16 @@ +-module(oop_tests). +-include_lib("eunit/include/eunit.hrl"). + +shell_test_() -> + Bird = oop:animal("Bird"), + Dog = oop:dog("Raptor-Dog"), + Cat = oop:cat("Sgt. McMittens"), + [?_assertEqual("living thing", Bird(type)), + ?_assertEqual("Bird eats worm", Bird({eat, "worm"})), + ?_assertEqual("Raptor-Dog says: Woof!", Dog(talk)), + ?_assertEqual("Raptor-Dog", Dog(name)), + ?_assertEqual("cat", Cat(type)), + ?_assertEqual("Raptor-Dog chases a cat named Sgt. McMittens around", + Dog({chase, Cat})), + ?_assertEqual("I'm sorry Dave, I can't do that.", Cat({play, "yarn"}))]. + diff --git a/learn-you-some-erlang/tests/records_tests.erl b/learn-you-some-erlang/tests/records_tests.erl new file mode 100644 index 0000000..327120f --- /dev/null +++ b/learn-you-some-erlang/tests/records_tests.erl @@ -0,0 +1,46 @@ +-module(records_tests). +-include_lib("eunit/include/eunit.hrl"). + +first_robot_test_() -> + ?_assertEqual(records:first_robot(), + {robot, + "Mechatron", + handmade, + undefined, + ["Moved by a small man inside"]}). + +car_factory_test_() -> + ?_assertEqual(records:car_factory("Jokeswagen"), + {robot, + "Jokeswagen", + industrial, + "building cars", + []}). + +repairman_test_() -> + ?_assertEqual(records:repairman({robot, + "Ulbert", + industrial, + ["trying to have feelings"], + []}), + {repaired, {robot, + "Ulbert", + industrial, + ["trying to have feelings"], + ["Repaired by repairman"]}}). + +admin_panel_test_() -> + [?_assertEqual(records:admin_panel({user, 1, "ferd", admin, 96}), + "ferd is allowed!"), + ?_assertEqual(records:admin_panel({user, 2, "you", users, 66}), + "you is not allowed")]. + +adult_section_test_() -> + [?_assertEqual(records:adult_section({user, 21, "Bill", users, 72}), + allowed), + ?_assertEqual(records:adult_section({user, 22, "Noah", users, 13}), + forbidden)]. + +included_test_() -> + ?_assertEqual(records:included(), + {included, "Some value", "yeah!", undefined}). diff --git a/learn-you-some-erlang/tests/recursive_tests.erl b/learn-you-some-erlang/tests/recursive_tests.erl new file mode 100644 index 0000000..554c691 --- /dev/null +++ b/learn-you-some-erlang/tests/recursive_tests.erl @@ -0,0 +1,109 @@ +-module(recursive_tests). +-include_lib("eunit/include/eunit.hrl"). + +%% those were not in the module, but yeah + +fac_test_() -> + [?_assert(24 == recursive:fac(4)), + ?_assert(1 == recursive:fac(0)), + ?_assert(1 == recursive:fac(1)), + ?_assertError(function_clause, recursive:fac(-1))]. + +tail_fac_test_() -> + [?_assert(recursive:fac(4) == recursive:tail_fac(4)), + ?_assert(recursive:fac(0) == recursive:tail_fac(0)), + ?_assert(recursive:fac(1) == recursive:tail_fac(1)), + ?_assertError(function_clause, recursive:tail_fac(-1))]. + +len_test_() -> + [?_assert(1 == recursive:len([a])), + ?_assert(0 == recursive:len([])), + ?_assert(5 == recursive:len([1,2,3,4,5]))]. + +tail_len_test_() -> + [?_assert(recursive:len([a]) == recursive:tail_len([a])), + ?_assert(recursive:len([]) == recursive:tail_len([])), + ?_assert(recursive:len([1,2,3,4,5]) == recursive:tail_len([1,2,3,4,5]))]. + +duplicate_test_() -> + [?_assert([] == recursive:duplicate(0,a)), + ?_assert([a] == recursive:duplicate(1,a)), + ?_assert([a,a,a] == recursive:duplicate(3,a))]. + +tail_duplicate_test_() -> + [?_assert(recursive:tail_duplicate(0,a) == recursive:duplicate(0,a)), + ?_assert(recursive:tail_duplicate(1,a) == recursive:duplicate(1,a)), + ?_assert(recursive:tail_duplicate(3,a) == recursive:duplicate(3,a))]. + +reverse_test_() -> + [?_assert([] == recursive:reverse([])), + ?_assert([1] == recursive:reverse([1])), + ?_assert([3,2,1] == recursive:reverse([1,2,3]))]. + +tail_reverse_test_() -> + [?_assertEqual(recursive:tail_reverse([]), + recursive:reverse([])), + ?_assertEqual(recursive:tail_reverse([1]), + recursive:reverse([1])), + ?_assertEqual(recursive:tail_reverse([1,2,3]), + recursive:reverse([1,2,3]))]. + +sublist_test_() -> + [?_assert([] == recursive:sublist([1,2,3],0)), + ?_assert([1,2] == recursive:sublist([1,2,3],2)), + ?_assert([] == recursive:sublist([], 4))]. + +tail_sublist_test_() -> + [?_assertEqual(recursive:tail_sublist([1,2,3],0), + recursive:sublist([1,2,3],0)), + ?_assertEqual(recursive:tail_sublist([1,2,3],2), + recursive:sublist([1,2,3],2)), + ?_assertEqual(recursive:tail_sublist([], 4), + recursive:sublist([], 4))]. + +zip_test_() -> + [?_assert([{a,1},{b,2},{c,3}] == recursive:zip([a,b,c],[1,2,3])), + ?_assert([] == recursive:zip([],[])), + ?_assertError(function_clause, recursive:zip([1],[1,2]))]. + +lenient_zip_test_() -> + [?_assertEqual([{a,1},{b,2},{c,3}], + recursive:lenient_zip([a,b,c],[1,2,3])), + ?_assert([] == recursive:lenient_zip([],[])), + ?_assert([{a,1}] == recursive:lenient_zip([a],[1,2]))]. + +%% exercises! +tail_zip_test_() -> + [?_assertEqual(recursive:tail_zip([a,b,c],[1,2,3]), + recursive:zip([a,b,c],[1,2,3])), + ?_assertEqual(recursive:tail_zip([],[]), + recursive:zip([],[])), + ?_assertError(function_clause, recursive:tail_zip([1],[1,2]))]. + +tail_lenient_zip_test_() -> + [?_assertEqual(recursive:tail_lenient_zip([a,b,c],[1,2,3]), + recursive:lenient_zip([a,b,c],[1,2,3])), + ?_assertEqual(recursive:tail_lenient_zip([],[]), + recursive:lenient_zip([],[])), + ?_assertEqual(recursive:tail_lenient_zip([a],[1,2]), + recursive:lenient_zip([a],[1,2]))]. + +%% quick, sort! +quicksort_test_() -> + [?_assert([] == recursive:quicksort([])), + ?_assert([1] == recursive:quicksort([1])), + ?_assert([1,2,2,4,6] == recursive:quicksort([4,2,6,2,1])), + ?_assert(" JRaceeinqqsuu" == recursive:quicksort("Jacques Requin"))]. + +lc_quicksort_test_() -> + [?_assert([] == recursive:lc_quicksort([])), + ?_assert([1] == recursive:lc_quicksort([1])), + ?_assert([1,2,2,4,6] == recursive:lc_quicksort([4,2,6,2,1])), + ?_assert(" JRaceeinqqsuu" == recursive:lc_quicksort("Jacques Requin"))]. + +bestest_qsort_test_() -> + [?_assert([] == recursive:bestest_qsort([])), + ?_assert([1] == recursive:bestest_qsort([1])), + ?_assert([1,2,2,4,6] == recursive:bestest_qsort([4,2,6,2,1])), + ?_assert(" JRaceeinqqsuu" == recursive:bestest_qsort("Jacques Requin"))]. + diff --git a/learn-you-some-erlang/tests/road_tests.erl b/learn-you-some-erlang/tests/road_tests.erl new file mode 100644 index 0000000..e99eeff --- /dev/null +++ b/learn-you-some-erlang/tests/road_tests.erl @@ -0,0 +1,33 @@ +-module(road_tests). +-include_lib("eunit/include/eunit.hrl"). +-test_warnings([main/1]). + +group_vals_test_() -> + [?_assertEqual([{a,b,x},{a,b,x}], road:group_vals([a,b,x,a,b,x],[])), + ?_assertEqual([], road:group_vals([],[])), + ?_assertError(function_clause, road:group_vals([a,b,x,a],[]))]. + +parse_map_test_() -> + [?_assertEqual([], road:parse_map("")), + ?_assertEqual([], road:parse_map(<<"">>)), + ?_assertEqual([{10,5,4}], road:parse_map("10 5 4")), + ?_assertEqual([{10,5,4},{1,2,3}], road:parse_map("10 5 4 1 2 3")), + ?_assertEqual([{10,5,4}], road:parse_map(<<"10 5 4">>)), + ?_assertEqual([{10,5,4}], road:parse_map("10\t5\n4")), + ?_assertEqual([{10,5,4},{1,2,3}], + road:parse_map("10\r\n5 4 1\t\t2\r3"))]. + +%% little testing required on this one, the optimal_path tests will +%% do it in a hidden manner. +shortest_step_test_() -> + [?_assertEqual({{1,[{a,1}]},{2,[{x,1},{a,1}]}}, + road:shortest_step({1,8,1},{{0,[]},{0,[]}}))]. + +optimal_path_test_() -> + [?_assertEqual([{b,10},{x,30},{a,5},{x,20},{b,2},{b,8}], + road:optimal_path( + road:parse_map("50 10 30 5 90 20 40 2 24 10 8 0"))), + ?_assertEqual([{a,1},{a,1},{a,1}], + road:optimal_path([{1,10,2},{1,3,3},{1,2,0}])), + ?_assertEqual([{a,1},{x,1},{b,1},{x,1},{a,1}], + road:optimal_path([{1,3,1},{4,1,1},{1,6,1}]))]. diff --git a/learn-you-some-erlang/tests/tree_tests.erl b/learn-you-some-erlang/tests/tree_tests.erl new file mode 100644 index 0000000..5a2633b --- /dev/null +++ b/learn-you-some-erlang/tests/tree_tests.erl @@ -0,0 +1,64 @@ +-module(tree_tests). +-include_lib("eunit/include/eunit.hrl"). + +empty_test() -> + ?assert({node, 'nil'} =:= tree:empty()). + +%% oh god this gets ugly +insert_test_() -> + T1 = tree:insert(a, "a", tree:empty()), + T2 = tree:insert(c, "c", T1), + T3 = tree:insert(n, "n", + tree:insert(z, "z", + tree:insert(t, "t", + tree:insert(x, "x", T2)))), + [?_assertEqual(T1, {node, {a,"a",{node,nil},{node,nil}}}), + ?_assertEqual(T2, {node, {a,"a", + {node,nil}, + {node, {c,"c",{node,nil},{node,nil}}}}}), + ?_assertEqual(T3, {node, {a,"a", + {node, nil}, + {node, {c,"c", + {node, nil}, + {node, {x,"x", + {node, {t,"t", + {node, {n,"n", + {node,nil}, + {node,nil}}}, + {node, nil}}}, + {node, {z,"z", + {node,nil}, + {node,nil}}}}}}}}})]. +%% not as bad! +lookup_test_() -> + T = tree:insert(x, "x", + tree:insert(t, "t", + tree:insert(z, "z", + tree:insert(n, "n", + tree:insert(c, "c", + tree:insert(a, "a", tree:empty())))))), + [?_assert({ok,"t"} == tree:lookup(t,T)), + ?_assert(undefined == tree:lookup(21, T)), + ?_assert(undefined == tree:lookup(a, tree:empty()))]. + +%% done with both insert and lookup +update_test_() -> + T1 = tree:insert(x, "x", + tree:insert(t, "t", + tree:insert(z, "z", + tree:insert(n, "n", + tree:insert(c, "c", + tree:insert(a, "a", tree:empty())))))), + T2 = tree:insert(x, "X", T1), + [?_assertEqual({ok, "x"}, tree:lookup(x, T1)), + ?_assertEqual({ok, "X"}, tree:lookup(x, T2))]. + +has_value_test_() -> + T1 = tree:insert(x, "x", + tree:insert(t, "t", + tree:insert(z, "z", + tree:insert(n, "n", + tree:insert(c, "c", + tree:insert(a, "a", tree:empty())))))), + [?_assertEqual(true, tree:has_value("z", T1)), + ?_assertEqual(false,tree:has_value("Z", T1))]. diff --git a/learn-you-some-erlang/tests/useless_tests.erl b/learn-you-some-erlang/tests/useless_tests.erl new file mode 100644 index 0000000..8f174be --- /dev/null +++ b/learn-you-some-erlang/tests/useless_tests.erl @@ -0,0 +1,18 @@ +-module(useless_tests). +-include_lib("eunit/include/eunit.hrl"). +-test_warnings([hello_test/0]). + +add_test_() -> + [?_assertEqual(-5, useless:add(-3, -2)), + ?_assertEqual(4, useless:add(2, 2)), + ?_assertEqual(2.5, useless:add(2.0, 0.5)), + ?_assertEqual(1, useless:add(-3, 4))]. + +hello_test() -> + ok. % no test possible for I/O. Curse you, side effects! + +greet_and_add_test_() -> + [?_assertEqual(useless:greet_and_add_two(-3), useless:add(-3, 2)), + ?_assertEqual(useless:greet_and_add_two(2), useless:add(2, 2)), + ?_assertEqual(useless:greet_and_add_two(0.5), useless:add(2, 0.5)), + ?_assertEqual(useless:greet_and_add_two(-3), useless:add(-3, 2))]. diff --git a/learn-you-some-erlang/tests/what_the_if_tests.erl b/learn-you-some-erlang/tests/what_the_if_tests.erl new file mode 100644 index 0000000..13465e9 --- /dev/null +++ b/learn-you-some-erlang/tests/what_the_if_tests.erl @@ -0,0 +1,17 @@ +-module(what_the_if_tests). +-include_lib("eunit/include/eunit.hrl"). + +heh_fine_test() -> + ?assertException(error, if_clause, what_the_if:heh_fine()). + +oh_god_test_() -> + [?_assertEqual(might_succeed, what_the_if:oh_god(2)), + ?_assertEqual(always_does, what_the_if:oh_god(3))]. + +help_me_test_() -> + [?_assertEqual({cat, "says meow!"}, what_the_if:help_me(cat)), + ?_assertEqual({beef, "says mooo!"}, what_the_if:help_me(beef)), + ?_assertEqual({dog, "says bark!"}, what_the_if:help_me(dog)), + ?_assertEqual({tree, "says bark!"}, what_the_if:help_me(tree)), + ?_assertEqual({"other", "says fgdadfgna!"}, what_the_if:help_me("other")), + ?_assertEqual({5, "says fgdadfgna!"}, what_the_if:help_me(5))]. diff --git a/learn-you-some-erlang/trade/trade_calls.erl b/learn-you-some-erlang/trade/trade_calls.erl new file mode 100644 index 0000000..e72c891 --- /dev/null +++ b/learn-you-some-erlang/trade/trade_calls.erl @@ -0,0 +1,144 @@ +-module(trade_calls). +-compile(export_all). + +%% test a little bit of everything and also deadlocks on ready state +%% -- leftover messages possible on race conditions on ready state +main_ab() -> + S = self(), + PidCliA = spawn(fun() -> a(S) end), + receive PidA -> PidA end, + spawn(fun() -> b(PidA, PidCliA) end). + +a(Parent) -> + {ok, Pid} = trade_fsm:start_link("Carl"), + Parent ! Pid, + io:format("Spawned Carl: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(800), + trade_fsm:accept_trade(Pid), + timer:sleep(400), + io:format("~p~n",[trade_fsm:ready(Pid)]), + timer:sleep(1000), + trade_fsm:make_offer(Pid, "horse"), + trade_fsm:make_offer(Pid, "sword"), + timer:sleep(1000), + io:format("a synchronizing~n"), + sync2(), + trade_fsm:ready(Pid), + timer:sleep(200), + trade_fsm:ready(Pid), + timer:sleep(1000). + +b(PidA, PidCliA) -> + {ok, Pid} = trade_fsm:start_link("Jim"), + io:format("Spawned Jim: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(500), + trade_fsm:trade(Pid, PidA), + trade_fsm:make_offer(Pid, "boots"), + timer:sleep(200), + trade_fsm:retract_offer(Pid, "boots"), + timer:sleep(500), + trade_fsm:make_offer(Pid, "shotgun"), + timer:sleep(1000), + io:format("b synchronizing~n"), + sync1(PidCliA), + trade_fsm:make_offer(Pid, "horse"), %% race condition! + trade_fsm:ready(Pid), + timer:sleep(200), + timer:sleep(1000). + +%% force a race condition on cd trade negotiation +main_cd() -> + S = self(), + PidCliC = spawn(fun() -> c(S) end), + receive PidC -> PidC end, + spawn(fun() -> d(S, PidC, PidCliC) end), + receive PidD -> PidD end, + PidCliC ! PidD. + +c(Parent) -> + {ok, Pid} = trade_fsm:start_link("Marc"), + Parent ! Pid, + receive PidD -> PidD end, + io:format("Spawned Marc: ~p~n", [Pid]), + %sys:trace(Pid, true), + sync2(), + trade_fsm:trade(Pid, PidD), + %% no need to accept_trade thanks to the race condition + timer:sleep(200), + trade_fsm:retract_offer(Pid, "car"), + trade_fsm:make_offer(Pid, "horse"), + timer:sleep(600), + trade_fsm:cancel(Pid), + timer:sleep(1000). + +d(Parent, PidC, PidCliC) -> + {ok, Pid} = trade_fsm:start_link("Pete"), + Parent ! Pid, + io:format("Spawned Jim: ~p~n", [Pid]), + %sys:trace(Pid,true), + sync1(PidCliC), + trade_fsm:trade(Pid, PidC), + %% no need to accept_trade thanks to the race condition + timer:sleep(200), + trade_fsm:retract_offer(Pid, "car"), + trade_fsm:make_offer(Pid, "manatee"), + timer:sleep(100), + trade_fsm:ready(Pid), + timer:sleep(1000). + +main_ef() -> + S = self(), + PidCliE = spawn(fun() -> e(S) end), + receive PidE -> PidE end, + spawn(fun() -> f(PidE, PidCliE) end). + +e(Parent) -> + {ok, Pid} = trade_fsm:start_link("Carl"), + Parent ! Pid, + io:format("Spawned Carl: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(800), + trade_fsm:accept_trade(Pid), + timer:sleep(400), + io:format("~p~n",[trade_fsm:ready(Pid)]), + timer:sleep(1000), + trade_fsm:make_offer(Pid, "horse"), + trade_fsm:make_offer(Pid, "sword"), + timer:sleep(1000), + io:format("a synchronizing~n"), + sync2(), + trade_fsm:ready(Pid), + timer:sleep(200), + trade_fsm:ready(Pid), + timer:sleep(1000). + +f(PidE, PidCliE) -> + {ok, Pid} = trade_fsm:start_link("Jim"), + io:format("Spawned Jim: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(500), + trade_fsm:trade(Pid, PidE), + trade_fsm:make_offer(Pid, "boots"), + timer:sleep(200), + trade_fsm:retract_offer(Pid, "boots"), + timer:sleep(500), + trade_fsm:make_offer(Pid, "shotgun"), + timer:sleep(1000), + io:format("b synchronizing~n"), + sync1(PidCliE), + trade_fsm:make_offer(Pid, "horse"), + timer:sleep(200), + trade_fsm:ready(Pid), + timer:sleep(1000). + +%%% Utils +sync1(Pid) -> + Pid ! self(), + receive ack -> ok end. + +sync2() -> + receive + From -> From ! ack + end. diff --git a/learn-you-some-erlang/trade/trade_fsm.erl b/learn-you-some-erlang/trade/trade_fsm.erl new file mode 100644 index 0000000..fc66870 --- /dev/null +++ b/learn-you-some-erlang/trade/trade_fsm.erl @@ -0,0 +1,359 @@ +-module(trade_fsm). +-behaviour(gen_fsm). + +%% public API +-export([start/1, start_link/1, trade/2, accept_trade/1, + make_offer/2, retract_offer/2, ready/1, cancel/1]). +%% gen_fsm callbacks +-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, + terminate/3, code_change/4, + % custom state names + idle/2, idle/3, idle_wait/2, idle_wait/3, negotiate/2, + negotiate/3, wait/2, ready/2, ready/3]). + +-record(state, {name="", + other, + ownitems=[], + otheritems=[], + monitor, + from}). + +%%% PUBLIC API +start(Name) -> + gen_fsm:start(?MODULE, [Name], []). + +start_link(Name) -> + gen_fsm:start_link(?MODULE, [Name], []). + +%% ask for a begin session. Returns when/if the other accepts +trade(OwnPid, OtherPid) -> + gen_fsm:sync_send_event(OwnPid, {negotiate, OtherPid}, 30000). + +%% Accept someone's trade offer. +accept_trade(OwnPid) -> + gen_fsm:sync_send_event(OwnPid, accept_negotiate). + +%% Send an item on the table to be traded +make_offer(OwnPid, Item) -> + gen_fsm:send_event(OwnPid, {make_offer, Item}). + +%% Cancel trade offer +retract_offer(OwnPid, Item) -> + gen_fsm:send_event(OwnPid, {retract_offer, Item}). + +%% Mention that you're ready for a trade. When the other +%% player also declares being ready, the trade is done +ready(OwnPid) -> + gen_fsm:sync_send_event(OwnPid, ready, infinity). + +%% Cancel the transaction. +cancel(OwnPid) -> + gen_fsm:sync_send_all_state_event(OwnPid, cancel). + +%%% CLIENT-TO-CLIENT API +%% These calls are only listed for the gen_fsm to call +%% among themselves +%% All calls are asynchronous to avoid deadlocks + +%% Ask the other FSM for a trade session +ask_negotiate(OtherPid, OwnPid) -> + gen_fsm:send_event(OtherPid, {ask_negotiate, OwnPid}). + +%% Forward the client message accepting the transaction +accept_negotiate(OtherPid, OwnPid) -> + gen_fsm:send_event(OtherPid, {accept_negotiate, OwnPid}). + +%% forward a client's offer +do_offer(OtherPid, Item) -> + gen_fsm:send_event(OtherPid, {do_offer, Item}). + +%% forward a client's offer cancellation +undo_offer(OtherPid, Item) -> + gen_fsm:send_event(OtherPid, {undo_offer, Item}). + +%% Ask the other side if he's ready to trade. +are_you_ready(OtherPid) -> + gen_fsm:send_event(OtherPid, are_you_ready). + +%% Reply that the side is not ready to trade +%% i.e. is not in 'wait' state. +not_yet(OtherPid) -> + gen_fsm:send_event(OtherPid, not_yet). + +%% Tells the other fsm that the user is currently waiting +%% for the ready state. State should transition to 'ready' +am_ready(OtherPid) -> + gen_fsm:send_event(OtherPid, 'ready!'). + +%% Acknowledge that the fsm is in a ready state. +ack_trans(OtherPid) -> + gen_fsm:send_event(OtherPid, ack). + +%% ask if ready to commit +ask_commit(OtherPid) -> + gen_fsm:sync_send_event(OtherPid, ask_commit). + +%% begin the synchronous commit +do_commit(OtherPid) -> + gen_fsm:sync_send_event(OtherPid, do_commit). + +%% Make the other FSM aware that your client cancelled the trade +notify_cancel(OtherPid) -> + gen_fsm:send_all_state_event(OtherPid, cancel). + +%%% GEN_FSM API +init(Name) -> + {ok, idle, #state{name=Name}}. + + +%% idle state is the state before any trade is done. +%% The other player asks for a negotiation. We basically +%% only wait for our own user to accept the trade, +%% and store the other's Pid for future uses +idle({ask_negotiate, OtherPid}, S=#state{}) -> + Ref = monitor(process, OtherPid), + notice(S, "~p asked for a trade negotiation", [OtherPid]), + {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref}}; +idle(Event, Data) -> + unexpected(Event, idle), + {next_state, idle, Data}. + +%% trade call coming from the user. Forward to the other side, +%% forward it and store the other's Pid +idle({negotiate, OtherPid}, From, S=#state{}) -> + ask_negotiate(OtherPid, self()), + notice(S, "asking user ~p for a trade", [OtherPid]), + Ref = monitor(process, OtherPid), + {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref, from=From}}; +idle(Event, _From, Data) -> + unexpected(Event, idle), + {next_state, idle, Data}. + +%% idle_wait allows to expect replies from the other side and +%% start negotiating for items + +%% the other side asked for a negotiation while we asked for it too. +%% this means both definitely agree to the idea of doing a trade. +%% Both sides can assume the other feels the same! +idle_wait({ask_negotiate, OtherPid}, S=#state{other=OtherPid}) -> + gen_fsm:reply(S#state.from, ok), + notice(S, "starting negotiation", []), + {next_state, negotiate, S}; +%% The other side has accepted our offer. Move to negotiate state +idle_wait({accept_negotiate, OtherPid}, S=#state{other=OtherPid}) -> + gen_fsm:reply(S#state.from, ok), + notice(S, "starting negotiation", []), + {next_state, negotiate, S}; +%% different call from someone else. Not supported! Let it die. +idle_wait(Event, Data) -> + unexpected(Event, idle_wait), + {next_state, idle_wait, Data}. + +%% Our own client has decided to accept the transaction. +%% Make the other FSM aware of it and move to negotiate state. +idle_wait(accept_negotiate, _From, S=#state{other=OtherPid}) -> + accept_negotiate(OtherPid, self()), + notice(S, "accepting negotiation", []), + {reply, ok, negotiate, S}; +idle_wait(Event, _From, Data) -> + unexpected(Event, idle_wait), + {next_state, idle_wait, Data}. + +%% own side offering an item +negotiate({make_offer, Item}, S=#state{ownitems=OwnItems}) -> + do_offer(S#state.other, Item), + notice(S, "offering ~p", [Item]), + {next_state, negotiate, S#state{ownitems=add(Item, OwnItems)}}; +%% Own side retracting an item offer +negotiate({retract_offer, Item}, S=#state{ownitems=OwnItems}) -> + undo_offer(S#state.other, Item), + notice(S, "cancelling offer on ~p", [Item]), + {next_state, negotiate, S#state{ownitems=remove(Item, OwnItems)}}; +%% other side offering an item +negotiate({do_offer, Item}, S=#state{otheritems=OtherItems}) -> + notice(S, "other player offering ~p", [Item]), + {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}}; +%% other side retracting an item offer +negotiate({undo_offer, Item}, S=#state{otheritems=OtherItems}) -> + notice(S, "Other player cancelling offer on ~p", [Item]), + {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}}; +%% Other side has declared itself ready. Our own FSM should tell it to +%% wait (with not_yet/1). +negotiate(are_you_ready, S=#state{other=OtherPid}) -> + io:format("Other user ready to trade.~n"), + notice(S, + "Other user ready to transfer goods:~n" + "You get ~p, The other side gets ~p", + [S#state.otheritems, S#state.ownitems]), + not_yet(OtherPid), + {next_state, negotiate, S}; +negotiate(Event, Data) -> + unexpected(Event, negotiate), + {next_state, negotiate, Data}. + +%% own user mentioning he is ready. Next state should be wait +%% and we add the 'from' to the state so we can reply to the +%% user once ready. +negotiate(ready, From, S = #state{other=OtherPid}) -> + are_you_ready(OtherPid), + notice(S, "asking if ready, waiting", []), + {next_state, wait, S#state{from=From}}; +negotiate(Event, _From, S) -> + unexpected(Event, negotiate), + {next_state, negotiate, S}. + +%% other side offering an item. Don't forget our client is still +%% waiting for a reply, so let's tell them the trade state changed +%% and move back to the negotiate state +wait({do_offer, Item}, S=#state{otheritems=OtherItems}) -> + gen_fsm:reply(S#state.from, offer_changed), + notice(S, "other side offering ~p", [Item]), + {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}}; +%% other side cancelling an item offer. Don't forget our client is still +%% waiting for a reply, so let's tell them the trade state changed +%% and move back to the negotiate state +wait({undo_offer, Item}, S=#state{otheritems=OtherItems}) -> + gen_fsm:reply(S#state.from, offer_changed), + notice(S, "Other side cancelling offer of ~p", [Item]), + {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}}; +%% The other client falls in ready state and asks us about it. +%% However, the other client could have moved out of wait state already. +%% Because of this, we send that we indeed are 'ready!' and hope for them +%% to do the same. +wait(are_you_ready, S=#state{}) -> + am_ready(S#state.other), + notice(S, "asked if ready, and I am. Waiting for same reply", []), + {next_state, wait, S}; +%% The other client is not ready to trade yet. We keep waiting +%% and won't reply to our own client yet. +wait(not_yet, S = #state{}) -> + notice(S, "Other not ready yet", []), + {next_state, wait, S}; +%% The other client was waiting for us! Let's reply to ours and +%% send the ack message for the commit initiation on the other end. +%% We can't go back after this. +wait('ready!', S=#state{}) -> + am_ready(S#state.other), + ack_trans(S#state.other), + gen_fsm:reply(S#state.from, ok), + notice(S, "other side is ready. Moving to ready state", []), + {next_state, ready, S}; +wait(Event, Data) -> + unexpected(Event, wait), + {next_state, wait, Data}. + +%% Ready state with the acknowledgement message coming from the +%% other side. We determine if we should begin the synchronous +%% commit or if the other side should. +%% A successful commit (if we initiated it) could be done +%% in the terminate function or any other before. +ready(ack, S=#state{}) -> + case priority(self(), S#state.other) of + true -> + try + notice(S, "asking for commit", []), + ready_commit = ask_commit(S#state.other), + notice(S, "ordering commit", []), + ok = do_commit(S#state.other), + notice(S, "committing...", []), + commit(S), + {stop, normal, S} + catch Class:Reason -> + %% abort! Either ready_commit or do_commit failed + notice(S, "commit failed", []), + {stop, {Class, Reason}, S} + end; + false -> + {next_state, ready, S} + end; +ready(Event, Data) -> + unexpected(Event, ready), + {next_state, ready, Data}. + +%% We weren't the ones to initiate the commit. +%% Let's reply to the other side to say we're doing our part +%% and terminate. +ready(ask_commit, _From, S) -> + notice(S, "replying to ask_commit", []), + {reply, ready_commit, ready, S}; +ready(do_commit, _From, S) -> + notice(S, "committing...", []), + commit(S), + {stop, normal, ok, S}; +ready(Event, _From, Data) -> + unexpected(Event, ready), + {next_state, ready, Data}. + +%% This cancel event has been sent by the other player +%% stop whatever we're doing and shut down! +handle_event(cancel, _StateName, S=#state{}) -> + notice(S, "received cancel event", []), + {stop, other_cancelled, S}; +handle_event(Event, StateName, Data) -> + unexpected(Event, StateName), + {next_state, StateName, Data}. + +%% This cancel event comes from the client. We must warn the other +%% player that we have a quitter! +handle_sync_event(cancel, _From, _StateName, S = #state{}) -> + notify_cancel(S#state.other), + notice(S, "cancelling trade, sending cancel event", []), + {stop, cancelled, ok, S}; +%% Note: DO NOT reply to unexpected calls. Let the call-maker crash! +handle_sync_event(Event, _From, StateName, Data) -> + unexpected(Event, StateName), + {next_state, StateName, Data}. + +%% The other player's FSM has gone down. We have +%% to abort the trade. +handle_info({'DOWN', Ref, process, Pid, Reason}, _, S=#state{other=Pid, monitor=Ref}) -> + notice(S, "Other side dead", []), + {stop, {other_down, Reason}, S}; +handle_info(Info, StateName, Data) -> + unexpected(Info, StateName), + {next_state, StateName, Data}. + +code_change(_OldVsn, StateName, Data, _Extra) -> + {ok, StateName, Data}. + +%% Transaction completed. +terminate(normal, ready, S=#state{}) -> + notice(S, "FSM leaving.", []); +terminate(_Reason, _StateName, _StateData) -> + ok. + +%%% PRIVATE FUNCTIONS + +%% adds an item to an item list +add(Item, Items) -> + [Item | Items]. + +%% remove an item from an item list +remove(Item, Items) -> + Items -- [Item]. + +%% Send players a notice. This could be messages to their clients +%% but for our purposes, outputting to the shell is enough. +notice(#state{name=N}, Str, Args) -> + io:format("~s: "++Str++"~n", [N|Args]). + +%% Unexpected allows to log unexpected messages +unexpected(Msg, State) -> + io:format("~p received unknown event ~p while in state ~p~n", + [self(), Msg, State]). + +%% This function allows two processes to make a synchronous call to each +%% other by electing one Pid to do it. Both processes call it and it +%% tells them whether they should initiate the call or not. +%% This is done by knowing that Erlang will alwys sort Pids in an +%% absolute manner depending on when and where they were spawned. +priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true; +priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false. + +commit(S = #state{}) -> + io:format("Transaction completed for ~s. " + "Items sent are:~n~p,~n received are:~n~p.~n" + "This operation should have some atomic save " + "in a database.~n", + [S#state.name, S#state.ownitems, S#state.otheritems]). + diff --git a/learn-you-some-erlang/tree.erl b/learn-you-some-erlang/tree.erl new file mode 100644 index 0000000..3cda7c3 --- /dev/null +++ b/learn-you-some-erlang/tree.erl @@ -0,0 +1,43 @@ +-module(tree). +-export([empty/0, insert/3, lookup/2, has_value/2]). + +empty() -> {node, 'nil'}. + +insert(Key, Val, {node, 'nil'}) -> + {node, {Key, Val, {node, 'nil'}, {node, 'nil'}}}; +insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) when NewKey < Key -> + {node, {Key, Val, insert(NewKey, NewVal, Smaller), Larger}}; +insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) when NewKey > Key -> + {node, {Key, Val, Smaller, insert(NewKey, NewVal, Larger)}}; +insert(Key, Val, {node, {Key, _, Smaller, Larger}}) -> + {node, {Key, Val, Smaller, Larger}}. + +lookup(_, {node, 'nil'}) -> + undefined; +lookup(Key, {node, {Key, Val, _, _}}) -> + {ok, Val}; +lookup(Key, {node, {NodeKey, _, Smaller, _}}) when Key < NodeKey -> + lookup(Key, Smaller); +lookup(Key, {node, {_, _, _, Larger}}) -> + lookup(Key, Larger). + +%%--------------------------------------------------------- +%% The code after this comment is added in the errors and +%% exceptions chapter. Ignore it if you're still reading +%% the chapter about recursion. +%%--------------------------------------------------------- + +has_value(Val, Tree) -> + try has_value1(Val, Tree) of + _ -> false + catch + true -> true + end. + +has_value1(_, {node, 'nil'}) -> + false; +has_value1(Val, {node, {_, Val, _, _}}) -> + throw(true); +has_value1(Val, {node, {_, _, Left, Right}}) -> + has_value1(Val, Left), + has_value1(Val, Right). diff --git a/learn-you-some-erlang/useless.erl b/learn-you-some-erlang/useless.erl new file mode 100644 index 0000000..d5542a2 --- /dev/null +++ b/learn-you-some-erlang/useless.erl @@ -0,0 +1,14 @@ +-module(useless). +-export([add/2, hello/0, greet_and_add_two/1]). + +add(A,B) -> + A + B. + +%% Shows greetings. +%% io:format/1 is the standard function used to output text. +hello() -> + io:format("Hello, world!~n"). + +greet_and_add_two(X) -> + hello(), + add(X,2). diff --git a/learn-you-some-erlang/what_the_if.erl b/learn-you-some-erlang/what_the_if.erl new file mode 100644 index 0000000..723c818 --- /dev/null +++ b/learn-you-some-erlang/what_the_if.erl @@ -0,0 +1,30 @@ +-module(what_the_if). +-export([heh_fine/0, oh_god/1, help_me/1]). + +%% should check if this actually works (hint: an error will be thrown) +heh_fine() -> + if 1 =:= 1 -> + works + end, + if 1 =:= 2; 1 =:= 1 -> + works + end, + if 1 =:= 2, 1 =:= 1 -> + fails + end. + +oh_god(N) -> + if N =:= 2 -> might_succeed; + true -> always_does %% this is Erlang's if's 'else!' + end. + +%% note, this one would be better as a pattern match in function heads! +%% I'm doing it this way for the sake of the example. +help_me(Animal) -> + Talk = if Animal == cat -> "meow"; + Animal == beef -> "mooo"; + Animal == dog -> "bark"; + Animal == tree -> "bark"; + true -> "fgdadfgna" + end, + {Animal, "says " ++ Talk ++ "!"}. diff --git a/learn-you-some-erlang/zoo.erl b/learn-you-some-erlang/zoo.erl new file mode 100644 index 0000000..1e3c2eb --- /dev/null +++ b/learn-you-some-erlang/zoo.erl @@ -0,0 +1,45 @@ +-module(zoo). +-export([main/0]). + +-type red_panda() :: bamboo | birds | eggs | berries. +-type squid() :: sperm_whale. +-type food(A) :: fun(() -> A). + +-spec feeder(red_panda) -> food(red_panda()); + (squid) -> food(squid()). +feeder(red_panda) -> + fun() -> + element(random:uniform(4), {bamboo, birds, eggs, berries}) + end; +feeder(squid) -> + fun() -> sperm_whale end. + +-spec feed_red_panda(food(red_panda())) -> red_panda(). +feed_red_panda(Generator) -> + Food = Generator(), + io:format("feeding ~p to the red panda~n", [Food]), + Food. + +-spec feed_squid(food(squid())) -> squid(). +feed_squid(Generator) -> + Food = Generator(), + io:format("feeding ~p to the squid~n", [Food]), + Food. + +main() -> + %% Random seeding + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed(A, B, C), + %% The zoo buys a feeder for both the red panda and squid + FeederRP = feeder(red_panda), + FeederSquid = feeder(squid), + %% Time to feed them! If we do it correctly at least once, + %% Then no warning ever happens. Comment the two lines + %% below to enable dialyzer figuring stuff out! This is likely + %% failing because Dialyzer sees both calls as valid and thus + %% needs not to reevaluate them again. + %feed_squid(FeederSquid), + %feed_red_panda(FeederRP), + %% This should not be right! + feed_squid(FeederRP), + feed_red_panda(FeederSquid). |