-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_observable_with/3, forall_observable/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, forall_round_trip_with/5, forall_round_trip/4, forall_round_trip_partial_with/5, forall_round_trip_partial/4, forall_round_trip_under_with/6, forall_round_trip_under/5]). -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_observable`, `forall_morph`, `assert_morph`,\n" " `forall_morphs`, `forall_round_trip`, the `Mr` smart constructors,\n" " and a handful of metamorphic-relation templates (`idempotency_of`,\n" " `invariant_under`, `equivariant_under`, `commutativity_of`).\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(HNI, HNJ) :: {mr, metamon@internal@runner:morph_spec(HNI, HNJ)}. -file("src/metamon.gleam", 32). ?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", 37). ?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", 42). ?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", 47). ?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", 52). ?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", 57). ?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", 62). ?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", 70). ?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", 75). ?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", 83). ?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", 90). ?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", 95). ?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", 100). ?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", 105). ?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", 110). ?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", 121). ?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", 139). ?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(HNU), metamon@relation:relation(HNW) ) -> mr(HNU, HNW). mr(Name, Transform, Relation) -> {mr, metamon@internal@runner:plain(Name, Transform, Relation)}. -file("src/metamon.gleam", 150). ?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(HOA), metamon@transform:transform(HOC), metamon@relation:relation(HOC) ) -> mr(HOA, HOC). mr_equivariant(Name, Input_transform, Output_transform, Relation) -> {mr, metamon@internal@runner:equivariant( Name, Input_transform, Output_transform, Relation )}. -file("src/metamon.gleam", 160). ?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", 172). ?DOC(" Run a property with an explicit configuration.\n"). -spec forall_with( metamon@config:config(), metamon@generator:generator(HON), fun((HON) -> boolean()) ) -> nil. forall_with(Cfg, G, Property) -> metamon@internal@runner:run_forall(Cfg, <<"forall"/utf8>>, G, Property). -file("src/metamon.gleam", 167). ?DOC(" Run a property over many random inputs.\n"). -spec forall(metamon@generator:generator(HOL), fun((HOL) -> boolean())) -> nil. forall(G, Property) -> forall_with(default_config(), G, Property). -file("src/metamon.gleam", 192). ?DOC(" `forall_observable` with an explicit configuration.\n"). -spec forall_observable_with( metamon@config:config(), metamon@generator:generator(HOS), fun((HOS) -> {any(), boolean()}) ) -> nil. forall_observable_with(Cfg, G, Predicate) -> metamon@internal@runner:run_forall( Cfg, <<"forall_observable"/utf8>>, G, fun(Input) -> {Observed, Holds} = Predicate(Input), metamon@annotate:annotate_value( <<"predicate value"/utf8>>, Observed ), Holds end ). -file("src/metamon.gleam", 187). ?DOC( " Run a property whose predicate also exposes its intermediate value.\n" "\n" " `predicate` returns `#(observation, holds)`. `holds` decides whether\n" " the property is satisfied (same as `forall`); `observation` is\n" " recorded under the label `predicate value` and shown in the failure\n" " report — no manual `annotate.annotate_value` is needed.\n" "\n" " Use this when the predicate's intermediate value (typically `f(input)`)\n" " is what determines the branch. Without it, `forall` failure reports\n" " only show the shrunk source input, which can force a debug round-trip\n" " to recover what `f(input)` actually was.\n" ). -spec forall_observable( metamon@generator:generator(HOP), fun((HOP) -> {any(), boolean()}) ) -> nil. forall_observable(G, Predicate) -> forall_observable_with(default_config(), G, Predicate). -file("src/metamon.gleam", 210). ?DOC(" Run a metamorphic relation with an explicit configuration.\n"). -spec forall_morph_with( metamon@config:config(), metamon@generator:generator(HPA), mr(HPA, HPC), fun((HPA) -> HPC) ) -> 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", 205). ?DOC(" Run a metamorphic relation over many random inputs.\n"). -spec forall_morph( metamon@generator:generator(HOV), mr(HOV, HOX), fun((HOV) -> HOX) ) -> nil. forall_morph(G, M, F) -> forall_morph_with(default_config(), G, M, F). -file("src/metamon.gleam", 220). ?DOC(" Run a metamorphic relation against a single input. Generator-free.\n"). -spec assert_morph(HPF, mr(HPF, HPG), fun((HPF) -> HPG)) -> 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", 243). ?DOC(" `forall_morph_n` with an explicit configuration.\n"). -spec forall_morph_n_with( metamon@config:config(), metamon@generator:generator(HPP), list(metamon@transform:transform(HPP)), metamon@relation:relation_n(HPT), fun((HPP) -> HPT) ) -> 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", 233). ?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" "\n" " Passing `[]` for `transforms` is a programming error (vacuous test)\n" " and panics with a structured message.\n" ). -spec forall_morph_n( metamon@generator:generator(HPJ), list(metamon@transform:transform(HPJ)), metamon@relation:relation_n(HPN), fun((HPJ) -> HPN) ) -> nil. forall_morph_n(G, Transforms, Rel, F) -> forall_morph_n_with(default_config(), G, Transforms, Rel, F). -file("src/metamon.gleam", 269). -spec list_map_specs(list(mr(HQB, HQC))) -> list(metamon@internal@runner:morph_spec(HQB, HQC)). list_map_specs(Ms) -> case Ms of [] -> []; [First | Rest] -> [erlang:element(2, First) | list_map_specs(Rest)] end. -file("src/metamon.gleam", 259). ?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" "\n" " Passing `[]` is a programming error (vacuous test) and panics with\n" " a structured message — use `forall(...)` for a single-input property.\n" ). -spec forall_morphs( metamon@generator:generator(HPV), list(mr(HPV, HPX)), fun((HPV) -> HPX) ) -> 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", 296). ?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((HQJ) -> HQJ)) -> mr(HQJ, HQJ). 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", 302). ?DOC(" `f(T(x)) == f(x)` — `f` is invariant under the input transform.\n"). -spec invariant_under(binary(), metamon@transform:transform(HQM)) -> mr(HQM, any()). invariant_under(Name, Under) -> mr(Name, Under, metamon@relation:equal()). -file("src/metamon.gleam", 308). ?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(HQR), metamon@transform:transform(HQT), metamon@relation:relation(HQT) ) -> mr(HQR, HQT). equivariant_under(Name, Input_transform, Output_transform, Rel) -> mr_equivariant(Name, Input_transform, Output_transform, Rel). -file("src/metamon.gleam", 335). ?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({HQY, HQY}, 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()). -file("src/metamon.gleam", 372). ?DOC(" `forall_round_trip` with an explicit configuration.\n"). -spec forall_round_trip_with( metamon@config:config(), metamon@generator:generator(HRI), binary(), fun((HRI) -> HRK), fun((HRK) -> {ok, HRI} | {error, any()}) ) -> nil. forall_round_trip_with(Cfg, Gen, Name, Encode, Decode) -> metamon@internal@runner:run_forall( Cfg, <<<<"round_trip["/utf8, Name/binary>>/binary, "]"/utf8>>, Gen, fun(Input) -> case Decode(Encode(Input)) of {ok, Decoded} -> Decoded =:= Input; {error, _} -> false end end ). -file("src/metamon.gleam", 356). ?DOC( " Run a round-trip property over many random inputs:\n" " `decode(encode(x))` must equal `Ok(x)` for every generated `x`.\n" "\n" " The failure report header is `round_trip[]` so it is\n" " immediately obvious from the panic which round-trip broke. The\n" " underlying machinery is the same as `forall`, including shrinking\n" " of the source input.\n" "\n" " ```gleam\n" " metamon.forall_round_trip(\n" " gen: generator.bit_array(range.constant(0, 16)),\n" " name: \"base64\",\n" " encode: base64.encode,\n" " decode: base64.decode,\n" " )\n" " ```\n" ). -spec forall_round_trip( metamon@generator:generator(HRC), binary(), fun((HRC) -> HRE), fun((HRE) -> {ok, HRC} | {error, any()}) ) -> nil. forall_round_trip(Gen, Name, Encode, Decode) -> forall_round_trip_with(default_config(), Gen, Name, Encode, Decode). -file("src/metamon.gleam", 444). ?DOC( " `forall_round_trip_partial` with an explicit configuration.\n" "\n" " The runner automatically registers a `coverage.cover_at_least(1,\n" " \"encoder_accepted\", ok)` requirement so a property whose generator\n" " is so wide that the encoder rejects every sample fails the test\n" " with a structured \"coverage shortfall\" message rather than passing\n" " silently. To enforce a stricter floor (e.g. \"at least 50% of\n" " inputs must round-trip\"), call\n" " `coverage.cover(50.0, \"encoder_accepted\", ok)` inside your own\n" " property body — coverage labels accumulate across calls.\n" ). -spec forall_round_trip_partial_with( metamon@config:config(), metamon@generator:generator(HRX), binary(), fun((HRX) -> {ok, HRZ} | {error, any()}), fun((HRZ) -> {ok, HRX} | {error, any()}) ) -> nil. forall_round_trip_partial_with(Cfg, Gen, Name, Encode, Decode) -> metamon@internal@runner:run_forall( Cfg, <<<<"round_trip["/utf8, Name/binary>>/binary, "]"/utf8>>, Gen, fun(Input) -> case Encode(Input) of {error, _} -> metamon@coverage:cover_at_least( 1, <<"encoder_accepted"/utf8>>, false ), true; {ok, Encoded} -> metamon@coverage:cover_at_least( 1, <<"encoder_accepted"/utf8>>, true ), case Decode(Encoded) of {ok, Decoded} -> Decoded =:= Input; {error, _} -> false end end end ). -file("src/metamon.gleam", 419). ?DOC( " Run a round-trip property where the encoder is partial: the\n" " encoder returns `Result(b, e_enc)` because not every generated\n" " input is a valid input for the codec. Inputs the encoder rejects\n" " (`Error(_)`) are treated as out of scope and skipped — the\n" " property succeeds for them.\n" "\n" " Use this variant for codecs whose encoder has structural\n" " preconditions: byte-alignment requirements, value-range checks,\n" " hrp / version constraints, etc. A typical pattern is to combine\n" " `forall_round_trip_partial` with a generator that produces the\n" " surrounding inputs (e.g. arbitrary `BitArray` plus arbitrary\n" " version bytes); the encoder filters down to the valid subset and\n" " the property checks the round-trip on that subset.\n" "\n" " The failure report header is `round_trip[]` so it is\n" " immediately obvious from the panic which round-trip broke. The\n" " underlying machinery is the same as `forall`, including shrinking\n" " of the source input.\n" "\n" " ```gleam\n" " metamon.forall_round_trip_partial(\n" " gen: generator.tuple2(byte_gen, payload_gen),\n" " name: \"base58check\",\n" " encode: fn(pair) { base58check.encode(pair.0, pair.1) },\n" " decode: fn(s) {\n" " case base58check.decode(s) {\n" " Ok(decoded) -> Ok(#(decoded.version, decoded.payload))\n" " Error(e) -> Error(e)\n" " }\n" " },\n" " )\n" " ```\n" ). -spec forall_round_trip_partial( metamon@generator:generator(HRO), binary(), fun((HRO) -> {ok, HRQ} | {error, any()}), fun((HRQ) -> {ok, HRO} | {error, any()}) ) -> nil. forall_round_trip_partial(Gen, Name, Encode, Decode) -> forall_round_trip_partial_with(default_config(), Gen, Name, Encode, Decode). -file("src/metamon.gleam", 520). ?DOC(" `forall_round_trip_under` with an explicit configuration.\n"). -spec forall_round_trip_under_with( metamon@config:config(), metamon@generator:generator(HSN), binary(), fun((HSN) -> HSP), fun((HSP) -> {ok, HSN} | {error, any()}), metamon@relation:relation(HSN) ) -> nil. forall_round_trip_under_with(Cfg, Gen, Name, Encode, Decode, Equality) -> metamon@internal@runner:run_forall( Cfg, <<<<"round_trip["/utf8, Name/binary>>/binary, "]"/utf8>>, Gen, fun(Input) -> case Decode(Encode(Input)) of {ok, Decoded} -> (erlang:element(3, Equality))(Decoded, Input); {error, _} -> false end end ). -file("src/metamon.gleam", 502). ?DOC( " Run a round-trip property using a caller-supplied equality\n" " `Relation(a)` instead of structural `==`. The decoded value must\n" " satisfy `equality.holds(decoded, input)`.\n" "\n" " Use this when the source type carries values that are not\n" " preserved verbatim across encode → decode: opaque types whose\n" " decoded form normalises (e.g. multipart `Part` with re-derived\n" " convenience fields), MIME types that lowercase the essence, JSON\n" " values whose key order is implementation-defined. Composing with\n" " `relation.equivalent_under(via, name)` lets you compare on a\n" " projection (e.g. `headers + body` ignoring derived caches).\n" "\n" " The failure report header is `round_trip[]`. The relation's\n" " own `name` appears under `relation:` in the failure block, just\n" " like `forall_morph` failures.\n" "\n" " ```gleam\n" " metamon.forall_round_trip_under(\n" " gen: parts_gen(),\n" " name: \"multipart_round_trip\",\n" " encode: fn(parts) { multipartkit.encode(boundary, parts) },\n" " decode: fn(body) { multipartkit.parse(body, content_type) },\n" " equality: relation.equivalent_under(\n" " fn(parts) { list.map(parts, part_payload) },\n" " \"wire_payload\",\n" " ),\n" " )\n" " ```\n" ). -spec forall_round_trip_under( metamon@generator:generator(HSG), binary(), fun((HSG) -> HSI), fun((HSI) -> {ok, HSG} | {error, any()}), metamon@relation:relation(HSG) ) -> nil. forall_round_trip_under(Gen, Name, Encode, Decode, Equality) -> forall_round_trip_under_with( default_config(), Gen, Name, Encode, Decode, Equality ).