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

Mr

opaque </>

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

Re-export of the seed type so callers can write metamon.Seed.

pub type Seed =
  seed.Seed

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 default_config() -> config.Config

Re-export of Config and default_config.

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 name_of(m: Mr(a, b)) -> String

Get the user-facing name of an MR.

pub fn random_seed() -> seed.Seed

Convenience: a fresh random seed.

pub fn seed(value: Int) -> seed.Seed

Construct a deterministic seed from an integer.

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.

Search Document