From 754c8fa1b959ec406dc2d1407852573f4f4efc04 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Mon, 26 Oct 2026 18:38:35 +0000 Subject: [PATCH] blogex: coverage floor (74) + test-scaffolding exclusions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_coverage` sets the floor the Lens pass enforces, measured first: 74.53% with test/support excluded (`mix test --cover` and the exported sample give the same number). The rule — a FLOOR, ratcheted up as behavior yaks land, never lowered — is written next to the config, along with why each module is excluded (test scaffolding only: fixtures measure the tests, not the library). Wiring, per the fast/push split: `test.coverage` runs the suite once with `--cover --export-coverage default`; the `coverage` sensor then enforces the floor from that export via `mix test.coverage` (no second suite run). So `verify` runs the suite first and the sensors after it. The sensor guards on the export existing: `mix test.coverage` exits 0 when it finds no .coverdata, and a floor check that did not run must not report success. blogex/.gitignore: cover/ (generated). --- blogex/.gitignore | 1 + blogex/Makefile | 31 ++++++++++++++++++++++++++----- blogex/mix.exs | 25 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/blogex/.gitignore b/blogex/.gitignore index c19f686..49a0bc5 100644 --- a/blogex/.gitignore +++ b/blogex/.gitignore @@ -1,6 +1,7 @@ /_build/ /deps/ /doc/ +/cover/ *.ez *.beam .elixir_ls/ diff --git a/blogex/Makefile b/blogex/Makefile index 3667b13..618feeb 100644 --- a/blogex/Makefile +++ b/blogex/Makefile @@ -3,7 +3,7 @@ MISE_BIN ?= mise MISE_EXEC = $(MISE_BIN) exec -- -.PHONY: check precommit deps compile test format verify +.PHONY: check precommit deps compile test test.coverage coverage format verify # Cheap static gate for this library (no database, no coverage). Runs from the # root `make check`, which loops app then blogex. @@ -30,6 +30,20 @@ compile: test: deps $(MISE_EXEC) mix test +# Run the suite once with coverage exported, so the coverage floor check (and, +# later, CRAP) reads the export instead of running the suite a second time. +# Stale exports are removed first: the floor must never report an older run. +test.coverage: deps + rm -f cover/*.coverdata + $(MISE_EXEC) mix test --cover --export-coverage default + +# Enforce the coverage floor (mix.exs -> test_coverage) from that export. No +# test run. `mix test.coverage` exits 0 when it finds no export at all, so the +# guard below is load-bearing: a floor check that did not run must not pass. +coverage: + @test -f cover/default.coverdata || { echo "coverage: no exported coverage - run 'make test.coverage' first" >&2; exit 1; } + $(MISE_EXEC) mix test.coverage + # Format code format: $(MISE_EXEC) mix format @@ -42,11 +56,18 @@ format: # and to VERIFY_SENSORS — that variable is the single source of truth for what # this target reports as wired. # - coverage (yak: coverage floors + framework-glue exclusions policy) +# WIRED below (test.coverage + coverage). # - crap (yak: CRAP scores with ex_crap (both apps)) # - mutation (yak: mutation testing with muex — a sensor, not a gate) -VERIFY_SENSORS := +# +# The suite runs FIRST: it is what produces the coverage export that the +# sensors below read. (Sampling and reporting are separate steps because +# `mix test --cover --export-coverage` exports instead of printing a summary, +# and reporting through `mix test.coverage` keeps it to one suite run.) Sensors +# still run cheapest-first among themselves. +VERIFY_SENSORS := coverage verify: - @echo "==> verify(blogex): sensors [$(if $(VERIFY_SENSORS),$(VERIFY_SENSORS),none wired yet)]" - @for sensor in $(VERIFY_SENSORS); do $(MAKE) $$sensor || exit 1; done - @$(MAKE) test + @echo "==> verify(blogex): suite (coverage exported), then sensors [$(VERIFY_SENSORS)]" + @$(MAKE) test.coverage + @for sensor in $(VERIFY_SENSORS); do echo "==> verify(blogex): sensor $$sensor"; $(MAKE) $$sensor || exit 1; done diff --git a/blogex/mix.exs b/blogex/mix.exs index c6e03a5..4796317 100644 --- a/blogex/mix.exs +++ b/blogex/mix.exs @@ -13,6 +13,7 @@ defmodule Blogex.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), + test_coverage: test_coverage(), docs: docs(), package: package(), description: "A multi-blog engine powered by NimblePublisher for Phoenix apps" @@ -49,6 +50,30 @@ defmodule Blogex.MixProject do ] end + # The coverage floor the Lens pass enforces (`make verify` -> `make coverage` + # in blogex/). Rule: the threshold is a FLOOR. Raise it as behavior-test yaks + # land; never lower it. A drop means the change (or an exclusion) is wrong. + # + # Measured 2026-10-26, with the test scaffolding below excluded: 74.53% — the + # same via `mix test --cover` and via `mix test --cover --export-coverage + # default` + `mix test.coverage`. Floor is the integer below that. + # + # Excluded: the test scaffolding (Blogex.Test.*) only. Fixtures measure the + # tests, not the library. Everything in lib/blogex counts, and today's gaps + # are real: Blogex (the delegating facade), Blogex.Components and + # Blogex.Layout are at 0% because the router tests only exercise the JSON + # responses — moving this floor up is the behavior-test work. + defp test_coverage do + [ + summary: [threshold: 74], + ignore_modules: [ + Blogex.Test.FakeBlog, + Blogex.Test.PostBuilder, + Blogex.Test.Setup + ] + ] + end + defp docs do [ main: "readme",