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

cobertura task #302

Merged
merged 3 commits into from
Mar 3, 2023
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
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