Skip to content

Commit

Permalink
cobertura task (#302)
Browse files Browse the repository at this point in the history
* cobertura task

* Update cobertura version field

* fix tests
  • Loading branch information
albertored authored Mar 3, 2023
1 parent f633a64 commit ca3e0d5
Show file tree
Hide file tree
Showing 8 changed files with 457 additions and 2 deletions.
6 changes: 6 additions & 0 deletions lib/excoveralls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule ExCoveralls do
Provides the entry point for coverage calculation and output.
This module method is called by Mix.Tasks.Test
"""
alias ExCoveralls.Cobertura
alias ExCoveralls.Stats
alias ExCoveralls.Cover
alias ExCoveralls.ConfServer
Expand Down Expand Up @@ -31,6 +32,7 @@ defmodule ExCoveralls do
@type_json "json"
@type_post "post"
@type_xml "xml"
@type_cobertura "cobertura"
@type_lcov "lcov"

@doc """
Expand Down Expand Up @@ -138,6 +140,10 @@ defmodule ExCoveralls do
def analyze(stats, @type_xml, options) do
Xml.execute(stats, options)
end

def analyze(stats, @type_cobertura, options) do
Cobertura.execute(stats, options)
end

def analyze(stats, @type_post, options) do
Post.execute(stats, options)
Expand Down
218 changes: 218 additions & 0 deletions lib/excoveralls/cobertura.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
defmodule ExCoveralls.Cobertura do
@moduledoc """
Generate XML Cobertura output for results.
"""

alias ExCoveralls.Settings
alias ExCoveralls.Stats

@file_name "cobertura.xml"

@doc """
Provides an entry point for the module.
"""
def execute(stats, options \\ []) do
stats
|> generate_xml(Enum.into(options, %{}))
|> write_file(options[:output_dir])

ExCoveralls.Local.print_summary(stats)

Stats.ensure_minimum_coverage(stats)
end

defp generate_xml(stats, _options) do
prolog = [
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<!DOCTYPE coverage SYSTEM \"http://cobertura.sourceforge.net/xml/coverage-04.dtd\">\n"
]

timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

# This is the version of the cobertura tool used to generate the XML
# We are not using the tool here but the version is mandatory in the DTD schema
# so we put the last released version (dated 2015)
# It is only a "placeholder" to make DTD happy
version = "2.1.1"

complexity = "0"
branch_rate = "0.0"
branches_covered = "0"
branches_valid = "0"

{valid, covered} =
Enum.reduce(stats, {0, 0}, fn %{coverage: lines}, {valid, covered} ->
valid_lines = Enum.reject(lines, &is_nil/1)
{valid + length(valid_lines), covered + Enum.count(valid_lines, &(&1 > 0))}
end)

line_rate = to_string(Float.floor(covered / valid, 3))
lines_covered = to_string(covered)
lines_valid = to_string(valid)

mix_project_config = Mix.Project.config()

c_paths =
Keyword.get(mix_project_config, :erlc_paths, []) ++
Keyword.get(mix_project_config, :elixirc_paths, [])

c_paths =
c_paths
|> Enum.filter(&File.exists?/1)
|> Enum.map(fn c_path ->
c_path = Path.absname(c_path)

if File.dir?(c_path) do
c_path
else
Path.dirname(c_path)
end
end)

sources = Enum.map(c_paths, &{:source, [to_charlist(&1)]})

packages = generate_packages(stats, c_paths)

root = {
:coverage,
[
timestamp: timestamp,
"line-rate": line_rate,
"lines-covered": lines_covered,
"lines-valid": lines_valid,
"branch-rate": branch_rate,
"branches-covered": branches_covered,
"branches-valid": branches_valid,
complexity: complexity,
version: version
],
[sources: sources, packages: packages]
}

:xmerl.export_simple([root], :xmerl_xml, [{:prolog, prolog}])
end

defp generate_packages(stats, c_paths) do
stats
|> Enum.reduce(%{}, fn %{name: path, source: source, coverage: lines}, acc ->
package_name = package_name(path, c_paths)
module = module_name(source)
x = %{module: module, path: path, lines: lines}
Map.update(acc, package_name, [x], &[x | &1])
end)
|> Enum.map(&generate_package(&1, c_paths))
end

defp generate_package({package_name, modules}, c_paths) do
classes = generate_classes(modules, c_paths)

line_rate =
modules |> Enum.flat_map(fn %{lines: lines} -> Enum.reject(lines, &is_nil/1) end) |> rate()

{
:package,
[
name: package_name,
"line-rate": to_string(line_rate),
"branch-rate": "0.0",
complexity: "0"
],
[classes: classes]
}
end

defp generate_classes(modules, c_paths) do
Enum.map(modules, fn %{module: module, path: path, lines: lines} ->
line_rate = lines |> Enum.reject(&is_nil/1) |> rate()

lines =
lines
|> Enum.with_index(1)
|> Enum.reject(fn {hits, _} -> is_nil(hits) end)
|> Enum.map(fn {hits, line} ->
{:line, [number: to_string(line), hits: to_string(hits), branch: "False"], []}
end)

{
:class,
[
name: module,
filename: relative_to(path, c_paths),
"line-rate": to_string(line_rate),
"branch-rate": "0.0",
complexity: "0"
],
[methods: [], lines: lines]
}
end)
end

defp relative_to(path, c_paths) do
abspath = Path.absname(path)

Enum.reduce_while(c_paths, path, fn c_path, path ->
case Path.relative_to(abspath, c_path) do
^abspath -> {:cont, path}
relative -> {:halt, relative}
end
end)
end

defp module_name(source) do
case Regex.run(~r/^defmodule\s+(.*)\s+do$/m, source, capture: :all_but_first) do
[module] ->
module

_ ->
[module] = Regex.run(~r/^-module\((.*)\)\.$/m, source, capture: :all_but_first)
module
end
end

defp package_name(path, c_paths) do
package_name = path |> Path.absname() |> Path.dirname()

c_paths
|> Enum.find_value(package_name, fn c_path ->
if String.starts_with?(package_name, c_path) do
String.slice(package_name, (String.length(c_path) + 1)..-1)
else
false
end
end)
|> Path.split()
|> Enum.join(".")
|> to_charlist()
end

defp rate(valid_lines) when length(valid_lines) == 0, do: 0.0

defp rate(valid_lines) do
Float.floor(Enum.count(valid_lines, &(&1 > 0)) / length(valid_lines), 3)
end

defp output_dir(output_dir) do
cond do
output_dir ->
output_dir

true ->
options = Settings.get_coverage_options()

case Map.fetch(options, "output_dir") do
{:ok, val} -> val
_ -> "cover/"
end
end
end

defp write_file(content, output_dir) do
file_path = output_dir(output_dir)

unless File.exists?(file_path) do
File.mkdir_p!(file_path)
end

File.write!(Path.expand(@file_name, file_path), content)
end
end
3 changes: 3 additions & 0 deletions lib/excoveralls/task/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Usage: mix coveralls.detail [--filter file-name-pattern]
Usage: mix coveralls.html
Used to display coverage information at the source-code level formatted as an HTML page.
Usage: mix coveralls.cobertura
Used to display coverage information at the source-code level formatted as an XML cobertura file.
Usage: mix coveralls.travis [--pro]
Used to post coverage from Travis CI server.
Expand Down
15 changes: 15 additions & 0 deletions lib/mix/tasks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@ defmodule Mix.Tasks.Coveralls do
Mix.Tasks.Coveralls.do_run(args, [ type: "xml" ])
end
end

defmodule Cobertura do
@moduledoc """
Provides an entry point for outputting coveralls information
as a Cobertura XML file.
"""
use Mix.Task

@shortdoc "Output the test coverage as a Cobertura XML file"
@preferred_cli_env :test

def run(args) do
Mix.Tasks.Coveralls.do_run(args, [ type: "cobertura" ])
end
end

defmodule Json do
@moduledoc """
Expand Down
8 changes: 6 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule ExCoveralls.Mixfile do
end

def application do
[extra_applications: [:eex, :tools]]
[extra_applications: [:eex, :tools, :xmerl]]
end

defp elixirc_paths(:test), do: ["lib", "test/fixtures/test_missing.ex"]
Expand All @@ -42,7 +42,11 @@ defmodule ExCoveralls.Mixfile do
{:hackney, "~> 1.16"},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:meck, "~> 0.8", only: :test},
{:mock, "~> 0.3.6", only: :test}
{:mock, "~> 0.3.6", only: :test},
{:sax_map, "~> 1.0", only: :test},
# saxy >= 1.0.0 uses defguard that has been introduced on elixir 1.6
# as soon as we support elixir 1.6+ we should drop this constraint on saxy
{:saxy, "< 1.0.0", only: :test, override: true}
]
end

Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"mock": {:hex, :mock, "0.3.6", "e810a91fabc7adf63ab5fdbec5d9d3b492413b8cda5131a2a8aa34b4185eb9b4", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "bcf1d0a6826fb5aee01bae3d74474669a3fa8b2df274d094af54a25266a1ebd2"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"sax_map": {:hex, :sax_map, "1.0.1", "51a9382d741504c34d49118fb36d691c303d042e1da88f8edae8ebe75fe74435", [:mix], [{:saxy, "~> 1.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "a7c57c25d23bfc3ce93cf93400dcfb447fe463d27ee8c6913545161e78dc487a"},
"saxy": {:hex, :saxy, "0.10.0", "38879f46a595862c22114792c71379355ecfcfa0f713b1cfcc59e1d4127f1f55", [:mix], [], "hexpm", "da130ed576e9f53d1a986ec5bd2fa72c1599501ede7d7a2dceb81acf53bf9790"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}
Loading

0 comments on commit ca3e0d5

Please sign in to comment.