Packages

Property-based testing and metamorphic testing combinator library for Gleam

Current section

Files

Jump to
metamon src metamon@coverage.erl
Raw

src/metamon@coverage.erl

-module(metamon@coverage).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/metamon/coverage.gleam").
-export([target_pct_of/2, actual_pct/2, shortfalls/1, hits_for/2, requirements_of/1, collected_of/1, first_shortfall/1, classify/2, classify_in_bucket/2, cover/3, cover_at_least/3, collect/2, reset/0, snapshot/0]).
-export_type([requirement/0, requirement_kind/0, snapshot/0]).
-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(
" Coverage and classification primitives for property-based tests.\n"
"\n"
" `classify` tags a generated input with a label so the runner can\n"
" report the distribution at the end of a successful run.\n"
" `cover` additionally asserts a minimum percentage — if fewer\n"
" inputs hit the label than required, the property fails even when\n"
" every individual run passed.\n"
" `collect` records the value itself (via a user-supplied `show`)\n"
" for histogram-style reporting.\n"
).
-type requirement() :: {requirement, binary(), requirement_kind(), integer()}.
-type requirement_kind() :: {pct, float()} | {count, integer()}.
-type snapshot() :: {snapshot,
integer(),
gleam@dict:dict(binary(), integer()),
list(requirement()),
gleam@dict:dict(binary(), integer())}.
-file("src/metamon/coverage.gleam", 46).
?DOC(
" Backwards-compat helper: extract the percentage target if the\n"
" requirement is a Pct one, otherwise compute it from the\n"
" minimum-count requirement and the recorded total. Used by the\n"
" failure formatter and the `target_pct` accessor in older code.\n"
).
-spec target_pct_of(requirement(), integer()) -> float().
target_pct_of(Req, Total) ->
case erlang:element(3, Req) of
{pct, Target} ->
Target;
{count, Min} ->
case Total =< 0 of
true ->
+0.0;
false ->
(case erlang:float(Total) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:float(Min) / Gleam@denominator
end) * 100.0
end
end.
-file("src/metamon/coverage.gleam", 160).
?DOC(" `(hits / total) * 100`, returning `0.0` if `total` is zero.\n").
-spec actual_pct(integer(), integer()) -> float().
actual_pct(Hits, Total) ->
case Total =< 0 of
true ->
+0.0;
false ->
(case erlang:float(Total) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:float(Hits) / Gleam@denominator
end) * 100.0
end.
-file("src/metamon/coverage.gleam", 146).
?DOC(
" Find requirements whose actual coverage falls short of the target.\n"
" Pct requirements are checked against `actual_pct`; Count\n"
" requirements are checked against the absolute hit count.\n"
).
-spec shortfalls(snapshot()) -> list(requirement()).
shortfalls(Snap) ->
case erlang:element(2, Snap) =< 0 of
true ->
[];
false ->
gleam@list:filter(
erlang:element(4, Snap),
fun(Req) -> case erlang:element(3, Req) of
{pct, Target} ->
actual_pct(
erlang:element(4, Req),
erlang:element(2, Snap)
)
< Target;
{count, Min} ->
erlang:element(4, Req) < Min
end end
)
end.
-file("src/metamon/coverage.gleam", 240).
?DOC(" Read a single label's hit count.\n").
-spec hits_for(snapshot(), binary()) -> integer().
hits_for(Snap, Label) ->
case gleam_stdlib:map_get(erlang:element(3, Snap), Label) of
{ok, N} ->
N;
{error, _} ->
0
end.
-file("src/metamon/coverage.gleam", 248).
?DOC(" Convenience accessor used by the runner / report.\n").
-spec requirements_of(snapshot()) -> list(requirement()).
requirements_of(Snap) ->
erlang:element(4, Snap).
-file("src/metamon/coverage.gleam", 253).
?DOC(" Convenience accessor used by the runner / report.\n").
-spec collected_of(snapshot()) -> gleam@dict:dict(binary(), integer()).
collected_of(Snap) ->
erlang:element(5, Snap).
-file("src/metamon/coverage.gleam", 258).
?DOC(" `Some(req)` when at least one shortfall exists, otherwise `None`.\n").
-spec first_shortfall(snapshot()) -> gleam@option:option(requirement()).
first_shortfall(Snap) ->
case shortfalls(Snap) of
[] ->
none;
[First | _] ->
{some, First}
end.
-file("src/metamon/coverage.gleam", 193).
-spec read_dict(binary()) -> gleam@dict:dict(binary(), integer()).
read_dict(Key) ->
case metamon_ffi:state_get(Key) of
{error, _} ->
maps:new();
{ok, Raw} ->
metamon_ffi:identity(Raw)
end.
-file("src/metamon/coverage.gleam", 180).
-spec bump_count(binary(), binary()) -> nil.
bump_count(Key, Label) ->
Counts = read_dict(Key),
Next = gleam@dict:upsert(Counts, Label, fun(Existing) -> case Existing of
{some, N} ->
N + 1;
none ->
1
end end),
metamon_ffi:state_put(Key, Next),
nil.
-file("src/metamon/coverage.gleam", 173).
-spec read_total() -> integer().
read_total() ->
case metamon_ffi:state_get(<<"coverage_total"/utf8>>) of
{error, _} ->
0;
{ok, Raw} ->
metamon_ffi:identity(Raw)
end.
-file("src/metamon/coverage.gleam", 167).
-spec bump_total() -> nil.
bump_total() ->
Updated = read_total() + 1,
metamon_ffi:state_put(<<"coverage_total"/utf8>>, Updated),
nil.
-file("src/metamon/coverage.gleam", 70).
?DOC(
" Tag the current input with `label` if `condition` is true. Only\n"
" labelled hits are counted, but every call to `classify` advances\n"
" the total-runs denominator.\n"
).
-spec classify(binary(), boolean()) -> nil.
classify(Label, Condition) ->
bump_total(),
case Condition of
false ->
nil;
true ->
bump_count(<<"coverage_counts"/utf8>>, Label)
end.
-file("src/metamon/coverage.gleam", 108).
?DOC(
" Tag the current input as belonging to a mutually-exclusive\n"
" `bucket` within `group`. Buckets are recorded as\n"
" `\"<group>=<bucket>\"` so the failure report can show distribution\n"
" by group at a glance. Calling `classify_in_bucket` more than once\n"
" per input within the same `group` is a programming error and is\n"
" silently kept (the runner does not enforce mutual exclusion).\n"
).
-spec classify_in_bucket(binary(), binary()) -> nil.
classify_in_bucket(Group, Bucket) ->
classify(<<<<Group/binary, "="/utf8>>/binary, Bucket/binary>>, true).
-file("src/metamon/coverage.gleam", 221).
-spec read_requirements() -> list(requirement()).
read_requirements() ->
case metamon_ffi:state_get(<<"coverage_requirements"/utf8>>) of
{error, _} ->
[];
{ok, Raw} ->
metamon_ffi:identity(Raw)
end.
-file("src/metamon/coverage.gleam", 208).
-spec ensure_requirement(binary(), requirement_kind()) -> nil.
ensure_requirement(Label, Kind) ->
Current = read_requirements(),
Already = gleam@list:any(
Current,
fun(Req) -> erlang:element(2, Req) =:= Label end
),
case Already of
true ->
nil;
false ->
Updated = [{requirement, Label, Kind, 0} | Current],
metamon_ffi:state_put(<<"coverage_requirements"/utf8>>, Updated),
nil
end.
-file("src/metamon/coverage.gleam", 200).
-spec ensure_pct_requirement(binary(), float()) -> nil.
ensure_pct_requirement(Label, Target_pct) ->
ensure_requirement(Label, {pct, Target_pct}).
-file("src/metamon/coverage.gleam", 80).
?DOC(
" Like `classify` but also asserts that the label hits at least\n"
" `target_pct` percent of all inputs in the run.\n"
).
-spec cover(float(), binary(), boolean()) -> nil.
cover(Target_pct, Label, Condition) ->
bump_total(),
ensure_pct_requirement(Label, Target_pct),
case Condition of
false ->
nil;
true ->
bump_count(<<"coverage_counts"/utf8>>, Label)
end.
-file("src/metamon/coverage.gleam", 204).
-spec ensure_count_requirement(binary(), integer()) -> nil.
ensure_count_requirement(Label, Min_hits) ->
ensure_requirement(Label, {count, Min_hits}).
-file("src/metamon/coverage.gleam", 93).
?DOC(
" Absolute-count variant of `cover`: assert that the label is hit\n"
" at least `min_hits` times across the entire run. Useful when you\n"
" know the exact number of edge cases that should fire (e.g. \"at\n"
" least 3 inputs trigger the empty-list path\").\n"
).
-spec cover_at_least(integer(), binary(), boolean()) -> nil.
cover_at_least(Min_hits, Label, Condition) ->
bump_total(),
ensure_count_requirement(Label, Min_hits),
case Condition of
false ->
nil;
true ->
bump_count(<<"coverage_counts"/utf8>>, Label)
end.
-file("src/metamon/coverage.gleam", 228).
-spec enriched_requirements(gleam@dict:dict(binary(), integer())) -> list(requirement()).
enriched_requirements(Counts) ->
_pipe = read_requirements(),
gleam@list:map(
_pipe,
fun(Req) ->
Hits = case gleam_stdlib:map_get(Counts, erlang:element(2, Req)) of
{ok, N} ->
N;
{error, _} ->
0
end,
{requirement, erlang:element(2, Req), erlang:element(3, Req), Hits}
end
).
-file("src/metamon/coverage.gleam", 114).
?DOC(
" Render `value` via `show` and add the result to the histogram of\n"
" collected values.\n"
).
-spec collect(FNT, fun((FNT) -> binary())) -> nil.
collect(Value, Show) ->
bump_total(),
bump_count(<<"coverage_collected"/utf8>>, Show(Value)).
-file("src/metamon/coverage.gleam", 121).
?DOC(
" Reset all coverage state. Called by the runner at the start of\n"
" each property.\n"
).
-spec reset() -> nil.
reset() ->
metamon_ffi:state_erase(<<"coverage_counts"/utf8>>),
metamon_ffi:state_erase(<<"coverage_total"/utf8>>),
metamon_ffi:state_erase(<<"coverage_requirements"/utf8>>),
metamon_ffi:state_erase(<<"coverage_collected"/utf8>>),
nil.
-file("src/metamon/coverage.gleam", 130).
?DOC(" Read the current coverage snapshot.\n").
-spec snapshot() -> snapshot().
snapshot() ->
Total = read_total(),
Counts = read_dict(<<"coverage_counts"/utf8>>),
Requirements = enriched_requirements(Counts),
Collected = read_dict(<<"coverage_collected"/utf8>>),
{snapshot, Total, Counts, Requirements, Collected}.