From a46d2b9f141206cd1cd6edf8b0961717f987d837 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 08:15:43 -0400 Subject: [PATCH 01/12] Accumulate coverage counts per module Assisted-by: Codex:GPT-6 --- lib/mix/lib/mix/tasks/test.coverage.ex | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index 8df29077057..e24c56c61db 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -335,19 +335,13 @@ defmodule Mix.Tasks.Test.Coverage do # When gathering coverage results, we need to skip any # entry with line equal to 0 as those are generated code. - # - # We may also have multiple entries on the same line. - # Each line is only considered once. - # - # We use ETS for performance, to avoid working with nested maps. table = :ets.new(__MODULE__, [:set, :private]) try do - for {{module, line}, cov} <- results, module in keep_set, line != 0 do - case cov do - {1, 0} -> :ets.insert(table, {{module, line}, true}) - {0, 1} -> :ets.insert_new(table, {{module, line}, false}) - end + for {{module, line}, {covered, not_covered}} <- results, + module in keep_set, + line != 0 do + :ets.update_counter(table, module, [{2, covered}, {3, not_covered}], {module, 0, 0}) end module_results = for module <- keep, do: {read_cover_results(table, module), module} @@ -358,8 +352,13 @@ defmodule Mix.Tasks.Test.Coverage do end defp read_cover_results(table, module) do - covered = :ets.select_count(table, [{{{module, :_}, true}, [], [true]}]) - not_covered = :ets.select_count(table, [{{{module, :_}, false}, [], [true]}]) + {covered, not_covered} = + table + |> :ets.match_object({module, :_, :_}) + |> Enum.reduce({0, 0}, fn {_, covered, not_covered}, {covered_acc, not_covered_acc} -> + {covered + covered_acc, not_covered + not_covered_acc} + end) + percentage(covered, not_covered) end From 4d04408123190399f3d3935bd839988d59c347af Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 08:24:36 -0400 Subject: [PATCH 02/12] Retain :ets note --- lib/mix/lib/mix/tasks/test.coverage.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index e24c56c61db..e9dae23785b 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -335,6 +335,7 @@ defmodule Mix.Tasks.Test.Coverage do # When gathering coverage results, we need to skip any # entry with line equal to 0 as those are generated code. + # We use ETS for performance, to avoid working with nested maps. table = :ets.new(__MODULE__, [:set, :private]) try do From 84f74b89200226a81ad0bf3ca57bd8a218ad769b Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 10:42:16 -0400 Subject: [PATCH 03/12] Track Mix coverage counts in a flat map Initialize counters for the modules being reported so the map also serves as the membership filter. Remove the ETS table setup and teardown. Assisted-by: Codex:GPT-6 --- lib/mix/lib/mix/tasks/test.coverage.ex | 45 ++++++++++++-------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index e9dae23785b..f7e9e16f3bb 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -331,40 +331,37 @@ defmodule Mix.Tasks.Test.Coverage do end defp gather_coverage(results, keep) do - keep_set = MapSet.new(keep) - # When gathering coverage results, we need to skip any # entry with line equal to 0 as those are generated code. - # We use ETS for performance, to avoid working with nested maps. - table = :ets.new(__MODULE__, [:set, :private]) - - try do + counts = for {{module, line}, {covered, not_covered}} <- results, - module in keep_set, - line != 0 do - :ets.update_counter(table, module, [{2, covered}, {3, not_covered}], {module, 0, 0}) + line != 0, + reduce: Map.new(keep, &{&1, {0, 0}}) do + %{^module => {covered_acc, not_covered_acc}} = counts -> + %{counts | module => {covered + covered_acc, not_covered + not_covered_acc}} + + counts -> + counts end - module_results = for module <- keep, do: {read_cover_results(table, module), module} - {module_results, read_cover_results(table, :_)} - after - :ets.delete(table) - end + module_results = for module <- keep, do: {read_cover_results(counts, module), module} + {module_results, read_cover_results(counts, :_)} end - defp read_cover_results(table, module) do - {covered, not_covered} = - table - |> :ets.match_object({module, :_, :_}) - |> Enum.reduce({0, 0}, fn {_, covered, not_covered}, {covered_acc, not_covered_acc} -> - {covered + covered_acc, not_covered + not_covered_acc} - end) + defp read_cover_results(counts, :_) do + counts + |> Enum.reduce({0, 0}, fn {_, {covered, not_covered}}, {covered_acc, not_covered_acc} -> + {covered + covered_acc, not_covered + not_covered_acc} + end) + |> percentage() + end - percentage(covered, not_covered) + defp read_cover_results(counts, module) do + counts |> Map.fetch!(module) |> percentage() end - defp percentage(0, 0), do: 100.0 - defp percentage(covered, not_covered), do: covered / (covered + not_covered) * 100 + defp percentage({0, 0}), do: 100.0 + defp percentage({covered, not_covered}), do: covered / (covered + not_covered) * 100 defp print_summary(results, totals, true), do: print_summary(results, totals, []) From d9100ce302a3870c3fd1d1e9fee759333226c4f5 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 11:48:09 -0400 Subject: [PATCH 04/12] Delineate module and total coverage functions --- lib/mix/lib/mix/tasks/test.coverage.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index f7e9e16f3bb..74c92e25327 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -344,11 +344,11 @@ defmodule Mix.Tasks.Test.Coverage do counts end - module_results = for module <- keep, do: {read_cover_results(counts, module), module} - {module_results, read_cover_results(counts, :_)} + module_results = for module <- keep, do: {module_coverage(counts, module), module} + {module_results, total_coverage(counts)} end - defp read_cover_results(counts, :_) do + defp total_coverage(counts) do counts |> Enum.reduce({0, 0}, fn {_, {covered, not_covered}}, {covered_acc, not_covered_acc} -> {covered + covered_acc, not_covered + not_covered_acc} @@ -356,7 +356,7 @@ defmodule Mix.Tasks.Test.Coverage do |> percentage() end - defp read_cover_results(counts, module) do + defp module_coverage(counts, module) do counts |> Map.fetch!(module) |> percentage() end From fc46b85d2c2a0655718ec94b9c8945062c029cf0 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 11:51:28 -0400 Subject: [PATCH 05/12] Inline percentage/1 call --- lib/mix/lib/mix/tasks/test.coverage.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index 74c92e25327..762ebdef5f3 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -344,7 +344,7 @@ defmodule Mix.Tasks.Test.Coverage do counts end - module_results = for module <- keep, do: {module_coverage(counts, module), module} + module_results = for module <- keep, do: {percentage(counts[module]), module} {module_results, total_coverage(counts)} end @@ -356,10 +356,6 @@ defmodule Mix.Tasks.Test.Coverage do |> percentage() end - defp module_coverage(counts, module) do - counts |> Map.fetch!(module) |> percentage() - end - defp percentage({0, 0}), do: 100.0 defp percentage({covered, not_covered}), do: covered / (covered + not_covered) * 100 From c1f6fc879d191a10cbccdf2c7def55510c42d047 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 18:01:38 -0400 Subject: [PATCH 06/12] Tighten --- lib/mix/lib/mix/tasks/test.coverage.ex | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index 762ebdef5f3..a27ffd9466f 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -344,18 +344,16 @@ defmodule Mix.Tasks.Test.Coverage do counts end - module_results = for module <- keep, do: {percentage(counts[module]), module} - {module_results, total_coverage(counts)} + {Enum.map(keep, &{percentage(counts[&1]), &1}), total_coverage(counts)} end defp total_coverage(counts) do - counts - |> Enum.reduce({0, 0}, fn {_, {covered, not_covered}}, {covered_acc, not_covered_acc} -> - {covered + covered_acc, not_covered + not_covered_acc} - end) - |> percentage() + percentage(Enum.reduce(counts, {0, 0}, &add_coverage_totals/2)) end + defp add_coverage_totals({_, {covered, not_covered}}, {covered_acc, not_covered_acc}), + do: {covered + covered_acc, not_covered + not_covered_acc} + defp percentage({0, 0}), do: 100.0 defp percentage({covered, not_covered}), do: covered / (covered + not_covered) * 100 From 126813db719bae37dbc39f96c3e056546438a1dc Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 18:20:59 -0400 Subject: [PATCH 07/12] Use Enum.map_reduce/3 to collect modules and totals in one pass --- lib/mix/lib/mix/tasks/test.coverage.ex | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index a27ffd9466f..e4d770353b5 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -344,15 +344,12 @@ defmodule Mix.Tasks.Test.Coverage do counts end - {Enum.map(keep, &{percentage(counts[&1]), &1}), total_coverage(counts)} + {module_results, totals} = Enum.map_reduce(counts, {0, 0}, &gather_module/2) + {module_results, percentage(totals)} end - defp total_coverage(counts) do - percentage(Enum.reduce(counts, {0, 0}, &add_coverage_totals/2)) - end - - defp add_coverage_totals({_, {covered, not_covered}}, {covered_acc, not_covered_acc}), - do: {covered + covered_acc, not_covered + not_covered_acc} + defp gather_module({module, {covered, not_covered} = counts}, {covered_acc, not_covered_acc}), + do: {{percentage(counts), module}, {covered + covered_acc, not_covered + not_covered_acc}} defp percentage({0, 0}), do: 100.0 defp percentage({covered, not_covered}), do: covered / (covered + not_covered) * 100 From 002f3737cc8180ad76600ddad85eac4e1fa9ba26 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 19:05:59 -0400 Subject: [PATCH 08/12] Pull acc up top --- lib/mix/lib/mix/tasks/test.coverage.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index e4d770353b5..43b617c62a9 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -331,12 +331,12 @@ defmodule Mix.Tasks.Test.Coverage do end defp gather_coverage(results, keep) do + counts = Map.new(keep, &{&1, {0, 0}}) + # When gathering coverage results, we need to skip any # entry with line equal to 0 as those are generated code. counts = - for {{module, line}, {covered, not_covered}} <- results, - line != 0, - reduce: Map.new(keep, &{&1, {0, 0}}) do + for {{module, line}, {covered, not_covered}} <- results, line != 0, reduce: counts do %{^module => {covered_acc, not_covered_acc}} = counts -> %{counts | module => {covered + covered_acc, not_covered + not_covered_acc}} From c18325dbf29205fb4ff7f18f65b293a99e411830 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 19:21:58 -0400 Subject: [PATCH 09/12] Use Map.from_keys/2 --- lib/mix/lib/mix/tasks/test.coverage.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index 43b617c62a9..c818a4aab3e 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -331,7 +331,7 @@ defmodule Mix.Tasks.Test.Coverage do end defp gather_coverage(results, keep) do - counts = Map.new(keep, &{&1, {0, 0}}) + counts = Map.from_keys(keep, {0, 0}) # When gathering coverage results, we need to skip any # entry with line equal to 0 as those are generated code. From e9f86df26a8aa94df17117123c7f2990a2a37da8 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 20:40:13 -0400 Subject: [PATCH 10/12] Batch coverage counters by consecutive module entries --- lib/mix/lib/mix/tasks/test.coverage.ex | 37 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index c818a4aab3e..a632ae7f299 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -331,21 +331,34 @@ defmodule Mix.Tasks.Test.Coverage do end defp gather_coverage(results, keep) do - counts = Map.from_keys(keep, {0, 0}) + counts = gather_coverage(results, Map.from_keys(keep, {0, 0}), nil, 0, 0) + {module_results, totals} = Enum.map_reduce(counts, {0, 0}, &gather_module/2) + {module_results, percentage(totals)} + end - # When gathering coverage results, we need to skip any - # entry with line equal to 0 as those are generated code. - counts = - for {{module, line}, {covered, not_covered}} <- results, line != 0, reduce: counts do - %{^module => {covered_acc, not_covered_acc}} = counts -> - %{counts | module => {covered + covered_acc, not_covered + not_covered_acc}} + # Batch consecutive entries for a module to avoid updating the map for every line. + # Add each batch to the existing counts so interleaved modules also work. + # Entries with line equal to 0 are generated code and must be skipped. + defp gather_coverage([{{_, 0}, _} | rest], counts, module, covered, not_covered), + do: gather_coverage(rest, counts, module, covered, not_covered) - counts -> - counts - end + defp gather_coverage([{{module, _}, {c, n}} | rest], counts, module, covered, not_covered), + do: gather_coverage(rest, counts, module, covered + c, not_covered + n) - {module_results, totals} = Enum.map_reduce(counts, {0, 0}, &gather_module/2) - {module_results, percentage(totals)} + defp gather_coverage([{{next, _}, {c, n}} | rest], counts, module, covered, not_covered), + do: gather_coverage(rest, update_coverage(counts, module, covered, not_covered), next, c, n) + + defp gather_coverage([_ | rest], counts, module, covered, not_covered), + do: gather_coverage(rest, counts, module, covered, not_covered) + + defp gather_coverage([], counts, module, covered, not_covered), + do: update_coverage(counts, module, covered, not_covered) + + defp update_coverage(counts, module, covered, not_covered) do + case counts do + %{^module => {c, n}} -> %{counts | module => {c + covered, n + not_covered}} + _ -> counts + end end defp gather_module({module, {covered, not_covered} = counts}, {covered_acc, not_covered_acc}), From 1e58459febced8e0b80653aa4e89a882a119a7f3 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 21:44:19 -0400 Subject: [PATCH 11/12] Snip unused gather_coverage/5 clause --- lib/mix/lib/mix/tasks/test.coverage.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index a632ae7f299..6f68d8bb1b0 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -348,9 +348,6 @@ defmodule Mix.Tasks.Test.Coverage do defp gather_coverage([{{next, _}, {c, n}} | rest], counts, module, covered, not_covered), do: gather_coverage(rest, update_coverage(counts, module, covered, not_covered), next, c, n) - defp gather_coverage([_ | rest], counts, module, covered, not_covered), - do: gather_coverage(rest, counts, module, covered, not_covered) - defp gather_coverage([], counts, module, covered, not_covered), do: update_coverage(counts, module, covered, not_covered) From d44af22be0355755d556c09e8bfb8c9eef62ab32 Mon Sep 17 00:00:00 2001 From: Clayton Gentry Date: Sat, 19 Sep 2026 21:50:14 -0400 Subject: [PATCH 12/12] Tighten update_coverage/4 --- lib/mix/lib/mix/tasks/test.coverage.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.coverage.ex b/lib/mix/lib/mix/tasks/test.coverage.ex index 6f68d8bb1b0..8bd1ecc34dc 100644 --- a/lib/mix/lib/mix/tasks/test.coverage.ex +++ b/lib/mix/lib/mix/tasks/test.coverage.ex @@ -352,9 +352,8 @@ defmodule Mix.Tasks.Test.Coverage do do: update_coverage(counts, module, covered, not_covered) defp update_coverage(counts, module, covered, not_covered) do - case counts do - %{^module => {c, n}} -> %{counts | module => {c + covered, n + not_covered}} - _ -> counts + with %{^module => {c, n}} <- counts do + %{counts | module => {c + covered, n + not_covered}} end end