Skip to content

Commit c76d8ed

Browse files
authored
Always floor coverage instead of rounding (#310)
* Always floor coverage instead of rounding We do not want to report a 100% coverage when there are lines that are not covered. * Add option to restore previous ceil coverage behaviour
1 parent 719d364 commit c76d8ed

File tree

9 files changed

+41
-54
lines changed

9 files changed

+41
-54
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,9 @@ to `false`:
441441
- When set to a number greater than 0, this setting causes the `mix coveralls` and `mix coveralls.html` tasks to exit with a status code of 1 if test coverage falls below the specified threshold (defaults to 0). This is useful to interrupt CI pipelines with strict code coverage rules. Should be expressed as a number between 0 and 100 signifying the minimum percentage of lines covered.
442442
- `html_filter_full_covered`
443443
- A boolean, when `true` files with 100% coverage are not shown in the HTML report. Default to `false`.
444+
- `floor_coverage`
445+
- A boolean, when `false` coverage values are ceiled instead of floored, this means that a project with some lines
446+
that are not covered can still have a total 100% coverage. Default to `true`.
444447
445448
Example configuration file:
446449

lib/conf/coveralls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"coverage_options": {
1717
"treat_no_relevant_lines_as_covered": false,
1818
"output_dir": "cover/",
19-
"minimum_coverage": 0
19+
"minimum_coverage": 0,
20+
"floor_coverage": true
2021
},
2122

2223
"terminal_options": {

lib/excoveralls/local.ex

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ defmodule ExCoveralls.Local do
22
@moduledoc """
33
Locally displays the result to screen.
44
"""
5-
6-
75

86
defmodule Count do
97
@moduledoc """
@@ -102,7 +100,7 @@ defmodule ExCoveralls.Local do
102100
Enum.map(count_info, fn original ->
103101
[stat, count] = original
104102
%{
105-
"cov" => get_coverage(count),
103+
"cov" => ExCoveralls.Stats.get_coverage(count.relevant, count.covered),
106104
"file" => stat[:name],
107105
"lines" => count.lines,
108106
"relevant" => count.relevant,
@@ -145,16 +143,16 @@ defmodule ExCoveralls.Local do
145143
end
146144

147145
defp format_info([stat, count]) do
148-
coverage = get_coverage(count)
146+
coverage = ExCoveralls.Stats.get_coverage(count.relevant, count.covered)
149147
file_width = ExCoveralls.Settings.get_file_col_width
150-
print_string("~5.1f% ~-#{file_width}s ~8w ~8w ~8w",
148+
print_string("~5w% ~-#{file_width}s ~8w ~8w ~8w",
151149
[coverage, stat[:name], count.lines, count.relevant, count.relevant - count.covered])
152150
end
153151

154152
defp format_total(info) do
155153
totals = Enum.reduce(info, %Count{}, fn([_, count], acc) -> append(count, acc) end)
156-
coverage = get_coverage(totals)
157-
print_string("[TOTAL] ~5.1f%", [coverage])
154+
coverage = ExCoveralls.Stats.get_coverage(totals.relevant, totals.covered)
155+
print_string("[TOTAL] ~5w%", [coverage])
158156
end
159157

160158
defp append(a, b) do
@@ -165,21 +163,6 @@ defmodule ExCoveralls.Local do
165163
}
166164
end
167165

168-
defp get_coverage(count) do
169-
case count.relevant do
170-
0 -> default_coverage_value()
171-
_ -> (count.covered / count.relevant) * 100
172-
end
173-
end
174-
175-
defp default_coverage_value do
176-
options = ExCoveralls.Settings.get_coverage_options
177-
case Map.fetch(options, "treat_no_relevant_lines_as_covered") do
178-
{:ok, false} -> 0.0
179-
_ -> 100.0
180-
end
181-
end
182-
183166
@doc """
184167
Calculate count information from the coverage stats.
185168
"""

lib/excoveralls/settings.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ defmodule ExCoveralls.Settings do
3030
Get default coverage value for lines marked as not relevant.
3131
"""
3232
def default_coverage_value do
33-
case Map.fetch(get_coverage_options(), "treat_no_relevant_lines_as_covered") do
34-
{:ok, true} -> 100.0
35-
_ -> 0.0
36-
end
33+
get_coverage_options() |> default_coverage_value()
3734
end
3835

36+
def default_coverage_value(%{"treat_no_relevant_lines_as_covered" => true}), do: 100.0
37+
def default_coverage_value(_), do: 0.0
38+
3939
@doc """
4040
Get terminal output options from the json file.
4141
"""

lib/excoveralls/stats.ex

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,17 @@ defmodule ExCoveralls.Stats do
198198
{s+sloc, h+hits, m+misses}
199199
end
200200

201-
defp get_coverage(relevant, covered) do
202-
value = case relevant do
203-
0 -> Settings.default_coverage_value
204-
_ -> (covered / relevant) * 100
201+
def get_coverage(relevant, covered) do
202+
coverage_options = Settings.get_coverage_options()
203+
204+
approximate_fn = case coverage_options do
205+
%{"floor_coverage" => false} -> &Float.round(&1, 1)
206+
_ -> &Float.floor(&1, 1)
205207
end
206208

207-
if value == trunc(value) do
208-
trunc(value)
209-
else
210-
Float.round(value, 1)
209+
case relevant do
210+
0 -> Settings.default_coverage_value(coverage_options)
211+
_ -> approximate_fn.((covered / relevant) * 100)
211212
end
212213
end
213214

@@ -226,7 +227,7 @@ defmodule ExCoveralls.Stats do
226227
Exit the process with a status of 1 if coverage is below the minimum.
227228
"""
228229
def ensure_minimum_coverage(stats) do
229-
coverage_options = ExCoveralls.Settings.get_coverage_options
230+
coverage_options = Settings.get_coverage_options
230231
minimum_coverage = coverage_options["minimum_coverage"] || 0
231232
if minimum_coverage > 0, do: check_coverage_threshold(stats, minimum_coverage)
232233
end

test/html_test.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule ExCoveralls.HtmlTest do
55
alias ExCoveralls.Html
66

77
@file_name "excoveralls.html"
8-
@file_size 20375
8+
@file_size 20381
99
@test_output_dir "cover_test/"
1010
@test_template_path "lib/templates/html/htmlcov/"
1111

@@ -35,7 +35,7 @@ defmodule ExCoveralls.HtmlTest do
3535
File.rm!(path)
3636
File.rmdir!(@test_output_dir)
3737
end
38-
38+
3939
ExCoveralls.ConfServer.clear()
4040
end
4141

@@ -78,7 +78,7 @@ defmodule ExCoveralls.HtmlTest do
7878
output = capture_io(fn ->
7979
assert catch_exit(Html.execute(@source_info)) == {:shutdown, 1}
8080
end)
81-
assert String.contains?(output, "FAILED: Expected minimum coverage of 100%, got 50%.")
81+
assert String.contains?(output, "FAILED: Expected minimum coverage of 100%, got 50.0%.")
8282
end
8383

8484
test_with_mock "Exit status code is 0 when actual coverage reaches the minimum",

test/local_test.exs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ defmodule ExCoveralls.LocalTest do
3737
"\e[31mdefmodule Test do\e[m\n\e[32m def test do\e[m\n" <>
3838
" end\n" <>
3939
"end"
40-
40+
4141
setup do
4242
ExCoveralls.ConfServer.clear()
4343
on_exit(fn -> ExCoveralls.ConfServer.clear() end)
@@ -75,11 +75,11 @@ defmodule ExCoveralls.LocalTest do
7575
end
7676

7777
test "Empty (no relevant lines) file is calculated as 0.0%" do
78-
assert String.contains?(Local.coverage(@empty_source_info), "[TOTAL] 100.0%")
78+
assert String.contains?(Local.coverage(@empty_source_info), "[TOTAL] 0.0%")
7979
end
8080

8181
test_with_mock "Empty (no relevant lines) file with treat_no_relevant_lines_as_covered=true option is calculated as 100.0%",
82-
ExCoveralls.Settings, [
82+
ExCoveralls.Settings, [:passthrough], [
8383
get_coverage_options: fn -> %{"treat_no_relevant_lines_as_covered" => true} end,
8484
get_file_col_width: fn -> 40 end,
8585
get_print_files: fn -> true end
@@ -88,7 +88,7 @@ defmodule ExCoveralls.LocalTest do
8888
end
8989

9090
test_with_mock "Empty (no relevant lines) file with treat_no_relevant_lines_as_covered=false option is calculated as 0.0%",
91-
ExCoveralls.Settings, [
91+
ExCoveralls.Settings, [:passthrough], [
9292
get_coverage_options: fn -> %{"treat_no_relevant_lines_as_covered" => false} end,
9393
get_file_col_width: fn -> 40 end,
9494
get_print_files: fn -> true end
@@ -97,7 +97,6 @@ defmodule ExCoveralls.LocalTest do
9797
end
9898

9999
test_with_mock "Exit status code is 1 when actual coverage does not reach the minimum",
100-
101100
ExCoveralls.Settings, [
102101
get_coverage_options: fn -> %{"minimum_coverage" => 100} end,
103102
get_file_col_width: fn -> 40 end,
@@ -107,7 +106,7 @@ defmodule ExCoveralls.LocalTest do
107106
output = capture_io(fn ->
108107
assert catch_exit(Local.execute(@source_info)) == {:shutdown, 1}
109108
end)
110-
assert String.contains?(output, "FAILED: Expected minimum coverage of 100%, got 50%.")
109+
assert String.contains?(output, "FAILED: Expected minimum coverage of 100%, got 50.0%.")
111110
end
112111

113112
test_with_mock "Exit status code is 0 when actual coverage reaches the minimum",

test/poster_test.exs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
defmodule PosterTest do
22
use ExUnit.Case
33
import ExUnit.CaptureIO
4-
4+
55
setup do
66
bypass = Bypass.open()
77
%{bypass: bypass, endpoint: "http://localhost:#{bypass.port}"}
88
end
9-
9+
1010
test "successfully posting JSON", %{bypass: bypass, endpoint: endpoint} do
1111
Bypass.expect(bypass, fn conn ->
1212
assert conn.method == "POST"
1313
assert {"host", "localhost"} in conn.req_headers
1414
Plug.Conn.resp(conn, 200, "")
1515
end)
16-
16+
1717
assert capture_io(fn ->
1818
ExCoveralls.Poster.execute("{}", endpoint: endpoint)
1919
end) =~ "Successfully uploaded"
2020
end
2121

2222
test "post JSON fails", %{bypass: bypass, endpoint: endpoint} do
2323
Bypass.down(bypass)
24-
24+
2525
assert_raise ExCoveralls.ReportUploadError, fn ->
2626
ExCoveralls.Poster.execute("{}", endpoint: endpoint)
2727
end
@@ -32,7 +32,7 @@ defmodule PosterTest do
3232
assert conn.method == "POST"
3333
Plug.Conn.resp(conn, 500, "")
3434
end)
35-
35+
3636
assert capture_io(fn ->
3737
assert ExCoveralls.Poster.execute("{}", endpoint: endpoint) == :ok
3838
end) =~ ~r/internal server error/
@@ -43,7 +43,7 @@ defmodule PosterTest do
4343
assert conn.method == "POST"
4444
Plug.Conn.resp(conn, 405, "")
4545
end)
46-
46+
4747
assert capture_io(fn ->
4848
assert ExCoveralls.Poster.execute("{}", endpoint: endpoint) == :ok
4949
end) =~ ~r/maintenance/
@@ -58,7 +58,7 @@ defmodule PosterTest do
5858

5959
Bypass.expect_once(bypass, "POST", "/api/v1/jobs", fn conn ->
6060
conn
61-
|> Plug.Conn.put_resp_header("location", Path.join(endpoint, "redirected") |> IO.inspect())
61+
|> Plug.Conn.put_resp_header("location", Path.join(endpoint, "redirected"))
6262
|> Plug.Conn.resp(302, "")
6363
end)
6464

test/stats_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,15 @@ defmodule ExCoveralls.StatsTest do
125125
end
126126

127127
test_with_mock "Empty (no relevant lines) file with treat_no_relevant_lines_as_covered option is calculated as 100.0%",
128-
ExCoveralls.Settings, [default_coverage_value: fn -> 100 end] do
128+
ExCoveralls.Settings, [:passthrough], [default_coverage_value: fn _ -> 100 end] do
129129

130130
results = Stats.source(@empty_source_info)
131131
assert(results.coverage == 100)
132132
end
133133

134134
test "coverage stats are rounded to one decimal place" do
135135
results = Stats.source(@fractional_source_info)
136-
assert(results.coverage == 66.7)
136+
assert(results.coverage == 66.6)
137137
end
138138

139139
describe "update_stats/2" do

0 commit comments

Comments
 (0)