Current section
Files
Jump to
Current section
Files
src/metamon.erl
-module(metamon).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/metamon.gleam").
-export([seed/1, random_seed/0, default_config/0, with_seed/2, with_runs/2, with_max_size/2, with_shrink_limit/2, with_max_edges/2, with_regression_file/2, with_diff_enabled/2, with_runs_or_panic/2, with_max_size_or_panic/2, with_shrink_limit_or_panic/2, with_max_edges_or_panic/2, with_regression_file_or_panic/2, with_output_format/2, mr/3, mr_equivariant/4, name_of/1, forall_with/3, forall/2, forall_morph_with/4, forall_morph/3, assert_morph/3, forall_morph_n_with/5, forall_morph_n/4, forall_morphs/3, idempotency_of/2, invariant_under/2, equivariant_under/4, commutativity_of/1]).
-export_type([mr/2]).
-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(
" Metamon top-level public API.\n"
"\n"
" `metamon` exports the small surface that most tests interact with:\n"
" `forall`, `forall_morph`, `assert_morph`, `forall_morphs`, the\n"
" `Mr` smart constructors, and a handful of metamorphic-relation\n"
" templates (`idempotency_of`, `round_trip_with`,\n"
" `invariant_under`, `equivariant_under`).\n"
"\n"
" Configuration lives in `metamon/config`; generators in\n"
" `metamon/generator`; transforms in `metamon/transform`; relations\n"
" in `metamon/relation`; per-property context in `metamon/annotate`\n"
" and `metamon/coverage`; structural diff in `metamon/diff`.\n"
).
-opaque mr(HBW, HBX) :: {mr, metamon@internal@runner:morph_spec(HBW, HBX)}.
-file("src/metamon.gleam", 30).
?DOC(" Construct a deterministic seed from an integer.\n").
-spec seed(integer()) -> metamon@generator@seed:seed().
seed(Value) ->
metamon@generator@seed:seed(Value).
-file("src/metamon.gleam", 35).
?DOC(" Convenience: a fresh random seed.\n").
-spec random_seed() -> metamon@generator@seed:seed().
random_seed() ->
metamon@generator@seed:random_seed().
-file("src/metamon.gleam", 40).
?DOC(" Re-export of `Config` and `default_config`.\n").
-spec default_config() -> metamon@config:config().
default_config() ->
metamon@config:default_config().
-file("src/metamon.gleam", 45).
?DOC(" Re-export of `with_seed`.\n").
-spec with_seed(metamon@config:config(), metamon@generator@seed:seed()) -> metamon@config:config().
with_seed(C, S) ->
metamon@config:with_seed(C, S).
-file("src/metamon.gleam", 50).
?DOC(" Re-export of `with_runs`.\n").
-spec with_runs(metamon@config:config(), integer()) -> {ok,
metamon@config:config()} |
{error, metamon@config:config_error()}.
with_runs(C, N) ->
metamon@config:with_runs(C, N).
-file("src/metamon.gleam", 55).
?DOC(" Re-export of `with_max_size`.\n").
-spec with_max_size(metamon@config:config(), integer()) -> {ok,
metamon@config:config()} |
{error, metamon@config:config_error()}.
with_max_size(C, N) ->
metamon@config:with_max_size(C, N).
-file("src/metamon.gleam", 60).
?DOC(" Re-export of `with_shrink_limit`.\n").
-spec with_shrink_limit(metamon@config:config(), integer()) -> {ok,
metamon@config:config()} |
{error, metamon@config:config_error()}.
with_shrink_limit(C, N) ->
metamon@config:with_shrink_limit(C, N).
-file("src/metamon.gleam", 68).
?DOC(" Re-export of `with_max_edges`.\n").
-spec with_max_edges(metamon@config:config(), integer()) -> {ok,
metamon@config:config()} |
{error, metamon@config:config_error()}.
with_max_edges(C, N) ->
metamon@config:with_max_edges(C, N).
-file("src/metamon.gleam", 73).
?DOC(" Re-export of `with_regression_file`.\n").
-spec with_regression_file(metamon@config:config(), binary()) -> {ok,
metamon@config:config()} |
{error, metamon@config:config_error()}.
with_regression_file(C, Path) ->
metamon@config:with_regression_file(C, Path).
-file("src/metamon.gleam", 81).
?DOC(" Re-export of `with_diff_enabled`.\n").
-spec with_diff_enabled(metamon@config:config(), boolean()) -> metamon@config:config().
with_diff_enabled(C, Enabled) ->
metamon@config:with_diff_enabled(C, Enabled).
-file("src/metamon.gleam", 88).
?DOC(
" Re-export of `with_runs_or_panic`. Use in test code where the bound\n"
" is statically known and the `let assert Ok(c) = ...` arm would be\n"
" dead code.\n"
).
-spec with_runs_or_panic(metamon@config:config(), integer()) -> metamon@config:config().
with_runs_or_panic(C, N) ->
metamon@config:with_runs_or_panic(C, N).
-file("src/metamon.gleam", 93).
?DOC(" Re-export of `with_max_size_or_panic`.\n").
-spec with_max_size_or_panic(metamon@config:config(), integer()) -> metamon@config:config().
with_max_size_or_panic(C, N) ->
metamon@config:with_max_size_or_panic(C, N).
-file("src/metamon.gleam", 98).
?DOC(" Re-export of `with_shrink_limit_or_panic`.\n").
-spec with_shrink_limit_or_panic(metamon@config:config(), integer()) -> metamon@config:config().
with_shrink_limit_or_panic(C, N) ->
metamon@config:with_shrink_limit_or_panic(C, N).
-file("src/metamon.gleam", 103).
?DOC(" Re-export of `with_max_edges_or_panic`.\n").
-spec with_max_edges_or_panic(metamon@config:config(), integer()) -> metamon@config:config().
with_max_edges_or_panic(C, N) ->
metamon@config:with_max_edges_or_panic(C, N).
-file("src/metamon.gleam", 108).
?DOC(" Re-export of `with_regression_file_or_panic`.\n").
-spec with_regression_file_or_panic(metamon@config:config(), binary()) -> metamon@config:config().
with_regression_file_or_panic(C, Path) ->
metamon@config:with_regression_file_or_panic(C, Path).
-file("src/metamon.gleam", 119).
?DOC(
" Choose the failure-report output format. `Text` (default) is\n"
" human-friendly; `Json` is single-line JSON for CI / LLM consumers.\n"
).
-spec with_output_format(
metamon@config:config(),
metamon@config:output_format()
) -> metamon@config:config().
with_output_format(C, Fmt) ->
metamon@config:with_output_format(C, Fmt).
-file("src/metamon.gleam", 137).
?DOC(
" Construct a Plain MR. The relation is checked between\n"
" `f(source_input)` and `f(transform.apply(source_input))`.\n"
).
-spec mr(
binary(),
metamon@transform:transform(HCI),
metamon@relation:relation(HCK)
) -> mr(HCI, HCK).
mr(Name, Transform, Relation) ->
{mr, metamon@internal@runner:plain(Name, Transform, Relation)}.
-file("src/metamon.gleam", 148).
?DOC(
" Construct an Equivariant MR. The relation is checked between\n"
" `output_transform.apply(f(source_input))` and\n"
" `f(input_transform.apply(source_input))`.\n"
).
-spec mr_equivariant(
binary(),
metamon@transform:transform(HCO),
metamon@transform:transform(HCQ),
metamon@relation:relation(HCQ)
) -> mr(HCO, HCQ).
mr_equivariant(Name, Input_transform, Output_transform, Relation) ->
{mr,
metamon@internal@runner:equivariant(
Name,
Input_transform,
Output_transform,
Relation
)}.
-file("src/metamon.gleam", 158).
?DOC(" Get the user-facing name of an MR.\n").
-spec name_of(mr(any(), any())) -> binary().
name_of(M) ->
metamon@internal@runner:morph_name(erlang:element(2, M)).
-file("src/metamon.gleam", 170).
?DOC(" Run a property with an explicit configuration.\n").
-spec forall_with(
metamon@config:config(),
metamon@generator:generator(HDB),
fun((HDB) -> boolean())
) -> nil.
forall_with(Cfg, G, Property) ->
metamon@internal@runner:run_forall(Cfg, <<"forall"/utf8>>, G, Property).
-file("src/metamon.gleam", 165).
?DOC(" Run a property over many random inputs.\n").
-spec forall(metamon@generator:generator(HCZ), fun((HCZ) -> boolean())) -> nil.
forall(G, Property) ->
forall_with(default_config(), G, Property).
-file("src/metamon.gleam", 180).
?DOC(" Run a metamorphic relation with an explicit configuration.\n").
-spec forall_morph_with(
metamon@config:config(),
metamon@generator:generator(HDI),
mr(HDI, HDK),
fun((HDI) -> HDK)
) -> nil.
forall_morph_with(Cfg, G, M, F) ->
metamon@internal@runner:run_forall_morph(
Cfg,
<<"forall_morph"/utf8>>,
G,
erlang:element(2, M),
F
).
-file("src/metamon.gleam", 175).
?DOC(" Run a metamorphic relation over many random inputs.\n").
-spec forall_morph(
metamon@generator:generator(HDD),
mr(HDD, HDF),
fun((HDD) -> HDF)
) -> nil.
forall_morph(G, M, F) ->
forall_morph_with(default_config(), G, M, F).
-file("src/metamon.gleam", 190).
?DOC(" Run a metamorphic relation against a single input. Generator-free.\n").
-spec assert_morph(HDN, mr(HDN, HDO), fun((HDN) -> HDO)) -> nil.
assert_morph(Input, M, F) ->
metamon@internal@runner:run_assert_morph(
<<"assert_morph"/utf8>>,
erlang:element(2, M),
F,
Input
).
-file("src/metamon.gleam", 210).
?DOC(" `forall_morph_n` with an explicit configuration.\n").
-spec forall_morph_n_with(
metamon@config:config(),
metamon@generator:generator(HDX),
list(metamon@transform:transform(HDX)),
metamon@relation:relation_n(HEB),
fun((HDX) -> HEB)
) -> nil.
forall_morph_n_with(Cfg, G, Transforms, Rel, F) ->
metamon@internal@runner:run_forall_morph_n(
Cfg,
<<"forall_morph_n"/utf8>>,
G,
Transforms,
Rel,
F
).
-file("src/metamon.gleam", 200).
?DOC(
" Run an N-ary metamorphic relation: apply each of `transforms` to\n"
" the source input to build follow-up inputs, then assert that the\n"
" resulting outputs `[f(x), f(T0(x)), ..., f(Tn(x))]` satisfy\n"
" `relation`. Useful when the property requires comparing more than\n"
" two outputs in one shot (e.g. `(a, b, c) ↦ op(op(a,b), c)` and its\n"
" re-associations all agree).\n"
).
-spec forall_morph_n(
metamon@generator:generator(HDR),
list(metamon@transform:transform(HDR)),
metamon@relation:relation_n(HDV),
fun((HDR) -> HDV)
) -> nil.
forall_morph_n(G, Transforms, Rel, F) ->
forall_morph_n_with(default_config(), G, Transforms, Rel, F).
-file("src/metamon.gleam", 233).
-spec list_map_specs(list(mr(HEJ, HEK))) -> list(metamon@internal@runner:morph_spec(HEJ, HEK)).
list_map_specs(Ms) ->
case Ms of
[] ->
[];
[First | Rest] ->
[erlang:element(2, First) | list_map_specs(Rest)]
end.
-file("src/metamon.gleam", 223).
?DOC(
" Run multiple metamorphic relations against the same generator.\n"
" Each MR is tried independently; failures are collected and reported\n"
" together at the end.\n"
).
-spec forall_morphs(
metamon@generator:generator(HED),
list(mr(HED, HEF)),
fun((HED) -> HEF)
) -> nil.
forall_morphs(G, Ms, F) ->
metamon@internal@runner:run_forall_morphs(
default_config(),
<<"forall_morphs"/utf8>>,
G,
list_map_specs(Ms),
F
).
-file("src/metamon.gleam", 257).
?DOC(
" `f(f(x)) == f(x)`. Idempotency.\n"
"\n"
" Encoded as a Plain MR whose transform is `f` itself and whose\n"
" relation is structural equality.\n"
).
-spec idempotency_of(binary(), fun((HER) -> HER)) -> mr(HER, HER).
idempotency_of(Name, F) ->
Apply_t = metamon@transform:new(<<"apply "/utf8, Name/binary>>, F),
mr(Name, Apply_t, metamon@relation:equal()).
-file("src/metamon.gleam", 263).
?DOC(" `f(T(x)) == f(x)` — `f` is invariant under the input transform.\n").
-spec invariant_under(binary(), metamon@transform:transform(HEU)) -> mr(HEU, any()).
invariant_under(Name, Under) ->
mr(Name, Under, metamon@relation:equal()).
-file("src/metamon.gleam", 269).
?DOC(
" `R(U(f(x)), f(T(x)))`. Equivariance: the input transform `T` and\n"
" the output transform `U` commute with `f` modulo `R`.\n"
).
-spec equivariant_under(
binary(),
metamon@transform:transform(HEZ),
metamon@transform:transform(HFB),
metamon@relation:relation(HFB)
) -> mr(HEZ, HFB).
equivariant_under(Name, Input_transform, Output_transform, Rel) ->
mr_equivariant(Name, Input_transform, Output_transform, Rel).
-file("src/metamon.gleam", 296).
?DOC(
" `op(a, b) == op(b, a)` — `op` is commutative.\n"
"\n"
" The MR is over the input pair `#(a, a)` and the output type `b`.\n"
" Use it as:\n"
"\n"
" ```gleam\n"
" let mr = metamon.commutativity_of(name: \"add_commutative\")\n"
" metamon.forall_morph(\n"
" generator.tuple2(int_gen, int_gen),\n"
" mr,\n"
" fn(pair) { add(pair.0, pair.1) },\n"
" )\n"
" ```\n"
).
-spec commutativity_of(binary()) -> mr({HFG, HFG}, any()).
commutativity_of(Name) ->
Swap = metamon@transform:new(
<<"swap"/utf8>>,
fun(Pair) -> {erlang:element(2, Pair), erlang:element(1, Pair)} end
),
mr(Name, Swap, metamon@relation:equal()).