Skip to content

Commit

Permalink
Merge pull request #45 from campezzi/master
Browse files Browse the repository at this point in the history
Add support for minimum coverage
  • Loading branch information
parroty committed Apr 6, 2016
2 parents 05b4c1a + 090c960 commit 10176a7
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 139 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ Stop words defined in `coveralls.json` will be excluded from the coverage calcul
- The directory which the HTML report will output to. Defaulted to `cover/`.
- template_path
- A custom path for html reports. This defaults to the htmlcov report in the excoveralls lib.
- minimum_coverage
- 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.

```javascript
{
Expand All @@ -243,7 +245,8 @@ Stop words defined in `coveralls.json` will be excluded from the coverage calcul
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"output_dir": "cover/",
"template_path": "custom/path/to/template/"
"template_path": "custom/path/to/template/",
"minimum_coverage": 90
}
}
```
Expand Down
3 changes: 2 additions & 1 deletion coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"coverage_options": {
"treat_no_relevant_lines_as_covered": false,
"output_dir": "cover/",
"template_path": "lib/templates/html/htmlcov/"
"template_path": "lib/templates/html/htmlcov/",
"minimum_coverage": 0
}
}
3 changes: 2 additions & 1 deletion lib/conf/coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

"coverage_options": {
"treat_no_relevant_lines_as_covered": false,
"output_dir": "cover/"
"output_dir": "cover/",
"minimum_coverage": 0
}
}
99 changes: 4 additions & 95 deletions lib/excoveralls/html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,21 @@ defmodule ExCoveralls.Html do
@moduledoc """
Generate HTML report of result.
"""

alias ExCoveralls.Html.View
alias ExCoveralls.Stats

@file_name "excoveralls.html"

defmodule Line do
@moduledoc """
Stores count information and source for a sigle line.
"""

defstruct coverage: nil, source: ""
end

defmodule Source do
@moduledoc """
Stores count information for a file and all source lines.
"""

defstruct filename: "", coverage: 0, sloc: 0, hits: 0, misses: 0, source: []
end

@doc """
Provides an entry point for the module.
"""
def execute(stats, options \\ []) do
ExCoveralls.Local.print_summary(stats)

source(stats, options[:filter]) |> generate_report
end
Stats.source(stats, options[:filter]) |> generate_report

@doc """
Format the source code as an HTML report.
"""
def source(stats, _patterns = nil), do: source(stats)
def source(stats, _patterns = []), do: source(stats)
def source(stats, patterns) do
Enum.filter(stats, fn(stat) -> String.contains?(stat[:name], patterns) end) |> source
end

def source(stats) do
stats = Enum.sort(stats, fn(x, y) -> x[:name] <= y[:name] end)
stats |> transform_cov
Stats.ensure_minimum_coverage(stats)
end

defp generate_report(map) do
Expand All @@ -67,68 +40,4 @@ defmodule ExCoveralls.Html do
File.write!(Path.expand(@file_name, file_path), content)
end

defp transform_cov(stats) do
files = Enum.map(stats, &populate_file/1)
{relevant, hits, misses} = Enum.reduce(files, {0,0,0}, &reduce_file_counts/2)
covered = relevant - misses

%{coverage: get_coverage(relevant, covered),
sloc: relevant,
hits: hits,
misses: misses,
files: files}
end

defp reduce_file_counts(%{sloc: sloc, hits: hits, misses: misses}, {s,h,m}) do
{s+sloc, h+hits, m+misses}
end

defp populate_file(stat) do
coverage = stat[:coverage]
source = map_source(stat[:source], coverage)
relevant = Enum.count(coverage, fn e -> e != nil end)
hits = Enum.reduce(coverage, 0, fn e, acc -> (e || 0) + acc end)
misses = Enum.count(coverage, fn e -> e == 0 end)
covered = relevant - misses

%Source{filename: stat[:name],
coverage: get_coverage(relevant, covered),
sloc: relevant,
hits: hits,
misses: misses,
source: source}
end

defp map_source(source, coverage) do
source
|> String.split("\n")
|> Enum.with_index()
|> Enum.map(&(populate_source(&1,coverage)))
end

defp populate_source({line, i}, coverage) do
%Line{coverage: Enum.at(coverage, i) , source: line}
end

defp get_coverage(relevant, covered) do
value = case relevant do
0 -> default_coverage_value
_ -> (covered / relevant) * 100
end

if value == trunc(value) do
trunc(value)
else
Float.round(value, 1)
end
end

defp default_coverage_value do
options = ExCoveralls.Settings.get_coverage_options
case Dict.fetch(options, "treat_no_relevant_lines_as_covered") do
{:ok, true} -> 100.0
_ -> 0.0
end
end

end
2 changes: 2 additions & 0 deletions lib/excoveralls/local.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ defmodule ExCoveralls.Local do
if options[:detail] == true do
source(stats, options[:filter]) |> IO.puts
end

ExCoveralls.Stats.ensure_minimum_coverage(stats)
end

@doc """
Expand Down
11 changes: 11 additions & 0 deletions lib/excoveralls/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ defmodule ExCoveralls.Settings do
read_config("coverage_options") |> Enum.into(HashDict.new)
end

@doc """
Get default coverage value for lines marked as not relevant.
"""
def default_coverage_value do
case Dict.fetch(get_coverage_options, "treat_no_relevant_lines_as_covered") do
{:ok, true} -> 100.0
_ -> 0.0
end
end


defp read_config_file(file_name) do
if File.exists?(file_name) do
case File.read!(file_name) |> JSX.decode do
Expand Down
108 changes: 107 additions & 1 deletion lib/excoveralls/stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ defmodule ExCoveralls.Stats do
Provide calculation logics of coverage stats.
"""
alias ExCoveralls.Cover
alias ExCoveralls.Settings

defmodule Source do
@moduledoc """
Stores count information for a file and all source lines.
"""

defstruct filename: "", coverage: 0, sloc: 0, hits: 0, misses: 0, source: []
end

defmodule Line do
@moduledoc """
Stores count information and source for a sigle line.
"""

defstruct coverage: nil, source: ""
end

@doc """
Report the statistical information for he specified module.
Expand Down Expand Up @@ -109,9 +126,98 @@ defmodule ExCoveralls.Stats do
end

def skip_files(converage) do
skip = ExCoveralls.Settings.get_skip_files
skip = Settings.get_skip_files
Enum.reject(converage, fn cov ->
Enum.any?(skip, &Regex.match?(&1, cov[:name]))
end)
end

@doc """
Summarizes source coverage details.
"""
def source(stats, _patterns = nil), do: source(stats)
def source(stats, _patterns = []), do: source(stats)
def source(stats, patterns) do
Enum.filter(stats, fn(stat) -> String.contains?(stat[:name], patterns) end) |> source
end

def source(stats) do
stats = Enum.sort(stats, fn(x, y) -> x[:name] <= y[:name] end)
stats |> transform_cov
end

defp transform_cov(stats) do
files = Enum.map(stats, &populate_file/1)
{relevant, hits, misses} = Enum.reduce(files, {0,0,0}, &reduce_file_counts/2)
covered = relevant - misses

%{coverage: get_coverage(relevant, covered),
sloc: relevant,
hits: hits,
misses: misses,
files: files}
end

defp populate_file(stat) do
coverage = stat[:coverage]
source = map_source(stat[:source], coverage)
relevant = Enum.count(coverage, fn e -> e != nil end)
hits = Enum.reduce(coverage, 0, fn e, acc -> (e || 0) + acc end)
misses = Enum.count(coverage, fn e -> e == 0 end)
covered = relevant - misses

%Source{filename: stat[:name],
coverage: get_coverage(relevant, covered),
sloc: relevant,
hits: hits,
misses: misses,
source: source}
end

defp reduce_file_counts(%{sloc: sloc, hits: hits, misses: misses}, {s,h,m}) do
{s+sloc, h+hits, m+misses}
end

defp get_coverage(relevant, covered) do
value = case relevant do
0 -> Settings.default_coverage_value
_ -> (covered / relevant) * 100
end

if value == trunc(value) do
trunc(value)
else
Float.round(value, 1)
end
end

defp map_source(source, coverage) do
source
|> String.split("\n")
|> Enum.with_index()
|> Enum.map(&(populate_source(&1,coverage)))
end

defp populate_source({line, i}, coverage) do
%Line{coverage: Enum.at(coverage, i) , source: line}
end

@doc """
Exit the process with a status of 1 if coverage is below the minimum.
"""
def ensure_minimum_coverage(stats) do
coverage_options = ExCoveralls.Settings.get_coverage_options
minimum_coverage = coverage_options["minimum_coverage"] || 0
if minimum_coverage > 0, do: check_coverage_threshold(stats, minimum_coverage)
end

defp check_coverage_threshold(stats, minimum_coverage) do
result = source(stats)
if result.coverage < minimum_coverage do
message = "FAILED: Expected minimum coverage of #{minimum_coverage}%, got #{result.coverage}%."
IO.puts IO.ANSI.format([:red, :bright, message])
exit({:shutdown, 1})
end
end

end
5 changes: 5 additions & 0 deletions test/fixtures/no_relevant_lines_are_covered.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true
}
}
5 changes: 5 additions & 0 deletions test/fixtures/no_relevant_lines_not_covered.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": false
}
}
Loading

0 comments on commit 10176a7

Please sign in to comment.