@@ -16,6 +16,7 @@ defmodule Castle.MixProject do
1616 aliases: aliases ( ) ,
1717 package: package ( ) ,
1818 docs: docs ( ) ,
19+ test_coverage: test_coverage ( ) ,
1920 source_url: @ source_url
2021 ]
2122 end
@@ -38,6 +39,85 @@ defmodule Castle.MixProject do
3839 defp elixirc_paths ( :test ) , do: [ "lib" , "test/support" ]
3940 defp elixirc_paths ( _env ) , do: [ "lib" ]
4041
42+ # `mix test --cover` measures the shipped code, which is `lib` - the modules
43+ # under `test/support` are fixtures, and a fixture is covered by being run at
44+ # all. Left in, they moved the total without ever being the thing measured, and
45+ # two of them moved it *down* for a reason that is not about tests:
46+ # `Castle.PeerProviderStub` and most of `Castle.IoSink` execute inside the peer
47+ # VM, which nothing instruments - see the threshold below. Their code genuinely
48+ # runs and genuinely cannot be observed from here, so the figure they
49+ # contributed was an artefact of where they run rather than a gap in the suite,
50+ # and raising it would have meant calling them directly on the test node, which
51+ # tests nothing.
52+ #
53+ # Named module by module rather than matched by a pattern. A regex over `Stub`
54+ # or over `Castle.*Release` would quietly swallow a production module that
55+ # happened to be spelled that way, which is the one thing an exclusion list
56+ # must not do. Renaming a fixture makes the total drop, which is visible.
57+ #
58+ # **The threshold is a floor across the supported range, not this machine's
59+ # reading.** `elixir: "~> 1.18"` invites in every version from 1.18 upwards,
60+ # and cover's line attribution is not the same across them: 1.18.3 through
61+ # 1.19.5 count 473 relevant lines and report 88.58%, while 1.20.3 counts one
62+ # more in `Castle.Peer` and reports 88.40%. Same tests, same covered lines, a
63+ # different denominator. So this is the *lowest* of those readings, rounded
64+ # down, so that it absorbs a line of drift instead of sitting on one version's
65+ # figure. The per-toolchain measurements are in AGENTS.md.
66+ #
67+ # Two earlier values were wrong in opposite directions, and both mistakes are
68+ # worth keeping written down. 85 sat *below* the figure it was meant to floor,
69+ # so it ratcheted nothing and licensed a thirteen-line regression. 88.58 was
70+ # the pinned toolchain's exact reading with no slack, which made the *mandatory*
71+ # `mix precommit` fail on a clean tree under Elixir 1.20 - a false failure for
72+ # any contributor on a current release, and the comment beside it had already
73+ # said attribution varies by version while the number ignored it. So: do not
74+ # set this from one machine, and do not raise it to 88.40 or above, which
75+ # re-creates the trap the moment another version attributes differently.
76+ #
77+ # 88 absorbs two uncovered lines added to `lib` and fails on the third, which
78+ # was measured on 1.20 (the least slack of the range) rather than estimated -
79+ # 88.40, 88.21, 88.03, then 87.84 and exit 3. That is looser than a floor
80+ # ideally is, and it is the deliberate price of enough headroom that one more
81+ # line of attribution drift does not fail a clean tree. The direction of the
82+ # trade is the point: a floor that fires on a clean tree teaches people to
83+ # bypass the gate.
84+ #
85+ # 90% would need 426 covered on 1.19's denominator, seven more than there are.
86+ # What is left there is 33 lines in the peer's VM (below) plus 21 observable in
87+ # principle: five are the compiler's own default-argument clauses for arities
88+ # nothing calls, and the other sixteen need a file mode, a device node, or a
89+ # config provider sabotaging Castle's working directory. So the seven would
90+ # have to include all five of the default-argument clauses, whose only effect
91+ # is on this number. That is the move this project does not make. See AGENTS.md
92+ # for the line-by-line account.
93+ #
94+ # **What cannot be measured is the peer's VM, and the reason is where
95+ # instrumentation is applied rather than anything cover cannot do.**
96+ # `:cover.start/0` works perfectly well in a VM with no node name -
97+ # `is_alive() == false` is no obstacle to it. What happens here is that Mix
98+ # starts cover on *this* node and instruments the modules loaded here; the peer
99+ # is a separate VM that loads `Castle.Peer` from the target release's own beam
100+ # files on disk, which nothing has instrumented. Cover's only mechanism for
101+ # another VM is `:cover.start/1` over a *distributed* node, and this peer
102+ # deliberately has no distribution at all. So `resolve/1` and everything below
103+ # the `## In the peer` comment - 33 lines, about 7% of the shipped total - run
104+ # on every `Castle.PeerTest` and are counted as missed, which puts the
105+ # observable ceiling near 93%.
106+ defp test_coverage do
107+ [
108+ summary: [ threshold: 88 ] ,
109+ ignore_modules: [
110+ Castle.DeploymentStub ,
111+ Castle.InitStub ,
112+ Castle.IoSink ,
113+ Castle.PeerProviderStub ,
114+ Castle.PeerStub ,
115+ Castle.ReleaseHandlerStub ,
116+ Castle.SyntheticRelease
117+ ]
118+ ]
119+ end
120+
41121 # Run "mix help deps" to learn about dependencies.
42122 defp deps do
43123 [
@@ -59,7 +139,19 @@ defmodule Castle.MixProject do
59139 "deps.unlock --unused" ,
60140 "format" ,
61141 "credo --strict" ,
62- "test"
142+ # With `--cover`, so the threshold in `test_coverage/0` is a gate rather
143+ # than decoration: nothing else runs it, and a floor nothing enforces is
144+ # a number in a comment.
145+ #
146+ # **This runs on whatever Elixir the contributor has**, which is the
147+ # thing to keep in mind before touching the threshold. CI's `test` matrix
148+ # stays on a plain `mix test` and CI's `precommit` job is pinned to one
149+ # version, so a floor set from that one version is not checked anywhere
150+ # against the rest of the `~> 1.18` range - it is checked here, on a
151+ # machine CI never sees, in the gate this project makes mandatory. That
152+ # is why the number is a floor across the range and not a reading. See
153+ # `test_coverage/0`.
154+ "test --cover"
63155 ]
64156 ]
65157 end
0 commit comments