Skip to content

Strottie cpp cobertura #44

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Displays a coverage summary report in a pop-up window.

Currently supports:

- C/C++ (lcov)
- C/C++ (lcov, cobertura)
- C# (lcov - see wiki for details)
- Dart (lcov)
- Go (coverprofile)
Expand All @@ -27,7 +27,7 @@ Branch (partial) coverage support:

| Language | Supported |
| --------------------- | ---------------------- |
| C/C++ | :x: |
| C/C++ | :x: (lcov), :heavy_check_mark: (cobertura) |
| C# | :x: |
| Dart | :heavy_check_mark: (untested) |
| Go | :x: |
Expand All @@ -36,7 +36,7 @@ Branch (partial) coverage support:
| Python | :heavy_check_mark: |
| Ruby | :x: |
| Rust | :x: |
| PHP | :x: |
| PHP | :heavy_check_mark: (untested) |
| Lua | :x: |

*Note:* This plugin does not run tests. It justs loads/displays a coverage report generated by a test suite.
Expand Down
2 changes: 2 additions & 0 deletions lua/coverage/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ local defaults = {
lang = {
cpp = {
coverage_file = "report.info",
xml_coverage_file = "coverage/cobertura.xml",
path_mappings = {},
},
cs = {
coverage_file = "TestResults/lcov.info",
Expand Down
13 changes: 10 additions & 3 deletions lua/coverage/languages/cpp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ M.summary = common.summary
-- @param callback called with the results of the coverage report
M.load = function(callback)
local cpp_config = config.opts.lang.cpp

local p = Path:new(util.get_coverage_file(cpp_config.coverage_file))
if not p:exists() then
vim.notify("No coverage file exists.", vim.log.levels.INFO)
if p:exists() then
callback(util.lcov_to_table(p))
return
end

local p = Path:new(util.get_coverage_file(cpp_config.xml_coverage_file))
if p:exists() then
callback(util.cobertura_to_table(p, cpp_config.path_mappings or {}))
return
end

callback(util.lcov_to_table(p))
vim.notify("No coverage file exists.", vim.log.levels.INFO)
end

return M
254 changes: 161 additions & 93 deletions lua/coverage/parsers/corbertura.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,12 @@ local function read_path(path)
return assert(xmlreader.from_string(path:read()))
end

--- Position the reader on the next element `name` found before the closing element for `parent`
---
--- @param name string
--- @param parent string|nil
local function next_element_in(name, parent)
while nil ~= reader and reader:next_node() do
if "element" == reader:node_type() and name == reader:name() then
return true
end
if "end element" == reader:node_type() and parent == reader:name() then
return false
end
end

return false
end

local function enter_current_element()
reader:read()
end

--- Enter in the next element `name` found before the closing element for `parent`
---
--- @param name string
--- @param parent string|nil
local function enter_next_element_in(name, parent)
if false == next_element_in(name, parent) then
return false
end

if reader:is_empty_element() then
return false
end

enter_current_element()

return true
local function is_element(name)
return "element" == reader:node_type() and name == reader:name()
end

--- Enter in the next element `name`, will jump over any other element
---
--- @param name string
local function enter_next_element(name)
return enter_next_element_in(name, nil)
local function is_end_element(name)
return "end element" == reader:node_type() and name == reader:name()
end

local function apply_path_mappings(source)
Expand All @@ -69,72 +31,162 @@ local function apply_path_mappings(source)
return source
end

local function load_sources()
if enter_next_element_in("sources", "coverage") then
while enter_next_element_in("source", "sources") do
local source = apply_path_mappings(reader:value())
table.insert(sources, source)
local function resolve_filename_from_sources(filename)
if filename == "" then
return ""
end
for _, source in pairs(sources) do
local filepath = Path:new({source, filename})
if filepath:exists() then
return filepath.filename
end
end
end

local function create_coverage_for_current_package()
local coverage = util.new_file_meta()
coverage.summary.percent_covered = tonumber(reader:get_attribute("line-rate")) * 100

return coverage
return filename
end

local function update_coverage_with_current_line(coverage)
local number = tonumber(reader:get_attribute("number"), 10)
local hits = tonumber(reader:get_attribute("hits"), 10)
if coverage == nil then
return
end
local linenr = tonumber(reader:get_attribute("number"), 10)
local count = tonumber(reader:get_attribute("hits"), 10)
local is_branch = reader:get_attribute("branch") == 'true'

-- no data for coverage.exclude_lines
-- no data for coverage.summary.exclude_lines

if 0 == hits then
table.insert(coverage.missing_lines, number)
if count == 0 then
table.insert(coverage.missing_lines, linenr)
coverage.summary.missing_lines = coverage.summary.missing_lines + 1
else
table.insert(coverage.executed_lines, number)
table.insert(coverage.executed_lines, linenr)
coverage.summary.covered_lines = coverage.summary.covered_lines + 1
end

coverage.summary.num_statements = coverage.summary.num_statements + 1
end

local function resolve_filename_from_sources(filename)
for _, source in pairs(sources) do
local filepath = Path:new({source, filename})
if filepath:exists() then
return filepath.filename
if is_branch then
rc, cond_info = pcall(reader.get_attribute, reader, "condition-coverage")
if rc then
-- Example: "87% (7/8)"
local br_percent, br_hits, br_total = cond_info:match("([0-9.]+)%% [(](%d+)/(%d+)[)]")
if br_percent ~= nil then
br_percent = tonumber(br_percent, 10)
br_hits = tonumber(br_hits, 10)
br_total = tonumber(br_total, 10)
if br_hits < br_total then
table.insert(coverage.missing_branches, {linenr, linenr}) -- { from, to }
end
coverage.summary.num_branches = coverage.summary.num_branches + br_total
coverage.summary.num_partial_branches = coverage.summary.num_partial_branches + (br_total - br_hits)
end
end
end

return filename
end

local function generate_coverages()
local coverages = {}
while next_element_in("package", "packages") do
local coverage = create_coverage_for_current_package()
local filename = resolve_filename_from_sources(reader:get_attribute("name"))

enter_current_element()
if enter_next_element_in("classes", "package") then
while enter_next_element_in("class", "classes") do
while enter_next_element_in("lines", "class") do
while next_element_in("line", "lines") do
update_coverage_with_current_line(coverage)
local function process_coverage_packages_element(files)
while not is_end_element("packages") do
if is_element("package") then
-- If a package has files (with filename), then report at the file level
local package_has_files = false
-- If a package has pure classes (with no filename), then report at the package level
local package_has_classes = false

local packages_coverage, packages_filename = nil, ""
local rc, package_name_attr = pcall(reader.get_attribute, reader, "name")
if rc then
package_filename = resolve_filename_from_sources(package_name_attr)
if package_filename ~= "" then
package_coverage = files[package_filename]
if package_coverage == nil then
package_coverage = util.new_file_meta()
end
end
end
end
if package_coverage ~= nil then
package_coverage.summary.percent_covered = tonumber(reader:get_attribute("line-rate")) * 100
end

reader:read()
while not is_end_element("package") do
if is_element("classes") then

reader:read()
while not is_end_element("classes") do
if is_element("class") then

local class_coverage, class_filename = nil, ""
local rc, class_filename_attr = pcall(reader.get_attribute, reader, "filename")
if rc then
class_filename = resolve_filename_from_sources(class_filename_attr)
if class_filename == package_filename then
class_filename = ""
end
if class_filename ~= "" then
package_has_files = true
class_coverage = files[class_filename]
if class_coverage == nil then
class_coverage = util.new_file_meta()
end
end
end
if class_coverage ~= nil then
class_coverage.summary.percent_covered = tonumber(reader:get_attribute("line-rate")) * 100
else
-- At least one class does not report its own coverage at the file level
package_has_classes = true
end

reader:read()
while not is_end_element("class") do
if is_element("lines") then

reader:read()
while not is_end_element("lines") do
if is_element("line") then
update_coverage_with_current_line(package_coverage)
update_coverage_with_current_line(class_coverage)
end
if not reader:next_node() then break end
end

end
if not reader:next_node() then break end
end

if class_coverage ~= nil and class_coverage.summary.num_statements > 0 then
files[class_filename] = class_coverage
end

end
if not reader:next_node() then break end
end

end
if not reader:next_node() then break end
end

if
(package_coverage ~= nil and package_coverage.summary.num_statements > 0) and
(package_has_classes or not package_has_files)
then
files[package_filename] = package_coverage
end

local is_not_interface = 0 < coverage.summary.num_statements
if is_not_interface then
coverages[filename] = coverage
end
if not reader:next_node() then break end
end
end

return coverages
local function process_coverage_sources_element()
while not is_end_element("sources") do
if is_element("source") then
reader:read()
local source = apply_path_mappings(reader:value())
table.insert(sources, source)
end
if not reader:next_node() then break end
end
end

--- Parses a cobertura report from path into files.
Expand All @@ -146,22 +198,38 @@ return function (path, files, a_path_mappings)
reader = read_path(path)
path_mappings = a_path_mappings
sources = {}
local found_coverage = false
local found_packages = false

while true do
if is_element("coverage") then
found_coverage = true

reader:read()
while not is_end_element("coverage") do
if is_element("sources") then
reader:read()
process_coverage_sources_element()
elseif is_element("packages") then
found_packages = true
reader:read()
process_coverage_packages_element(files)
end
if not reader:next_node() then break end
end

end
if not reader:next_node() then break end
end

if false == enter_next_element("coverage") then
if not found_coverage then
notify_element_missing("coverage")
return
end

load_sources()

if false == enter_next_element_in("packages", "coverage") then
if not found_packages then
notify_element_missing("packages")
return
end

for filename, coverage in pairs(generate_coverages()) do
files[filename] = coverage
end

reader:close()
end
2 changes: 2 additions & 0 deletions lua/coverage/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ M.new_file_meta = function()
excluded_lines = 0,
missing_lines = 0,
num_statements = 0,
num_branches = 0,
num_partial_branches = 0,
percent_covered = 0,
},
missing_lines = {},
Expand Down
Loading