-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", 47). ?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", 117). -spec validate_target_pct(float()) -> nil. validate_target_pct(Target_pct) -> case (Target_pct >= +0.0) andalso (Target_pct =< 100.0) of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<<<"metamon.coverage.cover: target_pct must be in [0.0, 100.0] (got "/utf8, (gleam_stdlib:float_to_string(Target_pct))/binary>>/binary, ")"/utf8>>), file => <>, module => <<"metamon/coverage"/utf8>>, function => <<"validate_target_pct"/utf8>>, line => 126}) end. -file("src/metamon/coverage.gleam", 134). -spec validate_min_hits(integer()) -> nil. validate_min_hits(Min_hits) -> case Min_hits < 0 of true -> erlang:error(#{gleam_error => panic, message => (<<<<"metamon.coverage.cover_at_least: min_hits must be >= 0 (got "/utf8, (erlang:integer_to_binary(Min_hits))/binary>>/binary, ")"/utf8>>), file => <>, module => <<"metamon/coverage"/utf8>>, function => <<"validate_min_hits"/utf8>>, line => 137}); false -> nil end. -file("src/metamon/coverage.gleam", 204). ?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", 190). ?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", 284). ?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", 292). ?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", 297). ?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", 302). ?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", 237). -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", 224). -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", 217). -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", 211). -spec bump_total() -> nil. bump_total() -> Updated = read_total() + 1, metamon_ffi:state_put(<<"coverage_total"/utf8>>, Updated), nil. -file("src/metamon/coverage.gleam", 71). ?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", 152). ?DOC( " Tag the current input as belonging to a mutually-exclusive\n" " `bucket` within `group`. Buckets are recorded as\n" " `\"=\"` 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(<<< 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", 252). -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", 244). -spec ensure_pct_requirement(binary(), float()) -> nil. ensure_pct_requirement(Label, Target_pct) -> ensure_requirement(Label, {pct, Target_pct}). -file("src/metamon/coverage.gleam", 88). ?DOC( " Like `classify` but also asserts that the label hits at least\n" " `target_pct` percent of all inputs in the run.\n" "\n" " `target_pct` must be a finite number in `[0.0, 100.0]`. Values\n" " outside that range — and `NaN` — are programming errors (an\n" " off-by-percent typo turns a target of 50 into 500, a negative\n" " target trivially passes for zero hits) and panic with a structured\n" " message so the misconfiguration surfaces immediately rather than\n" " as a confusing coverage shortfall.\n" ). -spec cover(float(), binary(), boolean()) -> nil. cover(Target_pct, Label, Condition) -> validate_target_pct(Target_pct), 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", 248). -spec ensure_count_requirement(binary(), integer()) -> nil. ensure_count_requirement(Label, Min_hits) -> ensure_requirement(Label, {count, Min_hits}). -file("src/metamon/coverage.gleam", 107). ?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" "\n" " `min_hits` must be `>= 0`. A negative threshold is trivially\n" " satisfied (zero hits clear a negative bar) and almost always a\n" " sign mistake on the caller's part, so it panics with a structured\n" " message at the call site.\n" ). -spec cover_at_least(integer(), binary(), boolean()) -> nil. cover_at_least(Min_hits, Label, Condition) -> validate_min_hits(Min_hits), 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", 272). -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", 158). ?DOC( " Render `value` via `show` and add the result to the histogram of\n" " collected values.\n" ). -spec collect(ECZ, fun((ECZ) -> binary())) -> nil. collect(Value, Show) -> bump_total(), bump_count(<<"coverage_collected"/utf8>>, Show(Value)). -file("src/metamon/coverage.gleam", 165). ?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", 174). ?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}.