%% ----------------------------------------------------------------------------- %% %% Hamcrest Erlang. %% %% Copyright (c) 2010 Tim Watson (watson.timothy@gmail.com) %% %% 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. %% ----------------------------------------------------------------------------- %% @author Tim Watson %% @copyright 2010 Tim Watson. %% @doc Hamcrest Matchers %% @reference See Hamcrest %% for more information. %% ----------------------------------------------------------------------------- -module(hamcrest_matchers). -author('Tim Watson '). -include("hamcrest_internal.hrl"). -import(hamcrest, [message/4]). -export([ all_of/1, anything/0, any_of/1, foreach/1, is/1, is_true/0, is_false/0, is_not/1, equal_to/1, exactly_equal_to/1, greater_than/1, greater_than_or_equal_to/1, less_than/1, less_than_or_equal_to/1, contains_string/1, starts_with/1, ends_with/1, will_fail/0, will_fail/2, has_length/1, has_same_contents_as/1, contains_member/1, matches_regex/1, match_mfa/3, match_mfa/4, reverse_match_mfa/3, reverse_match_mfa/4, isalive/0, isdead/0, isempty/0, check_isempty/1, check_member/2]). %%%============================================================================ %%% Types %%%============================================================================ -type matchfun(A) :: fun((A) -> boolean()). -type container_t() :: list() | hc_set() | hc_gb_set(). %%%============================================================================ %%% API %%%============================================================================ -spec(will_fail() -> hamcrest:matchspec()). will_fail() -> %% Matcher :: fun((fun(() -> any())) -> boolean()) Matcher = fun(F) -> try F() of _ -> false catch _:_ -> true end end, #'hamcrest.matchspec'{ matcher = Matcher, desc = will_fail, expected = {oneof,{exit,error,exception}} }. -spec(will_fail(atom(), term()) -> hamcrest:matchspec()). will_fail(Type, Reason) -> %% Matcher :: fun((fun(() -> any())) -> boolean()) Matcher = fun(F) -> try F() of _ -> false catch Type:Reason -> true end end, #'hamcrest.matchspec'{ matcher = Matcher, desc = will_fail, expected = {Type, Reason} }. -spec(anything() -> hamcrest:matchspec()). anything() -> ?MATCHER(fun(_) -> true end, any, anything). -spec(foreach(hamcrest:matchspec()) -> hamcrest:matchspec()). foreach(M) when is_record(M, 'hamcrest.matchspec') -> ?MATCHER(fun(L) -> drop_matches(M, L) == [] end, M, {foreach, M#'hamcrest.matchspec'.desc}). drop_matches(Match, []) -> case hamcrest:match([], Match) of true -> []; _ -> false end; drop_matches(Match, L) -> lists:dropwhile(fun(E) -> hamcrest:match(E, Match) end, L). -spec(any_of(list(matchfun(term()))) -> hamcrest:matchspec(); (list(hamcrest:matchspec())) -> hamcrest:matchspec()). any_of(Matchers) when is_list(Matchers) -> MatchFun = fun(M) when is_function(M) -> M; (#'hamcrest.matchspec'{matcher=F}) -> F end, #'hamcrest.matchspec'{ matcher = fun(X) -> lists:member(true, [ (MatchFun(M))(X) || M <- Matchers ]) end, desc = any_of, expected = {any, Matchers} }. %% TODO: older syntax for type specifications - we need to support %% >= R13B for the most part... -spec all_of(list(matchfun(term()))) -> hamcrest:matchspec(); (list(hamcrest:matchspec())) -> hamcrest:matchspec(). all_of(Matchers) when is_list(Matchers) -> MatchFun = fun(M) when is_function(M) -> M; (#'hamcrest.matchspec'{matcher=F}) -> F end, #'hamcrest.matchspec'{ matcher = fun(X) -> not(lists:member(false, [ (MatchFun(M))(X) || M <- Matchers ])) end, desc = all_of, expected = {all, Matchers} }. -spec(equal_to(term()) -> hamcrest:matchspec()). equal_to(Y) -> #'hamcrest.matchspec'{ matcher = fun(X) -> X == Y end, desc = equal_to, expected = Y }. -spec(exactly_equal_to(term()) -> hamcrest:matchspec()). exactly_equal_to(X) -> #'hamcrest.matchspec'{ matcher = fun(Y) -> X =:= Y end, desc = exactly_equal_to, expected = X }. -spec(is(matchfun(term())) -> hamcrest:matchspec(); (hamcrest:matchspec()) -> hamcrest:matchspec(); (any()) -> hamcrest:matchspec()). is(Matcher) when is_record(Matcher, 'hamcrest.matchspec') -> Matcher; is(Term) -> equal_to(Term). -spec(is_true() -> hamcrest:matchspec()). is_true() -> equal_to(true). -spec(is_false() -> hamcrest:matchspec()). is_false() -> is_not(equal_to(true)). -spec(is_not(matchfun(term())) -> hamcrest:matchspec(); (hamcrest:matchspec()) -> hamcrest:matchspec(); (term()) -> hamcrest:matchspec()). is_not(#'hamcrest.matchspec'{ matcher=MatchFun }=MatchSpec) when is_record(MatchSpec, 'hamcrest.matchspec') -> MatchSpec#'hamcrest.matchspec'{ matcher = (fun(X) -> not(MatchFun(X)) end) }; is_not(Term) -> is_not(equal_to(Term)). -spec(greater_than(number()) -> hamcrest:matchspec()). greater_than(X) -> #'hamcrest.matchspec'{ matcher = fun(Y) -> Y > X end, desc = greater_than, expected = X }. -spec(greater_than_or_equal_to(number()) -> hamcrest:matchspec()). greater_than_or_equal_to(X) -> #'hamcrest.matchspec'{ matcher = fun(Y) -> Y >= X end, desc = greater_than_or_equal_to, expected = X }. -spec(less_than(number()) -> hamcrest:matchspec()). less_than(X) -> #'hamcrest.matchspec'{ matcher = fun(Y) -> Y < X end, desc = less_than, expected = X }. -spec(less_than_or_equal_to(number()) -> hamcrest:matchspec()). less_than_or_equal_to(X) -> #'hamcrest.matchspec'{ matcher = fun(Y) -> Y =< X end, desc = {less_than_or_equal_to, X}, expected = X }. %% fun((string()) -> boolean()) matchers... -spec(contains_string(string()) -> hamcrest:matchspec()). contains_string([_|_]=X) -> ?MATCHER(fun(Y) -> string:str(Y, X) > 0 end, X, {contains_string, X}). -spec(starts_with(string()) -> hamcrest:matchspec()). starts_with(X) -> ?MATCHER(fun(Y) -> string:str(Y, X) == 1 end, X, {starts_with, X}). -spec(ends_with(string()) -> hamcrest:matchspec()). ends_with(X) -> ?MATCHER(fun(Y) -> string:equal(string:right(Y, length(X)), X) end, X, {ends_with, X}). -spec(matches_regex(string()) -> hamcrest:matchspec()). matches_regex(Rx) -> #'hamcrest.matchspec'{ matcher = fun(X) -> case re:run(X, Rx) of {match,_} -> true; _ -> false end end, desc = {regex_match, Rx}, expected = Rx }. -spec(match_mfa(module(), atom(), list(term())) -> hamcrest:matchspec()). match_mfa(Mod, Func, Args) -> #'hamcrest.matchspec'{ matcher = fun(X) -> catch(apply(Mod, Func, Args ++ [X])) == true end, desc = {eval, [Mod, Func, Args]}, expected = true }. -spec(match_mfa(module(), atom(), list(term()), term()) -> hamcrest:matchspec()). match_mfa(Mod, Func, Args, Desc) -> MS = match_mfa(Mod, Func, Args), MS#'hamcrest.matchspec'{desc=Desc}. -spec(reverse_match_mfa(module(), atom(), list(term())) -> hamcrest:matchspec()). reverse_match_mfa(Mod, Func, Args) when is_list(Args) -> #'hamcrest.matchspec'{ matcher = fun(X) -> catch(apply(Mod, Func, [X|Args])) == true end, desc = {eval, [Mod, Func, lists:reverse(Args)]}, expected = true }. -spec(reverse_match_mfa(module(), atom(), list(term()), term()) -> hamcrest:matchspec()). reverse_match_mfa(Mod, Func, Args, Desc) -> MS = reverse_match_mfa(Mod, Func, Args), MS#'hamcrest.matchspec'{desc=Desc}. -spec(isalive() -> hamcrest:matchspec()). isalive() -> MS = match_mfa(erlang, is_process_alive, []), MS#'hamcrest.matchspec'{desc=is_process_alive, expected=true}. -spec(isdead() -> hamcrest:matchspec()). isdead() -> #'hamcrest.matchspec'{ matcher = fun(X) -> not erlang:is_process_alive(X) end, desc = is_process_alive, expected = false }. -spec(has_length(number()) -> hamcrest:matchspec()). has_length(Size) when is_number(Size) -> ?MATCHER(fun(XS) -> length(XS) == Size end, Size, {length, Size}). -spec(has_same_contents_as(container_t()) -> hamcrest:matchspec()). has_same_contents_as(Container) when is_list(Container) -> MS = foreach(match_mfa(?MODULE, check_member, [Container])), MS#'hamcrest.matchspec'{desc=has_same_contents_as, expected=Container}. -spec(contains_member(term()) -> hamcrest:matchspec()). contains_member(E) -> reverse_match_mfa(?MODULE, check_member, [E]). check_member([], []) -> true; check_member(Container, E) when is_list(Container) -> lists:member(E, Container); check_member(Container, E) -> case sets:is_set(Container) of true -> sets:is_element(E, Container); false -> case gb_sets:is_set(Container) of true -> gb_sets:is_element(E, Container); false -> case ordsets:is_set(Container) of true -> ordsets:is_element(E, Container); false -> case is_tuple(Container) of true -> check_member(tuple_to_list(Container), E); false -> false end end end end. -spec(isempty() -> hamcrest:matchspec()). isempty() -> match_mfa(?MODULE, check_isempty, []). check_isempty([]) -> true; check_isempty({}) -> true; check_isempty(X) -> case sets:is_set(X) of true -> sets:size(X) == 0; _ -> case gb_sets:is_set(X) of true -> gb_sets:is_empty(X); _ -> case ordsets:is_set(X) of true -> ordsets:size(X) == 0; false -> false end end end.