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/release/erlcount-1.0 | |
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/release/erlcount-1.0')
8 files changed, 255 insertions, 0 deletions
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"))]. |