Packages

Property-based testing and metamorphic testing combinator library for Gleam

Current section

Files

Jump to
metamon src metamon@relation.erl
Raw

src/metamon@relation.erl

-module(metamon@relation).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/metamon/relation.gleam").
-export([new/2, equal/0, not_equal/0, equivalent_under/2, approximately/1, permutation_of/0, subset_of/0, monotone/1, implies/2, 'and'/2, 'or'/2, invert/1, rename/2, n_new/2, all_equal/0, pairwise/1]).
-export_type([relation/1, relation_n/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Named two-argument predicates used by metamorphic relations.\n"
"\n"
" A `Relation(b)` answers \"do these two outputs satisfy the\n"
" constraint?\". The `name` is surfaced in failure reports so the\n"
" user immediately sees which property broke.\n"
).
-type relation(GCI) :: {relation, binary(), fun((GCI, GCI) -> boolean())}.
-type relation_n(GCJ) :: {relation_n, binary(), fun((list(GCJ)) -> boolean())}.
-file("src/metamon/relation.gleam", 17).
?DOC(" Construct a relation from a name and a binary predicate.\n").
-spec new(binary(), fun((GCK, GCK) -> boolean())) -> relation(GCK).
new(Name, Holds) ->
{relation, Name, Holds}.
-file("src/metamon/relation.gleam", 22).
?DOC(" Equality (Gleam's structural `==`).\n").
-spec equal() -> relation(any()).
equal() ->
{relation, <<"equal"/utf8>>, fun(Left, Right) -> Left =:= Right end}.
-file("src/metamon/relation.gleam", 27).
?DOC(" Inequality.\n").
-spec not_equal() -> relation(any()).
not_equal() ->
{relation, <<"not_equal"/utf8>>, fun(Left, Right) -> Left /= Right end}.
-file("src/metamon/relation.gleam", 33).
?DOC(
" Equality after a shared normalisation step. Useful for\n"
" \"equal modulo whitespace / key order / formatting\".\n"
).
-spec equivalent_under(fun((GCQ) -> any()), binary()) -> relation(GCQ).
equivalent_under(Via, Name) ->
{relation,
<<<<"equivalent_under("/utf8, Name/binary>>/binary, ")"/utf8>>,
fun(Left, Right) -> Via(Left) =:= Via(Right) end}.
-file("src/metamon/relation.gleam", 40).
?DOC(" Floats within `epsilon` of each other.\n").
-spec approximately(float()) -> relation(float()).
approximately(Epsilon) ->
{relation,
<<<<"approximately("/utf8,
(gleam_stdlib:float_to_string(Epsilon))/binary>>/binary,
")"/utf8>>,
fun(Left, Right) ->
Diff = case Left >= Right of
true ->
Left - Right;
false ->
Right - Left
end,
Diff =< Epsilon
end}.
-file("src/metamon/relation.gleam", 81).
-spec remove_one_loop(GDJ, list(GDJ), list(GDJ)) -> {ok, list(GDJ)} |
{error, nil}.
remove_one_loop(Target, Items, Acc) ->
case Items of
[] ->
{error, nil};
[First | Rest] ->
case First =:= Target of
true ->
{ok, lists:append(lists:reverse(Acc), Rest)};
false ->
remove_one_loop(Target, Rest, [First | Acc])
end
end.
-file("src/metamon/relation.gleam", 77).
-spec remove_one(GDE, list(GDE)) -> {ok, list(GDE)} | {error, nil}.
remove_one(Target, Items) ->
remove_one_loop(Target, Items, []).
-file("src/metamon/relation.gleam", 65).
-spec remove_each(list(GDA), list(GDA)) -> list(GDA).
remove_each(To_remove, Remaining) ->
case To_remove of
[] ->
Remaining;
[First | Rest] ->
case remove_one(First, Remaining) of
{ok, Remaining_after} ->
remove_each(Rest, Remaining_after);
{error, _} ->
[First]
end
end.
-file("src/metamon/relation.gleam", 58).
-spec is_permutation(list(GCX), list(GCX)) -> boolean().
is_permutation(Left, Right) ->
case erlang:length(Left) =:= erlang:length(Right) of
false ->
false;
true ->
remove_each(Left, Right) =:= []
end.
-file("src/metamon/relation.gleam", 54).
?DOC(" Two lists are permutations of each other (same multiset).\n").
-spec permutation_of() -> relation(list(any())).
permutation_of() ->
{relation, <<"permutation_of"/utf8>>, fun is_permutation/2}.
-file("src/metamon/relation.gleam", 101).
-spec is_subset(list(GDS), list(GDS)) -> boolean().
is_subset(Left, Right) ->
case Left of
[] ->
true;
[First | Rest] ->
case remove_one(First, Right) of
{ok, Remaining} ->
is_subset(Rest, Remaining);
{error, _} ->
false
end
end.
-file("src/metamon/relation.gleam", 97).
?DOC(" `left` is a sub-multiset of `right`.\n").
-spec subset_of() -> relation(list(any())).
subset_of() ->
{relation,
<<"subset_of"/utf8>>,
fun(Left, Right) -> is_subset(Left, Right) end}.
-file("src/metamon/relation.gleam", 113).
?DOC(" `cmp(left, right)` returns `Lt` or `Eq` (i.e. left ≤ right).\n").
-spec monotone(fun((GDV, GDV) -> gleam@order:order())) -> relation(GDV).
monotone(Cmp) ->
{relation, <<"monotone"/utf8>>, fun(Left, Right) -> case Cmp(Left, Right) of
lt ->
true;
eq ->
true;
gt ->
false
end end}.
-file("src/metamon/relation.gleam", 126).
?DOC(
" Conditional relation: if `antecedent(left, right)` is `True`, the\n"
" inner relation must hold. Otherwise the relation is trivially\n"
" satisfied.\n"
).
-spec implies(fun((GDX, GDX) -> boolean()), relation(GDX)) -> relation(GDX).
implies(Antecedent, Inner) ->
{relation,
<<<<"implies("/utf8, (erlang:element(2, Inner))/binary>>/binary,
")"/utf8>>,
fun(Left, Right) -> case Antecedent(Left, Right) of
false ->
true;
true ->
(erlang:element(3, Inner))(Left, Right)
end end}.
-file("src/metamon/relation.gleam", 136).
?DOC(" Both `r1` and `r2` must hold.\n").
-spec 'and'(relation(GEA), relation(GEA)) -> relation(GEA).
'and'(R1, R2) ->
{relation,
<<<<(erlang:element(2, R1))/binary, " and "/utf8>>/binary,
(erlang:element(2, R2))/binary>>,
fun(Left, Right) ->
(erlang:element(3, R1))(Left, Right) andalso (erlang:element(3, R2))(
Left,
Right
)
end}.
-file("src/metamon/relation.gleam", 143).
?DOC(" At least one of `r1` and `r2` must hold.\n").
-spec 'or'(relation(GEE), relation(GEE)) -> relation(GEE).
'or'(R1, R2) ->
{relation,
<<<<(erlang:element(2, R1))/binary, " or "/utf8>>/binary,
(erlang:element(2, R2))/binary>>,
fun(Left, Right) ->
(erlang:element(3, R1))(Left, Right) orelse (erlang:element(3, R2))(
Left,
Right
)
end}.
-file("src/metamon/relation.gleam", 150).
?DOC(" Negate a relation.\n").
-spec invert(relation(GEI)) -> relation(GEI).
invert(R) ->
{relation,
<<<<"not("/utf8, (erlang:element(2, R))/binary>>/binary, ")"/utf8>>,
fun(Left, Right) -> not (erlang:element(3, R))(Left, Right) end}.
-file("src/metamon/relation.gleam", 157).
?DOC(" Override a relation's name.\n").
-spec rename(relation(GEL), binary()) -> relation(GEL).
rename(R, Name) ->
{relation, Name, erlang:element(3, R)}.
-file("src/metamon/relation.gleam", 173).
?DOC(" Construct an N-ary relation directly.\n").
-spec n_new(binary(), fun((list(GEO)) -> boolean())) -> relation_n(GEO).
n_new(Name, Holds) ->
{relation_n, Name, Holds}.
-file("src/metamon/relation.gleam", 183).
-spec all_equal_loop(list(any())) -> boolean().
all_equal_loop(Items) ->
case Items of
[] ->
true;
[_] ->
true;
[First, Second | Rest] ->
case First =:= Second of
true ->
all_equal_loop([Second | Rest]);
false ->
false
end
end.
-file("src/metamon/relation.gleam", 179).
?DOC(
" All elements of the input list are equal under structural `==`.\n"
" Trivially `True` for lists of length 0 or 1.\n"
).
-spec all_equal() -> relation_n(any()).
all_equal() ->
{relation_n, <<"all_equal"/utf8>>, fun(Items) -> all_equal_loop(Items) end}.
-file("src/metamon/relation.gleam", 205).
-spec pairwise_loop(relation(GEY), list(GEY)) -> boolean().
pairwise_loop(R, Items) ->
case Items of
[] ->
true;
[_] ->
true;
[First, Second | Rest] ->
case (erlang:element(3, R))(First, Second) of
true ->
pairwise_loop(R, [Second | Rest]);
false ->
false
end
end.
-file("src/metamon/relation.gleam", 199).
?DOC(
" Lift a binary `Relation(b)` to an N-ary relation by checking it\n"
" pairwise: every consecutive pair `(items[i], items[i+1])` must\n"
" satisfy `r`. For `name == \"equal\"` this gives a chain of equal\n"
" values; for `monotone` it gives a sorted chain.\n"
).
-spec pairwise(relation(GEV)) -> relation_n(GEV).
pairwise(R) ->
{relation_n,
<<<<"pairwise("/utf8, (erlang:element(2, R))/binary>>/binary, ")"/utf8>>,
fun(Items) -> pairwise_loop(R, Items) end}.