metamon
Metamon top-level public API.
metamon exports the small surface that most tests interact with:
forall, forall_observable, forall_morph, assert_morph,
forall_morphs, forall_round_trip, the Mr smart constructors,
and a handful of metamorphic-relation templates (idempotency_of,
invariant_under, equivariant_under, commutativity_of).
Configuration lives in metamon/config; generators in
metamon/generator; transforms in metamon/transform; relations
in metamon/relation; per-property context in metamon/annotate
and metamon/coverage; structural diff in metamon/diff.
Types
Re-export of Config so callers can write metamon.Config.
pub type Config =
config.Config
A named metamorphic relation. Construct via mr (the Plain form,
f(T(x)) ≈ f(x)) or mr_equivariant (the Equivariant form,
f(T(x)) ≈ U(f(x))).
Opaque on purpose: future versions may add fields without breaking callers that always go through the smart constructors.
pub opaque type Mr(a, b)
Re-export of OutputFormat so callers can write
metamon.OutputFormat.
pub type OutputFormat =
config.OutputFormat
Values
pub fn assert_morph(input: a, m: Mr(a, b), f: fn(a) -> b) -> Nil
Run a metamorphic relation against a single input. Generator-free.
pub fn commutativity_of(name name: String) -> Mr(#(a, a), b)
op(a, b) == op(b, a) — op is commutative.
The MR is over the input pair #(a, a) and the output type b.
Use it as:
let mr = metamon.commutativity_of(name: "add_commutative")
metamon.forall_morph(
generator.tuple2(int_gen, int_gen),
mr,
fn(pair) { add(pair.0, pair.1) },
)
pub fn equivariant_under(
name name: String,
input input_transform: transform.Transform(a),
output output_transform: transform.Transform(b),
relation rel: relation.Relation(b),
) -> Mr(a, b)
R(U(f(x)), f(T(x))). Equivariance: the input transform T and
the output transform U commute with f modulo R.
pub fn forall(
g: generator.Generator(a),
property: fn(a) -> Bool,
) -> Nil
Run a property over many random inputs.
pub fn forall_morph(
g: generator.Generator(a),
m: Mr(a, b),
f: fn(a) -> b,
) -> Nil
Run a metamorphic relation over many random inputs.
pub fn forall_morph_n(
g: generator.Generator(a),
transforms: List(transform.Transform(a)),
rel: relation.RelationN(b),
f: fn(a) -> b,
) -> Nil
Run an N-ary metamorphic relation: apply each of transforms to
the source input to build follow-up inputs, then assert that the
resulting outputs [f(x), f(T0(x)), ..., f(Tn(x))] satisfy
relation. Useful when the property requires comparing more than
two outputs in one shot (e.g. (a, b, c) ↦ op(op(a,b), c) and its
re-associations all agree).
Passing [] for transforms is a programming error (vacuous test)
and panics with a structured message.
pub fn forall_morph_n_with(
cfg: config.Config,
g: generator.Generator(a),
transforms: List(transform.Transform(a)),
rel: relation.RelationN(b),
f: fn(a) -> b,
) -> Nil
forall_morph_n with an explicit configuration.
pub fn forall_morph_with(
cfg: config.Config,
g: generator.Generator(a),
m: Mr(a, b),
f: fn(a) -> b,
) -> Nil
Run a metamorphic relation with an explicit configuration.
pub fn forall_morphs(
g: generator.Generator(a),
ms: List(Mr(a, b)),
f: fn(a) -> b,
) -> Nil
Run multiple metamorphic relations against the same generator. Each MR is tried independently; failures are collected and reported together at the end.
Passing [] is a programming error (vacuous test) and panics with
a structured message — use forall(...) for a single-input property.
pub fn forall_observable(
g: generator.Generator(a),
predicate: fn(a) -> #(b, Bool),
) -> Nil
Run a property whose predicate also exposes its intermediate value.
predicate returns #(observation, holds). holds decides whether
the property is satisfied (same as forall); observation is
recorded under the label predicate value and shown in the failure
report — no manual annotate.annotate_value is needed.
Use this when the predicate’s intermediate value (typically f(input))
is what determines the branch. Without it, forall failure reports
only show the shrunk source input, which can force a debug round-trip
to recover what f(input) actually was.
pub fn forall_observable_with(
cfg: config.Config,
g: generator.Generator(a),
predicate: fn(a) -> #(b, Bool),
) -> Nil
forall_observable with an explicit configuration.
pub fn forall_round_trip(
gen gen: generator.Generator(a),
name name: String,
encode encode: fn(a) -> b,
decode decode: fn(b) -> Result(a, e),
) -> Nil
Run a round-trip property over many random inputs:
decode(encode(x)) must equal Ok(x) for every generated x.
The failure report header is round_trip[<name>] so it is
immediately obvious from the panic which round-trip broke. The
underlying machinery is the same as forall, including shrinking
of the source input.
metamon.forall_round_trip(
gen: generator.bit_array(range.constant(0, 16)),
name: "base64",
encode: base64.encode,
decode: base64.decode,
)
pub fn forall_round_trip_partial(
gen gen: generator.Generator(a),
name name: String,
encode encode: fn(a) -> Result(b, e_enc),
decode decode: fn(b) -> Result(a, e_dec),
) -> Nil
Run a round-trip property where the encoder is partial: the
encoder returns Result(b, e_enc) because not every generated
input is a valid input for the codec. Inputs the encoder rejects
(Error(_)) are treated as out of scope and skipped — the
property succeeds for them.
Use this variant for codecs whose encoder has structural
preconditions: byte-alignment requirements, value-range checks,
hrp / version constraints, etc. A typical pattern is to combine
forall_round_trip_partial with a generator that produces the
surrounding inputs (e.g. arbitrary BitArray plus arbitrary
version bytes); the encoder filters down to the valid subset and
the property checks the round-trip on that subset.
The failure report header is round_trip[<name>] so it is
immediately obvious from the panic which round-trip broke. The
underlying machinery is the same as forall, including shrinking
of the source input.
metamon.forall_round_trip_partial(
gen: generator.tuple2(byte_gen, payload_gen),
name: "base58check",
encode: fn(pair) { base58check.encode(pair.0, pair.1) },
decode: fn(s) {
case base58check.decode(s) {
Ok(decoded) -> Ok(#(decoded.version, decoded.payload))
Error(e) -> Error(e)
}
},
)
pub fn forall_round_trip_partial_with(
cfg cfg: config.Config,
gen gen: generator.Generator(a),
name name: String,
encode encode: fn(a) -> Result(b, e_enc),
decode decode: fn(b) -> Result(a, e_dec),
) -> Nil
forall_round_trip_partial with an explicit configuration.
The runner automatically registers a coverage.cover_at_least(1, "encoder_accepted", ok) requirement so a property whose generator
is so wide that the encoder rejects every sample fails the test
with a structured “coverage shortfall” message rather than passing
silently. To enforce a stricter floor (e.g. “at least 50% of
inputs must round-trip”), call
coverage.cover(50.0, "encoder_accepted", ok) inside your own
property body — coverage labels accumulate across calls.
pub fn forall_round_trip_under(
gen gen: generator.Generator(a),
name name: String,
encode encode: fn(a) -> b,
decode decode: fn(b) -> Result(a, e),
equality equality: relation.Relation(a),
) -> Nil
Run a round-trip property using a caller-supplied equality
Relation(a) instead of structural ==. The decoded value must
satisfy equality.holds(decoded, input).
Use this when the source type carries values that are not
preserved verbatim across encode → decode: opaque types whose
decoded form normalises (e.g. multipart Part with re-derived
convenience fields), MIME types that lowercase the essence, JSON
values whose key order is implementation-defined. Composing with
relation.equivalent_under(via, name) lets you compare on a
projection (e.g. headers + body ignoring derived caches).
The failure report header is round_trip[<name>]. The relation’s
own name appears under relation: in the failure block, just
like forall_morph failures.
metamon.forall_round_trip_under(
gen: parts_gen(),
name: "multipart_round_trip",
encode: fn(parts) { multipartkit.encode(boundary, parts) },
decode: fn(body) { multipartkit.parse(body, content_type) },
equality: relation.equivalent_under(
fn(parts) { list.map(parts, part_payload) },
"wire_payload",
),
)
pub fn forall_round_trip_under_with(
cfg cfg: config.Config,
gen gen: generator.Generator(a),
name name: String,
encode encode: fn(a) -> b,
decode decode: fn(b) -> Result(a, e),
equality equality: relation.Relation(a),
) -> Nil
forall_round_trip_under with an explicit configuration.
pub fn forall_round_trip_with(
cfg cfg: config.Config,
gen gen: generator.Generator(a),
name name: String,
encode encode: fn(a) -> b,
decode decode: fn(b) -> Result(a, e),
) -> Nil
forall_round_trip with an explicit configuration.
pub fn forall_with(
cfg: config.Config,
g: generator.Generator(a),
property: fn(a) -> Bool,
) -> Nil
Run a property with an explicit configuration.
pub fn idempotency_of(
name name: String,
of f: fn(a) -> a,
) -> Mr(a, a)
f(f(x)) == f(x). Idempotency.
Encoded as a Plain MR whose transform is f itself and whose
relation is structural equality.
pub fn invariant_under(
name name: String,
under under: transform.Transform(a),
) -> Mr(a, b)
f(T(x)) == f(x) — f is invariant under the input transform.
pub fn mr(
name name: String,
transform transform: transform.Transform(a),
relation relation: relation.Relation(b),
) -> Mr(a, b)
Construct a Plain MR. The relation is checked between
f(source_input) and f(transform.apply(source_input)).
pub fn mr_equivariant(
name name: String,
input input_transform: transform.Transform(a),
output output_transform: transform.Transform(b),
relation relation: relation.Relation(b),
) -> Mr(a, b)
Construct an Equivariant MR. The relation is checked between
output_transform.apply(f(source_input)) and
f(input_transform.apply(source_input)).
pub fn with_diff_enabled(
c: config.Config,
enabled: Bool,
) -> config.Config
Re-export of with_diff_enabled.
pub fn with_max_edges(
c: config.Config,
n: Int,
) -> Result(config.Config, config.ConfigError)
Re-export of with_max_edges.
pub fn with_max_edges_or_panic(
c: config.Config,
n: Int,
) -> config.Config
Re-export of with_max_edges_or_panic.
pub fn with_max_size(
c: config.Config,
n: Int,
) -> Result(config.Config, config.ConfigError)
Re-export of with_max_size.
pub fn with_max_size_or_panic(
c: config.Config,
n: Int,
) -> config.Config
Re-export of with_max_size_or_panic.
pub fn with_output_format(
c: config.Config,
fmt: config.OutputFormat,
) -> config.Config
Choose the failure-report output format. Text (default) is
human-friendly; Json is single-line JSON for CI / LLM consumers.
pub fn with_regression_file(
c: config.Config,
path: String,
) -> Result(config.Config, config.ConfigError)
Re-export of with_regression_file.
pub fn with_regression_file_or_panic(
c: config.Config,
path: String,
) -> config.Config
Re-export of with_regression_file_or_panic.
pub fn with_runs(
c: config.Config,
n: Int,
) -> Result(config.Config, config.ConfigError)
Re-export of with_runs.
pub fn with_runs_or_panic(
c: config.Config,
n: Int,
) -> config.Config
Re-export of with_runs_or_panic. Use in test code where the bound
is statically known and the let assert Ok(c) = ... arm would be
dead code.
pub fn with_seed(c: config.Config, s: seed.Seed) -> config.Config
Re-export of with_seed.
pub fn with_shrink_limit(
c: config.Config,
n: Int,
) -> Result(config.Config, config.ConfigError)
Re-export of with_shrink_limit.
pub fn with_shrink_limit_or_panic(
c: config.Config,
n: Int,
) -> config.Config
Re-export of with_shrink_limit_or_panic.