Skip to content

Commit

Permalink
cobertura task
Browse files Browse the repository at this point in the history
  • Loading branch information
albertored committed Feb 28, 2023
1 parent 6890fdc commit fe0d7ab
Show file tree
Hide file tree
Showing 7 changed files with 440 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 @@ -137,6 +139,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
213 changes: 213 additions & 0 deletions lib/excoveralls/cobertura.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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)

version = "1.9.4.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
5 changes: 3 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,8 @@ 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}
]
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, "1.5.0", "0141127f2d042856f135fb2d94e0beecda7a2306f47546dbc6411fc5b07e28bf", [:mix], [], "hexpm", "ea7bb6328fbd1f2aceffa3ec6090bfb18c85aadf0f8e5030905e84235861cf89"},
"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 fe0d7ab

Please sign in to comment.