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, set_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(GNU) :: {relation, binary(), fun((GNU, GNU) -> boolean())}.
-type relation_n(GNV) :: {relation_n, binary(), fun((list(GNV)) -> boolean())}.
-file("src/metamon/relation.gleam", 17).
?DOC(" Construct a relation from a name and a binary predicate.\n").
-spec new(binary(), fun((GNW, GNW) -> boolean())) -> relation(GNW).
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((GOC) -> any()), binary()) -> relation(GOC).
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", 47).
?DOC(
" Floats within `epsilon` of each other.\n"
"\n"
" Panics at construction if `epsilon` is negative. A negative epsilon\n"
" would silently yield a degenerate \"always false\" relation (the\n"
" absolute difference is always non-negative, so `diff <=. epsilon`\n"
" can never hold). Failing visibly on a sign mistake matches the\n"
" project's posture for malformed numeric inputs in `Range.constant`,\n"
" `Range.linear`, etc.\n"
).
-spec approximately(float()) -> relation(float()).
approximately(Epsilon) ->
case Epsilon < +0.0 of
true ->
erlang:error(#{gleam_error => panic,
message => <<"metamon.approximately: epsilon must be >= 0.0 (a negative epsilon would make every pair compare False, including a value with itself)"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"metamon/relation"/utf8>>,
function => <<"approximately"/utf8>>,
line => 50});
false ->
{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}
end.
-file("src/metamon/relation.gleam", 93).
-spec remove_one_loop(GOV, list(GOV), list(GOV)) -> {ok, list(GOV)} |
{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", 89).
-spec remove_one(GOQ, list(GOQ)) -> {ok, list(GOQ)} | {error, nil}.
remove_one(Target, Items) ->
remove_one_loop(Target, Items, []).
-file("src/metamon/relation.gleam", 77).
-spec remove_each(list(GOM), list(GOM)) -> list(GOM).
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", 70).
-spec is_permutation(list(GOJ), list(GOJ)) -> 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", 66).
?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", 132).
-spec is_subset(list(GPH), list(GPH)) -> 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", 116).
?DOC(
" Multiset subset: every element of `left` is matched against a\n"
" *distinct* element of `right` (one-to-one, by structural equality).\n"
" `subset_of([1, 1], [1])` is `False`; `subset_of([1, 1], [1, 1, 2])`\n"
" is `True`. This matches the semantics of `permutation_of` (which is\n"
" also multiset-based).\n"
"\n"
" For the set-style interpretation that ignores multiplicity, use\n"
" `set_subset_of()` instead.\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", 143).
-spec is_set_subset(list(GPK), list(GPK)) -> boolean().
is_set_subset(Left, Right) ->
case Left of
[] ->
true;
[First | Rest] ->
case gleam@list:contains(Right, First) of
true ->
is_set_subset(Rest, Right);
false ->
false
end
end.
-file("src/metamon/relation.gleam", 126).
?DOC(
" Set subset: every element of `left` is contained somewhere in\n"
" `right`, ignoring multiplicity. `set_subset_of([1, 1], [1])` is\n"
" `True` because the only distinct value `1` is present.\n"
"\n"
" For the multiset interpretation that requires distinct matches\n"
" per occurrence, use `subset_of()` instead.\n"
).
-spec set_subset_of() -> relation(list(any())).
set_subset_of() ->
{relation,
<<"set_subset_of"/utf8>>,
fun(Left, Right) -> is_set_subset(Left, Right) end}.
-file("src/metamon/relation.gleam", 155).
?DOC(" `cmp(left, right)` returns `Lt` or `Eq` (i.e. left ≤ right).\n").
-spec monotone(fun((GPN, GPN) -> gleam@order:order())) -> relation(GPN).
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", 168).
?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((GPP, GPP) -> boolean()), relation(GPP)) -> relation(GPP).
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", 178).
?DOC(" Both `r1` and `r2` must hold.\n").
-spec 'and'(relation(GPS), relation(GPS)) -> relation(GPS).
'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", 185).
?DOC(" At least one of `r1` and `r2` must hold.\n").
-spec 'or'(relation(GPW), relation(GPW)) -> relation(GPW).
'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", 192).
?DOC(" Negate a relation.\n").
-spec invert(relation(GQA)) -> relation(GQA).
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", 199).
?DOC(" Override a relation's name.\n").
-spec rename(relation(GQD), binary()) -> relation(GQD).
rename(R, Name) ->
{relation, Name, erlang:element(3, R)}.
-file("src/metamon/relation.gleam", 215).
?DOC(" Construct an N-ary relation directly.\n").
-spec n_new(binary(), fun((list(GQG)) -> boolean())) -> relation_n(GQG).
n_new(Name, Holds) ->
{relation_n, Name, Holds}.
-file("src/metamon/relation.gleam", 225).
-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", 221).
?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", 247).
-spec pairwise_loop(relation(GQQ), list(GQQ)) -> 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", 241).
?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(GQN)) -> relation_n(GQN).
pairwise(R) ->
{relation_n,
<<<<"pairwise("/utf8, (erlang:element(2, R))/binary>>/binary, ")"/utf8>>,
fun(Items) -> pairwise_loop(R, Items) end}.