Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTMLCov style reports #40

Merged
merged 1 commit into from
Feb 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ end
- [mix coveralls.circle](#mix-coverallscircle-post-coverage-from-circle)
- [mix coveralls.post](#mix-coverallspost-post-coverage-from-localhost)
- [mix coveralls.detail](#mix-coverallsdetail-show-coverage-with-detail)
- [mix coveralls.html](#mix-coverallshtml-show-coverage-as-html-report)

### [mix coveralls] Show coverage
Run the `MIX_ENV=test mix coveralls` command to show coverage information on localhost.
Expand Down Expand Up @@ -197,6 +198,20 @@ defmodule ExCoveralls do
...
```

### [mix coveralls.html] Show coverage as HTML report
This task displays coverage information at the source-code level formatted as an HTML page.
The report follows the format inspired by HTMLCov from the Mocha testing library in JS.
Output to the shell is the same as running the command `mix coveralls`. In a similar manner to `mix coveralls.detail`, reported source code can be filtered by specifying arguments using the `--filter` flag.

```Shell
$ MIX_ENV=test mix coveralls.html
```
![HTML Report](./README/html_report.jpg?raw=true "HTML Report")

Output reports are written to `cover/excoveralls.html` by default, however, the path can be specified by overwriting the `"output_dir"` coverage option.
Custom reports can be created and utilized by defining `template_path` in `coveralls.json`. This directory should
contain an eex template named `coverage.html.eex`.

## coveralls.json
`coveralls.json` provides a setting for excoveralls.

Expand All @@ -207,7 +222,11 @@ Stop words defined in `coveralls.json` will be excluded from the coverage calcul

#### Coverage Options
- treat_no_relevant_lines_as_covered
- By default, coverage for [files with no relevant lines] are displayed as 0% for aligning with coveralls.io behavior. But, if `treat_no_relevant_lines_as_covered` is set to `true`, it will be displayed as 100%.
- By default, coverage for [files with no relevant lines] are displayed as 0% for aligning with coveralls.io behavior. But, if `treat_no_relevant_lines_as_covered` is set to `true`, it will be displayed as 100%.
- output_dir
- 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.

```javascript
{
Expand All @@ -222,7 +241,9 @@ Stop words defined in `coveralls.json` will be excluded from the coverage calcul
],

"coverage_options": {
"treat_no_relevant_lines_as_covered": true
"treat_no_relevant_lines_as_covered": true,
"output_dir": "cover/",
"template_path": "custom/path/to/template/"
}
}
```
Expand Down
Binary file added README/html_report.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
],

"coverage_options": {
"treat_no_relevant_lines_as_covered": false
"treat_no_relevant_lines_as_covered": false,
"output_dir": "cover/",
"template_path": "lib/templates/html/htmlcov/"
}
}
3 changes: 2 additions & 1 deletion lib/conf/coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
],

"coverage_options": {
"treat_no_relevant_lines_as_covered": false
"treat_no_relevant_lines_as_covered": false,
"output_dir": "cover/"
}
}
9 changes: 9 additions & 0 deletions lib/excoveralls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ defmodule ExCoveralls do
alias ExCoveralls.Travis
alias ExCoveralls.Circle
alias ExCoveralls.Local
alias ExCoveralls.Html
alias ExCoveralls.Post

@type_travis "travis"
@type_circle "circle"
@type_local "local"
@type_html "html"
@type_post "post"

@doc """
Expand Down Expand Up @@ -65,6 +67,13 @@ defmodule ExCoveralls do
Local.execute(stats, options)
end

@doc """
Logic for html stats display, without posting server
"""
def analyze(stats, @type_html, options) do
Html.execute(stats, options)
end

@doc """
Logic for posting from general CI server with token.
"""
Expand Down
134 changes: 134 additions & 0 deletions lib/excoveralls/html.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
defmodule ExCoveralls.Html do
@moduledoc """
Generate HTML report of result.
"""

alias ExCoveralls.Html.View

@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

@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
end

defp generate_report(map) do
IO.puts "Generating report..."
View.render([cov: map]) |> write_file
end

defp output_dir do
options = ExCoveralls.Settings.get_coverage_options
case Dict.fetch(options, "output_dir") do
{:ok, val} -> val
_ -> "cover/"
end
end

defp write_file(content) do
file_path = output_dir
unless File.exists?(file_path) do
File.mkdir!(file_path)
end
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
28 changes: 28 additions & 0 deletions lib/excoveralls/html/safe.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule ExCoveralls.Html.Safe do
@moduledoc """
Conveniences for generating HTML.
"""

@doc ~S"""
Escapes the given HTML.
"""
def html_escape(data) when is_binary(data) do
IO.iodata_to_binary(for <<char <- data>>, do: escape_char(char))
end

@compile {:inline, escape_char: 1}

@escapes [
{?<, "&lt;"},
{?>, "&gt;"},
{?&, "&amp;"},
{?", "&quot;"},
{?', "&#39;"}
]

Enum.each @escapes, fn { match, insert } ->
defp escape_char(unquote(match)), do: unquote(insert)
end

defp escape_char(char), do: char
end
44 changes: 44 additions & 0 deletions lib/excoveralls/html/view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule ExCoveralls.Html.View do
@moduledoc """
Conveniences for generating HTML.
"""
require EEx
require ExCoveralls.Html.Safe

alias ExCoveralls.Html.Safe

defmodule PathHelper do
def template_path(template) do
template |> Path.expand(get_template_path)
end

defp get_template_path() do
options = ExCoveralls.Settings.get_coverage_options
case Dict.fetch(options, "template_path") do
{:ok, path} -> path
_ -> Path.expand("excoveralls/lib/templates/html/htmlcov/", Mix.Project.deps_path())
end
end
end

@template "coverage.html.eex"

EEx.function_from_file(:def, :render, PathHelper.template_path(@template), [:assigns])

def partial(template, assigns \\ []) do
EEx.eval_file(PathHelper.template_path(template), assigns: assigns)
end

def safe(data) do
Safe.html_escape(data)
end

def coverage_class(percent) do
cond do
percent >= 75 -> "high"
percent >= 50 -> "medium"
percent >= 25 -> "low"
true -> "terrible"
end
end
end
15 changes: 11 additions & 4 deletions lib/excoveralls/local.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ defmodule ExCoveralls.Local do
Provides an entry point for the module.
"""
def execute(stats, options \\ []) do
IO.puts "----------------"
IO.puts print_string("~-6s ~-40s ~8s ~8s ~8s", ["COV", "FILE", "LINES", "RELEVANT", "MISSED"])
coverage(stats) |> IO.puts
IO.puts "----------------"
print_summary(stats)

if options[:detail] == true do
source(stats, options[:filter]) |> IO.puts
Expand All @@ -40,6 +37,16 @@ defmodule ExCoveralls.Local do
|> Enum.join("\n")
end

@doc """
Prints summary statistics for given coverage.
"""
def print_summary(stats) do
IO.puts "----------------"
IO.puts print_string("~-6s ~-40s ~8s ~8s ~8s", ["COV", "FILE", "LINES", "RELEVANT", "MISSED"])
coverage(stats) |> IO.puts
IO.puts "----------------"
end

defp format_source(stat) do
"\n\e[33m--------#{stat[:name]}--------\e[m\n" <> colorize(stat)
end
Expand Down
18 changes: 18 additions & 0 deletions lib/mix/tasks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ defmodule Mix.Tasks.Coveralls do
end
end

defmodule Html do
@moduledoc """
Provides an entry point for displaying coveralls information
with source code details as an HTML report.
"""
use Mix.Task

@shortdoc "Display the test coverage with source detail as an HTML report"

def run(args) do
{parsed, _, _} = OptionParser.parse(args, aliases: [f: :filter])

Mix.Tasks.Coveralls.do_run(args,
[ type: "html",
filter: parsed[:filter] || [] ])
end
end

defmodule Travis do
@moduledoc """
Provides an entry point for travis's script.
Expand Down
28 changes: 28 additions & 0 deletions lib/templates/html/htmlcov/_script.html.eex

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading