Current section
Files
Jump to
Current section
Files
src/exercism_parse_transform.erl
%% @doc This module provides a parse transform to replace remote calls into the
%% module under test with calls into the example implementation if necessary.
%%
%% To use this parse transform, add the following to your testing module:
%%
%% ```
%% -include_lib("erl_exercism/include/exercism.hrl").
%% '''
%%
%% Given a testing module `M_tests', all remote calls into `M' will be replaced
%% with calls into `example' if the module `example' is present in the project.
%%
%% "Available" means, the file `src/example.erl' does exist.
%% @end
-module(exercism_parse_transform).
%% API exports
-export([parse_transform/2]).
%%====================================================================
%% API functions
%%====================================================================
parse_transform(Form, _Option) ->
{ok, TestModuleName} = find_module_name(Form),
{ok, ModuleName} = extract_tested_module(TestModuleName),
case is_example() of
false ->
Form;
true ->
parse_trans:plain_transform(transform_fun(ModuleName), Form)
end.
%%====================================================================
%% Internal functions
%%====================================================================
is_example() ->
{ok, Files} = file:list_dir("./src"),
lists:member("example.erl", Files).
find_module_name([]) -> {error, notfound};
find_module_name([{attribute, _Line, module, Module} | _]) -> {ok, Module};
find_module_name([_ | T]) -> find_module_name(T).
extract_tested_module(ModuleName) when is_atom(ModuleName) ->
ModuleName1 = atom_to_list(ModuleName),
case re:run(ModuleName1, "^(.*)_tests$") of
{match, [_, {0, Length}]} ->
{ModuleName2, _} = lists:split(Length, ModuleName1),
{ok, list_to_atom(ModuleName2)};
_ ->
error
end.
transform_fun(Module) -> fun(F) -> do_transform(F, Module) end.
do_transform({call, LCall, {remote, LRemote, {atom, L1, ModuleName}, Fn}, Args}, ModuleName) ->
TransformedArgs = parse_trans:plain_transform(transform_fun(ModuleName), Args),
{call, LCall, {remote, LRemote, {atom, L1, example}, Fn}, TransformedArgs};
do_transform(_Form, _ModuleName) ->
continue.
%%====================================================================
%% Internal testing
%%====================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-define(_assertExtract(Exp, Module), ?_assertMatch(Exp, extract_tested_module(Module))).
to_test_name(Text, Exp, Act) ->
IoList = io_lib:format("~s (~p; ~p)", [Text, Exp, Act]),
erlang:iolist_to_binary(IoList).
to_form(String) ->
{ok, Tokens, _} = erl_scan:string(String),
{ok, Form} = erl_parse:parse_form(Tokens),
case is_list(Form) of
true -> Form;
false -> [Form]
end.
extract_tested_module_test_() ->
[
?_assertExtract({ok, hello_world}, hello_world_tests),
?_assertExtract({ok, zipper}, zipper_tests),
?_assertExtract(error, hello_world),
?_assertExtract(error, zipper)
].
transform_test_() ->
Lines =
[
{"f() -> foo:bar().", "f() -> example:bar().", foo},
{"f() -> foo:bar(foo:bar()).", "f() -> example:bar(example:bar()).", foo},
{"f() -> foo:bar(foo:bar()).", "f() -> foo:bar(foo:bar()).", bar},
{"f() -> foo:bar(bar:bar()).", "f() -> foo:bar(example:bar()).", bar},
{"f() -> bar:bar(foo:bar()).", "f() -> example:bar(foo:bar()).", bar}
],
Forms = lists:map(fun({From, To, Remote}) -> {to_form(From), to_form(To), Remote} end, Lines),
Transformed = lists:map(
fun({From, To, Remote}) ->
{parse_trans:plain_transform(transform_fun(Remote), From), To}
end,
Forms
),
lists:map(fun({Act, Exp}) -> ?_assertMatch(Exp, Act) end, Transformed).
find_module_name_test_() ->
Modules = [
{"-module(foo).", foo, foo_tests, error},
{"-module(foo_tests).", foo_tests, foo_tests, {ok, foo}}
],
Forms = lists:map(
fun({Source, ModuleName, TestModuleName, TestedModuleResult}) ->
{to_form(Source), ModuleName, TestModuleName, TestedModuleResult}
end,
Modules
),
ModuleNames = lists:map(
fun({Forms, ModuleName, _, _}) -> {find_module_name(Forms), ModuleName} end, Forms
),
TestedModuleNames = lists:map(
fun({_, ModuleName, _, TestedModuleResult}) ->
{extract_tested_module(ModuleName), TestedModuleResult}
end,
Forms
),
ModuleNameTests = lists:map(
fun({Act, Exp}) ->
{to_test_name("module name", Exp, Act), ?_assertMatch({ok, Exp}, Act)}
end,
ModuleNames
),
TestedModuleNameTests = lists:map(
fun({Act, Exp}) ->
{to_test_name("tested module name", Exp, Act), ?_assertMatch(Exp, Act)}
end,
TestedModuleNames
),
ModuleNameTests ++ TestedModuleNameTests.
-endif.